   569→    private function getThreadsMongo(?int $contactId, string $search, int $limit, int $offset): array
   570→    {
   571→        $mongo = MongoDatabase::getInstance();
   572→
   573→        // Build match filter
   574→        $matchFilter = [
   575→            'thread_id' => ['$ne' => null, '$ne' => '']
   576→        ];
   577→
   578→        if ($contactId) {
   579→            $matchFilter['contact_id'] = $contactId;
   580→        }
   581→
   582→        if ($search) {
   583→            $matchFilter['subject'] = ['$regex' => $search, '$options' => 'i'];
   584→        }
   585→
   586→        // Count distinct threads for total
   587→        $countPipeline = [
   588→            ['$match' => $matchFilter],
   589→            ['$group' => ['_id' => '$thread_id']],
   590→            ['$count' => 'total']
   591→        ];
   592→
   593→        $countResult = $mongo->aggregate('emails', $countPipeline);
   594→        $total = !empty($countResult) ? $countResult[0]['total'] : 0;
   595→
   596→        // Get threads with aggregated data
   597→        $pipeline = [
   598→            ['$match' => $matchFilter],
   599→            ['$group' => [
   600→                '_id' => '$thread_id',
   601→                'first_email_id' => ['$min' => '$id'],
   602→                'last_email_id' => ['$max' => '$id'],
   603→                'subject' => ['$first' => '$subject'],
   604→                'first_date' => ['$min' => '$date'],
   605→                'last_date' => ['$max' => '$date'],
   606→                'email_count' => ['$sum' => 1],
   607→                'inbound_count' => ['$sum' => ['$cond' => [['$eq' => ['$direction', 'inbound']], 1, 0]]],
   608→                'outbound_count' => ['$sum' => ['$cond' => [['$eq' => ['$direction', 'outbound']], 1, 0]]],
   609→                'contact_id' => ['$max' => '$contact_id']
   610→            ]],
   611→            ['$lookup' => [
   612→                'from' => 'contacts',
   613→                'localField' => 'contact_id',
   614→                'foreignField' => 'id',
   615→                'as' => 'contact'
   616→            ]],
   617→            ['$lookup' => [
   618→                'from' => 'thread_analysis',
   619→                'localField' => '_id',
   620→                'foreignField' => 'thread_id',
   621→                'as' => 'analysis'
   622→            ]],
   623→            ['$project' => [
   624→                '_id' => 0,
   625→                'thread_id' => '$_id',
   626→                'first_email_id' => 1,
   627→                'last_email_id' => 1,
   628→                'subject' => 1,
   629→                'first_date' => 1,
   630→                'last_date' => 1,
   631→                'email_count' => 1,
   632→                'inbound_count' => 1,
   633→                'outbound_count' => 1,
   634→                'contact_id' => 1,
   635→                'contact_name' => ['$arrayElemAt' => ['$contact.name', 0]],
   636→                'is_analyzed' => ['$arrayElemAt' => ['$analysis.is_analyzed', 0]],
   637→                'needs_reanalysis' => ['$arrayElemAt' => ['$analysis.needs_reanalysis', 0]]
   638→            ]],
   639→            ['$sort' => ['last_date' => -1]],
   640→            ['$skip' => $offset],
   641→            ['$limit' => $limit]
   642→        ];
   643→
   644→        $threads = $mongo->aggregate('emails', $pipeline);
   645→
   646→        return [
   647→            'threads' => $threads,
   648→            'total' => $total
   649→        ];
   650→    }
   651→
   652→    /**
   653→     * Get full thread details with all emails and analysis
   654→     *
   655→     * @param string $threadId Gmail thread ID
   656→     * @return array ['thread_id', 'emails', 'analysis', 'email_count']
   657→     */
   658→    public function getThreadDetails(string $threadId): array

    private function getThreadsMongo(?int $contactId, string $search, int $limit, int $offset): array
    {
        $mongo = MongoDatabase::getInstance();

        // Build match filter
        $matchFilter = [
            'thread_id' => ['$ne' => null, '$ne' => '']
        ];

        if ($contactId) {
            $matchFilter['contact_id'] = $contactId;
        }

        if ($search) {
            $matchFilter['subject'] = ['$regex' => $search, '$options' => 'i'];
        }

        // Count distinct threads for total
        $countPipeline = [
            ['$match' => $matchFilter],
            ['$group' => ['_id' => '$thread_id']],
            ['$count' => 'total']
        ];

        $countResult = $mongo->aggregate('emails', $countPipeline);
        $total = !empty($countResult) ? $countResult[0]['total'] : 0;

        // Get threads with aggregated data
        $pipeline = [
            ['$match' => $matchFilter],
            ['$group' => [
                '_id' => '$thread_id',
                'first_email_id' => ['$min' => '$id'],
                'last_email_id' => ['$max' => '$id'],
                'subject' => ['$first' => '$subject'],
                'first_date' => ['$min' => '$date'],
                'last_date' => ['$max' => '$date'],
                'email_count' => ['$sum' => 1],
                'inbound_count' => ['$sum' => ['$cond' => [['$eq' => ['$direction', 'inbound']], 1, 0]]],
                'outbound_count' => ['$sum' => ['$cond' => [['$eq' => ['$direction', 'outbound']], 1, 0]]],
                'contact_id' => ['$max' => '$contact_id']
            ]],
            ['$lookup' => [
                'from' => 'contacts',
                'localField' => 'contact_id',
                'foreignField' => 'id',
                'as' => 'contact'
            ]],
            ['$lookup' => [
                'from' => 'thread_analysis',
                'localField' => '_id',
                'foreignField' => 'thread_id',
                'as' => 'analysis'
            ]],
            ['$project' => [
                '_id' => 0,
                'thread_id' => '$_id',
                'first_email_id' => 1,
                'last_email_id' => 1,
                'subject' => 1,
                'first_date' => 1,
                'last_date' => 1,
                'email_count' => 1,
                'inbound_count' => 1,
                'outbound_count' => 1,
                'contact_id' => 1,
                'contact_name' => ['$arrayElemAt' => ['$contact.name', 0]],
                'is_analyzed' => ['$arrayElemAt' => ['$analysis.is_analyzed', 0]],
                'needs_reanalysis' => ['$arrayElemAt' => ['$analysis.needs_reanalysis', 0]]
            ]],
            ['$sort' => ['last_date' => -1]],
            ['$skip' => $offset],
            ['$limit' => $limit]
        ];

        $threads = $mongo->aggregate('emails', $pipeline);

        return [
            'threads' => $threads,
            'total' => $total
        ];
    }

    /**
     * Get full thread details with all emails and analysis
     *
     * @param string $threadId Gmail thread ID
     * @return array ['thread_id', 'emails', 'analysis', 'email_count']
     */
    public function getThreadDetails(string $threadId): array


Let me continue reading the rest of thread_view.php JS to understand the submitToAI function and other key pieces.


