RetrievalDetailRenderer.tsx 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. import { ChevronRight, ExternalLink } from "lucide-react";
  2. import type {
  3. InspectorDetailProjection,
  4. RetrievalActivity,
  5. RetrievalOutcome,
  6. RetrievalResultItem,
  7. } from "@/lib/types";
  8. import { RichText } from "@/components/ui/RichText";
  9. import { ValueView } from "@/components/ui/ValueView";
  10. const STATE_LABELS: Record<string, string> = {
  11. hit: "查询命中",
  12. empty: "查询成功 · 无匹配",
  13. failure: "查询失败",
  14. running: "查询中",
  15. interrupted: "构建中断",
  16. unknown: "状态未知",
  17. success: "已完成",
  18. };
  19. export function RetrievalDetailRenderer({
  20. projection,
  21. onNavigate,
  22. }: {
  23. projection: InspectorDetailProjection;
  24. onNavigate?: (activity: RetrievalActivity) => void;
  25. }) {
  26. if (projection.detailKind === "retrieval-event") {
  27. return <RetrievalEvent projection={projection} />;
  28. }
  29. return <RetrievalActivityGroup projection={projection} onNavigate={onNavigate} />;
  30. }
  31. function RetrievalActivityGroup({
  32. projection,
  33. onNavigate,
  34. }: {
  35. projection: InspectorDetailProjection;
  36. onNavigate?: (activity: RetrievalActivity) => void;
  37. }) {
  38. const activities = projection.activities || [];
  39. return <div className="retrievalInspector">
  40. {(projection.businessSections || []).map((section) => <section className={`inspectorPanel is-${section.variant || "default"}`} key={section.id}>
  41. <h3>{section.title}</h3>
  42. {section.content ? <RichText value={section.content} /> : null}
  43. </section>)}
  44. <section className="inspectorPanel retrievalActivityPanel">
  45. <div className="retrievalSectionHeader"><h3>{projection.retrievalKind === "direct-tools" ? "每次工具调用" : "查询记录"}</h3><span>{activities.length} 次</span></div>
  46. {activities.length ? <ol className="retrievalActivityList">
  47. {activities.map((activity, index) => <li key={`${activity.id}-${index}`}>
  48. <button type="button" onClick={() => onNavigate?.(activity)} disabled={!onNavigate || !activity.detailRef}>
  49. <span className="retrievalActivityIndex">{String(index + 1).padStart(2, "0")}</span>
  50. <span className="retrievalActivityCopy"><strong>{activity.label}</strong>{activity.summary ? <small>{activity.summary}</small> : null}</span>
  51. <span className={`retrievalActivityStatus is-${activity.status}`}>{activity.resultCount != null && activity.resultCount > 0 ? `${activity.resultCount} 条` : STATE_LABELS[activity.status] || "查看详情"}</span>
  52. <ChevronRight aria-hidden="true" size={16} />
  53. </button>
  54. </li>)}
  55. </ol> : <p className="retrievalMissingNotice">{projection.missingActivityNotice || "本次运行没有保存可下钻的结构化查询记录。"}</p>}
  56. </section>
  57. </div>;
  58. }
  59. function RetrievalEvent({ projection }: { projection: InspectorDetailProjection }) {
  60. const outcome = projection.outcome || { state: "unknown", message: "现有记录不足以可靠判断查询结果。" };
  61. const conditions = projection.queryConditions || [];
  62. const items = projection.items || [];
  63. const completeness = projection.completeness;
  64. return <div className="retrievalInspector retrievalEventDetail">
  65. <OutcomePanel outcome={outcome} />
  66. <section className="inspectorPanel retrievalConditionPanel">
  67. <h3>{projection.sourceKind === "direct-tool" ? "调用输入" : "查询条件"}</h3>
  68. {conditions.length ? <dl className="retrievalConditions">
  69. {conditions.map((condition) => <div key={condition.key}><dt>{condition.label}</dt><dd><StructuredValue value={condition.value} /></dd></div>)}
  70. </dl> : <p className="retrievalMissingNotice">本次调用没有保存额外输入条件。</p>}
  71. </section>
  72. {items.length ? <section className="inspectorPanel retrievalResultsPanel">
  73. <div className="retrievalSectionHeader"><h3>{projection.sourceKind === "direct-tool" ? "读取结果" : "返回结果"}</h3><span>{items.length} 项</span></div>
  74. <div className="retrievalResultList">{items.map((item, index) => <ResultDisclosure key={`${item.id}-${index}`} item={item} defaultOpen={items.length === 1} />)}</div>
  75. </section> : null}
  76. {!completeness?.bodyAvailable ? <p className="retrievalCompletenessNotice is-warning">仅保存了摘要,完整返回不可用。</p> : null}
  77. {completeness?.truncated ? <p className="retrievalCompletenessNotice is-warning">返回内容超过安全展示上限,已省略 {completeness.omittedCharacters || "部分"} 个字符。</p> : null}
  78. </div>;
  79. }
  80. function OutcomePanel({ outcome }: { outcome: RetrievalOutcome }) {
  81. return <section className={`retrievalOutcome is-${outcome.state}`} aria-live="polite">
  82. <span>{STATE_LABELS[outcome.state] || "查询状态"}</span>
  83. <strong>{outcome.message}</strong>
  84. </section>;
  85. }
  86. function ResultDisclosure({ item, defaultOpen }: { item: RetrievalResultItem; defaultOpen: boolean }) {
  87. const safeHref = typeof item.href === "string" && /^https?:\/\//i.test(item.href) ? item.href : undefined;
  88. return <details className="retrievalResult" open={defaultOpen || undefined}>
  89. <summary>
  90. <span className="retrievalResultChevron"><ChevronRight aria-hidden="true" size={17} /></span>
  91. <span className="retrievalResultHeading"><strong>{item.title}</strong>{item.subtitle ? <small>{item.subtitle}</small> : null}</span>
  92. {item.meta?.length ? <span className="retrievalResultMeta">{item.meta.map((entry) => <b key={entry.label}>{entry.label}:{scalarText(entry.value)}</b>)}</span> : null}
  93. </summary>
  94. <div className="retrievalResultBody">
  95. {safeHref ? <a className="retrievalResultLink" href={safeHref} target="_blank" rel="noreferrer">查看原始内容<ExternalLink aria-hidden="true" size={14} /></a> : null}
  96. {item.sections.map((section, index) => <section className="retrievalResultSection" key={`${section.title}-${index}`}><h4>{section.title}</h4><StructuredValue value={section.value} /></section>)}
  97. </div>
  98. </details>;
  99. }
  100. function StructuredValue({ value }: { value: unknown }) {
  101. if (typeof value === "string") return <RichText value={value} />;
  102. if (value == null) return <span className="retrievalEmptyValue">未记录</span>;
  103. return <ValueView value={value} />;
  104. }
  105. function scalarText(value: unknown) {
  106. if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return String(value);
  107. return "已记录";
  108. }