/* CorrectBench Web UI Logic */ const App = { state: { running: false, stopping: false, autoScroll: true, logLines: [], eventSource: null, statusInterval: null, }, // ===== 初始化 ===== init() { this.bindEvents(); this.loadConfigs(); this.startStatusPolling(); ResultsApp.init(); }, bindEvents() { document.getElementById('config-select').addEventListener('change', (e) => { this.onConfigChange(e.target.value); }); document.getElementById('btn-start').addEventListener('click', () => this.startTask()); document.getElementById('btn-stop').addEventListener('click', () => this.stopTask()); document.getElementById('btn-clear').addEventListener('click', () => this.clearLogs()); document.getElementById('auto-scroll').addEventListener('change', (e) => { this.state.autoScroll = e.target.checked; }); }, // ===== 配置管理 ===== async loadConfigs() { try { const resp = await fetch('/api/configs'); const configs = await resp.json(); const select = document.getElementById('config-select'); select.innerHTML = ''; // 分组:根目录文件 vs configs/ 子目录 const rootConfigs = configs.filter(c => !c.name.startsWith('configs/')); const subConfigs = configs.filter(c => c.name.startsWith('configs/')); if (rootConfigs.length > 0) { const group = document.createElement('optgroup'); group.label = '📝 用户配置'; rootConfigs.forEach(c => { const opt = document.createElement('option'); opt.value = c.name; opt.textContent = c.name; group.appendChild(opt); }); select.appendChild(group); } if (subConfigs.length > 0) { const group = document.createElement('optgroup'); group.label = '📁 预设配置'; subConfigs.forEach(c => { const opt = document.createElement('option'); opt.value = c.name; opt.textContent = c.name.replace('configs/', ''); group.appendChild(opt); }); select.appendChild(group); } // 默认选中 custom.yaml const opts = Array.from(select.options); const customIdx = opts.findIndex(o => o.value === 'custom.yaml'); if (customIdx >= 0) { select.selectedIndex = customIdx; } this.onConfigChange(select.value); } catch (e) { console.error('加载配置失败:', e); } }, async onConfigChange(configName) { if (!configName) return; try { const resp = await fetch(`/api/config/${encodeURIComponent(configName)}`); const data = await resp.json(); if (data.error) { this.setConfigInfo({ error: data.error }); return; } this.setConfigInfo(data.info); document.getElementById('config-path').textContent = data.path; } catch (e) { console.error('加载配置详情失败:', e); } }, setConfigInfo(info) { const container = document.getElementById('config-info'); const tasksContainer = document.getElementById('task-list'); if (info.error) { container.innerHTML = `
错误: ${info.error}
`; tasksContainer.innerHTML = ''; return; } container.innerHTML = `
模型 ${info.model || '未指定'}
RTL生成模型 ${info.rtlgen_model || '未指定'}
运行模式 ${info.mode || '未指定'}
最大迭代 ${info.itermax || '未指定'}
`; // 题目列表 const tasks = info.tasks || []; if (tasks.length > 0) { tasksContainer.innerHTML = tasks.map(t => `
${t}
`).join(''); } else if (info.dataset) { tasksContainer.innerHTML = `
数据集: ${info.dataset}
`; } else { tasksContainer.innerHTML = `
未指定题目
`; } }, // ===== 任务控制 ===== async startTask() { const configName = document.getElementById('config-select').value; if (!configName) return; try { const resp = await fetch('/api/start', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ config: configName }) }); const data = await resp.json(); if (data.error) { alert(data.error); return; } this.state.running = true; this.state.stopping = false; this.state.logLines = []; this.clearLogDisplay(); this.appendLogLine('正在启动...', 'system'); this.updateUI(); this.startLogStream(); } catch (e) { console.error('启动失败:', e); alert('启动失败: ' + e.message); } }, async stopTask() { if (!this.state.running) return; try { const resp = await fetch('/api/stop', { method: 'POST' }); const data = await resp.json(); if (data.error) { alert(data.error); return; } this.state.stopping = true; this.appendLogLine('[正在停止...]', 'warning'); this.updateUI(); } catch (e) { console.error('停止失败:', e); } }, clearLogs() { this.state.logLines = []; this.clearLogDisplay(); document.getElementById('log-line-count').textContent = '0 行'; }, // ===== 日志流 ===== startLogStream() { // 关闭旧连接 if (this.state.eventSource) { this.state.eventSource.close(); this.state.eventSource = null; } this._connectSSE(); }, _connectSSE() { const es = new EventSource('/api/logs'); this.state.eventSource = es; es.onmessage = (event) => { try { const data = JSON.parse(event.data); const line = data.line; const cls = this.classifyLine(line); this.appendLogLine(line, cls); // 检测结束标记 if (line === '[完成]' || line === '[已停止]') { this.state.running = false; this.state.stopping = false; this.updateUI(); es.close(); this.state.eventSource = null; } } catch (e) { console.error('解析日志失败:', e); } }; es.onerror = () => { es.close(); this.state.eventSource = null; if (this.state.running) { // SSE 断开但任务仍在运行,2秒后重连 // 先用快照补齐可能丢失的日志 this._reconnectWithSnapshot(); } else { // 任务已结束,确认最终状态 setTimeout(() => this.checkStatus(), 500); } }; }, async _reconnectWithSnapshot() { // 获取快照,补齐断开期间丢失的日志 try { const resp = await fetch('/api/logs/snapshot'); const data = await resp.json(); const snapshotSet = new Set(data.lines); // 找出快照中有但本地没有的行 let newLines = []; for (const line of data.lines) { if (!this.state.logLines.includes(line)) { newLines.push(line); } } // 追加缺失的日志 for (const line of newLines) { const cls = this.classifyLine(line); this.appendLogLine(line, cls, true); // true = 不重复检测 } // 检查快照中是否包含结束标记 const lastLine = data.lines[data.lines.length - 1]; if (lastLine === '[完成]' || lastLine === '[已停止]') { this.state.running = false; this.state.stopping = false; this.updateUI(); return; } } catch (e) { console.warn('获取日志快照失败:', e); } // 任务仍在运行,重连 SSE if (this.state.running) { setTimeout(() => this._connectSSE(), 2000); } }, classifyLine(line) { if (line.includes('[完成]')) return 'success system'; if (line.includes('[已停止]') || line.includes('[正在停止')) return 'warning system'; if (line.includes('[错误]') || line.includes('❌') || line.includes('ERROR')) return 'error'; if (line.includes('⚠️') || line.includes('WARNING')) return 'warning'; if (line.includes('INFO')) return 'info'; return ''; }, appendLogLine(text, cls, skipDuplicate) { // 跳过重复行(重连补齐时使用) if (skipDuplicate && this.state.logLines.includes(text)) { return; } this.state.logLines.push(text); const container = document.getElementById('log-content'); // 移除占位符 const placeholder = container.querySelector('.log-placeholder'); if (placeholder) placeholder.remove(); const div = document.createElement('div'); div.className = 'log-line' + (cls ? ' ' + cls : ''); div.textContent = text; container.appendChild(div); // 限制显示行数 while (container.children.length > 2000) { container.removeChild(container.firstChild); } // 更新行数 document.getElementById('log-line-count').textContent = `${this.state.logLines.length} 行`; // 自动滚动 if (this.state.autoScroll) { container.scrollTop = container.scrollHeight; } }, clearLogDisplay() { const container = document.getElementById('log-content'); container.innerHTML = `
⌨️
等待开始生成...
`; }, // ===== 状态管理 ===== startStatusPolling() { this.state.statusInterval = setInterval(() => this.checkStatus(), 3000); }, async checkStatus() { try { const resp = await fetch('/api/status'); const data = await resp.json(); const wasRunning = this.state.running; this.state.running = data.running; // 如果状态从 running 变为非 running,且没有通过 SSE 检测到 if (wasRunning && !data.running && this.state.eventSource) { this.state.eventSource.close(); this.state.eventSource = null; this.state.stopping = false; // 获取最终日志快照 await this.fetchLogSnapshot(); } this.updateUI(data); } catch (e) { // 忽略轮询错误 } }, async fetchLogSnapshot() { try { const resp = await fetch('/api/logs/snapshot'); const data = await resp.json(); // 只在日志为空时加载快照 if (this.state.logLines.length === 0 && data.lines.length > 0) { this.clearLogDisplay(); data.lines.forEach(line => { const cls = this.classifyLine(line); this.appendLogLine(line, cls); }); } } catch (e) { // 忽略 } }, updateUI(statusData) { const startBtn = document.getElementById('btn-start'); const stopBtn = document.getElementById('btn-stop'); const select = document.getElementById('config-select'); const badge = document.getElementById('status-badge'); const elapsed = document.getElementById('elapsed'); startBtn.disabled = this.state.running; stopBtn.disabled = !this.state.running; select.disabled = this.state.running; // 状态徽章 if (this.state.running && this.state.stopping) { badge.className = 'status-badge stopping'; badge.innerHTML = '正在停止'; } else if (this.state.running) { badge.className = 'status-badge running'; badge.innerHTML = '运行中'; } else if (this.state.logLines.length > 0) { const lastLine = this.state.logLines[this.state.logLines.length - 1]; if (lastLine === '[已停止]') { badge.className = 'status-badge stopped'; badge.innerHTML = '已停止'; } else if (lastLine === '[完成]') { badge.className = 'status-badge completed'; badge.innerHTML = '已完成'; } else { badge.className = 'status-badge idle'; badge.innerHTML = '就绪'; } } else { badge.className = 'status-badge idle'; badge.innerHTML = '就绪'; } // 计时 if (statusData && statusData.elapsed > 0) { elapsed.textContent = this.formatTime(statusData.elapsed); } else if (!this.state.running) { elapsed.textContent = ''; } }, formatTime(seconds) { const m = Math.floor(seconds / 60); const s = Math.floor(seconds % 60); if (m > 0) return `${m}m ${s}s`; return `${s}s`; }, }; // 页面加载完成后初始化 document.addEventListener('DOMContentLoaded', () => App.init());