v7 (1103): schema-driven blocks, v7 display rules, ANNOT_DIR
Browse files- app/main.py +4 -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"
|
| 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 |
-
|
|
|
|
|
|
|
| 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
|
| 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(
|
| 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">${
|
| 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
|
|
|
|
| 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(
|
| 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=
|
| 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
|
|
|
|
| 942 |
groups.forEach(gr=>{
|
| 943 |
-
if(
|
| 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 |
-
|
| 1016 |
-
|
| 1017 |
-
|
| 1018 |
-
|
|
|
|
|
|
|
| 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=
|
| 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);
|