Files
范智鹏 3280836124 data: A3 提效对比实验数据(23 样例 + 人工基线 + 插件实测,整体提速约 266 倍)
- data/efficiency-samples/ 23 个多语言样例 + data/manual-review-tool.html 人工审核工具
- tests/measure/measure-plugin-samples.mjs 四语言插件侧测量脚本
- tests/measure/compare-a3.mjs 聚合对比 + results(插件实测/人工基线/comparison)
- performance-comparison.md 填实基线对比与结论;README 效果总结由占位改为真实数据
2026-08-27 22:54:07 +08:00

231 lines
28 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN"><head>
<meta charset="UTF-8">
<title>人工审核记录工具 · A3 实验</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: "Segoe UI", "Noto Sans CJK SC", "Microsoft YaHei", sans-serif; background: #1e1e2e; color: #cdd6f4; display: flex; flex-direction: column; height: 100vh; overflow: hidden; }
header { padding: 10px 16px; background: #181825; border-bottom: 1px solid #313244; display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
header h1 { font-size: 15px; color: #89b4fa; margin-right: 8px; }
header .field { display: flex; align-items: center; gap: 4px; font-size: 13px; }
header input, header select { background: #313244; color: #cdd6f4; border: 1px solid #45475a; border-radius: 4px; padding: 4px 8px; font-size: 13px; }
#totalTimer { font-family: Consolas, monospace; font-size: 16px; color: #a6e3a1; margin-left: auto; }
.btn { cursor: pointer; border: none; border-radius: 4px; padding: 6px 14px; font-size: 13px; background: #45475a; color: #cdd6f4; }
.btn:hover { background: #585b70; }
.btn.primary { background: #89b4fa; color: #1e1e2e; font-weight: 600; }
.btn.green { background: #a6e3a1; color: #1e1e2e; font-weight: 600; }
.btn.red { background: #f38ba8; color: #1e1e2e; }
main { display: flex; flex: 1; min-height: 0; }
#sidebar { width: 240px; background: #181825; border-right: 1px solid #313244; overflow-y: auto; padding: 8px; }
#sidebar .group-title { font-size: 12px; color: #6c7086; text-transform: uppercase; letter-spacing: 1px; margin: 10px 4px 4px; }
.file-item { padding: 7px 10px; border-radius: 6px; cursor: pointer; font-size: 13px; display: flex; align-items: center; gap: 6px; margin-bottom: 2px; }
.file-item:hover { background: #313244; }
.file-item.active { background: #45475a; }
.file-item .dot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
.dot.todo { background: #6c7086; }
.dot.doing { background: #f9e2af; box-shadow: 0 0 6px #f9e2af; }
.dot.done { background: #a6e3a1; }
.file-item .cnt { margin-left: auto; font-size: 11px; color: #a6adc8; background: #313244; border-radius: 8px; padding: 1px 6px; }
#content { flex: 1; display: flex; flex-direction: column; min-width: 0; }
#codePanel { flex: 1; overflow: auto; background: #11111b; margin: 8px; border-radius: 8px; border: 1px solid #313244; padding: 10px 0; font-family: Consolas, "Courier New", monospace; font-size: 13px; line-height: 1.55; }
.code-line { display: flex; white-space: pre; }
.code-line:hover { background: #181825; }
.code-line .ln { flex: none; width: 48px; text-align: right; padding-right: 12px; color: #585b70; user-select: none; cursor: pointer; }
.code-line .ln:hover { color: #89b4fa; text-decoration: underline; }
.code-line .ln.marked { color: #f38ba8; font-weight: 700; }
.code-line .code { color: #cdd6f4; }
#issueBar { background: #181825; border-top: 1px solid #313244; padding: 10px 16px; }
#issueBar .row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
#issueBar label { font-size: 12px; color: #a6adc8; }
#issueBar input, #issueBar select { background: #313244; color: #cdd6f4; border: 1px solid #45475a; border-radius: 4px; padding: 5px 8px; font-size: 13px; }
#noteInput { flex: 1; min-width: 240px; }
#issueList { margin-top: 8px; display: flex; flex-wrap: wrap; gap: 6px; }
.issue-chip { font-size: 12px; background: #313244; border: 1px solid #45475a; border-radius: 12px; padding: 3px 10px; display: inline-flex; gap: 6px; align-items: center; }
.issue-chip .sev { font-weight: 700; }
.sev.error { color: #f38ba8; } .sev.warning { color: #f9e2af; } .sev.info { color: #89b4fa; }
.issue-chip .del { cursor: pointer; color: #6c7086; } .issue-chip .del:hover { color: #f38ba8; }
#fileTimer { font-family: Consolas, monospace; color: #f9e2af; font-size: 14px; }
.status { font-size: 12px; color: #a6adc8; }
#toast { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background: #a6e3a1; color: #1e1e2e; padding: 8px 20px; border-radius: 6px; font-size: 13px; display: none; z-index: 99; }
.hint { font-size: 11px; color: #6c7086; margin-top: 6px; }
</style>
</head>
<body>
<header>
<h1>人工审核记录 · A3</h1>
<div class="field">受试者 <input id="reviewer" placeholder="编号/昵称" style="width:110px"></div>
<div class="field">经验 <select id="level"><option value="junior">初级 (&lt;2年)</option><option value="mid">中级 (2-5年)</option><option value="senior">高级 (&gt;5年)</option></select></div>
<button class="btn green" id="exportBtn">导出 JSON</button>
<button class="btn red" id="resetBtn">清空全部数据</button>
<div id="totalTimer">00:00:00</div>
</header>
<main>
<div id="sidebar"></div>
<div id="content">
<div id="codePanel"></div>
<div id="issueBar">
<div class="row">
<button class="btn primary" id="startBtn">▶ 开始审查此文件</button>
<button class="btn" id="finishBtn" disabled="">⏹ 完成</button>
<span id="fileTimer">本文件 00:00</span>
<span class="status" id="fileStatus"></span>
</div>
<div class="row" style="margin-top:8px">
<label>行号 <input id="lineInput" type="number" min="1" style="width:70px"></label>
<label>严重度 <select id="sevInput"><option value="error">error</option><option value="warning" selected="">warning</option><option value="info">info</option></select></label>
<label>问题描述 <input id="noteInput" placeholder="例:使用了 == 应为 ==="></label>
<button class="btn" id="addIssueBtn">+ 记录问题</button>
</div>
<div id="issueList"></div>
<div class="hint">提示:点击代码左侧行号可快速填入行号。先点"开始审查"再读代码,读完点"完成"停止计时。中途休息请先点"完成"。</div>
</div>
</div>
</main>
<div id="toast"></div>
<script>
const DATA = {"generatedAt":"2026-08-22T04:39:38.089Z","files":[{"rel":"data/efficiency-samples/js/sample-01.js","lang":"JavaScript","languageId":"javascript","code":"// sample-1: order service module\nvar config = { retries: 3 };\n\nfunction orderProcess(items) {\n var total = 0;\n for (var i = 0; i < items.length; i++) {\n total += items[i].amount;\n }\n if (total == 0) {\n return null;\n }\n return total;\n}\n\nfunction orderValidate(record) {\n var shadowed = record.id;\n function inner() {\n var shadowed = record.alt;\n return shadowed;\n }\n if (record.value === record.value) {\n inner();\n }\n return shadowed;\n}\n\nfunction orderUnsafe(input) {\n var unused = computeLegacy(input);\n eval(input.expression);\n debugger;\n}\n\nfunction computeLegacy(x) {\n if (x.pending) {}\n x.count = x.count;\n return x;\n}\n\nmodule.exports = { orderProcess, orderValidate, orderUnsafe, config };\n"},{"rel":"data/efficiency-samples/js/sample-02.js","lang":"JavaScript","languageId":"javascript","code":"// sample-2: inventory service module\nvar config = { retries: 3 };\n\nfunction inventoryProcess(items) {\n var total = 0;\n for (var i = 0; i < items.length; i++) {\n total += items[i].amount;\n }\n if (total == 0) {\n return null;\n }\n return total;\n}\n\nfunction inventoryValidate(record) {\n var shadowed = record.id;\n function inner() {\n var shadowed = record.alt;\n return shadowed;\n }\n if (record.value === record.value) {\n inner();\n }\n return shadowed;\n}\n\nfunction inventoryUnsafe(input) {\n var unused = computeLegacy(input);\n eval(input.expression);\n debugger;\n}\n\nfunction computeLegacy(x) {\n if (x.pending) {}\n x.count = x.count;\n return x;\n}\n\nmodule.exports = { inventoryProcess, inventoryValidate, inventoryUnsafe, config };\n"},{"rel":"data/efficiency-samples/js/sample-03.js","lang":"JavaScript","languageId":"javascript","code":"// sample-3: billing service module\nvar config = { retries: 3 };\n\nfunction billingProcess(items) {\n var total = 0;\n for (var i = 0; i < items.length; i++) {\n total += items[i].amount;\n }\n if (total == 0) {\n return null;\n }\n return total;\n}\n\nfunction billingValidate(record) {\n var shadowed = record.id;\n function inner() {\n var shadowed = record.alt;\n return shadowed;\n }\n if (record.value === record.value) {\n inner();\n }\n return shadowed;\n}\n\nfunction billingUnsafe(input) {\n var unused = computeLegacy(input);\n eval(input.expression);\n debugger;\n}\n\nfunction computeLegacy(x) {\n if (x.pending) {}\n x.count = x.count;\n return x;\n}\n\nmodule.exports = { billingProcess, billingValidate, billingUnsafe, config };\n"},{"rel":"data/efficiency-samples/js/sample-04.js","lang":"JavaScript","languageId":"javascript","code":"// sample-4: customer service module\nvar config = { retries: 3 };\n\nfunction customerProcess(items) {\n var total = 0;\n for (var i = 0; i < items.length; i++) {\n total += items[i].amount;\n }\n if (total == 0) {\n return null;\n }\n return total;\n}\n\nfunction customerValidate(record) {\n var shadowed = record.id;\n function inner() {\n var shadowed = record.alt;\n return shadowed;\n }\n if (record.value === record.value) {\n inner();\n }\n return shadowed;\n}\n\nfunction customerUnsafe(input) {\n var unused = computeLegacy(input);\n eval(input.expression);\n debugger;\n}\n\nfunction computeLegacy(x) {\n if (x.pending) {}\n x.count = x.count;\n return x;\n}\n\nmodule.exports = { customerProcess, customerValidate, customerUnsafe, config };\n"},{"rel":"data/efficiency-samples/js/sample-05.js","lang":"JavaScript","languageId":"javascript","code":"// sample-5: shipping service module\nvar config = { retries: 3 };\n\nfunction shippingProcess(items) {\n var total = 0;\n for (var i = 0; i < items.length; i++) {\n total += items[i].amount;\n }\n if (total == 0) {\n return null;\n }\n return total;\n}\n\nfunction shippingValidate(record) {\n var shadowed = record.id;\n function inner() {\n var shadowed = record.alt;\n return shadowed;\n }\n if (record.value === record.value) {\n inner();\n }\n return shadowed;\n}\n\nfunction shippingUnsafe(input) {\n var unused = computeLegacy(input);\n eval(input.expression);\n debugger;\n}\n\nfunction computeLegacy(x) {\n if (x.pending) {}\n x.count = x.count;\n return x;\n}\n\nmodule.exports = { shippingProcess, shippingValidate, shippingUnsafe, config };\n"},{"rel":"data/efficiency-samples/js/sample-06.js","lang":"JavaScript","languageId":"javascript","code":"// sample-6: coupon service module\nvar config = { retries: 3 };\n\nfunction couponProcess(items) {\n var total = 0;\n for (var i = 0; i < items.length; i++) {\n total += items[i].amount;\n }\n if (total == 0) {\n return null;\n }\n return total;\n}\n\nfunction couponValidate(record) {\n var shadowed = record.id;\n function inner() {\n var shadowed = record.alt;\n return shadowed;\n }\n if (record.value === record.value) {\n inner();\n }\n return shadowed;\n}\n\nfunction couponUnsafe(input) {\n var unused = computeLegacy(input);\n eval(input.expression);\n debugger;\n}\n\nfunction computeLegacy(x) {\n if (x.pending) {}\n x.count = x.count;\n return x;\n}\n\nmodule.exports = { couponProcess, couponValidate, couponUnsafe, config };\n"},{"rel":"data/efficiency-samples/js/sample-07.js","lang":"JavaScript","languageId":"javascript","code":"// sample-7: refund service module\nvar config = { retries: 3 };\n\nfunction refundProcess(items) {\n var total = 0;\n for (var i = 0; i < items.length; i++) {\n total += items[i].amount;\n }\n if (total == 0) {\n return null;\n }\n return total;\n}\n\nfunction refundValidate(record) {\n var shadowed = record.id;\n function inner() {\n var shadowed = record.alt;\n return shadowed;\n }\n if (record.value === record.value) {\n inner();\n }\n return shadowed;\n}\n\nfunction refundUnsafe(input) {\n var unused = computeLegacy(input);\n eval(input.expression);\n debugger;\n}\n\nfunction computeLegacy(x) {\n if (x.pending) {}\n x.count = x.count;\n return x;\n}\n\nmodule.exports = { refundProcess, refundValidate, refundUnsafe, config };\n"},{"rel":"data/efficiency-samples/js/sample-08.js","lang":"JavaScript","languageId":"javascript","code":"// sample-8: invoice service module\nvar config = { retries: 3 };\n\nfunction invoiceProcess(items) {\n var total = 0;\n for (var i = 0; i < items.length; i++) {\n total += items[i].amount;\n }\n if (total == 0) {\n return null;\n }\n return total;\n}\n\nfunction invoiceValidate(record) {\n var shadowed = record.id;\n function inner() {\n var shadowed = record.alt;\n return shadowed;\n }\n if (record.value === record.value) {\n inner();\n }\n return shadowed;\n}\n\nfunction invoiceUnsafe(input) {\n var unused = computeLegacy(input);\n eval(input.expression);\n debugger;\n}\n\nfunction computeLegacy(x) {\n if (x.pending) {}\n x.count = x.count;\n return x;\n}\n\nmodule.exports = { invoiceProcess, invoiceValidate, invoiceUnsafe, config };\n"},{"rel":"data/efficiency-samples/java/InventoryService.java","lang":"Java","languageId":"java","code":"public class InventoryService {\n\n private static final String DEFAULT_USER = \"guest\";\n\n public void register(String userName) {\n String password = \"admin123\";\n System.out.println(\"register user: \" + userName);\n try {\n persist(userName);\n } catch (Exception e) {\n }\n int leftover = computeChecksum(userName);\n audit(DEFAULT_USER);\n }\n\n private int computeChecksum(String value) {\n String copy = value.toString();\n return copy.length();\n }\n\n private void persist(String userName) {\n // placeholder persistence\n }\n\n private void audit(String actor) {\n System.out.println(\"audit by \" + actor);\n }\n}\n"},{"rel":"data/efficiency-samples/java/NotifyService.java","lang":"Java","languageId":"java","code":"public class NotifyService {\n\n private static final String DEFAULT_USER = \"guest\";\n\n public void register(String userName) {\n String password = \"admin123\";\n System.out.println(\"register user: \" + userName);\n try {\n persist(userName);\n } catch (Exception e) {\n }\n int leftover = computeChecksum(userName);\n audit(DEFAULT_USER);\n }\n\n private int computeChecksum(String value) {\n String copy = value.toString();\n return copy.length();\n }\n\n private void persist(String userName) {\n // placeholder persistence\n }\n\n private void audit(String actor) {\n System.out.println(\"audit by \" + actor);\n }\n}\n"},{"rel":"data/efficiency-samples/java/OrderService.java","lang":"Java","languageId":"java","code":"public class OrderService {\n\n private static final String DEFAULT_USER = \"guest\";\n\n public void register(String userName) {\n String password = \"admin123\";\n System.out.println(\"register user: \" + userName);\n try {\n persist(userName);\n } catch (Exception e) {\n }\n int leftover = computeChecksum(userName);\n audit(DEFAULT_USER);\n }\n\n private int computeChecksum(String value) {\n String copy = value.toString();\n return copy.length();\n }\n\n private void persist(String userName) {\n // placeholder persistence\n }\n\n private void audit(String actor) {\n System.out.println(\"audit by \" + actor);\n }\n}\n"},{"rel":"data/efficiency-samples/java/PaymentService.java","lang":"Java","languageId":"java","code":"public class PaymentService {\n\n private static final String DEFAULT_USER = \"guest\";\n\n public void register(String userName) {\n String password = \"admin123\";\n System.out.println(\"register user: \" + userName);\n try {\n persist(userName);\n } catch (Exception e) {\n }\n int leftover = computeChecksum(userName);\n audit(DEFAULT_USER);\n }\n\n private int computeChecksum(String value) {\n String copy = value.toString();\n return copy.length();\n }\n\n private void persist(String userName) {\n // placeholder persistence\n }\n\n private void audit(String actor) {\n System.out.println(\"audit by \" + actor);\n }\n}\n"},{"rel":"data/efficiency-samples/java/ReportService.java","lang":"Java","languageId":"java","code":"public class ReportService {\n\n private static final String DEFAULT_USER = \"guest\";\n\n public void register(String userName) {\n String password = \"admin123\";\n System.out.println(\"register user: \" + userName);\n try {\n persist(userName);\n } catch (Exception e) {\n }\n int leftover = computeChecksum(userName);\n audit(DEFAULT_USER);\n }\n\n private int computeChecksum(String value) {\n String copy = value.toString();\n return copy.length();\n }\n\n private void persist(String userName) {\n // placeholder persistence\n }\n\n private void audit(String actor) {\n System.out.println(\"audit by \" + actor);\n }\n}\n"},{"rel":"data/efficiency-samples/java/UserService.java","lang":"Java","languageId":"java","code":"public class UserService {\n\n private static final String DEFAULT_USER = \"guest\";\n\n public void register(String userName) {\n String password = \"admin123\";\n System.out.println(\"register user: \" + userName);\n try {\n persist(userName);\n } catch (Exception e) {\n }\n int leftover = computeChecksum(userName);\n audit(DEFAULT_USER);\n }\n\n private int computeChecksum(String value) {\n String copy = value.toString();\n return copy.length();\n }\n\n private void persist(String userName) {\n // placeholder persistence\n }\n\n private void audit(String actor) {\n System.out.println(\"audit by \" + actor);\n }\n}\n"},{"rel":"data/efficiency-samples/css/sample-01.css","lang":"CSS","languageId":"css","code":"/* header panel styles */\n.header-card {\n color: #FFFFFF;\n background: #FFF1A2B;\n margin: 0px;\n padding: 12px 12px;\n -webkit-border-radius: 6px;\n border: 1px solid #dddddd;\n}\n\n.header-title {\n font-size: 16px;\n font-weight: 700;\n color: #333333;\n color: #444444;\n}\n\n.header-actions {\n display: flex;\n gap: 8px;\n -webkit-box-shadow: 0 2px 4px rgba(0, 0, 0, 0.10);\n}\n"},{"rel":"data/efficiency-samples/css/sample-02.css","lang":"CSS","languageId":"css","code":"/* sidebar panel styles */\n.sidebar-card {\n color: #FFFFFF;\n background: #FFF1A2B;\n margin: 0px;\n padding: 12px 12px;\n -webkit-border-radius: 6px;\n border: 1px solid #dddddd;\n}\n\n.sidebar-title {\n font-size: 16px;\n font-weight: 700;\n color: #333333;\n color: #444444;\n}\n\n.sidebar-actions {\n display: flex;\n gap: 8px;\n -webkit-box-shadow: 0 2px 4px rgba(0, 0, 0, 0.10);\n}\n"},{"rel":"data/efficiency-samples/css/sample-03.css","lang":"CSS","languageId":"css","code":"/* footer panel styles */\n.footer-card {\n color: #FFFFFF;\n background: #FFF1A2B;\n margin: 0px;\n padding: 12px 12px;\n -webkit-border-radius: 6px;\n border: 1px solid #dddddd;\n}\n\n.footer-title {\n font-size: 16px;\n font-weight: 700;\n color: #333333;\n color: #444444;\n}\n\n.footer-actions {\n display: flex;\n gap: 8px;\n -webkit-box-shadow: 0 2px 4px rgba(0, 0, 0, 0.10);\n}\n"},{"rel":"data/efficiency-samples/css/sample-04.css","lang":"CSS","languageId":"css","code":"/* modal panel styles */\n.modal-card {\n color: #FFFFFF;\n background: #FFF1A2B;\n margin: 0px;\n padding: 12px 12px;\n -webkit-border-radius: 6px;\n border: 1px solid #dddddd;\n}\n\n.modal-title {\n font-size: 16px;\n font-weight: 700;\n color: #333333;\n color: #444444;\n}\n\n.modal-actions {\n display: flex;\n gap: 8px;\n -webkit-box-shadow: 0 2px 4px rgba(0, 0, 0, 0.10);\n}\n"},{"rel":"data/efficiency-samples/css/sample-05.css","lang":"CSS","languageId":"css","code":"/* toast panel styles */\n.toast-card {\n color: #FFFFFF;\n background: #FFF1A2B;\n margin: 0px;\n padding: 12px 12px;\n -webkit-border-radius: 6px;\n border: 1px solid #dddddd;\n}\n\n.toast-title {\n font-size: 16px;\n font-weight: 700;\n color: #333333;\n color: #444444;\n}\n\n.toast-actions {\n display: flex;\n gap: 8px;\n -webkit-box-shadow: 0 2px 4px rgba(0, 0, 0, 0.10);\n}\n"},{"rel":"data/efficiency-samples/sql/sample-01.sql","lang":"SQL","languageId":"sql","code":"-- daily-orders report query\nselect u.user_id,u.user_name,o.order_id,o.amount\nfrom t_user u\njoin t_order o on u.user_id = o.user_id\nwhere u.status = 'active'\n and o.created_at > sysdate - 30\n and o.amount > 100\nORDER BY o.amount desc\n"},{"rel":"data/efficiency-samples/sql/sample-02.sql","lang":"SQL","languageId":"sql","code":"-- monthly-revenue summary query\nSELECT d.dept_name, COUNT(*) AS total\nFROM t_order o\nJOIN t_user u ON o.user_id = u.user_id\nJOIN t_dept d ON u.dept_id = d.dept_id\nWHERE o.status = 'paid'\nGROUP BY d.dept_name\nhaving total > 5\n"},{"rel":"data/efficiency-samples/sql/sample-03.sql","lang":"SQL","languageId":"sql","code":"-- active-users report query\nselect u.user_id,u.user_name,o.order_id,o.amount\nfrom t_user u\njoin t_order o on u.user_id = o.user_id\nwhere u.status = 'active'\n and o.created_at > sysdate - 30\n and o.amount > 100\nORDER BY o.amount desc\n"},{"rel":"data/efficiency-samples/sql/sample-04.sql","lang":"SQL","languageId":"sql","code":"-- top-departments summary query\nSELECT d.dept_name, COUNT(*) AS total\nFROM t_order o\nJOIN t_user u ON o.user_id = u.user_id\nJOIN t_dept d ON u.dept_id = d.dept_id\nWHERE o.status = 'paid'\nGROUP BY d.dept_name\nhaving total > 5\n"}]};
const LS_KEY = 'a3-manual-review-v1';
const state = load() || { reviewer: '', level: 'mid', files: {} };
// state.files[rel] = { elapsedMs, startedAt, done, issues: [{line, severity, note, at}] }
const $ = (id) => document.getElementById(id);
let current = DATA.files[0]?.rel ?? null;
let tickHandle = setInterval(renderTimers, 1000);
function load() { try { return JSON.parse(localStorage.getItem(LS_KEY)); } catch { return null; } }
function save() { localStorage.setItem(LS_KEY, JSON.stringify(state)); }
function fstate(rel) { return state.files[rel] ??= { elapsedMs: 0, startedAt: null, done: false, issues: [] }; }
function fmtClock(ms) {
const s = Math.floor(ms / 1000);
const h = String(Math.floor(s / 3600)).padStart(2, '0');
return h + ':' + String(Math.floor((s % 3600) / 60)).padStart(2, '0') + ':' + String(s % 60).padStart(2, '0');
}
function fmtShort(ms) {
const s = Math.floor(ms / 1000);
return String(Math.floor(s / 60)).padStart(2, '0') + ':' + String(s % 60).padStart(2, '0');
}
function totalElapsed() {
let t = 0;
for (const f of Object.values(state.files)) { t += f.elapsedMs + (f.startedAt ? Date.now() - f.startedAt : 0); }
return t;
}
function esc(s) { return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); }
function renderSidebar() {
const el = $('sidebar'); el.innerHTML = '';
const groups = {};
for (const f of DATA.files) { (groups[f.lang] ??= []).push(f); }
for (const [lang, fs] of Object.entries(groups)) {
const t = document.createElement('div'); t.className = 'group-title'; t.textContent = lang; el.appendChild(t);
for (const f of fs) {
const st = fstate(f.rel);
const item = document.createElement('div');
item.className = 'file-item' + (f.rel === current ? ' active' : '');
const dotCls = st.done ? 'done' : (st.startedAt ? 'doing' : (st.elapsedMs > 0 || st.issues.length ? 'doing' : 'todo'));
item.innerHTML = '<span class="dot ' + dotCls + '"></span><span>' + f.rel.split('/').pop() + '</span>' +
(st.issues.length ? '<span class="cnt">' + st.issues.length + ' 题目</span>' : '');
item.onclick = () => switchFile(f.rel);
el.appendChild(item);
}
}
}
function renderCode() {
const f = DATA.files.find((x) => x.rel === current);
const panel = $('codePanel'); panel.innerHTML = '';
if (!f) { panel.textContent = '(无文件)'; return; }
const st = fstate(f.rel);
const markedLines = new Set(st.issues.map((i) => i.line));
const lines = f.code.split('\n');
lines.forEach((text, i) => {
const row = document.createElement('div'); row.className = 'code-line';
const ln = document.createElement('span');
ln.className = 'ln' + (markedLines.has(i + 1) ? ' marked' : '');
ln.textContent = String(i + 1);
ln.onclick = () => { $('lineInput').value = i + 1; $('noteInput').focus(); };
const code = document.createElement('span'); code.className = 'code'; code.textContent = text || ' ';
row.appendChild(ln); row.appendChild(code); panel.appendChild(row);
});
panel.scrollTop = 0;
}
function renderIssueBar() {
const st = fstate(current);
$('startBtn').disabled = !!st.startedAt;
$('finishBtn').disabled = !st.startedAt;
$('fileStatus').textContent = st.done ? '已完成' : (st.startedAt ? '审查中…' : '未开始');
const list = $('issueList'); list.innerHTML = '';
for (let idx = 0; idx < st.issues.length; idx++) {
const it = st.issues[idx];
const chip = document.createElement('span'); chip.className = 'issue-chip';
chip.innerHTML = '<span class="sev ' + it.severity + '">' + it.severity + '</span> L' + it.line +
' <span>' + esc(it.note || '(未描述)') + '</span> <span class="del" title="删除">✕</span>';
chip.querySelector('.del').onclick = () => { st.issues.splice(idx, 1); save(); renderAll(); };
list.appendChild(chip);
}
}
function renderTimers() {
$('totalTimer').textContent = fmtClock(totalElapsed());
const st = current && fstate(current);
if (st) { $('fileTimer').textContent = '本文件 ' + fmtShort(st.elapsedMs + (st.startedAt ? Date.now() - st.startedAt : 0)); }
}
function renderAll() { renderSidebar(); renderCode(); renderIssueBar(); renderTimers(); }
function switchFile(rel) {
const prev = fstate(current);
if (prev.startedAt) { prev.elapsedMs += Date.now() - prev.startedAt; prev.startedAt = null; } // 切换视为暂停
current = rel; save(); renderAll();
}
$('startBtn').onclick = () => { const st = fstate(current); if (!st.startedAt) { st.startedAt = Date.now(); st.done = false; save(); renderAll(); } };
$('finishBtn').onclick = () => {
const st = fstate(current);
if (st.startedAt) { st.elapsedMs += Date.now() - st.startedAt; st.startedAt = null; }
st.done = true; save(); renderAll();
};
$('addIssueBtn').onclick = () => {
const line = parseInt($('lineInput').value, 10);
const note = $('noteInput').value.trim();
if (!line || line < 1) { toast('请填写行号(可点击代码行号)'); return; }
if (!note) { toast('请填写问题描述'); return; }
const st = fstate(current);
st.issues.push({ line, severity: $('sevInput').value, note, at: new Date().toISOString() });
$('lineInput').value = ''; $('noteInput').value = '';
save(); renderAll(); toast('已记录 L' + line);
};
$('reviewer').oninput = (e) => { state.reviewer = e.target.value; save(); };
$('level').onchange = (e) => { state.level = e.target.value; save(); };
$('exportBtn').onclick = () => {
state.reviewer = $('reviewer').value; state.level = $('level').value;
for (const st of Object.values(state.files)) {
if (st.startedAt) { st.elapsedMs += Date.now() - st.startedAt; st.startedAt = null; }
}
save();
const out = { experiment: 'A3 人工审核 vs 插件审核', exportedAt: new Date().toISOString(), reviewer: state.reviewer, level: state.level, totalElapsedMs: totalElapsed(), files: state.files };
const blob = new Blob([JSON.stringify(out, null, 2)], { type: 'application/json' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'manual-review-' + (state.reviewer || 'anon') + '.json';
a.click(); URL.revokeObjectURL(a.href);
toast('已导出,请交给实验组织者');
};
$('resetBtn').onclick = () => {
if (!confirm('确定清空全部记录?此操作不可恢复。')) return;
localStorage.removeItem(LS_KEY); location.reload();
};
function toast(msg) {
const t = $('toast'); t.textContent = msg; t.style.display = 'block';
setTimeout(() => { t.style.display = 'none'; }, 1600);
}
// 恢复受试者信息
if (state.reviewer) { $('reviewer').value = state.reviewer; }
if (state.level) { $('level').value = state.level; }
renderAll();
</script>
</body></html>