Files
CGA-bench/web/static/app.js
2026-05-22 10:02:42 +08:00

438 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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 = `<div class="config-info-row"><span class="label" style="color:var(--danger)">错误: ${info.error}</span></div>`;
tasksContainer.innerHTML = '';
return;
}
container.innerHTML = `
<div class="config-info-row">
<span class="label">模型</span>
<span class="value model">${info.model || '未指定'}</span>
</div>
<div class="config-info-row">
<span class="label">RTL生成模型</span>
<span class="value model">${info.rtlgen_model || '未指定'}</span>
</div>
<div class="config-info-row">
<span class="label">运行模式</span>
<span class="value">${info.mode || '未指定'}</span>
</div>
<div class="config-info-row">
<span class="label">最大迭代</span>
<span class="value">${info.itermax || '未指定'}</span>
</div>
`;
// 题目列表
const tasks = info.tasks || [];
if (tasks.length > 0) {
tasksContainer.innerHTML = tasks.map(t => `<div class="task-item">${t}</div>`).join('');
} else if (info.dataset) {
tasksContainer.innerHTML = `<div class="task-item" style="color:var(--text-muted)">数据集: ${info.dataset}</div>`;
} else {
tasksContainer.innerHTML = `<div class="task-item" style="color:var(--text-muted)">未指定题目</div>`;
}
},
// ===== 任务控制 =====
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 = `
<div class="log-placeholder">
<div class="log-placeholder-content">
<div class="icon">⌨️</div>
<div>等待开始生成...</div>
</div>
</div>`;
},
// ===== 状态管理 =====
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 = '<span class="status-dot"></span>正在停止';
} else if (this.state.running) {
badge.className = 'status-badge running';
badge.innerHTML = '<span class="status-dot"></span>运行中';
} 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 = '<span class="status-dot"></span>已停止';
} else if (lastLine === '[完成]') {
badge.className = 'status-badge completed';
badge.innerHTML = '<span class="status-dot"></span>已完成';
} 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());