Files
blog-app/static/js/pages/analyze.js
2026-08-17 17:11:27 +08:00

109 lines
4.4 KiB
JavaScript

/* Analyze 页逻辑: SSE 流式分析 (共用 components/sse-client.js).
* 与 ai-panel.js 共用 createAnalysisStream, 只负责本页 DOM/Tab 交互.
*/
import { createAnalysisStream } from "../components/sse-client.js";
import { escapeHtml } from "../utils.js";
export function initAnalyzePage() {
let currentStream = null;
const scopeSelect = document.getElementById("scopeSelect");
const questionInput = document.getElementById("questionInput");
const startBtn = document.getElementById("startBtn");
const presetBtns = document.querySelectorAll(".preset-btn");
const observationContent = document.getElementById("observationContent");
const promptContent = document.getElementById("promptContent");
const reasoningContent = document.getElementById("reasoningContent");
const conclusionContent = document.getElementById("conclusionContent");
// Programmatic tab switching (Bootstrap tabs)
function switchTab(name) {
const btn = document.getElementById(name + "-tab");
if (btn) btn.click();
}
function startAnalysis(question, preset) {
if (currentStream) {
currentStream.close();
currentStream = null;
}
// Reset content
observationContent.innerHTML = '<div class="text-muted">正在收集上下文...</div>';
promptContent.innerHTML = '<div class="text-muted">等待...</div>';
reasoningContent.innerHTML = '<div class="text-muted">等待...</div>';
conclusionContent.innerHTML = '<div class="text-muted">等待...</div>';
currentStream = createAnalysisStream(scopeSelect.value, question, preset, {
onStep(step, content) {
if (step === "context") {
observationContent.innerHTML = `<div class="alert alert-info">${escapeHtml(content)}</div>`;
} else if (step === "prompt") {
promptContent.innerHTML = `<pre class="bg-light p-3 rounded" style="white-space: pre-wrap;">${escapeHtml(content)}</pre>`;
}
},
onToken(text, section) {
const pane = section === "conclusion" ? conclusionContent : reasoningContent;
if (pane.querySelector(".text-muted")) pane.innerHTML = "";
pane.innerHTML += escapeHtml(text);
pane.scrollTop = pane.scrollHeight;
},
onSectionStart(section) {
if (section === "reasoning") {
reasoningContent.innerHTML = "";
switchTab("reasoning");
} else {
conclusionContent.innerHTML = "";
switchTab("conclusion");
}
},
onError(msg) {
observationContent.innerHTML = `<div class="alert alert-danger">错误: ${escapeHtml(msg)}</div>`;
currentStream = null;
},
onComplete(content) {
// 追加提示, 不覆盖已流式写入的结论内容
conclusionContent.innerHTML += `<div class="alert alert-success">${escapeHtml(content)}</div>`;
currentStream = null;
},
});
}
// Preset button click
presetBtns.forEach((btn) => {
btn.addEventListener("click", () => {
startAnalysis(null, btn.dataset.preset);
});
});
// Start button click
startBtn.addEventListener("click", () => {
const question = questionInput.value.trim();
if (!question) {
alert("请输入自定义问题或选择预设分析");
return;
}
startAnalysis(question, null);
});
// Dynamically fill scope <select> from /api/projects so the options always
// match the DB (previously hardcoded project/1..3 which drifted out of sync).
// Same pattern as ai-panel.js loadScopes().
fetch("/api/projects")
.then((r) => r.json())
.then((data) => {
const projects = data.projects || data || [];
projects.forEach((p) => {
const opt = document.createElement("option");
opt.value = "project/" + p.id;
opt.textContent = "项目: " + (p.name || p.full_name || "#" + p.id);
scopeSelect.appendChild(opt);
});
})
.catch((err) => {
console.error("analyze: 加载项目列表失败", err);
});
}