const state = { data: null, filter: "UNREVIEWED", visibleItems: [], currentIndex: 0, selectedStatus: null, selectedLayers: new Set(), selectedStdKey: null, selectedComponentKind: null }; const $ = function(id){ return document.getElementById(id); }; function esc(v){ return String(v == null ? "" : v).replace(/[&<>"']/g, function(ch){ return {"&":"&","<":"<",">":">","\"":""","'":"'"}[ch]; }); } async function api(path, options){ const res = await fetch(path, options || {}); const data = await res.json(); if(!res.ok) throw new Error(data.error || ("HTTP " + res.status)); return data; } async function load(){ state.data = await api("/api/bootstrap"); populateLayerFilter(); applyFilter(true); updateProgress(state.data.progress); } function decision(item){ return item.decision || null; } function applyFilter(resetIndex){ const f = state.filter; state.visibleItems = state.data.items.filter(function(item){ const d = decision(item); if(f === "ALL") return true; if(f === "UNREVIEWED") return !d; return d && d.status === f; }); if(resetIndex) state.currentIndex = 0; if(state.currentIndex >= state.visibleItems.length) state.currentIndex = Math.max(0, state.visibleItems.length - 1); renderQueue(); renderCurrent(); } function renderQueue(){ $("queueCount").textContent = state.visibleItems.length + " 项"; $("queue").innerHTML = state.visibleItems.map(function(item, idx){ const d = decision(item); const st = d ? d.status : "未确认"; return ''; }).join(""); document.querySelectorAll(".queue-item").forEach(function(btn){ btn.addEventListener("click", function(){ state.currentIndex = Number(btn.dataset.index); renderQueue(); renderCurrent(); }); }); } function renderCurrent(){ const item = state.visibleItems[state.currentIndex]; const empty = !item; $("emptyState").classList.toggle("hidden", !empty); $("reviewContent").classList.toggle("hidden", empty); if(empty) return; $("cmNameZh").textContent = item.display_name_zh || "未命名视觉对象"; $("anonId").textContent = item.anonymous_id; $("cmOfficialDesc").textContent = item.official_description_en ? ("S-52: " + item.official_description_en) : (item.name_source || ""); $("cmImage").src = "/sprite/cm95/" + encodeURIComponent(item.anonymous_id) + ".png"; $("componentRoles").textContent = item.component_roles.join(", ") || "—"; $("geometryRoles").textContent = item.geometry_roles.join(", ") || "—"; $("occurrenceCount").textContent = item.occurrence_count; const sem = item.semantic || {}; $("semanticCategory").textContent = sem.category_zh || "尚未建立语义分类"; $("semanticReason").textContent = sem.reason_zh || "当前没有可靠的 Provider 语义规则。"; const componentHint = $("semanticComponentHint"); if(sem.component_kind){ const kind = (state.data.component_kinds || []).find(function(x){ return x.id === sem.component_kind; }); componentHint.classList.remove("hidden"); componentHint.textContent = "建议操作:COMPONENT · " + (kind ? kind.name_zh : sem.component_kind); } else { componentHint.classList.add("hidden"); componentHint.textContent = ""; } $("decisionConflict").classList.toggle("hidden", !item.decision_conflict); const s = item.suggestion; if(s){ const layers = s.standard_layer ? [s.standard_layer] : (s.standard_layers || []); $("suggestionBox").classList.remove("hidden"); const layerText = layers.map(function(layer){ return (state.data.standard_layer_zh[layer] || layer) + "(" + layer + ")"; }).join(" / "); const nameText = s.standard_name_zh || "未指定标准对象名"; $("suggestionBox").innerHTML = "旧视觉匹配建议(辅助证据)
" + '' + esc(nameText) + '
' + esc(s.status || "—") + " → " + esc(layerText || "—") + (s.review_stage ? '
' + esc(s.review_stage) + "" : ""); } else { $("suggestionBox").classList.add("hidden"); $("suggestionBox").innerHTML = ""; } const d = decision(item); state.selectedStatus = d ? d.status : null; state.selectedLayers = new Set(d ? (d.standard_layers || []) : []); state.selectedStdKey = d ? d.standard_resource_key : null; state.selectedComponentKind = d ? (d.component_kind || null) : null; $("note").value = d ? (d.note || "") : ""; renderDecisionBadge(d); renderStatusButtons(); renderModeFields(); renderComponentKindSelection(); renderLayerSelection(); renderLayerSuggestions(item); renderCandidates(item); renderSelectedStd(); $("auditDetails").open = false; $("auditText").textContent = "尚未读取。"; $("message").textContent = ""; $("message").className = "message"; } function renderDecisionBadge(d){ const badge = $("decisionBadge"); const st = d ? d.status : null; badge.className = "badge " + (st ? st.toLowerCase() : "neutral"); badge.textContent = st || "未确认"; } function renderStatusButtons(){ document.querySelectorAll(".status-btn").forEach(function(btn){ btn.classList.toggle("active", btn.dataset.status === state.selectedStatus); }); } function renderModeFields(){ const isComponent = state.selectedStatus === "COMPONENT"; $("componentKindField").classList.toggle("hidden", !isComponent); $("standardLayerField").classList.toggle("hidden", isComponent); $("standardObjectField").classList.toggle("hidden", isComponent); } function renderComponentKindSelection(){ const kinds = state.data.component_kinds || []; $("componentKindSelection").innerHTML = kinds.map(function(k){ return ''; }).join(""); document.querySelectorAll(".component-kind-chip").forEach(function(btn){ btn.addEventListener("click", function(){ const kind = btn.dataset.kind; state.selectedComponentKind = state.selectedComponentKind === kind ? null : kind; renderComponentKindSelection(); }); }); } function renderLayerSelection(){ $("layerSelection").innerHTML = state.data.standard_layers.map(function(layer){ const zh = state.data.standard_layer_zh[layer] || layer; return ''; }).join(""); document.querySelectorAll(".layer-chip").forEach(function(btn){ btn.addEventListener("click", function(){ const layer = btn.dataset.layer; if(state.selectedStatus === "UNKNOWN" || state.selectedStatus === "COMPONENT") return; if(state.selectedStatus === "MATCH" || state.selectedStatus === "PARTIAL"){ state.selectedLayers = new Set([layer]); if(state.selectedStdKey){ const sr = state.data.standard_resources.find(function(x){ return x.resource_key === state.selectedStdKey; }); if(sr && sr.standard_layer !== layer) state.selectedStdKey = null; } } else { if(state.selectedLayers.has(layer)) state.selectedLayers.delete(layer); else state.selectedLayers.add(layer); } renderLayerSelection(); renderSelectedStd(); }); }); } function renderLayerSuggestions(item){ const rows = item.layer_suggestions || []; const box = $("layerSuggestionList"); if(item.semantic && item.semantic.component_kind){ const kind = (state.data.component_kinds || []).find(function(x){ return x.id === item.semantic.component_kind; }); const kindName = kind ? kind.name_zh : item.semantic.component_kind; box.innerHTML = '
' + '
建议先作为组件处理
' + '
COMPONENT · ' + esc(kindName) + '
' + '
' + esc(item.semantic.reason_zh || '') + '
' + '' + '
' + renderLayerRows(rows); const btn = $("applyComponentSuggestion"); if(btn){ btn.addEventListener("click", function(){ chooseStatus("COMPONENT"); state.selectedComponentKind = item.semantic.component_kind; renderComponentKindSelection(); }); } bindLayerSuggestionClicks(); return; } if(!rows.length){ box.innerHTML = '
当前 S-52 语义没有可靠的 NavSea Layer 建议。请人工选择,或保留 UNKNOWN。
'; return; } box.innerHTML = renderLayerRows(rows); bindLayerSuggestionClicks(); } function renderLayerRows(rows){ return rows.map(function(r, idx){ const top = idx === 0; const score = (Number(r.combined_score || 0) * 100).toFixed(1); const sem = (Number(r.semantic_score || 0) * 100).toFixed(0); const vis = (Number(r.visual_score || 0) * 100).toFixed(0); return ''; }).join(''); } function bindLayerSuggestionClicks(){ document.querySelectorAll(".layer-suggestion-card").forEach(function(card){ card.addEventListener("click", function(){ const layer = card.dataset.layer; state.selectedLayers = new Set([layer]); if(!state.selectedStatus || state.selectedStatus === "UNKNOWN") state.selectedStatus = "PARTIAL"; if(state.selectedStdKey){ const sr = state.data.standard_resources.find(function(x){ return x.resource_key === state.selectedStdKey; }); if(sr && sr.standard_layer !== layer) state.selectedStdKey = null; } renderStatusButtons(); renderModeFields(); renderLayerSelection(); renderSelectedStd(); showMessage("已选择 Layer:" + (state.data.standard_layer_zh[layer] || layer) + "。请确认后保存。", true); }); }); } function renderCandidates(item){ const seen = new Set(); const candidates = []; if(item.suggestion && item.suggestion.standard_resource_key){ const sr = state.data.standard_resources.find(function(x){ return x.resource_key === item.suggestion.standard_resource_key; }); const raw = (item.candidates || []).find(function(x){ return x.resource_key === item.suggestion.standard_resource_key; }); if(sr){ candidates.push({ resource_key:sr.resource_key,standard_layer:sr.standard_layer,standard_layer_zh:sr.standard_layer_zh,display_name_zh:sr.display_name_zh, score:raw ? raw.score : null,text_score:raw ? raw.text_score : null,recommendation_score:raw ? raw.recommendation_score : null, source:"已有建议" }); seen.add(sr.resource_key); } } (item.candidates || []).forEach(function(c){ if(!seen.has(c.resource_key)){ candidates.push({resource_key:c.resource_key,standard_layer:c.standard_layer,standard_layer_zh:c.standard_layer_zh,display_name_zh:c.display_name_zh,score:c.score,text_score:c.text_score,recommendation_score:c.recommendation_score,source:"图标参考"}); seen.add(c.resource_key); } }); if(state.selectedStdKey && !seen.has(state.selectedStdKey)){ const sr = state.data.standard_resources.find(function(x){ return x.resource_key === state.selectedStdKey; }); if(sr) candidates.unshift({resource_key:sr.resource_key,standard_layer:sr.standard_layer,standard_layer_zh:sr.standard_layer_zh,display_name_zh:sr.display_name_zh,source:"当前选择"}); } if(!candidates.length){ $("candidateGrid").innerHTML = '
没有 Standard Visual Object 图标参考。Layer 仍可单独人工确认。
'; return; } $("candidateGrid").innerHTML = candidates.map(function(c){ const selected = c.resource_key === state.selectedStdKey; const recommended = c.resource_key === item.recommended_resource_key; const score = c.score != null ? (" · 视觉 " + (Number(c.score)*100).toFixed(1) + "%") : ""; const recScore = c.recommendation_score != null ? (" · 综合 " + (Number(c.recommendation_score)*100).toFixed(1) + "%") : ""; return '
' + '
' + '
' + esc(c.display_name_zh || c.standard_layer_zh || c.resource_key) + (recommended ? '最可能' : '') + '
' + '
' + esc(c.resource_key) + '
' + '' + esc(c.standard_layer_zh || c.standard_layer) + ' · ' + esc(c.standard_layer) + '' + '' + esc(c.source) + score + recScore + '
'; }).join(""); document.querySelectorAll("#candidateGrid .candidate").forEach(function(card){ card.addEventListener("click", function(){ selectStandard(card.dataset.key, card.dataset.layer); }); }); } function selectStandard(key, layer){ if(state.selectedStatus === "COMPONENT" || state.selectedStatus === "UNKNOWN") return; state.selectedStdKey = key; if(state.selectedStatus !== "AMBIGUOUS" && state.selectedStatus !== "UNKNOWN"){ state.selectedLayers = new Set([layer]); } else if(state.selectedStatus === "AMBIGUOUS"){ state.selectedLayers.add(layer); } renderCandidates(state.visibleItems[state.currentIndex]); renderLayerSelection(); renderSelectedStd(); } function renderSelectedStd(){ const el = $("selectedStd"); if(!state.selectedStdKey){ el.textContent = "未选择"; el.classList.add("muted"); return; } const sr = state.data.standard_resources.find(function(x){ return x.resource_key === state.selectedStdKey; }); if(!sr) return; el.classList.remove("muted"); el.innerHTML = "" + esc(sr.display_name_zh || sr.resource_key) + " · " + esc(sr.standard_layer_zh || sr.standard_layer) + '
' + esc(sr.resource_key) + ' · ' + esc(sr.standard_layer) + '
'; } function useLayerOnly(){ if(state.selectedLayers.size !== 1){ showMessage("请先选择一个标准 Layer。", false); return; } state.selectedStdKey = null; if(!state.selectedStatus || state.selectedStatus === "MATCH") state.selectedStatus = "PARTIAL"; renderStatusButtons(); renderCandidates(state.visibleItems[state.currentIndex]); renderSelectedStd(); showMessage("已设为:仅确认 Layer,不绑定标准图标。", true); } function chooseStatus(status){ state.selectedStatus = status; if(status === "UNKNOWN" || status === "COMPONENT"){ state.selectedLayers.clear(); state.selectedStdKey = null; } else { state.selectedComponentKind = null; if((status === "MATCH" || status === "PARTIAL") && state.selectedLayers.size > 1){ state.selectedLayers = new Set([Array.from(state.selectedLayers)[0]]); } } renderStatusButtons(); renderModeFields(); renderComponentKindSelection(); renderLayerSelection(); renderCandidates(state.visibleItems[state.currentIndex]); renderSelectedStd(); } async function saveDecision(goNext){ const item = state.visibleItems[state.currentIndex]; if(!item) return; if(!state.selectedStatus){ showMessage("请选择 MATCH / PARTIAL / AMBIGUOUS / COMPONENT / UNKNOWN。", false); return; } const payload = { anonymous_id:item.anonymous_id, status:state.selectedStatus, standard_layers:Array.from(state.selectedLayers), standard_resource_key:state.selectedStdKey, component_kind:state.selectedComponentKind, note:$("note").value || "" }; try{ const res = await api("/api/review", {method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(payload)}); item.decision = res.decision; updateProgress(res.progress); showMessage("已保存。", true); if(goNext){ if(state.filter === "UNREVIEWED") applyFilter(false); else { if(state.currentIndex < state.visibleItems.length-1) state.currentIndex++; renderQueue(); renderCurrent(); } } else { renderDecisionBadge(item.decision); renderQueue(); } }catch(e){ showMessage(e.message, false); } } async function clearDecision(){ const item = state.visibleItems[state.currentIndex]; if(!item || !item.decision) return; try{ const res = await api("/api/unreview", {method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({anonymous_id:item.anonymous_id})}); item.decision = null; updateProgress(res.progress); applyFilter(false); }catch(e){ showMessage(e.message, false); } } function showMessage(text, ok){ $("message").textContent = text; $("message").className = "message " + (ok?"ok":"err"); } function updateProgress(p){ const pct = p.total ? (p.reviewed/p.total*100) : 0; $("progressText").textContent = p.reviewed + "/" + p.total + " 已确认 · MATCH " + p.MATCH + " · PARTIAL " + p.PARTIAL + " · AMBIGUOUS " + p.AMBIGUOUS + " · COMPONENT " + p.COMPONENT + " · UNKNOWN " + p.UNKNOWN; $("progressBar").style.width = pct + "%"; } function move(delta){ if(!state.visibleItems.length) return; state.currentIndex = Math.max(0, Math.min(state.visibleItems.length-1, state.currentIndex+delta)); renderQueue(); renderCurrent(); const active = document.querySelector(".queue-item.active"); if(active) active.scrollIntoView({block:"nearest"}); } function populateLayerFilter(){ $("layerFilter").innerHTML = '' + state.data.standard_layers.map(function(x){ return '"; }).join(""); } function openLibrary(){ renderLibrary(); $("libraryDialog").showModal(); } function renderLibrary(){ const layer = $("layerFilter").value; const q = $("librarySearch").value.trim().toLowerCase(); const rows = state.data.standard_resources.filter(function(x){ return (!layer || x.standard_layer === layer) && (!q || x.resource_key.toLowerCase().includes(q) || x.standard_layer.toLowerCase().includes(q) || (x.display_name_zh || "").toLowerCase().includes(q) || (x.standard_layer_zh || "").toLowerCase().includes(q)); }); $("libraryGrid").innerHTML = rows.map(function(x){ return '
' + '' + '
' + esc(x.display_name_zh || x.resource_key) + '
' + '
' + esc(x.resource_key) + '
' + '' + esc(x.standard_layer_zh || x.standard_layer) + ' · ' + esc(x.standard_layer) + '
'; }).join(""); document.querySelectorAll(".library-item").forEach(function(el){ el.addEventListener("click", function(){ selectStandard(el.dataset.key, el.dataset.layer); $("libraryDialog").close(); }); }); } async function loadAudit(){ const item = state.visibleItems[state.currentIndex]; if(!item) return; try{ const data = await api("/api/audit?id=" + encodeURIComponent(item.anonymous_id)); $("auditText").textContent = JSON.stringify(data, null, 2); }catch(e){ $("auditText").textContent = e.message; } } async function abandonAllUnreviewed(){ const p = state.data && state.data.progress ? state.data.progress : null; const count = p ? p.unreviewed : 0; if(!count){ alert("当前没有未确认项。 "); return; } if(!confirm("确定放弃剩余 " + count + " 个未确认项吗?\n\n它们会全部设为 UNKNOWN;已经人工确认的项目不会改变。")) return; try{ const res = await api("/api/abandon-unreviewed", {method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({dry_run:false})}); state.data = await api("/api/bootstrap"); updateProgress(state.data.progress); applyFilter(true); alert("已放弃 " + res.affected + " 个未确认项,全部标记为 UNKNOWN。 "); }catch(e){ alert("批量放弃失败:" + e.message); } } async function finalize(){ if(!confirm("生成当前人工确认结果?未确认项目会保持 UNKNOWN,不会自动继承机器建议。")) return; try{ const res = await api("/api/finalize", {method:"POST",headers:{"Content-Type":"application/json"},body:"{}"}); alert("已生成。\n\n" + res.assignment_dir); }catch(e){ alert("生成失败:" + e.message); } } document.addEventListener("click", function(e){ const f = e.target.closest(".filter"); if(f){ document.querySelectorAll(".filter").forEach(function(x){ x.classList.remove("active"); }); f.classList.add("active"); state.filter = f.dataset.filter; applyFilter(true); } const s = e.target.closest(".status-btn"); if(s) chooseStatus(s.dataset.status); }); $("prevBtn").addEventListener("click", function(){ move(-1); }); $("nextBtn").addEventListener("click", function(){ move(1); }); $("saveBtn").addEventListener("click", function(){ saveDecision(true); }); $("saveStayBtn").addEventListener("click", function(){ saveDecision(false); }); $("clearBtn").addEventListener("click", clearDecision); $("layerOnlyBtn").addEventListener("click", useLayerOnly); $("openLibraryBtn").addEventListener("click", openLibrary); $("closeLibraryBtn").addEventListener("click", function(){ $("libraryDialog").close(); }); $("layerFilter").addEventListener("change", renderLibrary); $("librarySearch").addEventListener("input", renderLibrary); $("loadAuditBtn").addEventListener("click", loadAudit); $("abandonAllBtn").addEventListener("click", abandonAllUnreviewed); $("finalizeBtn").addEventListener("click", finalize); document.addEventListener("keydown", function(e){ const tag = document.activeElement ? document.activeElement.tagName : ""; if(["TEXTAREA","INPUT","SELECT"].includes(tag)) return; if(e.key === "m" || e.key === "M") chooseStatus("MATCH"); else if(e.key === "p" || e.key === "P") chooseStatus("PARTIAL"); else if(e.key === "a" || e.key === "A") chooseStatus("AMBIGUOUS"); else if(e.key === "c" || e.key === "C") chooseStatus("COMPONENT"); else if(e.key === "u" || e.key === "U") chooseStatus("UNKNOWN"); else if(e.key === "ArrowLeft") move(-1); else if(e.key === "ArrowRight") move(1); else if(e.key === "Enter") saveDecision(true); }); load().catch(function(e){ document.body.innerHTML = '
' + esc(e.stack || e.message) + "
"; });