+
+ {CHALLENGE_TEXT.head(report.label || report.node_id)}
+
+ {total > 0 || items.length ? (
+
+ {VERDICTS.map((v, i) => (
+
+ {i > 0 && ·}
+
+ {CHALLENGE_TEXT.count(CHALLENGE_TEXT.verdict[v], counts[v])}
+
+
+ ))}
+ {refuted && !flagged && (
+
+ {CHALLENGE_TEXT.degrade}
+ :{CHALLENGE_TEXT.degradeHint}
+
+ )}
+
+ ) : (
+
{CHALLENGE_TEXT.none}
+ )}
+ {refuted && flagged && (
+
{CHALLENGE_TEXT.flagNote}
+ )}
+ {rows.length > 0 && (
+
+ {rows.map((item, i) => {
+ const key = `${item.unit}\u0000${i}`
+ return (
+ toggle(key)}>
+
+
+ )
+ })}
+
+ )}
+ {/* 工件取回来了、却没有条目之外的句子(计数和工件对不上):没什么可补的,这一行也不留 */}
+ {hidden > 0 && !extra.length && challenge.artifact && !(all && detail.status === 'ok') && (
+
+ {CHALLENGE_TEXT.more(hidden)}
+ {detail.status === 'error' && all ? (
+ <>
+ {CHALLENGE_TEXT.failed}
+ {detail.retry && }
+ >
+ ) : (
+
+ )}
+
+ )}
+ {/* 判断标签看着像结论,得说清是谁判的:整块说一次,工件取回来之后带上模型名 */}
+ {(total > 0 || items.length > 0) && (
+
+ {CHALLENGE_TEXT.judgedBy(detail.data?.model ?? null)}
+
+ )}
+
+ )
+}
+
+/** 判断标签:文字加颜色(去掉颜色也读得出),不铺底色——浅色主题下底色会把小字的对比度压到 4.5 以下 */
+function VerdictTag({ verdict }: { verdict: string }) {
+ const color = isVerdict(verdict) ? VERDICT_COLOR[verdict] : 'var(--text-dim)'
+ return (
+ ('[data-challenge-query-head]')
+ if (!part || !head) return
+ head.focus({ preventScroll: true })
+ head.scrollIntoView({ block: 'nearest' })
+ part.dataset.flash = 'focus'
+ setTimeout(() => { if (part.dataset.flash === 'focus') delete part.dataset.flash }, 1600)
+ }, [baseId])
+ if (detail.status === 'error') {
+ return (
+
+ {CHALLENGE_TEXT.failed}
+ {detail.retry && }
+
+ )
+ }
+ if (detail.status !== 'ok') {
+ return {CHALLENGE_TEXT.loading}
+ }
+ if (!claim) return {CHALLENGE_TEXT.claimMissing}
+ const aliases = new Set(claim.queries.map((q) => q.alias))
+ return (
+ <>
+
+ {CHALLENGE_TEXT.rationale}
+ (aliases.has(alias) ? jump(alias) : undefined)} />
+
+
+
+ {claim.queries.length ? CHALLENGE_TEXT.queries(claim.queries.length) : CHALLENGE_TEXT.noQueries}
+
+
+ {claim.queries.map((q, i) => )}
+
+
+ >
+ )
+}
+
+function QueryCard({ id, query }: { id: string; query: ChallengeQuery }) {
+ return (
+
+
+ {/* 从理由里的数字跳过来时焦点落在这里:得看得见落在哪 */}
+
+ {EVIDENCE_TEXT.query(query.alias)}
+ {query.source && {query.source}}
+
+ {query.sql && query.sql} />}
+
+ {query.sql && (
+
+ {query.sql}
+
+ )}
+
+ {query.row_count != null && {CHALLENGE_TEXT.rows(query.row_count)}}
+ {query.truncated && (
+ {CHALLENGE_TEXT.truncated}
+ )}
+
+
+ )
+}
+
+// ---------------------------------------------------------------- 理由
+
+/**
+ * 理由正文。有理由文档时按文档画:数字的出处照报告里的样子(有出处的实线、裸数字点状线加「无证据」),有出处的数字后面
+ * 挂查询编号,点一下跳到那次查询。文档可能直接放在工件里,也可能是另一个工件的 id(约定没定死,两种都认);
+ * 都没有、或者取不到时按渲染后的正文显示,数字照写,只是不标出处
+ */
+function Rationale({ claim, onJump }: { claim: ChallengeClaim; onJump: (alias: string) => void }) {
+ const ref = claim.rationale_doc
+ const inline = ref && typeof ref === 'object' ? asDoc(ref) : null
+ const artifact = typeof ref === 'string' ? ref : ''
+ const slot = useEvidence((s) => (artifact ? s.docs[artifact] : undefined))
+ const loadDoc = useEvidence((s) => s.loadDoc)
+ useEffect(() => { if (artifact) void loadDoc(artifact) }, [artifact, loadDoc])
+ const doc = inline ?? (slot?.status === 'ok' ? slot.data ?? null : null)
+ if (doc && (doc.blocks ?? []).length) return
+ if (artifact && slot?.status !== 'error' && !doc) {
+ return {CHALLENGE_TEXT.docLoading}
+ }
+ if (!claim.rationale.trim()) return {CHALLENGE_TEXT.rationaleEmpty}
+ return
+}
+
+/** 列表项从哪一个片段开始:同 EvidenceDoc(行首的「- 」「1. 」是结构片段) */
+const ITEM_START = /(^|\n)[ \t]*([-*+]|\d+[.)])[ \t]+$/
+
+function RationaleDoc({ doc, onJump }: { doc: EvidenceDocData; onJump: (alias: string) => void }) {
+ return (
+
+ {doc.blocks.map((block, b) => )}
+
+ )
+}
+
+/** 一块:外观借 Markdown 的 BlockView,和报告文档、普通答案长得一样(块的分法同 EvidenceDoc 的 DocBlock) */
+function RationaleBlock({ block, doc, onJump }: { block: EvidenceBlock; doc: EvidenceDocData; onJump: (alias: string) => void }) {
+ const units = block.units ?? []
+ const unit = (u: EvidenceUnit, code = false) =>
+ switch (block.type) {
+ case 'heading':
+ return unit(u)) }} />
+ case 'hr':
+ return
+ case 'quote':
+ return unit(u)) }} />
+ case 'code':
+ return unit(u, true)) }} />
+ case 'list': {
+ const items: { key: string; depth: number; units: EvidenceUnit[] }[] = []
+ for (const u of units) {
+ const lead = u.segments?.[0]
+ if (!items.length || (lead?.kind === 'structural' && ITEM_START.test(lead.text))) {
+ items.push({ key: u.id, depth: u.depth ?? 0, units: [u] })
+ } else items[items.length - 1].units.push(u)
+ }
+ return (
+ ({ key: it.key, depth: it.depth, body: it.units.map((u) => unit(u)) })),
+ }} />
+ )
+ }
+ case 'table': {
+ const cells = new Map()
+ let cols = 0
+ const rowIds: number[] = []
+ for (const u of units) {
+ const loc = u.loc ?? { row: 0, col: 0 }
+ cells.set(`${loc.row}:${loc.col}`, [...(cells.get(`${loc.row}:${loc.col}`) ?? []), u])
+ cols = Math.max(cols, loc.col + 1)
+ if (loc.row >= 0 && !rowIds.includes(loc.row)) rowIds.push(loc.row)
+ }
+ rowIds.sort((a, b) => a - b)
+ const plain = (r: number, c: number) => (cells.get(`${r}:${c}`) ?? []).map(unitText).join('')
+ const node = (r: number, c: number) => (cells.get(`${r}:${c}`) ?? []).map((u) => unit(u))
+ const head = Array.from({ length: cols }, (_, c) => plain(-1, c))
+ const body = rowIds.map((r) => Array.from({ length: cols }, (_, c) => plain(r, c)))
+ return (
+ node(-1, c)),
+ rows: rowIds.map((r) => Array.from({ length: cols }, (_, c) => node(r, c))),
+ numeric: numericColumns(head, body),
+ csv: () => [head, ...body].map((r) => r.join(',')).join('\n'),
+ }} />
+ )
+ }
+ default:
+ return unit(u)) }} />
+ }
+}
+
+/** 片段的颜色写成变量交给 index.css 的 .ev-seg / .ev-mark(同 EvidenceDoc) */
+const evStyle = (state: EvidenceStateCode) => ({
+ '--ev-line': EVIDENCE_STATE[state].decoration, '--ev-soft': EVIDENCE_STATE[state].soft,
+}) as CSSProperties
+
+/** 引用的是查询结果里的一格:返回查询编号 */
+function cellAlias(seg: EvidenceSegment, doc: EvidenceDocData): string | null {
+ const cite = seg.cite
+ if (!cite?.alias || segmentState(seg) !== 'deterministic') return null
+ const entry = doc.catalog?.[cite.alias]
+ return cite.kind === 'cell' || (cite.kind !== 'metric' && cite.kind !== 'input' && entry?.kind === 'query') ? cite.alias : null
+}
+
+/**
+ * 一句话:文字按行内 Markdown 渲染(整句算一次样式,同 EvidenceDoc 的 UnitView)。有出处的数字是按钮,后面挂查询编号;
+ * 裸数字、疑似不存在的名称画点状线,句末挂「无证据」「疑似不存在的名称」。别的片段(表名、字段名的链接)照普通文字
+ */
+function RationaleUnit({ unit, doc, code, onJump }: {
+ unit: EvidenceUnit; doc: EvidenceDocData; code: boolean; onJump: (alias: string) => void
+}) {
+ const segs = (unit.segments ?? []).filter((s) => s.kind !== 'structural')
+ const whole = segs.map((s) => s.text).join('')
+ const styles = useMemo(() => (code ? null : inlineStyles(whole)), [whole, code])
+ const alerts = new Set()
+ let pos = 0
+ const nodes = segs.map((s) => {
+ const from = pos
+ pos += s.text.length
+ const state = segmentState(s)
+ const alias = cellAlias(s, doc)
+ const body = styles ? : s.text
+ if (alias) {
+ const source = sourceOf(s, doc)
+ return (
+
+ )
+ }
+ if (state === 'none' || state === 'suspect') {
+ alerts.add(state)
+ return (
+
+ {body}({EVIDENCE_STATE_LABEL[state]})
+
+ )
+ }
+ return {s.strong ? {body} : body}
+ })
+ return (
+ <>
+ {nodes}
+ {!code && [...alerts].map((state) => (
+
+ {EVIDENCE_STATE[state].glyph}{EVIDENCE_STATE_LABEL[state]}
+
+ ))}
+ >
+ )
}