const historyList = document.querySelector("#history-list"); const refreshHistoryBtn = document.querySelector("#refresh-history-btn"); const apiStatus = document.querySelector("#api-status"); const artifactStatus = document.querySelector("#artifact-status"); const questionInput = document.querySelector("#question"); const runBtn = document.querySelector("#run-btn"); const timeline = document.querySelector("#timeline"); const newChatBtn = document.querySelector("#new-chat-btn"); const clearScreenBtn = document.querySelector("#clear-screen-btn"); const STORAGE_KEY = "algorithm-agent-history-v1"; let eventSource = null; let currentWorkflow = null; let currentQuestion = ""; let running = false; const defaultQuestion = `给定一个有向带权图,节点为 A, B, C, D, E。 边为 A->B:4, A->C:2, B->C:1, B->D:5, C->D:8, C->E:10, D->E:2。 请从 A 到 E 找到最短路径,并说明应该使用什么算法。`; function escapeHtml(value) { return String(value || "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function nowText() { const d = new Date(); const pad = (n) => String(n).padStart(2, "0"); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; } function loadHistory() { try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]"); } catch { return []; } } function saveHistory(history) { localStorage.setItem(STORAGE_KEY, JSON.stringify(history.slice(0, 20))); } function renderHistory() { const history = loadHistory(); if (!history.length) { historyList.innerHTML = '
还没有历史记录。
'; return; } historyList.innerHTML = history .map( (item, index) => ` `, ) .join(""); } async function loadStatus() { const resp = await fetch("/api/status"); const status = await resp.json(); apiStatus.textContent = status.enabled ? `${status.provider} · ${status.model} · LLM` : `${status.provider} · 未检测到 Key`; apiStatus.classList.toggle("offline", !status.enabled); } function setRunning(value) { running = value; runBtn.disabled = value; runBtn.textContent = value ? "运行中" : "发送"; newChatBtn.disabled = value; clearScreenBtn.disabled = value; historyList.classList.toggle("locked", value); } function clearTimeline(message = "从右下方输入问题开始。") { timeline.innerHTML = `
${escapeHtml(message)}
`; } function removeEmpty() { const empty = timeline.querySelector(".empty-state"); if (empty) empty.remove(); } function appendChatMessage({ role, title, content, time, artifact }) { removeEmpty(); const el = document.createElement("article"); el.className = `chat-message ${role}`; const badge = role === "user" ? "用户" : "回答"; el.innerHTML = `
${escapeHtml(title || "")} ${escapeHtml(time || "")}
${badge}
${formatText(content || "")}
${artifact ? renderArtifacts(artifact) : ""}
`; timeline.appendChild(el); typesetMath(el); timeline.scrollTop = timeline.scrollHeight; } function formatText(text) { return renderMarkdown(text); } function protectLatex(text) { const store = []; const protectedText = String(text || "").replace( /(\$\$[\s\S]*?\$\$|\\\[[\s\S]*?\\\]|\\\([\s\S]*?\\\)|\$[^$\n]+\$)/g, (match) => { const token = `@@LATEX_${store.length}@@`; store.push(match); return token; }, ); return { protectedText, store }; } function restoreLatex(html, store) { let output = html; store.forEach((item, index) => { output = output.replaceAll(`@@LATEX_${index}@@`, item); }); return output; } function renderMarkdown(text) { const { protectedText, store } = protectLatex(text); const lines = protectedText.replaceAll("\r\n", "\n").split("\n"); const html = []; let paragraph = []; let listType = null; let inCode = false; let codeLines = []; let blockquote = []; const flushParagraph = () => { if (!paragraph.length) return; html.push(`

${renderInline(paragraph.join(" "), store)}

`); paragraph = []; }; const flushList = () => { if (!listType) return; html.push(``); listType = null; }; const flushQuote = () => { if (!blockquote.length) return; html.push(`
${blockquote.map((line) => renderInline(line, store)).join("
")}
`); blockquote = []; }; const closeBlocks = () => { flushParagraph(); flushList(); flushQuote(); }; for (let i = 0; i < lines.length; i += 1) { const rawLine = lines[i]; const line = rawLine.trimEnd(); if (line.trim().startsWith("```")) { if (inCode) { html.push(`
${escapeHtml(codeLines.join("\n"))}
`); codeLines = []; inCode = false; } else { closeBlocks(); inCode = true; } continue; } if (inCode) { codeLines.push(rawLine); continue; } if (!line.trim()) { closeBlocks(); continue; } if (isTableStart(lines, i)) { closeBlocks(); const table = collectTable(lines, i); html.push(renderTable(table.rows, store)); i = table.nextIndex - 1; continue; } const heading = /^(#{1,4})\s+(.+)$/.exec(line); if (heading) { closeBlocks(); const level = heading[1].length; html.push(`${renderInline(heading[2], store)}`); continue; } const quote = /^>\s?(.*)$/.exec(line); if (quote) { flushParagraph(); flushList(); blockquote.push(quote[1]); continue; } const unordered = /^[-*]\s+(.+)$/.exec(line); if (unordered) { flushParagraph(); flushQuote(); if (listType !== "ul") { flushList(); listType = "ul"; html.push("