/* 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());