@assistant-ui/mcp-docs-server 0.1.22 → 0.1.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.docs/organized/code-examples/waterfall.md +801 -0
- package/.docs/organized/code-examples/with-ag-ui.md +39 -27
- package/.docs/organized/code-examples/with-ai-sdk-v6.md +39 -29
- package/.docs/organized/code-examples/with-artifacts.md +467 -0
- package/.docs/organized/code-examples/with-assistant-transport.md +32 -25
- package/.docs/organized/code-examples/with-chain-of-thought.md +42 -33
- package/.docs/organized/code-examples/with-cloud-standalone.md +674 -0
- package/.docs/organized/code-examples/with-cloud.md +35 -28
- package/.docs/organized/code-examples/with-custom-thread-list.md +35 -28
- package/.docs/organized/code-examples/with-elevenlabs-scribe.md +42 -31
- package/.docs/organized/code-examples/with-expo.md +2012 -0
- package/.docs/organized/code-examples/with-external-store.md +32 -26
- package/.docs/organized/code-examples/with-ffmpeg.md +32 -28
- package/.docs/organized/code-examples/with-langgraph.md +97 -39
- package/.docs/organized/code-examples/with-parent-id-grouping.md +33 -26
- package/.docs/organized/code-examples/with-react-hook-form.md +63 -61
- package/.docs/organized/code-examples/with-react-router.md +38 -31
- package/.docs/organized/code-examples/with-store.md +17 -25
- package/.docs/organized/code-examples/with-tanstack.md +36 -26
- package/.docs/organized/code-examples/with-tap-runtime.md +11 -25
- package/.docs/raw/docs/(docs)/cli.mdx +13 -6
- package/.docs/raw/docs/(docs)/guides/attachments.mdx +26 -3
- package/.docs/raw/docs/(docs)/guides/chain-of-thought.mdx +5 -5
- package/.docs/raw/docs/(docs)/guides/context-api.mdx +53 -52
- package/.docs/raw/docs/(docs)/guides/dictation.mdx +0 -2
- package/.docs/raw/docs/(docs)/guides/message-timing.mdx +169 -0
- package/.docs/raw/docs/(docs)/guides/quoting.mdx +327 -0
- package/.docs/raw/docs/(docs)/guides/speech.mdx +0 -1
- package/.docs/raw/docs/(docs)/index.mdx +12 -2
- package/.docs/raw/docs/(docs)/installation.mdx +8 -2
- package/.docs/raw/docs/(docs)/llm.mdx +9 -7
- package/.docs/raw/docs/(reference)/api-reference/primitives/action-bar-more.mdx +1 -1
- package/.docs/raw/docs/(reference)/api-reference/primitives/action-bar.mdx +2 -2
- package/.docs/raw/docs/(reference)/api-reference/primitives/assistant-if.mdx +27 -27
- package/.docs/raw/docs/(reference)/api-reference/primitives/composer.mdx +60 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/message-part.mdx +78 -4
- package/.docs/raw/docs/(reference)/api-reference/primitives/message.mdx +32 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/selection-toolbar.mdx +61 -0
- package/.docs/raw/docs/(reference)/api-reference/primitives/thread.mdx +1 -1
- package/.docs/raw/docs/(reference)/legacy/styled/assistant-modal.mdx +1 -6
- package/.docs/raw/docs/(reference)/legacy/styled/decomposition.mdx +2 -2
- package/.docs/raw/docs/(reference)/legacy/styled/markdown.mdx +1 -6
- package/.docs/raw/docs/(reference)/legacy/styled/thread.mdx +1 -5
- package/.docs/raw/docs/(reference)/migrations/v0-12.mdx +17 -17
- package/.docs/raw/docs/cloud/ai-sdk-assistant-ui.mdx +209 -0
- package/.docs/raw/docs/cloud/ai-sdk.mdx +296 -0
- package/.docs/raw/docs/cloud/authorization.mdx +178 -79
- package/.docs/raw/docs/cloud/{persistence/langgraph.mdx → langgraph.mdx} +2 -2
- package/.docs/raw/docs/cloud/overview.mdx +29 -39
- package/.docs/raw/docs/react-native/adapters.mdx +118 -0
- package/.docs/raw/docs/react-native/custom-backend.mdx +210 -0
- package/.docs/raw/docs/react-native/hooks.mdx +364 -0
- package/.docs/raw/docs/react-native/index.mdx +332 -0
- package/.docs/raw/docs/react-native/primitives.mdx +653 -0
- package/.docs/raw/docs/runtimes/ai-sdk/v6.mdx +60 -15
- package/.docs/raw/docs/runtimes/assistant-transport.mdx +103 -0
- package/.docs/raw/docs/runtimes/custom/external-store.mdx +25 -2
- package/.docs/raw/docs/runtimes/data-stream.mdx +1 -3
- package/.docs/raw/docs/runtimes/langgraph/index.mdx +113 -9
- package/.docs/raw/docs/runtimes/pick-a-runtime.mdx +1 -4
- package/.docs/raw/docs/ui/attachment.mdx +4 -2
- package/.docs/raw/docs/ui/context-display.mdx +147 -0
- package/.docs/raw/docs/ui/message-timing.mdx +92 -0
- package/.docs/raw/docs/ui/part-grouping.mdx +1 -1
- package/.docs/raw/docs/ui/reasoning.mdx +4 -4
- package/.docs/raw/docs/ui/scrollbar.mdx +2 -2
- package/.docs/raw/docs/ui/syntax-highlighting.mdx +55 -50
- package/.docs/raw/docs/ui/thread.mdx +16 -9
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/tools/tests/integration.test.ts +2 -2
- package/src/tools/tests/json-parsing.test.ts +1 -1
- package/src/tools/tests/mcp-protocol.test.ts +1 -3
- package/.docs/raw/docs/cloud/persistence/ai-sdk.mdx +0 -108
|
@@ -0,0 +1,801 @@
|
|
|
1
|
+
# Example: waterfall
|
|
2
|
+
|
|
3
|
+
## app/globals.css
|
|
4
|
+
|
|
5
|
+
```css
|
|
6
|
+
@import "tailwindcss";
|
|
7
|
+
|
|
8
|
+
:root {
|
|
9
|
+
--background: #ffffff;
|
|
10
|
+
--foreground: #171717;
|
|
11
|
+
--muted-foreground: #737373;
|
|
12
|
+
--border: #e5e5e5;
|
|
13
|
+
--accent: #f5f5f5;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@theme inline {
|
|
17
|
+
--color-background: var(--background);
|
|
18
|
+
--color-foreground: var(--foreground);
|
|
19
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
20
|
+
--color-border: var(--border);
|
|
21
|
+
--color-accent: var(--accent);
|
|
22
|
+
--font-sans: var(--font-geist-sans);
|
|
23
|
+
--font-mono: var(--font-geist-mono);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@media (prefers-color-scheme: dark) {
|
|
27
|
+
:root {
|
|
28
|
+
--background: #0a0a0a;
|
|
29
|
+
--foreground: #ededed;
|
|
30
|
+
--muted-foreground: #a3a3a3;
|
|
31
|
+
--border: #2e2e2e;
|
|
32
|
+
--accent: #1a1a1a;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
body {
|
|
37
|
+
background: var(--background);
|
|
38
|
+
color: var(--foreground);
|
|
39
|
+
font-family: Arial, Helvetica, sans-serif;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## app/layout.tsx
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
import type { Metadata } from "next";
|
|
48
|
+
import { Geist, Geist_Mono } from "next/font/google";
|
|
49
|
+
import "./globals.css";
|
|
50
|
+
|
|
51
|
+
const geistSans = Geist({
|
|
52
|
+
variable: "--font-geist-sans",
|
|
53
|
+
subsets: ["latin"],
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const geistMono = Geist_Mono({
|
|
57
|
+
variable: "--font-geist-mono",
|
|
58
|
+
subsets: ["latin"],
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
export const metadata: Metadata = {
|
|
62
|
+
title: "Waterfall Example",
|
|
63
|
+
description: "Span waterfall visualization with @assistant-ui/react-o11y",
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export default function RootLayout({
|
|
67
|
+
children,
|
|
68
|
+
}: Readonly<{
|
|
69
|
+
children: React.ReactNode;
|
|
70
|
+
}>) {
|
|
71
|
+
return (
|
|
72
|
+
<html lang="en">
|
|
73
|
+
<body
|
|
74
|
+
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
|
75
|
+
>
|
|
76
|
+
{children}
|
|
77
|
+
</body>
|
|
78
|
+
</html>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## app/page.tsx
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
import { WaterfallPage } from "@/lib/waterfall-page";
|
|
88
|
+
|
|
89
|
+
export default function Home() {
|
|
90
|
+
return (
|
|
91
|
+
<div className="min-h-screen bg-background p-8">
|
|
92
|
+
<div className="mx-auto max-w-6xl">
|
|
93
|
+
<div className="mb-8">
|
|
94
|
+
<h1 className="mb-2 font-bold text-4xl text-foreground">
|
|
95
|
+
Waterfall Timeline
|
|
96
|
+
</h1>
|
|
97
|
+
<p className="text-lg text-muted-foreground">
|
|
98
|
+
Span visualization powered by @assistant-ui/react-o11y
|
|
99
|
+
</p>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<WaterfallPage />
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## lib/mock-data.ts
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
import type { SpanData } from "@assistant-ui/react-o11y";
|
|
114
|
+
|
|
115
|
+
const BASE = Date.now() - 5000;
|
|
116
|
+
|
|
117
|
+
export const mockSpans: SpanData[] = [
|
|
118
|
+
{
|
|
119
|
+
id: "span-1",
|
|
120
|
+
parentSpanId: null,
|
|
121
|
+
name: "POST /api/chat",
|
|
122
|
+
type: "api",
|
|
123
|
+
status: "completed",
|
|
124
|
+
startedAt: BASE,
|
|
125
|
+
endedAt: BASE + 4200,
|
|
126
|
+
latencyMs: 4200,
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
id: "span-2",
|
|
130
|
+
parentSpanId: "span-1",
|
|
131
|
+
name: "authenticate",
|
|
132
|
+
type: "action",
|
|
133
|
+
status: "completed",
|
|
134
|
+
startedAt: BASE + 50,
|
|
135
|
+
endedAt: BASE + 200,
|
|
136
|
+
latencyMs: 150,
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: "span-3",
|
|
140
|
+
parentSpanId: "span-1",
|
|
141
|
+
name: "agent-pipeline",
|
|
142
|
+
type: "pipeline",
|
|
143
|
+
status: "completed",
|
|
144
|
+
startedAt: BASE + 220,
|
|
145
|
+
endedAt: BASE + 4100,
|
|
146
|
+
latencyMs: 3880,
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
id: "span-4",
|
|
150
|
+
parentSpanId: "span-3",
|
|
151
|
+
name: "retrieve-context",
|
|
152
|
+
type: "tool",
|
|
153
|
+
status: "completed",
|
|
154
|
+
startedAt: BASE + 250,
|
|
155
|
+
endedAt: BASE + 900,
|
|
156
|
+
latencyMs: 650,
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
id: "span-5",
|
|
160
|
+
parentSpanId: "span-3",
|
|
161
|
+
name: "llm-generate",
|
|
162
|
+
type: "action",
|
|
163
|
+
status: "completed",
|
|
164
|
+
startedAt: BASE + 920,
|
|
165
|
+
endedAt: BASE + 2800,
|
|
166
|
+
latencyMs: 1880,
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
id: "span-6",
|
|
170
|
+
parentSpanId: "span-3",
|
|
171
|
+
name: "web-search",
|
|
172
|
+
type: "tool",
|
|
173
|
+
status: "completed",
|
|
174
|
+
startedAt: BASE + 2850,
|
|
175
|
+
endedAt: BASE + 3500,
|
|
176
|
+
latencyMs: 650,
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
id: "span-7",
|
|
180
|
+
parentSpanId: "span-3",
|
|
181
|
+
name: "llm-summarize",
|
|
182
|
+
type: "action",
|
|
183
|
+
status: "completed",
|
|
184
|
+
startedAt: BASE + 3520,
|
|
185
|
+
endedAt: BASE + 4050,
|
|
186
|
+
latencyMs: 530,
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
id: "span-8",
|
|
190
|
+
parentSpanId: "span-1",
|
|
191
|
+
name: "log-response",
|
|
192
|
+
type: "action",
|
|
193
|
+
status: "completed",
|
|
194
|
+
startedAt: BASE + 4110,
|
|
195
|
+
endedAt: BASE + 4180,
|
|
196
|
+
latencyMs: 70,
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
id: "span-9",
|
|
200
|
+
parentSpanId: null,
|
|
201
|
+
name: "POST /api/feedback",
|
|
202
|
+
type: "api",
|
|
203
|
+
status: "completed",
|
|
204
|
+
startedAt: BASE + 4500,
|
|
205
|
+
endedAt: BASE + 4700,
|
|
206
|
+
latencyMs: 200,
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
id: "span-10",
|
|
210
|
+
parentSpanId: "span-9",
|
|
211
|
+
name: "validate-input",
|
|
212
|
+
type: "action",
|
|
213
|
+
status: "completed",
|
|
214
|
+
startedAt: BASE + 4520,
|
|
215
|
+
endedAt: BASE + 4560,
|
|
216
|
+
latencyMs: 40,
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
id: "span-11",
|
|
220
|
+
parentSpanId: "span-9",
|
|
221
|
+
name: "store-feedback",
|
|
222
|
+
type: "action",
|
|
223
|
+
status: "failed",
|
|
224
|
+
startedAt: BASE + 4570,
|
|
225
|
+
endedAt: BASE + 4680,
|
|
226
|
+
latencyMs: 110,
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
id: "span-12",
|
|
230
|
+
parentSpanId: null,
|
|
231
|
+
name: "GET /api/status",
|
|
232
|
+
type: "api",
|
|
233
|
+
status: "running",
|
|
234
|
+
startedAt: BASE + 4800,
|
|
235
|
+
endedAt: null,
|
|
236
|
+
latencyMs: null,
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
id: "span-13",
|
|
240
|
+
parentSpanId: "span-12",
|
|
241
|
+
name: "health-check",
|
|
242
|
+
type: "flow",
|
|
243
|
+
status: "running",
|
|
244
|
+
startedAt: BASE + 4830,
|
|
245
|
+
endedAt: null,
|
|
246
|
+
latencyMs: null,
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
id: "span-14",
|
|
250
|
+
parentSpanId: "span-4",
|
|
251
|
+
name: "vector-search",
|
|
252
|
+
type: "tool",
|
|
253
|
+
status: "completed",
|
|
254
|
+
startedAt: BASE + 280,
|
|
255
|
+
endedAt: BASE + 700,
|
|
256
|
+
latencyMs: 420,
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
id: "span-15",
|
|
260
|
+
parentSpanId: "span-4",
|
|
261
|
+
name: "rerank",
|
|
262
|
+
type: "action",
|
|
263
|
+
status: "completed",
|
|
264
|
+
startedAt: BASE + 710,
|
|
265
|
+
endedAt: BASE + 880,
|
|
266
|
+
latencyMs: 170,
|
|
267
|
+
},
|
|
268
|
+
];
|
|
269
|
+
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## lib/waterfall-bar.tsx
|
|
273
|
+
|
|
274
|
+
```tsx
|
|
275
|
+
"use client";
|
|
276
|
+
|
|
277
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
278
|
+
import { useAuiState } from "@assistant-ui/store";
|
|
279
|
+
import type { SpanItemState } from "@assistant-ui/react-o11y";
|
|
280
|
+
import { useWaterfallLayout } from "./waterfall-timeline";
|
|
281
|
+
|
|
282
|
+
const TYPE_COLORS: Record<string, string> = {
|
|
283
|
+
action: "hsl(221 83% 53%)",
|
|
284
|
+
api: "hsl(262 83% 58%)",
|
|
285
|
+
tool: "hsl(142 71% 45%)",
|
|
286
|
+
flow: "hsl(25 95% 53%)",
|
|
287
|
+
pipeline: "hsl(340 75% 55%)",
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const FALLBACK_COLOR = "hsl(220 9% 46%)";
|
|
291
|
+
|
|
292
|
+
const STATUS_OPACITY: Record<SpanItemState["status"], number> = {
|
|
293
|
+
running: 0.7,
|
|
294
|
+
completed: 1,
|
|
295
|
+
failed: 1,
|
|
296
|
+
skipped: 0.5,
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
export function WaterfallBar() {
|
|
300
|
+
const { barWidth, timeRange, barHeight } = useWaterfallLayout();
|
|
301
|
+
const startedAt = useAuiState((s) => s.span.startedAt);
|
|
302
|
+
const endedAt = useAuiState((s) => s.span.endedAt) as number | null;
|
|
303
|
+
const status = useAuiState((s) => s.span.status) as SpanItemState["status"];
|
|
304
|
+
const type = useAuiState((s) => s.span.type);
|
|
305
|
+
|
|
306
|
+
const barRef = useRef<SVGRectElement>(null);
|
|
307
|
+
|
|
308
|
+
const scale = useCallback(
|
|
309
|
+
(t: number) => {
|
|
310
|
+
const range = timeRange.max - timeRange.min || 1;
|
|
311
|
+
return ((t - timeRange.min) / range) * barWidth;
|
|
312
|
+
},
|
|
313
|
+
[timeRange, barWidth],
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
const x = scale(startedAt);
|
|
317
|
+
|
|
318
|
+
useEffect(() => {
|
|
319
|
+
if (status !== "running") return;
|
|
320
|
+
|
|
321
|
+
let frameId: number;
|
|
322
|
+
const tick = () => {
|
|
323
|
+
const width = scale(Date.now()) - x;
|
|
324
|
+
barRef.current?.setAttribute("width", String(Math.max(0, width)));
|
|
325
|
+
frameId = requestAnimationFrame(tick);
|
|
326
|
+
};
|
|
327
|
+
frameId = requestAnimationFrame(tick);
|
|
328
|
+
return () => cancelAnimationFrame(frameId);
|
|
329
|
+
}, [status, x, scale]);
|
|
330
|
+
|
|
331
|
+
const rawWidth = endedAt ? scale(endedAt) - x : 0;
|
|
332
|
+
const width = Math.max(rawWidth, 4);
|
|
333
|
+
const fill = TYPE_COLORS[type] ?? FALLBACK_COLOR;
|
|
334
|
+
const opacity = STATUS_OPACITY[status];
|
|
335
|
+
|
|
336
|
+
return (
|
|
337
|
+
<g>
|
|
338
|
+
<rect
|
|
339
|
+
ref={barRef}
|
|
340
|
+
x={x}
|
|
341
|
+
y={4}
|
|
342
|
+
width={width}
|
|
343
|
+
height={barHeight - 8}
|
|
344
|
+
rx={3}
|
|
345
|
+
fill={fill}
|
|
346
|
+
opacity={opacity}
|
|
347
|
+
className={status === "running" ? "animate-pulse" : ""}
|
|
348
|
+
/>
|
|
349
|
+
{status === "failed" && (
|
|
350
|
+
<rect
|
|
351
|
+
x={x}
|
|
352
|
+
y={4}
|
|
353
|
+
width={width}
|
|
354
|
+
height={barHeight - 8}
|
|
355
|
+
rx={3}
|
|
356
|
+
fill="none"
|
|
357
|
+
stroke="hsl(0 84% 60%)"
|
|
358
|
+
strokeWidth={2}
|
|
359
|
+
/>
|
|
360
|
+
)}
|
|
361
|
+
</g>
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
## lib/waterfall-page.tsx
|
|
368
|
+
|
|
369
|
+
```tsx
|
|
370
|
+
"use client";
|
|
371
|
+
|
|
372
|
+
import { useAui, AuiProvider } from "@assistant-ui/store";
|
|
373
|
+
import { SpanResource } from "@assistant-ui/react-o11y";
|
|
374
|
+
import { mockSpans } from "./mock-data";
|
|
375
|
+
import { WaterfallTimeline } from "./waterfall-timeline";
|
|
376
|
+
|
|
377
|
+
export function WaterfallPage() {
|
|
378
|
+
const aui = useAui({
|
|
379
|
+
span: SpanResource({ spans: mockSpans }),
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
return (
|
|
383
|
+
<AuiProvider value={aui}>
|
|
384
|
+
<WaterfallTimeline />
|
|
385
|
+
</AuiProvider>
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
## lib/waterfall-row.tsx
|
|
392
|
+
|
|
393
|
+
```tsx
|
|
394
|
+
"use client";
|
|
395
|
+
|
|
396
|
+
import { SpanPrimitive } from "@assistant-ui/react-o11y";
|
|
397
|
+
import { AuiIf } from "@assistant-ui/store";
|
|
398
|
+
import { WaterfallBar } from "./waterfall-bar";
|
|
399
|
+
import { useWaterfallLayout } from "./waterfall-timeline";
|
|
400
|
+
|
|
401
|
+
const LABEL_WIDTH = 200;
|
|
402
|
+
|
|
403
|
+
export function WaterfallRow() {
|
|
404
|
+
const { barWidth, contentWidth, barHeight } = useWaterfallLayout();
|
|
405
|
+
|
|
406
|
+
return (
|
|
407
|
+
<SpanPrimitive.Root
|
|
408
|
+
className="group flex cursor-pointer items-center"
|
|
409
|
+
style={{ width: contentWidth, height: barHeight }}
|
|
410
|
+
>
|
|
411
|
+
<SpanPrimitive.Indent
|
|
412
|
+
baseIndent={8}
|
|
413
|
+
indentPerLevel={12}
|
|
414
|
+
className="sticky left-0 z-10 flex shrink-0 items-center gap-1 overflow-hidden border-border border-r bg-background px-2 group-hover:bg-accent/50"
|
|
415
|
+
style={{ width: LABEL_WIDTH, height: barHeight }}
|
|
416
|
+
>
|
|
417
|
+
<AuiIf condition={(s) => s.span.hasChildren}>
|
|
418
|
+
<SpanPrimitive.CollapseToggle className="flex shrink-0 items-center justify-center rounded p-0.5 text-muted-foreground hover:text-foreground">
|
|
419
|
+
<svg
|
|
420
|
+
className="size-3.5 transition-transform data-[collapsed=true]:[-rotate-90]"
|
|
421
|
+
viewBox="0 0 16 16"
|
|
422
|
+
fill="currentColor"
|
|
423
|
+
>
|
|
424
|
+
<path d="M4 6l4 4 4-4H4z" />
|
|
425
|
+
</svg>
|
|
426
|
+
</SpanPrimitive.CollapseToggle>
|
|
427
|
+
</AuiIf>
|
|
428
|
+
<AuiIf condition={(s) => !s.span.hasChildren}>
|
|
429
|
+
<span className="w-4.5 shrink-0" />
|
|
430
|
+
</AuiIf>
|
|
431
|
+
<SpanPrimitive.StatusIndicator className="size-1.5 shrink-0 rounded-full bg-current" />
|
|
432
|
+
<SpanPrimitive.TypeBadge className="shrink-0 rounded border border-border px-1 text-[10px] text-muted-foreground" />
|
|
433
|
+
<SpanPrimitive.Name className="truncate text-sm" />
|
|
434
|
+
</SpanPrimitive.Indent>
|
|
435
|
+
|
|
436
|
+
{/* Timeline bar */}
|
|
437
|
+
<div
|
|
438
|
+
className="group-hover:bg-accent/30"
|
|
439
|
+
style={{ width: barWidth, height: barHeight }}
|
|
440
|
+
>
|
|
441
|
+
<svg width={barWidth} height={barHeight}>
|
|
442
|
+
<WaterfallBar />
|
|
443
|
+
</svg>
|
|
444
|
+
</div>
|
|
445
|
+
</SpanPrimitive.Root>
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
## lib/waterfall-timeline.tsx
|
|
452
|
+
|
|
453
|
+
```tsx
|
|
454
|
+
"use client";
|
|
455
|
+
|
|
456
|
+
import {
|
|
457
|
+
createContext,
|
|
458
|
+
useContext,
|
|
459
|
+
useEffect,
|
|
460
|
+
useLayoutEffect,
|
|
461
|
+
useMemo,
|
|
462
|
+
useRef,
|
|
463
|
+
useState,
|
|
464
|
+
} from "react";
|
|
465
|
+
import { useAuiState } from "@assistant-ui/store";
|
|
466
|
+
import { SpanPrimitive, type SpanState } from "@assistant-ui/react-o11y";
|
|
467
|
+
import { WaterfallRow } from "./waterfall-row";
|
|
468
|
+
|
|
469
|
+
const LABEL_WIDTH = 200;
|
|
470
|
+
const MAX_LIST_HEIGHT = 400;
|
|
471
|
+
const MIN_ZOOM = 1;
|
|
472
|
+
const MAX_ZOOM = 20;
|
|
473
|
+
const RIGHT_PADDING_RATIO = 0.08;
|
|
474
|
+
|
|
475
|
+
export type WaterfallLayoutContextValue = {
|
|
476
|
+
barWidth: number;
|
|
477
|
+
timeRange: { min: number; max: number };
|
|
478
|
+
barHeight: number;
|
|
479
|
+
contentWidth: number;
|
|
480
|
+
selectedSpanId: string | null;
|
|
481
|
+
onSelectSpan: (spanId: string) => void;
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
export const WaterfallLayoutContext =
|
|
485
|
+
createContext<WaterfallLayoutContextValue | null>(null);
|
|
486
|
+
|
|
487
|
+
export function useWaterfallLayout(): WaterfallLayoutContextValue {
|
|
488
|
+
const ctx = useContext(WaterfallLayoutContext);
|
|
489
|
+
if (!ctx) {
|
|
490
|
+
throw new Error(
|
|
491
|
+
"useWaterfallLayout must be used within WaterfallLayoutContext",
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
return ctx;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function formatTime(ms: number): string {
|
|
498
|
+
if (ms < 1000) return `${Math.round(ms)} ms`;
|
|
499
|
+
return `${(ms / 1000).toFixed(1)} s`;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function TimeAxisTicks({
|
|
503
|
+
timeRange,
|
|
504
|
+
barWidth,
|
|
505
|
+
}: {
|
|
506
|
+
timeRange: { min: number; max: number };
|
|
507
|
+
barWidth: number;
|
|
508
|
+
}) {
|
|
509
|
+
const duration = timeRange.max - timeRange.min;
|
|
510
|
+
const tickCount = Math.min(5, Math.max(2, Math.floor(barWidth / 100)));
|
|
511
|
+
const ticks = Array.from({ length: tickCount + 1 }, (_, i) => {
|
|
512
|
+
const t = (i / tickCount) * duration;
|
|
513
|
+
const x = (i / tickCount) * barWidth;
|
|
514
|
+
return { t, x };
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
return (
|
|
518
|
+
<svg width={barWidth} height={28} className="overflow-visible">
|
|
519
|
+
{ticks.map(({ t, x }) => (
|
|
520
|
+
<g key={x}>
|
|
521
|
+
<line
|
|
522
|
+
x1={x}
|
|
523
|
+
y1={20}
|
|
524
|
+
x2={x}
|
|
525
|
+
y2={28}
|
|
526
|
+
stroke="currentColor"
|
|
527
|
+
className="text-border"
|
|
528
|
+
/>
|
|
529
|
+
<text
|
|
530
|
+
x={x}
|
|
531
|
+
y={14}
|
|
532
|
+
textAnchor="middle"
|
|
533
|
+
className="fill-muted-foreground text-[10px]"
|
|
534
|
+
>
|
|
535
|
+
{formatTime(t)}
|
|
536
|
+
</text>
|
|
537
|
+
</g>
|
|
538
|
+
))}
|
|
539
|
+
</svg>
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
export function WaterfallTimeline() {
|
|
544
|
+
const outerRef = useRef<HTMLDivElement>(null);
|
|
545
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
546
|
+
|
|
547
|
+
const [baseBarWidth, setBaseBarWidth] = useState(400);
|
|
548
|
+
const [zoom, setZoom] = useState(1);
|
|
549
|
+
const zoomRef = useRef(1);
|
|
550
|
+
const baseBarWidthRef = useRef(400);
|
|
551
|
+
const scrollAdjustRef = useRef<{
|
|
552
|
+
mouseX: number;
|
|
553
|
+
ratio: number;
|
|
554
|
+
} | null>(null);
|
|
555
|
+
|
|
556
|
+
const [selectedSpanId, setSelectedSpanId] = useState<string | null>(null);
|
|
557
|
+
|
|
558
|
+
const barWidth = Math.max(200, Math.round(baseBarWidth * zoom));
|
|
559
|
+
|
|
560
|
+
const hasSpans = useAuiState((s) => s.span.hasChildren);
|
|
561
|
+
const timeRange = useAuiState(
|
|
562
|
+
(s) => s.span.timeRange,
|
|
563
|
+
) as SpanState["timeRange"];
|
|
564
|
+
|
|
565
|
+
// Measure outer container width — re-attach when scroll container mounts
|
|
566
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: hasSpans triggers re-attach when scroll container mounts
|
|
567
|
+
useEffect(() => {
|
|
568
|
+
const el = outerRef.current;
|
|
569
|
+
if (!el) return;
|
|
570
|
+
|
|
571
|
+
const observer = new ResizeObserver((entries) => {
|
|
572
|
+
const entry = entries[0];
|
|
573
|
+
if (entry) {
|
|
574
|
+
const w = Math.max(200, entry.contentRect.width - LABEL_WIDTH);
|
|
575
|
+
baseBarWidthRef.current = w;
|
|
576
|
+
setBaseBarWidth(w);
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
observer.observe(el);
|
|
581
|
+
return () => observer.disconnect();
|
|
582
|
+
}, [hasSpans]);
|
|
583
|
+
|
|
584
|
+
// Cmd + scroll wheel zoom — re-attach when scroll container mounts
|
|
585
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: hasSpans triggers re-attach when scroll container mounts
|
|
586
|
+
useEffect(() => {
|
|
587
|
+
const el = scrollRef.current;
|
|
588
|
+
if (!el) return;
|
|
589
|
+
|
|
590
|
+
const handleWheel = (e: WheelEvent) => {
|
|
591
|
+
if (!e.metaKey) return;
|
|
592
|
+
e.preventDefault();
|
|
593
|
+
|
|
594
|
+
const rect = el.getBoundingClientRect();
|
|
595
|
+
const mouseXInView = e.clientX - rect.left - LABEL_WIDTH;
|
|
596
|
+
const mouseXInContent = el.scrollLeft + mouseXInView;
|
|
597
|
+
const currentBarWidth = baseBarWidthRef.current * zoomRef.current;
|
|
598
|
+
const ratio = currentBarWidth > 0 ? mouseXInContent / currentBarWidth : 0;
|
|
599
|
+
|
|
600
|
+
const newZoom = Math.max(
|
|
601
|
+
MIN_ZOOM,
|
|
602
|
+
Math.min(MAX_ZOOM, zoomRef.current * 2 ** (-e.deltaY * 0.003)),
|
|
603
|
+
);
|
|
604
|
+
|
|
605
|
+
scrollAdjustRef.current = { mouseX: mouseXInView, ratio };
|
|
606
|
+
zoomRef.current = newZoom;
|
|
607
|
+
setZoom(newZoom);
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
el.addEventListener("wheel", handleWheel, { passive: false });
|
|
611
|
+
return () => el.removeEventListener("wheel", handleWheel);
|
|
612
|
+
}, [hasSpans]);
|
|
613
|
+
|
|
614
|
+
// Adjust scroll after zoom
|
|
615
|
+
useLayoutEffect(() => {
|
|
616
|
+
const adj = scrollAdjustRef.current;
|
|
617
|
+
const el = scrollRef.current;
|
|
618
|
+
if (!adj || !el) return;
|
|
619
|
+
scrollAdjustRef.current = null;
|
|
620
|
+
|
|
621
|
+
const newBarX = adj.ratio * barWidth;
|
|
622
|
+
el.scrollLeft = newBarX - adj.mouseX;
|
|
623
|
+
}, [barWidth]);
|
|
624
|
+
|
|
625
|
+
const renderTimeRange = useMemo(() => {
|
|
626
|
+
const duration = timeRange.max - timeRange.min;
|
|
627
|
+
return {
|
|
628
|
+
min: timeRange.min,
|
|
629
|
+
max: timeRange.max + duration * RIGHT_PADDING_RATIO,
|
|
630
|
+
};
|
|
631
|
+
}, [timeRange]);
|
|
632
|
+
|
|
633
|
+
const contentWidth = LABEL_WIDTH + barWidth;
|
|
634
|
+
const scrollMaxHeight = MAX_LIST_HEIGHT + 28;
|
|
635
|
+
|
|
636
|
+
const layoutValue = useMemo(
|
|
637
|
+
() => ({
|
|
638
|
+
barWidth,
|
|
639
|
+
timeRange: renderTimeRange,
|
|
640
|
+
barHeight: 32,
|
|
641
|
+
contentWidth,
|
|
642
|
+
selectedSpanId,
|
|
643
|
+
onSelectSpan: setSelectedSpanId,
|
|
644
|
+
}),
|
|
645
|
+
[barWidth, renderTimeRange, contentWidth, selectedSpanId],
|
|
646
|
+
);
|
|
647
|
+
|
|
648
|
+
if (!hasSpans) {
|
|
649
|
+
return (
|
|
650
|
+
<div className="rounded-lg border border-border py-12 text-center text-muted-foreground text-sm">
|
|
651
|
+
No spans recorded.
|
|
652
|
+
</div>
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
return (
|
|
657
|
+
<div ref={outerRef} className="rounded-lg border border-border">
|
|
658
|
+
<div
|
|
659
|
+
ref={scrollRef}
|
|
660
|
+
className="overflow-auto"
|
|
661
|
+
style={{ maxHeight: scrollMaxHeight }}
|
|
662
|
+
>
|
|
663
|
+
{/* Time axis */}
|
|
664
|
+
<div
|
|
665
|
+
className="sticky top-0 z-20 flex border-border border-b bg-background"
|
|
666
|
+
style={{ width: contentWidth }}
|
|
667
|
+
>
|
|
668
|
+
<div
|
|
669
|
+
className="sticky left-0 z-30 shrink-0 border-border border-r bg-background px-2 py-1.5 text-muted-foreground text-xs"
|
|
670
|
+
style={{ width: LABEL_WIDTH }}
|
|
671
|
+
>
|
|
672
|
+
Span
|
|
673
|
+
</div>
|
|
674
|
+
<div style={{ width: barWidth, height: 28 }}>
|
|
675
|
+
<TimeAxisTicks timeRange={renderTimeRange} barWidth={barWidth} />
|
|
676
|
+
</div>
|
|
677
|
+
</div>
|
|
678
|
+
|
|
679
|
+
{/* Span rows */}
|
|
680
|
+
<WaterfallLayoutContext.Provider value={layoutValue}>
|
|
681
|
+
<div
|
|
682
|
+
style={{ width: contentWidth }}
|
|
683
|
+
onClick={(e) => {
|
|
684
|
+
const target = e.target as HTMLElement;
|
|
685
|
+
const el = target.closest("[data-span-id]") as HTMLElement | null;
|
|
686
|
+
if (el?.dataset.spanId) {
|
|
687
|
+
setSelectedSpanId(el.dataset.spanId);
|
|
688
|
+
}
|
|
689
|
+
}}
|
|
690
|
+
>
|
|
691
|
+
<SpanPrimitive.Children components={{ Span: WaterfallRow }} />
|
|
692
|
+
</div>
|
|
693
|
+
</WaterfallLayoutContext.Provider>
|
|
694
|
+
</div>
|
|
695
|
+
|
|
696
|
+
{/* Legend */}
|
|
697
|
+
<div className="flex items-center gap-4 border-border border-t px-3 py-2 text-muted-foreground text-xs">
|
|
698
|
+
<div className="flex items-center gap-1.5">
|
|
699
|
+
<span
|
|
700
|
+
className="size-2.5 rounded-sm"
|
|
701
|
+
style={{ background: "hsl(221 83% 53%)" }}
|
|
702
|
+
/>
|
|
703
|
+
<span>action</span>
|
|
704
|
+
</div>
|
|
705
|
+
<div className="flex items-center gap-1.5">
|
|
706
|
+
<span
|
|
707
|
+
className="size-2.5 rounded-sm"
|
|
708
|
+
style={{ background: "hsl(262 83% 58%)" }}
|
|
709
|
+
/>
|
|
710
|
+
<span>api</span>
|
|
711
|
+
</div>
|
|
712
|
+
<div className="flex items-center gap-1.5">
|
|
713
|
+
<span
|
|
714
|
+
className="size-2.5 rounded-sm"
|
|
715
|
+
style={{ background: "hsl(142 71% 45%)" }}
|
|
716
|
+
/>
|
|
717
|
+
<span>tool</span>
|
|
718
|
+
</div>
|
|
719
|
+
<div className="flex items-center gap-1.5">
|
|
720
|
+
<span
|
|
721
|
+
className="size-2.5 rounded-sm"
|
|
722
|
+
style={{ background: "hsl(25 95% 53%)" }}
|
|
723
|
+
/>
|
|
724
|
+
<span>flow</span>
|
|
725
|
+
</div>
|
|
726
|
+
<div className="flex items-center gap-1.5">
|
|
727
|
+
<span
|
|
728
|
+
className="size-2.5 rounded-sm"
|
|
729
|
+
style={{ background: "hsl(340 75% 55%)" }}
|
|
730
|
+
/>
|
|
731
|
+
<span>pipeline</span>
|
|
732
|
+
</div>
|
|
733
|
+
</div>
|
|
734
|
+
</div>
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
## next.config.ts
|
|
741
|
+
|
|
742
|
+
```typescript
|
|
743
|
+
import type { NextConfig } from "next";
|
|
744
|
+
|
|
745
|
+
const nextConfig: NextConfig = {
|
|
746
|
+
/* config options here */
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
export default nextConfig;
|
|
750
|
+
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
## package.json
|
|
754
|
+
|
|
755
|
+
```json
|
|
756
|
+
{
|
|
757
|
+
"name": "waterfall",
|
|
758
|
+
"version": "0.0.0",
|
|
759
|
+
"private": true,
|
|
760
|
+
"type": "module",
|
|
761
|
+
"scripts": {
|
|
762
|
+
"dev": "next dev",
|
|
763
|
+
"build": "next build",
|
|
764
|
+
"start": "next start"
|
|
765
|
+
},
|
|
766
|
+
"dependencies": {
|
|
767
|
+
"@assistant-ui/react-o11y": "workspace:*",
|
|
768
|
+
"@assistant-ui/store": "workspace:*",
|
|
769
|
+
"@assistant-ui/tap": "workspace:*",
|
|
770
|
+
"next": "^16.1.6",
|
|
771
|
+
"react": "^19.2.4",
|
|
772
|
+
"react-dom": "^19.2.4"
|
|
773
|
+
},
|
|
774
|
+
"devDependencies": {
|
|
775
|
+
"@assistant-ui/x-buildutils": "workspace:*",
|
|
776
|
+
"@tailwindcss/postcss": "^4.2.1",
|
|
777
|
+
"@types/node": "^25.3.3",
|
|
778
|
+
"@types/react": "^19.2.14",
|
|
779
|
+
"@types/react-dom": "^19.2.3",
|
|
780
|
+
"postcss": "^8.5.8",
|
|
781
|
+
"tailwindcss": "^4.2.1",
|
|
782
|
+
"typescript": "^5.9.3"
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
## tsconfig.json
|
|
789
|
+
|
|
790
|
+
```json
|
|
791
|
+
{
|
|
792
|
+
"extends": "@assistant-ui/x-buildutils/ts/next",
|
|
793
|
+
"compilerOptions": {
|
|
794
|
+
"paths": { "@/*": ["./*"] }
|
|
795
|
+
},
|
|
796
|
+
"include": ["**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
797
|
+
"exclude": ["node_modules"]
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
```
|
|
801
|
+
|