| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178 |
- "use client";
- import { useMemo, useState } from "react";
- import { Activity, Calculator, Database, FileText, History, ScrollText } from "lucide-react";
- import type { InspectorComparisonRow, InspectorModule, SourceBinding, SourceRecord, SourceSelector } from "@/lib/types";
- import { BindingBadge, selectorDescription } from "@/components/inspector/BindingBadge";
- import { BusinessValueRenderer } from "@/components/inspector/BusinessValueRenderer";
- import { LongValueDisclosure } from "@/components/inspector/LongValueDisclosure";
- interface RowProps {
- row: InspectorComparisonRow;
- module: InspectorModule;
- businessProjection: unknown;
- sources: Record<string, SourceRecord>;
- markers: Map<string, string>;
- activeBindingId?: string;
- onActivate: (bindingId: string, toggle?: boolean) => void;
- }
- export function ComparisonRowCells(props: RowProps) {
- const bindings = props.row.bindingIds
- .map((id) => props.module.bindings.find((binding) => binding.id === id))
- .filter((binding): binding is SourceBinding => Boolean(binding));
- const businessResult = pointerValue(props.businessProjection, props.row.businessSelector);
- const active = bindings.some((binding) => binding.id === props.activeBindingId);
- return <article className={`comparisonItemRow ${active ? "is-active" : ""}`} data-row-id={props.row.id}>
- <section className="comparisonItemBusiness" aria-label="业务详情中的这一项">
- {props.row.label ? <h4>{props.row.label}</h4> : null}
- {businessResult.found
- ? <BusinessValueRenderer module={props.module} value={businessResult.value} label={props.row.label || props.module.title} />
- : <p className="comparisonEmptyValue">业务详情中的这一项无法定位。</p>}
- </section>
- <section className="comparisonItemEvidence" aria-label="这一项的数据依据">
- {bindings.length ? bindings.map((binding) => <EvidenceSummary
- key={binding.id}
- binding={binding}
- source={props.sources[binding.sourceId]}
- marker={props.markers.get(binding.id) || "S?"}
- active={props.activeBindingId === binding.id}
- onActivate={props.onActivate}
- />) : <p className="comparisonEmptyValue">没有返回来源绑定。</p>}
- </section>
- <section className="comparisonItemSource" aria-label="对应的原始字段或原文">
- {bindings.length ? bindings.map((binding) => <SourceExcerpt
- key={binding.id}
- binding={binding}
- source={props.sources[binding.sourceId]}
- sources={props.sources}
- marker={props.markers.get(binding.id) || "S?"}
- />) : <p className="comparisonEmptyValue">没有可展示的原始记录。</p>}
- </section>
- </article>;
- }
- function EvidenceSummary({ binding, source, marker, active, onActivate }: { binding: SourceBinding; source?: SourceRecord; marker: string; active: boolean; onActivate: RowProps["onActivate"] }) {
- return <div className={`comparisonEvidenceSummary ${active ? "is-active" : ""}`} data-binding-id={binding.id}>
- <BindingBadge binding={binding} marker={marker} active={active} onActivate={onActivate} />
- <div className="comparisonEvidenceCopy">
- <strong>{source?.label || (binding.selector.kind === "unresolved" ? "无法定位来源" : binding.sourceId)}</strong>
- <small>{roleLabel(binding.role)} · {transformLabel(binding)}</small>
- <details className="comparisonLocatorDetails">
- <summary>定位详情</summary>
- <code>{selectorDescription(binding)}</code>
- <small>{confidenceLabel(binding.evidence.confidence)}{binding.resolution !== "resolved" ? ` · ${resolutionLabel(binding.resolution)}` : ""}</small>
- </details>
- </div>
- </div>;
- }
- function SourceExcerpt({ binding, source, sources, marker }: { binding: SourceBinding; source?: SourceRecord; sources: Record<string, SourceRecord>; marker: string }) {
- const [rawOpen, setRawOpen] = useState(false);
- const excerpt = useMemo(() => resolveExcerpt(binding.selector, source, sources), [binding.selector, source, sources]);
- const Icon = sourceIcon(source?.kind);
- return <div className={`comparisonSourceExcerpt is-${source?.resultState || "present"}`} data-binding-id={binding.id}>
- <header>
- <Icon aria-hidden="true" size={15} />
- <div><strong>{source?.label || "原始记录不可用"}</strong><small>{sourceKindLabel(source?.kind)}</small></div>
- <span className="sourceExcerptMarker">{marker}</span>
- </header>
- {source ? <SourceMeta source={source} /> : null}
- {source?.resultState === "empty" ? <div className="sourceEmptyResult"><strong>查询完成,返回 0 条</strong><span>这不是记录缺失。</span></div> : null}
- {excerpt.kind === "span" ? <TextSpanExcerpt before={excerpt.before} exact={excerpt.exact} after={excerpt.after} />
- : excerpt.kind === "members" ? <div className="sourceMemberList">{excerpt.items.map((item, index) => <section key={`${item.label}-${index}`}><strong>{item.label}</strong><LongValueDisclosure value={item.value} label="完整成员值" /></section>)}</div>
- : excerpt.kind === "missing" ? <p className="sourceMissingReason">{excerpt.reason}</p>
- : <LongValueDisclosure value={excerpt.value} label="完整字段值" />}
- {source?.rawRecord !== undefined && source?.rawRecord !== null ? <details className="rawRecordDisclosure" onToggle={(event) => setRawOpen(event.currentTarget.open)}>
- <summary><span>完整原始记录</span><small>{source.truncated ? `已省略 ${source.omittedCharacters || 0} 字符` : "按需展开"}</small></summary>
- {rawOpen ? <div><LongValueDisclosure value={source.rawRecord} label="完整原始记录" threshold={Number.MAX_SAFE_INTEGER} /></div> : null}
- </details> : null}
- </div>;
- }
- type Excerpt =
- | { kind: "value"; value: unknown }
- | { kind: "span"; before: string; exact: string; after: string }
- | { kind: "members"; items: Array<{ label: string; value: unknown }> }
- | { kind: "missing"; reason: string };
- function resolveExcerpt(selector: SourceSelector, source: SourceRecord | undefined, sources: Record<string, SourceRecord>): Excerpt {
- if (selector.kind === "unresolved") return { kind: "missing", reason: selector.reason };
- if (!source) return { kind: "missing", reason: "来源记录没有随接口响应返回。" };
- if (selector.kind === "whole-record") return { kind: "value", value: source.rawRecord };
- if (selector.kind === "json-pointer") {
- const selected = pointerValue(source.rawRecord, selector.path);
- return selected.found ? { kind: "value", value: selected.value } : { kind: "missing", reason: `原始记录中不存在字段 ${selector.path}` };
- }
- if (selector.kind === "text-span") {
- const selected = pointerValue(source.rawRecord, selector.path);
- if (!selected.found || typeof selected.value !== "string") return { kind: "missing", reason: `原始记录中不存在文本字段 ${selector.path}` };
- const characters = Array.from(selected.value);
- const start = selector.startCodePoint;
- const end = selector.endCodePoint;
- return {
- kind: "span",
- before: characters.slice(Math.max(0, start - 80), start).join(""),
- exact: characters.slice(start, end).join("") || selector.exactText,
- after: characters.slice(end, end + 80).join(""),
- };
- }
- const items = selector.members.map((member, index) => {
- const memberSource = sources[member.sourceId];
- const resolved = resolveExcerpt(member.selector, memberSource, sources);
- return {
- label: memberSource?.label || `参与记录 ${index + 1}`,
- value: resolved.kind === "value" ? resolved.value
- : resolved.kind === "span" ? `${resolved.before}${resolved.exact}${resolved.after}`
- : resolved.kind === "missing" ? resolved.reason
- : resolved.items.map((item) => item.value),
- };
- });
- return { kind: "members", items };
- }
- function TextSpanExcerpt({ before, exact, after }: { before: string; exact: string; after: string }) {
- return <p className="comparisonTextExcerpt">{before ? <span>{before}</span> : null}<mark>{exact}</mark>{after ? <span>{after}</span> : null}</p>;
- }
- function pointerValue(value: unknown, pointer: string): { found: boolean; value: unknown } {
- if (!pointer) return { found: true, value };
- let current = value;
- for (const rawPart of pointer.replace(/^\//, "").split("/")) {
- const part = rawPart.replaceAll("~1", "/").replaceAll("~0", "~");
- if (Array.isArray(current)) {
- const index = Number(part);
- if (!Number.isInteger(index) || index < 0 || index >= current.length) return { found: false, value: undefined };
- current = current[index];
- } else if (current && typeof current === "object" && Object.prototype.hasOwnProperty.call(current, part)) {
- current = (current as Record<string, unknown>)[part];
- } else return { found: false, value: undefined };
- }
- return { found: true, value: current };
- }
- function SourceMeta({ source }: { source: SourceRecord }) {
- const locator = typeof source.locator === "string" ? undefined : source.locator;
- const fields = locator ? Object.entries(locator).filter(([, value]) => value !== undefined && value !== null && value !== "") : [];
- return fields.length || source.status || typeof source.durationMs === "number" ? <dl className="sourceMeta">
- {fields.map(([key, value]) => <div key={key}><dt>{locatorLabel(key)}</dt><dd><code>{printMeta(value)}</code></dd></div>)}
- {source.status ? <div><dt>状态</dt><dd>{source.status}</dd></div> : null}
- {typeof source.durationMs === "number" ? <div><dt>耗时</dt><dd>{source.durationMs >= 1000 ? `${(source.durationMs / 1000).toFixed(1)} 秒` : `${source.durationMs} ms`}</dd></div> : null}
- </dl> : null;
- }
- function sourceIcon(kind?: string) {
- if (kind === "database") return Database;
- if (kind === "runtime-event") return Activity;
- if (kind === "artifact") return FileText;
- if (kind === "log-anchor") return ScrollText;
- if (kind === "calculation") return Calculator;
- return History;
- }
- function sourceKindLabel(kind?: string) { return ({ database: "数据库记录", "runtime-event": "运行事件 / Event Body", artifact: "脚本或候选产物", "log-anchor": "日志锚点", calculation: "可视化确定性计算", "historical-fallback": "历史回退", missing: "原始记录缺失" } as Record<string, string>)[kind || "missing"] || kind || "原始记录缺失"; }
- function roleLabel(role: SourceBinding["role"]) { return ({ input: "输入", basis: "判断依据", constraint: "约束", process: "运行过程", output: "产出", status: "状态" } as const)[role]; }
- function transformLabel(binding: SourceBinding) { const value = binding.transform; return value.kind === "direct" ? "直接展示" : value.kind === "parsed" ? "确定性解析" : value.kind === "combined" ? "多来源组合" : value.kind === "calculated" ? "确定性计算" : "历史回退"; }
- function confidenceLabel(value: SourceBinding["evidence"]["confidence"]) { return ({ exact: "精确对应", "safe-association": "可安全关联", deterministic: "确定性生成", unconfirmed: "关联未确认" } as const)[value]; }
- function resolutionLabel(value: SourceBinding["resolution"]) { return ({ partial: "部分定位", missing: "记录缺失", unsafe: "无法安全关联", resolved: "已定位" } as const)[value]; }
- function locatorLabel(key: string) { return ({ table: "数据库表", recordId: "记录 ID", eventId: "Event ID", eventName: "Event 名称", eventType: "Event 类型", artifactRef: "产物", logAnchor: "日志锚点", fieldPath: "字段路径", filters: "查询条件", resultCount: "结果数量", reason: "缺失原因" } as Record<string, string>)[key] || key; }
- function printMeta(value: unknown) { if (typeof value === "string") return value; try { return JSON.stringify(value); } catch { return String(value); } }
|