TodoWrite

Add Two-Round AI checkbox and response selection UI

Implement Round 2 auto-summary logic in submitToAI

Add manual response selection + follow-up feature

Add timeout handling for slow providers

Test and verify PHP syntax


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

Add Two-Round AI checkbox and response selection UI

Implement Round 2 auto-summary logic in submitToAI

Add manual response selection + follow-up feature

Add timeout handling for slow providers

Test and verify PHP syntax

Add timeout handling for slow providers

Test and verify PHP syntax


Now the big JS changes. I need to:
1. Add a `lastRound1Responses` variable to store Round 1 data
2. Modify `displayAIResponse()` to add selection checkboxes on each response
3. Add `sendRound2()` function for auto two-round
4. Add `askFollowUp()` for manual follow-up with selected responses
5. Add timeout handling with `AbortController`
6. Modify `submitToAI()` to support the timeout + auto-round-2 flow

Let me first add the new state variables and functions. I'll add them right after the `logAiDebug` function:


1685→                    var html = '
Threads from ' + escapeHtml(contactName) + '
';
  1686→
  1687→                    if (threads.length > 1) {
  1688→                        html += '
<button class="context-add-btn" onclick="addAllContactThreads()" style="width:100%;text-align:center;">Add All ' + threads.length + ' Threads</button>
';
  1689→                    }
  1690→
  1691→                    // Store for "Add All"
  1692→                    window._lastContactThreads = threads;
  1693→
  1694→                    threads.forEach(function(t) {
  1695→                        var already = contextThreads.some(function(ct) { return ct.threadId === t.thread_id; });
  1696→                        var isCurrent = t.thread_id === currentThreadId;
  1697→                        html += '
';
  1698→                        html += '
';
  1699→                        html += '' + escapeHtml(t.subject || '(No Subject)') + '';

var html = '
Threads from ' + escapeHtml(contactName) + '
';

                    if (threads.length > 1) {
                        html += '
<button class="context-add-btn" onclick="addAllContactThreads()" style="width:100%;text-align:center;">Add All ' + threads.length + ' Threads</button>
';
                    }

                    // Store for "Add All"
                    window._lastContactThreads = threads;

                    threads.forEach(function(t) {
                        var already = contextThreads.some(function(ct) { return ct.threadId === t.thread_id; });
                        var isCurrent = t.thread_id === currentThreadId;
                        html += '
';
                        html += '
';
                        html += '' + escapeHtml(t.subject || '(No Subject)') + '';


1856:        function logAiDebug(data) {


  1856→        function logAiDebug(data) {
  1857→            try {
  1858→                fetch('api.php?action=log_ai_debug', {
  1859→                    method: 'POST',
  1860→                    headers: { 'Content-Type': 'application/json' },
  1861→                    body: JSON.stringify(data)
  1862→                });
  1863→            } catch (e) {
  1864→                console.error('Debug log failed:', e);
  1865→            }
  1866→        }
  1867→
  1868→        // ============================================
  1869→        // CONVERSATION MEMORY - ChatGPT-like history
  1870→        // ============================================

        function logAiDebug(data) {
            try {
                fetch('api.php?action=log_ai_debug', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify(data)
                });
            } catch (e) {
                console.error('Debug log failed:', e);
            }
        }

        // ============================================
        // CONVERSATION MEMORY - ChatGPT-like history
        // ============================================


Now I'll add the two-round and follow-up state + functions after `logAiDebug`:


Edit

// ============================================
        // TWO-ROUND AI & FOLLOW-UP
        // ============================================
        var lastRound1Responses = {}; // {providerName: {success, response, model, ...}}
        var lastRound1Providers = [];
        var lastRound1Models = {};
        var lastRound1Question = '';
        var lastFullContent = '';

        // Send Round 2 with all Round 1 responses as context
        function sendRound2(round1Responses, providersList, modelsMap, originalQuestion) {
            var successfulResponses = [];
            for (var prov in round1Responses) {
                var r = round1Responses[prov];
                if (r && r.success && r.response) {
                    successfulResponses.push({
                        provider: prov,
                        model: r.model || 'unknown',
                        response: r.response
                    });
                }
            }

            if (successfulResponses.length === 0) {
                console.warn('No successful Round 1 responses for Round 2');
                return;
            }

            // Build Round 2 prompt
            var round2Content = '=== ROUND 2: UNIFIED ANALYSIS ===\n';
            round2Content += 'You previously analyzed an email conversation. Below are the analyses from ' + successfulResponses.length + ' different AI models.\n';
            round2Content += 'Your task: Synthesize ALL analyses into a single comprehensive summary. Highlight where models AGREE and where they DISAGREE. Provide a final unified assessment.\n\n';
            round2Content += 'Original question: ' + originalQuestion + '\n\n';

            successfulResponses.forEach(function(sr, idx) {
                round2Content += '--- ANALYSIS FROM ' + sr.provider.toUpperCase() + ' (' + sr.model + ') ---\n';
                // Cap each response to 4000 chars to stay within limits
                var respText = sr.response;
                if (respText.length > 4000) {
                    respText = respText.substring(0, 4000) + '\n...(truncated)';
                }
                round2Content += respText + '\n\n';
            });

            round2Content += '=== END OF ROUND 1 ANALYSES ===\n\n';
            round2Content += 'Please provide a unified, comprehensive summary that synthesizes all the above analyses. Note agreements, disagreements, and your overall assessment.';

            // Check token budget
            var r2Tokens = Math.ceil(round2Content.length / 3.5);
            var modelLimit = getSmallestModelLimit();
            var maxChars = Math.floor(modelLimit * 3.5 * 0.85);
            if (round2Content.length > maxChars) {
                round2Content = round2Content.substring(0, maxChars) + '\n\n...(Content truncated to fit model limit)';
                r2Tokens = Math.ceil(round2Content.length / 3.5);
            }

            console.log('Round 2: ' + round2Content.length + ' chars, ~' + r2Tokens + ' tokens, ' + providersList.length + ' providers');

            document.getElementById('loading-overlay').classList.add('active');
            document.getElementById('loading-message').textContent = 'Round 2: Synthesizing with ' + providersList.length + ' AI(s)...';

            var requestBody = {
                user_content: round2Content,
                providers: providersList
            };
            for (var p in modelsMap) {
                requestBody['model_' + p] = modelsMap[p];
            }

            var timeoutSec = parseInt(document.getElementById('provider-timeout').value) || 120;

            logAiDebug({
                type: 'request_round2',
                thread_id: currentThreadId,
                agent: selectedAgent,
                providers: providersList,
                content_length: round2Content.length,
                estimated_tokens: r2Tokens,
                question: 'Round 2 synthesis of ' + successfulResponses.length + ' responses'
            });

            fetchWithTimeout('../aiapi/api_opus/api/query.php', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(requestBody)
            }, timeoutSec * 1000)
            .then(function(response) {
                if (!response.ok) throw new Error('HTTP ' + response.status);
                return response.text();
            })
            .then(function(text) {
                var data;
                try { data = JSON.parse(text); } catch(e) { throw new Error('Invalid JSON from AI API'); }

                if (data.success) {
                    displayRound2Response(data);

                    // Save Round 2 responses
                    providersList.forEach(function(prov) {
                        var pr = data.responses[prov + '_response'];
                        if (pr && pr.success) {
                            saveAIResponse({
                                thread_ids: [currentThreadId],
                                contact_id: primaryContactId,
                                question: 'Round 2 Synthesis: ' + originalQuestion,
                                agent: selectedAgent,
                                content_mode: 'round2',
                                content_length: round2Content.length,
                                estimated_tokens: r2Tokens,
                                provider: prov,
                                model: modelsMap[prov],
                                query_id: data.query_id,
                                response: pr,
                                total_cost: pr.cost || 0,
                                all_providers: providersList,
                                round: 2
                            });
                        }
                    });

                    logAiDebug({
                        type: 'response_round2',
                        thread_id: currentThreadId,
                        providers: providersList,
                        question: 'Round 2 synthesis'
                    });
                } else {
                    alert('Round 2 failed: ' + (data.error || 'Unknown error'));
                }
            })
            .catch(function(err) {
                console.error('Round 2 error:', err);
                alert('Round 2 error: ' + err.message);
            })
            .finally(function() {
                document.getElementById('loading-overlay').classList.remove('active');
                document.getElementById('submit-ai-btn').disabled = false;
            });
        }

        // Display Round 2 responses in separate container
        function displayRound2Response(data) {
            var container = document.getElementById('ai-response-round2-container');
            var tabsContainer = document.getElementById('response-tabs-round2');
            var contentContainer = document.getElementById('ai-responses-content-round2');

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

            var tabIndex = 0;
            for (var key in data.responses) {
                var response = data.responses[key];
                var providerName = key.replace('_response', '');
                var providerConfig = providers[providerName] || { name: providerName };

                var tab = document.createElement('button');
                tab.className = 'response-tab' + (tabIndex === 0 ? ' active' : '');
                tab.textContent = providerConfig.name;
                tab.dataset.index = tabIndex;
                (function(idx) {
                    tab.onclick = function() { switchRound2Tab(idx); };
                })(tabIndex);
                tabsContainer.appendChild(tab);

                var content = document.createElement('div');
                content.className = 'response-content' + (tabIndex === 0 ? ' active' : '');
                content.id = 'response-tab-r2-' + tabIndex;

                if (response.success) {
                    content.innerHTML = '
' +
                        '
' +
                        '' + escapeHtml(providerConfig.name) + ' Round 2' +
                        'Model: ' + escapeHtml(response.model || '?') + ' | Time: ' + (response.request_time ? response.request_time.toFixed(2) : '?') + 's | Cost: $' + (response.cost ? response.cost.toFixed(6) : '0') + '' +
                        '
' +
                        '
' + escapeHtml(response.response) + '
' +
                        '
';
                } else {
                    content.innerHTML = '
' +
                        'Error:' +
                        '
' + escapeHtml(response.error || 'Unknown error') + '

';
                }

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

            container.scrollIntoView({ behavior: 'smooth' });
        }

        function switchRound2Tab(index) {
            var tabs = document.getElementById('response-tabs-round2');
            var content = document.getElementById('ai-responses-content-round2');
            if (!tabs || !content) return;
            tabs.querySelectorAll('.response-tab').forEach(function(t) {
                t.classList.toggle('active', parseInt(t.dataset.index) === index);
            });
            content.querySelectorAll('.response-content').forEach(function(c, i) {
                c.classList.toggle('active', i === index);
            });
        }

        // Manual follow-up: user selects responses and asks a new question
        function askFollowUp() {
            var question = prompt('Enter your follow-up question:');
            if (!question || !question.trim()) return;

            // Collect selected responses
            var selected = getSelectedResponseTexts();
            if (selected.length === 0) {
                alert('Please select at least one response to include as context.');
                return;
            }

            // Build follow-up content
            var followUpContent = '=== FOLLOW-UP QUESTION ===\n';
            followUpContent += 'The user previously asked about an email conversation and received the following AI analyses.\n';
            followUpContent += 'Now they have a follow-up question based on these analyses.\n\n';

            selected.forEach(function(sel) {
                followUpContent += '--- Previous analysis from ' + sel.provider.toUpperCase() + ' (' + sel.model + ') ---\n';
                var text = sel.response;
                if (text.length > 4000) text = text.substring(0, 4000) + '\n...(truncated)';
                followUpContent += text + '\n\n';
            });

            followUpContent += '=== END PREVIOUS ANALYSES ===\n\n';
            followUpContent += 'FOLLOW-UP QUESTION: ' + question.trim();

            // Use currently selected providers
            var providersList = getSelectedProviders();
            if (providersList.length === 0) {
                alert('No AI providers selected.');
                return;
            }

            var modelsMap = {};
            providersList.forEach(function(prov) {
                var sel = document.getElementById('model_' + prov);
                modelsMap[prov] = sel ? sel.value : '';
            });

            // Token check
            var tokens = Math.ceil(followUpContent.length / 3.5);
            var modelLimit = getSmallestModelLimit();
            var maxChars = Math.floor(modelLimit * 3.5 * 0.85);
            if (followUpContent.length > maxChars) {
                followUpContent = followUpContent.substring(0, maxChars) + '\n\n...(truncated)';
            }

            console.log('Follow-up: ' + followUpContent.length + ' chars, ' + providersList.length + ' providers');

            document.getElementById('loading-overlay').classList.add('active');
            document.getElementById('loading-message').textContent = 'Sending follow-up to ' + providersList.length + ' AI(s)...';
            document.getElementById('submit-ai-btn').disabled = true;

            var requestBody = {
                user_content: followUpContent,
                providers: providersList
            };
            for (var p in modelsMap) {
                requestBody['model_' + p] = modelsMap[p];
            }

            var timeoutSec = parseInt(document.getElementById('provider-timeout').value) || 120;

            logAiDebug({
                type: 'request_followup',
                thread_id: currentThreadId,
                providers: providersList,
                content_length: followUpContent.length,
                question: question.trim()
            });

            fetchWithTimeout('../aiapi/api_opus/api/query.php', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(requestBody)
            }, timeoutSec * 1000)
            .then(function(response) {
                if (!response.ok) throw new Error('HTTP ' + response.status);
                return response.text();
            })
            .then(function(text) {
                var data;
                try { data = JSON.parse(text); } catch(e) { throw new Error('Invalid JSON'); }
                if (data.success) {
                    // Show in Round 2 container
                    displayRound2Response(data);

                    // Add to conversation history
                    for (var key in data.responses) {
                        var r = data.responses[key];
                        if (r && r.success && r.response) {
                            var pName = key.replace('_response', '');
                            addToConversation(question.trim(), r.response, pName, modelsMap[pName] || '', '');
                            break;
                        }
                    }

                    // Save
                    providersList.forEach(function(prov) {
                        var pr = data.responses[prov + '_response'];
                        if (pr && pr.success) {
                            saveAIResponse({
                                thread_ids: [currentThreadId],
                                contact_id: primaryContactId,
                                question: 'Follow-up: ' + question.trim(),
                                agent: selectedAgent,
                                content_mode: 'followup',
                                content_length: followUpContent.length,
                                provider: prov,
                                model: modelsMap[prov],
                                query_id: data.query_id,
                                response: pr,
                                total_cost: pr.cost || 0,
                                round: 'followup'
                            });
                        }
                    });
                } else {
                    alert('Follow-up failed: ' + (data.error || 'Unknown'));
                }
            })
            .catch(function(err) {
                alert('Follow-up error: ' + err.message);
            })
            .finally(function() {
                document.getElementById('loading-overlay').classList.remove('active');
                document.getElementById('submit-ai-btn').disabled = false;
            });
        }

        // Get selected response texts for follow-up
        function getSelectedResponseTexts() {
            var result = [];
            document.querySelectorAll('.response-select-cb:checked').forEach(function(cb) {
                result.push({
                    provider: cb.dataset.provider,
                    model: cb.dataset.model || '',
                    response: cb.dataset.responseText || ''
                });
            });
            return result;
        }

        function selectAllResponses() {
            document.querySelectorAll('.response-select-cb').forEach(function(cb) { cb.checked = true; });
        }

        function deselectAllResponses() {
            document.querySelectorAll('.response-select-cb').forEach(function(cb) { cb.checked = false; });
        }

        // Fetch with timeout using AbortController
        function fetchWithTimeout(url, options, timeoutMs) {
            var controller = new AbortController();
            options.signal = controller.signal;
            var timeoutId = setTimeout(function() { controller.abort(); }, timeoutMs);

            return fetch(url, options).then(function(response) {
                clearTimeout(timeoutId);
                return response;
            }).catch(function(err) {
                clearTimeout(timeoutId);
                if (err.name === 'AbortError') {
                    throw new Error('Request timed out after ' + (timeoutMs / 1000) + 's');
                }
                throw err;
            });
        }

        // ============================================
        // CONVERSATION MEMORY - ChatGPT-like history
        // ============================================