109 lines
4.4 KiB
JavaScript
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);
|
|
});
|
|
}
|