marvelous1/prostate-mri-2afc
0
1<!doctype html>2<html lang="zh">3<head>4 <meta charset="utf-8" />5 <title>2AFC 阅片 · 前列腺 MRI 研究平台</title>6 <link rel="stylesheet" href="/static/css/style.css" />7 <style>8 .twoafc-layout {9 display: grid;10 grid-template-columns: 1fr 400px;11 gap: 16px;12 padding: 16px;13 height: calc(100vh - 56px);14 box-sizing: border-box;15 }16 .card-viewer {17 background: #0b1018;18 border-radius: 8px;19 overflow: hidden;20 display: flex;21 align-items: center;22 justify-content: center;23 position: relative;24 }25 .card-viewer img {26 max-width: 100%;27 max-height: 100%;28 object-fit: contain;29 }30 .card-viewer .loading {31 color: #8b949e;32 font-size: 14px;33 }34 .afc-form {35 background: #161b22;36 border: 1px solid #30363d;37 border-radius: 8px;38 padding: 18px;39 overflow-y: auto;40 }41 .afc-form h2 {42 margin: 0 0 4px;43 font-size: 16px;44 color: #c9d1d9;45 font-weight: 600;46 }47 .afc-form .meta {48 color: #8b949e;49 font-size: 11px;50 margin-bottom: 12px;51 line-height: 1.6;52 }53 .afc-q {54 border-top: 1px solid #30363d;55 padding: 10px 0;56 }57 .afc-q .q-title {58 font-size: 13px;59 font-weight: 600;60 color: #e6edf3;61 margin-bottom: 6px;62 }63 .afc-q .q-sub {64 font-size: 11px;65 color: #8b949e;66 margin-bottom: 8px;67 }68 .opt-row {69 display: flex; gap: 6px; flex-wrap: wrap;70 }71 .opt-row label {72 flex: 1; min-width: 50px;73 background: #0d1117;74 border: 1px solid #30363d;75 border-radius: 5px;76 padding: 7px 4px;77 text-align: center;78 color: #c9d1d9;79 cursor: pointer;80 font-size: 12px;81 user-select: none;82 }83 .opt-row input[type=radio] {84 display: none;85 }86 .opt-row input[type=radio]:checked + span {87 font-weight: 700; color: #58a6ff;88 }89 .opt-row label:has(input:checked) {90 background: #1f6feb33; border-color: #58a6ff;91 }92 .ab-row {93 display: grid; grid-template-columns: 54px 1fr; gap: 8px;94 align-items: center; margin-bottom: 4px;95 }96 .ab-row .side {97 font-size: 12px; font-weight: 700; color: #e6edf3;98 }99 .afc-form textarea {100 width: 100%; min-height: 50px;101 background: #0d1117; border: 1px solid #30363d;102 color: #c9d1d9; border-radius: 4px; padding: 6px;103 font-family: inherit; font-size: 12px; resize: vertical;104 box-sizing: border-box;105 }106 .afc-submit {107 margin-top: 10px; width: 100%;108 background: #238636; color: #fff; border: none;109 padding: 12px; border-radius: 6px; font-weight: 700;110 font-size: 14px; cursor: pointer;111 }112 .afc-submit:disabled {113 background: #30363d; cursor: not-allowed;114 }115 .afc-status { font-size: 11px; color: #8b949e; margin-top: 6px; }116 .afc-status.err { color: #f85149; }117 .hdr-progress {118 font-size: 12px; color: #c9d1d9;119 }120 </style>121</head>122<body>123 <nav class="topbar">124 <div class="brand">阅片会话 · 2AFC <span id="caseCode">—</span></div>125 <div class="user">126 <span id="progressText" class="hdr-progress">—</span>127 <a class="logout" href="/dashboard2afc">← 返回主页</a>128 </div>129 </nav>130 131 <div class="twoafc-layout">132 <div class="card-viewer" id="cardViewer">133 <div class="loading">加载中…</div>134 </div>135 136 <form id="afcForm" class="afc-form" onsubmit="return false;">137 <h2 id="formHeader">请对本例进行评分</h2>138 <div class="meta" id="formMeta">139 共 5 道必答题。A / B 中一个是 AI 生成,另一个是真实扫描(位置已随机)。140 </div>141 142 <div class="afc-q">143 <div class="q-title">Q1. 您认为哪一侧是 AI 生成?</div>144 <div class="opt-row" data-field="q1_ai_choice">145 <label><input type="radio" name="q1" value="A"/><span>A</span></label>146 <label><input type="radio" name="q1" value="B"/><span>B</span></label>147 <label><input type="radio" name="q1" value="Cannot tell"/><span>无法判断</span></label>148 </div>149 </div>150 151 <div class="afc-q">152 <div class="q-title">Q2. 图像真实感 / 整体质量(1=最差,5=最佳)</div>153 <div class="ab-row">154 <span class="side">A 版本</span>155 <div class="opt-row" data-field="q2_quality_a">156 {% for n in [1,2,3,4,5] %}157 <label><input type="radio" name="q2a" value="{{ n }}"/><span>{{ n }}</span></label>158 {% endfor %}159 </div>160 </div>161 <div class="ab-row">162 <span class="side">B 版本</span>163 <div class="opt-row" data-field="q2_quality_b">164 {% for n in [1,2,3,4,5] %}165 <label><input type="radio" name="q2b" value="{{ n }}"/><span>{{ n }}</span></label>166 {% endfor %}167 </div>168 </div>169 </div>170 171 <div class="afc-q">172 <div class="q-title">Q3. 解剖结构保真度(1–5)</div>173 <div class="ab-row">174 <span class="side">A 版本</span>175 <div class="opt-row" data-field="q3_anatomy_a">176 {% for n in [1,2,3,4,5] %}177 <label><input type="radio" name="q3a" value="{{ n }}"/><span>{{ n }}</span></label>178 {% endfor %}179 </div>180 </div>181 <div class="ab-row">182 <span class="side">B 版本</span>183 <div class="opt-row" data-field="q3_anatomy_b">184 {% for n in [1,2,3,4,5] %}185 <label><input type="radio" name="q3b" value="{{ n }}"/><span>{{ n }}</span></label>186 {% endfor %}187 </div>188 </div>189 </div>190 191 <div class="afc-q">192 <div class="q-title">Q4. 用于临床诊断报告时,您更倾向于哪一侧?</div>193 <div class="opt-row" data-field="q4_preference">194 <label><input type="radio" name="q4" value="A"/><span>A</span></label>195 <label><input type="radio" name="q4" value="B"/><span>B</span></label>196 <label><input type="radio" name="q4" value="Equivalent"/><span>两者相当</span></label>197 </div>198 </div>199 200 <div class="afc-q">201 <div class="q-title">Q5. 对您 Q1 判断的把握</div>202 <div class="q-sub">1 = 完全猜测 · 2 = 较低 · 3 = 中等 · 4 = 较高 · 5 = 非常确定</div>203 <div class="opt-row" data-field="q5_confidence">204 {% for n in [1,2,3,4,5] %}205 <label><input type="radio" name="q5" value="{{ n }}"/><span>{{ n }}</span></label>206 {% endfor %}207 </div>208 </div>209 210 <div class="afc-q">211 <div class="q-title">备注 / 评述(选填)</div>212 <textarea id="afcComments" maxlength="2000" placeholder="值得记录的观察、伪影、判据……"></textarea>213 </div>214 215 <button id="afcSubmit" class="afc-submit" type="button">提交并进入下一例 →</button>216 <div class="afc-status" id="afcStatus"></div>217 </form>218 </div>219 220<script>221const FIELD_NAMES_CN = {222 q1_ai_choice: "Q1 AI 判断",223 q2_quality_a: "Q2 A 质量",224 q2_quality_b: "Q2 B 质量",225 q3_anatomy_a: "Q3 A 保真",226 q3_anatomy_b: "Q3 B 保真",227 q4_preference: "Q4 偏好",228 q5_confidence: "Q5 把握",229};230 231let currentAssignmentId = null;232let sessionStartTs = null;233 234async function loadNext() {235 const statusEl = document.getElementById("afcStatus");236 statusEl.className = "afc-status";237 statusEl.textContent = "";238 document.getElementById("cardViewer").innerHTML = '<div class="loading">加载中…</div>';239 240 const r = await fetch("/api/2afc/next").then(r => r.json());241 242 if (!r.assignment_id) {243 document.getElementById("cardViewer").innerHTML =244 '<div class="loading" style="font-size:18px;color:#2ea043">🎉 您的阅片任务已全部完成,感谢参与!</div>';245 document.getElementById("afcForm").style.display = "none";246 document.getElementById("caseCode").textContent = "—";247 document.getElementById("progressText").textContent =248 `已完成 ${r.progress.done} / ${r.progress.total}`;249 return;250 }251 252 currentAssignmentId = r.assignment_id;253 sessionStartTs = Date.now();254 document.getElementById("caseCode").textContent = r.case_code;255 document.getElementById("formHeader").textContent =256 `病例 ${r.case_code} · 第 ${r.progress.this_order} 例`;257 document.getElementById("progressText").textContent =258 `已完成 ${r.progress.done} / ${r.progress.total}`;259 260 const viewer = document.getElementById("cardViewer");261 viewer.innerHTML = "";262 const img = new Image();263 img.src = r.png_url + "?t=" + Date.now();264 img.onerror = () => {265 viewer.innerHTML = `<div class="loading">图片加载失败:${r.png_url}</div>`;266 };267 viewer.appendChild(img);268 269 document.querySelectorAll("#afcForm input[type=radio]").forEach(el => el.checked = false);270 document.getElementById("afcComments").value = "";271}272 273function getChecked(name) {274 const el = document.querySelector(`#afcForm input[name="${name}"]:checked`);275 return el ? el.value : null;276}277 278async function submitForm() {279 const statusEl = document.getElementById("afcStatus");280 const body = {281 assignment_id: currentAssignmentId,282 q1_ai_choice: getChecked("q1"),283 q2_quality_a: getChecked("q2a"),284 q2_quality_b: getChecked("q2b"),285 q3_anatomy_a: getChecked("q3a"),286 q3_anatomy_b: getChecked("q3b"),287 q4_preference: getChecked("q4"),288 q5_confidence: getChecked("q5"),289 comments: document.getElementById("afcComments").value,290 time_spent_sec: Math.round((Date.now() - sessionStartTs) / 1000),291 };292 293 const missing = [];294 for (const [k, v] of Object.entries(body)) {295 if (k === "assignment_id" || k === "comments" || k === "time_spent_sec") continue;296 if (v === null || v === "") missing.push(FIELD_NAMES_CN[k] || k);297 }298 if (missing.length) {299 statusEl.className = "afc-status err";300 statusEl.textContent = "请先完成:" + missing.join("、");301 return;302 }303 304 document.getElementById("afcSubmit").disabled = true;305 statusEl.className = "afc-status";306 statusEl.textContent = "提交中…";307 308 const r = await fetch("/api/2afc/submit", {309 method: "POST",310 headers: {"Content-Type": "application/json"},311 body: JSON.stringify(body),312 }).then(r => r.json()).catch(e => ({ok: false, error: e.toString()}));313 314 document.getElementById("afcSubmit").disabled = false;315 if (!r.ok) {316 statusEl.className = "afc-status err";317 statusEl.textContent = "提交失败:" + (r.detail || r.error || "未知错误");318 return;319 }320 loadNext();321}322 323document.getElementById("afcSubmit").addEventListener("click", submitForm);324loadNext();325</script>326</body>327</html>328 