@cyber-dash-tech/revela 0.17.0 → 0.17.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -567
- package/README.zh-CN.md +47 -535
- package/designs/monet/DESIGN.md +1 -1
- package/designs/starter/DESIGN.md +1 -1
- package/designs/summit/DESIGN.md +1 -1
- package/lib/commands/help.ts +4 -4
- package/lib/commands/init.ts +9 -7
- package/lib/commands/narrative.ts +13 -4
- package/lib/commands/pdf.ts +24 -15
- package/lib/commands/pptx.ts +2 -16
- package/lib/commands/research.ts +2 -0
- package/lib/commands/review.ts +81 -93
- package/lib/deck-html/contract.ts +26 -10
- package/lib/decks-state.ts +75 -86
- package/lib/edit/deck-state.ts +3 -111
- package/lib/edit/open.ts +2 -2
- package/lib/edit/resolve-deck.ts +14 -24
- package/lib/inspect/open.ts +2 -2
- package/lib/media/download.ts +23 -3
- package/lib/media/save.ts +1 -0
- package/lib/media/types.ts +1 -0
- package/lib/narrative-state/deck-plan-artifact.ts +584 -0
- package/lib/narrative-state/display.ts +74 -4
- package/lib/narrative-state/map-html.ts +242 -107
- package/lib/narrative-state/render-plan.ts +649 -44
- package/lib/narrative-state/research-gaps.ts +5 -2
- package/lib/narrative-vault/compile.ts +16 -1
- package/lib/narrative-vault/types.ts +4 -2
- package/lib/qa/checks.ts +206 -5
- package/lib/qa/measure.ts +63 -1
- package/lib/refine/open.ts +2 -2
- package/lib/refine/server.ts +157 -20
- package/package.json +1 -1
- package/plugin.ts +2 -2
- package/skill/NARRATIVE_SKILL.md +19 -19
- package/skill/SKILL.md +99 -35
- package/tools/decks.ts +83 -51
- package/tools/narrative-view.ts +16 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { NarrativeMap, NarrativeMapClaim, NarrativeMapClaimRelation } from "./map"
|
|
1
|
+
import type { NarrativeMap, NarrativeMapClaim, NarrativeMapClaimRelation, NarrativeMapResearchGap } from "./map"
|
|
2
2
|
import { emptyDisplayModel, isChineseLanguage, isJapaneseLanguage, relationKey, type ValidatedNarrativeDisplayModel } from "./display"
|
|
3
3
|
|
|
4
4
|
interface FlowNode {
|
|
@@ -6,7 +6,14 @@ interface FlowNode {
|
|
|
6
6
|
claim: NarrativeMapClaim
|
|
7
7
|
title: string
|
|
8
8
|
displayCard?: ReturnType<ValidatedNarrativeDisplayModel["claimCards"]["get"]>
|
|
9
|
-
|
|
9
|
+
claimHtml: string
|
|
10
|
+
claimPanelTitle: string
|
|
11
|
+
claimPanelSubtitle: string
|
|
12
|
+
initialDetailId?: string
|
|
13
|
+
initialDetailHtml: string
|
|
14
|
+
initialDetailTitle: string
|
|
15
|
+
initialDetailSubtitle: string
|
|
16
|
+
detailTemplates: string
|
|
10
17
|
}
|
|
11
18
|
|
|
12
19
|
export function renderNarrativeMapHtml(map: NarrativeMap, display?: ValidatedNarrativeDisplayModel): string {
|
|
@@ -19,8 +26,7 @@ export function renderNarrativeMapHtmlWithDisplay(map: NarrativeMap, display: Va
|
|
|
19
26
|
const initial = nodes[0]
|
|
20
27
|
const inferredCount = map.claimRelations.filter((relation) => relation.inferred).length
|
|
21
28
|
const pageTitle = display.pageTitle ?? valueOrFallback(map.snapshot.thesis, map.snapshot.decisionAction || "Narrative claim flow")
|
|
22
|
-
const summaryLine = display.summaryLine ?? "
|
|
23
|
-
const nonCurrentArtifacts = map.artifactCoverage.filter((artifact) => artifact.coverageStatus !== "current").length
|
|
29
|
+
const summaryLine = display.summaryLine ?? "Select a claim to read its evidence and gaps. Evidence cards show what the source says, why it supports the claim, and where it came from."
|
|
24
30
|
return `<!doctype html>
|
|
25
31
|
<html lang="${escapeAttr(display.language)}">
|
|
26
32
|
<head>
|
|
@@ -28,59 +34,83 @@ export function renderNarrativeMapHtmlWithDisplay(map: NarrativeMap, display: Va
|
|
|
28
34
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
29
35
|
<title>${escapeHtml(title)}</title>
|
|
30
36
|
<style>
|
|
31
|
-
:root { color-scheme:light; --bg:#f5f1e9; --paper:#fffdf8; --ink:#1c1917; --muted:#766d63; --line:#ded4c7; --accent:#d8612b; --good:#177044; --warn:#a56015; --bad:#a33434; --soft:#f7f0e7; --shadow:0 20px 54px rgba(54,43,31,.13); font-
|
|
37
|
+
:root { color-scheme:light; --bg:#f5f1e9; --paper:#fffdf8; --ink:#1c1917; --muted:#766d63; --line:#ded4c7; --accent:#d8612b; --good:#177044; --warn:#a56015; --bad:#a33434; --gap:#6d4aa2; --soft:#f7f0e7; --shadow:0 20px 54px rgba(54,43,31,.13); --reading-font:"EB Garamond","Cormorant Garamond",Garamond,Georgia,serif; --ui-font:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; font-family:var(--reading-font); }
|
|
32
38
|
* { box-sizing:border-box; }
|
|
33
39
|
body { margin:0; min-height:100vh; background:radial-gradient(circle at 12% 0,#fff7ed 0,transparent 26rem),radial-gradient(circle at 85% 10%,#edf4f0 0,transparent 24rem),var(--bg); color:var(--ink); }
|
|
34
40
|
.shell { max-width:1440px; margin:0 auto; padding:22px; }
|
|
35
41
|
.topbar { background:rgba(255,253,248,.9); border:1px solid var(--line); border-radius:24px; box-shadow:var(--shadow); padding:20px 22px; display:grid; grid-template-columns:minmax(0,1fr) auto; gap:18px; align-items:start; }
|
|
36
|
-
.eyebrow { margin:0; color:var(--accent); font-size:12px; font-weight:850; letter-spacing:.15em; text-transform:uppercase; }
|
|
42
|
+
.eyebrow { margin:0; color:var(--accent); font-family:var(--ui-font); font-size:12px; font-weight:850; letter-spacing:.15em; text-transform:uppercase; }
|
|
37
43
|
h1 { margin:7px 0 0; max-width:860px; font-size:clamp(24px,3.2vw,42px); line-height:1.02; letter-spacing:-.05em; }
|
|
38
|
-
.summary { margin:10px 0 0; color:var(--muted); font-size:
|
|
44
|
+
.summary { margin:10px 0 0; color:var(--muted); font-size:17px; line-height:1.45; max-width:920px; }
|
|
39
45
|
.pills { display:flex; flex-wrap:wrap; justify-content:flex-end; gap:8px; }
|
|
40
|
-
.pill { display:inline-flex; border-radius:999px; padding:7px 10px; font-size:12px; font-weight:780; border:1px solid var(--line); background:#fff; color:var(--muted); white-space:nowrap; }
|
|
46
|
+
.pill { display:inline-flex; border-radius:999px; padding:7px 10px; font-family:var(--ui-font); font-size:12px; font-weight:780; border:1px solid var(--line); background:#fff; color:var(--muted); white-space:nowrap; }
|
|
41
47
|
.pill.current,.pill.supported { color:var(--good); background:#e8f4ed; border-color:#b9dcc8; }
|
|
42
48
|
.pill.stale,.pill.missing { color:var(--bad); background:#fbe7e7; border-color:#efb9b9; }
|
|
43
49
|
.pill.partial,.pill.weak,.pill.open { color:var(--warn); background:#fff1dc; border-color:#edd0a5; }
|
|
44
|
-
.layout { display:grid; grid-template-columns:minmax(0,1fr)
|
|
45
|
-
.flow,.detail-panel { background:rgba(255,253,248,.92); border:1px solid var(--line); border-radius:24px; box-shadow:var(--shadow); }
|
|
50
|
+
.layout { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:18px; margin-top:18px; align-items:start; }
|
|
51
|
+
.flow,.claim-panel,.detail-panel { background:rgba(255,253,248,.92); border:1px solid var(--line); border-radius:24px; box-shadow:var(--shadow); }
|
|
46
52
|
.flow { padding:20px; }
|
|
47
53
|
.flow-head { display:flex; justify-content:space-between; gap:14px; align-items:flex-start; margin-bottom:18px; }
|
|
48
54
|
.flow-head h2 { margin:0; font-size:18px; letter-spacing:-.025em; }
|
|
49
|
-
.flow-note { margin:4px 0 0; color:var(--muted); font-size:
|
|
55
|
+
.flow-note { margin:4px 0 0; color:var(--muted); font-size:15px; line-height:1.45; }
|
|
50
56
|
.claim-list { display:flex; flex-direction:column; gap:0; }
|
|
51
57
|
.claim-step { display:grid; grid-template-columns:42px minmax(0,1fr); gap:14px; }
|
|
52
58
|
.step-rail { display:flex; flex-direction:column; align-items:center; }
|
|
53
59
|
.step-dot { width:32px; height:32px; border-radius:999px; display:grid; place-items:center; background:#fff; border:1px solid var(--line); color:var(--muted); font-size:12px; font-weight:850; }
|
|
54
60
|
.step-line { flex:1; width:2px; min-height:30px; background:linear-gradient(var(--line),rgba(222,212,199,.25)); margin:8px 0; }
|
|
55
61
|
.claim-step:last-child .step-line { display:none; }
|
|
56
|
-
.claim-card { width:100%; text-align:left; cursor:pointer; border:
|
|
57
|
-
.claim-card:hover,.claim-card.active {
|
|
62
|
+
.claim-card { width:100%; min-width:0; text-align:left; cursor:pointer; border:0; border-left:6px solid var(--good); background:transparent; color:var(--ink); border-radius:0; padding:15px 16px; margin-bottom:16px; box-shadow:none; font-family:var(--reading-font); transition:none; }
|
|
63
|
+
.claim-card:hover .claim-title,.claim-card.active .claim-title { color:var(--accent); }
|
|
64
|
+
.claim-card.supported { border-left-color:var(--good); }
|
|
58
65
|
.claim-card.partial,.claim-card.weak { border-left-color:var(--warn); }
|
|
59
66
|
.claim-card.missing { border-left-color:var(--bad); }
|
|
60
|
-
.claim-
|
|
61
|
-
.claim-
|
|
62
|
-
.
|
|
67
|
+
.claim-card.not_required { border-left-color:var(--line); }
|
|
68
|
+
.claim-title { display:block; min-width:0; font-size:20px; font-weight:850; line-height:1.18; letter-spacing:-.018em; overflow-wrap:anywhere; }
|
|
69
|
+
.claim-meta { display:flex; flex-wrap:wrap; gap:6px; margin-top:10px; min-width:0; }
|
|
70
|
+
.tag { display:inline-flex; min-width:0; max-width:100%; border-radius:999px; padding:4px 8px; background:var(--soft); color:var(--muted); font-family:var(--ui-font); font-size:11px; font-weight:800; white-space:normal; overflow-wrap:anywhere; word-break:break-word; }
|
|
63
71
|
.claim-sections { margin-top:13px; display:grid; gap:9px; }
|
|
64
72
|
.claim-section { border-top:1px solid #eee4d8; padding-top:9px; }
|
|
65
|
-
.section-label { display:block; margin-bottom:3px; color:var(--accent); font-size:10px; font-weight:900; letter-spacing:.08em; text-transform:uppercase; }
|
|
66
|
-
.section-text { display:block; color:#51483f; font-size:
|
|
73
|
+
.section-label { display:block; margin-bottom:3px; color:var(--accent); font-family:var(--ui-font); font-size:10px; font-weight:900; letter-spacing:.08em; text-transform:uppercase; }
|
|
74
|
+
.section-text { display:block; min-width:0; color:#51483f; font-family:var(--reading-font); font-size:15px; line-height:1.46; white-space:pre-line; overflow-wrap:anywhere; }
|
|
67
75
|
.relation-strip { margin-top:12px; display:grid; gap:7px; }
|
|
68
|
-
.relation { display:grid; grid-template-columns:
|
|
69
|
-
.relation-badge {
|
|
70
|
-
.relation-target { display:block; color:#51483f; font-weight:720; }
|
|
71
|
-
.relation-note { display:block; margin-top:3px; color:var(--muted); }
|
|
76
|
+
.relation { display:grid; grid-template-columns:1fr; gap:6px; align-items:flex-start; color:var(--muted); font-size:14px; line-height:1.35; min-width:0; }
|
|
77
|
+
.relation-badge { width:fit-content; max-width:100%; border-radius:999px; padding:3px 7px; background:#fff4e8; color:#9c4d1d; border:1px solid #efcfb8; font-family:var(--ui-font); font-size:10px; font-weight:850; letter-spacing:.04em; white-space:normal; overflow-wrap:anywhere; }
|
|
78
|
+
.relation-target { display:block; color:#51483f; font-weight:720; overflow-wrap:anywhere; }
|
|
79
|
+
.relation-note { display:block; margin-top:3px; color:var(--muted); overflow-wrap:anywhere; }
|
|
72
80
|
.relation.inferred .relation-badge { background:#f4f0ea; border-color:var(--line); color:var(--muted); }
|
|
73
|
-
.detail-panel { position:sticky; top:18px; max-height:calc(100vh - 36px); overflow:hidden; display:flex; flex-direction:column; }
|
|
81
|
+
.claim-panel,.detail-panel { position:sticky; top:18px; max-height:calc(100vh - 36px); overflow:hidden; display:flex; flex-direction:column; }
|
|
74
82
|
.detail-head { padding:20px 20px 14px; border-bottom:1px solid var(--line); }
|
|
75
|
-
.detail-title { margin:7px 0 0; font-size:
|
|
76
|
-
.detail-sub { margin-top:8px; color:var(--muted); font-size:
|
|
83
|
+
.detail-title { margin:7px 0 0; font-size:24px; line-height:1.12; letter-spacing:-.035em; }
|
|
84
|
+
.detail-sub { margin-top:8px; color:var(--muted); font-size:15px; line-height:1.4; }
|
|
77
85
|
.detail-body { padding:16px 20px 22px; overflow:auto; }
|
|
78
|
-
.detail-card { border:
|
|
79
|
-
.detail-card h3 { margin:0 0 8px; font-size:13px; letter-spacing:-.01em; }
|
|
80
|
-
.detail-card p { margin:0; color:var(--muted); line-height:1.45; font-size:
|
|
86
|
+
.detail-card { min-width:0; border:0; border-left:5px solid var(--line); border-radius:0; padding:13px; background:transparent; margin-bottom:10px; box-shadow:none; font-family:var(--reading-font); }
|
|
87
|
+
.detail-card h3 { margin:0 0 8px; font-family:var(--ui-font); font-size:13px; letter-spacing:-.01em; }
|
|
88
|
+
.detail-card p { margin:0; color:var(--muted); font-family:var(--reading-font); line-height:1.45; font-size:15px; overflow-wrap:anywhere; }
|
|
89
|
+
.evidence-list { display:grid; gap:10px; min-width:0; }
|
|
90
|
+
.evidence-group-title { margin:12px 0 7px; color:var(--accent); font-family:var(--ui-font); font-size:11px; font-weight:900; letter-spacing:.09em; text-transform:uppercase; }
|
|
91
|
+
.evidence-item { width:100%; min-width:0; text-align:left; cursor:pointer; border:0; border-left:5px solid var(--good); border-radius:0; background:transparent; padding:14px; color:var(--ink); box-shadow:none; font-family:var(--reading-font); overflow-wrap:anywhere; word-break:break-word; transition:none; }
|
|
92
|
+
.evidence-item:hover .evidence-title,.evidence-item.active .evidence-title,.evidence-item:hover .evidence-source,.evidence-item.active .evidence-source { color:var(--accent); }
|
|
93
|
+
.evidence-item.strong { border-left-color:var(--good); }
|
|
94
|
+
.evidence-item.partial,.evidence-item.weak { border-left-color:var(--warn); }
|
|
95
|
+
.evidence-item.gap { border-left-color:var(--gap); background:transparent; }
|
|
96
|
+
.evidence-kind { display:inline-flex; width:fit-content; margin-bottom:9px; border-radius:999px; padding:3px 8px; font-family:var(--ui-font); font-size:10px; font-weight:900; letter-spacing:.08em; text-transform:uppercase; background:#e8f4ed; color:var(--good); }
|
|
97
|
+
.evidence-item.partial .evidence-kind,.evidence-item.weak .evidence-kind { background:#fff1dc; color:var(--warn); }
|
|
98
|
+
.evidence-item.gap .evidence-kind { background:#eee6ff; color:var(--gap); }
|
|
99
|
+
.evidence-title { display:block; min-width:0; font-family:var(--reading-font); font-size:20px; font-weight:850; line-height:1.18; letter-spacing:-.012em; overflow-wrap:anywhere; word-break:break-word; }
|
|
100
|
+
.evidence-source { display:block; min-width:0; font-family:var(--reading-font); font-size:20px; font-weight:850; line-height:1.18; letter-spacing:-.012em; overflow-wrap:anywhere; word-break:break-word; }
|
|
101
|
+
.evidence-preview { display:block; margin-top:7px; color:var(--muted); font-size:15px; line-height:1.4; }
|
|
102
|
+
.evidence-field { display:block; min-width:0; margin-top:12px; }
|
|
103
|
+
.evidence-field-title { display:block; margin:0 0 4px; color:var(--accent); font-family:var(--ui-font); font-size:11px; font-weight:900; letter-spacing:.1em; text-transform:uppercase; }
|
|
104
|
+
.evidence-bullets { min-width:0; margin:10px 0 0; padding-left:18px; color:#51483f; font-family:var(--reading-font); font-size:15px; line-height:1.45; overflow-wrap:anywhere; word-break:break-word; }
|
|
105
|
+
.evidence-bullets li { margin:5px 0; }
|
|
106
|
+
.evidence-bullets strong { color:var(--accent); font-family:var(--ui-font); font-size:10px; font-weight:900; letter-spacing:.08em; text-transform:uppercase; }
|
|
107
|
+
.evidence-why { display:block; margin-top:10px; padding-top:10px; border-top:1px solid #eee4d8; color:#51483f; font-size:15px; line-height:1.43; }
|
|
108
|
+
.evidence-why-label,.evidence-source-label { display:block; margin-bottom:3px; color:var(--accent); font-family:var(--ui-font); font-size:10px; font-weight:900; letter-spacing:.08em; text-transform:uppercase; }
|
|
109
|
+
.evidence-source-line { display:block; margin-top:10px; color:var(--muted); font-family:var(--ui-font); font-size:12px; line-height:1.35; overflow-wrap:anywhere; }
|
|
110
|
+
.evidence-meta { display:flex; flex-wrap:wrap; gap:6px; margin-top:9px; min-width:0; }
|
|
81
111
|
.empty { color:var(--muted); font-style:italic; }
|
|
82
112
|
.hidden-detail { display:none; }
|
|
83
|
-
@media (max-width:
|
|
113
|
+
@media (max-width:1180px) { .layout { grid-template-columns:1fr; } .claim-panel,.detail-panel { position:static; max-height:none; } .topbar { grid-template-columns:1fr; } .pills { justify-content:flex-start; } }
|
|
84
114
|
@media (max-width:680px) { .shell { padding:12px; } .topbar,.flow,.detail-panel { border-radius:18px; } .claim-step { grid-template-columns:30px minmax(0,1fr); gap:10px; } .step-dot { width:26px; height:26px; font-size:11px; } .claim-title { font-size:16px; } }
|
|
85
115
|
</style>
|
|
86
116
|
</head>
|
|
@@ -97,7 +127,6 @@ export function renderNarrativeMapHtmlWithDisplay(map: NarrativeMap, display: Va
|
|
|
97
127
|
<span class="pill">${escapeHtml(display.labels.status)}: ${escapeHtml(localizeValue(map.snapshot.status, display))}</span>
|
|
98
128
|
<span class="pill supported">${escapeHtml(systemTerm("claims", display))}: ${nodes.length}</span>
|
|
99
129
|
<span class="pill ${inferredCount > 0 ? "open" : "current"}">${escapeHtml(systemTerm("relations", display))}: ${map.claimRelations.length}${inferredCount > 0 ? ` (${inferredCount} ${escapeHtml(systemTerm("inferred", display))})` : ""}</span>
|
|
100
|
-
<span class="pill ${nonCurrentArtifacts > 0 ? "partial" : "current"}">${escapeHtml(systemTerm("artifacts", display))}: ${map.artifactCoverage.length}${nonCurrentArtifacts > 0 ? ` (${nonCurrentArtifacts} ${escapeHtml(systemTerm("attention", display))})` : ""}</span>
|
|
101
130
|
</div>
|
|
102
131
|
</header>
|
|
103
132
|
<div class="layout">
|
|
@@ -112,32 +141,69 @@ export function renderNarrativeMapHtmlWithDisplay(map: NarrativeMap, display: Va
|
|
|
112
141
|
${nodes.length ? nodes.map((node, index) => renderStep(node, map, display, index, index === 0)).join("") : emptyCard(display.labels.claimFlow, display.labels.noClaims)}
|
|
113
142
|
</div>
|
|
114
143
|
</section>
|
|
115
|
-
<aside class="
|
|
144
|
+
<aside class="claim-panel selected-claim">
|
|
116
145
|
<div class="detail-head">
|
|
117
146
|
<p class="eyebrow">${escapeHtml(display.labels.selectedClaim)}</p>
|
|
118
|
-
<h2 class="detail-title" id="detail-title">${escapeHtml(initial?.
|
|
119
|
-
<div class="detail-sub" id="detail-sub">${escapeHtml(initial
|
|
147
|
+
<h2 class="detail-title" id="detail-title">${escapeHtml(initial?.claimPanelTitle ?? display.labels.noClaims)}</h2>
|
|
148
|
+
<div class="detail-sub" id="detail-sub">${escapeHtml(initial?.claimPanelSubtitle ?? "Run /revela init to create narrative claims.")}</div>
|
|
120
149
|
</div>
|
|
121
|
-
<div class="detail-body" id="
|
|
150
|
+
<div class="detail-body" id="claim-body">${initial?.claimHtml ?? emptyCard(display.labels.claimFlow, display.labels.noClaims)}</div>
|
|
151
|
+
</aside>
|
|
152
|
+
<aside class="detail-panel selected-evidence">
|
|
153
|
+
<div class="detail-head">
|
|
154
|
+
<p class="eyebrow">${escapeHtml(display.labels.selectedEvidence)}</p>
|
|
155
|
+
<h2 class="detail-title" id="evidence-title">${escapeHtml(initial?.initialDetailTitle ?? display.labels.selectedEvidence)}</h2>
|
|
156
|
+
<div class="detail-sub" id="evidence-sub">${escapeHtml(initial?.initialDetailSubtitle ?? display.labels.selectEvidencePrompt)}</div>
|
|
157
|
+
</div>
|
|
158
|
+
<div class="detail-body" id="evidence-body">${initial?.initialDetailHtml ?? emptyCard(display.labels.selectedEvidence, display.labels.selectEvidencePrompt)}</div>
|
|
122
159
|
</aside>
|
|
123
160
|
</div>
|
|
124
161
|
</main>
|
|
125
162
|
<div class="hidden-detail">
|
|
126
|
-
${nodes.map((node) => `<template id="
|
|
163
|
+
${nodes.map((node) => `<template id="claim-panel-${escapeAttr(node.id)}" data-title="${escapeHtml(node.claimPanelTitle)}" data-subtitle="${escapeHtml(node.claimPanelSubtitle)}" data-initial-detail-id="${escapeAttr(node.initialDetailId ?? "")}">${node.claimHtml}</template>${node.detailTemplates}`).join("")}
|
|
127
164
|
</div>
|
|
128
165
|
<script>
|
|
129
166
|
const buttons = Array.from(document.querySelectorAll('.claim-card'));
|
|
130
167
|
const title = document.getElementById('detail-title');
|
|
131
168
|
const sub = document.getElementById('detail-sub');
|
|
132
|
-
const
|
|
169
|
+
const claimBody = document.getElementById('claim-body');
|
|
170
|
+
const evidenceTitle = document.getElementById('evidence-title');
|
|
171
|
+
const evidenceSub = document.getElementById('evidence-sub');
|
|
172
|
+
const evidenceBody = document.getElementById('evidence-body');
|
|
173
|
+
function bindEvidenceItems() {
|
|
174
|
+
const items = Array.from(claimBody.querySelectorAll('.evidence-item'));
|
|
175
|
+
items.forEach((item) => item.addEventListener('click', () => selectDetail(item.dataset.detailId)));
|
|
176
|
+
}
|
|
177
|
+
function bindDetailItems() {
|
|
178
|
+
const items = Array.from(evidenceBody.querySelectorAll('.evidence-item'));
|
|
179
|
+
items.forEach((item) => item.addEventListener('click', () => selectDetail(item.dataset.detailId)));
|
|
180
|
+
}
|
|
181
|
+
function selectDetail(id) {
|
|
182
|
+
if (!id) return;
|
|
183
|
+
const template = document.getElementById('detail-item-' + CSS.escape(id));
|
|
184
|
+
if (!template) return;
|
|
185
|
+
evidenceTitle.textContent = template.dataset.title || '';
|
|
186
|
+
evidenceSub.textContent = template.dataset.subtitle || '';
|
|
187
|
+
evidenceBody.innerHTML = template.innerHTML;
|
|
188
|
+
Array.from(claimBody.querySelectorAll('.evidence-item')).forEach((item) => item.classList.toggle('active', item.dataset.detailId === id));
|
|
189
|
+
bindDetailItems();
|
|
190
|
+
}
|
|
133
191
|
function selectClaim(id) {
|
|
134
|
-
const template = document.getElementById('
|
|
192
|
+
const template = document.getElementById('claim-panel-' + CSS.escape(id));
|
|
135
193
|
if (!template) return;
|
|
136
194
|
title.textContent = template.dataset.title || '';
|
|
137
195
|
sub.textContent = template.dataset.subtitle || '';
|
|
138
|
-
|
|
196
|
+
claimBody.innerHTML = template.innerHTML;
|
|
139
197
|
buttons.forEach((button) => button.classList.toggle('active', button.dataset.nodeId === id));
|
|
198
|
+
bindEvidenceItems();
|
|
199
|
+
if (template.dataset.initialDetailId) selectDetail(template.dataset.initialDetailId);
|
|
200
|
+
else {
|
|
201
|
+
evidenceTitle.textContent = '${escapeJs(display.labels.selectedEvidence)}';
|
|
202
|
+
evidenceSub.textContent = '${escapeJs(display.labels.selectEvidencePrompt)}';
|
|
203
|
+
evidenceBody.innerHTML = '${escapeJs(emptyCard(display.labels.selectedEvidence, display.labels.selectEvidencePrompt))}';
|
|
204
|
+
}
|
|
140
205
|
}
|
|
206
|
+
bindEvidenceItems();
|
|
141
207
|
buttons.forEach((button) => button.addEventListener('click', () => selectClaim(button.dataset.nodeId)));
|
|
142
208
|
</script>
|
|
143
209
|
</body>
|
|
@@ -145,13 +211,23 @@ export function renderNarrativeMapHtmlWithDisplay(map: NarrativeMap, display: Va
|
|
|
145
211
|
}
|
|
146
212
|
|
|
147
213
|
function buildFlowNodes(map: NarrativeMap, display: ValidatedNarrativeDisplayModel): FlowNode[] {
|
|
148
|
-
return allClaims(map).map((claim) =>
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
214
|
+
return allClaims(map).map((claim) => {
|
|
215
|
+
const panel = claimEvidencePanel(claim, map, display)
|
|
216
|
+
return {
|
|
217
|
+
id: nodeId(claim.id),
|
|
218
|
+
claim,
|
|
219
|
+
title: display.claimCards.get(claim.id)?.displayTitle ?? claim.text,
|
|
220
|
+
displayCard: display.claimCards.get(claim.id),
|
|
221
|
+
claimHtml: panel.html,
|
|
222
|
+
claimPanelTitle: panel.title,
|
|
223
|
+
claimPanelSubtitle: panel.subtitle,
|
|
224
|
+
initialDetailId: panel.initialDetailId,
|
|
225
|
+
initialDetailHtml: panel.initialDetailHtml,
|
|
226
|
+
initialDetailTitle: panel.initialDetailTitle,
|
|
227
|
+
initialDetailSubtitle: panel.initialDetailSubtitle,
|
|
228
|
+
detailTemplates: panel.detailTemplates,
|
|
229
|
+
}
|
|
230
|
+
})
|
|
155
231
|
}
|
|
156
232
|
|
|
157
233
|
function renderStep(node: FlowNode, map: NarrativeMap, display: ValidatedNarrativeDisplayModel, index: number, active: boolean): string {
|
|
@@ -174,7 +250,6 @@ function renderDisplayCardSummary(card: FlowNode["displayCard"], display: Valida
|
|
|
174
250
|
[labels.role, card.roleLabel],
|
|
175
251
|
[labels.narrativeJob, card.narrativeJob],
|
|
176
252
|
[labels.evidenceSummary, card.evidenceSummary],
|
|
177
|
-
[labels.riskOrGapSummary, card.riskOrGapSummary],
|
|
178
253
|
]
|
|
179
254
|
if (rows.length === 0) return ""
|
|
180
255
|
return `<span class="claim-sections">${rows.filter(([, value]) => Boolean(value)).map(([label, value]) => `<span class="claim-section"><span class="section-label">${escapeHtml(label)}</span><span class="section-text">${escapeHtml(value)}</span></span>`).join("")}</span>`
|
|
@@ -187,52 +262,125 @@ function renderOutgoingRelation(relation: NarrativeMapClaimRelation, display: Va
|
|
|
187
262
|
return `<span class="relation${relation.inferred ? " inferred" : ""}"><span class="relation-badge">${escapeHtml(label)}</span><span><span class="relation-target">${escapeHtml(systemTerm("to", display))}: ${escapeHtml(shorten(target, 120))}</span>${rationale ? `<span class="relation-note">${escapeHtml(systemTerm("rationale", display))}: ${escapeHtml(rationale)}</span>` : ""}</span></span>`
|
|
188
263
|
}
|
|
189
264
|
|
|
190
|
-
function
|
|
191
|
-
const
|
|
192
|
-
const outgoing = map.claimRelations.filter((relation) => relation.fromClaimId === claim.id)
|
|
193
|
-
const objections = map.objections.filter((item) => item.claimId === claim.id)
|
|
194
|
-
const risks = map.risks.filter((item) => item.claimId === claim.id)
|
|
195
|
-
const gaps = map.researchGaps.filter((item) => item.targetType === "claim" && item.targetId === claim.id)
|
|
196
|
-
const slideRefs = map.artifactCoverage.flatMap((artifact) => artifact.slideRefs.filter((ref) => ref.claimId === claim.id).map((ref) => `${artifact.type} slide ${ref.slideIndex} (${ref.role}, ${ref.match}/${ref.location}, coverage:${artifact.coverageStatus})`))
|
|
197
|
-
const coverageGaps = map.artifactCoverage.filter((artifact) => artifact.missingClaimIds.includes(claim.id) || artifact.affectedClaimIds.includes(claim.id))
|
|
265
|
+
function claimEvidencePanel(claim: NarrativeMapClaim, map: NarrativeMap, display: ValidatedNarrativeDisplayModel): { title: string; subtitle: string; html: string; initialDetailId?: string; initialDetailHtml: string; initialDetailTitle: string; initialDetailSubtitle: string; detailTemplates: string } {
|
|
266
|
+
const gaps = relatedGaps(claim, map)
|
|
198
267
|
const card = display.claimCards.get(claim.id)
|
|
268
|
+
const evidenceItems = claim.evidence.map((evidence) => ({
|
|
269
|
+
id: itemDomId("evidence", evidence.id),
|
|
270
|
+
kind: "evidence" as const,
|
|
271
|
+
title: display.labels.linkedGaps,
|
|
272
|
+
subtitle: evidence.source || evidence.id,
|
|
273
|
+
html: linkedGapsPanel(evidence, gaps.filter((gap) => gap.evidenceBindingIds?.includes(evidence.id)), display),
|
|
274
|
+
card: evidenceCard(evidence, display, card),
|
|
275
|
+
}))
|
|
276
|
+
const gapItems = gaps.map((gap) => ({
|
|
277
|
+
id: itemDomId("gap", gap.id),
|
|
278
|
+
kind: "gap" as const,
|
|
279
|
+
title: display.labels.selectedGap,
|
|
280
|
+
subtitle: `${localizeValue(gap.status, display)} / ${localizeValue(gap.priority, display)}`,
|
|
281
|
+
html: gapDetail(gap, display),
|
|
282
|
+
card: gapCard(gap, display),
|
|
283
|
+
}))
|
|
284
|
+
const items = [...evidenceItems, ...gapItems]
|
|
285
|
+
const initial = items[0]
|
|
286
|
+
const title = gaps.length ? `${display.labels.evidenceList} / ${display.labels.gaps}` : display.labels.evidenceList
|
|
287
|
+
const statusLine = [`${claim.evidence.length} ${display.labels.evidenceList}`, gaps.length ? `${gaps.length} ${display.labels.gaps}` : undefined].filter(Boolean).join(" / ")
|
|
288
|
+
const html = [
|
|
289
|
+
`<div class="evidence-list">`,
|
|
290
|
+
claim.evidence.length ? `<div class="evidence-group-title">${escapeHtml(display.labels.evidenceList)}</div>${evidenceItems.map((item) => item.card).join("")}` : `<div class="detail-card"><h3>${escapeHtml(display.labels.evidenceList)}</h3><p class="empty">${escapeHtml(display.labels.noEvidence)}</p></div>`,
|
|
291
|
+
gaps.length ? `<div class="evidence-group-title">${escapeHtml(display.labels.gaps)}</div>${gapItems.map((item) => item.card).join("")}` : "",
|
|
292
|
+
`</div>`,
|
|
293
|
+
].join("")
|
|
294
|
+
return {
|
|
295
|
+
title,
|
|
296
|
+
subtitle: statusLine || display.labels.selectEvidencePrompt,
|
|
297
|
+
html,
|
|
298
|
+
initialDetailId: initial?.id,
|
|
299
|
+
initialDetailHtml: initial?.html ?? emptyCard(display.labels.selectedEvidence, display.labels.selectEvidencePrompt),
|
|
300
|
+
initialDetailTitle: initial?.title ?? display.labels.selectedEvidence,
|
|
301
|
+
initialDetailSubtitle: initial?.subtitle ?? display.labels.selectEvidencePrompt,
|
|
302
|
+
detailTemplates: items.map((item) => `<template id="detail-item-${escapeAttr(item.id)}" data-title="${escapeHtml(item.title)}" data-subtitle="${escapeHtml(item.subtitle)}">${item.html}</template>`).join(""),
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function evidenceCard(evidence: NarrativeMapClaim["evidence"][number], display: ValidatedNarrativeDisplayModel, card: ReturnType<ValidatedNarrativeDisplayModel["claimCards"]["get"]>): string {
|
|
307
|
+
const id = itemDomId("evidence", evidence.id)
|
|
308
|
+
const description = evidence.quote || evidence.source || evidence.location || evidence.supportScope || display.labels.evidence
|
|
309
|
+
const why = card?.supportRationale || card?.supportedScope || evidence.supportScope
|
|
310
|
+
const sources = sourceItems(evidence, display)
|
|
311
|
+
return `<button class="evidence-item ${escapeAttr(evidence.strength)}" type="button" data-evidence-id="${escapeAttr(evidence.id)}" data-detail-id="${escapeAttr(id)}">
|
|
312
|
+
<span class="evidence-kind">${escapeHtml(display.labels.evidence)}</span>
|
|
313
|
+
<span class="evidence-title">${escapeHtml(shorten(description, 180))}</span>
|
|
314
|
+
${why ? `<span class="evidence-field"><span class="evidence-field-title">${escapeHtml(display.labels.whyThisSupports)}</span><ul class="evidence-bullets"><li>${escapeHtml(shorten(why, 220))}</li></ul></span>` : ""}
|
|
315
|
+
<span class="evidence-field"><span class="evidence-field-title">${escapeHtml(display.labels.evidenceSource)}</span><ul class="evidence-bullets">${sources.map((source) => `<li>${escapeHtml(source)}</li>`).join("")}</ul></span>
|
|
316
|
+
<span class="evidence-meta"><span class="tag">${escapeHtml(localizeValue(evidence.strength, display))}</span>${evidence.location ? `<span class="tag">${escapeHtml(evidence.location)}</span>` : ""}${evidence.findingsFile ? `<span class="tag">${escapeHtml(evidence.findingsFile)}</span>` : ""}</span>
|
|
317
|
+
</button>`
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function gapCard(gap: NarrativeMapResearchGap, display: ValidatedNarrativeDisplayModel): string {
|
|
321
|
+
const id = itemDomId("gap", gap.id)
|
|
322
|
+
const question = displayGapQuestion(gap, display)
|
|
323
|
+
return `<button class="evidence-item gap" type="button" data-gap-id="${escapeAttr(gap.id)}" data-detail-id="${escapeAttr(id)}">
|
|
324
|
+
<span class="evidence-kind">${escapeHtml(display.labels.gap)}</span>
|
|
325
|
+
<span class="evidence-source">${escapeHtml(question)}</span>
|
|
326
|
+
<ul class="evidence-bullets"><li><strong>${escapeHtml(display.labels.status)}</strong> ${escapeHtml(localizeValue(gap.status, display))}</li><li><strong>${escapeHtml(systemTerm("priority", display))}</strong> ${escapeHtml(localizeValue(gap.priority, display))}</li></ul>
|
|
327
|
+
<span class="evidence-meta"><span class="tag">${escapeHtml(localizeValue(gap.status, display))}</span><span class="tag">${escapeHtml(localizeValue(gap.priority, display))}</span></span>
|
|
328
|
+
</button>`
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function linkedGapsPanel(evidence: NarrativeMapClaim["evidence"][number], gaps: NarrativeMapResearchGap[], display: ValidatedNarrativeDisplayModel): string {
|
|
332
|
+
if (!gaps.length) return emptyCard(display.labels.linkedGaps, display.labels.noLinkedGaps)
|
|
333
|
+
return `<div class="evidence-list">${gaps.map((gap) => gapCard(gap, display)).join("")}</div>`
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function gapDetail(gap: NarrativeMapResearchGap, display: ValidatedNarrativeDisplayModel): string {
|
|
199
337
|
return detailCards([
|
|
200
|
-
[display.labels.
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
...(
|
|
204
|
-
...(
|
|
205
|
-
|
|
206
|
-
[display.labels.status, `${localizeValue(claim.evidenceStatus, display)} / ${localizeValue(claim.importance, display)} / ${localizeValue(claim.kind, display)}`],
|
|
207
|
-
...localizedOptionalRow(display.labels.supportedScope, card?.supportedScope, claim.supportedScope, display),
|
|
208
|
-
...localizedOptionalRow(display.labels.unsupportedScope, card?.unsupportedScope, claim.unsupportedScope, display),
|
|
209
|
-
[display.labels.incomingRelations, incoming.length ? incoming.map((relation) => relationText(relation, display)).join("<br><br>") : display.labels.none],
|
|
210
|
-
[display.labels.outgoingRelations, outgoing.length ? outgoing.map((relation) => relationText(relation, display)).join("<br><br>") : display.labels.none],
|
|
211
|
-
...(claim.evidence.length ? claim.evidence.map((evidence) => [`${display.labels.evidence}: ${evidence.source}`, evidenceDetailText(evidence, display, card)] as [string, string]) : [[display.labels.evidence, display.labels.none] as [string, string]]),
|
|
212
|
-
...localizedOptionalRow(display.labels.objections, card?.objectionsSummary, objections.length ? objections.map((item) => `${item.text}${item.response ? ` -> ${item.response}` : ""}`).join("<br>") : undefined, display),
|
|
213
|
-
...localizedOptionalRow(display.labels.risks, card?.risksSummary, risks.length ? risks.map((item) => `${item.text}${item.mitigation ? ` -> ${item.mitigation}` : ""}`).join("<br>") : undefined, display),
|
|
214
|
-
...localizedOptionalRow(display.labels.researchGaps, card?.researchGapsSummary ?? card?.riskOrGapSummary, gaps.length ? gaps.map((item) => `${item.question} [${localizeValue(item.status, display)}/${localizeValue(item.priority, display)}]`).join("<br>") : undefined, display),
|
|
215
|
-
...(slideRefs.length ? [[display.labels.coveredSlides, slideRefs.map((ref) => localizeSlideRef(ref, display)).join("<br>")] as [string, string]] : []),
|
|
216
|
-
...(coverageGaps.length ? [[display.labels.coveredSlides, coverageGaps.map((artifact) => `${artifact.type}: ${localizeValue(artifact.coverageStatus, display)}${artifact.staleReasons.length ? ` - ${artifact.staleReasons.join("; ")}` : ""}`).join("<br>")] as [string, string]] : []),
|
|
338
|
+
[display.labels.gap, displayGapQuestion(gap, display)],
|
|
339
|
+
[display.labels.status, `${localizeValue(gap.status, display)} / ${localizeValue(gap.priority, display)}`],
|
|
340
|
+
[systemTerm("target", display), `${gap.targetType}${gap.targetId ? `: ${gap.targetId}` : ""}`],
|
|
341
|
+
...optionalRows(systemTerm("findingsFile", display), gap.findingsFile),
|
|
342
|
+
...optionalRows(systemTerm("evidenceBindingIds", display), gap.evidenceBindingIds?.join(", ")),
|
|
343
|
+
...optionalRows(systemTerm("notes", display), gap.notes),
|
|
217
344
|
])
|
|
218
345
|
}
|
|
219
346
|
|
|
220
|
-
function
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
return
|
|
347
|
+
function relatedGaps(claim: NarrativeMapClaim, map: NarrativeMap): NarrativeMapResearchGap[] {
|
|
348
|
+
const evidenceIds = new Set(claim.evidence.map((evidence) => evidence.id))
|
|
349
|
+
const seen = new Set<string>()
|
|
350
|
+
return map.researchGaps.filter((gap) => {
|
|
351
|
+
const matchesClaim = gap.targetType === "claim" && gap.targetId === claim.id
|
|
352
|
+
const matchesEvidence = (gap.evidenceBindingIds ?? []).some((id) => evidenceIds.has(id))
|
|
353
|
+
if ((!matchesClaim && !matchesEvidence) || seen.has(gap.id)) return false
|
|
354
|
+
seen.add(gap.id)
|
|
355
|
+
return true
|
|
356
|
+
})
|
|
224
357
|
}
|
|
225
358
|
|
|
226
|
-
function
|
|
227
|
-
return
|
|
359
|
+
function optionalRows(label: string, value: string | undefined): Array<[string, string]> {
|
|
360
|
+
return value ? [[label, value]] : []
|
|
228
361
|
}
|
|
229
362
|
|
|
230
|
-
function
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
const
|
|
235
|
-
return
|
|
363
|
+
function sourceItems(evidence: NarrativeMapClaim["evidence"][number], display: ValidatedNarrativeDisplayModel): string[] {
|
|
364
|
+
const items = [evidence.source, evidence.location, evidence.findingsFile, evidence.sourcePath, ...splitSourceUrls(evidence.url)]
|
|
365
|
+
.map((value) => value?.trim())
|
|
366
|
+
.filter((value): value is string => Boolean(value))
|
|
367
|
+
const unique = Array.from(new Set(items))
|
|
368
|
+
return unique.length ? unique : [display.labels.none]
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function splitSourceUrls(value: string | undefined): string[] {
|
|
372
|
+
if (!value?.trim()) return []
|
|
373
|
+
const urls = value.match(/https?:\/\/[^\s,;]+/g)
|
|
374
|
+
if (urls?.length) return urls
|
|
375
|
+
return value.split(/\s*[;·]\s*/).filter(Boolean)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function displayGapQuestion(gap: NarrativeMapResearchGap, display: ValidatedNarrativeDisplayModel): string {
|
|
379
|
+
return display.researchGapCards.get(gap.id)?.displayQuestion ?? gap.question
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function isLocalizedDisplay(display: ValidatedNarrativeDisplayModel): boolean {
|
|
383
|
+
return display.language !== "en"
|
|
236
384
|
}
|
|
237
385
|
|
|
238
386
|
function displayClaimText(claimId: string, fallback: string | undefined, display: ValidatedNarrativeDisplayModel): string {
|
|
@@ -279,25 +427,10 @@ function emptyCard(label: string, value: string): string {
|
|
|
279
427
|
return `<div class="detail-card"><h3>${escapeHtml(label)}</h3><p class="empty">${escapeHtml(value)}</p></div>`
|
|
280
428
|
}
|
|
281
429
|
|
|
282
|
-
function evidenceDetailText(evidence: NarrativeMapClaim["evidence"][number], display: ValidatedNarrativeDisplayModel, card: ReturnType<ValidatedNarrativeDisplayModel["claimCards"]["get"]>): string {
|
|
283
|
-
return [
|
|
284
|
-
`${systemTerm("strength", display)}: ${localizeValue(evidence.strength, display)}`,
|
|
285
|
-
evidence.findingsFile ? `${systemTerm("findingsFile", display)}: ${evidence.findingsFile}` : "",
|
|
286
|
-
evidence.location ? `${systemTerm("location", display)}: ${evidence.location}` : "",
|
|
287
|
-
evidence.quote ? `${systemTerm("quote", display)}: ${evidence.quote}` : "",
|
|
288
|
-
card?.unsupportedScope ? `${display.labels.unsupportedScope}: ${card.unsupportedScope}` : evidence.unsupportedScope && !isLocalizedDisplay(display) ? `${display.labels.unsupportedScope}: ${evidence.unsupportedScope}` : "",
|
|
289
|
-
card?.supportRationale ? `${systemTerm("supportRationale", display)}: ${card.supportRationale}` : evidence.caveat && !isLocalizedDisplay(display) ? `${systemTerm("caveat", display)}: ${evidence.caveat}` : "",
|
|
290
|
-
].filter(Boolean).join(" | ")
|
|
291
|
-
}
|
|
292
|
-
|
|
293
430
|
function allClaims(map: NarrativeMap): NarrativeMapClaim[] {
|
|
294
431
|
return map.claimFlow.length > 0 ? map.claimFlow : map.claims.supported.concat(map.claims.partial, map.claims.weak, map.claims.missing, map.claims.not_required)
|
|
295
432
|
}
|
|
296
433
|
|
|
297
|
-
function claimSubtitle(claim: NarrativeMapClaim, display: ValidatedNarrativeDisplayModel): string {
|
|
298
|
-
return `${localizeValue(claim.kind, display)} / ${localizeValue(claim.importance, display)} / ${localizeValue(claim.evidenceStatus, display)}`
|
|
299
|
-
}
|
|
300
|
-
|
|
301
434
|
function sectionLabels(display: ValidatedNarrativeDisplayModel): Record<string, string> {
|
|
302
435
|
if (isChineseLanguage(display.language)) return { role: "角色", narrativeJob: "叙事任务", evidenceSummary: "证据摘要", riskOrGapSummary: "风险/缺口" }
|
|
303
436
|
if (isJapaneseLanguage(display.language)) return { role: "役割", narrativeJob: "ナラティブ上の役割", evidenceSummary: "根拠の要約", riskOrGapSummary: "リスク/ギャップ" }
|
|
@@ -305,9 +438,9 @@ function sectionLabels(display: ValidatedNarrativeDisplayModel): Record<string,
|
|
|
305
438
|
}
|
|
306
439
|
|
|
307
440
|
function systemTerm(term: string, display: ValidatedNarrativeDisplayModel): string {
|
|
308
|
-
const zh: Record<string, string> = { approval: "审批", claims: "主张", relations: "关系", inferred: "未确认", relation: "关系", from: "来自", to: "指向", rationale: "说明", supportRationale: "支撑逻辑", strength: "强度", findingsFile: "研究文件", location: "位置", quote: "引用", caveat: "注意事项", artifacts: "产物", attention: "需关注", none: display.labels.none }
|
|
309
|
-
const ja: Record<string, string> = { approval: "承認", claims: "クレーム", relations: "関係", inferred: "未確認", relation: "関係", from: "起点", to: "終点", rationale: "理由", supportRationale: "裏付けの論理", strength: "強度", findingsFile: "調査ファイル", location: "場所", quote: "引用", caveat: "留意点", artifacts: "成果物", attention: "要確認", none: display.labels.none }
|
|
310
|
-
const en: Record<string, string> = { approval: "approval", claims: "claims", relations: "relations", inferred: "unconfirmed", relation: "relation", from: "from", to: "to", rationale: "rationale", supportRationale: "why this supports the claim", strength: "strength", findingsFile: "findings file", location: "location", quote: "quote", caveat: "caveat", artifacts: "artifacts", attention: "need attention", none: display.labels.none }
|
|
441
|
+
const zh: Record<string, string> = { approval: "审批", claims: "主张", relations: "关系", inferred: "未确认", relation: "关系", from: "来自", to: "指向", rationale: "说明", supportRationale: "支撑逻辑", strength: "强度", priority: "优先级", findingsFile: "研究文件", sourcePath: "来源文件", url: "链接", location: "位置", quote: "引用", caveat: "注意事项", target: "目标", evidenceBindingIds: "论据 ID", notes: "备注", artifacts: "产物", attention: "需关注", none: display.labels.none }
|
|
442
|
+
const ja: Record<string, string> = { approval: "承認", claims: "クレーム", relations: "関係", inferred: "未確認", relation: "関係", from: "起点", to: "終点", rationale: "理由", supportRationale: "裏付けの論理", strength: "強度", priority: "優先度", findingsFile: "調査ファイル", sourcePath: "出典ファイル", url: "URL", location: "場所", quote: "引用", caveat: "留意点", target: "対象", evidenceBindingIds: "根拠ID", notes: "メモ", artifacts: "成果物", attention: "要確認", none: display.labels.none }
|
|
443
|
+
const en: Record<string, string> = { approval: "approval", claims: "claims", relations: "relations", inferred: "unconfirmed", relation: "relation", from: "from", to: "to", rationale: "rationale", supportRationale: "why this supports the claim", strength: "strength", priority: "priority", findingsFile: "findings file", sourcePath: "source path", url: "URL", location: "location", quote: "quote", caveat: "caveat", target: "target", evidenceBindingIds: "evidence binding IDs", notes: "notes", artifacts: "artifacts", attention: "need attention", none: display.labels.none }
|
|
311
444
|
return (isChineseLanguage(display.language) ? zh : isJapaneseLanguage(display.language) ? ja : en)[term] ?? term
|
|
312
445
|
}
|
|
313
446
|
|
|
@@ -329,16 +462,14 @@ function localizeValue(value: string, display: ValidatedNarrativeDisplayModel):
|
|
|
329
462
|
return table[value] ?? value
|
|
330
463
|
}
|
|
331
464
|
|
|
332
|
-
function localizeSlideRef(value: string, display: ValidatedNarrativeDisplayModel): string {
|
|
333
|
-
if (isChineseLanguage(display.language)) return value.replace(/slide/g, "页面")
|
|
334
|
-
if (isJapaneseLanguage(display.language)) return value.replace(/slide/g, "スライド")
|
|
335
|
-
return value
|
|
336
|
-
}
|
|
337
|
-
|
|
338
465
|
function nodeId(id: string): string {
|
|
339
466
|
return `claim-${id}`.replace(/[^a-zA-Z0-9._-]+/g, "-")
|
|
340
467
|
}
|
|
341
468
|
|
|
469
|
+
function itemDomId(prefix: string, id: string): string {
|
|
470
|
+
return `${prefix}-${id}`.replace(/[^a-zA-Z0-9._-]+/g, "-")
|
|
471
|
+
}
|
|
472
|
+
|
|
342
473
|
function shorten(value: string | undefined, max: number): string {
|
|
343
474
|
const text = valueOrFallback(value, "-")
|
|
344
475
|
return text.length > max ? `${text.slice(0, max - 1)}...` : text
|
|
@@ -359,3 +490,7 @@ function escapeHtml(value: string | undefined): string {
|
|
|
359
490
|
function escapeAttr(value: string | undefined): string {
|
|
360
491
|
return escapeHtml(value).replace(/[^a-zA-Z0-9_-]/g, "_")
|
|
361
492
|
}
|
|
493
|
+
|
|
494
|
+
function escapeJs(value: string | undefined): string {
|
|
495
|
+
return escapeHtml(value).replace(/[\\'`$]/g, (ch) => ({ "\\": "\\\\", "'": "\\'", "`": "\\`", "$": "\\$" }[ch] ?? ch))
|
|
496
|
+
}
|