Let me read the rest of the thread_view.php JS - the submitToAI function and remaining code.


  1300→            updateSelectedCount();
  1301→        }
  1302→
  1303→        // Select all providers
  1304→        function selectAllProviders() {
  1305→            for (const key of Object.keys(providers)) {
  1306→                if (providers[key].has_key) {
  1307→                    toggleProvider(key, true);
  1308→                }
  1309→            }
  1310→        }
  1311→
  1312→        // Deselect all providers
  1313→        function deselectAllProviders() {
  1314→            for (const key of Object.keys(providers)) {
  1315→                toggleProvider(key, false);
  1316→            }
  1317→        }
  1318→
  1319→        // Update selected count display
  1320→        function updateSelectedCount() {
  1321→            const count = document.querySelectorAll('input[name="providers[]"]:checked').length;
  1322→            const countEl = document.getElementById('selected-count');
  1323→            if (countEl) {
  1324→                countEl.textContent = count + ' selected';
  1325→                countEl.style.color = count === 0 ? '#ef4444' : 'var(--text-secondary)';
  1326→            }
  1327→        }
  1328→
  1329→        // Get selected providers
  1330→        function getSelectedProviders() {
  1331→            const checkboxes = document.querySelectorAll('input[name="providers[]"]:checked');
  1332→            return Array.from(checkboxes).map(cb => cb.value);
  1333→        }
  1334→
  1335→        // Get selected additional threads
  1336→        function getSelectedThreads() {
  1337→            const checkboxes = document.querySelectorAll('input[name="additional_threads[]"]:checked');
  1338→            return Array.from(checkboxes).map(cb => cb.value);
  1339→        }
  1340→
  1341→        /**
  1342→         * Strip quoted content from email body
  1343→         * Removes:
  1344→         * - Lines starting with ">" (quoted text)
  1345→         * - "On [date] [name] wrote:" patterns
  1346→         * - "From: ... Sent: ... To: ... Subject:" forwarded headers
  1347→         * - Lines starting with "-----Original Message-----"
  1348→         */
  1349→        function stripQuotedContent(body) {
  1350→            if (!body) return '';
  1351→
  1352→            let lines = body.split('\n');
  1353→            let result = [];
  1354→            let inQuoteBlock = false;
  1355→            let consecutiveEmptyLines = 0;
  1356→
  1357→            for (let i = 0; i < lines.length; i++) {
  1358→                let line = lines[i];
  1359→                let trimmedLine = line.trim();
  1360→
  1361→                // Detect start of quoted section
  1362→                if (/^On\s+.+wrote:\s*$/i.test(trimmedLine) ||
  1363→                    /^On\s+\d+.+at\s+\d+.+wrote:\s*$/i.test(trimmedLine) ||
  1364→                    /^-{3,}\s*Original Message\s*-{3,}/i.test(trimmedLine) ||
  1365→                    /^-{3,}\s*Forwarded message\s*-{3,}/i.test(trimmedLine) ||
  1366→                    /^From:\s*.+\s+Sent:\s*.+\s+To:/i.test(trimmedLine)) {
  1367→                    inQuoteBlock = true;
  1368→                    continue;
  1369→                }
  1370→
  1371→                // Skip quoted lines (starting with >)
  1372→                if (trimmedLine.startsWith('>')) {
  1373→                    inQuoteBlock = true;
  1374→                    continue;
  1375→                }
  1376→
  1377→                // If we were in a quote block and hit non-quoted content, exit quote mode
  1378→                if (inQuoteBlock && trimmedLine && !trimmedLine.startsWith('>')) {
  1379→                    // Check if this is still part of quote (continuation)
  1380→                    if (i + 1 < lines.length && lines[i + 1].trim().startsWith('>')) {
  1381→                        continue; // Skip this line too
  1382→                    }
  1383→                    inQuoteBlock = false;
  1384→                }
  1385→
  1386→                // Skip if still in quote block
  1387→                if (inQuoteBlock) continue;
  1388→
  1389→                // Handle consecutive empty lines (max 2)
  1390→                if (!trimmedLine) {
  1391→                    consecutiveEmptyLines++;
  1392→                    if (consecutiveEmptyLines <= 2) {
  1393→                        result.push('');
  1394→                    }
  1395→                    continue;
  1396→                }
  1397→
  1398→                consecutiveEmptyLines = 0;
  1399→                result.push(line);
  1400→            }
  1401→
  1402→            // Trim trailing empty lines
  1403→            while (result.length > 0 && !result[result.length - 1].trim()) {
  1404→                result.pop();
  1405→            }
  1406→
  1407→            return result.join('\n').trim();
  1408→        }
  1409→
  1410→        /**
  1411→         * Build email content string for AI
  1412→         * Supports different processing modes:
  1413→         * - full: Include all content as-is
  1414→         * - stripped: Remove quoted replies from all emails
  1415→         * - smart: Keep quotes only in the last email
  1416→         */
  1417→        function buildEmailContent() {
  1418→            const mode = document.getElementById('content-mode').value;
  1419→            let content = 'EMAIL THREAD CONVERSATION:\n';
  1420→            content += `(Content mode: ${mode})\n\n`;
  1421→
  1422→            const totalEmails = emailContent.length;
  1423→
  1424→            emailContent.forEach((email, index) => {
  1425→                content += `--- Email ${index + 1} of ${totalEmails} ---\n`;
  1426→                content += `From: ${email.from} <${email.from_email}>\n`;
  1427→                content += `Date: ${email.date}\n`;
  1428→                content += `Direction: ${email.direction}\n\n`;
  1429→
  1430→                let emailBody = email.body;
  1431→
  1432→                if (mode === 'stripped') {
  1433→                    // Strip quoted content from all emails
  1434→                    emailBody = stripQuotedContent(emailBody);
  1435→                    if (!emailBody.trim()) {
  1436→                        emailBody = '(Only quoted content - no new text in this email)';
  1437→                    }
  1438→                } else if (mode === 'smart') {
  1439→                    // Strip quotes from all emails except the last one
  1440→                    if (index < totalEmails - 1) {
  1441→                        emailBody = stripQuotedContent(emailBody);
  1442→                        if (!emailBody.trim()) {
  1443→                            emailBody = '(Only quoted content - no new text in this email)';
  1444→                        }
  1445→                    }
  1446→                    // Last email keeps full content for context
  1447→                }
  1448→                // mode === 'full' keeps everything as-is
  1449→
  1450→                content += emailBody + '\n\n';
  1451→            });
  1452→
  1453→            return content;
  1454→        }
  1455→
  1456→        /**
  1457→         * Process additional thread content with the selected mode
  1458→         */
  1459→        function processAdditionalThreadContent(emails, mode) {
  1460→            let content = '';
  1461→            const totalEmails = emails.length;
  1462→
  1463→            emails.forEach((email, idx) => {
  1464→                const senderName = email.from_name || email.from_email || 'Unknown';
  1465→                content += `Email ${idx + 1} from ${senderName}:\n`;
  1466→
  1467→                let emailBody = email.body_text || email.body_html || '';
  1468→
  1469→                if (mode === 'stripped') {
  1470→                    emailBody = stripQuotedContent(emailBody);
  1471→                    if (!emailBody.trim()) {
  1472→                        emailBody = '(Only quoted content)';
  1473→                    }
  1474→                } else if (mode === 'smart' && idx < totalEmails - 1) {
  1475→                    emailBody = stripQuotedContent(emailBody);
  1476→                    if (!emailBody.trim()) {
  1477→                        emailBody = '(Only quoted content)';
  1478→                    }
  1479→                }
  1480→
  1481→                content += emailBody + '\n\n';
  1482→            });
  1483→
  1484→            return content;
  1485→        }
  1486→
  1487→        // Submit to AI
  1488→        async function submitToAI() {
  1489→            const selectedProvidersList = getSelectedProviders();
  1490→
  1491→            if (selectedProvidersList.length === 0) {
  1492→                alert('Please select at least one AI provider');
  1493→                return;
  1494→            }
  1495→
  1496→            const question = document.getElementById('ai-question').value.trim();
  1497→            const context = document.getElementById('ai-context').value.trim();
  1498→            const contentMode = document.getElementById('content-mode').value;
  1499→            const additionalThreads = getSelectedThreads();
  1500→
  1501→            // Build models map for each selected provider
  1502→            const modelsMap = {};
  1503→            selectedProvidersList.forEach(provider => {
  1504→                const modelSelect = document.getElementById('model_' + provider);
  1505→                modelsMap[provider] = modelSelect ? modelSelect.value : '';
  1506→            });
  1507→
  1508→            // Show loading
  1509→            document.getElementById('loading-overlay').classList.add('active');
  1510→            document.getElementById('submit-ai-btn').disabled = true;
  1511→
  1512→            try {
  1513→                // Build the full content
  1514→                let fullContent = buildEmailContent();
  1515→
  1516→                // ADD CONVERSATION HISTORY FOR CONTEXT (ChatGPT-like memory)
  1517→                const conversationContext = buildConversationContext();
  1518→                if (conversationContext) {
  1519→                    fullContent += conversationContext;
  1520→                    console.log('Including conversation history:', conversationHistory.length, 'previous exchanges');
  1521→                }
  1522→
  1523→                // Add question if provided
  1524→                const actualQuestion = question || 'Please analyze this email conversation and provide insights on:\n1. Key points and summary\n2. Sentiment and tone analysis\n3. Any potential concerns or red flags\n4. Recommended next steps';
  1525→
  1526→                if (question) {
  1527→                    fullContent += '\n\nCURRENT USER QUESTION:\n' + question;
  1528→                } else {
  1529→                    fullContent += '\n\n' + actualQuestion;
  1530→                }
  1531→
  1532→                // Add context if provided
  1533→                if (context) {
  1534→                    fullContent += '\n\nADDITIONAL CONTEXT:\n' + context;
  1535→                }
  1536→
  1537→                // Fetch additional thread content if selected
  1538→                if (additionalThreads.length > 0) {
  1539→                    document.getElementById('loading-message').textContent = 'Loading additional threads...';
  1540→
  1541→                    for (const threadId of additionalThreads) {
  1542→                        const threadData = await fetch(`api.php?action=thread&thread_id=${threadId}`);
  1543→                        const thread = await threadData.json();
  1544→
  1545→                        if (thread.emails && thread.emails.length > 0) {
  1546→                            fullContent += '\n\n--- ADDITIONAL THREAD ---\n';
  1547→                            fullContent += 'Subject: ' + (thread.emails[0].subject || '(No Subject)') + '\n\n';
  1548→                            fullContent += processAdditionalThreadContent(thread.emails, contentMode);
  1549→                        }
  1550→                    }
  1551→                }
  1552→
  1553→                // Log what's being sent
  1554→                console.log('=== AI REQUEST LOG ===');
  1555→                console.log('Providers:', selectedProvidersList);
  1556→                console.log('Models:', modelsMap);
  1557→                console.log('Content Mode:', contentMode);
  1558→                console.log('Conversation History Entries:', conversationHistory.length);
  1559→                console.log('Content Length:', fullContent.length, 'characters');
  1560→                console.log('Estimated Tokens:', Math.ceil(fullContent.length / 4));
  1561→                console.log('Full Content Preview (first 500 chars):', fullContent.substring(0, 500));
  1562→                console.log('======================');
  1563→
  1564→                document.getElementById('loading-message').textContent = `Analyzing with ${selectedProvidersList.length} AI provider(s)...`;
  1565→
  1566→                // Build request body with all selected providers and their models
  1567→                const requestBody = {
  1568→                    user_content: fullContent,
  1569→                    providers: selectedProvidersList
  1570→                };
  1571→
  1572→                // Add model for each provider
  1573→                for (const [provider, model] of Object.entries(modelsMap)) {
  1574→                    requestBody['model_' + provider] = model;
  1575→                }
  1576→
  1577→                // Send to AI API
  1578→                const response = await fetch('../aiapi/api_opus/api/query.php', {
  1579→                    method: 'POST',
  1580→                    headers: { 'Content-Type': 'application/json' },
  1581→                    body: JSON.stringify(requestBody)
  1582→                });
  1583→
  1584→                const data = await response.json();
  1585→
  1586→                // Log response
  1587→                console.log('=== AI RESPONSE LOG ===');
  1588→                console.log('Success:', data.success);
  1589→                console.log('Query ID:', data.query_id);
  1590→                console.log('Total Cost:', data.total_cost);
  1591→                console.log('Response Keys:', Object.keys(data.responses || {}));
  1592→                console.log('=======================');
  1593→
  1594→                if (data.success) {
  1595→                    // Track if we've added to conversation history (only add once)
  1596→                    let addedToConversation = false;
  1597→                    let firstSuccessfulProvider = null;
  1598→                    let firstSuccessfulResponse = null;
  1599→
  1600→                    // Save to MongoDB for each provider
  1601→                    for (const provider of selectedProvidersList) {
  1602→                        const providerResponse = data.responses[provider + '_response'];
  1603→                        if (providerResponse && providerResponse.success) {
  1604→                            // Track first successful response for conversation history
  1605→                            if (!firstSuccessfulProvider) {
  1606→                                firstSuccessfulProvider = provider;
  1607→                                firstSuccessfulResponse = providerResponse;
  1608→                            }
  1609→
  1610→                            // Save to MongoDB via our API
  1611→                            await saveAIResponse({
  1612→                                thread_ids: [currentThreadId, ...additionalThreads],
  1613→                                contact_id: primaryContactId,
  1614→                                question: actualQuestion,
  1615→                                context: context,
  1616→                                content_mode: contentMode,
  1617→                                content_sent: fullContent, // Log what was sent
  1618→                                content_length: fullContent.length,
  1619→                                estimated_tokens: Math.ceil(fullContent.length / 4),
  1620→                                provider: provider,
  1621→                                model: modelsMap[provider],
  1622→                                query_id: data.query_id,
  1623→                                response: providerResponse,
  1624→                                total_cost: providerResponse.cost || 0,
  1625→                                all_providers: selectedProvidersList,
  1626→                                conversation_history_count: conversationHistory.length
  1627→                            });
  1628→                        }
  1629→                    }
  1630→
  1631→                    // ADD TO CONVERSATION HISTORY (first successful response)
  1632→                    // This creates the ChatGPT-like memory effect
  1633→                    if (firstSuccessfulProvider && firstSuccessfulResponse && firstSuccessfulResponse.response) {
  1634→                        addToConversation(
  1635→                            actualQuestion,
  1636→                            firstSuccessfulResponse.response,
  1637→                            firstSuccessfulProvider,
  1638→                            modelsMap[firstSuccessfulProvider],
  1639→                            context  // Pass context to conversation history
  1640→                        );
  1641→                        console.log('Added to conversation history:', firstSuccessfulProvider);
  1642→                    }
  1643→
  1644→                    // Clear the question field for next question
  1645→                    document.getElementById('ai-question').value = '';
  1646→
  1647→                    // Display response
  1648→                    displayAIResponse(data);
  1649→                } else {
  1650→                    alert('AI request failed: ' + (data.error || 'Unknown error'));
  1651→                }
  1652→            } catch (error) {
  1653→                console.error('AI request error:', error);
  1654→                alert('Error: ' + error.message);
  1655→            } finally {
  1656→                document.getElementById('loading-overlay').classList.remove('active');
  1657→                document.getElementById('submit-ai-btn').disabled = false;
  1658→            }
  1659→        }
  1660→
  1661→        // Save AI response to MongoDB
  1662→        async function saveAIResponse(responseData) {
  1663→            try {
  1664→                await fetch('api.php?action=save_ai_response', {
  1665→                    method: 'POST',
  1666→                    headers: { 'Content-Type': 'application/json' },
  1667→                    body: JSON.stringify(responseData)
  1668→                });
  1669→            } catch (error) {
  1670→                console.error('Failed to save response:', error);
  1671→            }
  1672→        }
  1673→
  1674→        // Display AI response
  1675→        function displayAIResponse(data) {
  1676→            const container = document.getElementById('ai-response-container');
  1677→            const tabsContainer = document.getElementById('response-tabs');
  1678→            const contentContainer = document.getElementById('ai-responses-content');
  1679→
  1680→            container.style.display = 'block';
  1681→            tabsContainer.innerHTML = '';
  1682→            contentContainer.innerHTML = '';
  1683→
  1684→            let tabIndex = 0;
  1685→            for (const [key, response] of Object.entries(data.responses)) {
  1686→                const providerName = key.replace('_response', '');
  1687→                const providerConfig = providers[providerName] || { name: providerName };
  1688→
  1689→                // Create tab
  1690→                const tab = document.createElement('button');
  1691→                tab.className = 'response-tab' + (tabIndex === 0 ? ' active' : '');
  1692→                tab.textContent = providerConfig.name;
  1693→                tab.onclick = () => switchResponseTab(tabIndex);
  1694→                tab.dataset.index = tabIndex;
  1695→                tabsContainer.appendChild(tab);
  1696→
  1697→                // Create content
  1698→                const content = document.createElement('div');
  1699→                content.className = 'response-content' + (tabIndex === 0 ? ' active' : '');

            updateSelectedCount();
        }

        // Select all providers
        function selectAllProviders() {
            for (const key of Object.keys(providers)) {
                if (providers[key].has_key) {
                    toggleProvider(key, true);
                }
            }
        }

        // Deselect all providers
        function deselectAllProviders() {
            for (const key of Object.keys(providers)) {
                toggleProvider(key, false);
            }
        }

        // Update selected count display
        function updateSelectedCount() {
            const count = document.querySelectorAll('input[name="providers[]"]:checked').length;
            const countEl = document.getElementById('selected-count');
            if (countEl) {
                countEl.textContent = count + ' selected';
                countEl.style.color = count === 0 ? '#ef4444' : 'var(--text-secondary)';
            }
        }

        // Get selected providers
        function getSelectedProviders() {
            const checkboxes = document.querySelectorAll('input[name="providers[]"]:checked');
            return Array.from(checkboxes).map(cb => cb.value);
        }

        // Get selected additional threads
        function getSelectedThreads() {
            const checkboxes = document.querySelectorAll('input[name="additional_threads[]"]:checked');
            return Array.from(checkboxes).map(cb => cb.value);
        }

        /**
         * Strip quoted content from email body
         * Removes:
         * - Lines starting with ">" (quoted text)
         * - "On [date] [name] wrote:" patterns
         * - "From: ... Sent: ... To: ... Subject:" forwarded headers
         * - Lines starting with "-----Original Message-----"
         */
        function stripQuotedContent(body) {
            if (!body) return '';

            let lines = body.split('\n');
            let result = [];
            let inQuoteBlock = false;
            let consecutiveEmptyLines = 0;

            for (let i = 0; i < lines.length; i++) {
                let line = lines[i];
                let trimmedLine = line.trim();

                // Detect start of quoted section
                if (/^On\s+.+wrote:\s*$/i.test(trimmedLine) ||
                    /^On\s+\d+.+at\s+\d+.+wrote:\s*$/i.test(trimmedLine) ||
                    /^-{3,}\s*Original Message\s*-{3,}/i.test(trimmedLine) ||
                    /^-{3,}\s*Forwarded message\s*-{3,}/i.test(trimmedLine) ||
                    /^From:\s*.+\s+Sent:\s*.+\s+To:/i.test(trimmedLine)) {
                    inQuoteBlock = true;
                    continue;
                }

                // Skip quoted lines (starting with >)
                if (trimmedLine.startsWith('>')) {
                    inQuoteBlock = true;
                    continue;
                }

                // If we were in a quote block and hit non-quoted content, exit quote mode
                if (inQuoteBlock && trimmedLine && !trimmedLine.startsWith('>')) {
                    // Check if this is still part of quote (continuation)
                    if (i + 1 < lines.length && lines[i + 1].trim().startsWith('>')) {
                        continue; // Skip this line too
                    }
                    inQuoteBlock = false;
                }

                // Skip if still in quote block
                if (inQuoteBlock) continue;

                // Handle consecutive empty lines (max 2)
                if (!trimmedLine) {
                    consecutiveEmptyLines++;
                    if (consecutiveEmptyLines <= 2) {
                        result.push('');
                    }
                    continue;
                }

                consecutiveEmptyLines = 0;
                result.push(line);
            }

            // Trim trailing empty lines
            while (result.length > 0 && !result[result.length - 1].trim()) {
                result.pop();
            }

            return result.join('\n').trim();
        }

        /**
         * Build email content string for AI
         * Supports different processing modes:
         * - full: Include all content as-is
         * - stripped: Remove quoted replies from all emails
         * - smart: Keep quotes only in the last email
         */
        function buildEmailContent() {
            const mode = document.getElementById('content-mode').value;
            let content = 'EMAIL THREAD CONVERSATION:\n';
            content += `(Content mode: ${mode})\n\n`;

            const totalEmails = emailContent.length;

            emailContent.forEach((email, index) => {
                content += `--- Email ${index + 1} of ${totalEmails} ---\n`;
                content += `From: ${email.from} <${email.from_email}>\n`;
                content += `Date: ${email.date}\n`;
                content += `Direction: ${email.direction}\n\n`;

                let emailBody = email.body;

                if (mode === 'stripped') {
                    // Strip quoted content from all emails
                    emailBody = stripQuotedContent(emailBody);
                    if (!emailBody.trim()) {
                        emailBody = '(Only quoted content - no new text in this email)';
                    }
                } else if (mode === 'smart') {
                    // Strip quotes from all emails except the last one
                    if (index < totalEmails - 1) {
                        emailBody = stripQuotedContent(emailBody);
                        if (!emailBody.trim()) {
                            emailBody = '(Only quoted content - no new text in this email)';
                        }
                    }
                    // Last email keeps full content for context
                }
                // mode === 'full' keeps everything as-is

                content += emailBody + '\n\n';
            });

            return content;
        }

        /**
         * Process additional thread content with the selected mode
         */
        function processAdditionalThreadContent(emails, mode) {
            let content = '';
            const totalEmails = emails.length;

            emails.forEach((email, idx) => {
                const senderName = email.from_name || email.from_email || 'Unknown';
                content += `Email ${idx + 1} from ${senderName}:\n`;

                let emailBody = email.body_text || email.body_html || '';

                if (mode === 'stripped') {
                    emailBody = stripQuotedContent(emailBody);
                    if (!emailBody.trim()) {
                        emailBody = '(Only quoted content)';
                    }
                } else if (mode === 'smart' && idx < totalEmails - 1) {
                    emailBody = stripQuotedContent(emailBody);
                    if (!emailBody.trim()) {
                        emailBody = '(Only quoted content)';
                    }
                }

                content += emailBody + '\n\n';
            });

            return content;
        }

        // Submit to AI
        async function submitToAI() {
            const selectedProvidersList = getSelectedProviders();

            if (selectedProvidersList.length === 0) {
                alert('Please select at least one AI provider');
                return;
            }

            const question = document.getElementById('ai-question').value.trim();
            const context = document.getElementById('ai-context').value.trim();
            const contentMode = document.getElementById('content-mode').value;
            const additionalThreads = getSelectedThreads();

            // Build models map for each selected provider
            const modelsMap = {};
            selectedProvidersList.forEach(provider => {
                const modelSelect = document.getElementById('model_' + provider);
                modelsMap[provider] = modelSelect ? modelSelect.value : '';
            });

            // Show loading
            document.getElementById('loading-overlay').classList.add('active');
            document.getElementById('submit-ai-btn').disabled = true;

            try {
                // Build the full content
                let fullContent = buildEmailContent();

                // ADD CONVERSATION HISTORY FOR CONTEXT (ChatGPT-like memory)
                const conversationContext = buildConversationContext();
                if (conversationContext) {
                    fullContent += conversationContext;
                    console.log('Including conversation history:', conversationHistory.length, 'previous exchanges');
                }

                // Add question if provided
                const actualQuestion = question || 'Please analyze this email conversation and provide insights on:\n1. Key points and summary\n2. Sentiment and tone analysis\n3. Any potential concerns or red flags\n4. Recommended next steps';

                if (question) {
                    fullContent += '\n\nCURRENT USER QUESTION:\n' + question;
                } else {
                    fullContent += '\n\n' + actualQuestion;
                }

                // Add context if provided
                if (context) {
                    fullContent += '\n\nADDITIONAL CONTEXT:\n' + context;
                }

                // Fetch additional thread content if selected
                if (additionalThreads.length > 0) {
                    document.getElementById('loading-message').textContent = 'Loading additional threads...';

                    for (const threadId of additionalThreads) {
                        const threadData = await fetch(`api.php?action=thread&thread_id=${threadId}`);
                        const thread = await threadData.json();

                        if (thread.emails && thread.emails.length > 0) {
                            fullContent += '\n\n--- ADDITIONAL THREAD ---\n';
                            fullContent += 'Subject: ' + (thread.emails[0].subject || '(No Subject)') + '\n\n';
                            fullContent += processAdditionalThreadContent(thread.emails, contentMode);
                        }
                    }
                }

                // Log what's being sent
                console.log('=== AI REQUEST LOG ===');
                console.log('Providers:', selectedProvidersList);
                console.log('Models:', modelsMap);
                console.log('Content Mode:', contentMode);
                console.log('Conversation History Entries:', conversationHistory.length);
                console.log('Content Length:', fullContent.length, 'characters');
                console.log('Estimated Tokens:', Math.ceil(fullContent.length / 4));
                console.log('Full Content Preview (first 500 chars):', fullContent.substring(0, 500));
                console.log('======================');

                document.getElementById('loading-message').textContent = `Analyzing with ${selectedProvidersList.length} AI provider(s)...`;

                // Build request body with all selected providers and their models
                const requestBody = {
                    user_content: fullContent,
                    providers: selectedProvidersList
                };

                // Add model for each provider
                for (const [provider, model] of Object.entries(modelsMap)) {
                    requestBody['model_' + provider] = model;
                }

                // Send to AI API
                const response = await fetch('../aiapi/api_opus/api/query.php', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify(requestBody)
                });

                const data = await response.json();

                // Log response
                console.log('=== AI RESPONSE LOG ===');
                console.log('Success:', data.success);
                console.log('Query ID:', data.query_id);
                console.log('Total Cost:', data.total_cost);
                console.log('Response Keys:', Object.keys(data.responses || {}));
                console.log('=======================');

                if (data.success) {
                    // Track if we've added to conversation history (only add once)
                    let addedToConversation = false;
                    let firstSuccessfulProvider = null;
                    let firstSuccessfulResponse = null;

                    // Save to MongoDB for each provider
                    for (const provider of selectedProvidersList) {
                        const providerResponse = data.responses[provider + '_response'];
                        if (providerResponse && providerResponse.success) {
                            // Track first successful response for conversation history
                            if (!firstSuccessfulProvider) {
                                firstSuccessfulProvider = provider;
                                firstSuccessfulResponse = providerResponse;
                            }

                            // Save to MongoDB via our API
                            await saveAIResponse({
                                thread_ids: [currentThreadId, ...additionalThreads],
                                contact_id: primaryContactId,
                                question: actualQuestion,
                                context: context,
                                content_mode: contentMode,
                                content_sent: fullContent, // Log what was sent
                                content_length: fullContent.length,
                                estimated_tokens: Math.ceil(fullContent.length / 4),
                                provider: provider,
                                model: modelsMap[provider],
                                query_id: data.query_id,
                                response: providerResponse,
                                total_cost: providerResponse.cost || 0,
                                all_providers: selectedProvidersList,
                                conversation_history_count: conversationHistory.length
                            });
                        }
                    }

                    // ADD TO CONVERSATION HISTORY (first successful response)
                    // This creates the ChatGPT-like memory effect
                    if (firstSuccessfulProvider && firstSuccessfulResponse && firstSuccessfulResponse.response) {
                        addToConversation(
                            actualQuestion,
                            firstSuccessfulResponse.response,
                            firstSuccessfulProvider,
                            modelsMap[firstSuccessfulProvider],
                            context  // Pass context to conversation history
                        );
                        console.log('Added to conversation history:', firstSuccessfulProvider);
                    }

                    // Clear the question field for next question
                    document.getElementById('ai-question').value = '';

                    // Display response
                    displayAIResponse(data);
                } else {
                    alert('AI request failed: ' + (data.error || 'Unknown error'));
                }
            } catch (error) {
                console.error('AI request error:', error);
                alert('Error: ' + error.message);
            } finally {
                document.getElementById('loading-overlay').classList.remove('active');
                document.getElementById('submit-ai-btn').disabled = false;
            }
        }

        // Save AI response to MongoDB
        async function saveAIResponse(responseData) {
            try {
                await fetch('api.php?action=save_ai_response', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify(responseData)
                });
            } catch (error) {
                console.error('Failed to save response:', error);
            }
        }

        // Display AI response
        function displayAIResponse(data) {
            const container = document.getElementById('ai-response-container');
            const tabsContainer = document.getElementById('response-tabs');
            const contentContainer = document.getElementById('ai-responses-content');

            container.style.display = 'block';
            tabsContainer.innerHTML = '';
            contentContainer.innerHTML = '';

            let tabIndex = 0;
            for (const [key, response] of Object.entries(data.responses)) {
                const providerName = key.replace('_response', '');
                const providerConfig = providers[providerName] || { name: providerName };

                // Create tab
                const tab = document.createElement('button');
                tab.className = 'response-tab' + (tabIndex === 0 ? ' active' : '');
                tab.textContent = providerConfig.name;
                tab.onclick = () => switchResponseTab(tabIndex);
                tab.dataset.index = tabIndex;
                tabsContainer.appendChild(tab);

                // Create content
                const content = document.createElement('div');
                content.className = 'response-content' + (tabIndex === 0 ? ' active' : '');


1700→                content.id = 'response-tab-' + tabIndex;
  1701→
  1702→                if (response.success) {
  1703→                    content.innerHTML = `
  1704→

  1705→

  1706→                                ${providerConfig.name}
  1707→
  1708→                                    Model: ${response.model || 'Unknown'} |
  1709→                                    Time: ${response.request_time?.toFixed(2) || '?'}s |
  1710→                                    Cost: $${response.cost?.toFixed(6) || '0'}
  1711→
  1712→

  1713→
${escapeHtml(response.response)}

  1714→

  1715→                    `;
  1716→                } else {
  1717→                    content.innerHTML = `
  1718→

  1719→                            Error:
  1720→
${escapeHtml(response.error || 'Unknown error')}

  1721→

  1722→                    `;
  1723→                }
  1724→
  1725→                contentContainer.appendChild(content);
  1726→                tabIndex++;
  1727→            }
  1728→
  1729→            // Scroll to response
  1730→            container.scrollIntoView({ behavior: 'smooth' });
  1731→        }
  1732→
  1733→        // Switch response tabs - scoped to response container only
  1734→        function switchResponseTab(index) {
  1735→            const tabsContainer = document.getElementById('response-tabs');
  1736→            const contentContainer = document.getElementById('ai-responses-content');
  1737→
  1738→            if (!tabsContainer || !contentContainer) return;
  1739→
  1740→            // Only target tabs within our container
  1741→            tabsContainer.querySelectorAll('.response-tab').forEach(tab => {
  1742→                const tabIndex = parseInt(tab.dataset.index);
  1743→                tab.classList.remove('active');
  1744→                if (tabIndex === index) {
  1745→                    tab.classList.add('active');
  1746→                }
  1747→            });
  1748→
  1749→            // Only target content within our container
  1750→            contentContainer.querySelectorAll('.response-content').forEach((content, i) => {
  1751→                content.classList.remove('active');
  1752→                if (i === index) {
  1753→                    content.classList.add('active');
  1754→                }
  1755→            });
  1756→        }
  1757→
  1758→        // Close response
  1759→        function closeResponse() {
  1760→            document.getElementById('ai-response-container').style.display = 'none';
  1761→        }
  1762→
  1763→        // Mark a saved response row as last viewed
  1764→        function markAsLastViewed(responseId) {
  1765→            // Remove last-viewed from all rows
  1766→            document.querySelectorAll('.saved-response.last-viewed').forEach(el => {
  1767→                el.classList.remove('last-viewed');
  1768→            });
  1769→            // Add to the current row
  1770→            const row = document.getElementById('saved-response-' + responseId);
  1771→            if (row) {
  1772→                row.classList.add('last-viewed');
  1773→            }
  1774→        }
  1775→
  1776→        // Load saved response
  1777→        async function loadSavedResponse(responseId) {
  1778→            try {
  1779→                // Mark this row as last viewed
  1780→                markAsLastViewed(responseId);
  1781→
  1782→                document.getElementById('loading-overlay').classList.add('active');
  1783→                document.getElementById('loading-message').textContent = 'Loading saved response...';
  1784→
  1785→                const response = await fetch(`api.php?action=get_ai_response&id=${responseId}`);
  1786→                const data = await response.json();
  1787→
  1788→                if (data.success && data.response) {
  1789→                    const saved = data.response;
  1790→
  1791→                    // Display it
  1792→                    const container = document.getElementById('ai-response-container');
  1793→                    const tabsContainer = document.getElementById('response-tabs');
  1794→                    const contentContainer = document.getElementById('ai-responses-content');
  1795→
  1796→                    container.style.display = 'block';
  1797→                    tabsContainer.innerHTML = `<button class="response-tab active">${saved.provider}</button>`;
  1798→
  1799→                    const aiResponse = saved.response || {};
  1800→
  1801→                    // Determine how to display question/context
  1802→                    const isDefaultQuestion = saved.question && saved.question.startsWith('Please analyze this email conversation');
  1803→                    const hasContext = saved.context && saved.context.length > 0;
  1804→
  1805→                    let questionHtml = '';
  1806→                    if (isDefaultQuestion && hasContext) {
  1807→                        // Show context as the main input if question was default
  1808→                        questionHtml = `
  1809→
User Input (Context):

  1810→
${escapeHtml(saved.context)}

  1811→
Default question was used for analysis

  1812→                        `;
  1813→                    } else if (hasContext) {
  1814→                        // Show both question and context
  1815→                        questionHtml = `
  1816→
Question: ${escapeHtml(saved.question)}

  1817→
Additional Context:

  1818→
${escapeHtml(saved.context)}

  1819→                        `;
  1820→                    } else {
  1821→                        // Just show question
  1822→                        questionHtml = `<p><strong>Question:</strong> ${escapeHtml(saved.question)}</p>`;
  1823→                    }
  1824→
  1825→                    contentContainer.innerHTML = `
  1826→

  1827→

  1828→

  1829→                                    ${saved.provider} - ${saved.model}
  1830→                                    Saved: ${saved.created_at}
  1831→

  1832→                                ${questionHtml}
  1833→

  1834→
${escapeHtml(aiResponse.response || 'No response content')}

  1835→

  1836→

  1837→                    `;
  1838→
  1839→                    container.scrollIntoView({ behavior: 'smooth' });
  1840→                }
  1841→            } catch (error) {
  1842→                console.error('Failed to load saved response:', error);
  1843→                alert('Failed to load saved response');
  1844→            } finally {
  1845→                document.getElementById('loading-overlay').classList.remove('active');
  1846→            }
  1847→        }
  1848→
  1849→        // Quick view response in modal (no page scroll)
  1850→        async function quickViewResponse(responseId) {
  1851→            try {
  1852→                // Mark this row as last viewed
  1853→                markAsLastViewed(responseId);
  1854→
  1855→                const overlay = document.getElementById('quick-view-overlay');
  1856→                overlay.style.display = 'flex';
  1857→                document.getElementById('quick-view-response').textContent = 'Loading...';
  1858→                document.getElementById('quick-view-meta').innerHTML = '';
  1859→                document.getElementById('quick-view-question-section').innerHTML = '';
  1860→
  1861→                const response = await fetch(`api.php?action=get_ai_response&id=${responseId}`);
  1862→                const data = await response.json();
  1863→
  1864→                if (data.success && data.response) {
  1865→                    const saved = data.response;
  1866→                    const aiResponse = saved.response || {};
  1867→
  1868→                    // Update title
  1869→                    document.getElementById('quick-view-title').textContent = `${saved.provider} / ${saved.model}`;
  1870→
  1871→                    // Update meta
  1872→                    document.getElementById('quick-view-meta').innerHTML = `
  1873→
Provider: ${escapeHtml(saved.provider)}

  1874→
Model: ${escapeHtml(saved.model)}

  1875→
Date: ${escapeHtml(saved.created_at)}

  1876→
Cost: $${(saved.total_cost || 0).toFixed(6)}

  1877→                    `;
  1878→
  1879→                    // Update question/context section
  1880→                    const isDefaultQuestion = saved.question && saved.question.startsWith('Please analyze this email conversation');
  1881→                    const hasContext = saved.context && saved.context.length > 0;
  1882→
  1883→                    let questionHtml = '';
  1884→                    if (isDefaultQuestion && hasContext) {
  1885→                        questionHtml = `
  1886→

  1887→                                User Context:

  1888→                                ${escapeHtml(saved.context)}
  1889→
Default analysis question was used

  1890→

  1891→                        `;
  1892→                    } else if (hasContext) {
  1893→                        questionHtml = `
  1894→

  1895→                                Question: ${escapeHtml(saved.question)}

  1896→                                Context: ${escapeHtml(saved.context)}
  1897→

  1898→                        `;
  1899→                    } else {
  1900→                        questionHtml = `
  1901→

  1902→                                Question: ${escapeHtml(saved.question)}
  1903→

  1904→                        `;
  1905→                    }
  1906→                    document.getElementById('quick-view-question-section').innerHTML = questionHtml;
  1907→
  1908→                    // Update response
  1909→                    document.getElementById('quick-view-response').textContent = aiResponse.response || 'No response content';
  1910→                } else {
  1911→                    document.getElementById('quick-view-response').textContent = 'Error loading response';
  1912→                }
  1913→            } catch (error) {
  1914→                console.error('Failed to quick view response:', error);
  1915→                document.getElementById('quick-view-response').textContent = 'Error: ' + error.message;
  1916→            }
  1917→        }
  1918→
  1919→        // Close quick view modal
  1920→        function closeQuickView(event) {
  1921→            if (event && event.target !== event.currentTarget) return;
  1922→            document.getElementById('quick-view-overlay').style.display = 'none';
  1923→        }
  1924→
  1925→        // Close quick view on Escape key
  1926→        document.addEventListener('keydown', function(e) {
  1927→            if (e.key === 'Escape') {
  1928→                closeQuickView();
  1929→            }
  1930→        });
  1931→
  1932→        // Load more saved responses
  1933→        async function loadMoreResponses() {
  1934→            const btn = document.getElementById('load-more-btn');
  1935→            if (!btn) return;
  1936→
  1937→            const currentOffset = parseInt(btn.dataset.offset) || 0;
  1938→            const total = parseInt(btn.dataset.total) || 0;
  1939→            const limit = 5; // Load 5 more at a time
  1940→
  1941→            btn.disabled = true;
  1942→            btn.textContent = 'Loading...';
  1943→
  1944→            try {
  1945→                const response = await fetch(`api.php?action=list_ai_responses&thread_id=${currentThreadId}&limit=${limit}&offset=${currentOffset}`);
  1946→                const data = await response.json();
  1947→
  1948→                if (data.success && data.responses && data.responses.length > 0) {
  1949→                    const container = document.getElementById('saved-responses');
  1950→
  1951→                    // Append new responses
  1952→                    data.responses.forEach(saved => {
  1953→                        // Determine what to display as the "question"
  1954→                        let displayQuestion = saved.question || 'General Analysis';
  1955→                        const hasContext = saved.context && saved.context.length > 0;
  1956→                        const isDefaultQuestion = displayQuestion.startsWith('Please analyze this email conversation');
  1957→
  1958→                        if (isDefaultQuestion && hasContext) {
  1959→                            displayQuestion = '[Context] ' + saved.context;
  1960→                        } else if (hasContext) {
  1961→                            displayQuestion = displayQuestion + ' (+context)';
  1962→                        }
  1963→
  1964→                        // Truncate to 200 chars
  1965→                        if (displayQuestion.length > 200) {
  1966→                            displayQuestion = displayQuestion.substring(0, 200) + '...';
  1967→                        }
  1968→
  1969→                        // Get response excerpt
  1970→                        const responseText = saved.response?.response || '';
  1971→                        let responseExcerpt = responseText.length > 120
  1972→                            ? responseText.substring(0, 120) + '...'
  1973→                            : responseText;
  1974→
  1975→                        const responseEl = document.createElement('div');
  1976→                        responseEl.className = 'saved-response';
  1977→                        responseEl.id = 'saved-response-' + saved._id;
  1978→                        responseEl.dataset.id = saved._id;
  1979→                        responseEl.innerHTML = `
  1980→

  1981→                                ${escapeHtml(displayQuestion)}
  1982→                                ${saved.created_at || 'Unknown date'}
  1983→

  1984→

  1985→                                ${escapeHtml(saved.provider || 'Unknown')} /
  1986→                                ${escapeHtml(saved.model || 'Unknown')}
  1987→

  1988→                            ${responseExcerpt ? `<div class="saved-response-excerpt">${escapeHtml(responseExcerpt)}</div>` : ''}
  1989→

  1990→                                <button class="btn-view" onclick="event.stopPropagation(); quickViewResponse('${saved._id}')">
  1991→                                    👁 Quick View
  1992→                                </button>
  1993→                                <button class="btn-expand" onclick="event.stopPropagation(); loadSavedResponse('${saved._id}')">
  1994→                                    📄 Full View
  1995→                                </button>
  1996→

  1997→                        `;
  1998→
  1999→                        container.appendChild(responseEl);
  2000→                    });
  2001→
  2002→                    // Update offset
  2003→                    const newOffset = currentOffset + data.responses.length;
  2004→                    btn.dataset.offset = newOffset;
  2005→
  2006→                    // Update button text or hide if no more
  2007→                    const remaining = total - newOffset;
  2008→                    if (remaining > 0) {
  2009→                        btn.textContent = `Load More (${remaining} remaining)`;
  2010→                        btn.disabled = false;
  2011→                    } else {
  2012→                        btn.parentElement.style.display = 'none';
  2013→                    }
  2014→                } else {
  2015→                    btn.parentElement.style.display = 'none';
  2016→                }
  2017→            } catch (error) {
  2018→                console.error('Failed to load more responses:', error);
  2019→                btn.textContent = 'Error - Try Again';
  2020→                btn.disabled = false;
  2021→            }
  2022→        }
  2023→
  2024→        // Escape HTML
  2025→        function escapeHtml(text) {
  2026→            if (!text) return '';
  2027→            const div = document.createElement('div');
  2028→            div.textContent = text;
  2029→            return div.innerHTML;
  2030→        }
  2031→    </script>
  2032→

