| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894 |
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>完整性审查方案对比测试</title>
- <style>
- *{margin:0;padding:0;box-sizing:border-box}
- body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;background:#f5f7fa;color:#333;line-height:1.6}
- .container{max-width:1200px;margin:0 auto;padding:20px}
- header{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:#fff;padding:24px;border-radius:12px;margin-bottom:20px}
- header h1{font-size:24px;margin-bottom:6px}
- header p{opacity:.9;font-size:13px}
- .panel{background:#fff;border-radius:12px;padding:20px;margin-bottom:16px;box-shadow:0 2px 8px rgba(0,0,0,.06)}
- .panel h2{font-size:16px;margin-bottom:12px;padding-bottom:10px;border-bottom:2px solid #f0f0f0}
- .form-row{display:flex;gap:12px;align-items:flex-end;flex-wrap:wrap;margin-bottom:12px}
- .form-group{flex:1;min-width:200px}
- .form-group label{display:block;font-size:13px;font-weight:600;margin-bottom:4px;color:#555}
- select,textarea{width:100%;padding:8px 12px;border:1.5px solid #e0e0e0;border-radius:8px;font-size:14px;font-family:inherit;background:#fff}
- select:focus,textarea:focus{outline:none;border-color:#667eea}
- select[multiple]{min-height:120px}
- .mode-tabs{display:flex;gap:8px;margin-bottom:12px}
- .mode-tab{flex:1;padding:10px;border:2px solid #e0e0e0;border-radius:8px;text-align:center;cursor:pointer;font-size:14px;font-weight:600;transition:all .2s;background:#fff}
- .mode-tab:hover{border-color:#667eea}
- .mode-tab.active{border-color:#667eea;background:linear-gradient(135deg,rgba(102,126,234,.1),rgba(118,75,162,.1));color:#667eea}
- .mode-tab .desc{font-size:11px;font-weight:400;color:#888;margin-top:2px}
- .btn{display:inline-flex;align-items:center;gap:8px;padding:10px 24px;border:none;border-radius:8px;font-size:14px;font-weight:600;cursor:pointer;background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;transition:all .2s}
- .btn:hover{transform:translateY(-1px);box-shadow:0 4px 12px rgba(0,0,0,.15)}
- .btn:disabled{opacity:.6;cursor:not-allowed;transform:none}
- .btn-sm{padding:6px 14px;font-size:12px}
- .btn-outline{background:#fff;color:#667eea;border:1.5px solid #667eea}
- .btn-outline:hover{background:rgba(102,126,234,.08)}
- .btn-stop{background:linear-gradient(135deg,#ef4444,#dc2626);color:#fff}
- .btn-stop:hover{box-shadow:0 4px 12px rgba(239,68,68,.3)}
- .loading{display:inline-block;width:14px;height:14px;border:2px solid rgba(255,255,255,.3);border-top-color:#fff;border-radius:50%;animation:spin .8s linear infinite}
- @keyframes spin{to{transform:rotate(360deg)}}
- .progress-bar{width:100%;height:8px;background:#e0e0e0;border-radius:4px;overflow:hidden;margin-top:8px}
- .progress-fill{height:100%;background:linear-gradient(90deg,#667eea,#764ba2);border-radius:4px;transition:width .3s;width:0}
- .progress-text{font-size:12px;color:#888;margin-top:4px}
- .stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px;margin-bottom:16px}
- .stat-card{background:linear-gradient(135deg,#f8f9ff,#f0f2ff);border-radius:10px;padding:16px;text-align:center;border:1px solid #e8ecff}
- .stat-card .value{font-size:28px;font-weight:700;color:#667eea}
- .stat-card .label{font-size:12px;color:#888;margin-top:4px}
- .stat-card.green .value{color:#22c55e}
- .stat-card.red .value{color:#ef4444}
- .stat-card.orange .value{color:#f59e0b}
- table{width:100%;border-collapse:collapse;font-size:13px}
- th,td{padding:10px 12px;text-align:left;border-bottom:1px solid #f0f0f0}
- th{background:#f8f9fa;font-weight:600;color:#555;position:sticky;top:0}
- tr:hover{background:#fafbff}
- .badge{display:inline-block;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600}
- .badge-green{background:#dcfce7;color:#16a34a}
- .badge-red{background:#fee2e2;color:#dc2626}
- .badge-blue{background:#dbeafe;color:#2563eb}
- .badge-orange{background:#fef3c7;color:#d97706}
- .diff-section{margin-top:12px}
- .diff-item{padding:10px 12px;border-radius:8px;margin-bottom:8px;font-size:13px}
- .diff-item.a-only{background:#fef2f2;border-left:3px solid #ef4444}
- .diff-item.b-only{background:#fff7ed;border-left:3px solid #f59e0b}
- .diff-item .code{font-family:monospace;font-weight:600}
- .collapse-header{cursor:pointer;display:flex;align-items:center;gap:8px;padding:8px 0;font-weight:600;font-size:14px}
- .collapse-header .arrow{transition:transform .2s;font-size:12px}
- .collapse-header.open .arrow{transform:rotate(90deg)}
- .collapse-body{display:none;padding:8px 0}
- .collapse-body.open{display:block}
- .item-row{padding:8px 12px;border-radius:6px;margin-bottom:6px;font-size:13px;background:#f8f9fa}
- .item-row .meta{font-size:11px;color:#888;margin-top:2px}
- .bar-chart{display:flex;align-items:flex-end;gap:8px;height:120px;padding:8px 0}
- .bar-col{flex:1;display:flex;flex-direction:column;align-items:center;gap:4px}
- .bar{width:100%;border-radius:4px 4px 0 0;transition:height .5s;min-height:2px}
- .bar.a{background:linear-gradient(180deg,#667eea,#764ba2)}
- .bar.b{background:linear-gradient(180deg,#22c55e,#16a34a)}
- .bar-label{font-size:10px;color:#888;writing-mode:horizontal-tb;max-width:80px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
- .bar-value{font-size:10px;font-weight:600;color:#555}
- .hidden{display:none}
- .empty-state{text-align:center;padding:40px;color:#aaa}
- .empty-state svg{width:48px;height:48px;margin-bottom:12px;opacity:.3}
- .log-area{background:#1e1e2e;color:#cdd6f4;border-radius:8px;padding:12px;font-family:'Cascadia Code','Fira Code',monospace;font-size:12px;max-height:200px;overflow-y:auto;line-height:1.8}
- .log-area .log-entry{border-bottom:1px solid #313244;padding:2px 0}
- .log-area .log-time{color:#6c7086;margin-right:8px}
- .log-area .log-ok{color:#a6e3a1}
- .log-area .log-warn{color:#f9e2af}
- .log-area .log-err{color:#f38ba8}
- .flex-between{display:flex;justify-content:space-between;align-items:center}
- .batch-file-progress{margin-bottom:10px;padding:8px 10px;background:#f8f9fa;border-radius:8px}
- .batch-file-progress .fname{font-size:12px;font-weight:600;color:#555;margin-bottom:4px}
- .batch-file-result-card{background:#fff;border:1px solid #e0e0e0;border-radius:10px;padding:14px;margin-bottom:10px}
- .batch-file-result-card .fhead{font-size:14px;font-weight:600;margin-bottom:8px;padding-bottom:6px;border-bottom:1px solid #f0f0f0}
- .batch-file-result-card table{font-size:12px}
- .batch-file-result-card th,.batch-file-result-card td{padding:6px 8px}
- .export-btn-wrap{text-align:right;margin-top:12px}
- </style>
- </head>
- <body>
- <div class="container">
- <!-- Header -->
- <header>
- <h1>完整性审查方案对比测试</h1>
- <p>对比「方案A:先分类再比对」与「方案B:直接LLM解释」的审查效果</p>
- </header>
- <!-- 配置区 -->
- <div class="panel">
- <h2>测试配置</h2>
- <div class="form-row">
- <div class="form-group">
- <label>测试文件</label>
- <select id="fileSelect"><option value="">加载中...</option></select>
- </div>
- </div>
- <div class="form-row">
- <div class="form-group">
- <label>测试章节(Ctrl+多选,不选=全部)</label>
- <select id="chapterSelect" multiple></select>
- </div>
- </div>
- <div class="form-group">
- <label>运行模式</label>
- <div class="mode-tabs">
- <div class="mode-tab active" data-mode="compare" onclick="setMode(this)">
- 双方案对比
- <div class="desc">同时运行A和B,对比差异</div>
- </div>
- <div class="mode-tab" data-mode="method_a" onclick="setMode(this)">
- 仅方案A
- <div class="desc">先分类再比对(当前系统)</div>
- </div>
- <div class="mode-tab" data-mode="method_b" onclick="setMode(this)">
- 仅方案B
- <div class="desc">直接LLM解释</div>
- </div>
- <div class="mode-tab" data-mode="batch" onclick="setMode(this)">
- 批量测试
- <div class="desc">自动选5份文件,对比并导出</div>
- </div>
- </div>
- </div>
- <div class="form-row">
- <button class="btn" id="runBtn" onclick="runTest()">
- <span id="runBtnText">开始测试</span>
- </button>
- <button class="btn btn-stop hidden" id="stopBtn" onclick="stopTest()">
- <span>停止</span>
- </button>
- <button class="btn btn-outline btn-sm" onclick="clearResults()">清空结果</button>
- </div>
- <div id="progressArea" class="hidden">
- <div class="progress-bar"><div class="progress-fill" id="progressFill"></div></div>
- <div class="progress-text" id="progressText">准备中...</div>
- </div>
- </div>
- <!-- 批量配置提示 -->
- <div class="panel hidden" id="batchConfigPanel">
- <h2>批量测试配置</h2>
- <div class="form-row">
- <div class="form-group" style="max-width:200px">
- <label>并发数</label>
- <select id="batchConcurrency">
- <option value="1">1 — 串行(最慢)</option>
- <option value="2" selected>2 — 2个文件同时</option>
- <option value="3">3 — 3个文件同时(推荐)</option>
- <option value="4">4 — 4个文件同时</option>
- <option value="5">5 — 5个文件同时(最快)</option>
- </select>
- </div>
- </div>
- <p style="font-size:13px;color:#555">系统自动选择5个不同文件,对每个文件的所有章节执行双方案对比。并发数越高越快,但对LLM服务的压力越大。</p>
- </div>
- <!-- 批量进度 -->
- <div class="panel hidden" id="batchProgressPanel">
- <h2>批量测试进度</h2>
- <div id="batchFileProgressList"></div>
- <div style="margin-top:10px">
- <div class="progress-bar"><div class="progress-fill" id="batchOverallProgress"></div></div>
- <div class="progress-text" id="batchOverallProgressText">准备中...</div>
- </div>
- </div>
- <!-- 日志区 -->
- <div class="panel hidden" id="logPanel">
- <div class="flex-between">
- <h2 style="border:none;margin:0;padding:0">运行日志</h2>
- <button class="btn btn-outline btn-sm" onclick="toggleLog()">收起</button>
- </div>
- <div class="log-area" id="logArea"></div>
- </div>
- <!-- 汇总统计 -->
- <div class="panel hidden" id="summaryPanel">
- <h2>汇总统计</h2>
- <div class="stats-grid" id="statsGrid"></div>
- <div id="barChartArea" class="hidden">
- <h3 style="font-size:14px;margin-bottom:8px">各章节完整率对比</h3>
- <div class="bar-chart" id="barChart"></div>
- </div>
- <div class="export-btn-wrap hidden" id="exportBtnWrap">
- <button class="btn btn-outline btn-sm" onclick="exportResults()">📦 导出ZIP报告</button>
- </div>
- </div>
- <!-- 章节对比表格 -->
- <div class="panel hidden" id="tablePanel">
- <div class="flex-between">
- <h2 style="border:none;margin:0;padding:0">章节对比明细</h2>
- </div>
- <div style="overflow-x:auto">
- <table id="resultTable">
- <thead><tr id="tableHead"></tr></thead>
- <tbody id="tableBody"></tbody>
- </table>
- </div>
- </div>
- <!-- 差异分析 -->
- <div class="panel hidden" id="diffPanel">
- <h2>差异分析</h2>
- <div id="diffContent"></div>
- </div>
- <!-- 详情展开 -->
- <div class="panel hidden" id="detailPanel">
- <h2>章节详情</h2>
- <div id="detailContent"></div>
- </div>
- <!-- 批量测试汇总 -->
- <div class="panel hidden" id="batchSummaryPanel">
- <h2>批量测试汇总</h2>
- <div class="stats-grid" id="batchStatsGrid"></div>
- <div class="export-btn-wrap" id="batchExportBtnWrap" style="display:none">
- <button class="btn btn-outline btn-sm" onclick="exportBatchResults()">📦 导出批量报告ZIP</button>
- </div>
- </div>
- <!-- 批量各文件结果 -->
- <div class="panel hidden" id="batchFileResultsPanel">
- <h2>各文件测试结果</h2>
- <div id="batchFileResults"></div>
- </div>
- </div><!-- /container -->
- <script>
- const API_BASE = window.location.origin;
- let currentMode = 'compare';
- let allResults = [];
- let abortController = null;
- let lastSummary = null;
- let batchAllFiles = [];
- let batchAbortController = null;
- // ── 初始化 ──
- (async function(){
- try {
- const res = await fetch(`${API_BASE}/api/compare/files`);
- const data = await res.json();
- const sel = document.getElementById('fileSelect');
- sel.innerHTML = '<option value="">-- 请选择测试文件 --</option>';
- data.files.forEach(f => {
- sel.innerHTML += `<option value="${f.file_id}">${f.file_name} (${f.chunks_count} chunks)</option>`;
- });
- sel.addEventListener('change', loadChapters);
- } catch(e) {
- document.getElementById('fileSelect').innerHTML = `<option value="">加载失败: ${e.message}</option>`;
- }
- })();
- async function loadChapters() {
- const fileId = document.getElementById('fileSelect').value;
- const sel = document.getElementById('chapterSelect');
- if (!fileId) { sel.innerHTML = ''; return; }
- try {
- const res = await fetch(`${API_BASE}/api/compare/chapters`, {
- method: 'POST',
- headers: {'Content-Type':'application/json'},
- body: JSON.stringify({file_id: fileId})
- });
- const data = await res.json();
- sel.innerHTML = '';
- data.chapters.forEach(c => {
- sel.innerHTML += `<option value="${c.code}">${c.code} - ${c.name} (${c.chunks_count} chunks)</option>`;
- });
- } catch(e) {
- sel.innerHTML = `<option>加载失败</option>`;
- }
- }
- function setMode(el) {
- document.querySelectorAll('.mode-tab').forEach(t => t.classList.remove('active'));
- el.classList.add('active');
- currentMode = el.dataset.mode;
- // 批量模式隐藏文件选择器,显示说明
- const isBatch = currentMode === 'batch';
- document.getElementById('fileSelect').parentElement.parentElement.style.display = isBatch ? 'none' : '';
- document.getElementById('chapterSelect').parentElement.style.display = isBatch ? 'none' : '';
- document.getElementById('batchConfigPanel').classList.toggle('hidden', !isBatch);
- if (isBatch) {
- clearResults();
- }
- }
- // ── 运行测试 ──
- async function runTest() {
- if (currentMode === 'batch') { runBatchTest(); return; }
- const fileId = document.getElementById('fileSelect').value;
- if (!fileId) { alert('请选择测试文件'); return; }
- const chapterSel = document.getElementById('chapterSelect');
- const selectedChapters = Array.from(chapterSel.selectedOptions).map(o => o.value);
- const btn = document.getElementById('runBtn');
- const btnText = document.getElementById('runBtnText');
- const stopBtn = document.getElementById('stopBtn');
- btn.disabled = true;
- btnText.innerHTML = '<span class="loading"></span> 运行中...';
- stopBtn.classList.remove('hidden');
- abortController = new AbortController();
- document.getElementById('progressArea').classList.remove('hidden');
- document.getElementById('logPanel').classList.remove('hidden');
- document.getElementById('logArea').innerHTML = '';
- document.getElementById('progressFill').style.width = '0%';
- allResults = [];
- try {
- const res = await fetch(`${API_BASE}/api/compare/run`, {
- method: 'POST',
- headers: {'Content-Type':'application/json'},
- body: JSON.stringify({
- file_id: fileId,
- chapters: selectedChapters,
- mode: currentMode
- }),
- signal: abortController.signal
- });
- const reader = res.body.getReader();
- const decoder = new TextDecoder();
- let buffer = '';
- while (true) {
- const {done, value} = await reader.read();
- if (done) break;
- buffer += decoder.decode(value, {stream: true});
- const events = buffer.split('\n\n');
- buffer = events.pop();
- for (const evt of events) {
- if (!evt.trim()) continue;
- const lines = evt.split('\n');
- let eventType = '', eventData = '';
- for (const line of lines) {
- if (line.startsWith('event: ')) eventType = line.slice(7);
- if (line.startsWith('data: ')) eventData = line.slice(6);
- }
- if (eventType && eventData) {
- handleSSE(eventType, JSON.parse(eventData));
- }
- }
- }
- } catch(e) {
- if (e.name === 'AbortError') {
- addLog('测试已停止', 'warn');
- document.getElementById('progressText').textContent = '已停止';
- } else {
- addLog('错误: ' + e.message, 'err');
- }
- } finally {
- btn.disabled = false;
- btnText.textContent = '开始测试';
- stopBtn.classList.add('hidden');
- abortController = null;
- }
- }
- function stopTest() {
- if (abortController) {
- abortController.abort();
- }
- if (batchAbortController) {
- batchAbortController.abort();
- }
- }
- // ── 导出单文件结果 ──
- async function exportResults() {
- if (!lastSummary) { alert('请先完成测试'); return; }
- const fileId = document.getElementById('fileSelect').value;
- const fileName = document.getElementById('fileSelect').selectedOptions[0]?.text.split(' (')[0] || fileId || 'report';
- try {
- const res = await fetch(`${API_BASE}/api/compare/export`, {
- method: 'POST',
- headers: {'Content-Type':'application/json'},
- body: JSON.stringify({file_name: fileName, mode: currentMode, chapters: allResults, summary: lastSummary}),
- });
- const blob = await res.blob();
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url; a.download = `${fileName}_对比报告.zip`; a.click();
- URL.revokeObjectURL(url);
- addLog('报告已导出', 'ok');
- } catch(e) {
- addLog('导出失败: ' + e.message, 'err');
- }
- }
- // ── 批量测试 ──
- async function runBatchTest() {
- const btn = document.getElementById('runBtn');
- const btnText = document.getElementById('runBtnText');
- const stopBtn = document.getElementById('stopBtn');
- btn.disabled = true;
- btnText.innerHTML = '<span class="loading"></span> 批量测试中...';
- stopBtn.classList.remove('hidden');
- batchAbortController = new AbortController();
- batchAllFiles = [];
- lastSummary = null;
- // 隐藏单文件面板
- ['summaryPanel','tablePanel','diffPanel','detailPanel'].forEach(id => document.getElementById(id).classList.add('hidden'));
- document.getElementById('exportBtnWrap').classList.add('hidden');
- // 显示批量面板
- document.getElementById('batchProgressPanel').classList.remove('hidden');
- document.getElementById('batchFileProgressList').innerHTML = '';
- document.getElementById('batchOverallProgress').style.width = '0%';
- document.getElementById('batchOverallProgressText').textContent = '准备中...';
- document.getElementById('logPanel').classList.remove('hidden');
- document.getElementById('logArea').innerHTML = '';
- try {
- const concurrency = parseInt(document.getElementById('batchConcurrency').value) || 2;
- const res = await fetch(`${API_BASE}/api/compare/batch/run`, {
- method: 'POST',
- headers: {'Content-Type':'application/json'},
- body: JSON.stringify({concurrency: concurrency}),
- signal: batchAbortController.signal
- });
- const reader = res.body.getReader();
- const decoder = new TextDecoder();
- let buffer = '';
- while (true) {
- const {done, value} = await reader.read();
- if (done) break;
- buffer += decoder.decode(value, {stream: true});
- const events = buffer.split('\n\n');
- buffer = events.pop();
- for (const evt of events) {
- if (!evt.trim()) continue;
- const lines = evt.split('\n');
- let eventType = '', eventData = '';
- for (const line of lines) {
- if (line.startsWith('event: ')) eventType = line.slice(7);
- if (line.startsWith('data: ')) eventData = line.slice(6);
- }
- if (eventType && eventData) {
- handleBatchSSE(eventType, JSON.parse(eventData));
- }
- }
- }
- } catch(e) {
- if (e.name === 'AbortError') {
- addLog('批量测试已停止', 'warn');
- } else {
- addLog('批量测试错误: ' + e.message, 'err');
- }
- } finally {
- btn.disabled = false;
- btnText.textContent = '开始测试';
- stopBtn.classList.add('hidden');
- batchAbortController = null;
- }
- }
- function handleBatchSSE(type, data) {
- switch(type) {
- case 'batch_started':
- addLog(`批量测试开始: ${data.total_files} 个文件, 并发${data.concurrency || '?'}`, 'ok');
- const list = document.getElementById('batchFileProgressList');
- list.innerHTML = '';
- data.files.forEach((f, i) => {
- list.innerHTML += `<div class="batch-file-progress" id="bprog-${f.file_id}">
- <div class="fname">文件${i+1}: ${f.file_name.substring(0,40)}</div>
- <div class="progress-bar"><div class="progress-fill" id="bfill-${f.file_id}" style="width:0"></div></div>
- </div>`;
- });
- break;
- case 'batch_file_started':
- const bprog = document.getElementById(`bprog-${data.file_id}`);
- if (bprog) bprog.style.background = '#e8f0ff';
- addLog(`开始: ${data.file_name.substring(0,40)}`, 'ok');
- break;
- case 'batch_chapter_progress':
- const bf = document.getElementById(`bfill-${data.file_id}`);
- if (bf) bf.style.width = Math.round(data.current / data.total * 100) + '%';
- document.getElementById('batchOverallProgressText').textContent =
- `${data.file_id.slice(0,8)}... ${data.chapter_code}`;
- break;
- case 'batch_file_result':
- const bfillEl = document.getElementById(`bfill-${data.file_id}`);
- if (bfillEl) bfillEl.style.width = '100%';
- batchAllFiles.push(data);
- document.getElementById('batchOverallProgress').style.width =
- Math.round(batchAllFiles.length / 5 * 100) + '%';
- addLog(`完成: ${data.file_name.substring(0,40)} (${data.summary.chapter_count}章节, 一致率${data.summary.agreement_rate}%)`, 'ok');
- renderBatchFileResults();
- break;
- case 'batch_summary':
- addLog(`全部完成: ${data.total_files}文件, ${data.total_chapters}章节, 耗时${data.total_time}s`, 'ok');
- document.getElementById('batchOverallProgress').style.width = '100%';
- document.getElementById('batchOverallProgressText').textContent = '完成';
- lastSummary = data;
- renderBatchSummary(data);
- break;
- case 'batch_file_error':
- addLog(`文件错误: ${data.file_id} - ${data.error}`, 'err');
- break;
- case 'error':
- addLog('错误: ' + data.message, 'err');
- break;
- }
- }
- function renderBatchSummary(data) {
- document.getElementById('batchSummaryPanel').classList.remove('hidden');
- const grid = document.getElementById('batchStatsGrid');
- const totalFs = data.total_files;
- const totalChs = data.total_chapters;
- const totalTime = data.total_time;
- let aM = 0, bM = 0, sumRate = 0;
- data.files.forEach(f => { aM += f.summary.total_a_missing; bM += f.summary.total_b_missing; sumRate += f.summary.agreement_rate; });
- const avgRate = totalFs > 0 ? Math.round(sumRate / totalFs * 10) / 10 : 0;
- grid.innerHTML =
- `<div class="stat-card"><div class="value">${totalFs}</div><div class="label">测试文件数</div></div>` +
- `<div class="stat-card"><div class="value">${totalChs}</div><div class="label">总章节数</div></div>` +
- `<div class="stat-card"><div class="value">${totalTime}s</div><div class="label">总耗时</div></div>` +
- `<div class="stat-card red"><div class="value">${aM}</div><div class="label">方案A总缺失</div></div>` +
- `<div class="stat-card orange"><div class="value">${bM}</div><div class="label">方案B总缺失</div></div>` +
- `<div class="stat-card green"><div class="value">${avgRate}%</div><div class="label">平均一致率</div></div>`;
- document.getElementById('batchExportBtnWrap').style.display = 'block';
- }
- function renderBatchFileResults() {
- document.getElementById('batchFileResultsPanel').classList.remove('hidden');
- const container = document.getElementById('batchFileResults');
- let html = '';
- batchAllFiles.forEach((f, fi) => {
- const s = f.summary || {};
- html += `<div class="batch-file-result-card">
- <div class="fhead">文件${fi+1}: ${f.file_name} (${s.chapter_count}章节, 一致率${s.agreement_rate}%)</div>
- <div style="overflow-x:auto"><table><thead><tr><th>章节</th><th>A缺失</th><th>A完整率</th><th>A耗时</th><th>B缺失</th><th>B完整率</th><th>B耗时</th><th>一致</th><th>分歧</th></tr></thead><tbody>`;
- (f.chapters || []).forEach(c => {
- html += `<tr>
- <td><strong>${c.chapter_code}</strong></td>
- <td>${c.a_missing}</td><td>${c.a_rate.toFixed(1)}%</td><td>${c.a_time}s</td>
- <td>${c.b_missing}</td><td>${c.b_rate.toFixed(1)}%</td><td>${c.b_time}s</td>
- <td><span class="badge badge-green">${c.agreement}</span></td>
- <td><span class="badge badge-red">${c.disagreement}</span></td>
- </tr>`;
- });
- html += '</tbody></table></div>';
- // 差异
- const nm = f.code_name_map || {};
- const diffs = (f.chapters || []).filter(c => (c.a_only_missing||[]).length > 0 || (c.b_only_missing||[]).length > 0);
- if (diffs.length > 0) {
- html += '<div style="margin-top:6px;font-size:12px">';
- diffs.forEach(c => {
- if ((c.a_only_missing||[]).length) html += `<span class="diff-item a-only" style="display:inline-block;margin:2px 4px 2px 0;padding:2px 6px;font-size:11px">仅A缺失: ${c.a_only_missing.map(x=>nm[x]||x).join(', ')}</span>`;
- if ((c.b_only_missing||[]).length) html += `<span class="diff-item b-only" style="display:inline-block;margin:2px 4px 2px 0;padding:2px 6px;font-size:11px">仅B缺失: ${c.b_only_missing.map(x=>nm[x]||x).join(', ')}</span>`;
- });
- html += '</div>';
- }
- html += '</div>';
- });
- container.innerHTML = html;
- }
- async function exportBatchResults() {
- if (!lastSummary) { alert('请先完成批量测试'); return; }
- try {
- const res = await fetch(`${API_BASE}/api/compare/batch/export`, {
- method: 'POST',
- headers: {'Content-Type':'application/json'},
- body: JSON.stringify({files: batchAllFiles, summary: lastSummary}),
- });
- const blob = await res.blob();
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url; a.download = `批量对比报告_${new Date().toISOString().slice(0,10)}.zip`; a.click();
- URL.revokeObjectURL(url);
- addLog('批量报告已导出', 'ok');
- } catch(e) {
- addLog('批量导出失败: ' + e.message, 'err');
- }
- }
- function handleSSE(type, data) {
- switch(type) {
- case 'started':
- addLog(`开始测试: ${data.file_name} (${data.total_chapters} 章节, 模式: ${data.mode})`, 'ok');
- break;
- case 'progress':
- const pct = Math.round(data.current / data.total * 100);
- document.getElementById('progressFill').style.width = pct + '%';
- const methodTag = data.method ? ` [方案${data.method}]` : '';
- document.getElementById('progressText').textContent =
- `${data.current}/${data.total} ${data.chapter}${methodTag} - ${data.status}`;
- if (data.status === 'skipped') {
- addLog(`跳过 ${data.chapter}: ${data.reason}`, 'warn');
- } else {
- addLog(`${data.chapter} 方案${data.method} 执行中...`);
- }
- break;
- case 'chapter_result':
- allResults.push(data);
- addLog(`${data.chapter_code} 完成`, 'ok');
- renderPartialResult(data);
- break;
- case 'summary':
- addLog(`测试完成,总耗时 ${data.total_time}s`, 'ok');
- lastSummary = data;
- document.getElementById('exportBtnWrap').classList.remove('hidden');
- renderSummary(data);
- renderTable();
- renderDiff();
- renderDetails();
- document.getElementById('progressFill').style.width = '100%';
- break;
- case 'error':
- addLog('错误: ' + data.message, 'err');
- break;
- }
- }
- // ── 日志 ──
- function addLog(msg, level='info') {
- const area = document.getElementById('logArea');
- const t = new Date().toLocaleTimeString();
- const cls = level === 'ok' ? 'log-ok' : level === 'err' ? 'log-err' : level === 'warn' ? 'log-warn' : '';
- area.innerHTML += `<div class="log-entry"><span class="log-time">${t}</span><span class="${cls}">${msg}</span></div>`;
- area.scrollTop = area.scrollHeight;
- }
- function toggleLog() {
- const area = document.getElementById('logArea');
- area.style.display = area.style.display === 'none' ? 'block' : 'none';
- }
- // ── 渲染:汇总统计 ──
- function renderSummary(data) {
- const grid = document.getElementById('statsGrid');
- document.getElementById('summaryPanel').classList.remove('hidden');
- let html = '';
- html += `<div class="stat-card"><div class="value">${data.total_chapters}</div><div class="label">测试章节数</div></div>`;
- html += `<div class="stat-card"><div class="value">${data.total_time}s</div><div class="label">总耗时</div></div>`;
- if (data.method_a) {
- html += `<div class="stat-card"><div class="value">${data.method_a.total_time}s</div><div class="label">方案A总耗时</div></div>`;
- html += `<div class="stat-card red"><div class="value">${data.method_a.total_missing}</div><div class="label">方案A总缺失</div></div>`;
- }
- if (data.method_b) {
- html += `<div class="stat-card"><div class="value">${data.method_b.total_time}s</div><div class="label">方案B总耗时</div></div>`;
- html += `<div class="stat-card orange"><div class="value">${data.method_b.total_missing}</div><div class="label">方案B总缺失</div></div>`;
- }
- if (data.comparison) {
- html += `<div class="stat-card green"><div class="value">${data.comparison.agreement_rate}%</div><div class="label">一致率</div></div>`;
- html += `<div class="stat-card"><div class="value">${data.comparison.total_agreement}</div><div class="label">一致项</div></div>`;
- html += `<div class="stat-card red"><div class="value">${data.comparison.total_disagreement}</div><div class="label">分歧项</div></div>`;
- }
- grid.innerHTML = html;
- renderBarChart();
- }
- function renderBarChart() {
- const chartArea = document.getElementById('barChartArea');
- const chart = document.getElementById('barChart');
- if (currentMode !== 'compare') { chartArea.classList.add('hidden'); return; }
- chartArea.classList.remove('hidden');
- chart.innerHTML = '';
- allResults.forEach(r => {
- if (!r.comparison) return;
- const col = document.createElement('div');
- col.className = 'bar-col';
- const aRate = r.comparison.a_rate || 0;
- const bRate = r.comparison.b_rate || 0;
- col.innerHTML = `
- <div class="bar-value">${aRate.toFixed(0)}|${bRate.toFixed(0)}</div>
- <div style="display:flex;gap:2px;align-items:flex-end;height:80px;width:100%">
- <div class="bar a" style="height:${aRate * 0.8}px;flex:1"></div>
- <div class="bar b" style="height:${bRate * 0.8}px;flex:1"></div>
- </div>
- <div class="bar-label">${r.chapter_code}</div>
- `;
- chart.appendChild(col);
- });
- }
- // ── 渲染:中间结果 ──
- function renderPartialResult(data) {
- // 每收到一个章节结果就更新表格
- renderTable();
- }
- // ── 渲染:对比表格 ──
- function renderTable() {
- document.getElementById('tablePanel').classList.remove('hidden');
- const thead = document.getElementById('tableHead');
- const tbody = document.getElementById('tableBody');
- if (currentMode === 'compare') {
- thead.innerHTML = `
- <th>章节</th><th>方案A缺失</th><th>方案A完整率</th><th>方案A耗时</th>
- <th>方案B缺失</th><th>方案B完整率</th><th>方案B耗时</th>
- <th>一致</th><th>分歧</th>`;
- } else if (currentMode === 'method_a') {
- thead.innerHTML = `<th>章节</th><th>总要求</th><th>已有</th><th>缺失</th><th>完整率</th><th>耗时</th><th>LLM调用</th>`;
- } else {
- thead.innerHTML = `<th>章节</th><th>总要求</th><th>覆盖</th><th>缺失</th><th>完整率</th><th>耗时</th>`;
- }
- let rows = '';
- allResults.forEach(r => {
- if (currentMode === 'compare' && r.comparison) {
- const c = r.comparison;
- rows += `<tr>
- <td><strong>${r.chapter_code}</strong> ${r.chapter_name}</td>
- <td>${c.a_missing}</td>
- <td>${c.a_rate.toFixed(1)}%</td>
- <td>${c.a_time}s</td>
- <td>${c.b_missing}</td>
- <td>${c.b_rate.toFixed(1)}%</td>
- <td>${c.b_time ? r.method_b?.execution_time + 's' : '-'}</td>
- <td><span class="badge badge-green">${c.agreement}</span></td>
- <td><span class="badge badge-red">${c.disagreement}</span></td>
- </tr>`;
- } else if (currentMode === 'method_a' && r.method_a) {
- const t = r.method_a.result?.tertiary_completeness || {};
- rows += `<tr>
- <td><strong>${r.chapter_code}</strong> ${r.chapter_name}</td>
- <td>${t.total || 0}</td>
- <td>${t.present || 0}</td>
- <td>${t.missing || 0}</td>
- <td>${t.completeness_rate || '0%'}</td>
- <td>${r.method_a.time}s</td>
- <td>${r.method_a.llm_calls}</td>
- </tr>`;
- } else if (currentMode === 'method_b' && r.method_b) {
- rows += `<tr>
- <td><strong>${r.chapter_code}</strong> ${r.chapter_name}</td>
- <td>${r.method_b.total_required}</td>
- <td>${r.method_b.covered_count}</td>
- <td>${r.method_b.missing_count}</td>
- <td>${r.method_b.completeness_rate}</td>
- <td>${r.method_b.execution_time}s</td>
- </tr>`;
- }
- });
- tbody.innerHTML = rows;
- }
- // ── 渲染:差异分析 ──
- function renderDiff() {
- if (currentMode !== 'compare') {
- document.getElementById('diffPanel').classList.add('hidden');
- return;
- }
- document.getElementById('diffPanel').classList.remove('hidden');
- const container = document.getElementById('diffContent');
- let html = '';
- allResults.forEach(r => {
- if (!r.comparison) return;
- const c = r.comparison;
- if (c.a_only_missing.length === 0 && c.b_only_missing.length === 0) return;
- const nm = r.code_name_map || {};
- html += `<h3 style="font-size:14px;margin:12px 0 8px">${r.chapter_code} - ${r.chapter_name}</h3>`;
- if (c.a_only_missing.length > 0) {
- html += `<div class="diff-item a-only">
- <strong>仅方案A认为缺失</strong>(方案B认为已覆盖):${c.a_only_missing.length}项
- <div style="margin-top:4px">${c.a_only_missing.map(code => `<span class="code">${nm[code] || code}</span>`).join(', ')}</div>
- </div>`;
- }
- if (c.b_only_missing.length > 0) {
- html += `<div class="diff-item b-only">
- <strong>仅方案B认为缺失</strong>(方案A认为已覆盖):${c.b_only_missing.length}项
- <div style="margin-top:4px">${c.b_only_missing.map(code => `<span class="code">${nm[code] || code}</span>`).join(', ')}</div>
- </div>`;
- }
- });
- if (!html) html = '<div class="empty-state">无分歧项,两种方案判断完全一致</div>';
- container.innerHTML = html;
- }
- // ── 渲染:详情展开 ──
- function renderDetails() {
- document.getElementById('detailPanel').classList.remove('hidden');
- const container = document.getElementById('detailContent');
- let html = '';
- allResults.forEach((r, idx) => {
- html += `<div class="collapse-header" onclick="toggleCollapse(this)">
- <span class="arrow">▶</span> ${r.chapter_code} - ${r.chapter_name}
- </div>`;
- html += `<div class="collapse-body" id="detail-${idx}">`;
- // 方案A详情
- if (r.method_a) {
- const recs = r.method_a.result?.recommendations || [];
- const passRec = recs.find(rec => rec.level === '通过');
- const issueRecs = recs.filter(rec => rec.level !== '通过');
- html += `<h4 style="font-size:13px;color:#667eea;margin:8px 0">方案A(先分类再比对)</h4>`;
- if (passRec) {
- html += `<div class="item-row" style="background:#f0fdf4;border-left:3px solid #22c55e">${passRec.issue_point}</div>`;
- }
- issueRecs.forEach(rec => {
- html += `<div class="item-row">
- <div><strong>[${rec.level}]</strong> ${rec.issue_point}</div>
- <div class="meta">位置: ${rec.location || '-'}</div>
- ${rec.suggestion ? `<div class="meta">建议: ${rec.suggestion}</div>` : ''}
- ${rec.reason ? `<div class="meta">依据: ${rec.reason}</div>` : ''}
- </div>`;
- });
- }
- // 方案B详情
- if (r.method_b) {
- html += `<h4 style="font-size:13px;color:#22c55e;margin:12px 0 8px">方案B(直接LLM解释)</h4>`;
- const items = r.method_b.items || [];
- const covered = items.filter(i => i.is_covered);
- const missing = items.filter(i => !i.is_covered);
- if (missing.length > 0) {
- missing.forEach(item => {
- html += `<div class="item-row" style="border-left:3px solid #ef4444">
- <div><span class="badge badge-red">缺失</span> <strong>${item.standard_name}</strong> (${item.standard_code})</div>
- <div class="meta">原因: ${item.reason || '-'}</div>
- <div class="meta">置信度: ${((item.confidence || 0) * 100).toFixed(0)}%</div>
- </div>`;
- });
- }
- if (covered.length > 0) {
- html += `<div style="margin-top:8px;font-size:12px;color:#888">已覆盖 ${covered.length} 项:</div>`;
- covered.slice(0, 5).forEach(item => {
- html += `<div class="item-row" style="border-left:3px solid #22c55e;font-size:12px">
- <div><span class="badge badge-green">覆盖</span> ${item.standard_name}</div>
- ${item.evidence ? `<div class="meta">证据: ${item.evidence.substring(0, 120)}...</div>` : ''}
- </div>`;
- });
- if (covered.length > 5) {
- html += `<div style="font-size:11px;color:#aaa;padding:4px 12px">... 还有 ${covered.length - 5} 项</div>`;
- }
- }
- }
- html += `</div>`;
- });
- container.innerHTML = html;
- }
- function toggleCollapse(el) {
- el.classList.toggle('open');
- const body = el.nextElementSibling;
- body.classList.toggle('open');
- }
- function clearResults() {
- allResults = [];
- batchAllFiles = [];
- lastSummary = null;
- ['summaryPanel','tablePanel','diffPanel','detailPanel','logPanel','progressArea','batchProgressPanel','batchSummaryPanel','batchFileResultsPanel'].forEach(id => {
- document.getElementById(id).classList.add('hidden');
- });
- document.getElementById('exportBtnWrap').classList.add('hidden');
- document.getElementById('batchExportBtnWrap').style.display = 'none';
- document.getElementById('logArea').innerHTML = '';
- document.getElementById('progressFill').style.width = '0%';
- }
- </script>
- </body>
- </html>
|