roz322 commited on
Commit
7901436
·
verified ·
1 Parent(s): 23eae0c

v7 (1103): schema-driven blocks, v7 display rules, ANNOT_DIR

Browse files
Files changed (2) hide show
  1. app/main.py +4 -2
  2. app/static/index.html +54 -24
app/main.py CHANGED
@@ -5,7 +5,7 @@
5
  云部署: 见 ../README.md「云部署」一节(Dockerfile 在 03_annotation/)
6
 
7
  设计要点:
8
- - schema 驱动: 表单结构全部来自 ../schema/schema_0817.json, 改 schema 不改代码
9
  - 盲标: 不加载任何 LLM 预填; 标注人只能读写自己的仓库, 看不到别人的; 每条标注盖 schema_version + annotator + 时间戳章
10
  - grounding: evidence quote 保存时按 ground_check 同款算法(NFKC + 空白折叠)回填 char_span(折叠坐标)
11
  - 存储: 见 store.py。一篇 × 一人一份 JSON, 内存即时生效, 后台定时 commit 到该标注人的私有仓库;
@@ -64,6 +64,8 @@ if BACKEND == "github":
64
  # 默认一个仓库管所有: DATA_REPO 里 papers/ + annotations/<annotator>/;
65
  # 某人配了 "repo" 则写进那个仓库的 annotations/(每人一仓库的旧方案, 仍支持)
66
  TOKEN = os.environ["GITHUB_TOKEN"]
 
 
67
  DATA_REPO = os.environ.get("DATA_REPO", "")
68
  if not ANNOTATORS:
69
  raise SystemExit("github backend needs ANNOTATORS_JSON")
@@ -72,7 +74,7 @@ if BACKEND == "github":
72
  if c.get("repo"):
73
  locations[a] = (c["repo"], "annotations")
74
  elif DATA_REPO:
75
- locations[a] = (DATA_REPO, f"annotations/{a}")
76
  else:
77
  raise SystemExit(f"annotator {a}: set DATA_REPO or give them a repo")
78
  STORE = GitHubAnnotationStore(TOKEN, locations, branch=os.environ.get("DATA_BRANCH") or None)
 
5
  云部署: 见 ../README.md「云部署」一节(Dockerfile 在 03_annotation/)
6
 
7
  设计要点:
8
+ - schema 驱动: 表单结构全部来自 schema 文件(SCHEMA_PATH;0817 = ../schema/schema_0817.json,1103 起 v7 = ../schema/schema_v7.json), 改 schema 不改代码
9
  - 盲标: 不加载任何 LLM 预填; 标注人只能读写自己的仓库, 看不到别人的; 每条标注盖 schema_version + annotator + 时间戳章
10
  - grounding: evidence quote 保存时按 ground_check 同款算法(NFKC + 空白折叠)回填 char_span(折叠坐标)
11
  - 存储: 见 store.py。一篇 × 一人一份 JSON, 内存即时生效, 后台定时 commit 到该标注人的私有仓库;
 
64
  # 默认一个仓库管所有: DATA_REPO 里 papers/ + annotations/<annotator>/;
65
  # 某人配了 "repo" 则写进那个仓库的 annotations/(每人一仓库的旧方案, 仍支持)
66
  TOKEN = os.environ["GITHUB_TOKEN"]
67
+ # 1103: v7 标注写进 annotations_v7/(ANNOT_DIR),0817 的 annotations/ 原样保留;论文同理用 PAPERS_DIR=papers_v7
68
+ ANNOT_DIR = os.environ.get("ANNOT_DIR", "annotations")
69
  DATA_REPO = os.environ.get("DATA_REPO", "")
70
  if not ANNOTATORS:
71
  raise SystemExit("github backend needs ANNOTATORS_JSON")
 
74
  if c.get("repo"):
75
  locations[a] = (c["repo"], "annotations")
76
  elif DATA_REPO:
77
+ locations[a] = (DATA_REPO, f"{ANNOT_DIR}/{a}")
78
  else:
79
  raise SystemExit(f"annotator {a}: set DATA_REPO or give them a repo")
80
  STORE = GitHubAnnotationStore(TOKEN, locations, branch=os.environ.get("DATA_BRANCH") or None)
