async function streamChatResponse(messages, chatContainer, agentType, tabId) { const currentSettings = getSettings(); const backendEndpoint = '/api'; // Resolve model configuration for this agent type let modelConfig = resolveModelConfig(agentType); if (!modelConfig) { modelConfig = getDefaultModelConfig(); } if (!modelConfig) { // No models configured - show error const errorDiv = document.createElement('div'); errorDiv.className = 'message assistant error'; errorDiv.innerHTML = `
No models configured. Please open Settings and add a provider and model.
`; chatContainer.appendChild(errorDiv); return; } // Resolve research sub-agent model if configured let researchSubAgentConfig = null; if (currentSettings.researchSubAgentModel) { const subAgentModel = currentSettings.models?.[currentSettings.researchSubAgentModel]; if (subAgentModel) { const subAgentProvider = currentSettings.providers?.[subAgentModel.providerId]; if (subAgentProvider) { researchSubAgentConfig = { endpoint: subAgentProvider.endpoint, token: subAgentProvider.token, model: subAgentModel.modelId }; } } } // Resolve image model selections to HF model ID strings const imageGenModelId = currentSettings.imageGenModel ? currentSettings.models?.[currentSettings.imageGenModel]?.modelId || null : null; const imageEditModelId = currentSettings.imageEditModel ? currentSettings.models?.[currentSettings.imageEditModel]?.modelId || null : null; // Set up AbortController for this request const abortController = new AbortController(); if (tabId !== undefined) { activeAbortControllers[tabId] = abortController; } try { // Determine parent agent ID for abort propagation const parentAgentId = timelineData[tabId]?.parentTabId != null ? timelineData[tabId].parentTabId.toString() : null; const response = await apiFetch(`${backendEndpoint}/chat/stream`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, signal: abortController.signal, body: JSON.stringify({ messages: messages, agent_type: agentType, stream: true, endpoint: modelConfig.endpoint, token: modelConfig.token || null, model: modelConfig.model, extra_params: modelConfig.extraParams || null, multimodal: modelConfig.multimodal || false, e2b_key: currentSettings.e2bKey || null, serper_key: currentSettings.serperKey || null, hf_token: currentSettings.hfToken || null, image_gen_model: imageGenModelId, image_edit_model: imageEditModelId, research_sub_agent_model: researchSubAgentConfig?.model || null, research_sub_agent_endpoint: researchSubAgentConfig?.endpoint || null, research_sub_agent_token: researchSubAgentConfig?.token || null, research_sub_agent_extra_params: researchSubAgentConfig?.extraParams || null, research_parallel_workers: currentSettings.researchParallelWorkers || null, research_max_websites: currentSettings.researchMaxWebsites || null, agent_id: tabId.toString(), // Send unique tab ID for sandbox sessions parent_agent_id: parentAgentId, // Parent agent ID for abort propagation frontend_context: getFrontendContext() // Dynamic context for system prompts }) }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; let fullResponse = ''; let currentMessageEl = null; let progressHidden = false; // Flush any accumulated assistant text to the timeline // Call before adding tool/code/report timeline events to preserve ordering function flushResponseToTimeline() { if (fullResponse && tabId !== undefined) { const evIdx = addTimelineEvent(tabId, 'assistant', fullResponse); if (currentMessageEl) currentMessageEl.dataset.timelineIndex = evIdx; fullResponse = ''; } } while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() || ''; for (const line of lines) { if (line.startsWith('data: ')) { const data = JSON.parse(line.slice(6)); // Hide progress widget on first meaningful response if (!progressHidden && data.type !== 'generating' && data.type !== 'retry' && !data.type.startsWith('debug_')) { hideProgressWidget(chatContainer); progressHidden = true; } // Handle different message types from backend if (data.type === 'thinking') { // Assistant thinking - create message if not exists if (!currentMessageEl) { currentMessageEl = createAssistantMessage(chatContainer); } fullResponse += data.content; appendToMessage(currentMessageEl, resolveGlobalFigureRefs(parseMarkdown(fullResponse))); scrollChatToBottom(chatContainer); } else if (data.type === 'code') { // Code execution result - update the last code cell updateLastCodeCell(chatContainer, data.output, data.error, data.images); currentMessageEl = null; // Reset for next thinking scrollChatToBottom(chatContainer); } else if (data.type === 'code_start') { // Code cell starting execution - show with spinner flushResponseToTimeline(); createCodeCell(chatContainer, data.code, null, false, true); currentMessageEl = null; scrollChatToBottom(chatContainer); // Add to timeline - show code snippet preview const codePreview = data.code ? data.code.split('\n')[0] : 'code execution'; const codeEvIdx = addTimelineEvent(tabId, 'assistant', codePreview, null, { tag: 'CODE' }); chatContainer.lastElementChild.dataset.timelineIndex = codeEvIdx; } else if (data.type === 'upload') { // File upload notification flushResponseToTimeline(); createUploadMessage(chatContainer, data.paths, data.output); currentMessageEl = null; scrollChatToBottom(chatContainer); // Add to timeline const upEvIdx = addTimelineEvent(tabId, 'assistant', data.paths?.join(', ') || 'files', null, { tag: 'UPLOAD' }); chatContainer.lastElementChild.dataset.timelineIndex = upEvIdx; } else if (data.type === 'download') { // File download notification flushResponseToTimeline(); createDownloadMessage(chatContainer, data.paths, data.output); currentMessageEl = null; scrollChatToBottom(chatContainer); // Add to timeline const dlEvIdx = addTimelineEvent(tabId, 'assistant', data.paths?.join(', ') || 'files', null, { tag: 'DOWNLOAD' }); chatContainer.lastElementChild.dataset.timelineIndex = dlEvIdx; } else if (data.type === 'generating') { // Still generating - no action needed } else if (data.type === 'result') { // References are globally namespaced by the backend (e.g., figure_T3_1) const resultText = data.content || ''; // Populate global registry if (data.figures) { for (const [name, figData] of Object.entries(data.figures)) { if (new RegExp(``, 'i').test(resultText)) { globalFigureRegistry[name] = figData; } } } // Agent result - update command center widget updateActionWidgetWithResult(tabId, resultText, data.figures || {}); } else if (data.type === 'result_preview') { // Show result preview flushResponseToTimeline(); // Replace tags with placeholders BEFORE markdown processing let previewContent = data.content; const figurePlaceholders = {}; if (data.figures) { for (const [figureName, figureData] of Object.entries(data.figures)) { // Use %%% delimiters to avoid markdown interpretation const placeholderId = `%%%FIGURE_${figureName}%%%`; figurePlaceholders[placeholderId] = figureData; // Handle both self-closing and pairs // First replace paired tags, preserving them as block elements const pairedTag = new RegExp(`<${figureName}>`, 'gi'); previewContent = previewContent.replace(pairedTag, `\n\n${placeholderId}\n\n`); // Then replace remaining self-closing tags or orphaned closing tags const singleTag = new RegExp(``, 'gi'); previewContent = previewContent.replace(singleTag, `\n\n${placeholderId}\n\n`); } } // Process markdown let html = parseMarkdown(previewContent); // Replace placeholders with actual images AFTER markdown processing for (const [placeholderId, figureData] of Object.entries(figurePlaceholders)) { let imageHtml = ''; if (figureData.type === 'png' || figureData.type === 'jpeg') { imageHtml = ``; } else if (figureData.type === 'svg') { imageHtml = `
${atob(figureData.data)}
`; } // Replace both the placeholder and any paragraph-wrapped version html = html.replace(new RegExp(`

${placeholderId}

`, 'g'), imageHtml); html = html.replace(new RegExp(placeholderId, 'g'), imageHtml); } // Store raw result as hidden assistant message for history reconstruction const resultMsg = document.createElement('div'); resultMsg.className = 'message assistant'; resultMsg.style.display = 'none'; resultMsg.innerHTML = `
${escapeHtml(data.content)}
`; chatContainer.appendChild(resultMsg); // Create result block const resultDiv = document.createElement('div'); resultDiv.className = 'agent-result'; resultDiv.innerHTML = `
Result
${html}
`; chatContainer.appendChild(resultDiv); scrollChatToBottom(chatContainer); // Add result dot to parent research timeline const figCount = data.figures ? Object.keys(data.figures).length : 0; const reportDesc = figCount > 0 ? `${figCount} figures` : (data.content?.replace(/<[^>]+>/g, '').trim().substring(0, 60) || 'done'); const resEvIdx = addTimelineEvent(tabId, 'assistant', reportDesc, null, { tag: 'RESULT' }); resultDiv.dataset.timelineIndex = resEvIdx; } else if (data.type === 'status') { // Research status update createStatusMessage(chatContainer, data.message, data.iteration, data.total_iterations); scrollChatToBottom(chatContainer); } else if (data.type === 'queries') { // Research queries generated createQueriesMessage(chatContainer, data.queries, data.iteration); scrollChatToBottom(chatContainer); // Register each query as a virtual sub-agent in the timeline const startIdx = Object.keys(researchQueryTabIds).length; for (let qi = 0; qi < data.queries.length; qi++) { const globalIdx = startIdx + qi; const virtualId = `research-${tabId}-q${globalIdx}`; researchQueryTabIds[globalIdx] = virtualId; registerAgentInTimeline(virtualId, 'search', data.queries[qi], tabId); setTimelineGenerating(virtualId, true); } } else if (data.type === 'progress') { // Research progress updateProgress(chatContainer, data.message, data.websites_visited, data.max_websites); scrollChatToBottom(chatContainer); } else if (data.type === 'source') { // Research source found - now includes query grouping createSourceMessage(chatContainer, data); scrollChatToBottom(chatContainer); // Add source as dot in virtual sub-agent timeline const sourceVirtualId = researchQueryTabIds[data.query_index]; if (sourceVirtualId) { addTimelineEvent(sourceVirtualId, 'assistant', data.title || data.url || 'source'); } else if (data.query_index === -1) { // Browse result — create a virtual browse entry if needed const browseId = `research-${tabId}-browse-${Date.now()}`; registerAgentInTimeline(browseId, 'browse', data.url || 'webpage', tabId); addTimelineEvent(browseId, 'assistant', data.title || data.url || 'page'); setTimelineGenerating(browseId, false); } } else if (data.type === 'query_stats') { // Update query statistics updateQueryStats(data.query_index, { relevant: data.relevant_count, irrelevant: data.irrelevant_count, error: data.error_count }); // Mark search sub-agent as done const statsVirtualId = researchQueryTabIds[data.query_index]; if (statsVirtualId) { setTimelineGenerating(statsVirtualId, false); } } else if (data.type === 'assessment') { // Research completeness assessment createAssessmentMessage(chatContainer, data.sufficient, data.missing_aspects, data.findings_count, data.reasoning); scrollChatToBottom(chatContainer); } else if (data.type === 'report') { // Final research report flushResponseToTimeline(); createReportMessage(chatContainer, data.content, data.sources_count, data.websites_visited); scrollChatToBottom(chatContainer); // Add to timeline const rptEvIdx = addTimelineEvent(tabId, 'assistant', `${data.sources_count || 0} sources, ${data.websites_visited || 0} sites`, null, { tag: 'RESULT' }); chatContainer.lastElementChild.dataset.timelineIndex = rptEvIdx; } else if (data.type === 'tool_start') { // Agent tool execution starting — create a tool-cell box (like code cells) flushResponseToTimeline(); currentMessageEl = null; const toolLabels = { 'web_search': 'SEARCH', 'read_url': 'READ', 'screenshot_url': 'SCREENSHOT', 'generate_image': 'GENERATE', 'edit_image': 'EDIT', 'read_image_url': 'LOAD IMAGE', 'read_image': 'LOAD IMAGE', 'save_image': 'SAVE IMAGE', 'show_html': 'HTML' }; const toolDescriptions = { 'web_search': data.args?.query || '', 'read_url': data.args?.url || '', 'screenshot_url': data.args?.url || '', 'generate_image': data.args?.prompt || '', 'edit_image': `${data.args?.prompt || ''} (from ${data.args?.source || ''})`, 'read_image_url': data.args?.url || '', 'read_image': data.args?.source || '', 'save_image': `${data.args?.source || ''} → ${data.args?.filename || ''}`, 'show_html': data.args?.source?.substring(0, 80) || '' }; const label = toolLabels[data.tool] || data.tool.toUpperCase(); const description = toolDescriptions[data.tool] || ''; // Store tool call in DOM for history reconstruction // Reuse currentMessageEl (from thinking) if it exists, like launch events do let toolCallMsg = currentMessageEl; if (!toolCallMsg) { toolCallMsg = document.createElement('div'); toolCallMsg.className = 'message assistant'; toolCallMsg.style.display = 'none'; chatContainer.appendChild(toolCallMsg); } toolCallMsg.setAttribute('data-tool-call', JSON.stringify({ tool_call_id: data.tool_call_id, function_name: data.tool, arguments: data.arguments, thinking: data.thinking || '' })); // Create tool-cell box (similar to code-cell) const toolCell = document.createElement('div'); toolCell.className = 'tool-cell'; if (document.getElementById('collapseToolsCheckbox')?.checked) { toolCell.classList.add('collapsed'); } toolCell.setAttribute('data-tool-name', data.tool); const descHtml = description ? `${escapeHtml(description)}` : ''; toolCell.innerHTML = `
${label}${descHtml}${createSpinnerHtml()}
${escapeHtml(description)}
`; setupCollapseToggle(toolCell, '.tool-cell-label'); chatContainer.appendChild(toolCell); scrollChatToBottom(chatContainer); const toolEvIdx = addTimelineEvent(tabId, 'assistant', description, null, { tag: label }); toolCell.dataset.timelineIndex = toolEvIdx; } else if (data.type === 'tool_result') { // Agent tool result — populate the last tool-cell with output const lastToolCell = chatContainer.querySelector('.tool-cell:last-of-type'); // Remove spinner if (lastToolCell) { const spinner = lastToolCell.querySelector('.tool-spinner'); if (spinner) spinner.remove(); } // Store tool response in DOM for history reconstruction const toolResponseMsg = document.createElement('div'); toolResponseMsg.className = 'message tool'; toolResponseMsg.style.display = 'none'; toolResponseMsg.setAttribute('data-tool-response', JSON.stringify({ tool_call_id: data.tool_call_id, content: data.response || '' })); chatContainer.appendChild(toolResponseMsg); // Build output HTML based on tool type let outputHtml = ''; if (data.tool === 'web_search' && data.result?.results) { try { const results = typeof data.result.results === 'string' ? JSON.parse(data.result.results) : data.result.results; if (Array.isArray(results)) { outputHtml = '
' + results.map(r => `
${escapeHtml(r.title)}${escapeHtml(r.snippet)}
` ).join('') + '
'; } } catch(e) { /* ignore parse errors */ } } else if (data.tool === 'read_url') { const len = data.result?.length || 0; const markdown = data.result?.markdown || ''; const summaryText = len > 0 ? `Extracted ${(len / 1000).toFixed(1)}k chars` : 'No content extracted'; if (markdown) { const toggleId = `read-content-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`; outputHtml = `
${summaryText}
${parseMarkdown(markdown)}
`; } else { outputHtml = `
${summaryText}
`; } } else if (data.tool === 'screenshot_url' && data.image) { outputHtml = `Screenshot`; } else if ((data.tool === 'generate_image' || data.tool === 'edit_image' || data.tool === 'read_image_url' || data.tool === 'read_image') && data.image) { const imgName = data.image_name || 'image'; outputHtml = `${escapeHtml(imgName)}`; } else if ((data.tool === 'generate_image' || data.tool === 'edit_image' || data.tool === 'read_image_url' || data.tool === 'read_image') && !data.image) { const errMsg = data.response || 'Failed to process image'; outputHtml = `
${escapeHtml(errMsg)}
`; } else if (data.tool === 'save_image' && data.image) { const filename = data.result?.filename || ''; outputHtml = `${escapeHtml(filename)}
Saved as ${escapeHtml(filename)}
`; } else if (data.tool === 'save_image' && !data.image) { const errMsg = data.response || 'Failed to save image'; outputHtml = `
${escapeHtml(errMsg)}
`; } else if (data.tool === 'show_html' && data.result?.html) { // Create iframe programmatically to avoid escaping issues with srcdoc if (lastToolCell) { const outputEl = document.createElement('div'); outputEl.className = 'tool-cell-output'; const iframe = document.createElement('iframe'); iframe.className = 'show-html-iframe'; iframe.sandbox = 'allow-scripts allow-same-origin'; iframe.srcdoc = data.result.html; outputEl.appendChild(iframe); lastToolCell.appendChild(outputEl); } } else if (data.tool === 'show_html' && !data.result?.html) { const errMsg = data.response || 'Failed to load HTML'; outputHtml = `
${escapeHtml(errMsg)}
`; } if (outputHtml && lastToolCell) { const outputEl = document.createElement('div'); outputEl.className = 'tool-cell-output'; outputEl.innerHTML = outputHtml; lastToolCell.appendChild(outputEl); } scrollChatToBottom(chatContainer); } else if (data.type === 'content') { // Regular streaming content (non-code agents) if (!currentMessageEl) { currentMessageEl = createAssistantMessage(chatContainer); } fullResponse += data.content; appendToMessage(currentMessageEl, resolveGlobalFigureRefs(parseMarkdown(fullResponse))); scrollChatToBottom(chatContainer); } else if (data.type === 'launch') { // Tool-based agent launch from command center pendingAgentLaunches++; const agentType = data.agent_type; const initialMessage = data.initial_message; const taskId = data.task_id; const toolCallId = data.tool_call_id; // Add tool call data to existing message (from thinking) or create new one // This keeps content + tool_calls together for proper history reconstruction let toolCallMsg = currentMessageEl; if (!toolCallMsg) { toolCallMsg = document.createElement('div'); toolCallMsg.className = 'message assistant'; toolCallMsg.innerHTML = '
'; chatContainer.appendChild(toolCallMsg); } toolCallMsg.setAttribute('data-tool-call', JSON.stringify({ agent_type: agentType, message: initialMessage, tool_call_id: toolCallId })); // Add tool response so LLM knows the tool was executed const toolResponseMsg = document.createElement('div'); toolResponseMsg.className = 'message tool'; toolResponseMsg.style.display = 'none'; toolResponseMsg.setAttribute('data-tool-response', JSON.stringify({ tool_call_id: toolCallId, content: `Launched ${agentType} agent with task: ${initialMessage}` })); chatContainer.appendChild(toolResponseMsg); // The action widget will show the launch visually handleActionToken(agentType, initialMessage, (targetTabId) => { showActionWidget(chatContainer, agentType, initialMessage, targetTabId, taskId); // Store tool call ID for this agent tab so we can send result back toolCallIds[targetTabId] = toolCallId; }, taskId, tabId); // Reset current message element so any subsequent thinking starts fresh currentMessageEl = null; } else if (data.type === 'debug_call_input') { // Debug: LLM call input (before API call) if (!debugHistory[tabId]) debugHistory[tabId] = []; debugHistory[tabId].push({ call_number: data.call_number, timestamp: new Date().toLocaleTimeString(), input: data.messages, output: null, error: null }); if (document.getElementById('debugPanel')?.classList.contains('active')) loadDebugMessages(); } else if (data.type === 'debug_call_output') { // Debug: LLM call output (after API call) // Match the last pending call (call_numbers reset per streaming request) const calls = debugHistory[tabId] || []; const call = calls.findLast(c => c.output === null && c.error === null); if (call) { call.output = data.response || null; call.error = data.error || null; } if (document.getElementById('debugPanel')?.classList.contains('active')) loadDebugMessages(); } else if (data.type === 'aborted') { // Agent was aborted by user hideProgressWidget(chatContainer); removeRetryIndicator(chatContainer); // Mark the action widget as aborted (show × instead of ✓) const widget = actionWidgets[tabId]; if (widget) { const orbitIndicator = widget.querySelector('.orbit-indicator'); if (orbitIndicator) { const abortedIndicator = document.createElement('div'); abortedIndicator.className = 'done-indicator aborted'; orbitIndicator.replaceWith(abortedIndicator); } } // For timeline agent boxes, mark as aborted if (timelineData[tabId]) { timelineData[tabId].aborted = true; } break; // Stop reading stream } else if (data.type === 'done') { // Remove retry indicator on success removeRetryIndicator(chatContainer); // Reset research state when research agent completes if (agentType === 'research' && typeof resetResearchState === 'function') { // Mark all research virtual sub-agents as done for (const virtualId of Object.values(researchQueryTabIds)) { setTimelineGenerating(virtualId, false); } researchQueryTabIds = {}; resetResearchState(); } // Check for action tokens in regular agents (legacy support) if (fullResponse) { const actionMatch = fullResponse.match(/([\s\S]*?)<\/action>/i); if (actionMatch) { const action = actionMatch[1].toLowerCase(); const actionMessage = actionMatch[2].trim(); // Remove action token from fullResponse const cleanedResponse = fullResponse.replace(/[\s\S]*?<\/action>/i, '').trim(); // Update the display to remove action tags if (cleanedResponse.length === 0 && currentMessageEl) { currentMessageEl.remove(); } else if (currentMessageEl) { // Clear and replace the entire message content const messageContent = currentMessageEl.querySelector('.message-content'); if (messageContent) { messageContent.innerHTML = parseMarkdown(cleanedResponse); linkifyFilePaths(messageContent); } } handleActionToken(action, actionMessage, (targetTabId) => { showActionWidget(chatContainer, action, actionMessage, targetTabId); }, null, tabId); } } } else if (data.type === 'info') { // Info message (e.g., sandbox restart) const infoDiv = document.createElement('div'); infoDiv.className = 'system-message'; infoDiv.innerHTML = `${escapeHtml(data.content)}`; infoDiv.style.color = 'var(--theme-accent)'; chatContainer.appendChild(infoDiv); scrollChatToBottom(chatContainer); } else if (data.type === 'retry') { // Show retry indicator showRetryIndicator(chatContainer, data); } else if (data.type === 'error') { // Remove any retry indicator before showing error removeRetryIndicator(chatContainer); const errorDiv = document.createElement('div'); errorDiv.className = 'message assistant'; errorDiv.innerHTML = `
Error: ${escapeHtml(data.content)}
`; chatContainer.appendChild(errorDiv); scrollChatToBottom(chatContainer); // Propagate error to parent action widget updateActionWidgetWithResult(tabId, `Error: ${data.content}`, {}); const errorWidget = actionWidgets[tabId]; if (errorWidget) { const doneIndicator = errorWidget.querySelector('.done-indicator'); if (doneIndicator) { doneIndicator.classList.add('errored'); } } } } } } // Add assistant response to timeline if (fullResponse && tabId !== undefined) { const finalEvIdx = addTimelineEvent(tabId, 'assistant', fullResponse); if (currentMessageEl) currentMessageEl.dataset.timelineIndex = finalEvIdx; } } catch (error) { hideProgressWidget(chatContainer); if (error.name === 'AbortError') { // User-initiated abort — show as a result block const abortResultText = 'Generation aborted by user.'; const resultDiv = document.createElement('div'); resultDiv.className = 'agent-result'; resultDiv.innerHTML = `
Result

${abortResultText}

`; chatContainer.appendChild(resultDiv); // Send abort result to parent action widget (so command center knows it was aborted) updateActionWidgetWithResult(tabId, abortResultText, {}); // Override the done indicator to show × instead of ✓ const widget = actionWidgets[tabId]; if (widget) { const doneIndicator = widget.querySelector('.done-indicator'); if (doneIndicator) { doneIndicator.classList.add('aborted'); } } // Mark timeline data as aborted if (timelineData[tabId]) { timelineData[tabId].aborted = true; } } else { const errorDiv = document.createElement('div'); errorDiv.className = 'message assistant'; errorDiv.innerHTML = `
Connection error: ${escapeHtml(error.message)}
`; chatContainer.appendChild(errorDiv); } if (tabId) { setTabGenerating(tabId, false); } } finally { // Clean up abort controller delete activeAbortControllers[tabId]; } } function createAssistantMessage(chatContainer) { const msg = document.createElement('div'); msg.className = 'message assistant'; msg.innerHTML = '
'; chatContainer.appendChild(msg); return msg; } function appendToMessage(messageEl, content) { const contentEl = messageEl.querySelector('.message-content'); if (contentEl) { contentEl.innerHTML = content; linkifyFilePaths(contentEl); } } function createSpinnerHtml() { return `
`; } function createCodeCell(chatContainer, code, output, isError, isExecuting = false) { const codeCell = document.createElement('div'); codeCell.className = 'code-cell'; let outputHtml = ''; const cleanedOutput = cleanCodeOutput(output); if (cleanedOutput && !isExecuting) { outputHtml = `
${escapeHtml(cleanedOutput)}
`; } const spinnerHtml = isExecuting ? createSpinnerHtml() : ''; codeCell.innerHTML = `
CODE${spinnerHtml}
${escapeHtml(code)}
${outputHtml} `; setupCollapseToggle(codeCell, '.code-cell-label'); chatContainer.appendChild(codeCell); // Apply syntax highlighting if (typeof Prism !== 'undefined') { const codeBlock = codeCell.querySelector('code.language-python'); if (codeBlock) { Prism.highlightElement(codeBlock); } } } function createFileTransferCell(chatContainer, type, paths, output, isExecuting = false) { const cell = document.createElement('div'); cell.className = 'action-widget'; const label = type === 'upload' ? 'UPLOAD' : 'DOWNLOAD'; const hasError = output && output.includes('Error:'); // Indicator: spinner while executing, checkmark when done const indicatorHtml = isExecuting ? `
` : `
`; // Format paths as list items (make local paths clickable for downloads) const pathsList = paths.map(p => { if (type === 'download') { const arrowIdx = p.indexOf(' -> '); if (arrowIdx !== -1) { const localPath = p.substring(arrowIdx + 4); return `
  • ${escapeHtml(p.substring(0, arrowIdx + 4))}${escapeHtml(localPath)}
  • `; } } return `
  • ${escapeHtml(p)}
  • `; }).join(''); let outputHtml = ''; if (output && !isExecuting) { const outputClass = hasError ? 'transfer-output error' : 'transfer-output'; outputHtml = `
    ${escapeHtml(output)}
    `; } cell.innerHTML = `
    ${label}
    ${indicatorHtml}
      ${pathsList}
    ${outputHtml}
    `; cell.querySelector('.action-widget-header').style.cursor = 'pointer'; cell.querySelector('.action-widget-header').addEventListener('click', () => { cell.classList.toggle('collapsed'); cell.querySelector('.widget-collapse-toggle').classList.toggle('collapsed'); }); // Make download path links clickable to navigate in file explorer cell.querySelectorAll('.transfer-path-link').forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); navigateToFileInExplorer(link.dataset.path); }); }); chatContainer.appendChild(cell); return cell; } function createUploadMessage(chatContainer, paths, output) { createFileTransferCell(chatContainer, 'upload', paths, output, false); } function createDownloadMessage(chatContainer, paths, output) { createFileTransferCell(chatContainer, 'download', paths, output, false); } function updateLastCodeCell(chatContainer, output, isError, images) { const codeCells = chatContainer.querySelectorAll('.code-cell'); if (codeCells.length === 0) return; const lastCell = codeCells[codeCells.length - 1]; // Remove spinner if present const spinner = lastCell.querySelector('.tool-spinner'); if (spinner) { spinner.remove(); } // Remove existing output if any const existingOutput = lastCell.querySelector('.code-cell-output'); if (existingOutput) { existingOutput.remove(); } // Add images if any if (images && images.length > 0) { for (const img of images) { const imgDiv = document.createElement('div'); imgDiv.className = 'code-cell-image'; // Add figure label if available if (img.name) { const label = document.createElement('div'); label.className = 'figure-label'; label.textContent = img.name; imgDiv.appendChild(label); } if (img.type === 'png' || img.type === 'jpeg') { const imgEl = document.createElement('img'); imgEl.src = `data:image/${img.type};base64,${img.data}`; imgEl.onclick = function() { openImageModal(this.src); }; imgDiv.appendChild(imgEl); } else if (img.type === 'svg') { const svgContainer = document.createElement('div'); svgContainer.innerHTML = atob(img.data); imgDiv.appendChild(svgContainer); } lastCell.appendChild(imgDiv); } } // Add text output const cleanedOut = cleanCodeOutput(output); if (cleanedOut) { const outputDiv = document.createElement('div'); outputDiv.className = `code-cell-output${isError ? ' error' : ''}`; outputDiv.textContent = cleanedOut; lastCell.appendChild(outputDiv); } } function showActionWidget(chatContainer, action, message, targetTabId, taskId = null) { const widget = document.createElement('div'); widget.className = 'action-widget'; if (document.getElementById('collapseAgentsCheckbox')?.checked) { widget.classList.add('collapsed'); } widget.dataset.targetTabId = targetTabId; // Display task_id as title if provided const titleDisplay = taskId ? taskId : action.toUpperCase(); const agentCollapsed = document.getElementById('collapseAgentsCheckbox')?.checked; widget.innerHTML = `
    ${action.toUpperCase()}: ${escapeHtml(titleDisplay)}
    ${parseMarkdown(message)}
    `; // Collapse toggle — stop propagation so it doesn't navigate to the agent tab const collapseToggle = widget.querySelector('.widget-collapse-toggle'); collapseToggle.addEventListener('click', (e) => { e.stopPropagation(); widget.classList.toggle('collapsed'); collapseToggle.classList.toggle('collapsed'); }); // Make header clickable to jump to the agent const clickableArea = widget.querySelector('.action-widget-clickable'); const clickHandler = () => { const tabId = parseInt(targetTabId); // Check if the tab still exists (use .tab to avoid matching timeline elements) const tab = document.querySelector(`.tab[data-tab-id="${tabId}"]`); if (tab) { // Tab exists, just switch to it switchToTab(tabId); } else { // Tab was closed - restore from timeline data const notebook = timelineData[tabId]; if (notebook) { reopenClosedTab(tabId, notebook); } } }; clickableArea.addEventListener('click', clickHandler); chatContainer.appendChild(widget); scrollChatToBottom(chatContainer); // Store widget for later updates actionWidgets[targetTabId] = widget; } async function updateActionWidgetWithResult(tabId, resultContent, figures) { const widget = actionWidgets[tabId]; if (!widget) return; // Replace orbiting dots with checkmark const orbitIndicator = widget.querySelector('.orbit-indicator'); if (orbitIndicator) { const doneIndicator = document.createElement('div'); doneIndicator.className = 'done-indicator'; orbitIndicator.replaceWith(doneIndicator); } // Process and display result content FIRST (before async operations) // Replace tags with placeholders BEFORE markdown processing let processedContent = resultContent; const figurePlaceholders = {}; if (figures) { for (const [figureName, figureData] of Object.entries(figures)) { // Use %%% delimiters to avoid markdown interpretation const placeholderId = `%%%FIGURE_${figureName}%%%`; figurePlaceholders[placeholderId] = figureData; // Handle both self-closing and pairs // First replace paired tags, preserving them as block elements const pairedTag = new RegExp(`<${figureName}>`, 'gi'); processedContent = processedContent.replace(pairedTag, `\n\n${placeholderId}\n\n`); // Then replace remaining self-closing tags or orphaned closing tags const singleTag = new RegExp(``, 'gi'); processedContent = processedContent.replace(singleTag, `\n\n${placeholderId}\n\n`); } } // Process markdown let html = parseMarkdown(processedContent); // Replace placeholders with actual images AFTER markdown processing for (const [placeholderId, figureData] of Object.entries(figurePlaceholders)) { let imageHtml = ''; if (figureData.type === 'png' || figureData.type === 'jpeg') { imageHtml = ``; } else if (figureData.type === 'svg') { imageHtml = `
    ${atob(figureData.data)}
    `; } // Replace both the placeholder and any paragraph-wrapped version html = html.replace(new RegExp(`

    ${placeholderId}

    `, 'g'), imageHtml); html = html.replace(new RegExp(placeholderId, 'g'), imageHtml); } // Add result section inside the body const body = widget.querySelector('.action-widget-body'); if (body) { const resultSection = document.createElement('div'); resultSection.className = 'action-widget-result-section'; resultSection.innerHTML = `
    ${html}
    `; body.appendChild(resultSection); } // Update the tool response DOM element so getConversationHistory picks up actual results const toolCallId = toolCallIds[tabId]; if (toolCallId) { // Find the hidden tool response element with this tool_call_id in the command center const commandContainer = document.getElementById('messages-command'); if (commandContainer) { const toolMsgs = commandContainer.querySelectorAll('.message.tool[data-tool-response]'); for (const toolMsg of toolMsgs) { try { const data = JSON.parse(toolMsg.getAttribute('data-tool-response')); if (data.tool_call_id === toolCallId) { data.content = resultContent; toolMsg.setAttribute('data-tool-response', JSON.stringify(data)); break; } } catch (e) { /* ignore parse errors */ } } } // Also send to backend (non-blocking) apiFetch('/api/conversation/add-tool-response', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tab_id: '0', tool_call_id: toolCallId, content: resultContent }) }).catch(error => { console.error('Failed to update conversation history with result:', error); }); } } function sendMessageToTab(tabId, message) { // Programmatically send a message to an existing agent tab const content = document.querySelector(`[data-content-id="${tabId}"]`); if (!content) return; const input = content.querySelector('textarea') || content.querySelector('input[type="text"]'); if (!input) return; // Set the message and trigger send input.value = message; sendMessage(tabId); } function handleActionToken(action, message, callback, taskId = null, parentTabId = null) { // Check if an agent with this task_id already exists if (taskId && taskIdToTabId[taskId]) { const existingTabId = taskIdToTabId[taskId]; const existingContent = document.querySelector(`[data-content-id="${existingTabId}"]`); if (existingContent) { // Only reuse if the agent type matches — different type with same task_id should create a new tab const existingType = existingContent.querySelector('.chat-container')?.dataset?.agentType; if (existingType === action) { // Send the message to the existing agent sendMessageToTab(existingTabId, message); if (callback) { callback(existingTabId); } return; } } else { // Tab no longer exists, clean up the mapping delete taskIdToTabId[taskId]; } } // Open the agent with the extracted message as initial prompt // Don't auto-switch to the new tab (autoSwitch = false) setTimeout(() => { const newTabId = createAgentTab(action, message, false, taskId, parentTabId); if (callback) { callback(newTabId); } }, 500); } function setTabGenerating(tabId, isGenerating) { const tab = document.querySelector(`[data-tab-id="${tabId}"]`); if (!tab) return; const statusIndicator = tab.querySelector('.tab-status'); if (!statusIndicator) return; if (isGenerating) { statusIndicator.style.display = 'block'; statusIndicator.classList.add('generating'); } else { statusIndicator.classList.remove('generating'); // Keep visible but stop animation setTimeout(() => { statusIndicator.style.display = 'none'; }, 300); } // Toggle SEND/STOP button const content = document.querySelector(`[data-content-id="${tabId}"]`); if (content) { const sendBtn = content.querySelector('.input-container button'); if (sendBtn) { if (isGenerating) { sendBtn.textContent = 'STOP'; sendBtn.classList.add('stop-btn'); sendBtn.disabled = false; // Keep enabled so user can click STOP } else { sendBtn.textContent = 'SEND'; sendBtn.classList.remove('stop-btn'); } } } // Update timeline to reflect generating state setTimelineGenerating(tabId, isGenerating); // Track when a pending agent launch actually starts generating if (isGenerating && pendingAgentLaunches > 0 && tabId !== 0 && timelineData[tabId]?.parentTabId === 0) { pendingAgentLaunches--; } // When a child agent finishes and command center is blocked, check if all agents are done if (!isGenerating && commandInputBlocked && tabId !== 0) { const anyStillGenerating = Object.values(timelineData).some( td => td.parentTabId === 0 && td.isGenerating ); if (!anyStillGenerating && pendingAgentLaunches === 0) { commandInputBlocked = false; setCommandCenterStopState(false); // Auto-continue: call command center again with agent results now in history continueCommandCenter(); } } } function cleanCodeOutput(text) { if (!text) return text; return text.split('\n') .filter(line => !line.match(/^\[Plot\/Image generated\]$/) && !line.match(/^\[Generated figures:.*\]$/)) .join('\n') .trim(); } function showRetryIndicator(chatContainer, data) { // Remove existing retry indicator if present removeRetryIndicator(chatContainer); const retryDiv = document.createElement('div'); retryDiv.className = 'retry-indicator'; retryDiv.innerHTML = `
    ${escapeHtml(data.message)}
    Retrying (${data.attempt}/${data.max_attempts}) in ${data.delay}s...
    `; chatContainer.appendChild(retryDiv); scrollChatToBottom(chatContainer); // Start countdown let remaining = data.delay; const statusEl = retryDiv.querySelector('.retry-status'); const countdownInterval = setInterval(() => { remaining--; if (remaining > 0 && statusEl) { statusEl.textContent = `Retrying (${data.attempt}/${data.max_attempts}) in ${remaining}s...`; } else { clearInterval(countdownInterval); if (statusEl) { statusEl.textContent = `Retrying (${data.attempt}/${data.max_attempts})...`; } } }, 1000); // Store interval ID for cleanup retryDiv.dataset.countdownInterval = countdownInterval; } function removeRetryIndicator(chatContainer) { const existing = chatContainer.querySelector('.retry-indicator'); if (existing) { // Clear countdown interval if exists if (existing.dataset.countdownInterval) { clearInterval(parseInt(existing.dataset.countdownInterval)); } existing.remove(); } } // Configure marked options once if (typeof marked !== 'undefined') { // Custom renderer to add target="_blank" to links and syntax highlighting const renderer = new marked.Renderer(); // Handle both old API (separate args) and new API (token object) renderer.link = function(hrefOrToken, title, text) { const href = typeof hrefOrToken === 'object' ? hrefOrToken.href : hrefOrToken; const titleVal = typeof hrefOrToken === 'object' ? hrefOrToken.title : title; const textVal = typeof hrefOrToken === 'object' ? hrefOrToken.text : text; const titleAttr = titleVal ? ` title="${titleVal}"` : ''; return `${textVal}`; }; renderer.code = function(codeOrToken, language) { // Handle both old API (separate args) and new API (token object) const code = typeof codeOrToken === 'object' ? codeOrToken.text : codeOrToken; const lang = typeof codeOrToken === 'object' ? codeOrToken.lang : language; // Use Prism for syntax highlighting if available if (typeof Prism !== 'undefined' && lang && Prism.languages[lang]) { const highlighted = Prism.highlight(code, Prism.languages[lang], lang); return `
    ${highlighted}
    `; } return `
    ${escapeHtml(code)}
    `; }; marked.setOptions({ gfm: true, // GitHub Flavored Markdown breaks: false, // Don't convert \n to
    pedantic: false, renderer: renderer }); } // Resolve and references using the global registry function resolveGlobalFigureRefs(html) { return html.replace(/<\/?(figure_(?:T\d+_)?\d+|image_(?:T\d+_)?\d+)>/gi, (match) => { // Extract the name (strip < > and /) const name = match.replace(/[<>/]/g, ''); const data = globalFigureRegistry[name]; if (!data) return match; // Leave unresolved refs as-is if (data.type === 'png' || data.type === 'jpeg') { return ``; } else if (data.type === 'svg') { return `
    ${atob(data.data)}
    `; } return match; }); } function parseMarkdown(text) { // Use marked library for proper markdown parsing let html; if (typeof marked !== 'undefined') { html = marked.parse(text); } else { // Fallback: just escape HTML and convert newlines to paragraphs html = `

    ${escapeHtml(text).replace(/\n\n/g, '

    ').replace(/\n/g, '
    ')}

    `; } // Render LaTeX with KaTeX if available if (typeof katex !== 'undefined') { // Block math: $$ ... $$ (must handle newlines) html = html.replace(/\$\$([\s\S]*?)\$\$/g, (match, latex) => { try { return katex.renderToString(latex.trim(), { displayMode: true, throwOnError: false }); } catch (e) { return `${escapeHtml(match)}`; } }); // Inline math: $ ... $ (but not $$ or escaped \$) html = html.replace(/(? { try { return katex.renderToString(latex.trim(), { displayMode: false, throwOnError: false }); } catch (e) { return `${escapeHtml(match)}`; } }); } return html; }