api.php done. Now let me handle app.js. This is the trickiest one because v1 has different content. Let me read the AI chat section in v1's app.js.


TodoWrite

Analyze full diff scope between extension2 and extension2v1

Port ThreadAnalyzer.php changes (stripQuotedContent public static)

Port api.php changes (contacts fix, ai_chat rewrite, new endpoints)

Port app.js changes (context builder, agents, sessions, memory UI)

Port index.php changes (AI chat panel with agents, context builder)


Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable

Analyze full diff scope between extension2 and extension2v1

Port ThreadAnalyzer.php changes (stripQuotedContent public static)

Port api.php changes (contacts fix, ai_chat rewrite, new endpoints)

Port app.js changes (context builder, agents, sessions, memory UI)

Port index.php changes (AI chat panel with agents, context builder)


No matches found


The v1 app.js has NO AI chat functionality at all! Let me check what functions are in the v1 app.js at the end of the file, since our AI chat code was appended to the end of app.js in extension2.


v1's app.js has no AI chat code at all! Let me check the end of the file and figure out where our AI chat JS block exists in extension2.


  3180→
  3181→        const msgEl = document.getElementById('owner-status-message');
  3182→        if (result.success) {
  3183→            msgEl.textContent = '✓ Saved successfully';
  3184→            msgEl.style.color = '#059669';
  3185→        } else {
  3186→            msgEl.textContent = '✗ Error saving';
  3187→            msgEl.style.color = '#dc2626';
  3188→        }
  3189→
  3190→        setTimeout(() => { msgEl.textContent = ''; }, 3000);
  3191→    } catch (error) {
  3192→        alert('Error saving: ' + error.message);
  3193→    }
  3194→}
  3195→
  3196→

        const msgEl = document.getElementById('owner-status-message');
        if (result.success) {
            msgEl.textContent = '✓ Saved successfully';
            msgEl.style.color = '#059669';
        } else {
            msgEl.textContent = '✗ Error saving';
            msgEl.style.color = '#dc2626';
        }

        setTimeout(() => { msgEl.textContent = ''; }, 3000);
    } catch (error) {
        alert('Error saving: ' + error.message);
    }
}


