Now add the chart JS functions. Let me find a good spot near the end of the script block:


2242:        function fetchWithTimeout(url, options, timeoutMs) {


  2242→        function fetchWithTimeout(url, options, timeoutMs) {
  2243→            var controller = new AbortController();
  2244→            options.signal = controller.signal;
  2245→            var timeoutId = setTimeout(function() { controller.abort(); }, timeoutMs);
  2246→
  2247→            return fetch(url, options).then(function(response) {
  2248→                clearTimeout(timeoutId);
  2249→                return response;
  2250→            }).catch(function(err) {
  2251→                clearTimeout(timeoutId);
  2252→                if (err.name === 'AbortError') {
  2253→                    throw new Error('Request timed out after ' + (timeoutMs / 1000) + 's');
  2254→                }
  2255→                throw err;
  2256→            });
  2257→        }
  2258→
  2259→        // ============================================
  2260→        // CONVERSATION MEMORY - ChatGPT-like history
  2261→        // ============================================

        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
        // ============================================


I'll add the chart functions right before the conversation memory section:


Edit

// ============================================
        // D3.JS CHART GENERATION
        // ============================================
        async function generateChart() {
            // Collect AI response text from the displayed response
            var responseEls = document.querySelectorAll('#ai-responses-content .ai-response-content, #ai-responses-content-round2 .ai-response-content');
            var responseText = '';
            responseEls.forEach(function(el) {
                if (el.innerText) responseText += el.innerText + '\n\n';
            });
            if (!responseText.trim()) {
                alert('No AI response to chart. Submit a query first.');
                return;
            }

            var chartPrompt = 'Based on the following analysis of an email conversation, generate a JSON object for a relationship/entity chart.\n' +
                'Return ONLY valid JSON with this exact structure (no other text, no markdown):\n' +
                '{"nodes": [{"id": "unique_id", "label": "Display Name", "type": "person|company|topic|event|action", "details": "brief description"}], ' +
                '"links": [{"source": "node_id", "target": "node_id", "label": "relationship description", "strength": 1}]}\n' +
                'Extract key people, companies, topics, events, and actions mentioned. Show relationships between them.\n' +
                'Use strength 1-10 where 10 is strongest relationship.\n\n' +
                'Analysis:\n' + responseText.substring(0, 6000);

            document.getElementById('d3-chart-container').style.display = 'block';
            document.getElementById('d3-chart').innerHTML = '
Generating chart data with AI...
';

            try {
                // Send to AI proxy to generate structured JSON
                var providersList = getSelectedProviders();
                var provider = providersList.length > 0 ? providersList[0] : 'claude';
                var modelSel = document.getElementById('model_' + provider);
                var model = modelSel ? modelSel.value : 'claude-sonnet-4-6';

                var requestBody = {
                    user_content: chartPrompt,
                    providers: [provider]
                };
                requestBody['model_' + provider] = model;

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

                if (!data.success) throw new Error(data.error || 'AI request failed');

                // Extract the response text
                var aiResponse = '';
                for (var key in data.responses) {
                    var r = data.responses[key];
                    if (r && r.success && r.response) {
                        aiResponse = r.response;
                        break;
                    }
                }
                if (!aiResponse) throw new Error('No AI response received');

                // Parse JSON from response
                var jsonMatch = aiResponse.match(/\{[\s\S]*\}/);
                if (!jsonMatch) throw new Error('AI did not return valid JSON. Response: ' + aiResponse.substring(0, 200));

                var chartData = JSON.parse(jsonMatch[0]);
                if (!chartData.nodes || !chartData.links) throw new Error('Invalid chart data: missing nodes or links');

                renderD3Chart(chartData);
            } catch(e) {
                document.getElementById('d3-chart').innerHTML = '
Error: ' + escapeHtml(e.message) + '
';
            }
        }

        function renderD3Chart(data) {
            var container = document.getElementById('d3-chart');
            container.innerHTML = '';
            var width = container.clientWidth || 800;
            var height = 600;

            var typeColors = {
                person: '#3b82f6',
                company: '#10b981',
                topic: '#f59e0b',
                event: '#ef4444',
                action: '#8b5cf6'
            };

            var svg = d3.select('#d3-chart').append('svg')
                .attr('width', width)
                .attr('height', height)
                .style('background', '#fafafa');

            var g = svg.append('g');

            // Zoom
            svg.call(d3.zoom().scaleExtent([0.3, 4]).on('zoom', function(event) {
                g.attr('transform', event.transform);
            }));

            // Arrow markers
            svg.append('defs').selectAll('marker')
                .data(['arrow'])
                .enter().append('marker')
                .attr('id', 'chart-arrow')
                .attr('viewBox', '0 -5 10 10')
                .attr('refX', 28)
                .attr('refY', 0)
                .attr('markerWidth', 6)
                .attr('markerHeight', 6)
                .attr('orient', 'auto')
                .append('path')
                .attr('d', 'M0,-5L10,0L0,5')
                .attr('fill', '#94a3b8');

            var simulation = d3.forceSimulation(data.nodes)
                .force('link', d3.forceLink(data.links).id(function(d) { return d.id; }).distance(140))
                .force('charge', d3.forceManyBody().strength(-400))
                .force('center', d3.forceCenter(width / 2, height / 2))
                .force('collision', d3.forceCollide().radius(45));

            // Links
            var link = g.append('g').selectAll('line')
                .data(data.links)
                .enter().append('line')
                .attr('stroke', '#94a3b8')
                .attr('stroke-width', function(d) { return Math.max(1, (d.strength || 5) / 3); })
                .attr('marker-end', 'url(#chart-arrow)');

            // Link labels
            var linkLabel = g.append('g').selectAll('text')
                .data(data.links)
                .enter().append('text')
                .attr('text-anchor', 'middle')
                .attr('font-size', '9px')
                .attr('fill', '#64748b')
                .text(function(d) { return d.label || ''; });

            // Node groups
            var node = g.append('g').selectAll('g')
                .data(data.nodes)
                .enter().append('g')
                .call(d3.drag()
                    .on('start', function(event, d) {
                        if (!event.active) simulation.alphaTarget(0.3).restart();
                        d.fx = d.x; d.fy = d.y;
                    })
                    .on('drag', function(event, d) {
                        d.fx = event.x; d.fy = event.y;
                    })
                    .on('end', function(event, d) {
                        if (!event.active) simulation.alphaTarget(0);
                        d.fx = null; d.fy = null;
                    })
                );

            // Node circles
            node.append('circle')
                .attr('r', 22)
                .attr('fill', function(d) { return typeColors[d.type] || '#6b7280'; })
                .attr('stroke', '#fff')
                .attr('stroke-width', 2.5)
                .style('cursor', 'grab')
                .style('filter', 'drop-shadow(0 1px 2px rgba(0,0,0,0.15))');

            // Type icon inside circle
            node.append('text')
                .attr('text-anchor', 'middle')
                .attr('dy', '5px')
                .attr('font-size', '14px')
                .attr('fill', 'white')
                .style('pointer-events', 'none')
                .text(function(d) {
                    var icons = {person: '\u{1F464}', company: '\u{1F3E2}', topic: '\u{1F4AC}', event: '\u{1F4C5}', action: '\u26A1'};
                    return icons[d.type] || '\u2022';
                });

            // Node labels below
            node.append('text')
                .attr('dy', 38)
                .attr('text-anchor', 'middle')
                .attr('font-size', '11px')
                .attr('font-weight', '600')
                .attr('fill', '#1e293b')
                .style('pointer-events', 'none')
                .text(function(d) {
                    var label = d.label || '';
                    return label.length > 20 ? label.substring(0, 18) + '..' : label;
                });

            // Tooltip
            var tooltip = d3.select('#chart-tooltip');
            node.on('mouseover', function(event, d) {
                tooltip.style('display', 'block')
                    .html('' + escapeHtml(d.label) + '
' + escapeHtml(d.type) + '' +
                        (d.details ? '
' + escapeHtml(d.details) : ''))
                    .style('left', (event.pageX + 12) + 'px')
                    .style('top', (event.pageY - 10) + 'px');
            }).on('mousemove', function(event) {
                tooltip.style('left', (event.pageX + 12) + 'px')
                    .style('top', (event.pageY - 10) + 'px');
            }).on('mouseout', function() {
                tooltip.style('display', 'none');
            });

            // Tick
            simulation.on('tick', function() {
                link.attr('x1', function(d) { return d.source.x; })
                    .attr('y1', function(d) { return d.source.y; })
                    .attr('x2', function(d) { return d.target.x; })
                    .attr('y2', function(d) { return d.target.y; });

                linkLabel.attr('x', function(d) { return (d.source.x + d.target.x) / 2; })
                    .attr('y', function(d) { return (d.source.y + d.target.y) / 2; });

                node.attr('transform', function(d) { return 'translate(' + d.x + ',' + d.y + ')'; });
            });

            // Legend
            var legend = svg.append('g').attr('transform', 'translate(15, 15)');
            var types = Object.keys(typeColors);
            types.forEach(function(type, i) {
                var lg = legend.append('g').attr('transform', 'translate(0, ' + (i * 22) + ')');
                lg.append('circle').attr('r', 7).attr('fill', typeColors[type]);
                lg.append('text').attr('x', 14).attr('dy', '4px').attr('font-size', '11px').attr('fill', '#475569').text(type);
            });
        }

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