138 lines
4.9 KiB
JavaScript
138 lines
4.9 KiB
JavaScript
|
|
// State
|
|
let currentEventSource = null;
|
|
let currentTab = 'observation'; // Track which tab we're writing to
|
|
let currentSection = 'reasoning'; // 'reasoning' | 'conclusion', 由 SSE section 事件驱动
|
|
|
|
// DOM elements
|
|
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();
|
|
}
|
|
|
|
// Tab switching
|
|
document.querySelectorAll('#outputTabs button').forEach(button => {
|
|
button.addEventListener('click', (e) => {
|
|
const tabId = e.target.id.replace('-tab', '');
|
|
currentTab = tabId;
|
|
});
|
|
});
|
|
|
|
// Preset button click
|
|
presetBtns.forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const preset = btn.dataset.preset;
|
|
startAnalysis(null, preset);
|
|
});
|
|
});
|
|
|
|
// Start button click
|
|
startBtn.addEventListener('click', () => {
|
|
const question = questionInput.value.trim();
|
|
if (!question) {
|
|
alert('请输入自定义问题或选择预设分析');
|
|
return;
|
|
}
|
|
startAnalysis(question, null);
|
|
});
|
|
|
|
function startAnalysis(question, preset) {
|
|
// Close existing connection
|
|
if (currentEventSource) {
|
|
currentEventSource.close();
|
|
currentEventSource = 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>';
|
|
currentSection = 'reasoning';
|
|
|
|
// Build URL
|
|
const scope = scopeSelect.value;
|
|
const params = new URLSearchParams({scope});
|
|
if (question) params.append('question', question);
|
|
if (preset) params.append('preset', preset);
|
|
|
|
const url = `/api/analyze/stream?${params.toString()}`;
|
|
|
|
// Start SSE
|
|
currentEventSource = new EventSource(url);
|
|
|
|
// Handle step events
|
|
currentEventSource.addEventListener('step', (e) => {
|
|
const data = JSON.parse(e.data);
|
|
const step = data.step;
|
|
const content = data.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>`;
|
|
} else if (step === 'complete') {
|
|
// 追加提示, 不覆盖已流式写入的结论内容
|
|
conclusionContent.innerHTML += `<div class="alert alert-success">${escapeHtml(content)}</div>`;
|
|
currentEventSource.close();
|
|
currentEventSource = null;
|
|
}
|
|
});
|
|
|
|
// Section 事件: 后端识别 ===REASONING=== / ===CONCLUSION=== 后显式下发
|
|
currentEventSource.addEventListener('reasoning_start', () => {
|
|
currentSection = 'reasoning';
|
|
reasoningContent.innerHTML = '';
|
|
switchTab('reasoning');
|
|
});
|
|
|
|
currentEventSource.addEventListener('conclusion_start', () => {
|
|
currentSection = 'conclusion';
|
|
conclusionContent.innerHTML = '';
|
|
switchTab('conclusion');
|
|
});
|
|
|
|
// Handle token events (streaming LLM output, 分隔符已被后端剥离)
|
|
currentEventSource.addEventListener('token', (e) => {
|
|
const data = JSON.parse(e.data);
|
|
const text = data.text;
|
|
const pane = currentSection === 'conclusion' ? conclusionContent : reasoningContent;
|
|
if (pane.querySelector('.text-muted')) pane.innerHTML = '';
|
|
pane.innerHTML += escapeHtml(text);
|
|
pane.scrollTop = pane.scrollHeight;
|
|
});
|
|
|
|
// Handle errors
|
|
currentEventSource.addEventListener('error', (e) => {
|
|
const data = JSON.parse(e.data);
|
|
const errorMsg = data.error || '未知错误';
|
|
observationContent.innerHTML = `<div class="alert alert-danger">错误: ${escapeHtml(errorMsg)}</div>`;
|
|
currentEventSource.close();
|
|
currentEventSource = null;
|
|
});
|
|
|
|
currentEventSource.onerror = (e) => {
|
|
observationContent.innerHTML = `<div class="alert alert-danger">连接错误, 请检查后端日志</div>`;
|
|
currentEventSource.close();
|
|
currentEventSource = null;
|
|
};
|
|
}
|
|
|
|
// Helper: escape HTML
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|