722:                <button onclick="openAiChatForThread('${escapeHtml(threadId)}', '${escapeHtml((data.emails[0].subject || '').replace(/'/g, "\\'"))}', ${data.emails[0].contact_id || 0})" style="background:linear-gradient(135deg,#6366f1,#8b5cf6);color:#fff;border:none;border-radius:8px;padding:8px 16px;cursor:pointer;font-size:0.85em;font-weight:600;">💬 Ask AI About This Thread</button>
723:                <button onclick="openAiChatForReply('${escapeHtml(threadId)}', ${data.emails[data.email_count - 1].id || 0}, '${escapeHtml((data.emails[0].subject || '').replace(/'/g, "\\'"))}')" style="background:linear-gradient(135deg,#10b981,#059669);color:#fff;border:none;border-radius:8px;padding:8px 16px;cursor:pointer;font-size:0.85em;font-weight:600;">✍ Compose Reply</button>
2760:function toggleAiChat() {
2767:function openAiChatForThread(threadId, subject, contactId) {
2771:    if (!aiChat.isOpen) toggleAiChat(); else document.getElementById('ai-chat-input').focus();
2774:function openAiChatForContact(contactId, contactName) {
2778:    if (!aiChat.isOpen) toggleAiChat(); else document.getElementById('ai-chat-input').focus();
2781:function openAiChatForReply(threadId, emailId, subject) {
2784:    if (!aiChat.isOpen) toggleAiChat(); else document.getElementById('ai-chat-input').focus();
3234:        if (!aiChat.isOpen) toggleAiChat();


2650→            msgEl.style.color = '#991b1b';
  2651→            msgEl.style.border = '1px solid #fecaca';
  2652→            break;
  2653→        default:
  2654→            msgEl.style.background = '#eff6ff';
  2655→            msgEl.style.color = '#1e40af';
  2656→            msgEl.style.border = '1px solid #bfdbfe';
  2657→    }
  2658→
  2659→    // Auto-hide after 5 seconds for non-error messages
  2660→    if (type !== 'error') {
  2661→        setTimeout(() => {
  2662→            msgEl.style.display = 'none';
  2663→        }, 5000);
  2664→    }
  2665→}
  2666→
  2667→
  2668→// ============================================
  2669→// AI CHAT PANEL
  2670→// ============================================
  2671→
  2672→const aiChat = {
  2673→    isOpen: false,
  2674→    mode: 'general',
  2675→    threadId: null,
  2676→    contactId: null,
  2677→    emailId: null,
  2678→    threadSubject: null,
  2679→    contactName: null,
  2680→    history: [],
  2681→    isDragging: false,
  2682→    dragOffsetX: 0,
  2683→    dragOffsetY: 0,
  2684→    // Context builder
  2685→    contextThreads: [],      // [{thread_id, subject, email_count, contact_name}]
  2686→    contextContact: null,    // {id, name, email}
  2687→    _availableThreads: [],   // temp storage for thread picker
  2688→    // Session persistence
  2689→    sessionId: null,
  2690→    // Agent
  2691→    agent: 'general',
  2692→    // Contact memories
  2693→    contactMemories: [],
  2694→};
  2695→
  2696→// Provider/Model config — synced with Chrome extension contentScript.js
  2697→const AI_PROVIDER_MODELS = {
  2698→    claude: [
  2699→        { id: 'claude-sonnet-4-5-20250929', name: 'Sonnet 4.5' },
  2700→        { id: 'claude-opus-4-5-20251101', name: 'Opus 4.5 (Best)' },
  2701→        { id: 'claude-haiku-4-5-20251001', name: 'Haiku 4.5 (Fast)' },
  2702→    ],
  2703→    openai: [
  2704→        { id: 'gpt-5.2', name: 'GPT-5.2 (Latest)' },
  2705→        { id: 'gpt-5.2-pro', name: 'GPT-5.2 Pro (Best)' },
  2706→        { id: 'gpt-5-mini', name: 'GPT-5 Mini' },
  2707→        { id: 'gpt-5-nano', name: 'GPT-5 Nano (Fast)' },
  2708→    ],
  2709→    gemini: [
  2710→        { id: 'gemini-2.5-flash', name: 'Gemini 2.5 Flash' },
  2711→        { id: 'gemini-3-pro', name: 'Gemini 3 Pro' },
  2712→        { id: 'gemini-2.5-flash-lite', name: 'Flash-Lite (Fast)' },
  2713→    ],
  2714→    grok: [
  2715→        { id: 'grok-4', name: 'Grok 4 (Latest)' },
  2716→        { id: 'grok-3', name: 'Grok 3' },
  2717→        { id: 'grok-3-mini', name: 'Grok 3 Mini (Fast)' },
  2718→    ],
  2719→    deepseek: [
  2720→        { id: 'deepseek-chat', name: 'DeepSeek V3.2' },
  2721→        { id: 'deepseek-reasoner', name: 'Reasoner (Thinking)' },
  2722→    ],
  2723→    qwen: [
  2724→        { id: 'qwen-max-latest', name: 'Qwen Max' },
  2725→        { id: 'qwen-turbo-latest', name: 'Qwen Turbo (Fast)' },
  2726→    ],
  2727→    mistral: [
  2728→        { id: 'mistral-large-latest', name: 'Mistral Large' },
  2729→        { id: 'mistral-small-latest', name: 'Mistral Small (Fast)' },
  2730→    ],
  2731→    cohere: [
  2732→        { id: 'command-a-03-2025', name: 'Command A' },
  2733→        { id: 'command-r7b-12-2024', name: 'Command R7B (Fast)' },
  2734→    ],
  2735→};
  2736→
  2737→const AI_PROVIDER_NAMES = {
  2738→    claude: 'Claude', openai: 'OpenAI', gemini: 'Gemini', grok: 'Grok',
  2739→    deepseek: 'DeepSeek', qwen: 'Qwen', mistral: 'Mistral', cohere: 'Cohere',
  2740→};
  2741→
  2742→// Agent presets
  2743→const AI_AGENTS = {
  2744→    general:             { id: 'general',             name: 'General',    icon: '\u{1F4AC}', preferredProvider: null,    preferredModel: null, quickPrompts: ['What are my most at-risk clients?', 'Which threads need urgent attention?', 'Summarize recent activity'] },
  2745→    quote_analyst:       { id: 'quote_analyst',       name: 'Quotes',     icon: '\u{1F4B0}', preferredProvider: 'claude', preferredModel: 'claude-sonnet-4-5-20250929', quickPrompts: ['How much have we quoted across these threads?', 'What is the price sensitivity?', 'Suggest optimal pricing', 'What discounts were given?'] },
  2746→    deception_detector:  { id: 'deception_detector',  name: 'Deception',  icon: '\u{1F50D}', preferredProvider: 'claude', preferredModel: 'claude-opus-4-5-20251101', quickPrompts: ['What is the deception level?', 'Any contradictions between statements?', 'Red flags or pressure tactics?', 'Rate honesty 1-10 with evidence'] },
  2747→    contact_profiler:    { id: 'contact_profiler',    name: 'Profiler',   icon: '\u{1F464}', preferredProvider: 'claude', preferredModel: 'claude-sonnet-4-5-20250929', quickPrompts: ['Build a complete profile', 'Communication style analysis', 'Decision-making patterns', 'How should I approach them?'] },
  2748→    email_composer:      { id: 'email_composer',      name: 'Composer',   icon: '\u{270D}\u{FE0F}', preferredProvider: 'openai', preferredModel: 'gpt-5.2', quickPrompts: ['Write a professional reply', 'Write a firm but polite response', 'Write a follow-up email', 'Write a brief acknowledgment'] },
  2749→};
  2750→
  2751→const AI_QUICK_PROMPTS = {
  2752→    general: ['What are my most at-risk clients?', 'Which threads need urgent attention?', 'Summarize recent activity'],
  2753→    thread: ['Summarize this thread', 'What are the key risks?', 'What action should I take next?', 'Is this client trustworthy?', 'What are they really asking for?'],
  2754→    contact: ['What is their communication style?', 'Are there any red flags?', 'How should I approach them?', 'What opportunities exist with them?'],
  2755→    quote: ['How much should I quote for this project?', 'What is the scope of work?', 'What are the risks?', 'Suggest a project timeline'],
  2756→    reply: ['Write a professional reply', 'Write a firm but polite response', 'Write a follow-up email', 'Write a brief acknowledgment'],
  2757→    multithread: ['Summarize all conversations', 'How much have we quoted?', 'What is the deception level?', 'Build a complete profile', 'What patterns emerge across these threads?'],
  2758→};
  2759→
  2760→function toggleAiChat() {
  2761→    const panel = document.getElementById('ai-chat-panel');
  2762→    aiChat.isOpen = !aiChat.isOpen;
  2763→    panel.style.display = aiChat.isOpen ? 'flex' : 'none';
  2764→    if (aiChat.isOpen) { document.getElementById('ai-chat-input').focus(); renderQuickPrompts(); }
  2765→}
  2766→
  2767→function openAiChatForThread(threadId, subject, contactId) {
  2768→    aiChat.threadId = threadId; aiChat.contactId = contactId || null;
  2769→    aiChat.threadSubject = subject || threadId; aiChat.emailId = null;
  2770→    setAiChatMode('thread', document.querySelector('.ai-mode-btn[data-mode="thread"]'));
  2771→    if (!aiChat.isOpen) toggleAiChat(); else document.getElementById('ai-chat-input').focus();
  2772→}
  2773→
  2774→function openAiChatForContact(contactId, contactName) {
  2775→    aiChat.contactId = contactId; aiChat.contactName = contactName || 'Contact';
  2776→    aiChat.threadId = null; aiChat.emailId = null;
  2777→    setAiChatMode('contact', document.querySelector('.ai-mode-btn[data-mode="contact"]'));
  2778→    if (!aiChat.isOpen) toggleAiChat(); else document.getElementById('ai-chat-input').focus();
  2779→}
  2780→
  2781→function openAiChatForReply(threadId, emailId, subject) {
  2782→    aiChat.threadId = threadId; aiChat.emailId = emailId; aiChat.threadSubject = subject || threadId;
  2783→    setAiChatMode('reply', document.querySelector('.ai-mode-btn[data-mode="reply"]'));
  2784→    if (!aiChat.isOpen) toggleAiChat(); else document.getElementById('ai-chat-input').focus();
  2785→}
  2786→
  2787→function setAiChatMode(mode, btn) {
  2788→    aiChat.mode = mode;
  2789→    document.querySelectorAll('.ai-mode-btn').forEach(b => { b.style.background = '#2d2d4e'; b.style.color = '#94a3b8'; });
  2790→    if (btn) { btn.style.background = '#6366f1'; btn.style.color = '#fff'; }
  2791→    updateAiChatContextLabel(); renderQuickPrompts();
  2792→}
  2793→
  2794→function updateAiChatContextLabel() {
  2795→    const label = document.getElementById('ai-chat-context-label');
  2796→    if (!label) return;
  2797→
  2798→    // Multi-thread context takes priority
  2799→    if (aiChat.contextThreads.length > 0) {
  2800→        const totalEmails = aiChat.contextThreads.reduce((sum, t) => sum + (t.email_count || 0), 0);
  2801→        const contactLabel = aiChat.contextContact ? aiChat.contextContact.name : '';
  2802→        label.textContent = `${aiChat.contextThreads.length} threads, ~${totalEmails} emails` + (contactLabel ? ` from ${contactLabel}` : '');
  2803→        label.style.color = '#10b981';
  2804→        return;
  2805→    }
  2806→
  2807→    const modeLabels = { general: 'General Chat', thread: 'Thread Analysis', contact: 'Contact Intelligence', quote: 'Quote Assistant', reply: 'Reply Composer' };
  2808→    let ctx = modeLabels[aiChat.mode] || 'General Chat';
  2809→    if (aiChat.mode === 'thread' && aiChat.threadSubject) ctx += ' \u00B7 ' + aiChat.threadSubject.substring(0, 30);
  2810→    if (aiChat.mode === 'contact' && aiChat.contactName) ctx += ' \u00B7 ' + aiChat.contactName;
  2811→    label.textContent = ctx;
  2812→    label.style.color = 'rgba(255,255,255,0.7)';
  2813→}
  2814→
  2815→function renderQuickPrompts() {
  2816→    const container = document.getElementById('ai-quick-prompts');
  2817→    if (!container) return;
  2818→    // Use agent prompts if agent selected, then multithread if context loaded, then mode
  2819→    let prompts;
  2820→    const agent = AI_AGENTS[aiChat.agent];
  2821→    if (agent && agent.id !== 'general') {
  2822→        prompts = agent.quickPrompts;
  2823→    } else if (aiChat.contextThreads.length > 0) {
  2824→        prompts = AI_QUICK_PROMPTS.multithread;
  2825→    } else {
  2826→        prompts = AI_QUICK_PROMPTS[aiChat.mode] || AI_QUICK_PROMPTS.general;
  2827→    }
  2828→    container.innerHTML = prompts.map(p =>
  2829→        `<button onclick="sendAiChatPrompt(${JSON.stringify(p)})" style="background:#2d2d4e;color:#94a3b8;border:1px solid #3d3d5c;border-radius:12px;padding:3px 10px;font-size:0.68em;cursor:pointer;white-space:nowrap;" onmouseover="this.style.background='#6366f1';this.style.color='#fff'" onmouseout="this.style.background='#2d2d4e';this.style.color='#94a3b8'">${escapeHtml(p)}</button>`
  2830→    ).join('');
  2831→}
  2832→
  2833→function sendAiChatPrompt(prompt) {
  2834→    document.getElementById('ai-chat-input').value = prompt;
  2835→    sendAiChat();
  2836→}
  2837→
  2838→function handleAiChatKey(e) {
  2839→    if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendAiChat(); }
  2840→}
  2841→
  2842→// --- Provider/Model Dropdown ---
  2843→function initAiProviderDropdown() {
  2844→    const providerSelect = document.getElementById('ai-chat-provider');
  2845→    if (!providerSelect) return;
  2846→    providerSelect.innerHTML = '';
  2847→    Object.keys(AI_PROVIDER_MODELS).forEach(code => {
  2848→        const opt = document.createElement('option');
  2849→        opt.value = code;
  2850→        opt.textContent = AI_PROVIDER_NAMES[code] || code;
  2851→        providerSelect.appendChild(opt);
  2852→    });
  2853→    providerSelect.value = 'claude';
  2854→    updateAiModelDropdown();
  2855→}
  2856→
  2857→function updateAiModelDropdown() {
  2858→    const providerSelect = document.getElementById('ai-chat-provider');
  2859→    const modelSelect = document.getElementById('ai-chat-model');
  2860→    if (!providerSelect || !modelSelect) return;
  2861→    const provider = providerSelect.value;
  2862→    const models = AI_PROVIDER_MODELS[provider] || [];
  2863→    modelSelect.innerHTML = '';
  2864→    models.forEach(m => {
  2865→        const opt = document.createElement('option');
  2866→        opt.value = m.id;
  2867→        opt.textContent = m.name;
  2868→        modelSelect.appendChild(opt);
  2869→    });
  2870→}
  2871→
  2872→// --- Agent Selector ---
  2873→function selectAgent(agentId) {
  2874→    aiChat.agent = agentId;
  2875→    const agent = AI_AGENTS[agentId];
  2876→    // Highlight active agent button
  2877→    document.querySelectorAll('.ai-agent-btn').forEach(b => {
  2878→        b.style.background = b.dataset.agent === agentId ? '#6366f1' : '#2d2d4e';
  2879→        b.style.color = b.dataset.agent === agentId ? '#fff' : '#94a3b8';
  2880→    });
  2881→    // Set preferred provider/model
  2882→    if (agent && agent.preferredProvider) {
  2883→        const providerSelect = document.getElementById('ai-chat-provider');
  2884→        if (providerSelect) { providerSelect.value = agent.preferredProvider; updateAiModelDropdown(); }
  2885→        if (agent.preferredModel) {
  2886→            const modelSelect = document.getElementById('ai-chat-model');
  2887→            if (modelSelect) modelSelect.value = agent.preferredModel;
  2888→        }
  2889→    }
  2890→    updateAiChatContextLabel();
  2891→    renderQuickPrompts();
  2892→}
  2893→
  2894→// --- Context Builder ---
  2895→function toggleContextBuilder() {
  2896→    const area = document.getElementById('ai-context-search-area');
  2897→    const icon = document.getElementById('ai-context-toggle-icon');
  2898→    if (!area) return;
  2899→    const isVisible = area.style.display !== 'none';
  2900→    area.style.display = isVisible ? 'none' : 'block';
  2901→    icon.textContent = isVisible ? '+' : '-';
  2902→}
  2903→
  2904→async function searchContextContacts() {
  2905→    const query = document.getElementById('ai-context-search-input').value.trim();
  2906→    if (!query) return;
  2907→    // Strip wildcard chars — backend already does partial matching
  2908→    const cleanQuery = query.replace(/[*%]/g, '').trim();
  2909→    if (!cleanQuery) return;
  2910→
  2911→    const resultsDiv = document.getElementById('ai-context-search-results');
  2912→    resultsDiv.innerHTML = '
Searching contacts & threads...
';
  2913→
  2914→    try {
  2915→        // Search both contacts and threads in parallel
  2916→        const [contactsData, threadsData] = await Promise.all([
  2917→            apiCall('contacts', { search: cleanQuery, limit: 10 }),
  2918→            apiCall('threads', { search: cleanQuery, limit: 10 }),
  2919→        ]);
  2920→
  2921→        let html = '';
  2922→        const contacts = contactsData.contacts || [];
  2923→        const threads = threadsData.threads || [];
  2924→
  2925→        if (contacts.length === 0 && threads.length === 0) {
  2926→            resultsDiv.innerHTML = '
No contacts or threads found for "' + escapeHtml(cleanQuery) + '"
';
  2927→            return;
  2928→        }
  2929→
  2930→        // Contacts section
  2931→        if (contacts.length > 0) {
  2932→            html += '
CONTACTS
';
  2933→            html += contacts.map(c => {
  2934→                const name = c.name || c.email;
  2935→                return `

  2938→                    \u{1F464} ${escapeHtml(name)} ${escapeHtml(c.email)}
  2939→                    show threads \u25B6
  2940→
`;
  2941→            }).join('');
  2942→        }
  2943→
  2944→        // Threads section — directly addable
  2945→        if (threads.length > 0) {
  2946→            html += '
THREADS
';
  2947→            html += threads.map(t => {
  2948→                const isAdded = aiChat.contextThreads.some(ct => ct.thread_id === t.thread_id);
  2949→                const subject = t.subject || '(No Subject)';
  2950→                return `

  2952→
  2953→                        \u{1F4E7} ${escapeHtml(subject.substring(0, 45))}
  2954→                        (${t.email_count || '?'})
  2955→
  2956→                    <button onclick="event.stopPropagation();addThreadToContext('${t.thread_id}', \`${escapeHtml(subject.substring(0,50))}\`, ${t.email_count||0}, '')"
  2957→                            style="background:${isAdded ? '#10b981' : '#6366f1'}; color:#fff; border:none; border-radius:4px; padding:2px 8px; font-size:0.85em; cursor:pointer; margin-left:4px; white-space:nowrap;"
  2958→                            ${isAdded ? 'disabled' : ''}>
  2959→                        ${isAdded ? '\u2713 Added' : '+ Add'}
  2960→                    </button>
  2961→
`;
  2962→            }).join('');
  2963→        }
  2964→
  2965→        resultsDiv.innerHTML = html;
  2966→    } catch (err) {
  2967→        resultsDiv.innerHTML = '
Error: ' + escapeHtml(err.message) + '
';
  2968→    }
  2969→}
  2970→
  2971→async function selectContextContact(contactId, name, email) {
  2972→    aiChat.contextContact = { id: contactId, name, email };
  2973→    aiChat.contactId = contactId;
  2974→    aiChat.contactName = name;
  2975→    document.getElementById('ai-context-search-results').innerHTML = '';
  2976→
  2977→    const picker = document.getElementById('ai-context-thread-picker');
  2978→    picker.style.display = 'block';
  2979→    picker.innerHTML = '
Loading threads...
';
  2980→
  2981→    // Load contact memories
  2982→    loadContactMemories(contactId);
  2983→
  2984→    try {
  2985→        const data = await apiCall('threads', { contact: contactId, limit: 50 });
  2986→        if (!data.threads || data.threads.length === 0) {
  2987→            picker.innerHTML = '
No threads for this contact
';
  2988→            return;
  2989→        }
  2990→        aiChat._availableThreads = data.threads;
  2991→        let html = `

  2992→            ${escapeHtml(name)} - ${data.threads.length} threads
  2993→            <button onclick="addAllContactThreads()" style="background:#10b981; color:#fff; border:none; border-radius:4px; padding:2px 8px; font-size:0.68em; cursor:pointer;">+ Add All</button>
  2994→
`;
  2995→        html += data.threads.map(t => {
  2996→            const isAdded = aiChat.contextThreads.some(ct => ct.thread_id === t.thread_id);
  2997→            return `

  2999→
  3000→                    ${escapeHtml((t.subject || '(No Subject)').substring(0, 40))}
  3001→                    (${t.email_count || '?'} emails)
  3002→
  3003→                <button onclick="addThreadToContext('${t.thread_id}', \`${escapeHtml((t.subject||'').substring(0,50))}\`, ${t.email_count||0}, '${escapeHtml(name)}')"
  3004→                        style="background:${isAdded ? '#4b5563' : '#6366f1'}; color:#fff; border:none; border-radius:3px; padding:1px 6px; font-size:0.95em; cursor:pointer; margin-left:4px;"
  3005→                        ${isAdded ? 'disabled' : ''}>
  3006→                    ${isAdded ? '\u2713' : '+'}
  3007→                </button>
  3008→
`;
  3009→        }).join('');
  3010→        picker.innerHTML = html;
  3011→    } catch (err) {
  3012→        picker.innerHTML = '
Error: ' + escapeHtml(err.message) + '
';
  3013→    }
  3014→}
  3015→
  3016→function addThreadToContext(threadId, subject, emailCount, contactName) {
  3017→    if (aiChat.contextThreads.some(t => t.thread_id === threadId)) return;
  3018→    aiChat.contextThreads.push({ thread_id: threadId, subject, email_count: emailCount, contact_name: contactName });
  3019→    updateContextCart();
  3020→    updateAiChatContextLabel();
  3021→    renderQuickPrompts();
  3022→    // Refresh picker to update button states
  3023→    if (aiChat.contextContact) selectContextContact(aiChat.contextContact.id, aiChat.contextContact.name, aiChat.contextContact.email);
  3024→}
  3025→
  3026→function removeThreadFromContext(threadId) {
  3027→    aiChat.contextThreads = aiChat.contextThreads.filter(t => t.thread_id !== threadId);
  3028→    updateContextCart();
  3029→    updateAiChatContextLabel();
  3030→    renderQuickPrompts();
  3031→}
  3032→
  3033→function addAllContactThreads() {
  3034→    const threads = aiChat._availableThreads || [];
  3035→    threads.forEach(t => {
  3036→        if (!aiChat.contextThreads.some(ct => ct.thread_id === t.thread_id)) {
  3037→            aiChat.contextThreads.push({
  3038→                thread_id: t.thread_id, subject: t.subject || '(No Subject)',
  3039→                email_count: t.email_count || 0, contact_name: aiChat.contextContact?.name || 'Unknown'
  3040→            });
  3041→        }
  3042→    });
  3043→    updateContextCart();
  3044→    updateAiChatContextLabel();
  3045→    renderQuickPrompts();
  3046→    if (aiChat.contextContact) selectContextContact(aiChat.contextContact.id, aiChat.contextContact.name, aiChat.contextContact.email);
  3047→}
  3048→
  3049→function clearContextCart() {
  3050→    aiChat.contextThreads = [];
  3051→    aiChat.contextContact = null;
  3052→    aiChat.contactMemories = [];
  3053→    aiChat._availableThreads = [];
  3054→    updateContextCart();
  3055→    updateAiChatContextLabel();
  3056→    renderQuickPrompts();
  3057→    const picker = document.getElementById('ai-context-thread-picker');
  3058→    if (picker) picker.style.display = 'none';
  3059→    const memDiv = document.getElementById('ai-context-memories');
  3060→    if (memDiv) memDiv.style.display = 'none';
  3061→}
  3062→
  3063→function updateContextCart() {
  3064→    const cartDiv = document.getElementById('ai-context-cart');
  3065→    if (!cartDiv) return;
  3066→    if (aiChat.contextThreads.length === 0) { cartDiv.style.display = 'none'; return; }
  3067→    cartDiv.style.display = 'block';
  3068→    const totalEmails = aiChat.contextThreads.reduce((sum, t) => sum + (t.email_count || 0), 0);
  3069→    cartDiv.innerHTML = `
  3070→

  3071→            Context: ${aiChat.contextThreads.length} thread${aiChat.contextThreads.length > 1 ? 's' : ''}, ~${totalEmails} emails
  3072→            <button onclick="clearContextCart()" style="background:#7f1d1d; color:#fca5a5; border:none; border-radius:3px; padding:1px 6px; font-size:0.65em; cursor:pointer;">Clear All</button>
  3073→

  3074→

  3075→            ${aiChat.contextThreads.map(t => `
  3076→

  3077→                    ${escapeHtml(t.subject.substring(0, 35))}
  3078→                    <button onclick="removeThreadFromContext('${t.thread_id}')" style="background:none; border:none; color:#f87171; cursor:pointer; font-size:1.1em; padding:0 2px;">\u00D7</button>
  3079→

  3080→            `).join('')}
  3081→

  3082→    `;
  3083→}
  3084→
  3085→// --- Contact Memory ---
  3086→async function loadContactMemories(contactId) {
  3087→    const memDiv = document.getElementById('ai-context-memories');
  3088→    if (!memDiv) return;
  3089→    try {
  3090→        const data = await apiCall('ai_contact_memories', { contact_id: contactId });
  3091→        aiChat.contactMemories = data.memories || [];
  3092→        renderContactMemories();
  3093→    } catch (e) {
  3094→        aiChat.contactMemories = [];
  3095→    }
  3096→}
  3097→
  3098→function renderContactMemories() {
  3099→    const memDiv = document.getElementById('ai-context-memories');
  3100→    if (!memDiv) return;
  3101→    if (aiChat.contactMemories.length === 0 && !aiChat.contextContact) { memDiv.style.display = 'none'; return; }
  3102→    memDiv.style.display = 'block';
  3103→    memDiv.innerHTML = `
  3104→

  3105→            Memories (${aiChat.contactMemories.length})
  3106→            <button onclick="promptAddMemory()" style="background:#2d2d4e; color:#94a3b8; border:1px solid #3d3d5c; border-radius:3px; padding:1px 6px; font-size:0.63em; cursor:pointer;">+ Add</button>
  3107→

  3108→

  3109→            ${aiChat.contactMemories.map(m => `
  3110→
  3111→                    ${m.source === 'auto' ? '\u{1F916}' : '\u{1F4CC}'} ${escapeHtml(m.fact.substring(0, 40))}${m.fact.length > 40 ? '...' : ''}
  3112→                    <button onclick="deleteContactMemory('${m.id}')" style="background:none;border:none;color:#f87171;cursor:pointer;font-size:1em;padding:0;line-height:1;">\u00D7</button>
  3113→
  3114→            `).join('')}
  3115→

  3116→    `;
  3117→}
  3118→
  3119→function promptAddMemory() {
  3120→    const fact = prompt('Enter a fact about this contact:');
  3121→    if (!fact || !aiChat.contextContact) return;
  3122→    fetch('api.php?action=ai_contact_memory_add', {
  3123→        method: 'POST', headers: { 'Content-Type': 'application/json' },
  3124→        body: JSON.stringify({ contact_id: aiChat.contextContact.id, fact })
  3125→    }).then(r => r.json()).then(data => {
  3126→        if (data.success && data.memory) {
  3127→            aiChat.contactMemories.push(data.memory);
  3128→            renderContactMemories();
  3129→        }
  3130→    });
  3131→}
  3132→
  3133→function deleteContactMemory(memoryId) {
  3134→    if (!aiChat.contextContact) return;
  3135→    fetch('api.php?action=ai_contact_memory_delete', {
  3136→        method: 'POST', headers: { 'Content-Type': 'application/json' },
  3137→        body: JSON.stringify({ contact_id: aiChat.contextContact.id, memory_id: memoryId })
  3138→    }).then(r => r.json()).then(data => {
  3139→        if (data.success) {
  3140→            aiChat.contactMemories = aiChat.contactMemories.filter(m => m.id !== memoryId);
  3141→            renderContactMemories();
  3142→        }
  3143→    });
  3144→}
  3145→
  3146→// --- Session Management ---
  3147→async function loadChatSessions() {
  3148→    const overlay = document.getElementById('ai-session-overlay');
  3149→    if (!overlay) return;
  3150→    overlay.style.display = 'flex';
  3151→    overlay.innerHTML = '
Loading sessions...
';
  3152→    try {
  3153→        const data = await apiCall('ai_chat_sessions', { limit: 20 });
  3154→        const sessions = data.sessions || [];
  3155→        if (sessions.length === 0) {
  3156→            overlay.innerHTML = '

No saved sessions
<button onclick="closeSessionOverlay()" style="margin-top:10px;background:#2d2d4e;color:#94a3b8;border:none;border-radius:6px;padding:5px 12px;font-size:0.75em;cursor:pointer;">Close</button>
';
  3157→            return;
  3158→        }
  3159→        const agentIcons = {}; Object.values(AI_AGENTS).forEach(a => agentIcons[a.id] = a.icon);
  3160→        overlay.innerHTML = `
  3161→

  3162→                Chat History
  3163→                <button onclick="closeSessionOverlay()" style="background:none;border:none;color:#94a3b8;cursor:pointer;font-size:1.1em;">\u2715</button>
  3164→

  3165→

  3166→                ${sessions.map(s => `
  3167→

  3169→

  3170→

  3171→                                ${agentIcons[s.agent] || '\u{1F4AC}'} ${escapeHtml(s.title)}
  3172→

  3173→
${s.message_count} messages ${s.contact_name ? '\u00B7 ' + escapeHtml(s.contact_name) : ''}

  3174→

  3175→

  3176→                            ${s.updated_at ? s.updated_at.substring(0,10) : ''}
  3177→                            <button onclick="event.stopPropagation();deleteChatSession('${s.session_id}')" style="background:none;border:none;color:#f87171;cursor:pointer;font-size:0.8em;" title="Delete">\u{1F5D1}</button>
  3178→

  3179→

  3180→                `).join('')}
  3181→

  3182→        `;
  3183→    } catch (err) {
  3184→        overlay.innerHTML = '
Error: ' + escapeHtml(err.message) + '
';
  3185→    }
  3186→}
  3187→
  3188→function closeSessionOverlay() {
  3189→    const overlay = document.getElementById('ai-session-overlay');
  3190→    if (overlay) overlay.style.display = 'none';
  3191→}
  3192→
  3193→async function loadChatSession(sessionId) {
  3194→    closeSessionOverlay();
  3195→    try {
  3196→        const data = await apiCall('ai_chat_session', { session_id: sessionId });
  3197→        const session = data.session;
  3198→        if (!session) return;
  3199→
  3200→        // Restore state
  3201→        aiChat.sessionId = session.session_id;
  3202→        aiChat.agent = session.agent || 'general';
  3203→        aiChat.contactId = session.contact_id || null;
  3204→        aiChat.contactName = session.contact_name || null;
  3205→        aiChat.contextThreads = (session.context_thread_ids || []).map(tid => ({ thread_id: tid, subject: '', email_count: 0, contact_name: session.contact_name || '' }));
  3206→        aiChat.history = [];
  3207→
  3208→        // Restore provider/model
  3209→        if (session.provider) {
  3210→            const ps = document.getElementById('ai-chat-provider');
  3211→            if (ps) { ps.value = session.provider; updateAiModelDropdown(); }
  3212→        }
  3213→        if (session.model) {
  3214→            const ms = document.getElementById('ai-chat-model');
  3215→            if (ms) ms.value = session.model;
  3216→        }
  3217→
  3218→        // Restore agent button
  3219→        selectAgent(aiChat.agent);
  3220→
  3221→        // Render messages
  3222→        const msgContainer = document.getElementById('ai-chat-messages');
  3223→        msgContainer.innerHTML = '';
  3224→        (session.messages || []).forEach(m => {
  3225→            if (m.role === 'user' || m.role === 'assistant') {
  3226→                appendAiChatMessage(m.role, m.content, m.model || null);
  3227→                aiChat.history.push({ role: m.role, content: m.content });
  3228→            }
  3229→        });
  3230→
  3231→        updateContextCart();
  3232→        updateAiChatContextLabel();
  3233→        renderQuickPrompts();
  3234→        if (!aiChat.isOpen) toggleAiChat();
  3235→    } catch (err) {
  3236→        console.error('Failed to load session:', err);
  3237→    }
  3238→}
  3239→
  3240→function startNewSession() {
  3241→    aiChat.sessionId = null;
  3242→    aiChat.history = [];
  3243→    aiChat.contextThreads = [];
  3244→    aiChat.contextContact = null;
  3245→    aiChat.contactMemories = [];
  3246→    aiChat._availableThreads = [];
  3247→    aiChat.threadId = null;
  3248→    aiChat.contactId = null;
  3249→    aiChat.emailId = null;
  3250→    aiChat.agent = 'general';
  3251→    selectAgent('general');
  3252→    clearAiChat();
  3253→    const picker = document.getElementById('ai-context-thread-picker');
  3254→    if (picker) { picker.style.display = 'none'; picker.innerHTML = ''; }
  3255→    const cart = document.getElementById('ai-context-cart');
  3256→    if (cart) cart.style.display = 'none';
  3257→    const memDiv = document.getElementById('ai-context-memories');
  3258→    if (memDiv) memDiv.style.display = 'none';
  3259→    updateAiChatContextLabel();
  3260→    renderQuickPrompts();
  3261→}
  3262→
  3263→async function deleteChatSession(sessionId) {
  3264→    try {
  3265→        await fetch('api.php?action=ai_chat_session_delete', {
  3266→            method: 'POST', headers: { 'Content-Type': 'application/json' },
  3267→            body: JSON.stringify({ session_id: sessionId })
  3268→        });
  3269→        loadChatSessions(); // refresh list
  3270→    } catch (e) { console.error(e); }
  3271→}
  3272→
  3273→// --- Send AI Chat (enhanced) ---
  3274→async function sendAiChat() {
  3275→    const input = document.getElementById('ai-chat-input');
  3276→    const message = input.value.trim();
  3277→    if (!message) return;
  3278→    const model = document.getElementById('ai-chat-model').value;
  3279→    const providerSelect = document.getElementById('ai-chat-provider');
  3280→    const provider = providerSelect ? providerSelect.value : 'openai';
  3281→    const sendBtn = document.getElementById('ai-chat-send-btn');
  3282→
  3283→    appendAiChatMessage('user', message);
  3284→    input.value = '';
  3285→    sendBtn.disabled = true; sendBtn.style.opacity = '0.5';
  3286→    aiChat.history.push({ role: 'user', content: message });
  3287→    const typingId = appendAiChatTyping();
  3288→
  3289→    try {
  3290→        const payload = {
  3291→            message, model, provider,
  3292→            mode: aiChat.mode,
  3293→            agent: aiChat.agent !== 'general' ? aiChat.agent : null,
  3294→            history: aiChat.history.slice(-10),
  3295→            session_id: aiChat.sessionId,
  3296→        };
  3297→
  3298→        // Multi-thread context
  3299→        if (aiChat.contextThreads.length > 0) {
  3300→            payload.thread_ids = aiChat.contextThreads.map(t => t.thread_id);
  3301→        } else if (aiChat.threadId) {
  3302→            payload.thread_id = aiChat.threadId;
  3303→        }
  3304→
  3305→        if (aiChat.contactId) payload.contact_id = aiChat.contactId;
  3306→        if (aiChat.emailId) payload.email_id = aiChat.emailId;
  3307→
  3308→        const resp = await fetch('api.php?action=ai_chat', {
  3309→            method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload)
  3310→        });
  3311→        const data = await resp.json();
  3312→        removeAiChatTyping(typingId);
  3313→
  3314→        if (data.success && data.answer) {
  3315→            // Store session ID from server
  3316→            if (data.session_id) aiChat.sessionId = data.session_id;
  3317→
  3318→            aiChat.history.push({ role: 'assistant', content: data.answer });
  3319→            appendAiChatMessage('assistant', data.answer, data.model);
  3320→
  3321→            // Show context stats if truncated
  3322→            if (data.context_stats && data.context_stats.truncated) {
  3323→                appendAiChatMessage('error', 'Note: Context was trimmed to fit model token limit (' + data.context_stats.estimated_tokens + ' tokens used)');
  3324→            }
  3325→
  3326→            // Show extracted memories
  3327→            if (data.extracted_memories && data.extracted_memories.length > 0) {
  3328→                data.extracted_memories.forEach(m => aiChat.contactMemories.push({ id: 'new_' + Date.now(), fact: m, source: 'auto' }));
  3329→                renderContactMemories();
  3330→            }
  3331→        } else {
  3332→            appendAiChatMessage('error', (data.message || data.error || 'Unknown error'));
  3333→        }
  3334→    } catch (err) {
  3335→        removeAiChatTyping(typingId);
  3336→        appendAiChatMessage('error', 'Network error: ' + err.message);
  3337→    } finally {
  3338→        sendBtn.disabled = false; sendBtn.style.opacity = '1';
  3339→    }
  3340→}
  3341→
  3342→function appendAiChatMessage(role, content, model) {
  3343→    const container = document.getElementById('ai-chat-messages');
  3344→    const isUser = role === 'user', isError = role === 'error';
  3345→    const wrapper = document.createElement('div');
  3346→    wrapper.style.cssText = `display:flex;flex-direction:column;align-items:${isUser ? 'flex-end' : 'flex-start'};gap:4px;`;
  3347→    const bubble = document.createElement('div');
  3348→    bubble.style.cssText = `max-width:88%;padding:10px 13px;border-radius:${isUser ? '14px 14px 4px 14px' : '14px 14px 14px 4px'};font-size:0.82em;line-height:1.5;word-break:break-word;background:${isUser ? 'linear-gradient(135deg,#6366f1,#8b5cf6)' : isError ? '#3d1515' : '#252540'};color:${isUser ? '#fff' : isError ? '#fca5a5' : '#e2e8f0'};${isError ? 'border:1px solid #7f1d1d;' : ''}`;
  3349→    if (!isUser && !isError) bubble.innerHTML = renderAiMarkdown(content);
  3350→    else bubble.textContent = content;
  3351→    wrapper.appendChild(bubble);
  3352→
  3353→    if (!isUser && !isError) {
  3354→        const meta = document.createElement('div');
  3355→        meta.style.cssText = 'display:flex;gap:6px;align-items:center;';
  3356→        if (model) {
  3357→            const badge = document.createElement('span');
  3358→            badge.style.cssText = 'font-size:0.65em;color:#4a4a6a;background:#1e1e2e;padding:2px 6px;border-radius:4px;';
  3359→            badge.textContent = model; meta.appendChild(badge);
  3360→        }
  3361→        const copyBtn = document.createElement('button');
  3362→        copyBtn.style.cssText = 'font-size:0.65em;color:#6366f1;background:none;border:none;cursor:pointer;padding:2px 4px;';
  3363→        copyBtn.textContent = '📋 Copy';
  3364→        copyBtn.onclick = () => navigator.clipboard.writeText(content).then(() => { copyBtn.textContent = '✓ Copied!'; setTimeout(() => copyBtn.textContent = '📋 Copy', 2000); });
  3365→        meta.appendChild(copyBtn);
  3366→        wrapper.appendChild(meta);
  3367→    }
  3368→    container.appendChild(wrapper);
  3369→    container.scrollTop = container.scrollHeight;
  3370→    return wrapper;
  3371→}
  3372→
  3373→function appendAiChatTyping() {
  3374→    const container = document.getElementById('ai-chat-messages');
  3375→    const id = 'typing-' + Date.now();
  3376→    const el = document.createElement('div');
  3377→    el.id = id;
  3378→    el.innerHTML = `
●●●
`;
  3379→    if (!document.getElementById('ai-chat-style')) {
  3380→        const s = document.createElement('style'); s.id = 'ai-chat-style';
  3381→        s.textContent = '@keyframes aiBounce{0%,80%,100%{opacity:0.3;transform:translateY(0)}40%{opacity:1;transform:translateY(-4px)}}';
  3382→        document.head.appendChild(s);
  3383→    }
  3384→    container.appendChild(el); container.scrollTop = container.scrollHeight;
  3385→    return id;
  3386→}
  3387→
  3388→function removeAiChatTyping(id) { const el = document.getElementById(id); if (el) el.remove(); }
  3389→
  3390→function renderAiMarkdown(text) {
  3391→    return text
  3392→        .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
  3393→        .replace(/```([\s\S]*?)```/g, '
$1
')
  3394→        .replace(/`([^`]+)`/g, '$1')
  3395→        .replace(/\*\*([^*]+)\*\*/g, '$1').replace(/\*([^*]+)\*/g, '$1')
  3396→        .replace(/^### (.+)$/gm, '
$1
')
  3397→        .replace(/^## (.+)$/gm, '
$1
')
  3398→        .replace(/^# (.+)$/gm, '
$1
')
  3399→        .replace(/^[-*] (.+)$/gm, '
• $1
')
  3400→        .replace(/\n/g, '
');
  3401→}
  3402→
  3403→function clearAiChat() {
  3404→    aiChat.history = [];
  3405→    document.getElementById('ai-chat-messages').innerHTML = `<div style="text-align:center;color:#4a4a6a;font-size:0.8em;padding:20px 0;"><div style="font-size:2em;margin-bottom:8px;">🤖</div><div>Chat cleared. Ask me anything!</div></div>`;
  3406→}
  3407→
  3408→// Draggable panel + initialization
  3409→document.addEventListener('DOMContentLoaded', () => {
  3410→    initAiProviderDropdown();
  3411→    const header = document.getElementById('ai-chat-header');
  3412→    const panel = document.getElementById('ai-chat-panel');
  3413→    if (!header || !panel) return;
  3414→    header.addEventListener('mousedown', e => {
  3415→        if (e.target.tagName === 'BUTTON') return;
  3416→        aiChat.isDragging = true;
  3417→        const rect = panel.getBoundingClientRect();
  3418→        aiChat.dragOffsetX = e.clientX - rect.left;
  3419→        aiChat.dragOffsetY = e.clientY - rect.top;
  3420→        panel.style.transition = 'none';
  3421→    });
  3422→    document.addEventListener('mousemove', e => {
  3423→        if (!aiChat.isDragging) return;
  3424→        panel.style.right = 'auto'; panel.style.bottom = 'auto';
  3425→        panel.style.left = Math.max(0, Math.min(e.clientX - aiChat.dragOffsetX, window.innerWidth - panel.offsetWidth)) + 'px';
  3426→        panel.style.top = Math.max(0, Math.min(e.clientY - aiChat.dragOffsetY, window.innerHeight - panel.offsetHeight)) + 'px';
  3427→    });
  3428→    document.addEventListener('mouseup', () => { aiChat.isDragging = false; });
  3429→});

msgEl.style.color = '#991b1b';
            msgEl.style.border = '1px solid #fecaca';
            break;
        default:
            msgEl.style.background = '#eff6ff';
            msgEl.style.color = '#1e40af';
            msgEl.style.border = '1px solid #bfdbfe';
    }

    // Auto-hide after 5 seconds for non-error messages
    if (type !== 'error') {
        setTimeout(() => {
            msgEl.style.display = 'none';
        }, 5000);
    }
}

// ============================================
// AI CHAT PANEL
// ============================================

const aiChat = {
    isOpen: false,
    mode: 'general',
    threadId: null,
    contactId: null,
    emailId: null,
    threadSubject: null,
    contactName: null,
    history: [],
    isDragging: false,
    dragOffsetX: 0,
    dragOffsetY: 0,
    // Context builder
    contextThreads: [],      // [{thread_id, subject, email_count, contact_name}]
    contextContact: null,    // {id, name, email}
    _availableThreads: [],   // temp storage for thread picker
    // Session persistence
    sessionId: null,
    // Agent
    agent: 'general',
    // Contact memories
    contactMemories: [],
};

// Provider/Model config — synced with Chrome extension contentScript.js
const AI_PROVIDER_MODELS = {
    claude: [
        { id: 'claude-sonnet-4-5-20250929', name: 'Sonnet 4.5' },
        { id: 'claude-opus-4-5-20251101', name: 'Opus 4.5 (Best)' },
        { id: 'claude-haiku-4-5-20251001', name: 'Haiku 4.5 (Fast)' },
    ],
    openai: [
        { id: 'gpt-5.2', name: 'GPT-5.2 (Latest)' },
        { id: 'gpt-5.2-pro', name: 'GPT-5.2 Pro (Best)' },
        { id: 'gpt-5-mini', name: 'GPT-5 Mini' },
        { id: 'gpt-5-nano', name: 'GPT-5 Nano (Fast)' },
    ],
    gemini: [
        { id: 'gemini-2.5-flash', name: 'Gemini 2.5 Flash' },
        { id: 'gemini-3-pro', name: 'Gemini 3 Pro' },
        { id: 'gemini-2.5-flash-lite', name: 'Flash-Lite (Fast)' },
    ],
    grok: [
        { id: 'grok-4', name: 'Grok 4 (Latest)' },
        { id: 'grok-3', name: 'Grok 3' },
        { id: 'grok-3-mini', name: 'Grok 3 Mini (Fast)' },
    ],
    deepseek: [
        { id: 'deepseek-chat', name: 'DeepSeek V3.2' },
        { id: 'deepseek-reasoner', name: 'Reasoner (Thinking)' },
    ],
    qwen: [
        { id: 'qwen-max-latest', name: 'Qwen Max' },
        { id: 'qwen-turbo-latest', name: 'Qwen Turbo (Fast)' },
    ],
    mistral: [
        { id: 'mistral-large-latest', name: 'Mistral Large' },
        { id: 'mistral-small-latest', name: 'Mistral Small (Fast)' },
    ],
    cohere: [
        { id: 'command-a-03-2025', name: 'Command A' },
        { id: 'command-r7b-12-2024', name: 'Command R7B (Fast)' },
    ],
};

const AI_PROVIDER_NAMES = {
    claude: 'Claude', openai: 'OpenAI', gemini: 'Gemini', grok: 'Grok',
    deepseek: 'DeepSeek', qwen: 'Qwen', mistral: 'Mistral', cohere: 'Cohere',
};

// Agent presets
const AI_AGENTS = {
    general:             { id: 'general',             name: 'General',    icon: '\u{1F4AC}', preferredProvider: null,    preferredModel: null, quickPrompts: ['What are my most at-risk clients?', 'Which threads need urgent attention?', 'Summarize recent activity'] },
    quote_analyst:       { id: 'quote_analyst',       name: 'Quotes',     icon: '\u{1F4B0}', preferredProvider: 'claude', preferredModel: 'claude-sonnet-4-5-20250929', quickPrompts: ['How much have we quoted across these threads?', 'What is the price sensitivity?', 'Suggest optimal pricing', 'What discounts were given?'] },
    deception_detector:  { id: 'deception_detector',  name: 'Deception',  icon: '\u{1F50D}', preferredProvider: 'claude', preferredModel: 'claude-opus-4-5-20251101', quickPrompts: ['What is the deception level?', 'Any contradictions between statements?', 'Red flags or pressure tactics?', 'Rate honesty 1-10 with evidence'] },
    contact_profiler:    { id: 'contact_profiler',    name: 'Profiler',   icon: '\u{1F464}', preferredProvider: 'claude', preferredModel: 'claude-sonnet-4-5-20250929', quickPrompts: ['Build a complete profile', 'Communication style analysis', 'Decision-making patterns', 'How should I approach them?'] },
    email_composer:      { id: 'email_composer',      name: 'Composer',   icon: '\u{270D}\u{FE0F}', preferredProvider: 'openai', preferredModel: 'gpt-5.2', quickPrompts: ['Write a professional reply', 'Write a firm but polite response', 'Write a follow-up email', 'Write a brief acknowledgment'] },
};

const AI_QUICK_PROMPTS = {
    general: ['What are my most at-risk clients?', 'Which threads need urgent attention?', 'Summarize recent activity'],
    thread: ['Summarize this thread', 'What are the key risks?', 'What action should I take next?', 'Is this client trustworthy?', 'What are they really asking for?'],
    contact: ['What is their communication style?', 'Are there any red flags?', 'How should I approach them?', 'What opportunities exist with them?'],
    quote: ['How much should I quote for this project?', 'What is the scope of work?', 'What are the risks?', 'Suggest a project timeline'],
    reply: ['Write a professional reply', 'Write a firm but polite response', 'Write a follow-up email', 'Write a brief acknowledgment'],
    multithread: ['Summarize all conversations', 'How much have we quoted?', 'What is the deception level?', 'Build a complete profile', 'What patterns emerge across these threads?'],
};

function toggleAiChat() {
    const panel = document.getElementById('ai-chat-panel');
    aiChat.isOpen = !aiChat.isOpen;
    panel.style.display = aiChat.isOpen ? 'flex' : 'none';
    if (aiChat.isOpen) { document.getElementById('ai-chat-input').focus(); renderQuickPrompts(); }
}

function openAiChatForThread(threadId, subject, contactId) {
    aiChat.threadId = threadId; aiChat.contactId = contactId || null;
    aiChat.threadSubject = subject || threadId; aiChat.emailId = null;
    setAiChatMode('thread', document.querySelector('.ai-mode-btn[data-mode="thread"]'));
    if (!aiChat.isOpen) toggleAiChat(); else document.getElementById('ai-chat-input').focus();
}

function openAiChatForContact(contactId, contactName) {
    aiChat.contactId = contactId; aiChat.contactName = contactName || 'Contact';
    aiChat.threadId = null; aiChat.emailId = null;
    setAiChatMode('contact', document.querySelector('.ai-mode-btn[data-mode="contact"]'));
    if (!aiChat.isOpen) toggleAiChat(); else document.getElementById('ai-chat-input').focus();
}

function openAiChatForReply(threadId, emailId, subject) {
    aiChat.threadId = threadId; aiChat.emailId = emailId; aiChat.threadSubject = subject || threadId;
    setAiChatMode('reply', document.querySelector('.ai-mode-btn[data-mode="reply"]'));
    if (!aiChat.isOpen) toggleAiChat(); else document.getElementById('ai-chat-input').focus();
}

function setAiChatMode(mode, btn) {
    aiChat.mode = mode;
    document.querySelectorAll('.ai-mode-btn').forEach(b => { b.style.background = '#2d2d4e'; b.style.color = '#94a3b8'; });
    if (btn) { btn.style.background = '#6366f1'; btn.style.color = '#fff'; }
    updateAiChatContextLabel(); renderQuickPrompts();
}

function updateAiChatContextLabel() {
    const label = document.getElementById('ai-chat-context-label');
    if (!label) return;

    // Multi-thread context takes priority
    if (aiChat.contextThreads.length > 0) {
        const totalEmails = aiChat.contextThreads.reduce((sum, t) => sum + (t.email_count || 0), 0);
        const contactLabel = aiChat.contextContact ? aiChat.contextContact.name : '';
        label.textContent = `${aiChat.contextThreads.length} threads, ~${totalEmails} emails` + (contactLabel ? ` from ${contactLabel}` : '');
        label.style.color = '#10b981';
        return;
    }

    const modeLabels = { general: 'General Chat', thread: 'Thread Analysis', contact: 'Contact Intelligence', quote: 'Quote Assistant', reply: 'Reply Composer' };
    let ctx = modeLabels[aiChat.mode] || 'General Chat';
    if (aiChat.mode === 'thread' && aiChat.threadSubject) ctx += ' \u00B7 ' + aiChat.threadSubject.substring(0, 30);
    if (aiChat.mode === 'contact' && aiChat.contactName) ctx += ' \u00B7 ' + aiChat.contactName;
    label.textContent = ctx;
    label.style.color = 'rgba(255,255,255,0.7)';
}

function renderQuickPrompts() {
    const container = document.getElementById('ai-quick-prompts');
    if (!container) return;
    // Use agent prompts if agent selected, then multithread if context loaded, then mode
    let prompts;
    const agent = AI_AGENTS[aiChat.agent];
    if (agent && agent.id !== 'general') {
        prompts = agent.quickPrompts;
    } else if (aiChat.contextThreads.length > 0) {
        prompts = AI_QUICK_PROMPTS.multithread;
    } else {
        prompts = AI_QUICK_PROMPTS[aiChat.mode] || AI_QUICK_PROMPTS.general;
    }
    container.innerHTML = prompts.map(p =>
        `<button onclick="sendAiChatPrompt(${JSON.stringify(p)})" style="background:#2d2d4e;color:#94a3b8;border:1px solid #3d3d5c;border-radius:12px;padding:3px 10px;font-size:0.68em;cursor:pointer;white-space:nowrap;" onmouseover="this.style.background='#6366f1';this.style.color='#fff'" onmouseout="this.style.background='#2d2d4e';this.style.color='#94a3b8'">${escapeHtml(p)}</button>`
    ).join('');
}

function sendAiChatPrompt(prompt) {
    document.getElementById('ai-chat-input').value = prompt;
    sendAiChat();
}

function handleAiChatKey(e) {
    if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendAiChat(); }
}

// --- Provider/Model Dropdown ---
function initAiProviderDropdown() {
    const providerSelect = document.getElementById('ai-chat-provider');
    if (!providerSelect) return;
    providerSelect.innerHTML = '';
    Object.keys(AI_PROVIDER_MODELS).forEach(code => {
        const opt = document.createElement('option');
        opt.value = code;
        opt.textContent = AI_PROVIDER_NAMES[code] || code;
        providerSelect.appendChild(opt);
    });
    providerSelect.value = 'claude';
    updateAiModelDropdown();
}

function updateAiModelDropdown() {
    const providerSelect = document.getElementById('ai-chat-provider');
    const modelSelect = document.getElementById('ai-chat-model');
    if (!providerSelect || !modelSelect) return;
    const provider = providerSelect.value;
    const models = AI_PROVIDER_MODELS[provider] || [];
    modelSelect.innerHTML = '';
    models.forEach(m => {
        const opt = document.createElement('option');
        opt.value = m.id;
        opt.textContent = m.name;
        modelSelect.appendChild(opt);
    });
}

// --- Agent Selector ---
function selectAgent(agentId) {
    aiChat.agent = agentId;
    const agent = AI_AGENTS[agentId];
    // Highlight active agent button
    document.querySelectorAll('.ai-agent-btn').forEach(b => {
        b.style.background = b.dataset.agent === agentId ? '#6366f1' : '#2d2d4e';
        b.style.color = b.dataset.agent === agentId ? '#fff' : '#94a3b8';
    });
    // Set preferred provider/model
    if (agent && agent.preferredProvider) {
        const providerSelect = document.getElementById('ai-chat-provider');
        if (providerSelect) { providerSelect.value = agent.preferredProvider; updateAiModelDropdown(); }
        if (agent.preferredModel) {
            const modelSelect = document.getElementById('ai-chat-model');
            if (modelSelect) modelSelect.value = agent.preferredModel;
        }
    }
    updateAiChatContextLabel();
    renderQuickPrompts();
}

// --- Context Builder ---
function toggleContextBuilder() {
    const area = document.getElementById('ai-context-search-area');
    const icon = document.getElementById('ai-context-toggle-icon');
    if (!area) return;
    const isVisible = area.style.display !== 'none';
    area.style.display = isVisible ? 'none' : 'block';
    icon.textContent = isVisible ? '+' : '-';
}

async function searchContextContacts() {
    const query = document.getElementById('ai-context-search-input').value.trim();
    if (!query) return;
    // Strip wildcard chars — backend already does partial matching
    const cleanQuery = query.replace(/[*%]/g, '').trim();
    if (!cleanQuery) return;

    const resultsDiv = document.getElementById('ai-context-search-results');
    resultsDiv.innerHTML = '
Searching contacts & threads...
';

    try {
        // Search both contacts and threads in parallel
        const [contactsData, threadsData] = await Promise.all([
            apiCall('contacts', { search: cleanQuery, limit: 10 }),
            apiCall('threads', { search: cleanQuery, limit: 10 }),
        ]);

        let html = '';
        const contacts = contactsData.contacts || [];
        const threads = threadsData.threads || [];

        if (contacts.length === 0 && threads.length === 0) {
            resultsDiv.innerHTML = '
No contacts or threads found for "' + escapeHtml(cleanQuery) + '"
';
            return;
        }

        // Contacts section
        if (contacts.length > 0) {
            html += '
CONTACTS
';
            html += contacts.map(c => {
                const name = c.name || c.email;
                return `

                    \u{1F464} ${escapeHtml(name)} ${escapeHtml(c.email)}
                    show threads \u25B6

`;
            }).join('');
        }

        // Threads section — directly addable
        if (threads.length > 0) {
            html += '
THREADS
';
            html += threads.map(t => {
                const isAdded = aiChat.contextThreads.some(ct => ct.thread_id === t.thread_id);
                const subject = t.subject || '(No Subject)';
                return `

                        \u{1F4E7} ${escapeHtml(subject.substring(0, 45))}
                        (${t.email_count || '?'})

                    <button onclick="event.stopPropagation();addThreadToContext('${t.thread_id}', \`${escapeHtml(subject.substring(0,50))}\`, ${t.email_count||0}, '')"
                            style="background:${isAdded ? '#10b981' : '#6366f1'}; color:#fff; border:none; border-radius:4px; padding:2px 8px; font-size:0.85em; cursor:pointer; margin-left:4px; white-space:nowrap;"
                            ${isAdded ? 'disabled' : ''}>
                        ${isAdded ? '\u2713 Added' : '+ Add'}
                    </button>

`;
            }).join('');
        }

        resultsDiv.innerHTML = html;
    } catch (err) {
        resultsDiv.innerHTML = '
Error: ' + escapeHtml(err.message) + '
';
    }
}

async function selectContextContact(contactId, name, email) {
    aiChat.contextContact = { id: contactId, name, email };
    aiChat.contactId = contactId;
    aiChat.contactName = name;
    document.getElementById('ai-context-search-results').innerHTML = '';

    const picker = document.getElementById('ai-context-thread-picker');
    picker.style.display = 'block';
    picker.innerHTML = '
Loading threads...
';

    // Load contact memories
    loadContactMemories(contactId);

    try {
        const data = await apiCall('threads', { contact: contactId, limit: 50 });
        if (!data.threads || data.threads.length === 0) {
            picker.innerHTML = '
No threads for this contact
';
            return;
        }
        aiChat._availableThreads = data.threads;
        let html = `

            ${escapeHtml(name)} - ${data.threads.length} threads
            <button onclick="addAllContactThreads()" style="background:#10b981; color:#fff; border:none; border-radius:4px; padding:2px 8px; font-size:0.68em; cursor:pointer;">+ Add All</button>

`;
        html += data.threads.map(t => {
            const isAdded = aiChat.contextThreads.some(ct => ct.thread_id === t.thread_id);
            return `

                    ${escapeHtml((t.subject || '(No Subject)').substring(0, 40))}
                    (${t.email_count || '?'} emails)

                <button onclick="addThreadToContext('${t.thread_id}', \`${escapeHtml((t.subject||'').substring(0,50))}\`, ${t.email_count||0}, '${escapeHtml(name)}')"
                        style="background:${isAdded ? '#4b5563' : '#6366f1'}; color:#fff; border:none; border-radius:3px; padding:1px 6px; font-size:0.95em; cursor:pointer; margin-left:4px;"
                        ${isAdded ? 'disabled' : ''}>
                    ${isAdded ? '\u2713' : '+'}
                </button>

`;
        }).join('');
        picker.innerHTML = html;
    } catch (err) {
        picker.innerHTML = '
Error: ' + escapeHtml(err.message) + '
';
    }
}

function addThreadToContext(threadId, subject, emailCount, contactName) {
    if (aiChat.contextThreads.some(t => t.thread_id === threadId)) return;
    aiChat.contextThreads.push({ thread_id: threadId, subject, email_count: emailCount, contact_name: contactName });
    updateContextCart();
    updateAiChatContextLabel();
    renderQuickPrompts();
    // Refresh picker to update button states
    if (aiChat.contextContact) selectContextContact(aiChat.contextContact.id, aiChat.contextContact.name, aiChat.contextContact.email);
}

function removeThreadFromContext(threadId) {
    aiChat.contextThreads = aiChat.contextThreads.filter(t => t.thread_id !== threadId);
    updateContextCart();
    updateAiChatContextLabel();
    renderQuickPrompts();
}

function addAllContactThreads() {
    const threads = aiChat._availableThreads || [];
    threads.forEach(t => {
        if (!aiChat.contextThreads.some(ct => ct.thread_id === t.thread_id)) {
            aiChat.contextThreads.push({
                thread_id: t.thread_id, subject: t.subject || '(No Subject)',
                email_count: t.email_count || 0, contact_name: aiChat.contextContact?.name || 'Unknown'
            });
        }
    });
    updateContextCart();
    updateAiChatContextLabel();
    renderQuickPrompts();
    if (aiChat.contextContact) selectContextContact(aiChat.contextContact.id, aiChat.contextContact.name, aiChat.contextContact.email);
}

function clearContextCart() {
    aiChat.contextThreads = [];
    aiChat.contextContact = null;
    aiChat.contactMemories = [];
    aiChat._availableThreads = [];
    updateContextCart();
    updateAiChatContextLabel();
    renderQuickPrompts();
    const picker = document.getElementById('ai-context-thread-picker');
    if (picker) picker.style.display = 'none';
    const memDiv = document.getElementById('ai-context-memories');
    if (memDiv) memDiv.style.display = 'none';
}

function updateContextCart() {
    const cartDiv = document.getElementById('ai-context-cart');
    if (!cartDiv) return;
    if (aiChat.contextThreads.length === 0) { cartDiv.style.display = 'none'; return; }
    cartDiv.style.display = 'block';
    const totalEmails = aiChat.contextThreads.reduce((sum, t) => sum + (t.email_count || 0), 0);
    cartDiv.innerHTML = `

            Context: ${aiChat.contextThreads.length} thread${aiChat.contextThreads.length > 1 ? 's' : ''}, ~${totalEmails} emails
            <button onclick="clearContextCart()" style="background:#7f1d1d; color:#fca5a5; border:none; border-radius:3px; padding:1px 6px; font-size:0.65em; cursor:pointer;">Clear All</button>

            ${aiChat.contextThreads.map(t => `

                    ${escapeHtml(t.subject.substring(0, 35))}
                    <button onclick="removeThreadFromContext('${t.thread_id}')" style="background:none; border:none; color:#f87171; cursor:pointer; font-size:1.1em; padding:0 2px;">\u00D7</button>

            `).join('')}

    `;
}

// --- Contact Memory ---
async function loadContactMemories(contactId) {
    const memDiv = document.getElementById('ai-context-memories');
    if (!memDiv) return;
    try {
        const data = await apiCall('ai_contact_memories', { contact_id: contactId });
        aiChat.contactMemories = data.memories || [];
        renderContactMemories();
    } catch (e) {
        aiChat.contactMemories = [];
    }
}

function renderContactMemories() {
    const memDiv = document.getElementById('ai-context-memories');
    if (!memDiv) return;
    if (aiChat.contactMemories.length === 0 && !aiChat.contextContact) { memDiv.style.display = 'none'; return; }
    memDiv.style.display = 'block';
    memDiv.innerHTML = `

            Memories (${aiChat.contactMemories.length})
            <button onclick="promptAddMemory()" style="background:#2d2d4e; color:#94a3b8; border:1px solid #3d3d5c; border-radius:3px; padding:1px 6px; font-size:0.63em; cursor:pointer;">+ Add</button>

            ${aiChat.contactMemories.map(m => `

                    ${m.source === 'auto' ? '\u{1F916}' : '\u{1F4CC}'} ${escapeHtml(m.fact.substring(0, 40))}${m.fact.length > 40 ? '...' : ''}
                    <button onclick="deleteContactMemory('${m.id}')" style="background:none;border:none;color:#f87171;cursor:pointer;font-size:1em;padding:0;line-height:1;">\u00D7</button>

            `).join('')}

    `;
}

function promptAddMemory() {
    const fact = prompt('Enter a fact about this contact:');
    if (!fact || !aiChat.contextContact) return;
    fetch('api.php?action=ai_contact_memory_add', {
        method: 'POST', headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ contact_id: aiChat.contextContact.id, fact })
    }).then(r => r.json()).then(data => {
        if (data.success && data.memory) {
            aiChat.contactMemories.push(data.memory);
            renderContactMemories();
        }
    });
}

function deleteContactMemory(memoryId) {
    if (!aiChat.contextContact) return;
    fetch('api.php?action=ai_contact_memory_delete', {
        method: 'POST', headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ contact_id: aiChat.contextContact.id, memory_id: memoryId })
    }).then(r => r.json()).then(data => {
        if (data.success) {
            aiChat.contactMemories = aiChat.contactMemories.filter(m => m.id !== memoryId);
            renderContactMemories();
        }
    });
}

// --- Session Management ---
async function loadChatSessions() {
    const overlay = document.getElementById('ai-session-overlay');
    if (!overlay) return;
    overlay.style.display = 'flex';
    overlay.innerHTML = '
Loading sessions...
';
    try {
        const data = await apiCall('ai_chat_sessions', { limit: 20 });
        const sessions = data.sessions || [];
        if (sessions.length === 0) {
            overlay.innerHTML = '

No saved sessions
<button onclick="closeSessionOverlay()" style="margin-top:10px;background:#2d2d4e;color:#94a3b8;border:none;border-radius:6px;padding:5px 12px;font-size:0.75em;cursor:pointer;">Close</button>
';
            return;
        }
        const agentIcons = {}; Object.values(AI_AGENTS).forEach(a => agentIcons[a.id] = a.icon);
        overlay.innerHTML = `

                Chat History
                <button onclick="closeSessionOverlay()" style="background:none;border:none;color:#94a3b8;cursor:pointer;font-size:1.1em;">\u2715</button>

                ${sessions.map(s => `

                                ${agentIcons[s.agent] || '\u{1F4AC}'} ${escapeHtml(s.title)}

${s.message_count} messages ${s.contact_name ? '\u00B7 ' + escapeHtml(s.contact_name) : ''}

                            ${s.updated_at ? s.updated_at.substring(0,10) : ''}
                            <button onclick="event.stopPropagation();deleteChatSession('${s.session_id}')" style="background:none;border:none;color:#f87171;cursor:pointer;font-size:0.8em;" title="Delete">\u{1F5D1}</button>

                `).join('')}

        `;
    } catch (err) {
        overlay.innerHTML = '
Error: ' + escapeHtml(err.message) + '
';
    }
}

function closeSessionOverlay() {
    const overlay = document.getElementById('ai-session-overlay');
    if (overlay) overlay.style.display = 'none';
}

async function loadChatSession(sessionId) {
    closeSessionOverlay();
    try {
        const data = await apiCall('ai_chat_session', { session_id: sessionId });
        const session = data.session;
        if (!session) return;

        // Restore state
        aiChat.sessionId = session.session_id;
        aiChat.agent = session.agent || 'general';
        aiChat.contactId = session.contact_id || null;
        aiChat.contactName = session.contact_name || null;
        aiChat.contextThreads = (session.context_thread_ids || []).map(tid => ({ thread_id: tid, subject: '', email_count: 0, contact_name: session.contact_name || '' }));
        aiChat.history = [];

        // Restore provider/model
        if (session.provider) {
            const ps = document.getElementById('ai-chat-provider');
            if (ps) { ps.value = session.provider; updateAiModelDropdown(); }
        }
        if (session.model) {
            const ms = document.getElementById('ai-chat-model');
            if (ms) ms.value = session.model;
        }

        // Restore agent button
        selectAgent(aiChat.agent);

        // Render messages
        const msgContainer = document.getElementById('ai-chat-messages');
        msgContainer.innerHTML = '';
        (session.messages || []).forEach(m => {
            if (m.role === 'user' || m.role === 'assistant') {
                appendAiChatMessage(m.role, m.content, m.model || null);
                aiChat.history.push({ role: m.role, content: m.content });
            }
        });

        updateContextCart();
        updateAiChatContextLabel();
        renderQuickPrompts();
        if (!aiChat.isOpen) toggleAiChat();
    } catch (err) {
        console.error('Failed to load session:', err);
    }
}

function startNewSession() {
    aiChat.sessionId = null;
    aiChat.history = [];
    aiChat.contextThreads = [];
    aiChat.contextContact = null;
    aiChat.contactMemories = [];
    aiChat._availableThreads = [];
    aiChat.threadId = null;
    aiChat.contactId = null;
    aiChat.emailId = null;
    aiChat.agent = 'general';
    selectAgent('general');
    clearAiChat();
    const picker = document.getElementById('ai-context-thread-picker');
    if (picker) { picker.style.display = 'none'; picker.innerHTML = ''; }
    const cart = document.getElementById('ai-context-cart');
    if (cart) cart.style.display = 'none';
    const memDiv = document.getElementById('ai-context-memories');
    if (memDiv) memDiv.style.display = 'none';
    updateAiChatContextLabel();
    renderQuickPrompts();
}

async function deleteChatSession(sessionId) {
    try {
        await fetch('api.php?action=ai_chat_session_delete', {
            method: 'POST', headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ session_id: sessionId })
        });
        loadChatSessions(); // refresh list
    } catch (e) { console.error(e); }
}

// --- Send AI Chat (enhanced) ---
async function sendAiChat() {
    const input = document.getElementById('ai-chat-input');
    const message = input.value.trim();
    if (!message) return;
    const model = document.getElementById('ai-chat-model').value;
    const providerSelect = document.getElementById('ai-chat-provider');
    const provider = providerSelect ? providerSelect.value : 'openai';
    const sendBtn = document.getElementById('ai-chat-send-btn');

    appendAiChatMessage('user', message);
    input.value = '';
    sendBtn.disabled = true; sendBtn.style.opacity = '0.5';
    aiChat.history.push({ role: 'user', content: message });
    const typingId = appendAiChatTyping();

    try {
        const payload = {
            message, model, provider,
            mode: aiChat.mode,
            agent: aiChat.agent !== 'general' ? aiChat.agent : null,
            history: aiChat.history.slice(-10),
            session_id: aiChat.sessionId,
        };

        // Multi-thread context
        if (aiChat.contextThreads.length > 0) {
            payload.thread_ids = aiChat.contextThreads.map(t => t.thread_id);
        } else if (aiChat.threadId) {
            payload.thread_id = aiChat.threadId;
        }

        if (aiChat.contactId) payload.contact_id = aiChat.contactId;
        if (aiChat.emailId) payload.email_id = aiChat.emailId;

        const resp = await fetch('api.php?action=ai_chat', {
            method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload)
        });
        const data = await resp.json();
        removeAiChatTyping(typingId);

        if (data.success && data.answer) {
            // Store session ID from server
            if (data.session_id) aiChat.sessionId = data.session_id;

            aiChat.history.push({ role: 'assistant', content: data.answer });
            appendAiChatMessage('assistant', data.answer, data.model);

            // Show context stats if truncated
            if (data.context_stats && data.context_stats.truncated) {
                appendAiChatMessage('error', 'Note: Context was trimmed to fit model token limit (' + data.context_stats.estimated_tokens + ' tokens used)');
            }

            // Show extracted memories
            if (data.extracted_memories && data.extracted_memories.length > 0) {
                data.extracted_memories.forEach(m => aiChat.contactMemories.push({ id: 'new_' + Date.now(), fact: m, source: 'auto' }));
                renderContactMemories();
            }
        } else {
            appendAiChatMessage('error', (data.message || data.error || 'Unknown error'));
        }
    } catch (err) {
        removeAiChatTyping(typingId);
        appendAiChatMessage('error', 'Network error: ' + err.message);
    } finally {
        sendBtn.disabled = false; sendBtn.style.opacity = '1';
    }
}

function appendAiChatMessage(role, content, model) {
    const container = document.getElementById('ai-chat-messages');
    const isUser = role === 'user', isError = role === 'error';
    const wrapper = document.createElement('div');
    wrapper.style.cssText = `display:flex;flex-direction:column;align-items:${isUser ? 'flex-end' : 'flex-start'};gap:4px;`;
    const bubble = document.createElement('div');
    bubble.style.cssText = `max-width:88%;padding:10px 13px;border-radius:${isUser ? '14px 14px 4px 14px' : '14px 14px 14px 4px'};font-size:0.82em;line-height:1.5;word-break:break-word;background:${isUser ? 'linear-gradient(135deg,#6366f1,#8b5cf6)' : isError ? '#3d1515' : '#252540'};color:${isUser ? '#fff' : isError ? '#fca5a5' : '#e2e8f0'};${isError ? 'border:1px solid #7f1d1d;' : ''}`;
    if (!isUser && !isError) bubble.innerHTML = renderAiMarkdown(content);
    else bubble.textContent = content;
    wrapper.appendChild(bubble);

    if (!isUser && !isError) {
        const meta = document.createElement('div');
        meta.style.cssText = 'display:flex;gap:6px;align-items:center;';
        if (model) {
            const badge = document.createElement('span');
            badge.style.cssText = 'font-size:0.65em;color:#4a4a6a;background:#1e1e2e;padding:2px 6px;border-radius:4px;';
            badge.textContent = model; meta.appendChild(badge);
        }
        const copyBtn = document.createElement('button');
        copyBtn.style.cssText = 'font-size:0.65em;color:#6366f1;background:none;border:none;cursor:pointer;padding:2px 4px;';
        copyBtn.textContent = '📋 Copy';
        copyBtn.onclick = () => navigator.clipboard.writeText(content).then(() => { copyBtn.textContent = '✓ Copied!'; setTimeout(() => copyBtn.textContent = '📋 Copy', 2000); });
        meta.appendChild(copyBtn);
        wrapper.appendChild(meta);
    }
    container.appendChild(wrapper);
    container.scrollTop = container.scrollHeight;
    return wrapper;
}

function appendAiChatTyping() {
    const container = document.getElementById('ai-chat-messages');
    const id = 'typing-' + Date.now();
    const el = document.createElement('div');
    el.id = id;
    el.innerHTML = `
●●●
`;
    if (!document.getElementById('ai-chat-style')) {
        const s = document.createElement('style'); s.id = 'ai-chat-style';
        s.textContent = '@keyframes aiBounce{0%,80%,100%{opacity:0.3;transform:translateY(0)}40%{opacity:1;transform:translateY(-4px)}}';
        document.head.appendChild(s);
    }
    container.appendChild(el); container.scrollTop = container.scrollHeight;
    return id;
}

function removeAiChatTyping(id) { const el = document.getElementById(id); if (el) el.remove(); }

function renderAiMarkdown(text) {
    return text
        .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
        .replace(/```([\s\S]*?)```/g, '
$1
')
        .replace(/`([^`]+)`/g, '$1')
        .replace(/\*\*([^*]+)\*\*/g, '$1').replace(/\*([^*]+)\*/g, '$1')
        .replace(/^### (.+)$/gm, '
$1
')
        .replace(/^## (.+)$/gm, '
$1
')
        .replace(/^# (.+)$/gm, '
$1
')
        .replace(/^[-*] (.+)$/gm, '
• $1
')
        .replace(/\n/g, '
');
}

function clearAiChat() {
    aiChat.history = [];
    document.getElementById('ai-chat-messages').innerHTML = `<div style="text-align:center;color:#4a4a6a;font-size:0.8em;padding:20px 0;"><div style="font-size:2em;margin-bottom:8px;">🤖</div><div>Chat cleared. Ask me anything!</div></div>`;
}

// Draggable panel + initialization
document.addEventListener('DOMContentLoaded', () => {
    initAiProviderDropdown();
    const header = document.getElementById('ai-chat-header');
    const panel = document.getElementById('ai-chat-panel');
    if (!header || !panel) return;
    header.addEventListener('mousedown', e => {
        if (e.target.tagName === 'BUTTON') return;
        aiChat.isDragging = true;
        const rect = panel.getBoundingClientRect();
        aiChat.dragOffsetX = e.clientX - rect.left;
        aiChat.dragOffsetY = e.clientY - rect.top;
        panel.style.transition = 'none';
    });
    document.addEventListener('mousemove', e => {
        if (!aiChat.isDragging) return;
        panel.style.right = 'auto'; panel.style.bottom = 'auto';
        panel.style.left = Math.max(0, Math.min(e.clientX - aiChat.dragOffsetX, window.innerWidth - panel.offsetWidth)) + 'px';
        panel.style.top = Math.max(0, Math.min(e.clientY - aiChat.dragOffsetY, window.innerHeight - panel.offsetHeight)) + 'px';
    });
    document.addEventListener('mouseup', () => { aiChat.isDragging = false; });
});


Now I have the entire AI chat JS block from extension2 (lines 2668-3429). I need to append this to the end of v1's app.js (which ends at line 3195). Let me do that.


  3188→        }
  3189→
  3190→        setTimeout(() => { msgEl.textContent = ''; }, 3000);
  3191→    } catch (error) {
  3192→        alert('Error saving: ' + error.message);
  3193→    }
  3194→}
  3195→
  3196→

        }

        setTimeout(() => { msgEl.textContent = ''; }, 3000);
    } catch (error) {
        alert('Error saving: ' + error.message);
    }
}