Files
UHD_Media_Tool/apps/web/app/components/downloads/business-timeline.tsx
T
rootandClaude Opus 5.5 1023642ef9 feat(web): W4-A wire console to real endpoints
- Agent page uses contracts (AgentRun/AgentRunDetail/AgentGoalReport) instead of a local
  mirror; ?mock=1 stays as an explicit demo switch; run snapshot refreshes on agent.run_status;
  stream auto-follow only while the user stays near the bottom (abort stays reachable)
- settings: Pi provider select incl. fake, new Agent runtime card (budgets, fakeScript)
- download trace uses /tasks?subjectId= and /organization-records?downloadTaskId= instead of
  time-window engine run matching and path matching; new business-task tab with cancel/retry
- workbench download dialog posts /acquisition-plans and shows per-slot rejection reasons
- preference tag revision history; subscription search by title/kind; identity/alias/subscription
  lists show entity titles; slot cards show current asset summary; policy preview sample titles
- DecisionSummary / IdentityRevisionList / ReleaseIdentityResult / TaskAccepted / PolicyPreviewSummary /
  OrganizePlanArtifactContent taken from contracts
- docs: SSE for console (cookie) vs GET /events?after=&limit= JSON replay for API keys

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 21:32:11 +08:00

69 lines
2.5 KiB
TypeScript

import { ExternalLink } from "lucide-react";
import { Link } from "react-router";
import type { TimelineEvent, TimelineStage } from "~/api/tasks.ts";
import { LoadingState } from "~/components/console/empty-state.tsx";
import { StatusBadge } from "~/components/console/status-badge.tsx";
import { Timeline } from "~/components/console/timeline.tsx";
// 业务时间线(参考 MovieClaw task-activity):把观察 → 计划 → 下载 → 整理 → Agent 串成一个业务过程,
// 阶段计数与首页/侧栏同口径;每条事件都有文字说明,不只靠颜色。
export const STAGE_LABEL: Record<TimelineStage, string> = {
observation: "观察",
plan: "计划",
download: "下载",
organization: "整理",
agent: "Agent",
engine: "任务",
};
const ORDER: TimelineStage[] = ["observation", "plan", "download", "organization", "agent", "engine"];
export function BusinessTimeline({ events, loading }: { events: TimelineEvent[]; loading?: boolean }) {
const counts = new Map<TimelineStage, number>();
for (const e of events) counts.set(e.stage, (counts.get(e.stage) ?? 0) + 1);
const ongoing = events.filter((e) => e.ongoing);
return (
<div className="space-y-3">
<div className="flex flex-wrap gap-1.5">
{ORDER.map((s) => {
const n = counts.get(s) ?? 0;
const bad = events.some((e) => e.stage === s && e.tone === "destructive");
return (
<StatusBadge key={s} tone={bad ? "destructive" : n ? "success" : "neutral"}>
{STAGE_LABEL[s]} {n ? `· ${n}` : "· 无"}
</StatusBadge>
);
})}
</div>
{ongoing.length ? (
<p className="text-caption text-muted-foreground">当前:{ongoing.map((e) => e.title).join(";")}</p>
) : null}
{loading && !events.length ? (
<LoadingState rows={3} />
) : (
<Timeline
items={events.map((e) => ({
id: e.key,
at: e.at,
tone: e.tone,
meta: STAGE_LABEL[e.stage],
title: e.href ? (
<Link to={e.href} className="inline-flex items-center gap-1 underline-offset-4 hover:underline">
{e.title}
<ExternalLink className="h-3 w-3" />
</Link>
) : (
e.title
),
description: e.detail ?? undefined,
}))}
/>
)}
{loading && events.length ? (
<p className="text-meta text-muted-foreground">正在补全关联的整理记录与引擎 run…</p>
) : null}
</div>
);
}