CoolFace
Apppublic

marvelous1/prostate-mri-2afc

sourceHugging Facemitupdated 5mo agoView on Hugging Face
0likes
reader_2afc.html328 linesDownload Raw Back to static
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