RunWalkPage.tsx 3.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. "use client";
  2. import { useCallback, useEffect, useMemo, useState } from "react";
  3. import { AppShell } from "@/components/AppShell";
  4. import { ErrorState, LoadingState } from "@/components/StateBlocks";
  5. import { getRunWalk } from "@/lib/api/client";
  6. import type { FlowLedgerWalkResponse, RawRecord } from "@/lib/api/types";
  7. import { asRecord, text } from "@/lib/flow-ledger/format";
  8. import { WalkTree } from "@/features/walk/WalkTree";
  9. export function RunWalkPage({ runId }: { runId: string }) {
  10. const [data, setData] = useState<FlowLedgerWalkResponse | null>(null);
  11. const [loading, setLoading] = useState(true);
  12. const [error, setError] = useState<string | null>(null);
  13. const load = useCallback(async () => {
  14. setLoading(true);
  15. setError(null);
  16. try {
  17. const debug = new URLSearchParams(window.location.search).get("debug") === "1";
  18. setData(await getRunWalk(runId, debug));
  19. } catch (err) {
  20. setError(err instanceof Error ? err.message : String(err));
  21. } finally {
  22. setLoading(false);
  23. }
  24. }, [runId]);
  25. useEffect(() => {
  26. void load();
  27. }, [load]);
  28. const summary = asRecord(data?.summary);
  29. const actions = useMemo(
  30. () => (data?.actions || []).filter((a): a is RawRecord => typeof a === "object" && a !== null),
  31. [data?.actions]
  32. );
  33. const queryLabels = useMemo(() => {
  34. const raw = asRecord(summary.query_labels);
  35. const out: Record<string, string> = {};
  36. for (const key of Object.keys(raw)) out[key] = text(raw[key], "");
  37. return out;
  38. }, [summary]);
  39. const budget = useMemo(() => budgetStats(summary), [summary]);
  40. return (
  41. <AppShell
  42. title="全 run 游走图"
  43. subtitle="整条 run 从命中视频向外游走的全部路径"
  44. runId={runId}
  45. showBack
  46. onRefresh={load}
  47. >
  48. {loading ? <LoadingState /> : null}
  49. {error ? <ErrorState message={error} /> : null}
  50. {data && !loading && !error ? (
  51. <>
  52. <header className="wt-overview">
  53. <div className="wt-overview-head">
  54. <span className="wt-root-chip">游走预算</span>
  55. <strong>本 run 总览</strong>
  56. </div>
  57. <div className="wt-overview-stats">
  58. {budget.map((stat) => (
  59. <span className={`wt-stat ${stat.tone || ""}`} key={stat.label}>
  60. <b>{stat.value}</b>
  61. {stat.label}
  62. </span>
  63. ))}
  64. </div>
  65. </header>
  66. <WalkTree
  67. actions={actions}
  68. runId={runId}
  69. groupByQuery
  70. queryLabels={queryLabels}
  71. emptyLabel="本 run 没有任何向外游走"
  72. />
  73. </>
  74. ) : null}
  75. </AppShell>
  76. );
  77. }
  78. function fmtUsed(used: unknown, cap: unknown): string {
  79. const u = Number(used) || 0;
  80. if (cap === null || cap === undefined || cap === "") return String(u);
  81. return `${u} / ${Number(cap) || 0}`;
  82. }
  83. function budgetStats(summary: RawRecord): { label: string; value: string; tone?: string }[] {
  84. return [
  85. { label: "游走到的视频", value: String(Number(summary.walk_video_total) || 0), tone: "go" },
  86. { label: "标签→新搜索", value: fmtUsed(summary.tag_used, summary.tag_cap) },
  87. { label: "作者→作品", value: fmtUsed(summary.author_used, summary.author_cap) },
  88. { label: "最深层数", value: fmtUsed(summary.max_depth_reached, summary.max_depth_cap) }
  89. ];
  90. }