|
|
@@ -0,0 +1,221 @@
|
|
|
+import {
|
|
|
+ ArrowDown,
|
|
|
+ Brain,
|
|
|
+ CheckCircle2,
|
|
|
+ CircleDot,
|
|
|
+ Database,
|
|
|
+ GitBranch,
|
|
|
+ Play,
|
|
|
+ RefreshCcw,
|
|
|
+ Route,
|
|
|
+ SearchCheck,
|
|
|
+ Wrench,
|
|
|
+} from "lucide-react";
|
|
|
+
|
|
|
+import { buildTaskCycles, groupParallelSteps, statusLabel } from "@/lib/journey";
|
|
|
+import type { JourneyEdge, JourneyStep, JourneyView, StepType, ZoomLevel } from "@/lib/types";
|
|
|
+
|
|
|
+const STEP_META: Record<StepType, { label: string; icon: typeof Brain }> = {
|
|
|
+ plan: { label: "规划", icon: Route },
|
|
|
+ execute: { label: "执行", icon: Play },
|
|
|
+ validate: { label: "验收", icon: SearchCheck },
|
|
|
+ decide: { label: "决策", icon: GitBranch },
|
|
|
+};
|
|
|
+
|
|
|
+interface JourneyCanvasProps {
|
|
|
+ journey: JourneyView;
|
|
|
+ zoom: ZoomLevel;
|
|
|
+ selectedStepId: string | null;
|
|
|
+ onSelectStep: (stepId: string) => void;
|
|
|
+}
|
|
|
+
|
|
|
+export function JourneyCanvas({ journey, zoom, selectedStepId, onSelectStep }: JourneyCanvasProps) {
|
|
|
+ if (zoom === "journey") {
|
|
|
+ return <JourneyOverview journey={journey} onSelectStep={onSelectStep} />;
|
|
|
+ }
|
|
|
+ const groups = groupParallelSteps(journey.steps);
|
|
|
+ const loops = new Map(
|
|
|
+ journey.edges.filter((edge) => edge.relation === "loop").map((edge) => [edge.source, edge]),
|
|
|
+ );
|
|
|
+ return (
|
|
|
+ <section className="flow-canvas" aria-label="创作步骤">
|
|
|
+ <div className="flow-heading">
|
|
|
+ <div>
|
|
|
+ <span>按真实发生顺序</span>
|
|
|
+ <h2>Planner 如何一步步完成目标</h2>
|
|
|
+ </div>
|
|
|
+ <p>点击任何步骤后切换到“证据”,可查看对应的合同、工具调用和验收记录。</p>
|
|
|
+ </div>
|
|
|
+ <div className="causal-spine">
|
|
|
+ {groups.map((group, index) => (
|
|
|
+ <div className={`flow-unit ${group.kind}`} key={group.id}>
|
|
|
+ {group.kind === "parallel" ? (
|
|
|
+ <div className="parallel-frame">
|
|
|
+ <div className="parallel-heading">
|
|
|
+ <GitBranch size={16} />
|
|
|
+ <span>Host 并行派发 {group.steps.length} 个任务</span>
|
|
|
+ </div>
|
|
|
+ <div className="parallel-lanes">
|
|
|
+ {group.steps.map((step) => (
|
|
|
+ <StepCard
|
|
|
+ key={step.step_id}
|
|
|
+ step={step}
|
|
|
+ loop={loops.get(step.step_id)}
|
|
|
+ selected={step.step_id === selectedStepId}
|
|
|
+ onSelect={() => onSelectStep(step.step_id)}
|
|
|
+ />
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ ) : (
|
|
|
+ <StepCard
|
|
|
+ step={group.steps[0]}
|
|
|
+ loop={loops.get(group.steps[0].step_id)}
|
|
|
+ selected={group.steps[0].step_id === selectedStepId}
|
|
|
+ onSelect={() => onSelectStep(group.steps[0].step_id)}
|
|
|
+ />
|
|
|
+ )}
|
|
|
+ {index < groups.length - 1 ? (
|
|
|
+ <span className="flow-connector" aria-hidden="true">
|
|
|
+ <ArrowDown size={15} />
|
|
|
+ </span>
|
|
|
+ ) : null}
|
|
|
+ </div>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ {journey.warnings.map((warning) => (
|
|
|
+ <p className="projection-note" key={warning}>{warning}</p>
|
|
|
+ ))}
|
|
|
+ </section>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+function JourneyOverview({
|
|
|
+ journey,
|
|
|
+ onSelectStep,
|
|
|
+}: {
|
|
|
+ journey: JourneyView;
|
|
|
+ onSelectStep: (stepId: string) => void;
|
|
|
+}) {
|
|
|
+ const cycles = buildTaskCycles(journey.steps, journey.edges);
|
|
|
+ return (
|
|
|
+ <section className="overview-canvas" aria-label="全局创作旅程">
|
|
|
+ <div className="overview-heading">
|
|
|
+ <h2>这次创作经过了 {cycles.length} 个任务回合</h2>
|
|
|
+ <p>默认只看业务因果。点开一个回合,再看 Planner、Worker 和 Validator 如何协作。</p>
|
|
|
+ </div>
|
|
|
+ <ol className="cycle-line">
|
|
|
+ {cycles.map((cycle, index) => (
|
|
|
+ <li key={cycle.taskId}>
|
|
|
+ <button type="button" onClick={() => onSelectStep(cycle.steps[0].step_id)}>
|
|
|
+ <span className="cycle-index">{String(index + 1).padStart(2, "0")}</span>
|
|
|
+ <div className="cycle-copy">
|
|
|
+ <div className="cycle-title-row">
|
|
|
+ <h3>{cycle.title}</h3>
|
|
|
+ <span className={`status-chip status-${cycle.status}`}>{statusLabel(cycle.status)}</span>
|
|
|
+ </div>
|
|
|
+ <p>{cycle.steps[0].reasoning}</p>
|
|
|
+ <div className="cycle-facts">
|
|
|
+ {cycle.goals.length ? <span>目标 {cycle.goals.join("、")}</span> : null}
|
|
|
+ <span>{cycle.steps.length} 个真实步骤</span>
|
|
|
+ {cycle.parallel ? <span><GitBranch size={12} /> 包含并行</span> : null}
|
|
|
+ {cycle.loopCount ? <span><RefreshCcw size={12} /> 返工 {cycle.loopCount} 次</span> : null}
|
|
|
+ </div>
|
|
|
+ <div className="cycle-lifecycle" aria-label="回合生命周期">
|
|
|
+ {(["plan", "execute", "validate", "decide"] as StepType[]).map((type) => {
|
|
|
+ const present = cycle.steps.some((step) => step.step_type === type);
|
|
|
+ const Icon = STEP_META[type].icon;
|
|
|
+ return (
|
|
|
+ <span className={present ? "present" : ""} key={type}>
|
|
|
+ <Icon size={13} /> {STEP_META[type].label}
|
|
|
+ </span>
|
|
|
+ );
|
|
|
+ })}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <ArrowDown className="cycle-open" size={17} />
|
|
|
+ </button>
|
|
|
+ </li>
|
|
|
+ ))}
|
|
|
+ </ol>
|
|
|
+ </section>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+function StepCard({
|
|
|
+ step,
|
|
|
+ loop,
|
|
|
+ selected,
|
|
|
+ onSelect,
|
|
|
+}: {
|
|
|
+ step: JourneyStep;
|
|
|
+ loop?: JourneyEdge;
|
|
|
+ selected: boolean;
|
|
|
+ onSelect: () => void;
|
|
|
+}) {
|
|
|
+ const meta = STEP_META[step.step_type];
|
|
|
+ const Icon = meta.icon;
|
|
|
+ return (
|
|
|
+ <article className={`step-node type-${step.step_type} ${selected ? "selected" : ""}`}>
|
|
|
+ <button type="button" className="step-hit-area" onClick={onSelect} aria-label={`查看${step.title}的原始证据`} />
|
|
|
+ <header className="step-header">
|
|
|
+ <span className="step-type"><Icon size={15} /> {meta.label}</span>
|
|
|
+ <span className={`status-chip status-${step.status}`}>{statusLabel(step.status)}</span>
|
|
|
+ </header>
|
|
|
+ <h3>{step.title}</h3>
|
|
|
+
|
|
|
+ <div className="step-layer reasoning-layer">
|
|
|
+ <span><Brain size={14} /> 思考</span>
|
|
|
+ <p>{step.reasoning}</p>
|
|
|
+ <small>{step.reasoning_source === "observable" ? "Planner / Agent 主动说明" : "由真实冻结记录转述"}</small>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="step-layer process-layer">
|
|
|
+ <span><Wrench size={14} /> 动作</span>
|
|
|
+ <p>{step.process.label}</p>
|
|
|
+ <small>{step.process.actor}{step.process.tool_name ? ` · ${step.process.tool_name}` : ""}</small>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="data-flow">
|
|
|
+ <div>
|
|
|
+ <span><Database size={13} /> 输入</span>
|
|
|
+ {step.input_data.map((item, index) => <b key={`${item.label}-${index}`}>{item.label}</b>)}
|
|
|
+ </div>
|
|
|
+ <ArrowDown size={14} aria-hidden="true" />
|
|
|
+ <div>
|
|
|
+ <span><CircleDot size={13} /> 产出</span>
|
|
|
+ {step.output_data.map((item, index) => <b key={`${item.label}-${index}`}>{item.label}</b>)}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {step.contract ? (
|
|
|
+ <div className="contract-summary">
|
|
|
+ <strong>任务合同</strong>
|
|
|
+ <span>{step.contract.goals.length ? `覆盖 ${step.contract.goals.join("、")}` : "不限定单一 Goal"}</span>
|
|
|
+ <span>产出 {step.contract.output}</span>
|
|
|
+ <span>{step.contract.criteria.length} 项验收标准</span>
|
|
|
+ </div>
|
|
|
+ ) : null}
|
|
|
+
|
|
|
+ {step.parallel_total ? (
|
|
|
+ <span className="parallel-badge">并行 {step.parallel_position}/{step.parallel_total}</span>
|
|
|
+ ) : null}
|
|
|
+ {loop ? (
|
|
|
+ <div className="loop-ribbon"><RefreshCcw size={14} /> {loop.label}·回到前面继续</div>
|
|
|
+ ) : null}
|
|
|
+ <footer className="step-metrics">
|
|
|
+ {step.duration_ms !== null ? <span>{formatDuration(step.duration_ms)}</span> : null}
|
|
|
+ {step.tokens !== null ? <span>{step.tokens.toLocaleString()} tokens</span> : null}
|
|
|
+ {step.cost !== null ? <span>cost {step.cost.toFixed(4)}</span> : null}
|
|
|
+ {step.model ? <span>{step.model}</span> : null}
|
|
|
+ <span className="evidence-cue"><CheckCircle2 size={13} /> 可追溯</span>
|
|
|
+ </footer>
|
|
|
+ </article>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+function formatDuration(value: number): string {
|
|
|
+ if (value < 1000) return `${value}ms`;
|
|
|
+ if (value < 60_000) return `${(value / 1000).toFixed(1)}s`;
|
|
|
+ return `${Math.floor(value / 60_000)}m ${Math.round((value % 60_000) / 1000)}s`;
|
|
|
+}
|