content.id = 'response-tab-' + tabIndex;

                if (response.success) {
                    content.innerHTML = `

                                ${providerConfig.name}

                                    Model: ${response.model || 'Unknown'} |
                                    Time: ${response.request_time?.toFixed(2) || '?'}s |
                                    Cost: $${response.cost?.toFixed(6) || '0'}

${escapeHtml(response.response)}

                    `;
                } else {
                    content.innerHTML = `

                            Error:

${escapeHtml(response.error || 'Unknown error')}

                    `;
                }

                contentContainer.appendChild(content);
                tabIndex++;
            }

            // Scroll to response
            container.scrollIntoView({ behavior: 'smooth' });
        }

        // Switch response tabs - scoped to response container only
        function switchResponseTab(index) {
            const tabsContainer = document.getElementById('response-tabs');
            const contentContainer = document.getElementById('ai-responses-content');

            if (!tabsContainer || !contentContainer) return;

            // Only target tabs within our container
            tabsContainer.querySelectorAll('.response-tab').forEach(tab => {
                const tabIndex = parseInt(tab.dataset.index);
                tab.classList.remove('active');
                if (tabIndex === index) {
                    tab.classList.add('active');
                }
            });

            // Only target content within our container
            contentContainer.querySelectorAll('.response-content').forEach((content, i) => {
                content.classList.remove('active');
                if (i === index) {
                    content.classList.add('active');
                }
            });
        }

        // Close response
        function closeResponse() {
            document.getElementById('ai-response-container').style.display = 'none';
        }

        // Mark a saved response row as last viewed
        function markAsLastViewed(responseId) {
            // Remove last-viewed from all rows
            document.querySelectorAll('.saved-response.last-viewed').forEach(el => {
                el.classList.remove('last-viewed');
            });
            // Add to the current row
            const row = document.getElementById('saved-response-' + responseId);
            if (row) {
                row.classList.add('last-viewed');
            }
        }

        // Load saved response
        async function loadSavedResponse(responseId) {
            try {
                // Mark this row as last viewed
                markAsLastViewed(responseId);

                document.getElementById('loading-overlay').classList.add('active');
                document.getElementById('loading-message').textContent = 'Loading saved response...';

                const response = await fetch(`api.php?action=get_ai_response&id=${responseId}`);
                const data = await response.json();

                if (data.success && data.response) {
                    const saved = data.response;

                    // Display it
                    const container = document.getElementById('ai-response-container');
                    const tabsContainer = document.getElementById('response-tabs');
                    const contentContainer = document.getElementById('ai-responses-content');

                    container.style.display = 'block';
                    tabsContainer.innerHTML = `<button class="response-tab active">${saved.provider}</button>`;

                    const aiResponse = saved.response || {};

                    // Determine how to display question/context
                    const isDefaultQuestion = saved.question && saved.question.startsWith('Please analyze this email conversation');
                    const hasContext = saved.context && saved.context.length > 0;

                    let questionHtml = '';
                    if (isDefaultQuestion && hasContext) {
                        // Show context as the main input if question was default
                        questionHtml = `

User Input (Context):

${escapeHtml(saved.context)}

Default question was used for analysis

                        `;
                    } else if (hasContext) {
                        // Show both question and context
                        questionHtml = `

Question: ${escapeHtml(saved.question)}

Additional Context:

${escapeHtml(saved.context)}

                        `;
                    } else {
                        // Just show question
                        questionHtml = `<p><strong>Question:</strong> ${escapeHtml(saved.question)}</p>`;
                    }

                    contentContainer.innerHTML = `

                                    ${saved.provider} - ${saved.model}
                                    Saved: ${saved.created_at}

                                ${questionHtml}

${escapeHtml(aiResponse.response || 'No response content')}

                    `;

                    container.scrollIntoView({ behavior: 'smooth' });
                }
            } catch (error) {
                console.error('Failed to load saved response:', error);
                alert('Failed to load saved response');
            } finally {
                document.getElementById('loading-overlay').classList.remove('active');
            }
        }

        // Quick view response in modal (no page scroll)
        async function quickViewResponse(responseId) {
            try {
                // Mark this row as last viewed
                markAsLastViewed(responseId);

                const overlay = document.getElementById('quick-view-overlay');
                overlay.style.display = 'flex';
                document.getElementById('quick-view-response').textContent = 'Loading...';
                document.getElementById('quick-view-meta').innerHTML = '';
                document.getElementById('quick-view-question-section').innerHTML = '';

                const response = await fetch(`api.php?action=get_ai_response&id=${responseId}`);
                const data = await response.json();

                if (data.success && data.response) {
                    const saved = data.response;
                    const aiResponse = saved.response || {};

                    // Update title
                    document.getElementById('quick-view-title').textContent = `${saved.provider} / ${saved.model}`;

                    // Update meta
                    document.getElementById('quick-view-meta').innerHTML = `

Provider: ${escapeHtml(saved.provider)}

Model: ${escapeHtml(saved.model)}

Date: ${escapeHtml(saved.created_at)}

Cost: $${(saved.total_cost || 0).toFixed(6)}

                    `;

                    // Update question/context section
                    const isDefaultQuestion = saved.question && saved.question.startsWith('Please analyze this email conversation');
                    const hasContext = saved.context && saved.context.length > 0;

                    let questionHtml = '';
                    if (isDefaultQuestion && hasContext) {
                        questionHtml = `

                                User Context:

                                ${escapeHtml(saved.context)}

Default analysis question was used

                        `;
                    } else if (hasContext) {
                        questionHtml = `

                                Question: ${escapeHtml(saved.question)}

                                Context: ${escapeHtml(saved.context)}

                        `;
                    } else {
                        questionHtml = `

                                Question: ${escapeHtml(saved.question)}

                        `;
                    }
                    document.getElementById('quick-view-question-section').innerHTML = questionHtml;

                    // Update response
                    document.getElementById('quick-view-response').textContent = aiResponse.response || 'No response content';
                } else {
                    document.getElementById('quick-view-response').textContent = 'Error loading response';
                }
            } catch (error) {
                console.error('Failed to quick view response:', error);
                document.getElementById('quick-view-response').textContent = 'Error: ' + error.message;
            }
        }

        // Close quick view modal
        function closeQuickView(event) {
            if (event && event.target !== event.currentTarget) return;
            document.getElementById('quick-view-overlay').style.display = 'none';
        }

        // Close quick view on Escape key
        document.addEventListener('keydown', function(e) {
            if (e.key === 'Escape') {
                closeQuickView();
            }
        });

        // Load more saved responses
        async function loadMoreResponses() {
            const btn = document.getElementById('load-more-btn');
            if (!btn) return;

            const currentOffset = parseInt(btn.dataset.offset) || 0;
            const total = parseInt(btn.dataset.total) || 0;
            const limit = 5; // Load 5 more at a time

            btn.disabled = true;
            btn.textContent = 'Loading...';

            try {
                const response = await fetch(`api.php?action=list_ai_responses&thread_id=${currentThreadId}&limit=${limit}&offset=${currentOffset}`);
                const data = await response.json();

                if (data.success && data.responses && data.responses.length > 0) {
                    const container = document.getElementById('saved-responses');

                    // Append new responses
                    data.responses.forEach(saved => {
                        // Determine what to display as the "question"
                        let displayQuestion = saved.question || 'General Analysis';
                        const hasContext = saved.context && saved.context.length > 0;
                        const isDefaultQuestion = displayQuestion.startsWith('Please analyze this email conversation');

                        if (isDefaultQuestion && hasContext) {
                            displayQuestion = '[Context] ' + saved.context;
                        } else if (hasContext) {
                            displayQuestion = displayQuestion + ' (+context)';
                        }

                        // Truncate to 200 chars
                        if (displayQuestion.length > 200) {
                            displayQuestion = displayQuestion.substring(0, 200) + '...';
                        }

                        // Get response excerpt
                        const responseText = saved.response?.response || '';
                        let responseExcerpt = responseText.length > 120
                            ? responseText.substring(0, 120) + '...'
                            : responseText;

                        const responseEl = document.createElement('div');
                        responseEl.className = 'saved-response';
                        responseEl.id = 'saved-response-' + saved._id;
                        responseEl.dataset.id = saved._id;
                        responseEl.innerHTML = `

                                ${escapeHtml(displayQuestion)}
                                ${saved.created_at || 'Unknown date'}

                                ${escapeHtml(saved.provider || 'Unknown')} /
                                ${escapeHtml(saved.model || 'Unknown')}

                            ${responseExcerpt ? `<div class="saved-response-excerpt">${escapeHtml(responseExcerpt)}</div>` : ''}

                                <button class="btn-view" onclick="event.stopPropagation(); quickViewResponse('${saved._id}')">
                                    👁 Quick View
                                </button>
                                <button class="btn-expand" onclick="event.stopPropagation(); loadSavedResponse('${saved._id}')">
                                    📄 Full View
                                </button>

                        `;

                        container.appendChild(responseEl);
                    });

                    // Update offset
                    const newOffset = currentOffset + data.responses.length;
                    btn.dataset.offset = newOffset;

                    // Update button text or hide if no more
                    const remaining = total - newOffset;
                    if (remaining > 0) {
                        btn.textContent = `Load More (${remaining} remaining)`;
                        btn.disabled = false;
                    } else {
                        btn.parentElement.style.display = 'none';
                    }
                } else {
                    btn.parentElement.style.display = 'none';
                }
            } catch (error) {
                console.error('Failed to load more responses:', error);
                btn.textContent = 'Error - Try Again';
                btn.disabled = false;
            }
        }

        // Escape HTML
        function escapeHtml(text) {
            if (!text) return '';
            const div = document.createElement('div');
            div.textContent = text;
            return div.innerHTML;
        }
    </script>


