# agent_chat_html.py - Chat UI HTML template and theme. Used by aiagent.

THEME = {
    # Colors
    "user_bubble_bg": "#207CE4",
    "agent_bubble_bg": "#EBECEE",
    "user_text": "#FFFFFF",
    "agent_text": "#333333",
    "tool_bubble_bg": "#EFEFEF",
    "tool_text": "#222222",
    "error_bubble_bg": "#FDECEC",
    "error_text": "#7A1C1C",
    "timestamp_color": "#282c35",
    "timestamp_color_dark": "#8e8e93",
    
    # Bubble appearance
    "bubble_radius": 16,
    "bubble_padding": 12,
    "bubble_spacing": 10,
    "max_bubble_width_frac": 0.80,
    "chat_font_px": 15,
    # Input bar (pill / floating bar)
    "input_bar_bg_light": "#F8F9FA",
    "input_bar_bg_dark": "#2C2C2E",
    "input_bar_text_light": "#000000",
    "input_bar_text_dark": "#E5E5EA",
    "input_bar_border_light": "#E5E7EB",
    "input_bar_border_dark": "#3A3A3C",
    "input_bar_height": 52,
    "input_bar_radius": 26,
    "input_bar_font_size": 15,
}


CHAT_HTML_TEMPLATE = """<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/dompurify@3/dist/purify.min.js"></script>
    <style>
        * {{
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }}
        html {{
            height: 100%;
            background: #b1b1b1;
            overflow: hidden;
        }}
        body {{
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
            font-size: {font_size}px;
            background: #b1b1b1;
            padding: 16px;
            padding-bottom: 80px;
            line-height: 1.5;
            margin: 0;
            height: 100%;
            overflow: hidden;
            display: flex;
            flex-direction: column;
        }}
        #messages {{
            flex: 1;
            min-height: 0;
            overflow-y: auto;
            scrollbar-gutter: stable;
        }}
        .message {{
            margin-bottom: {spacing}px;
            display: flex;
            flex-direction: row;
        }}
        .message.user {{
            justify-content: flex-end;
            margin-right: 16px;
        }}
        .message.agent {{
            justify-content: flex-start;
            flex-direction: column;
            align-items: flex-start;
        }}
        .message.agent .bubble {{
            flex-shrink: 0;
            min-width: min(320px, 85%);
        }}
        .message.fade-in {{
            opacity: 0;
            transform: translateY(12px);
            animation: messageFadeIn 0.4s ease-out forwards;
        }}
        @keyframes messageFadeIn {{
            to {{ opacity: 1; transform: translateY(0); }}
        }}
        .bubble {{
            max-width: 80%;
            padding: {padding}px;
            border-radius: {radius}px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.15);
            word-wrap: break-word;
            overflow-wrap: break-word;
            column-count: 1;
            column-width: auto;
            display: block;
        }}
        .bubble > * {{
            display: block !important;
            max-width: 100%;
            float: none !important;
        }}
        .bubble.user {{
            background: {user_bg};
            color: {user_text};
        }}
        .bubble.agent {{
            background: {agent_bg};
            color: {agent_text};
        }}
        .bubble.tool {{
            background: {tool_bg};
            color: {tool_text};
        }}
        .bubble.error {{
            background: {error_bg};
            color: {error_text};
        }}
        .bubble.thinking {{
            background: #f1f5f9;
            color: #475569;
            font-style: italic;
        }}
        .message-actions {{
            margin-top: 8px;
            padding-left: 2px;
        }}
        .message-actions .regenerate-link {{
            display: inline-block;
            font-size: 16px;
            line-height: 1;
            color: #6b7280;
            text-decoration: none;
        }}
        .message-actions .regenerate-link:hover {{
            color: #374151;
        }}
        .message-actions .feedback-link {{
            display: inline-block;
            font-size: 16px;
            line-height: 1;
            color: #6b7280;
            text-decoration: none;
            margin-right: 6px;
        }}
        .message-actions .feedback-link:hover {{
            color: #374151;
        }}
        .message-actions .feedback-recorded {{
            font-size: 13px;
            color: #6b7280;
        }}
        .message-context-menu {{
            position: fixed;
            z-index: 10000;
            background: #fff;
            border: 1px solid rgba(0,0,0,0.15);
            border-radius: 6px;
            box-shadow: 0 4px 12px rgba(0,0,0,0.15);
            padding: 4px 0;
            min-width: 180px;
        }}
        .message-context-menu button {{
            display: block;
            width: 100%;
            text-align: left;
            padding: 6px 14px;
            border: none;
            background: transparent;
            font-size: 13px;
            color: #1f2937;
            cursor: pointer;
        }}
        .message-context-menu button:hover {{
            background: rgba(0,0,0,0.06);
        }}
        .bubble p {{
            margin: 4px 0;
            white-space: normal;
            overflow-wrap: break-word;
            word-break: break-word;
        }}
        .bubble pre {{
            background: rgba(0,0,0,0.1);
            padding: 8px;
            border-radius: 4px;
            overflow-x: auto;
            white-space: pre-wrap;
            word-break: break-word;
        }}
        .bubble code {{
            background: rgba(0,0,0,0.1);
            padding: 2px 4px;
            border-radius: 3px;
            font-family: 'Monaco', 'Courier New', monospace;
        }}
        .bubble pre code {{
            background: transparent;
            padding: 0;
        }}
        .bubble ul, .bubble ol {{
            margin: 8px 0;
            padding-left: 24px;
        }}
        .bubble li {{
            margin: 4px 0;
        }}
        .bubble h1, .bubble h2, .bubble h3, .bubble h4, .bubble h5, .bubble h6 {{
            margin: 12px 0 8px 0;
            font-weight: 600;
        }}
        .bubble blockquote {{
            border-left: 3px solid rgba(0,0,0,0.2);
            padding-left: 12px;
            margin: 8px 0;
            font-style: italic;
        }}
        .bubble a {{
            color: inherit;
            text-decoration: underline;
        }}
        .bubble img.bubble-attachment {{
            display: block;
            max-width: 100%;
            height: auto;
            border-radius: 8px;
            margin: 6px 0;
        }}
        .timestamp {{
            font-size: 11px;
            color: {timestamp_color};
            margin-top: 8px;
        }}
        .message.user .timestamp {{
            text-align: right;
            color: {user_text};
        }}
        #thinking-indicator .thinking {{
            display: flex;
            gap: 6px;
            padding: 8px 12px;
            background: {agent_bg};
            border-radius: {radius}px;
            max-width: 60px;
            justify-content: center;
            margin-top: {spacing}px;
        }}
        .thinking-dot {{
            width: 8px;
            height: 8px;
            border-radius: 50%;
            background: #666;
            animation: pulse 1.4s ease-in-out infinite;
        }}
        .thinking-dot:nth-child(1) {{ animation-delay: 0s; }}
        .thinking-dot:nth-child(2) {{ animation-delay: 0.2s; }}
        .thinking-dot:nth-child(3) {{ animation-delay: 0.4s; }}
        @keyframes pulse {{
            0%, 100% {{ opacity: 0.3; }}
            50% {{ opacity: 1; }}
        }}
        /* Input bar at bottom of document (native input used in PyObjC app) */
        .input-bar {{
            flex-shrink: 0;
            padding: 12px 16px 16px;
            background: linear-gradient(to top, #e8e8ed 0%%, #e5e5ea 100%);
            transition: background 0.25s ease;
        }}
        body.research-mode .input-bar {{
            background: linear-gradient(to top, #e0e5ef 0%%, #dde2ec 100%);
        }}
        .input-bar-inner {{
            position: relative;
            overflow: hidden;
            pointer-events: auto;
            width: 100%%;
            max-width: 100%%;
            box-sizing: border-box;
            border-radius: 26px;
            background: #e8e8ed;
            border: 1px solid #d1d1d6;
            box-shadow: 0 4px 12px rgba(0,0,0,0.08), 0 2px 4px rgba(0,0,0,0.04);
            padding: 10px 16px 10px 18px;
            min-height: 52px;
            display: flex;
            align-items: flex-end;
            transition: transform 0.2s ease, box-shadow 0.2s ease, background 0.25s ease, border-color 0.25s ease;
        }}
        body.research-mode .input-bar-inner {{
            background: #e0e5ef;
            border-color: #c8ceda;
        }}
        .input-bar-inner:focus-within {{
            transform: scale(1.03);
            transform-origin: center;
            box-shadow: 0 8px 24px rgba(0,0,0,0.12), 0 4px 8px rgba(0,0,0,0.06);
        }}
        .input-bar-shine {{
            position: absolute;
            top: 0;
            height: 30%;
            width: 40%;
            left: 0;
            pointer-events: none;
            z-index: 1;
            background: linear-gradient(90deg, transparent 0%%, rgba(255,255,255,0.85) 50%%, transparent 100%);
            opacity: 0;
        }}
        .input-bar-row {{
            flex: 1;
            display: flex;
            flex-direction: column;
            min-width: 0;
        }}
        .input-bar-attachments {{
            width: 100%;
            display: flex;
            flex-wrap: wrap;
            gap: 8px;
            align-items: center;
            min-height: 0;
            margin-bottom: 6px;
        }}
        .input-bar-attachments:empty {{
            display: none;
        }}
        .attachment-chip {{
            display: inline-flex;
            align-items: center;
            gap: 6px;
            max-width: 180px;
            padding: 4px 8px;
            border-radius: 12px;
            background: rgba(0,0,0,0.06);
            font-size: 12px;
            color: #333;
        }}
        .attachment-chip img {{
            width: 28px;
            height: 28px;
            object-fit: cover;
            border-radius: 6px;
            flex-shrink: 0;
        }}
        .attachment-chip .chip-icon {{
            width: 28px;
            height: 28px;
            border-radius: 6px;
            background: rgba(0,0,0,0.08);
            display: flex;
            align-items: center;
            justify-content: center;
            flex-shrink: 0;
            font-size: 14px;
        }}
        .attachment-chip .chip-name {{
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }}
        .attachment-chip .chip-remove {{
            margin-left: 2px;
            padding: 0 2px;
            background: none;
            border: none;
            cursor: pointer;
            color: #8E8E93;
            font-size: 16px;
            line-height: 1;
        }}
        .attachment-chip .chip-remove:hover {{
            color: #c00;
        }}
        .input-bar-inner textarea {{
            flex: 1 1 auto;
            width: 100%;
            min-width: 0;
            border: none;
            background: transparent;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
            font-size: 15px;
            line-height: 1.4;
            color: #000;
            outline: none;
            min-height: 24px;
            max-height: 200px;
            resize: none;
            padding: 4px 0;
            overflow-y: auto;
            box-sizing: border-box;
            caret-color: #007AFF;
        }}
        .input-bar-inner textarea::placeholder {{
            color: #8E8E93;
        }}
        .input-bar-placeholder-wrap {{
            position: relative;
            flex: 1 1 auto;
            min-width: 0;
        }}
        .working-placeholder {{
            position: absolute;
            left: 0;
            top: 6px;
            right: 0;
            z-index: 1;
            padding: 4px 0;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
            font-size: 15px;
            line-height: 1.4;
            pointer-events: none;
            visibility: hidden;
            color: #8E8E93;
        }}
        .working-placeholder.visible {{
            visibility: visible;
        }}
        .working-placeholder.visible .pl-char {{
            -webkit-animation: working-pulse 1.5s ease-in-out infinite;
            animation: working-pulse 1.5s ease-in-out infinite;
        }}
        .working-placeholder .pl-char:nth-child(1) {{ animation-delay: 0s; -webkit-animation-delay: 0s; }}
        .working-placeholder .pl-char:nth-child(2) {{ animation-delay: 0.08s; -webkit-animation-delay: 0.08s; }}
        .working-placeholder .pl-char:nth-child(3) {{ animation-delay: 0.16s; -webkit-animation-delay: 0.16s; }}
        .working-placeholder .pl-char:nth-child(4) {{ animation-delay: 0.24s; -webkit-animation-delay: 0.24s; }}
        .working-placeholder .pl-char:nth-child(5) {{ animation-delay: 0.32s; -webkit-animation-delay: 0.32s; }}
        .working-placeholder .pl-char:nth-child(6) {{ animation-delay: 0.4s; -webkit-animation-delay: 0.4s; }}
        .working-placeholder .pl-char:nth-child(7) {{ animation-delay: 0.48s; -webkit-animation-delay: 0.48s; }}
        .working-placeholder .pl-char:nth-child(8) {{ animation-delay: 0.56s; -webkit-animation-delay: 0.56s; }}
        @-webkit-keyframes working-pulse {{
            from, to {{ opacity: 0.7; }}
            50% {{ opacity: 1; }}
        }}
        @keyframes working-pulse {{
            from, to {{ opacity: 0.7; }}
            50% {{ opacity: 1; }}
        }}
        .input-bar-inner .bar-btn {{
            background: none;
            border: none;
            cursor: pointer;
            padding: 6px;
            margin: 0 2px;
            font-size: 18px;
            line-height: 1;
            color: #8E8E93;
            display: flex;
            align-items: center;
            justify-content: center;
        }}
        .input-bar-inner .bar-btn:hover {{
            color: #007AFF;
        }}
        .input-bar-inner .bar-btn.send-stop {{
            font-size: 20px;
        }}
        .input-bar-inner .bar-btn.voice-btn {{
            font-size: 18px;
        }}
        .input-bar-inner .bar-btn.voice-btn.listening {{
            color: #FF3B30;
        }}
        .input-bar-inner .bar-btn.research-toggle {{
            font-size: 16px;
            border-radius: 8px;
            transition: color 0.2s ease, background 0.2s ease;
        }}
        .input-bar-inner .bar-btn.research-toggle.active {{
            color: #007AFF;
            background: rgba(0, 122, 255, 0.12);
        }}
        .input-bar-spinner {{
            width: 24px;
            height: 24px;
            margin: 0 4px;
            flex-shrink: 0;
            display: none;
            align-items: center;
            justify-content: center;
        }}
        .input-bar-spinner.visible {{
            display: flex;
        }}
        .input-bar-spinner::before {{
            content: '';
            width: 18px;
            height: 18px;
            border: 2px solid #E0E0E0;
            border-top-color: #007AFF;
            border-radius: 50%;
            animation: input-spin 0.7s linear infinite;
        }}
        @keyframes input-spin {{
            to {{ transform: rotate(360deg); }}
        }}
        .input-bar-inner .bar-btn svg {{
            width: 20px;
            height: 20px;
            fill: currentColor;
        }}
        .input-bar-inner {{
            flex-wrap: wrap;
        }}
        /* Respect macOS Light/Dark mode for chat window background */
        @media (prefers-color-scheme: light) {{
            html {{ background: #c5c5c5; }}
            body {{ background: #c5c5c5; }}
            .timestamp {{ color: {timestamp_color}; }}
            .input-bar {{ background: linear-gradient(to top, #e8e8ed 0%%, #e5e5ea 100%); }}
            .input-bar-inner {{ background: #e8e8ed; border-color: #d1d1d6; }}
        }}
        @media (prefers-color-scheme: dark) {{
            html {{ background: #292929; }}
            body {{ background: #292929; }}
            .timestamp {{ color: {timestamp_color_dark}; }}
            .bubble.agent {{ background: #353537; color: #e5e5e7; }}
            .bubble.tool {{ background: #353537; color: #e5e5e7; }}
            .bubble.error {{ background: #3d2a2a; color: #f5c6c6; }}
            .bubble.thinking {{ background: rgba(255,255,255,0.12); color: #94a3b8; font-style: italic; }}
            .bubble pre {{ background: rgba(255,255,255,0.08); }}
            .bubble code {{ background: rgba(255,255,255,0.08); }}
            .bubble blockquote {{ border-left-color: rgba(255,255,255,0.25); }}
            #thinking-indicator .thinking {{ background: #353537; }}
            .thinking-dot {{ background: #8e8e93; }}
            .input-bar {{ background: linear-gradient(to top, #242426 0%%, transparent 100%); }}
            body.research-mode .input-bar {{ background: linear-gradient(to top, #252836 0%%, transparent 100%); }}
            .input-bar-inner {{ background: #353537; border-color: #404042; box-shadow: 0 4px 12px rgba(0,0,0,0.3); }}
            body.research-mode .input-bar-inner {{ background: #363846; border-color: #454558; }}
            .input-bar-inner:focus-within {{ box-shadow: 0 8px 24px rgba(0,0,0,0.45), 0 4px 8px rgba(0,0,0,0.2); }}
            .input-bar-shine {{ background: linear-gradient(90deg, transparent 0%%, rgba(255,255,255,0.95) 50%%, transparent 100%); }}
            .attachment-chip {{ background: rgba(255,255,255,0.12); color: #E5E5EA; }}
            .attachment-chip .chip-icon {{ background: rgba(255,255,255,0.15); }}
            .input-bar-inner textarea {{ color: #E5E5EA; caret-color: #0A84FF; }}
            .input-bar-inner textarea::placeholder {{ color: #8E8E93; }}
            .input-bar-inner .bar-btn {{ color: #8E8E93; }}
            .input-bar-inner .bar-btn:hover {{ color: #0A84FF; }}
            .input-bar-spinner::before {{ border-color: #3A3A3C; border-top-color: #0A84FF; }}
            .input-bar-inner .bar-btn.research-toggle.active {{ color: #0A84FF; background: rgba(10, 132, 255, 0.18); }}
            .message-context-menu {{ background: #2c2c2e; border-color: #3a3a3c; }}
            .message-context-menu button {{ color: #e5e5e7; }}
            .message-context-menu button:hover {{ background: rgba(255,255,255,0.1); }}
            #messages::-webkit-scrollbar {{ width: 10px; }}
            #messages::-webkit-scrollbar-track {{ background: #1c1c1e; }}
            #messages::-webkit-scrollbar-thumb {{ background: #48484a; border-radius: 5px; }}
            #messages::-webkit-scrollbar-thumb:hover {{ background: #636366; }}
            #post-onboarding-tip-banner {{
                background: rgba(32, 124, 228, 0.22);
                color: #E5E5EA;
                box-shadow: 0 1px 4px rgba(0,0,0,0.2);
            }}
            #post-onboarding-tip-banner .tip-dismiss {{
                background: #0A84FF;
            }}
            #post-onboarding-tip-banner .tip-dismiss:hover {{
                background: #409CFF;
            }}
        }}
        /* Tip banner when app theme is dark (set via body.dark from /theme) */
        body.dark #post-onboarding-tip-banner {{
            background: rgba(32, 124, 228, 0.22);
            color: #E5E5EA;
            box-shadow: 0 1px 4px rgba(0,0,0,0.2);
        }}
        body.dark #post-onboarding-tip-banner .tip-dismiss {{
            background: #0A84FF;
        }}
        body.dark #post-onboarding-tip-banner .tip-dismiss:hover {{
            background: #409CFF;
        }}
        /* Post-onboarding tip banner (dismissible) */
        #post-onboarding-tip-banner {{
            display: none;
            flex-shrink: 0;
            flex-direction: row;
            align-items: center;
            justify-content: space-between;
            gap: 12px;
            padding: 10px 14px;
            margin-bottom: 28px;
            border-radius: 12px;
            background: #E8F0FE;
            color: #1a1a2e;
            font-size: 14px;
            line-height: 1.4;
            box-shadow: 0 1px 4px rgba(0,0,0,0.08);
        }}
        #post-onboarding-tip-banner.visible {{
            display: flex;
        }}
        #post-onboarding-tip-banner .tip-text {{
            flex: 1;
            min-width: 0;
        }}
        #post-onboarding-tip-banner .tip-dismiss {{
            flex-shrink: 0;
            padding: 6px 14px;
            border: none;
            border-radius: 8px;
            background: #207CE4;
            color: #fff;
            font-size: 13px;
            font-weight: 500;
            cursor: pointer;
        }}
        #post-onboarding-tip-banner .tip-dismiss:hover {{
            background: #1a6ac9;
        }}
    </style>
</head>
<body>
    <div id="messages"></div>
    <div id="post-onboarding-tip-banner" class="" aria-live="polite">
        <span class="tip-text" id="post-onboarding-tip-text"></span>
        <button type="button" class="tip-dismiss" id="post-onboarding-tip-dismiss">Got it</button>
    </div>
    <div class="input-bar">
        <div class="input-bar-inner">
            <div class="input-bar-shine" aria-hidden="true"></div>
            <div id="chat-spinner" class="input-bar-spinner" aria-hidden="true"></div>
            <div class="input-bar-row">
                <div id="chat-attachments" class="input-bar-attachments"></div>
                <div class="input-bar-placeholder-wrap">
                    <textarea id="chat-input" placeholder="Type your message... (Enter to send, Shift+Enter for new line)" autocomplete="off" rows="1"></textarea>
                    <span id="working-placeholder" class="working-placeholder" aria-hidden="true"><span class="pl-char">W</span><span class="pl-char">o</span><span class="pl-char">r</span><span class="pl-char">k</span><span class="pl-char">i</span><span class="pl-char">n</span><span class="pl-char">g</span><span class="pl-char">…</span></span>
                </div>
            </div>
            <input type="file" id="chat-file-input" multiple accept="*/*" style="position:absolute;width:0;height:0;opacity:0;pointer-events:none;">
            <button type="button" class="bar-btn attach-btn" id="chat-attach" title="Attach file" aria-label="Attach file"><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M16.5 6v11.5c0 2.2-1.8 4-4 4s-4-1.8-4-4V5c0-1.4 1.1-2.5 2.5-2.5s2.5 1.1 2.5 2.5v10.5c0 .6-.4 1-1 1s-1-.4-1-1V6H10v9.5c0 1.4 1.1 2.5 2.5 2.5s2.5-1.1 2.5-2.5V5c0-2.2-1.8-4-4-4S7 2.8 7 5v12.5C7 19.5 9.5 22 12.5 22s5.5-2.5 5.5-5.5V6h-1.5z"/></svg></button>
            <button type="button" class="bar-btn voice-btn" id="chat-voice" title="Voice input (click to start/stop, hold Option to send)" aria-label="Voice input"><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm5.91-3c-.49 0-.9.36-.98.85C16.52 14.2 14.47 16 12 16s-4.52-1.8-4.93-4.15c-.08-.49-.49-.85-.98-.85-.61 0-1.09.54-1 1.14.49 3 2.89 5.35 5.91 5.78V20c0 .55.45 1 1 1s1-.45 1-1v-2.08c3.02-.43 5.42-2.78 5.91-5.78.1-.6-.39-1.14-1-1.14z"/></svg></button>
            <button type="button" class="bar-btn research-toggle" id="chat-research-toggle" title="Research mode (deep search)" aria-label="Toggle research mode"><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M15.5 14h-.79l-.28-.27A6.471 6.471 0 0016 9.5 6.5 6.5 0 109.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg></button>
            <button type="button" class="bar-btn send-stop" id="chat-send-stop" title="Send" aria-label="Send">&#10148;</button>
        </div>
    </div>
    <script>
        // Apply app theme (light/dark/system) so tip banner and UI match user preference
        (function() {{
            fetch('/theme').then(function(r) {{ return r.json(); }}).then(function(data) {{
                var theme = (data && data.theme) || 'system';
                if (theme === 'dark') document.body.classList.add('dark');
                else if (theme === 'light') document.body.classList.add('light');
            }}).catch(function() {{}});
        }})();
        // Post-onboarding discoverability: show one rotating tip until all dismissed
        (function() {{
            var banner = document.getElementById('post-onboarding-tip-banner');
            var textEl = document.getElementById('post-onboarding-tip-text');
            var dismissBtn = document.getElementById('post-onboarding-tip-dismiss');
            if (!banner || !textEl || !dismissBtn) return;
            function hideTip() {{
                banner.classList.remove('visible');
            }}
            function showTip(text) {{
                textEl.textContent = text || '';
                banner.classList.add('visible');
            }}
            dismissBtn.addEventListener('click', function() {{
                var index = parseInt(banner.getAttribute('data-tip-index'), 10);
                if (isNaN(index)) index = 0;
                fetch('/post_onboarding_tip_dismiss', {{
                    method: 'POST',
                    headers: {{ 'Content-Type': 'application/json' }},
                    body: JSON.stringify({{ index: index }})
                }}).catch(function() {{}}).then(hideTip);
            }});
            fetch('/post_onboarding_tip').then(function(r) {{ return r.json(); }}).then(function(data) {{
                if (data && data.show && data.text !== undefined) {{
                    banner.setAttribute('data-tip-index', data.index);
                    showTip(data.text);
                }}
            }}).catch(function() {{}});
        }})();
        function sanitizeHtml(html) {{
            if (typeof DOMPurify !== 'undefined') {{
                return DOMPurify.sanitize(html, {{
                    ALLOWED_TAGS: ['p','br','strong','b','em','i','u','s','del','a','code','pre','ul','ol','li','h1','h2','h3','h4','h5','h6','blockquote','table','thead','tbody','tr','th','td','hr','div','span','img','sup','sub','details','summary'],
                    ALLOWED_ATTR: ['href','src','alt','class','title','target','rel','width','height'],
                    ALLOW_DATA_ATTR: false,
                    FORBID_ATTR: ['style','onerror','onload','onclick','onmouseover'],
                }});
            }}
            return html;
        }}
        function wrapPathLinks(html) {{
            if (!html) return html;
            var esc = function(s) {{ return (s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }};
            return html
                .replace(/([\\s>])(\\/Users\\/[^\\s<>"']+)([\\s<,]|$)/g, function(_, pre, path, post) {{ return pre + '<a href="/open_in_finder?path=' + encodeURIComponent(path) + '">' + esc(path) + '</a>' + post; }})
                .replace(/([\\s>])(~\\/[^\\s<>"']+)([\\s<,]|$)/g, function(_, pre, path, post) {{ return pre + '<a href="/open_in_finder?path=' + encodeURIComponent(path) + '">' + esc(path) + '</a>' + post; }});
        }}
        function normalizeMarkdownBold(text) {{
            if (!text || typeof text !== 'string') return text;
            // 1) Trim spaces between **/__ and content so bold is recognized (e.g. "** mlx-lm **" -> "**mlx-lm**")
            // Only collapse when adjacent to a word char — preserve structural spaces (e.g. "### **" and "- **")
            text = text.replace(/\\*\\* +(?=\\w)/g, '**').replace(/(?<=\\w) +\\*\\*/g, '**');
            text = text.replace(/__ +(?=\\w)/g, '__').replace(/(?<=\\w) +__/g, '__');
            // 1b) Numbered list + bold: "1.**Install**" is invalid (no space before **); ensure "1. **" so marked parses bold
            text = text.replace(/(\\d)\\.(\\*\\*)/g, '$1. $2');
            text = text.replace(/(\\d)\\.(__)/g, '$1. $2');
            // 1c) Heading + bold: "###**Title**" -> "### **Title**"
            text = text.replace(/(^|\\n)(#{{1,6}})(\\*\\*|__)/gm, '$1$2 $3');
            // 1d) List marker + bold: "-**Item**" -> "- **Item**"
            text = text.replace(/(^|\\n)([-*+])(\\*\\*|__)/gm, '$1$2 $3');
            // 2) Ensure space around bold so words do not concatenate (e.g. "with**x**and" -> "with **x** and")
            // [^*\\n] prevents matching across line boundaries (cross-group bold pairing)
            text = text.replace(/(\\w)\\*\\*([^*\\n]+?)\\*\\*/g, '$1 **$2**');
            text = text.replace(/\\*\\*([^*\\n]+?)\\*\\*(\\w)/g, '**$1** $2');
            text = text.replace(/(\\w)__([^_\\n]+?)__/g, '$1 __$2__');
            text = text.replace(/__([^_\\n]+?)__(\\w)/g, '__$1__ $2');
            return text;
        }}
        function scrollToBottom() {{
            var m = document.getElementById('messages');
            if (m) m.scrollTop = m.scrollHeight;
        }}
        function addMessage(text, isUser, bubbleType, timestamp, attachmentImageUrls) {{
            const messagesDiv = document.getElementById('messages');
            const messageDiv = document.createElement('div');
            messageDiv.className = 'message ' + (isUser ? 'user' : 'agent');
            
            const bubble = document.createElement('div');
            bubble.className = 'bubble ' + (bubbleType || (isUser ? 'user' : 'agent'));
            
            if (isUser && attachmentImageUrls && Array.isArray(attachmentImageUrls) && attachmentImageUrls.length > 0) {{
                var imagesToLoad = attachmentImageUrls.length;
                attachmentImageUrls.forEach(function(url) {{
                    const img = document.createElement('img');
                    img.src = url;
                    img.alt = '';
                    img.className = 'bubble-attachment';
                    img.onload = img.onerror = function() {{
                        scrollToBottom();
                    }};
                    bubble.appendChild(img);
                }});
            }}
            
            // Use marked.js for proper markdown rendering
            let html = '';
            if (isUser) {{
                // For user messages, just escape HTML and preserve line breaks
                html = text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\\n/g, '<br>');
            }} else {{
                // For agent messages, use marked.js for full markdown support
                if (typeof marked !== 'undefined') {{
                    html = sanitizeHtml(marked.parse(normalizeMarkdownBold(text)));
                }} else {{
                    // Fallback if marked.js hasn't loaded
                    html = text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\\n/g, '<br>');
                }}
                html = wrapPathLinks(html);
            }}
            
            const textNode = document.createElement('div');
            textNode.innerHTML = html;
            bubble.appendChild(textNode);
            
            if (timestamp) {{
                const ts = document.createElement('div');
                ts.className = 'timestamp';
                ts.textContent = timestamp;
                bubble.appendChild(ts);
            }}
            
            messageDiv.appendChild(bubble);
            messageDiv.classList.add('fade-in');
            messagesDiv.appendChild(messageDiv);
            const messageEls = Array.from(messagesDiv.querySelectorAll('.message')).filter(function(m) {{ return m.id !== 'thinking-indicator'; }});
            messageDiv.setAttribute('data-message-index', String(messageEls.length - 1));
            
            scrollToBottom();
            if (isUser && attachmentImageUrls && Array.isArray(attachmentImageUrls) && attachmentImageUrls.length > 0) {{
                setTimeout(scrollToBottom, 150);
            }}
        }}
        
        function addAgentMessage(text, isThinking) {{
            const messagesDiv = document.getElementById('messages');
            if (!messagesDiv) return;
            const messageDiv = document.createElement('div');
            messageDiv.className = 'message agent';
            const bubble = document.createElement('div');
            bubble.className = 'bubble agent' + (isThinking ? ' thinking' : '');
            let html = '';
            if (typeof marked !== 'undefined') {{
                html = sanitizeHtml(marked.parse(normalizeMarkdownBold(text)));
            }} else {{
                html = text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\\n/g, '<br>');
            }}
            html = wrapPathLinks(html);
            bubble.innerHTML = html;
            messageDiv.appendChild(bubble);
            messageDiv.classList.add('fade-in');
            const thinkingEl = document.getElementById('thinking-indicator');
            if (thinkingEl) {{
                messagesDiv.insertBefore(messageDiv, thinkingEl);
            }} else {{
                messagesDiv.appendChild(messageDiv);
            }}
            const messageEls = Array.from(messagesDiv.querySelectorAll('.message')).filter(function(m) {{ return m.id !== 'thinking-indicator'; }});
            messageDiv.setAttribute('data-message-index', String(messageEls.length - 1));
            scrollToBottom();
        }}
        
        function updateLastMessage(text, bubbleType) {{
            const messages = document.getElementById('messages');
            const allMsgs = Array.from(messages.querySelectorAll('.message')).filter(function(m) {{ return m.id !== 'thinking-indicator'; }});
            const lastMessage = allMsgs.length ? allMsgs[allMsgs.length - 1] : null;
            if (lastMessage && !lastMessage.classList.contains('user')) {{
                if (!text) {{
                    lastMessage.remove();
                    return;
                }}
                const bubble = lastMessage.querySelector('.bubble');
                if (bubble) {{
                    bubble.className = 'bubble ' + (bubbleType || 'agent');
                    let html = '';
                    if (typeof marked !== 'undefined') {{
                        html = sanitizeHtml(marked.parse(normalizeMarkdownBold(text)));
                    }} else {{
                        html = text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\\n/g, '<br>');
                    }}
                    html = wrapPathLinks(html);
                    bubble.innerHTML = html;
                    // Restore timestamp in agent bubble
                    const ts = document.createElement('div');
                    ts.className = 'timestamp';
                    const d = new Date();
                    ts.textContent = d.toLocaleTimeString(undefined, {{ hour: 'numeric', minute: '2-digit', hour12: true }});
                    bubble.appendChild(ts);
                    // Ensure message-actions (thumbs + regenerate) below last agent bubble
                    if (!lastMessage.querySelector('.message-actions')) {{
                        const actions = document.createElement('div');
                        actions.className = 'message-actions';
                        const idx = parseInt(lastMessage.getAttribute('data-message-index'), 10) || 0;
                        const upL = document.createElement('a');
                        upL.href = '#';
                        upL.title = 'Thumbs up';
                        upL.className = 'feedback-link';
                        upL.textContent = '\u2191';
                        upL.onclick = function(e) {{ e.preventDefault(); if (lastMessage.getAttribute('data-feedback-recorded')) return; lastMessage.setAttribute('data-feedback-recorded', 'true'); fetch('/message_action', {{ method: 'POST', headers: {{ 'Content-Type': 'application/json' }}, body: JSON.stringify({{ action: 'feedback_up', message_index: idx }}) }}).then(function() {{ const ac = lastMessage.querySelector('.message-actions'); if (ac) ac.innerHTML = '<span class=\\"feedback-recorded\\">\u2713 Feedback recorded</span>'; }}).catch(function() {{}}); }};
                        const downL = document.createElement('a');
                        downL.href = '#';
                        downL.title = 'Thumbs down';
                        downL.className = 'feedback-link';
                        downL.textContent = '\u2193';
                        downL.onclick = function(e) {{ e.preventDefault(); if (lastMessage.getAttribute('data-feedback-recorded')) return; lastMessage.setAttribute('data-feedback-recorded', 'true'); fetch('/message_action', {{ method: 'POST', headers: {{ 'Content-Type': 'application/json' }}, body: JSON.stringify({{ action: 'feedback_down', message_index: idx }}) }}).then(function() {{ const ac = lastMessage.querySelector('.message-actions'); if (ac) ac.innerHTML = '<span class=\\"feedback-recorded\\">\u2713 Feedback recorded</span>'; }}).catch(function() {{}}); }};
                        actions.appendChild(upL);
                        actions.appendChild(downL);
                        const a = document.createElement('a');
                        a.href = '#';
                        a.title = 'Regenerate';
                        a.textContent = '\u21BB';
                        a.className = 'regenerate-link';
                        a.onclick = function(e) {{ e.preventDefault(); fetch('/regenerate', {{ method: 'POST' }}).catch(function() {{}}); }};
                        actions.appendChild(a);
                        lastMessage.appendChild(actions);
                    }}
                    scrollToBottom();
                }}
            }}
        }}
        
        function appendToLastMessage(chunk) {{
            const messages = document.getElementById('messages');
            const allMsgs = Array.from(messages.querySelectorAll('.message')).filter(function(m) {{ return m.id !== 'thinking-indicator'; }});
            const lastMessage = allMsgs.length ? allMsgs[allMsgs.length - 1] : null;
            if (lastMessage && !lastMessage.classList.contains('user')) {{
                const bubble = lastMessage.querySelector('.bubble');
                if (bubble) {{
                    const timestamp = bubble.querySelector('.timestamp');
                    // Get current text (without timestamp)
                    const tempDiv = document.createElement('div');
                    tempDiv.innerHTML = bubble.innerHTML.replace(/<div class="timestamp">.*<\\/div>$/, '');
                    const currentText = tempDiv.textContent || tempDiv.innerText || '';
                    const newText = currentText + chunk;
                    // Plain text only during stream (no markdown) to avoid layout jump; markdown runs in updateLastMessage at stream end
                    const esc = function(s) {{ return (s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }};
                    const html = esc(newText).replace(/\\n/g, '<br>');
                    bubble.innerHTML = html;
                    if (timestamp) {{
                        bubble.appendChild(timestamp);
                    }}
                    scrollToBottom();
                }}
            }}
        }}
        
        function showThinking() {{
            const messagesDiv = document.getElementById('messages');
            // Remove existing thinking indicator if any
            const existing = document.getElementById('thinking-indicator');
            if (existing) {{
                existing.remove();
            }}
            
            // Always append thinking indicator to the end (below all messages)
            // This ensures it appears below agent bubbles
            const thinkingDiv = document.createElement('div');
            thinkingDiv.className = 'message agent';
            thinkingDiv.id = 'thinking-indicator';
            thinkingDiv.innerHTML = `
                <div class="thinking">
                    <div class="thinking-dot"></div>
                    <div class="thinking-dot"></div>
                    <div class="thinking-dot"></div>
                </div>
            `;
            
            // Always append to the end to ensure it's below all messages
            messagesDiv.appendChild(thinkingDiv);
            document.body.classList.add('agent-working');
            scrollToBottom();
        }}
        
        function hideThinking() {{
            document.body.classList.remove('agent-working');
            const thinking = document.getElementById('thinking-indicator');
            if (thinking) {{
                thinking.remove();
            }}
        }}
        
        function clearMessages() {{
            const messagesDiv = document.getElementById('messages');
            if (messagesDiv) {{
                messagesDiv.innerHTML = '';
            }}
        }}
        
        // Context menu on message bubbles: Copy, Copy to current scratchpad, Copy to new scratchpad
        let messageContextMenu = null;
        document.addEventListener('contextmenu', function(e) {{
            const msg = e.target.closest('.message');
            if (!msg || msg.id === 'thinking-indicator') return;
            const idx = msg.getAttribute('data-message-index');
            if (idx === null || idx === undefined) return;
            e.preventDefault();
            if (messageContextMenu) messageContextMenu.remove();
            messageContextMenu = document.createElement('div');
            messageContextMenu.className = 'message-context-menu';
            messageContextMenu.style.left = e.clientX + 'px';
            messageContextMenu.style.top = e.clientY + 'px';
            function sendAction(action) {{
                messageContextMenu.remove();
                messageContextMenu = null;
                var payload = {{ action: action, message_index: parseInt(idx, 10) }};
                fetch('/message_action', {{ method: 'POST', headers: {{ 'Content-Type': 'application/json' }}, body: JSON.stringify(payload) }}).catch(function(){{}});
            }}
            const copyBtn = document.createElement('button');
            copyBtn.textContent = 'Copy';
            copyBtn.onclick = function() {{ sendAction('copy'); }};
            messageContextMenu.appendChild(copyBtn);
            const toCurrentBtn = document.createElement('button');
            toCurrentBtn.textContent = 'Copy to current scratchpad';
            toCurrentBtn.onclick = function() {{ sendAction('copy_to_scratchpad'); }};
            messageContextMenu.appendChild(toCurrentBtn);
            const toNewBtn = document.createElement('button');
            toNewBtn.textContent = 'Copy to new scratchpad';
            toNewBtn.onclick = function() {{ sendAction('copy_to_new_scratchpad'); }};
            messageContextMenu.appendChild(toNewBtn);
            const isUser = msg.classList.contains('user');
            let addedEdit = false;
            if (isUser) {{
                const messagesEl = document.getElementById('messages');
                const userMsgs = messagesEl ? Array.from(messagesEl.querySelectorAll('.message.user')) : [];
                const lastUserIdx = userMsgs.length ? Math.max.apply(null, userMsgs.map(function(m) {{ return parseInt(m.getAttribute('data-message-index'), 10); }})) : -1;
                if (parseInt(idx, 10) === lastUserIdx) {{
                    const editBtn = document.createElement('button');
                    editBtn.textContent = 'Edit';
                    editBtn.onclick = function() {{ sendAction('edit_last'); }};
                    messageContextMenu.appendChild(editBtn);
                    addedEdit = true;
                }}
            }}
            document.body.appendChild(messageContextMenu);
        }}, true);
        document.addEventListener('click', function(e) {{
            if (!messageContextMenu) return;
            if (e.target.closest('.message-context-menu')) return;
            messageContextMenu.remove();
            messageContextMenu = null;
        }}, true);

        // Open external links in system browser; open-in-Finder links via fetch (no navigation)
        document.addEventListener('click', function(e) {{
            const a = e.target.closest('a');
            if (!a || !a.href) return;
            const href = a.href;
            try {{
                const u = new URL(href);
                if (u.pathname === '/open_in_finder' && u.searchParams.get('path')) {{
                    e.preventDefault();
                    e.stopPropagation();
                    fetch(href).catch(function() {{}});
                    return;
                }}
                // Markdown links like [report](/Users/.../report.md) resolve to localhost + pathname; no route exists -> 404. Treat as open-in-Finder.
                if (u.hostname === 'localhost' && (u.pathname.indexOf('/Users/') === 0 || u.pathname.indexOf('/Users%2F') === 0)) {{
                    e.preventDefault();
                    e.stopPropagation();
                    const path = u.pathname.indexOf('/Users%2F') === 0 ? decodeURIComponent(u.pathname) : u.pathname;
                    fetch('/open_in_finder?path=' + encodeURIComponent(path)).catch(function() {{}});
                    return;
                }}
            }} catch (err) {{}}
            if (href.indexOf('http') !== 0) return;
            if (href.indexOf('http://localhost') === 0) return;
            e.preventDefault();
            e.stopPropagation();
            fetch('/open?url=' + encodeURIComponent(href)).catch(function() {{}});
        }}, true);

        // WebView pill input: auto-resize textarea, attachment chips, FormData send
        (function() {{
            const input = document.getElementById('chat-input');
            const sendStopBtn = document.getElementById('chat-send-stop');
            const attachBtn = document.getElementById('chat-attach');
            const voiceBtn = document.getElementById('chat-voice');
            const researchToggleBtn = document.getElementById('chat-research-toggle');
            const fileInput = document.getElementById('chat-file-input');
            const attachmentsEl = document.getElementById('chat-attachments');
            if (!input || !sendStopBtn) return;
            let researchModeToggle = false;
            window.getResearchModeToggle = function() {{ return researchModeToggle; }};
            window.setResearchModeToggle = function(active) {{
                researchModeToggle = !!active;
                if (researchToggleBtn) researchToggleBtn.classList.toggle('active', researchModeToggle);
                if (researchModeToggle) document.body.classList.add('research-mode');
                else document.body.classList.remove('research-mode');
            }};
            if (researchToggleBtn) {{
                researchToggleBtn.addEventListener('click', function() {{
                    window.setResearchModeToggle(!researchModeToggle);
                }});
            }}
            let voicePushToTalk = false;
            let voiceListening = false;
            fetch('/voice_prefs').then(function(r) {{ return r.json(); }}).then(function(p) {{
                voicePushToTalk = !!p.voice_push_to_talk;
            }}).catch(function() {{}});
            window.setVoiceListening = function(isListening) {{
                voiceListening = !!isListening;
                if (voiceBtn) {{
                    voiceBtn.classList.toggle('listening', voiceListening);
                    voiceBtn.title = voiceListening ? 'Listening… (click to stop)' : 'Voice input (click to start/stop, hold Option to send)';
                    voiceBtn.setAttribute('aria-label', voiceListening ? 'Stop listening' : 'Voice input');
                }}
            }};
            const ORIGINAL_PLACEHOLDER = input.getAttribute('placeholder') || 'Type your message... (Enter to send, Shift+Enter for new line)';
            const TEXTAREA_MAX_HEIGHT = 200;
            let syncTimeout = null;
            let running = false;
            let attachmentFiles = [];
            let nativeAttachments = [];
            window.setNativeAttachments = function(list) {{
                nativeAttachments = list || [];
                renderChips();
            }};

            function resizeTextarea() {{
                input.style.height = '1px';
                var h = Math.max(24, input.scrollHeight);
                input.style.height = Math.min(h, TEXTAREA_MAX_HEIGHT) + 'px';
                input.style.overflowY = h > TEXTAREA_MAX_HEIGHT ? 'auto' : 'hidden';
            }}

            function isImage(file) {{
                return (file.type || '').indexOf('image/') === 0;
            }}
            function fileIcon(ext) {{
                var icons = {{ '.pdf':'📄', '.png':'🖼', '.jpg':'🖼', '.jpeg':'🖼', '.gif':'🖼', '.txt':'📝', '.md':'📝', '.py':'📝' }};
                return icons[ext.toLowerCase()] || '📎';
            }}

            function renderChips() {{
                if (!attachmentsEl) return;
                attachmentsEl.innerHTML = '';
                nativeAttachments.forEach(function(item, index) {{
                    var chip = document.createElement('div');
                    chip.className = 'attachment-chip';
                    chip.setAttribute('data-native-index', index);
                    var icon = document.createElement('span');
                    icon.className = 'chip-icon';
                    icon.textContent = fileIcon(item.ext || '');
                    chip.appendChild(icon);
                    var name = document.createElement('span');
                    name.className = 'chip-name';
                    name.textContent = item.name || 'file';
                    chip.appendChild(name);
                    var remove = document.createElement('button');
                    remove.type = 'button';
                    remove.className = 'chip-remove';
                    remove.setAttribute('aria-label', 'Remove');
                    remove.textContent = '×';
                    remove.addEventListener('click', function() {{
                        fetch('/remove_attach', {{ method: 'POST', headers: {{ 'Content-Type': 'application/json' }}, body: JSON.stringify({{ index: index }}) }}).catch(function() {{}});
                    }});
                    chip.appendChild(remove);
                    attachmentsEl.appendChild(chip);
                }});
                attachmentFiles.forEach(function(item, index) {{
                    var chip = document.createElement('div');
                    chip.className = 'attachment-chip';
                    chip.setAttribute('data-index', index);
                    if (item.preview) {{
                        var img = document.createElement('img');
                        img.src = item.preview;
                        img.alt = item.name;
                        chip.appendChild(img);
                    }} else {{
                        var icon = document.createElement('span');
                        icon.className = 'chip-icon';
                        icon.textContent = fileIcon(item.ext);
                        chip.appendChild(icon);
                    }}
                    var name = document.createElement('span');
                    name.className = 'chip-name';
                    name.textContent = item.name;
                    chip.appendChild(name);
                    var remove = document.createElement('button');
                    remove.type = 'button';
                    remove.className = 'chip-remove';
                    remove.setAttribute('aria-label', 'Remove');
                    remove.textContent = '×';
                    remove.addEventListener('click', function() {{
                        attachmentFiles.splice(index, 1);
                        renderChips();
                    }});
                    chip.appendChild(remove);
                    attachmentsEl.appendChild(chip);
                }});
            }}

            function addFiles(files) {{
                if (!files || !files.length) return;
                for (var i = 0; i < files.length; i++) {{
                    var f = files[i];
                    var name = f.name || 'file';
                    var ext = name.indexOf('.') >= 0 ? name.substring(name.lastIndexOf('.')) : '';
                    var item = {{ file: f, name: name, ext: ext, preview: null }};
                    if (isImage(f)) {{
                        var reader = new FileReader();
                        reader.onload = (function(it) {{ return function(e) {{
                            it.preview = e.target.result;
                            renderChips();
                        }}; }})(item);
                        reader.readAsDataURL(f);
                    }}
                    attachmentFiles.push(item);
                }}
                renderChips();
            }}

            function submitInput() {{
                var text = (input.value || '').trim();
                var hasWebFiles = attachmentFiles.length > 0;
                var hasNativeFiles = nativeAttachments.length > 0;
                var hasFiles = hasWebFiles || hasNativeFiles;
                if (!text && !hasFiles) return;

                if (hasWebFiles) {{
                    var form = new FormData();
                    form.append('message', text);
                    form.append('research_mode', researchModeToggle ? 'true' : 'false');
                    attachmentFiles.forEach(function(item) {{
                        form.append('files[]', item.file);
                    }});
                    fetch('/send', {{ method: 'POST', body: form }})
                        .then(function() {{
                            input.value = '';
                            attachmentFiles = [];
                            nativeAttachments = [];
                            renderChips();
                            resizeTextarea();
                        }})
                        .catch(function() {{}});
                }} else {{
                    fetch('/send', {{
                        method: 'POST',
                        headers: {{ 'Content-Type': 'application/json' }},
                        body: JSON.stringify({{ text: text, research_mode: researchModeToggle }})
                    }})
                        .then(function() {{
                            input.value = '';
                            if (hasNativeFiles) {{
                                nativeAttachments = [];
                                renderChips();
                            }}
                            resizeTextarea();
                        }})
                        .catch(function() {{}});
                }}
            }}

            function syncToApp() {{
                var text = input.value || '';
                fetch('/sync_input', {{
                    method: 'POST',
                    headers: {{ 'Content-Type': 'application/json' }},
                    body: JSON.stringify({{ text: text }})
                }}).catch(function() {{}});
            }}

            var workingPlaceholderEl = document.getElementById('working-placeholder');
            window.setSendButtonState = function(isRunning) {{
                running = !!isRunning;
                sendStopBtn.innerHTML = running ? '&#8855;' : '&#10148;';
                sendStopBtn.title = running ? 'Stop' : 'Send';
                sendStopBtn.setAttribute('aria-label', running ? 'Stop' : 'Send');
                if (voiceBtn) voiceBtn.disabled = running;
                if (researchToggleBtn) researchToggleBtn.disabled = running;
                if (running) {{
                    input.readOnly = true;
                    input.value = '';
                    input.placeholder = '';
                    if (workingPlaceholderEl) workingPlaceholderEl.classList.add('visible');
                    attachmentFiles = [];
                    nativeAttachments = [];
                    if (attachmentsEl) attachmentsEl.innerHTML = '';
                    resizeTextarea();
                    syncToApp();
                }} else {{
                    input.readOnly = false;
                    input.placeholder = ORIGINAL_PLACEHOLDER;
                    if (workingPlaceholderEl) workingPlaceholderEl.classList.remove('visible');
                }}
            }};
            window.setResearchMode = function(active) {{
                if (!!active) {{
                    document.body.classList.add('research-mode');
                }} else if (!researchModeToggle) {{
                    document.body.classList.remove('research-mode');
                }}
            }};
            window.setSpinnerVisible = function(visible) {{
                var el = document.getElementById('chat-spinner');
                if (el) {{
                    if (!!visible) {{
                        el.classList.add('visible');
                        el.setAttribute('aria-hidden', 'false');
                    }} else {{
                        el.classList.remove('visible');
                        el.setAttribute('aria-hidden', 'true');
                    }}
                }}
            }};

            input.addEventListener('input', function() {{
                resizeTextarea();
                clearTimeout(syncTimeout);
                syncTimeout = setTimeout(syncToApp, 300);
            }});
            input.addEventListener('keydown', function(e) {{
                if (e.key === 'Enter' && !e.shiftKey) {{
                    e.preventDefault();
                    if (!running) submitInput();
                }}
            }});
            input.addEventListener('paste', function(e) {{
                if (running) return;
                var dt = e.clipboardData;
                if (!dt || !dt.items || !dt.items.length) return;
                var imageFiles = [];
                for (var i = 0; i < dt.items.length; i++) {{
                    var item = dt.items[i];
                    if (item.kind === 'file' && (item.type || '').indexOf('image/') === 0) {{
                        var file = item.getAsFile();
                        if (file) imageFiles.push(file);
                    }}
                }}
                if (imageFiles.length) {{
                    e.preventDefault();
                    addFiles(imageFiles);
                }}
            }});
            input.addEventListener('blur', function() {{ syncToApp(); }});
            sendStopBtn.addEventListener('click', function() {{
                if (running) fetch('/trigger_stop', {{ method: 'POST' }}).catch(function() {{}});
                else submitInput();
            }});
            if (attachBtn) {{
                attachBtn.addEventListener('click', function() {{
                    fetch('/trigger_attach', {{ method: 'POST' }}).catch(function() {{}});
                }});
            }}
            if (voiceBtn) {{
                if (voicePushToTalk) {{
                    voiceBtn.addEventListener('mousedown', function(e) {{
                        if (running || voiceListening) return;
                        fetch('/voice_start', {{ method: 'POST', headers: {{ 'Content-Type': 'application/json' }}, body: JSON.stringify({{ sendOnRelease: e.altKey }}) }}).catch(function() {{}});
                    }});
                    voiceBtn.addEventListener('mouseup', function() {{
                        if (voiceListening) fetch('/voice_stop', {{ method: 'POST' }}).catch(function() {{}});
                    }});
                    voiceBtn.addEventListener('mouseleave', function() {{
                        if (voiceListening) fetch('/voice_stop', {{ method: 'POST' }}).catch(function() {{}});
                    }});
                }} else {{
                    voiceBtn.addEventListener('click', function(e) {{
                        if (running) return;
                        fetch('/voice_toggle', {{ method: 'POST', headers: {{ 'Content-Type': 'application/json' }}, body: JSON.stringify({{ sendOnRelease: e.altKey }}) }}).catch(function() {{}});
                    }});
                }}
            }}
            if (fileInput) {{
                fileInput.addEventListener('change', function() {{
                    addFiles(fileInput.files || []);
                    fileInput.value = '';
                }});
            }}
            resizeTextarea();
        }})();
    </script>
</body>
</html>
"""


def get_chat_html():
    """Generate the HTML template with theme values."""
    return CHAT_HTML_TEMPLATE.format(
        font_size=THEME["chat_font_px"],
        spacing=THEME["bubble_spacing"],
        padding=THEME["bubble_padding"],
        radius=THEME["bubble_radius"],
        user_bg=THEME["user_bubble_bg"],
        user_text=THEME["user_text"],
        agent_bg=THEME["agent_bubble_bg"],
        agent_text=THEME["agent_text"],
        tool_bg=THEME["tool_bubble_bg"],
        tool_text=THEME["tool_text"],
        error_bg=THEME["error_bubble_bg"],
        error_text=THEME["error_text"],
        timestamp_color=THEME["timestamp_color"],
        timestamp_color_dark=THEME["timestamp_color_dark"],
    )
