Files
wehub-resource-sync e30e75b5d4
Changesets / Create Version PR (push) Has been cancelled
Deploy Shadcn Registry / Deploy Production (push) Has been cancelled
Template Metrics / LOC + Bundle Size (push) Has been cancelled
Code Quality / Oxlint + Oxfmt (push) Has been cancelled
Code Quality / Template Sync (push) Has been cancelled
Code Quality / Build Changed Packages (push) Has been cancelled
Code Quality / Test Changed Packages (push) Has been cancelled
Deploy Expo Example / Deploy Production (push) Has been cancelled
Deploy Ink Example / Deploy Production (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.12) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.12) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:40:13 +08:00

90 lines
2.3 KiB
TypeScript

"use client";
import { useCallback, useEffect, useRef } from "react";
import { useAuiState } from "@assistant-ui/store";
import type { SpanItemState } from "@assistant-ui/react-o11y";
import { useWaterfallLayout } from "./waterfall-timeline";
const TYPE_COLORS: Record<string, string> = {
action: "hsl(221 83% 53%)",
api: "hsl(262 83% 58%)",
tool: "hsl(142 71% 45%)",
flow: "hsl(25 95% 53%)",
pipeline: "hsl(340 75% 55%)",
};
const FALLBACK_COLOR = "hsl(220 9% 46%)";
const STATUS_OPACITY: Record<SpanItemState["status"], number> = {
running: 0.7,
completed: 1,
failed: 1,
skipped: 0.5,
};
export function WaterfallBar() {
const { barWidth, timeRange, barHeight } = useWaterfallLayout();
const startedAt = useAuiState((s) => s.span.startedAt);
const endedAt = useAuiState((s) => s.span.endedAt) as number | null;
const status = useAuiState((s) => s.span.status) as SpanItemState["status"];
const type = useAuiState((s) => s.span.type);
const barRef = useRef<SVGRectElement>(null);
const scale = useCallback(
(t: number) => {
const range = timeRange.max - timeRange.min || 1;
return ((t - timeRange.min) / range) * barWidth;
},
[timeRange, barWidth],
);
const x = scale(startedAt);
useEffect(() => {
if (status !== "running") return;
let frameId: number;
const tick = () => {
const width = scale(Date.now()) - x;
barRef.current?.setAttribute("width", String(Math.max(0, width)));
frameId = requestAnimationFrame(tick);
};
frameId = requestAnimationFrame(tick);
return () => cancelAnimationFrame(frameId);
}, [status, x, scale]);
const rawWidth = endedAt ? scale(endedAt) - x : 0;
const width = Math.max(rawWidth, 4);
const fill = TYPE_COLORS[type] ?? FALLBACK_COLOR;
const opacity = STATUS_OPACITY[status];
return (
<g>
<rect
ref={barRef}
x={x}
y={4}
width={width}
height={barHeight - 8}
rx={3}
fill={fill}
opacity={opacity}
className={status === "running" ? "animate-pulse" : ""}
/>
{status === "failed" && (
<rect
x={x}
y={4}
width={width}
height={barHeight - 8}
rx={3}
fill="none"
stroke="hsl(0 84% 60%)"
strokeWidth={2}
/>
)}
</g>
);
}