// DOM Elements const uploadZone = document.getElementById('uploadZone'); const fileInput = document.getElementById('fileInput'); const fileList = document.getElementById('fileList'); const startBtn = document.getElementById('startBtn'); const requirementInput = document.getElementById('requirementInput'); const statusDot = document.getElementById('statusDot'); const statusText = document.getElementById('statusText'); const reportContainer = document.getElementById('reportContainer'); const downloadScriptBtn = document.getElementById('downloadScriptBtn'); let isRunning = false; let pollingInterval = null; let currentSessionId = null; let selectedTemplate = ''; // 报告段落数据(用于润色功能) let reportParagraphs = []; // Supporting data from report API let supportingData = {}; // Execution Process state let lastRenderedRound = 0; // --- Progress Bar --- function updateProgressBar(percentage, message, currentRound, maxRounds) { const container = document.getElementById('progressBarContainer'); const fill = document.getElementById('progressBarFill'); const label = document.getElementById('progressLabel'); const percent = document.getElementById('progressPercent'); const msg = document.getElementById('progressMessage'); if (!container || !fill) return; container.classList.remove('hidden'); fill.style.width = percentage + '%'; if (label) label.textContent = `Round ${currentRound || 0}/${maxRounds || 0}`; if (percent) percent.textContent = Math.round(percentage) + '%'; if (msg) msg.textContent = message || ''; } function hideProgressBar() { const container = document.getElementById('progressBarContainer'); if (container) container.classList.add('hidden'); } // --- Upload Logic --- if (uploadZone) { uploadZone.addEventListener('dragover', (e) => { e.preventDefault(); uploadZone.classList.add('dragover'); }); uploadZone.addEventListener('dragleave', () => uploadZone.classList.remove('dragover')); uploadZone.addEventListener('drop', (e) => { e.preventDefault(); uploadZone.classList.remove('dragover'); handleFiles(e.dataTransfer.files); }); uploadZone.addEventListener('click', () => fileInput.click()); } if (fileInput) { fileInput.addEventListener('change', (e) => handleFiles(e.target.files)); fileInput.addEventListener('click', (e) => e.stopPropagation()); } async function handleFiles(files) { if (files.length === 0) return; fileList.innerHTML = ''; const formData = new FormData(); for (const file of files) { formData.append('files', file); const fileItem = document.createElement('div'); fileItem.className = 'file-item'; fileItem.innerHTML = ` ${file.name}`; fileList.appendChild(fileItem); } try { const res = await fetch('/api/upload', { method: 'POST', body: formData }); if (!res.ok) alert('Upload failed'); } catch (e) { console.error(e); alert('Upload failed'); } } // --- Template Logic --- async function loadTemplates() { try { const res = await fetch('/api/templates'); if (!res.ok) return; const data = await res.json(); const selector = document.getElementById('templateSelector'); if (!selector || !data.templates) return; for (const tpl of data.templates) { const card = document.createElement('div'); card.className = 'template-card'; card.setAttribute('data-template', tpl.name); card.onclick = function() { selectTemplate(this, tpl.name); }; card.innerHTML = `
${tpl.display_name}
${tpl.description}
`; selector.appendChild(card); } } catch (e) { console.error('Failed to load templates', e); } } window.selectTemplate = function(el, name) { document.querySelectorAll('.template-card').forEach(c => c.classList.remove('selected')); el.classList.add('selected'); selectedTemplate = name; } // --- Analysis Logic --- if (startBtn) { startBtn.addEventListener('click', startAnalysis); } async function startAnalysis() { if (isRunning) return; const requirement = requirementInput.value.trim(); if (!requirement) { alert('Please enter analysis requirement'); return; } setRunningState(true); // Reset execution state for new analysis lastRenderedRound = 0; const wrapper = document.getElementById('roundCardsWrapper'); if (wrapper) wrapper.innerHTML = ''; const emptyState = document.getElementById('executionEmptyState'); if (emptyState) emptyState.remove(); try { const body = { requirement }; if (selectedTemplate) { body.template = selectedTemplate; } const res = await fetch('/api/start', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); if (res.ok) { const data = await res.json(); currentSessionId = data.session_id; startPolling(); switchTab('execution'); } else { const err = await res.json(); alert('Failed to start: ' + err.detail); setRunningState(false); } } catch (e) { console.error(e); alert('Error starting analysis'); setRunningState(false); } } function setRunningState(running) { isRunning = running; startBtn.disabled = running; if (running) { startBtn.innerHTML = ' Analysis in Progress...'; statusDot.className = 'status-dot running'; statusText.innerText = 'Analyzing'; statusText.style.color = 'var(--primary-color)'; const followUpSection = document.getElementById('followUpSection'); if (followUpSection) followUpSection.classList.add('hidden'); if (downloadScriptBtn) downloadScriptBtn.classList.add('hidden'); const tplGroup = document.getElementById('templateSelectorGroup'); if (tplGroup) tplGroup.classList.add('hidden'); } else { startBtn.innerHTML = ' Start Analysis'; statusDot.className = 'status-dot'; statusText.innerText = 'Completed'; statusText.style.color = 'var(--text-secondary)'; const followUpSection = document.getElementById('followUpSection'); if (currentSessionId && followUpSection) followUpSection.classList.remove('hidden'); } } // --- Polling --- function startPolling() { if (pollingInterval) clearInterval(pollingInterval); if (!currentSessionId) return; pollingInterval = setInterval(async () => { try { const res = await fetch(`/api/status?session_id=${currentSessionId}`); if (!res.ok) return; const data = await res.json(); // Render round cards incrementally const rounds = data.rounds || []; renderRoundCards(rounds); // Load data files during polling loadDataFiles(); // Update progress bar during analysis // Use rounds.length (actual completed analysis rounds) for display // instead of current_round (which includes non-code rounds like collect_figures) if (data.is_running && data.progress_percentage !== undefined) { const displayRound = rounds.length || data.current_round || 0; updateProgressBar(data.progress_percentage, data.status_message, displayRound, data.max_rounds); } if (!data.is_running && isRunning) { const displayRound = rounds.length || data.current_round || data.max_rounds; updateProgressBar(100, 'Analysis complete', displayRound, data.max_rounds); setTimeout(hideProgressBar, 3000); setRunningState(false); clearInterval(pollingInterval); // Final render of rounds renderRoundCards(data.rounds || []); loadDataFiles(); if (data.has_report) { await loadReport(); switchTab('report'); } if (data.script_path && downloadScriptBtn) { downloadScriptBtn.classList.remove('hidden'); downloadScriptBtn.style.display = 'inline-flex'; } } } catch (e) { console.error('Polling error', e); } }, 2000); } // --- Execution Process Tab (Task 11) --- function renderRoundCards(rounds) { if (!rounds || rounds.length === 0) return; const wrapper = document.getElementById('roundCardsWrapper'); if (!wrapper) return; // Handle server restart: if rounds shrunk, re-render all if (rounds.length < lastRenderedRound) { lastRenderedRound = 0; wrapper.innerHTML = ''; } // Remove empty state if present const emptyState = document.getElementById('executionEmptyState'); if (emptyState) emptyState.remove(); // Only render new rounds for (let i = lastRenderedRound; i < rounds.length; i++) { const rd = rounds[i]; const card = createRoundCard(rd); wrapper.appendChild(card); } lastRenderedRound = rounds.length; // Auto-scroll when running if (isRunning) { const executionTab = document.getElementById('executionTab'); if (executionTab) { executionTab.scrollTop = executionTab.scrollHeight; } } } function createRoundCard(rd) { const roundNum = rd.round || 0; const summary = escapeHtml(rd.result_summary || ''); const reasoning = escapeHtml(rd.reasoning || ''); const code = escapeHtml(rd.code || ''); const rawLog = escapeHtml(rd.raw_log || ''); const card = document.createElement('div'); card.className = 'round-card'; card.setAttribute('data-round', roundNum); // Build evidence table HTML let evidenceHtml = ''; const evidenceRows = rd.evidence_rows || []; if (evidenceRows.length > 0) { const cols = Object.keys(evidenceRows[0]); evidenceHtml = `
本轮数据案例
${cols.map(c => ``).join('')}${evidenceRows.map(row => `${cols.map(c => ``).join('')}` ).join('')}
${escapeHtml(c)}
${escapeHtml(String(row[c] ?? ''))}
`; } card.innerHTML = `
Round ${roundNum} ${summary}
`; return card; } window.toggleRoundCard = function(roundNum) { const card = document.querySelector(`.round-card[data-round="${roundNum}"]`); if (!card) return; const body = card.querySelector('.round-card-body'); const icon = card.querySelector('.round-toggle-icon'); if (!body) return; body.classList.toggle('hidden'); if (icon) { icon.classList.toggle('fa-chevron-down'); icon.classList.toggle('fa-chevron-up'); } card.classList.toggle('expanded'); } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // --- Data Files Tab (Task 12) --- async function loadDataFiles() { if (!currentSessionId) return; try { const res = await fetch(`/api/data-files?session_id=${currentSessionId}`); if (!res.ok) return; const data = await res.json(); const files = data.files || []; const grid = document.getElementById('fileCardsGrid'); const emptyState = document.getElementById('datafilesEmptyState'); if (!grid) return; if (files.length === 0) { grid.innerHTML = ''; if (emptyState) emptyState.classList.remove('hidden'); return; } if (emptyState) emptyState.classList.add('hidden'); grid.innerHTML = files.map(f => { const desc = escapeHtml(f.description || ''); const name = escapeHtml(f.filename || ''); const rows = f.rows || 0; const iconClass = name.endsWith('.xlsx') ? 'fa-file-excel' : 'fa-file-csv'; return `
${name}
${desc} · ${rows}行
`; }).join(''); } catch (e) { console.error('Failed to load data files', e); } } window.previewDataFile = async function(filename) { if (!currentSessionId) return; try { const res = await fetch(`/api/data-files/preview?session_id=${currentSessionId}&filename=${encodeURIComponent(filename)}`); if (!res.ok) { alert('Failed to load preview'); return; } const data = await res.json(); const columns = data.columns || []; const rows = data.rows || []; const panel = document.getElementById('dataPreviewPanel'); const nameEl = document.getElementById('previewFileName'); const container = document.getElementById('previewTableContainer'); if (!panel || !container) return; nameEl.textContent = filename; let tableHtml = ''; tableHtml += '' + columns.map(c => ``).join('') + ''; tableHtml += ''; for (const row of rows) { tableHtml += '' + columns.map(c => ``).join('') + ''; } tableHtml += '
${escapeHtml(c)}
${escapeHtml(String(row[c] ?? ''))}
'; container.innerHTML = tableHtml; panel.classList.remove('hidden'); } catch (e) { console.error('Preview failed', e); } } window.downloadDataFile = function(filename) { if (!currentSessionId) return; const link = document.createElement('a'); link.href = `/api/data-files/download?session_id=${currentSessionId}&filename=${encodeURIComponent(filename)}`; link.download = ''; document.body.appendChild(link); link.click(); document.body.removeChild(link); } window.closePreview = function() { const panel = document.getElementById('dataPreviewPanel'); if (panel) panel.classList.add('hidden'); } // --- Report Logic with Supporting Data (Task 14) --- async function loadReport() { if (!currentSessionId) return; try { const res = await fetch(`/api/report?session_id=${currentSessionId}`); const data = await res.json(); if (!data.content || data.content === "Report not ready.") { reportContainer.innerHTML = '

Analysis in progress or no report generated yet.

'; reportParagraphs = []; supportingData = {}; return; } reportParagraphs = data.paragraphs || []; supportingData = data.supporting_data || {}; renderParagraphReport(reportParagraphs); } catch (e) { reportContainer.innerHTML = '

Failed to load report.

'; } } function renderParagraphReport(paragraphs) { if (!paragraphs || paragraphs.length === 0) { reportContainer.innerHTML = '

No report content.

'; return; } let html = ''; for (const p of paragraphs) { const renderedContent = marked.parse(p.content); const typeClass = `para-${p.type}`; const hasSupportingData = supportingData[p.id] && supportingData[p.id].length > 0; const supportingBtn = hasSupportingData ? `` : ''; html += `
${renderedContent}
${supportingBtn}
`; } reportContainer.innerHTML = html; } window.showSupportingData = function(paraId) { const rows = supportingData[paraId]; if (!rows || rows.length === 0) return; const modal = document.getElementById('supportingDataModal'); const body = document.getElementById('supportingDataBody'); if (!modal || !body) return; const cols = Object.keys(rows[0]); let tableHtml = ''; tableHtml += '' + cols.map(c => ``).join('') + ''; tableHtml += ''; for (const row of rows) { tableHtml += '' + cols.map(c => ``).join('') + ''; } tableHtml += '
${escapeHtml(c)}
${escapeHtml(String(row[c] ?? ''))}
'; body.innerHTML = tableHtml; modal.classList.remove('hidden'); } window.closeSupportingData = function() { const modal = document.getElementById('supportingDataModal'); if (modal) modal.classList.add('hidden'); } // --- Paragraph Selection & Polishing (preserved from original) --- window.selectParagraph = function(paraId) { document.querySelectorAll('.report-paragraph').forEach(el => { el.classList.remove('selected'); el.querySelector('.para-actions')?.classList.add('hidden'); }); const target = document.querySelector(`[data-para-id="${paraId}"]`); if (target) { target.classList.add('selected'); target.querySelector('.para-actions')?.classList.remove('hidden'); } } window.polishParagraph = async function(paraId, mode, customInstruction = '') { if (!currentSessionId) return; const target = document.querySelector(`[data-para-id="${paraId}"]`); if (!target) return; const actionsEl = target.querySelector('.para-actions'); const originalActions = actionsEl.innerHTML; actionsEl.innerHTML = ' AI 润色中...'; try { const res = await fetch('/api/report/polish', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ session_id: currentSessionId, paragraph_id: paraId, mode: mode, custom_instruction: customInstruction, }) }); if (!res.ok) { const err = await res.json(); alert('润色失败: ' + (err.detail || 'Unknown error')); actionsEl.innerHTML = originalActions; return; } const data = await res.json(); showPolishDiff(target, paraId, data.original, data.polished); } catch (e) { console.error(e); alert('润色请求失败'); actionsEl.innerHTML = originalActions; } } function showPolishDiff(targetEl, paraId, original, polished) { const polishedHtml = marked.parse(polished); targetEl.innerHTML = `
润色结果预览
原文
${marked.parse(original)}
润色后
${polishedHtml}
`; document.getElementById(`acceptBtn-${paraId}`).addEventListener('click', (e) => { e.stopPropagation(); applyPolish(paraId, polished); }); document.getElementById(`rejectBtn-${paraId}`).addEventListener('click', (e) => { e.stopPropagation(); rejectPolish(paraId); }); } window.applyPolish = async function(paraId, newContent) { if (!currentSessionId) return; try { const res = await fetch('/api/report/apply', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ session_id: currentSessionId, paragraph_id: paraId, new_content: newContent, }) }); if (res.ok) { await loadReport(); } else { alert('应用失败'); } } catch (e) { console.error(e); alert('应用失败'); } } window.rejectPolish = function(paraId) { loadReport(); } window.showCustomPolish = function(paraId) { const target = document.querySelector(`[data-para-id="${paraId}"]`); if (!target) return; const actionsEl = target.querySelector('.para-actions'); if (!actionsEl) return; actionsEl.innerHTML = `
`; document.getElementById(`customInput-${paraId}`)?.focus(); } window.submitCustomPolish = function(paraId) { const input = document.getElementById(`customInput-${paraId}`); if (!input) return; const instruction = input.value.trim(); if (!instruction) { alert('请输入润色指令'); return; } polishParagraph(paraId, 'custom', instruction); } // --- Download / Export --- window.downloadScript = async function () { if (!currentSessionId) return; const link = document.createElement('a'); link.href = `/api/download_script?session_id=${currentSessionId}`; link.download = ''; document.body.appendChild(link); link.click(); document.body.removeChild(link); } window.triggerExport = async function () { if (!currentSessionId) { alert("No active session to export."); return; } const btn = document.getElementById('exportBtn'); const originalContent = btn.innerHTML; btn.innerHTML = ' Zipping...'; btn.disabled = true; try { window.open(`/api/export?session_id=${currentSessionId}`, '_blank'); } catch (e) { alert("Export failed: " + e.message); } finally { setTimeout(() => { btn.innerHTML = originalContent; btn.disabled = false; }, 2000); } } // --- Follow-up Chat --- window.sendFollowUp = async function () { if (!currentSessionId || isRunning) return; const input = document.getElementById('followUpInput'); const message = input.value.trim(); if (!message) return; input.disabled = true; try { const res = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ session_id: currentSessionId, message: message }) }); if (res.ok) { input.value = ''; setRunningState(true); // Reset round rendering for follow-up lastRenderedRound = 0; const wrapper = document.getElementById('roundCardsWrapper'); if (wrapper) wrapper.innerHTML = ''; startPolling(); switchTab('execution'); } else { alert('Failed to send request'); } } catch (e) { console.error(e); } finally { input.disabled = false; } } // --- History Logic --- async function loadHistory() { const list = document.getElementById('historyList'); if (!list) return; try { const res = await fetch('/api/history'); const data = await res.json(); if (data.history.length === 0) { list.innerHTML = '
No history yet
'; return; } let html = ''; data.history.forEach(item => { html += `
${item.id}
`; }); list.innerHTML = html; } catch (e) { console.error("Failed to load history", e); } } window.loadSession = async function (sessionId) { if (isRunning) { alert("Analysis in progress, please wait."); return; } currentSessionId = sessionId; document.querySelectorAll('.history-item').forEach(el => el.classList.remove('active')); const activeItem = document.getElementById(`hist-${sessionId}`); if (activeItem) activeItem.classList.add('active'); reportContainer.innerHTML = ""; if (downloadScriptBtn) downloadScriptBtn.classList.add('hidden'); const tplGroup = document.getElementById('templateSelectorGroup'); if (tplGroup) tplGroup.classList.add('hidden'); // Reset execution state for loaded session lastRenderedRound = 0; const wrapper = document.getElementById('roundCardsWrapper'); if (wrapper) wrapper.innerHTML = ''; try { const res = await fetch(`/api/status?session_id=${sessionId}`); if (res.ok) { const data = await res.json(); // Render rounds for historical session renderRoundCards(data.rounds || []); // Load data files for historical session loadDataFiles(); if (data.has_report) { await loadReport(); if (data.script_path && downloadScriptBtn) { downloadScriptBtn.classList.remove('hidden'); downloadScriptBtn.style.display = 'inline-flex'; } switchTab('report'); } else { switchTab('execution'); } } } catch (e) { console.error("Error loading session", e); } } // --- Init & Navigation (Task 13) --- document.addEventListener('DOMContentLoaded', () => { loadTemplates(); loadHistory(); }); window.switchView = function (viewName) { console.log("View switch requested:", viewName); } window.switchTab = function (tabName) { // Deactivate all tabs document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); // Hide all tab content ['execution', 'datafiles', 'report'].forEach(name => { const content = document.getElementById(`${name}Tab`); if (content) content.classList.add('hidden'); }); // Activate the clicked tab button document.querySelectorAll('.tab').forEach(btn => { if (btn.getAttribute('onclick') && btn.getAttribute('onclick').includes(`'${tabName}'`)) { btn.classList.add('active'); } }); // Show the selected tab content if (tabName === 'execution') { document.getElementById('executionTab').classList.remove('hidden'); } else if (tabName === 'datafiles') { document.getElementById('datafilesTab').classList.remove('hidden'); if (currentSessionId) loadDataFiles(); } else if (tabName === 'report') { document.getElementById('reportTab').classList.remove('hidden'); } }