initial: blog-app snapshot

This commit is contained in:
omo
2026-08-17 17:11:27 +08:00
commit 65cb9d5ead
125 changed files with 14720 additions and 0 deletions

View File

@@ -0,0 +1,137 @@
// 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;
}