Now I have a thorough understanding of the existing thread_view.php. Let me also check the analysis-status section in app.js and the getThreadsSQL method in QueryAdapter.

Let me also check the index.php analysis status section and the app.js DOMContentLoaded to understand the analysis refresh.


Let me check the app.js DOMContentLoaded for the analysis refresh and the getThreadsSQL method.


968:            refreshAnalysisStatus();
1515:async function refreshAnalysisStatus() {
1589:    refreshAnalysisStatus();
1592:    setInterval(refreshAnalysisStatus, 30000);


   514→    private function getThreadsSQL(?int $contactId, string $search, int $limit, int $offset): array
   515→    {
   516→        $where = 'e.thread_id IS NOT NULL AND e.thread_id != ""';
   517→        $params = [];
   518→
   519→        if ($contactId) {
   520→            $where .= ' AND e.contact_id = ?';
   521→            $params[] = $contactId;
   522→        }
   523→
   524→        if ($search) {
   525→            $where .= ' AND e.subject LIKE ?';
   526→            $params[] = '%' . $search . '%';
   527→        }
   528→
   529→        // Get total thread count
   530→        $total = $this->db->fetchColumn(
   531→            "SELECT COUNT(DISTINCT e.thread_id)
   532→             FROM emails e
   533→             WHERE {$where}",
   534→            $params
   535→        );
   536→
   537→        // Get threads with summary info
   538→        $threads = $this->db->fetchAll(
   539→            "SELECT
   540→                e.thread_id,
   541→                MIN(e.id) as first_email_id,
   542→                MAX(e.id) as last_email_id,
   543→                MIN(e.subject) as subject,
   544→                MIN(e.date) as first_date,
   545→                MAX(e.date) as last_date,
   546→                COUNT(*) as email_count,
   547→                SUM(CASE WHEN e.direction = 'inbound' THEN 1 ELSE 0 END) as inbound_count,
   548→                SUM(CASE WHEN e.direction = 'outbound' THEN 1 ELSE 0 END) as outbound_count,
   549→                MAX(e.contact_id) as contact_id,
   550→                MAX(c.name) as contact_name,
   551→                ta.is_analyzed,
   552→                ta.needs_reanalysis
   553→             FROM emails e
   554→             LEFT JOIN contacts c ON e.contact_id = c.id
   555→             LEFT JOIN thread_analysis ta ON e.thread_id = ta.thread_id
   556→             WHERE {$where}
   557→             GROUP BY e.thread_id
   558→             ORDER BY MAX(e.date) DESC
   559→             LIMIT ? OFFSET ?",
   560→            array_merge($params, [$limit, $offset])
   561→        );
   562→
   563→        return [
   564→            'threads' => $threads,
   565→            'total' => (int)$total
   566→        ];
   567→    }
   568→

    private function getThreadsSQL(?int $contactId, string $search, int $limit, int $offset): array
    {
        $where = 'e.thread_id IS NOT NULL AND e.thread_id != ""';
        $params = [];

        if ($contactId) {
            $where .= ' AND e.contact_id = ?';
            $params[] = $contactId;
        }

        if ($search) {
            $where .= ' AND e.subject LIKE ?';
            $params[] = '%' . $search . '%';
        }

        // Get total thread count
        $total = $this->db->fetchColumn(
            "SELECT COUNT(DISTINCT e.thread_id)
             FROM emails e
             WHERE {$where}",
            $params
        );

        // Get threads with summary info
        $threads = $this->db->fetchAll(
            "SELECT
                e.thread_id,
                MIN(e.id) as first_email_id,
                MAX(e.id) as last_email_id,
                MIN(e.subject) as subject,
                MIN(e.date) as first_date,
                MAX(e.date) as last_date,
                COUNT(*) as email_count,
                SUM(CASE WHEN e.direction = 'inbound' THEN 1 ELSE 0 END) as inbound_count,
                SUM(CASE WHEN e.direction = 'outbound' THEN 1 ELSE 0 END) as outbound_count,
                MAX(e.contact_id) as contact_id,
                MAX(c.name) as contact_name,
                ta.is_analyzed,
                ta.needs_reanalysis
             FROM emails e
             LEFT JOIN contacts c ON e.contact_id = c.id
             LEFT JOIN thread_analysis ta ON e.thread_id = ta.thread_id
             WHERE {$where}
             GROUP BY e.thread_id
             ORDER BY MAX(e.date) DESC
             LIMIT ? OFFSET ?",
            array_merge($params, [$limit, $offset])
        );

        return [
            'threads' => $threads,
            'total' => (int)$total
        ];
    }


78:            loadContacts();
81:            loadEmails();
163:async function loadContacts(page = 1) {
230:        renderPagination('contacts-pagination', data.pagination, loadContacts);
376:    loadEmails(1, contactId);
642:async function loadEmails(page = 1, contactId = null) {
702:        renderPagination('emails-pagination', data.pagination, (p) => loadEmails(p));
1569:document.addEventListener('DOMContentLoaded', function() {
1574:            searchTimeout = setTimeout(() => loadContacts(1), 300);
1580:        typeFilter.addEventListener('change', () => loadContacts(1));
2574:            setTimeout(() => loadContacts(state.contacts.page), 2000);
3937:document.addEventListener('DOMContentLoaded', function() {

[Showing results with pagination = limit: 30, offset: 0]


Let me check the loadEmails limit and the DOMContentLoaded section.


Let me check the loadEmails limit and the DOMContentLoaded initialization.


642→async function loadEmails(page = 1, contactId = null) {
   643→    const container = document.getElementById('emails-list');
   644→    container.innerHTML = '
Loading threads...
';
   645→
   646→    try {
   647→        state.emails.page = page;
   648→        if (contactId !== null) state.emails.contactId = contactId;
   649→
   650→        const params = { page: page, limit: 20 };
   651→        if (state.emails.contactId) params.contact = state.emails.contactId;
   652→
   653→        const data = await apiCall('threads', params);
   654→
   655→        if (data.threads.length === 0) {
   656→            container.innerHTML = '
No threads found
';
   657→            return;
   658→        }
   659→
   660→        let html = `
   661→

   662→                💡 Thread View: Each row is a complete conversation (all back-and-forth emails grouped together).
   663→                Click to see what Claude will analyze.
   664→

   665→

   666→

   667→

   668→                        Subject
   669→                        Contact
   670→                        Emails
   671→                        Status

async function loadEmails(page = 1, contactId = null) {
    const container = document.getElementById('emails-list');
    container.innerHTML = '
Loading threads...
';

    try {
        state.emails.page = page;
        if (contactId !== null) state.emails.contactId = contactId;

        const params = { page: page, limit: 20 };
        if (state.emails.contactId) params.contact = state.emails.contactId;

        const data = await apiCall('threads', params);

        if (data.threads.length === 0) {
            container.innerHTML = '
No threads found
';
            return;
        }

        let html = `

                💡 Thread View: Each row is a complete conversation (all back-and-forth emails grouped together).
                Click to see what Claude will analyze.

                        Subject
                        Contact
                        Emails
                        Status


  1569→document.addEventListener('DOMContentLoaded', function() {
  1570→    const contactSearch = document.getElementById('contact-search');
  1571→    if (contactSearch) {
  1572→        contactSearch.addEventListener('input', function() {
  1573→            clearTimeout(searchTimeout);
  1574→            searchTimeout = setTimeout(() => loadContacts(1), 300);
  1575→        });
  1576→    }
  1577→
  1578→    const typeFilter = document.getElementById('contact-type-filter');
  1579→    if (typeFilter) {
  1580→        typeFilter.addEventListener('change', () => loadContacts(1));
  1581→    }
  1582→
  1583→    const outcomeFilter = document.getElementById('quote-outcome-filter');
  1584→    if (outcomeFilter) {
  1585→        outcomeFilter.addEventListener('change', () => loadQuotes());
  1586→    }
  1587→
  1588→    // Load analysis status on page load
  1589→    refreshAnalysisStatus();
  1590→
  1591→    // Auto-refresh analysis status every 30 seconds
  1592→    setInterval(refreshAnalysisStatus, 30000);
  1593→
  1594→    // Set up semantic search input
  1595→    const semanticInput = document.getElementById('semantic-search-input');
  1596→    if (semanticInput) {
  1597→        semanticInput.addEventListener('keypress', function(e) {
  1598→            if (e.key === 'Enter') {

document.addEventListener('DOMContentLoaded', function() {
    const contactSearch = document.getElementById('contact-search');
    if (contactSearch) {
        contactSearch.addEventListener('input', function() {
            clearTimeout(searchTimeout);
            searchTimeout = setTimeout(() => loadContacts(1), 300);
        });
    }

    const typeFilter = document.getElementById('contact-type-filter');
    if (typeFilter) {
        typeFilter.addEventListener('change', () => loadContacts(1));
    }

    const outcomeFilter = document.getElementById('quote-outcome-filter');
    if (outcomeFilter) {
        outcomeFilter.addEventListener('change', () => loadQuotes());
    }

    // Load analysis status on page load
    refreshAnalysisStatus();

    // Auto-refresh analysis status every 30 seconds
    setInterval(refreshAnalysisStatus, 30000);

    // Set up semantic search input
    const semanticInput = document.getElementById('semantic-search-input');
    if (semanticInput) {
        semanticInput.addEventListener('keypress', function(e) {
            if (e.key === 'Enter') {


   960→    showLoading('Analyzing thread with Claude AI... This may take up to 90 seconds.');
   961→    closeModal('email-modal');
   962→
   963→    try {
   964→        const data = await apiCall('analyze_thread', { thread_id: threadId }, 'POST');
   965→
   966→        if (data.success) {
   967→            alert(`Thread Analyzed Successfully!\n\n${data.summary || 'Analysis complete.'}`);
   968→            refreshAnalysisStatus();
   969→            // Reopen the thread to show results
   970→            showThread(threadId);
   971→        } else {
   972→            alert('Analysis failed: ' + (data.error || 'Unknown error'));
   973→        }
   974→    } catch (error) {

    showLoading('Analyzing thread with Claude AI... This may take up to 90 seconds.');
    closeModal('email-modal');

    try {
        const data = await apiCall('analyze_thread', { thread_id: threadId }, 'POST');

        if (data.success) {
            alert(`Thread Analyzed Successfully!\n\n${data.summary || 'Analysis complete.'}`);
            refreshAnalysisStatus();
            // Reopen the thread to show results
            showThread(threadId);
        } else {
            alert('Analysis failed: ' + (data.error || 'Unknown error'));
        }
    } catch (error) {


Now I have a comprehensive understanding of all the files and issues. Let me now write the final plan to the plan file. The plan needs to cover 4 tasks:

1. Enhance thread_view.php with agents, context builder, tokens, logging
2. Speed up main page (reduce limits)
3. Fix thread contact name bug
4. Hide AI Thread Analysis section

Let me write a clear, actionable plan.


Now I have a thorough understanding of all the code. Let me write the final plan.


Write

# Plan: Enhance thread_view.php + Fix Main Dashboard Issues

## Context

The user found `thread_view.php` already has a working AI form with multi-provider support, conversation history (sessionStorage), saved responses, and quick view. They want the new AI features (agents, context builder, token estimation, debug logging) merged into this existing page. Additionally, the main dashboard has 3 bugs: slow loading, wrong contact names on threads, and a useless AI Thread Analysis section.

## 4 Tasks (in execution order)

---

### TASK 1: Hide AI Thread Analysis section (index.php + app.js)

**Problem:** The "AI Thread Analysis" section shows "Unknown" contacts, runs heavy N+1 queries, and auto-refreshes every 30 seconds — slowing the page.

**Files:**

| File | Change |
|------|--------|
| [public/index.php:136](public/index.php#L136) | Add `style="display:none"` to `#analysis-status-section` div |
| [public/assets/app.js:1589](public/assets/app.js#L1589) | Comment out `refreshAnalysisStatus()` call |
| [public/assets/app.js:1592](public/assets/app.js#L1592) | Comment out `setInterval(refreshAnalysisStatus, 30000)` |

---

### TASK 2: Speed up main page (app.js)

**Problem:** Threads and contacts load 20 records each — too many for initial page load.

**Files:**

| File | Change |
|------|--------|
| [public/assets/app.js:176](public/assets/app.js#L176) | `loadContacts` → change `limit: 20` to `limit: 10` |
| [public/assets/app.js:650](public/assets/app.js#L650) | `loadEmails` → change `limit: 20` to `limit: 10` |

---

### TASK 3: Fix thread contact name bug (QueryAdapter.php)

**Problem:** `getThreadsMongo()` line 609 uses `'$max' => '$contact_id'` which picks the numerically highest contact_id (always "Etienne Turner"). Same issue in `getThreadsSQL()` line 549-550 with `MAX(e.contact_id)` and `MAX(c.name)`.

**Fix approach:**

**MongoDB** ([src/QueryAdapter.php:569-650](src/QueryAdapter.php#L569)):
1. In the `$group` stage, collect all contacts instead of `$max`:
   - Replace `'contact_id' => ['$max' => '$contact_id']` with `'contacts' => ['$push' => ['cid' => '$contact_id', 'email' => '$from_email', 'dir' => '$direction']]`
2. After aggregation, post-process each thread in PHP:
   - Filter out contacts where `email === GMAIL_USER_EMAIL` (the user's own email)
   - Filter to only `direction === 'inbound'` contacts
   - Count occurrences of each remaining `contact_id`
   - Pick the most frequent one
   - If none found, fall back to the first non-user contact_id
3. Use the resolved contact_id for the `$lookup` (or do a separate contact name lookup after post-processing)

**SQL** ([src/QueryAdapter.php:514-567](src/QueryAdapter.php#L514)):
1. Replace `MAX(e.contact_id)` with a subquery that finds the most frequent inbound sender:
   ```sql
   (SELECT e2.contact_id FROM emails e2
    LEFT JOIN contacts c2 ON e2.contact_id = c2.id
    WHERE e2.thread_id = e.thread_id
    AND e2.direction = 'inbound'
    AND LOWER(e2.from_email) != ?
    GROUP BY e2.contact_id
    ORDER BY COUNT(*) DESC LIMIT 1) as contact_id
   ```
2. Similar subquery for contact_name.
3. Add `GMAIL_USER_EMAIL` as bind parameter.

---

### TASK 4: Enhance thread_view.php (the main task)

**Problem:** thread_view.php needs agents, cross-thread context builder, token estimation, and debug logging — while keeping existing multi-provider simultaneous sending.

**Current architecture (keep):**
- Multi-provider checkboxes → sends to `api_opus/api/query.php` simultaneously
- Conversation history in sessionStorage
- Saved responses in MongoDB `ai_query_responses`
- Quick view modal, load more responses
- Content processing modes (full/stripped/smart)

**New features to add:**

#### 4A. Full-width layout
- [thread_view.php:174](public/thread_view.php#L174): Change `.page-container { max-width: 1600px }` to `max-width: 100%; padding: 20px 30px;`
- [thread_view.php:213-217](public/thread_view.php#L213): Change `.main-grid` from `grid-template-columns: 1fr 400px` to `grid-template-columns: 1fr 500px` (wider AI panel)

#### 4B. Agent buttons with quick prompts
Add above the provider grid in the right column (after line 975):

```html
<!-- Agent Selector -->
<div class="ai-form-section">
    <label>AI Agent</label>
    <div class="agent-buttons" id="agent-buttons">
        <!-- Populated by JS -->
    </div>
    <div class="agent-quick-prompts" id="agent-quick-prompts" style="display:none;">
        <!-- Quick prompt buttons per agent -->
    </div>
</div>
```

**JS agent presets** (add as constants near top of `<script>` block):
```js
const AI_AGENTS = {
    general: { name: 'General', icon: '💬', systemPrompt: '...', quickPrompts: [...] },
    quote_analyst: { name: 'Quotes', icon: '💰', systemPrompt: '...', quickPrompts: [...] },
    deception_detector: { name: 'Deception', icon: '🔍', systemPrompt: '...', quickPrompts: [...] },
    contact_profiler: { name: 'Profiler', icon: '👤', systemPrompt: '...', quickPrompts: [...] },
    email_composer: { name: 'Composer', icon: '✍', systemPrompt: '...', quickPrompts: [...] }
};
```

When an agent is selected:
- Highlight the active agent button
- Show agent-specific quick prompts below the buttons
- Clicking a quick prompt fills the question textarea
- The agent's `systemPrompt` is prepended to the content sent to AI

#### 4C. Context builder (search + add threads from any contact)
Add a collapsible section between "Include Other Threads" and "Ask AI" cards:

```html
<!-- Context Builder -->
<div class="card">
    <div class="card-header">
        <h2>+ Add More Context</h2>
        <button class="btn btn-small" onclick="toggleContextBuilder()">Search</button>
    </div>
    <div id="context-builder" style="display:none;">
        <input type="text" id="context-search" placeholder="Search contacts or threads...">
        <div id="context-search-results"></div>
    </div>
    <div id="context-cart"></div>
</div>
```

**JS functions:**
- `toggleContextBuilder()` — show/hide search
- `searchContext(query)` — calls `api.php?action=contacts&search=...` AND `api.php?action=threads&search=...` in parallel
- Results show contacts (click to see their threads with "+Add" buttons) and threads (with "+Add" directly)
- `addThreadToContext(threadId, subject, emailCount)` — adds to `contextThreads[]` array
- `removeThreadFromContext(threadId)` — removes
- `updateContextCart()` — renders green summary: "3 additional threads, ~25 emails"
- When `submitToAI()` runs, fetches email content for each context thread (via `api.php?action=thread&thread_id=...`) and appends to the content

#### 4D. Token estimation
Add a live token counter near the submit button:

```html
<div id="token-estimate" style="font-size:0.85em; color:var(--text-secondary); margin-bottom:10px;">
    Estimated: ~0 tokens
</div>
```

**JS:**
- `MODEL_LIMITS` constant mapping model names to max context (e.g. `{'claude-sonnet-4-5-20250929': 200000, 'gpt-4o': 128000, ...}`)
- `estimateTokens()` — called on: agent change, context change, question typing, provider toggle
  - Calculates `Math.ceil(totalContentLength / 3.5)`
  - Shows warning if approaching any selected model's limit
  - Compares against the **smallest** selected model's limit (since multi-provider)
  - Display: "Estimated: ~12,500 tokens (OK for all selected models)" or "Warning: exceeds gpt-4o limit (128K)"
- In `submitToAI()`: if over limit, confirm before sending

#### 4E. System prompt injection per agent
In `submitToAI()`, prepend the selected agent's system prompt:

```js
let systemPrompt = '';
if (selectedAgent && AI_AGENTS[selectedAgent]) {
    systemPrompt = AI_AGENTS[selectedAgent].systemPrompt + '\n\n';
}
// Prepend to fullContent
fullContent = systemPrompt + fullContent;
```

This works because `api_opus/query.php` accepts `user_content` as one big string — the system prompt at the top guides the AI's behavior.

#### 4F. Debug file logging
**New API endpoint** in [public/api.php](public/api.php) — `log_ai_debug`:

```php
case 'log_ai_debug':
    $logDir = dirname(__DIR__) . '/logs';
    if (!is_dir($logDir)) mkdir($logDir, 0755, true);
    $logFile = $logDir . '/ai_debug.log';
    $logData = json_decode(file_get_contents('php://input'), true);
    $logEntry = [
        'timestamp' => date('Y-m-d H:i:s'),
        'thread_id' => $logData['thread_id'] ?? '',
        'agent' => $logData['agent'] ?? '',
        'providers' => $logData['providers'] ?? [],
        'content_length' => $logData['content_length'] ?? 0,
        'estimated_tokens' => $logData['estimated_tokens'] ?? 0,
        'question' => $logData['question'] ?? '',
        'type' => $logData['type'] ?? 'request',  // 'request' or 'response'
        'response_preview' => substr($logData['response'] ?? '', 0, 500),
        'full_content' => $logData['full_content'] ?? '' // full payload for debug
    ];
    file_put_contents($logFile, json_encode($logEntry) . "\n", FILE_APPEND | LOCK_EX);
    echo json_encode(['success' => true]);
    break;
```

**JS:** Call this endpoint in `submitToAI()`:
1. **Before** sending to AI: log the request (type: 'request', full_content, providers, tokens)
2. **After** receiving response: log the response (type: 'response', response preview, costs, timing)

Log file: `/var/www/mssql.danmarcrm.com/dev1/extension2v1/logs/ai_debug.log` — one JSON line per entry.

#### 4G. Contact memory injection (bonus)
If `primaryContactId` exists, fetch memories from `api.php?action=ai_contact_memories&contact_id=X` and inject as "KNOWN FACTS ABOUT [CONTACT]" section in the content sent to AI. Display memories as pills in the context area.

---

## Implementation Order

1. **TASK 1** — Hide AI Thread Analysis (3 line changes in index.php + app.js)
2. **TASK 2** — Speed up main page (2 line changes in app.js)
3. **TASK 3** — Fix thread contact name bug (QueryAdapter.php — MongoDB + SQL)
4. **TASK 4** — Enhance thread_view.php:
   - 4A: Full-width layout (CSS changes)
   - 4B: Agent buttons + quick prompts (HTML + JS)
   - 4C: Context builder (HTML + JS)
   - 4D: Token estimation (HTML + JS)
   - 4E: System prompt injection (JS modification to submitToAI)
   - 4F: Debug logging endpoint (api.php + JS calls)
   - 4G: Contact memory injection (JS)

## Files Modified (summary)

| File | Tasks |
|------|-------|
| [public/index.php](public/index.php) | T1: hide analysis section |
| [public/assets/app.js](public/assets/app.js) | T1: disable refresh, T2: reduce limits |
| [src/QueryAdapter.php](src/QueryAdapter.php) | T3: fix contact name bug |
| [public/api.php](public/api.php) | T4F: add `log_ai_debug` endpoint |
| [public/thread_view.php](public/thread_view.php) | T4: all thread_view enhancements |

## Verification

1. **Main dashboard:** Load page — should be fast, no analysis section visible, threads show correct contact names (not all "Etienne Turner")
2. **Thread view:** Open a thread — should be full-width, agent buttons visible, context builder expandable
3. **Agents:** Click "Quotes" agent → quick prompts appear → click one → fills question box → submit → AI response guided by quote analysis system prompt
4. **Context builder:** Click "Search" → type contact name → see their threads → click "+Add" → green cart updates → submit → AI sees all added threads
5. **Token counter:** Shows live estimate → add more context → count increases → shows warning if near limit
6. **Multi-provider:** Select Claude + GPT → submit → both respond in tabs (existing behavior preserved)
7. **Debug log:** Check `/var/www/mssql.danmarcrm.com/dev1/extension2v1/logs/ai_debug.log` → has request + response JSON lines
8. **Conversation continuity:** Ask a follow-up question → AI sees previous Q&A in context (existing behavior preserved)
9. **Saved responses:** Response auto-saves to MongoDB → appears in "Previous AI Analyses" (existing behavior preserved)