app/static/index.html CHANGED
@@ -29,7 +29,7 @@
29
  #login input[type=password]{font:inherit;border:1px solid var(--line);border-radius:6px;padding:4px 8px;width:100%}
30
  .login-err{color:var(--red);font-size:12px;min-height:16px;margin-top:8px}
31
  /* ---------- home ---------- */
32
- :root{--c-l1:#475569;--c-dataset:#2563eb;--c-split:#0891b2;--c-asset:#0d9488;--c-model:#7c3aed;--c-result:#d97706;--c-claim:#e11d48}
33
  #home{display:none;min-height:100vh}
34
  .topbar{position:sticky;top:0;z-index:20;display:flex;align-items:center;gap:12px;padding:10px 24px;background:rgba(255,255,255,.92);backdrop-filter:blur(8px);border-bottom:1px solid var(--line)}
35
  .brand{font-weight:700;font-size:15px;letter-spacing:-.01em}
@@ -374,10 +374,13 @@ $("#in-annotator").addEventListener("keydown",e=>{if(e.key==="Enter"&&!S.authMod
374
 
375
  /* ---------------- home: paper cards ---------------- */
376
  const enc=encodeURIComponent;
377
- const BLOCKS=[["l1","L1"],["dataset","D"],["split","S"],["asset","A"],["model","M"],["result","R"],["claim","C"]];
 
 
378
  /* All UI text is English (0925). Schema labels carry Chinese in parentheses; stripped for display (schema file untouched). */
379
  const EN_RT={l1:"L1 characterization",dataset:"Datasets",split:"Splits",asset:"Artifacts (code / weights / environment)",model:"Models",result:"Results",claim:"Claims"};
380
- const EN_STATUS={stated:"stated",inferred:"inferred",not_reported:"not reported",absent_deferred:"deferred elsewhere",uncertain:"uncertain"};
 
381
  const prettyEnum=t=>String(t).replace(/^C\d_/,"").replace(/_/g," ");
382
  const stripZh=t=>typeof t==="string"?t.replace(/\s*[((](?=[^()()]*[\u4e00-\u9fff])[^()()]*[))]/g,"").trim():t;
383
  function enSchema(x){if(Array.isArray(x))return x.map(enSchema);if(x&&typeof x==="object"){const o={};for(const [k,v] of Object.entries(x))o[k]=(k==="label"||k==="title"||k==="help"||k==="hint")?stripZh(v):enSchema(v);return o}return x}
@@ -390,6 +393,7 @@ function valOf(p,max=90){
390
  let v=p.value;
391
  if(v&&typeof v==="object")v=Array.isArray(v)?v.join(", "):Object.values(v).filter(Boolean).join(" · ");
392
  v=(v||"").toString().trim();
 
393
  if(!v&&p.status)v="("+(EN_STATUS[p.status]||p.status)+")";
394
  return v.length>max?v.slice(0,max)+"…":v;
395
  }
@@ -428,17 +432,17 @@ function renderGrid(){
428
  if(!rows.length){$("#hm-grid").innerHTML=`<div class="empty">No papers match</div>`;return}
429
  $("#hm-grid").innerHTML=rows.map(([pid,e])=>{
430
  const o=H.overview[pid]||{blocks:{},filled:0};
431
- const blocks=BLOCKS.map(([id,sh])=>{
432
  const b=o.blocks[id]||{records:0,filled:0},rt=rtDef(id);
433
  const on=b.filled>0;
434
- return `<span class="blk ${on?"on":""}" style="${on?`background:var(--c-${id})`:""}" title="${esc(EN_RT[id]||id)}: ${b.records} records · ${b.filled} fields filled">${sh}</span>`;
435
  }).join("");
436
  const st=statusOf(pid);
437
  return `<div class="pcard" data-pid="${esc(pid)}">
438
  <div class="meta"><span class="venue">${esc(e.venue||"")}</span><span>${esc(pid)}</span>${e.text_source==="jats"?'<span class="src-tag">PMC XML</span>':""}${S.me.admin&&S.manifest.pool?`<span style="flex:1"></span><span class="pool-pill ${S.manifest.pool.includes(pid)?"on":""}" data-pool="${esc(pid)}" title="Whether annotators (A/B/C…) see this paper">${S.manifest.pool.includes(pid)?"✓ in pool":"not in pool"}</span>`:""}</div>
439
  <h3>${esc(e.title||pid)}</h3>
440
  <div class="abs">${esc(e.abstract||"")}</div>
441
- <div class="foot"><div class="blocks">${blocks}</div>
442
  <span class="status ${st}">${st==="doing"?o.filled+" fields":"Not started"}</span></div>
443
  </div>`;
444
  }).join("");
@@ -510,10 +514,11 @@ function recordLine(rt,r,flds){
510
  const tags=Object.entries(x).filter(([k,v])=>v&&k!=="model_fk"&&typeof v!=="object").map(([k,v])=>{const t=v===true?k:String(v);return `<span class="tag">${esc(prettyEnum(t))}</span>`}).join("");
511
  let main;
512
  if(rt.id==="result"){main=[valOf(f.R1,50),valOf(f.R2,40)].filter(Boolean).join(" = ")}
513
- else if(rt.id==="claim"){main=valOf(f.F1,160)}
514
  else if(rt.id==="asset"){main=[x.asset_type&&prettyEnum(x.asset_type),valOf(f.availability_claim,90)].filter(Boolean).join(": ")}
515
  else{main=valOf(f[rt.label_field],80)}
516
- const extra=rt.id==="dataset"&&f.D3?`<span class="k">source</span>${esc(valOf(f.D3,40))}`:rt.id==="split"&&f.D26?`<span class="k">split unit</span>${esc(valOf(f.D26,40))}`:"";
 
517
  const n=fieldDefs(rt).filter(d=>isFilled(f[d.id])).length;
518
  return `<li>${main?esc(main):`<span class="none">(unnamed)</span>`}${rt.id==="asset"?"":tags} ${extra?"<br>"+extra:""}<span class="tag" style="float:right">${n}/${fieldDefs(rt).length}</span></li>`;
519
  }
@@ -534,8 +539,8 @@ function renderCards(st){
534
  body=recs.length?`<ul>${recs.map(r=>recordLine(rt,r,st.fields[r.uid])).join("")}</ul>`:`<div class="none">Not annotated yet</div>`;
535
  }
536
  const sub=rt.cardinality==="single"?`${filledN}/${defs.length} fields`:`${recs.length} record${recs.length===1?"":"s"} · ${filledN} fields filled`;
537
- return `<div class="bcard ${rt.cardinality==="single"||rt.id==="claim"?"wide":""}" style="--bc:var(--c-${rt.id})">
538
- <h4>${esc(EN_RT[rt.id]||rt.label)}</h4><div class="sub">${sub}</div>
539
  <div class="bar"><i style="width:${Math.min(pct,100)}%"></i></div>${body}</div>`;
540
  }).join("");
541
  $("#dt-sum").textContent=(st.read_only?`${st.annotator}'s annotation (read-only) · `:"")+`${totalFilled} fields filled in total`;
@@ -665,6 +670,7 @@ function mdToHtml(src){
665
  const SRC={jats:["good","Text: publisher XML (official)","Text comes from the PMC XML, not from PDF conversion"],
666
  docling:["ok","Text: layout-aware PDF conversion","Converted with Docling; tables/formulas can still be wrong, check against the PDF"],
667
  docling_0817:["ok","Text: PDF conversion (0817)","Docling conversion without PDF linking"],
 
668
  pymupdf:["weak","Text: plain PDF extraction","Flat PyMuPDF text: tables are often scrambled, check against the PDF"]};
669
  const W=(()=>{try{return Object.assign({split:64,single:46},JSON.parse(localStorage.getItem("repl_widths")||"{}"))}catch(e){return {split:64,single:46}}})();
670
  function saveW(){try{localStorage.setItem("repl_widths",JSON.stringify(W))}catch(e){}}
@@ -832,7 +838,7 @@ function addSelectionAsEvidence(){
832
  /* 在 PDF 上划选: 记下精确位置; 转换文本里对不上(损坏)时位置也不丢 */
833
  if(fromPdf){ev.origin="pdf";ev.pdf_anchor={blocks:[],boxes:selectionBoxes(sel.getRangeAt(0))}}
834
  pl.evidence.push(ev);
835
- if(!pl.status)pl.status="stated";
836
  saveField(S.active.uid,S.active.fid,pl,true);
837
  sel.removeAllRanges();seltool.style.display="none";
838
  }
@@ -906,6 +912,11 @@ function renderRecord(rec,def){
906
  const c=document.createElement("input");c.type="checkbox";c.checked=!!rec.extra[e.id];
907
  c.onchange=()=>{rec.extra[e.id]=c.checked;saveExtra(rec);renderRight()};
908
  lab.prepend(c);
 
 
 
 
 
909
  }else{
910
  const s=document.createElement("select");
911
  s.innerHTML="<option value=''>—</option>"+e.options.map(o=>`<option ${rec.extra[e.id]===o?"selected":""}>${o}</option>`).join("");
@@ -938,28 +949,39 @@ function renderRecord(rec,def){
938
  /* groups */
939
  const na=naSet();
940
  const groups=def.groups||[{title:null,fields:def.fields}];
941
- const simple=rec.extra.role&&rec.extra.role!=="proposed";
 
942
  groups.forEach(gr=>{
943
- if(gr.trigger&&!rec.extra[gr.trigger])return;
944
  const g=document.createElement("div");g.className="group";
945
  if(gr.title){
946
  const t=document.createElement("div");t.className="group-title";
947
  t.innerHTML=`<span class="tri">▾</span> ${esc(gr.title)}`;
948
  t.onclick=()=>g.classList.toggle("closed");
949
  g.appendChild(t);
950
- if(simple&&!/M1–M4|framework/i.test(gr.title))g.classList.add("closed");
951
  }
952
  gr.fields.forEach(f=>g.appendChild(renderField(rec,f,na)));
953
  card.appendChild(g);
954
  });
955
  if(simple){
956
  const n=document.createElement("div");n.className="f-hint";n.style.padding="6px 12px";
957
- n.textContent="role ≠ proposed: abbreviated record suffices; other groups collapsed, expand as needed.";
958
  card.appendChild(n);
959
  }
960
  return card;
961
  }
962
 
 
 
 
 
 
 
 
 
 
 
963
  function fkBox(target,selected,onchange){
964
  const targets=Array.isArray(target)?target:[target];
965
  const box=document.createElement("div");box.className="fk-box";
@@ -1002,7 +1024,7 @@ function renderField(rec,f,na){
1002
  ].join("");
1003
  const done=pl.status?`<span class="f-done">● ${pl.status}</span>`:"";
1004
  el.innerHTML=`<div class="f-head"><span class="f-id">${f.id}</span><span class="f-label">${esc(f.label)}</span>${badges}${done}</div>`
1005
- +(f.hint?`<div class="f-guide">${esc(f.hint)}${f.example?` <span class="eg">e.g. ${esc(f.example)}</span>`:""}</div>`:"");
1006
  el.querySelector(".f-head").onclick=()=>activate(uid,f.id);
1007
 
1008
  const body=document.createElement("div");body.className="f-body";
@@ -1012,10 +1034,12 @@ function renderField(rec,f,na){
1012
  st.onchange=()=>{pl.status=st.value;saveField(uid,f.id,pl,true)};
1013
  body.appendChild(st);
1014
  /* granularity */
1015
- const gr=document.createElement("select");
1016
- gr.innerHTML="<option value=''>granularity…</option>"+S.schema.attributes.granularity.map(s=>`<option value="${s.v}" ${pl.granularity===s.v?"selected":""}>${s.label}</option>`).join("");
1017
- gr.onchange=()=>{pl.granularity=gr.value;saveField(uid,f.id,pl)};
1018
- body.appendChild(gr);
 
 
1019
  /* value */
1020
  body.appendChild(valueEditor(f,pl,uid));
1021
  /* defer_target */
@@ -1027,7 +1051,7 @@ function renderField(rec,f,na){
1027
  body.appendChild(d);
1028
  }
1029
  /* evidence */
1030
- body.appendChild(evidenceList(uid,f.id,pl));
1031
  /* note */
1032
  const note=document.createElement("input");note.type="text";note.className="full";
1033
  note.placeholder="Note: reason for uncertain, internal contradictions, ...";note.value=pl.note||"";
@@ -1044,8 +1068,14 @@ function valueEditor(f,pl,uid){
1044
  if(f.type==="enum_field"){
1045
  const s=document.createElement("select");s.className="full";
1046
  s.innerHTML="<option value=''>value…</option>"+f.options.map(o=>`<option ${pl.value===o?"selected":""}>${o}</option>`).join("");
1047
- s.onchange=()=>{pl.value=s.value;saveField(uid,f.id,pl)};
1048
- return s;
 
 
 
 
 
 
1049
  }
1050
  if(f.type==="axes"){
1051
  const box=document.createElement("div");box.className="full";
@@ -1132,7 +1162,7 @@ function evidenceList(uid,fid,pl){
1132
  if(!q)return;
1133
  pl.evidence=pl.evidence||[];
1134
  pl.evidence.push({quote:q.replace(/\s+/g," ").trim(),section:"figure",flags:{figure_only:true},grounded:null,char_span:null});
1135
- if(!pl.status)pl.status="stated";
1136
  saveField(uid,fid,pl,true);
1137
  };
1138
  box.appendChild(add);
 
29
  #login input[type=password]{font:inherit;border:1px solid var(--line);border-radius:6px;padding:4px 8px;width:100%}
30
  .login-err{color:var(--red);font-size:12px;min-height:16px;margin-top:8px}
31
  /* ---------- home ---------- */
32
+ :root{--c-l1:#475569;--c-dataset:#2563eb;--c-split:#0891b2;--c-asset:#0d9488;--c-model:#7c3aed;--c-result:#d97706;--c-claim:#e11d48;--c-scope:#475569;--c-code:#0d9488;--c-summary:#e11d48;--c-notes:#64748b}
33
  #home{display:none;min-height:100vh}
34
  .topbar{position:sticky;top:0;z-index:20;display:flex;align-items:center;gap:12px;padding:10px 24px;background:rgba(255,255,255,.92);backdrop-filter:blur(8px);border-bottom:1px solid var(--line)}
35
  .brand{font-weight:700;font-size:15px;letter-spacing:-.01em}
 
374
 
375
  /* ---------------- home: paper cards ---------------- */
376
  const enc=encodeURIComponent;
377
+ /* 1103: block letters come from the schema (record_types[].short) so the same page serves 0817 and v7 */
378
+ const BLOCKS_0817=[["l1","L1"],["dataset","D"],["split","S"],["asset","A"],["model","M"],["result","R"],["claim","C"]];
379
+ const blocks=()=>S.schema.record_types.some(r=>r.short)?S.schema.record_types.map(r=>[r.id,r.short||r.id]):BLOCKS_0817;
380
  /* All UI text is English (0925). Schema labels carry Chinese in parentheses; stripped for display (schema file untouched). */
381
  const EN_RT={l1:"L1 characterization",dataset:"Datasets",split:"Splits",asset:"Artifacts (code / weights / environment)",model:"Models",result:"Results",claim:"Claims"};
382
+ const EN_STATUS={stated:"stated",directly_reported:"directly reported",inferred:"inferred",not_reported:"not reported",absent_deferred:"deferred elsewhere",uncertain:"uncertain"};
383
+ const rtName=rt=>rt.label_en||EN_RT[rt.id]||rt.label;
384
  const prettyEnum=t=>String(t).replace(/^C\d_/,"").replace(/_/g," ");
385
  const stripZh=t=>typeof t==="string"?t.replace(/\s*[((](?=[^()()]*[\u4e00-\u9fff])[^()()]*[))]/g,"").trim():t;
386
  function enSchema(x){if(Array.isArray(x))return x.map(enSchema);if(x&&typeof x==="object"){const o={};for(const [k,v] of Object.entries(x))o[k]=(k==="label"||k==="title"||k==="help"||k==="hint")?stripZh(v):enSchema(v);return o}return x}
 
393
  let v=p.value;
394
  if(v&&typeof v==="object")v=Array.isArray(v)?v.join(", "):Object.values(v).filter(Boolean).join(" · ");
395
  v=(v||"").toString().trim();
396
+ if(p.value_text)v=v?v+" · "+p.value_text:p.value_text; /* v7 choice + text */
397
  if(!v&&p.status)v="("+(EN_STATUS[p.status]||p.status)+")";
398
  return v.length>max?v.slice(0,max)+"…":v;
399
  }
 
432
  if(!rows.length){$("#hm-grid").innerHTML=`<div class="empty">No papers match</div>`;return}
433
  $("#hm-grid").innerHTML=rows.map(([pid,e])=>{
434
  const o=H.overview[pid]||{blocks:{},filled:0};
435
+ const blks=blocks().map(([id,sh])=>{
436
  const b=o.blocks[id]||{records:0,filled:0},rt=rtDef(id);
437
  const on=b.filled>0;
438
+ return `<span class="blk ${on?"on":""}" style="${on?`background:var(--c-${id})`:""}" title="${esc(rt?rtName(rt):id)}: ${b.records} records · ${b.filled} fields filled">${sh}</span>`;
439
  }).join("");
440
  const st=statusOf(pid);
441
  return `<div class="pcard" data-pid="${esc(pid)}">
442
  <div class="meta"><span class="venue">${esc(e.venue||"")}</span><span>${esc(pid)}</span>${e.text_source==="jats"?'<span class="src-tag">PMC XML</span>':""}${S.me.admin&&S.manifest.pool?`<span style="flex:1"></span><span class="pool-pill ${S.manifest.pool.includes(pid)?"on":""}" data-pool="${esc(pid)}" title="Whether annotators (A/B/C…) see this paper">${S.manifest.pool.includes(pid)?"✓ in pool":"not in pool"}</span>`:""}</div>
443
  <h3>${esc(e.title||pid)}</h3>
444
  <div class="abs">${esc(e.abstract||"")}</div>
445
+ <div class="foot"><div class="blocks">${blks}</div>
446
  <span class="status ${st}">${st==="doing"?o.filled+" fields":"Not started"}</span></div>
447
  </div>`;
448
  }).join("");
 
514
  const tags=Object.entries(x).filter(([k,v])=>v&&k!=="model_fk"&&typeof v!=="object").map(([k,v])=>{const t=v===true?k:String(v);return `<span class="tag">${esc(prettyEnum(t))}</span>`}).join("");
515
  let main;
516
  if(rt.id==="result"){main=[valOf(f.R1,50),valOf(f.R2,40)].filter(Boolean).join(" = ")}
517
+ else if(rt.id==="claim"||rt.id==="summary"){main=valOf(f.F1,160)}
518
  else if(rt.id==="asset"){main=[x.asset_type&&prettyEnum(x.asset_type),valOf(f.availability_claim,90)].filter(Boolean).join(": ")}
519
  else{main=valOf(f[rt.label_field],80)}
520
+ const ce=rt.card_extra; /* v7: [label, field id] from the schema */
521
+ const extra=ce?(f[ce[1]]?`<span class="k">${esc(ce[0])}</span>${esc(valOf(f[ce[1]],40))}`:""):rt.id==="dataset"&&f.D3?`<span class="k">source</span>${esc(valOf(f.D3,40))}`:rt.id==="split"&&f.D26?`<span class="k">split unit</span>${esc(valOf(f.D26,40))}`:"";
522
  const n=fieldDefs(rt).filter(d=>isFilled(f[d.id])).length;
523
  return `<li>${main?esc(main):`<span class="none">(unnamed)</span>`}${rt.id==="asset"?"":tags} ${extra?"<br>"+extra:""}<span class="tag" style="float:right">${n}/${fieldDefs(rt).length}</span></li>`;
524
  }
 
539
  body=recs.length?`<ul>${recs.map(r=>recordLine(rt,r,st.fields[r.uid])).join("")}</ul>`:`<div class="none">Not annotated yet</div>`;
540
  }
541
  const sub=rt.cardinality==="single"?`${filledN}/${defs.length} fields`:`${recs.length} record${recs.length===1?"":"s"} · ${filledN} fields filled`;
542
+ return `<div class="bcard ${rt.cardinality==="single"||rt.id==="claim"||rt.id==="summary"?"wide":""}" style="--bc:var(--c-${rt.id})">
543
+ <h4>${esc(rtName(rt))}</h4><div class="sub">${sub}</div>
544
  <div class="bar"><i style="width:${Math.min(pct,100)}%"></i></div>${body}</div>`;
545
  }).join("");
546
  $("#dt-sum").textContent=(st.read_only?`${st.annotator}'s annotation (read-only) · `:"")+`${totalFilled} fields filled in total`;
 
670
  const SRC={jats:["good","Text: publisher XML (official)","Text comes from the PMC XML, not from PDF conversion"],
671
  docling:["ok","Text: layout-aware PDF conversion","Converted with Docling; tables/formulas can still be wrong, check against the PDF"],
672
  docling_0817:["ok","Text: PDF conversion (0817)","Docling conversion without PDF linking"],
673
+ docling_package:["ok","Text: v7 package (Docling)","Same text the v7 extraction read: main text + supplementary parts, marked <<<PART n>>>; figures as captions only. No click-sync with the PDF."],
674
  pymupdf:["weak","Text: plain PDF extraction","Flat PyMuPDF text: tables are often scrambled, check against the PDF"]};
675
  const W=(()=>{try{return Object.assign({split:64,single:46},JSON.parse(localStorage.getItem("repl_widths")||"{}"))}catch(e){return {split:64,single:46}}})();
676
  function saveW(){try{localStorage.setItem("repl_widths",JSON.stringify(W))}catch(e){}}
 
838
  /* 在 PDF 上划选: 记下精确位置; 转换文本里对不上(损坏)时位置也不丢 */
839
  if(fromPdf){ev.origin="pdf";ev.pdf_anchor={blocks:[],boxes:selectionBoxes(sel.getRangeAt(0))}}
840
  pl.evidence.push(ev);
841
+ if(!pl.status)pl.status=S.schema.attributes.status[0].v;
842
  saveField(S.active.uid,S.active.fid,pl,true);
843
  sel.removeAllRanges();seltool.style.display="none";
844
  }
 
912
  const c=document.createElement("input");c.type="checkbox";c.checked=!!rec.extra[e.id];
913
  c.onchange=()=>{rec.extra[e.id]=c.checked;saveExtra(rec);renderRight()};
914
  lab.prepend(c);
915
+ }else if(e.type==="text"){ /* 1103 v7: free-text record attribute (model name, ablation variant) */
916
+ const t=document.createElement("input");t.type="text";t.value=rec.extra[e.id]||"";t.style.width="140px";
917
+ t.oninput=()=>{rec.extra[e.id]=t.value;saveExtra(rec)};
918
+ t.onchange=()=>{renderTabs();renderRight()}; /* refresh the card title (label_field may be this attribute) */
919
+ lab.appendChild(t);
920
  }else{
921
  const s=document.createElement("select");
922
  s.innerHTML="<option value=''>—</option>"+e.options.map(o=>`<option ${rec.extra[e.id]===o?"selected":""}>${o}</option>`).join("");
 
949
  /* groups */
950
  const na=naSet();
951
  const groups=def.groups||[{title:null,fields:def.fields}];
952
+ const su=def.simple_unless;
953
+ const simple=su?!!(rec.extra[su.extra]&&rec.extra[su.extra]!==su.equals):!!(def.id==="model"&&rec.extra.role&&rec.extra.role!=="proposed"); /* datasets also have a role in v7 */
954
  groups.forEach(gr=>{
955
+ if(!groupVisible(rec,gr))return;
956
  const g=document.createElement("div");g.className="group";
957
  if(gr.title){
958
  const t=document.createElement("div");t.className="group-title";
959
  t.innerHTML=`<span class="tri">▾</span> ${esc(gr.title)}`;
960
  t.onclick=()=>g.classList.toggle("closed");
961
  g.appendChild(t);
962
+ if(simple&&!gr.always_open&&!/M1–M4|framework/i.test(gr.title))g.classList.add("closed");
963
  }
964
  gr.fields.forEach(f=>g.appendChild(renderField(rec,f,na)));
965
  card.appendChild(g);
966
  });
967
  if(simple){
968
  const n=document.createElement("div");n.className="f-hint";n.style.padding="6px 12px";
969
+ n.textContent=su?"Baseline / comparison: header (incl. HP source) + M1 only.":"role ≠ proposed: abbreviated record suffices; other groups collapsed, expand as needed.";
970
  card.appendChild(n);
971
  }
972
  return card;
973
  }
974
 
975
+ /* 1103 v7 display rules (all optional in the schema):
976
+ trigger = flag extra must be on · only_if_extra / hide_if_extra = {extra: [values]} · show_if_field = {field id: [values]} */
977
+ function groupVisible(rec,gr){
978
+ if(gr.trigger&&!rec.extra[gr.trigger])return false;
979
+ for(const [k,vals] of Object.entries(gr.only_if_extra||{}))if(!vals.includes(rec.extra[k]))return false;
980
+ for(const [k,vals] of Object.entries(gr.hide_if_extra||{}))if(vals.includes(rec.extra[k]))return false;
981
+ for(const [fid,vals] of Object.entries(gr.show_if_field||{}))if(!vals.includes(S.fields[rec.uid]?.[fid]?.value))return false;
982
+ return true;
983
+ }
984
+
985
  function fkBox(target,selected,onchange){
986
  const targets=Array.isArray(target)?target:[target];
987
  const box=document.createElement("div");box.className="fk-box";
 
1024
  ].join("");
1025
  const done=pl.status?`<span class="f-done">● ${pl.status}</span>`:"";
1026
  el.innerHTML=`<div class="f-head"><span class="f-id">${f.id}</span><span class="f-label">${esc(f.label)}</span>${badges}${done}</div>`
1027
+ +(f.hint?`<div class="f-guide">${esc(f.hint)}${f.example?` <span class="eg">e.g. ${esc(f.example)}</span>`:""}${f.suggest&&f.example?` <span class="eg">vocabulary: ${esc(f.suggest.join(" · "))}</span>`:""}</div>`:"");
1028
  el.querySelector(".f-head").onclick=()=>activate(uid,f.id);
1029
 
1030
  const body=document.createElement("div");body.className="f-body";
 
1034
  st.onchange=()=>{pl.status=st.value;saveField(uid,f.id,pl,true)};
1035
  body.appendChild(st);
1036
  /* granularity */
1037
+ if(S.schema.attributes.granularity){ /* v7 has no granularity */
1038
+ const gr=document.createElement("select");
1039
+ gr.innerHTML="<option value=''>granularity…</option>"+S.schema.attributes.granularity.map(s=>`<option value="${s.v}" ${pl.granularity===s.v?"selected":""}>${s.label}</option>`).join("");
1040
+ gr.onchange=()=>{pl.granularity=gr.value;saveField(uid,f.id,pl)};
1041
+ body.appendChild(gr);
1042
+ }
1043
  /* value */
1044
  body.appendChild(valueEditor(f,pl,uid));
1045
  /* defer_target */
 
1051
  body.appendChild(d);
1052
  }
1053
  /* evidence */
1054
+ if(!f.no_evidence)body.appendChild(evidenceList(uid,f.id,pl));
1055
  /* note */
1056
  const note=document.createElement("input");note.type="text";note.className="full";
1057
  note.placeholder="Note: reason for uncertain, internal contradictions, ...";note.value=pl.note||"";
 
1068
  if(f.type==="enum_field"){
1069
  const s=document.createElement("select");s.className="full";
1070
  s.innerHTML="<option value=''>value…</option>"+f.options.map(o=>`<option ${pl.value===o?"selected":""}>${o}</option>`).join("");
1071
+ s.onchange=()=>{pl.value=s.value;saveField(uid,f.id,pl);if(f.rerender)renderRight()};
1072
+ if(!f.with_text)return s;
1073
+ /* 1103 v7: choice + free text (D12 name, D15/D10 "other", R1 metric) */
1074
+ const box=document.createElement("div");box.className="full";
1075
+ const t=document.createElement("input");t.type="text";t.className="full";t.style.marginTop="4px";
1076
+ t.placeholder=f.text_placeholder||"details";t.value=pl.value_text||"";
1077
+ t.oninput=()=>{pl.value_text=t.value;saveField(uid,f.id,pl)};
1078
+ box.append(s,t);return box;
1079
  }
1080
  if(f.type==="axes"){
1081
  const box=document.createElement("div");box.className="full";
 
1162
  if(!q)return;
1163
  pl.evidence=pl.evidence||[];
1164
  pl.evidence.push({quote:q.replace(/\s+/g," ").trim(),section:"figure",flags:{figure_only:true},grounded:null,char_span:null});
1165
+ if(!pl.status)pl.status=S.schema.attributes.status[0].v;
1166
  saveField(uid,fid,pl,true);
1167
  };
1168
  box.appendChild(add);