Skip to content

Commit 2bea527

Browse files
committed
Create a new compact horizontal span time UI
1 parent 0c6851e commit 2bea527

File tree

4 files changed

+103
-18
lines changed

4 files changed

+103
-18
lines changed

apps/webapp/app/components/runs/v3/PromptSpanDetails.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { useProject } from "~/hooks/useProject";
1010
import { v3PromptPath } from "~/utils/pathBuilder";
1111
import { TabButton, TabContainer } from "~/components/primitives/Tabs";
1212
import type { PromptSpanData } from "~/presenters/v3/SpanPresenter.server";
13+
import { SpanHorizontalTimeline } from "~/components/runs/v3/SpanHorizontalTimeline";
1314

1415
const StreamdownRenderer = lazy(() =>
1516
import("streamdown").then((mod) => ({
@@ -23,7 +24,15 @@ const StreamdownRenderer = lazy(() =>
2324

2425
type PromptTab = "overview" | "input" | "template";
2526

26-
export function PromptSpanDetails({ promptData }: { promptData: PromptSpanData }) {
27+
export function PromptSpanDetails({
28+
promptData,
29+
startTime,
30+
duration,
31+
}: {
32+
promptData: PromptSpanData;
33+
startTime?: string | Date;
34+
duration?: number | null;
35+
}) {
2736
const organization = useOrganization();
2837
const project = useProject();
2938
const environment = useEnvironment();
@@ -73,6 +82,7 @@ export function PromptSpanDetails({ promptData }: { promptData: PromptSpanData }
7382
<div className="scrollbar-gutter-stable min-h-0 flex-1 overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
7483
{tab === "overview" && (
7584
<div className="flex flex-col px-3">
85+
{startTime && <SpanHorizontalTimeline startTime={startTime} duration={duration ?? null} />}
7686
<div className="flex flex-col gap-1 py-2.5">
7787
<div className="flex flex-col text-xs @container">
7888
<MetricRow
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { DateTimeAccurate } from "~/components/primitives/DateTime";
2+
3+
function formatSpanDuration(nanoseconds: number): string {
4+
const ms = nanoseconds / 1_000_000;
5+
if (ms < 1000) return `${Math.round(ms)}ms`;
6+
if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`;
7+
const mins = Math.floor(ms / 60_000);
8+
const secs = ((ms % 60_000) / 1000).toFixed(0);
9+
return `${mins}m ${secs}s`;
10+
}
11+
12+
export function SpanHorizontalTimeline({
13+
startTime,
14+
duration,
15+
}: {
16+
startTime: string | Date;
17+
duration: number | null;
18+
}) {
19+
const startDate = startTime instanceof Date ? startTime : new Date(startTime);
20+
const endDate = duration != null ? new Date(startDate.getTime() + duration / 1_000_000) : null;
21+
22+
return (
23+
<div className="@container/timeline">
24+
<div className="flex flex-col gap-0.5 px-1 py-3">
25+
<div className="flex items-center justify-between">
26+
<span className="text-xs font-medium text-text-bright">Started</span>
27+
<span className="text-xs font-medium text-text-bright">Finished</span>
28+
</div>
29+
<div className="flex items-center">
30+
<span className="shrink-0 tabular-nums text-xxs text-text-dimmed @[350px]/timeline:text-xs">
31+
<DateTimeAccurate date={startDate} showTooltip={false} />
32+
</span>
33+
<div className="ml-2 h-3 w-px bg-charcoal-600" />
34+
<div className="h-px flex-1 bg-charcoal-600" />
35+
{duration != null && (
36+
<span className="shrink-0 tabular-nums px-2 text-xxs text-text-dimmed @[350px]/timeline:text-xs">
37+
{formatSpanDuration(duration)}
38+
</span>
39+
)}
40+
<div className="h-px flex-1 bg-charcoal-600" />
41+
<div className="mr-2 h-3 w-px bg-charcoal-600" />
42+
<span className="shrink-0 tabular-nums text-xxs text-text-dimmed @[350px]/timeline:text-xs">
43+
{endDate ? (
44+
<DateTimeAccurate date={endDate} previousDate={startDate} showTooltip={false} />
45+
) : (
46+
<span className="text-charcoal-500"></span>
47+
)}
48+
</span>
49+
</div>
50+
</div>
51+
</div>
52+
);
53+
}

apps/webapp/app/components/runs/v3/ai/AISpanDetails.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ import { useProject } from "~/hooks/useProject";
1313
import { useHasAdminAccess } from "~/hooks/useUser";
1414
import { v3PromptPath } from "~/utils/pathBuilder";
1515
import { CodeBlock } from "~/components/code/CodeBlock";
16-
import { AIChatMessages, AssistantResponse, ChatBubble } from "./AIChatMessages";
17-
import type { PromptLink } from "./AIChatMessages";
16+
import { AIChatMessages, AssistantResponse, ChatBubble, type PromptLink } from "./AIChatMessages";
1817
import { AIStatsSummary, AITagsRow } from "./AIModelSummary";
1918
import { AIToolsInventory } from "./AIToolsInventory";
2019
import type { AISpanData, DisplayItem } from "./types";
2120
import type { PromptSpanData } from "~/presenters/v3/SpanPresenter.server";
21+
import { SpanHorizontalTimeline } from "~/components/runs/v3/SpanHorizontalTimeline";
2222

2323
const StreamdownRenderer = lazy(() =>
2424
import("streamdown").then((mod) => ({
@@ -36,10 +36,14 @@ export function AISpanDetails({
3636
aiData,
3737
promptVersionData,
3838
rawProperties,
39+
startTime,
40+
duration,
3941
}: {
4042
aiData: AISpanData;
4143
promptVersionData?: PromptSpanData;
4244
rawProperties?: string;
45+
startTime?: string | Date;
46+
duration?: number | null;
4347
}) {
4448
const [tab, setTab] = useState<AITab>("overview");
4549
const isAdmin = useHasAdminAccess();
@@ -109,7 +113,12 @@ export function AISpanDetails({
109113

110114
{/* Tab content */}
111115
<div className="scrollbar-gutter-stable min-h-0 flex-1 overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
112-
{tab === "overview" && <OverviewTab aiData={aiData} />}
116+
{tab === "overview" && (
117+
<>
118+
{startTime && <div className="px-3"><SpanHorizontalTimeline startTime={startTime} duration={duration ?? null} /></div>}
119+
<OverviewTab aiData={aiData} />
120+
</>
121+
)}
113122
{tab === "messages" && <MessagesTab aiData={aiData} promptLink={promptLink} />}
114123
{tab === "tools" && <ToolsTab aiData={aiData} />}
115124
{tab === "prompt" && promptVersionData && (

apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import { TabButton, TabContainer } from "~/components/primitives/Tabs";
5555
import { TextLink } from "~/components/primitives/TextLink";
5656
import { InfoIconTooltip, SimpleTooltip } from "~/components/primitives/Tooltip";
5757
import { RunTimeline, RunTimelineEvent, SpanTimeline } from "~/components/run/RunTimeline";
58+
import { SpanHorizontalTimeline } from "~/components/runs/v3/SpanHorizontalTimeline";
5859
import { PacketDisplay } from "~/components/runs/v3/PacketDisplay";
5960
import { RunIcon } from "~/components/runs/v3/RunIcon";
6061
import { RunTag } from "~/components/runs/v3/RunTag";
@@ -289,17 +290,6 @@ function SpanBody({
289290
/>
290291
)}
291292
</div>
292-
{isAiInspector && (
293-
<div className="flex items-center gap-3 pb-1.5 pl-6 text-xs text-text-dimmed">
294-
<DateTime date={span.startTime} includeSeconds />
295-
{span.duration != null && (
296-
<>
297-
<span className="text-charcoal-600">/</span>
298-
<span className="text-text-bright">{formatSpanDuration(span.duration)}</span>
299-
</>
300-
)}
301-
</div>
302-
)}
303293
</div>
304294
{isAiInspector ? (
305295
<SpanEntity span={span} />
@@ -321,6 +311,7 @@ function formatSpanDuration(nanoseconds: number): string {
321311
return `${mins}m ${secs}s`;
322312
}
323313

314+
324315
function applySpanOverrides(span: Span, spanOverrides?: SpanOverride): Span {
325316
if (!spanOverrides) {
326317
return span;
@@ -1426,17 +1417,39 @@ function SpanEntity({ span }: { span: Span }) {
14261417
aiData={span.entity.object}
14271418
promptVersionData={span.entity.promptVersionData}
14281419
rawProperties={typeof span.properties === "string" ? span.properties : span.properties != null ? JSON.stringify(span.properties, null, 2) : undefined}
1420+
startTime={span.startTime}
1421+
duration={span.duration}
14291422
/>
14301423
);
14311424
}
14321425
case "ai-tool-call": {
1433-
return <AIToolCallSpanDetails data={span.entity.object} />;
1426+
return (
1427+
<div className="overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
1428+
<div className="px-3">
1429+
<SpanHorizontalTimeline startTime={span.startTime} duration={span.duration} />
1430+
</div>
1431+
<AIToolCallSpanDetails data={span.entity.object} />
1432+
</div>
1433+
);
14341434
}
14351435
case "ai-embed": {
1436-
return <AIEmbedSpanDetails data={span.entity.object} />;
1436+
return (
1437+
<div className="overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
1438+
<div className="px-3">
1439+
<SpanHorizontalTimeline startTime={span.startTime} duration={span.duration} />
1440+
</div>
1441+
<AIEmbedSpanDetails data={span.entity.object} />
1442+
</div>
1443+
);
14371444
}
14381445
case "prompt": {
1439-
return <PromptSpanDetails promptData={span.entity.object} />;
1446+
return (
1447+
<PromptSpanDetails
1448+
promptData={span.entity.object}
1449+
startTime={span.startTime}
1450+
duration={span.duration}
1451+
/>
1452+
);
14401453
}
14411454
default: {
14421455
assertNever(span.entity);

0 commit comments

Comments
 (0)