| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221 |
- 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`;
- }
|