@cloudbase/agent-react-ui 1.0.1-alpha.32

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.
Files changed (109) hide show
  1. package/README.md +123 -0
  2. package/components.json +21 -0
  3. package/dist/index.css +4241 -0
  4. package/dist/index.css.map +1 -0
  5. package/dist/index.d.mts +59 -0
  6. package/dist/index.d.ts +59 -0
  7. package/dist/index.js +2169 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/index.mjs +2182 -0
  10. package/dist/index.mjs.map +1 -0
  11. package/example/.env.sample +2 -0
  12. package/example/App.tsx +368 -0
  13. package/example/app.css +1 -0
  14. package/example/index.html +12 -0
  15. package/example/main.tsx +9 -0
  16. package/example/vite.config.ts +34 -0
  17. package/package.json +75 -0
  18. package/postcss.config.cjs +3 -0
  19. package/src/components/ai-elements/agent.tsx +140 -0
  20. package/src/components/ai-elements/artifact.tsx +147 -0
  21. package/src/components/ai-elements/attachments.tsx +421 -0
  22. package/src/components/ai-elements/audio-player.tsx +228 -0
  23. package/src/components/ai-elements/canvas.tsx +22 -0
  24. package/src/components/ai-elements/chain-of-thought.tsx +228 -0
  25. package/src/components/ai-elements/checkpoint.tsx +71 -0
  26. package/src/components/ai-elements/code-block.tsx +532 -0
  27. package/src/components/ai-elements/commit.tsx +448 -0
  28. package/src/components/ai-elements/confirmation.tsx +176 -0
  29. package/src/components/ai-elements/connection.tsx +28 -0
  30. package/src/components/ai-elements/context.tsx +408 -0
  31. package/src/components/ai-elements/controls.tsx +18 -0
  32. package/src/components/ai-elements/conversation.tsx +100 -0
  33. package/src/components/ai-elements/edge.tsx +140 -0
  34. package/src/components/ai-elements/environment-variables.tsx +295 -0
  35. package/src/components/ai-elements/file-tree.tsx +258 -0
  36. package/src/components/ai-elements/image.tsx +24 -0
  37. package/src/components/ai-elements/inline-citation.tsx +287 -0
  38. package/src/components/ai-elements/message.tsx +336 -0
  39. package/src/components/ai-elements/mic-selector.tsx +370 -0
  40. package/src/components/ai-elements/model-selector.tsx +211 -0
  41. package/src/components/ai-elements/node.tsx +71 -0
  42. package/src/components/ai-elements/open-in-chat.tsx +365 -0
  43. package/src/components/ai-elements/package-info.tsx +233 -0
  44. package/src/components/ai-elements/panel.tsx +15 -0
  45. package/src/components/ai-elements/persona.tsx +270 -0
  46. package/src/components/ai-elements/plan.tsx +142 -0
  47. package/src/components/ai-elements/prompt-input.tsx +1263 -0
  48. package/src/components/ai-elements/queue.tsx +274 -0
  49. package/src/components/ai-elements/reasoning.tsx +193 -0
  50. package/src/components/ai-elements/sandbox.tsx +126 -0
  51. package/src/components/ai-elements/schema-display.tsx +458 -0
  52. package/src/components/ai-elements/shimmer.tsx +64 -0
  53. package/src/components/ai-elements/snippet.tsx +139 -0
  54. package/src/components/ai-elements/sources.tsx +77 -0
  55. package/src/components/ai-elements/speech-input.tsx +301 -0
  56. package/src/components/ai-elements/stack-trace.tsx +482 -0
  57. package/src/components/ai-elements/suggestion.tsx +53 -0
  58. package/src/components/ai-elements/task.tsx +87 -0
  59. package/src/components/ai-elements/terminal.tsx +261 -0
  60. package/src/components/ai-elements/test-results.tsx +485 -0
  61. package/src/components/ai-elements/tool.tsx +174 -0
  62. package/src/components/ai-elements/toolbar.tsx +16 -0
  63. package/src/components/ai-elements/transcription.tsx +124 -0
  64. package/src/components/ai-elements/voice-selector.tsx +479 -0
  65. package/src/components/ai-elements/web-preview.tsx +263 -0
  66. package/src/components/chat/Chat.tsx +178 -0
  67. package/src/components/chat/Input.tsx +98 -0
  68. package/src/components/chat/Message.tsx +276 -0
  69. package/src/components/chat/index.ts +2 -0
  70. package/src/components/index.ts +1 -0
  71. package/src/components/ui/accordion.tsx +64 -0
  72. package/src/components/ui/alert.tsx +66 -0
  73. package/src/components/ui/avatar.tsx +107 -0
  74. package/src/components/ui/badge.tsx +48 -0
  75. package/src/components/ui/button-group.tsx +83 -0
  76. package/src/components/ui/button.tsx +64 -0
  77. package/src/components/ui/card.tsx +92 -0
  78. package/src/components/ui/carousel.tsx +239 -0
  79. package/src/components/ui/collapsible.tsx +31 -0
  80. package/src/components/ui/command.tsx +184 -0
  81. package/src/components/ui/dialog.tsx +158 -0
  82. package/src/components/ui/dropdown-menu.tsx +257 -0
  83. package/src/components/ui/hover-card.tsx +42 -0
  84. package/src/components/ui/input-group.tsx +168 -0
  85. package/src/components/ui/input.tsx +21 -0
  86. package/src/components/ui/popover.tsx +87 -0
  87. package/src/components/ui/progress.tsx +31 -0
  88. package/src/components/ui/scroll-area.tsx +56 -0
  89. package/src/components/ui/select.tsx +190 -0
  90. package/src/components/ui/separator.tsx +28 -0
  91. package/src/components/ui/spinner.tsx +16 -0
  92. package/src/components/ui/switch.tsx +33 -0
  93. package/src/components/ui/tabs.tsx +91 -0
  94. package/src/components/ui/textarea.tsx +18 -0
  95. package/src/components/ui/tooltip.tsx +61 -0
  96. package/src/css/global.css +123 -0
  97. package/src/css/index.css +1 -0
  98. package/src/hooks/index.ts +1 -0
  99. package/src/hooks/use-copy-to-clipboard.ts +31 -0
  100. package/src/index.ts +4 -0
  101. package/src/lib/utils.ts +6 -0
  102. package/src/locales/context.ts +8 -0
  103. package/src/locales/hooks.ts +20 -0
  104. package/src/locales/index.ts +3 -0
  105. package/src/locales/langs/en.ts +17 -0
  106. package/src/locales/langs/index.ts +12 -0
  107. package/src/locales/langs/zh-cn.ts +18 -0
  108. package/tsconfig.json +21 -0
  109. package/tsup.config.ts +21 -0
@@ -0,0 +1,485 @@
1
+ "use client";
2
+
3
+ import { Badge } from "@/components/ui/badge";
4
+ import {
5
+ Collapsible,
6
+ CollapsibleContent,
7
+ CollapsibleTrigger,
8
+ } from "@/components/ui/collapsible";
9
+ import { cn } from "@/lib/utils";
10
+ import {
11
+ CheckCircle2Icon,
12
+ ChevronRightIcon,
13
+ CircleDotIcon,
14
+ CircleIcon,
15
+ XCircleIcon,
16
+ } from "lucide-react";
17
+ import {
18
+ type ComponentProps,
19
+ createContext,
20
+ type HTMLAttributes,
21
+ useContext,
22
+ } from "react";
23
+
24
+ type TestStatus = "passed" | "failed" | "skipped" | "running";
25
+
26
+ interface TestResultsSummary {
27
+ passed: number;
28
+ failed: number;
29
+ skipped: number;
30
+ total: number;
31
+ duration?: number;
32
+ }
33
+
34
+ interface TestResultsContextType {
35
+ summary?: TestResultsSummary;
36
+ }
37
+
38
+ const TestResultsContext = createContext<TestResultsContextType>({});
39
+
40
+ export type TestResultsProps = HTMLAttributes<HTMLDivElement> & {
41
+ summary?: TestResultsSummary;
42
+ };
43
+
44
+ export const TestResults = ({
45
+ summary,
46
+ className,
47
+ children,
48
+ ...props
49
+ }: TestResultsProps) => (
50
+ <TestResultsContext.Provider value={{ summary }}>
51
+ <div
52
+ className={cn("rounded-lg border bg-background", className)}
53
+ {...props}
54
+ >
55
+ {children ??
56
+ (summary && (
57
+ <TestResultsHeader>
58
+ <TestResultsSummary />
59
+ <TestResultsDuration />
60
+ </TestResultsHeader>
61
+ ))}
62
+ </div>
63
+ </TestResultsContext.Provider>
64
+ );
65
+
66
+ export type TestResultsHeaderProps = HTMLAttributes<HTMLDivElement>;
67
+
68
+ export const TestResultsHeader = ({
69
+ className,
70
+ children,
71
+ ...props
72
+ }: TestResultsHeaderProps) => (
73
+ <div
74
+ className={cn(
75
+ "flex items-center justify-between border-b px-4 py-3",
76
+ className
77
+ )}
78
+ {...props}
79
+ >
80
+ {children}
81
+ </div>
82
+ );
83
+
84
+ export type TestResultsSummaryProps = HTMLAttributes<HTMLDivElement>;
85
+
86
+ export const TestResultsSummary = ({
87
+ className,
88
+ children,
89
+ ...props
90
+ }: TestResultsSummaryProps) => {
91
+ const { summary } = useContext(TestResultsContext);
92
+
93
+ if (!summary) {
94
+ return null;
95
+ }
96
+
97
+ return (
98
+ <div className={cn("flex items-center gap-3", className)} {...props}>
99
+ {children ?? (
100
+ <>
101
+ <Badge
102
+ className="gap-1 bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400"
103
+ variant="secondary"
104
+ >
105
+ <CheckCircle2Icon className="size-3" />
106
+ {summary.passed} passed
107
+ </Badge>
108
+ {summary.failed > 0 && (
109
+ <Badge
110
+ className="gap-1 bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400"
111
+ variant="secondary"
112
+ >
113
+ <XCircleIcon className="size-3" />
114
+ {summary.failed} failed
115
+ </Badge>
116
+ )}
117
+ {summary.skipped > 0 && (
118
+ <Badge
119
+ className="gap-1 bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400"
120
+ variant="secondary"
121
+ >
122
+ <CircleIcon className="size-3" />
123
+ {summary.skipped} skipped
124
+ </Badge>
125
+ )}
126
+ </>
127
+ )}
128
+ </div>
129
+ );
130
+ };
131
+
132
+ export type TestResultsDurationProps = HTMLAttributes<HTMLSpanElement>;
133
+
134
+ export const TestResultsDuration = ({
135
+ className,
136
+ children,
137
+ ...props
138
+ }: TestResultsDurationProps) => {
139
+ const { summary } = useContext(TestResultsContext);
140
+
141
+ if (!summary?.duration) {
142
+ return null;
143
+ }
144
+
145
+ const formatDuration = (ms: number) => {
146
+ if (ms < 1000) {
147
+ return `${ms}ms`;
148
+ }
149
+ return `${(ms / 1000).toFixed(2)}s`;
150
+ };
151
+
152
+ return (
153
+ <span className={cn("text-muted-foreground text-sm", className)} {...props}>
154
+ {children ?? formatDuration(summary.duration)}
155
+ </span>
156
+ );
157
+ };
158
+
159
+ export type TestResultsProgressProps = HTMLAttributes<HTMLDivElement>;
160
+
161
+ export const TestResultsProgress = ({
162
+ className,
163
+ children,
164
+ ...props
165
+ }: TestResultsProgressProps) => {
166
+ const { summary } = useContext(TestResultsContext);
167
+
168
+ if (!summary) {
169
+ return null;
170
+ }
171
+
172
+ const passedPercent = (summary.passed / summary.total) * 100;
173
+ const failedPercent = (summary.failed / summary.total) * 100;
174
+
175
+ return (
176
+ <div className={cn("space-y-2", className)} {...props}>
177
+ {children ?? (
178
+ <>
179
+ <div className="flex h-2 overflow-hidden rounded-full bg-muted">
180
+ <div
181
+ className="bg-green-500 transition-all"
182
+ style={{ width: `${passedPercent}%` }}
183
+ />
184
+ <div
185
+ className="bg-red-500 transition-all"
186
+ style={{ width: `${failedPercent}%` }}
187
+ />
188
+ </div>
189
+ <div className="flex justify-between text-muted-foreground text-xs">
190
+ <span>
191
+ {summary.passed}/{summary.total} tests passed
192
+ </span>
193
+ <span>{passedPercent.toFixed(0)}%</span>
194
+ </div>
195
+ </>
196
+ )}
197
+ </div>
198
+ );
199
+ };
200
+
201
+ export type TestResultsContentProps = HTMLAttributes<HTMLDivElement>;
202
+
203
+ export const TestResultsContent = ({
204
+ className,
205
+ children,
206
+ ...props
207
+ }: TestResultsContentProps) => (
208
+ <div className={cn("space-y-2 p-4", className)} {...props}>
209
+ {children}
210
+ </div>
211
+ );
212
+
213
+ interface TestSuiteContextType {
214
+ name: string;
215
+ status: TestStatus;
216
+ }
217
+
218
+ const TestSuiteContext = createContext<TestSuiteContextType>({
219
+ name: "",
220
+ status: "passed",
221
+ });
222
+
223
+ export type TestSuiteProps = ComponentProps<typeof Collapsible> & {
224
+ name: string;
225
+ status: TestStatus;
226
+ };
227
+
228
+ export const TestSuite = ({
229
+ name,
230
+ status,
231
+ className,
232
+ children,
233
+ ...props
234
+ }: TestSuiteProps) => (
235
+ <TestSuiteContext.Provider value={{ name, status }}>
236
+ <Collapsible className={cn("rounded-lg border", className)} {...props}>
237
+ {children}
238
+ </Collapsible>
239
+ </TestSuiteContext.Provider>
240
+ );
241
+
242
+ export type TestSuiteNameProps = ComponentProps<typeof CollapsibleTrigger>;
243
+
244
+ export const TestSuiteName = ({
245
+ className,
246
+ children,
247
+ ...props
248
+ }: TestSuiteNameProps) => {
249
+ const { name, status } = useContext(TestSuiteContext);
250
+
251
+ return (
252
+ <CollapsibleTrigger
253
+ className={cn(
254
+ "group flex w-full items-center gap-2 px-4 py-3 text-left transition-colors hover:bg-muted/50",
255
+ className
256
+ )}
257
+ {...props}
258
+ >
259
+ <ChevronRightIcon className="size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-90" />
260
+ <TestStatusIcon status={status} />
261
+ <span className="font-medium text-sm">{children ?? name}</span>
262
+ </CollapsibleTrigger>
263
+ );
264
+ };
265
+
266
+ export type TestSuiteStatsProps = HTMLAttributes<HTMLDivElement> & {
267
+ passed?: number;
268
+ failed?: number;
269
+ skipped?: number;
270
+ };
271
+
272
+ export const TestSuiteStats = ({
273
+ passed = 0,
274
+ failed = 0,
275
+ skipped = 0,
276
+ className,
277
+ children,
278
+ ...props
279
+ }: TestSuiteStatsProps) => (
280
+ <div
281
+ className={cn("ml-auto flex items-center gap-2 text-xs", className)}
282
+ {...props}
283
+ >
284
+ {children ?? (
285
+ <>
286
+ {passed > 0 && (
287
+ <span className="text-green-600 dark:text-green-400">
288
+ {passed} passed
289
+ </span>
290
+ )}
291
+ {failed > 0 && (
292
+ <span className="text-red-600 dark:text-red-400">
293
+ {failed} failed
294
+ </span>
295
+ )}
296
+ {skipped > 0 && (
297
+ <span className="text-yellow-600 dark:text-yellow-400">
298
+ {skipped} skipped
299
+ </span>
300
+ )}
301
+ </>
302
+ )}
303
+ </div>
304
+ );
305
+
306
+ export type TestSuiteContentProps = ComponentProps<typeof CollapsibleContent>;
307
+
308
+ export const TestSuiteContent = ({
309
+ className,
310
+ children,
311
+ ...props
312
+ }: TestSuiteContentProps) => (
313
+ <CollapsibleContent className={cn("border-t", className)} {...props}>
314
+ <div className="divide-y">{children}</div>
315
+ </CollapsibleContent>
316
+ );
317
+
318
+ interface TestContextType {
319
+ name: string;
320
+ status: TestStatus;
321
+ duration?: number;
322
+ }
323
+
324
+ const TestContext = createContext<TestContextType>({
325
+ name: "",
326
+ status: "passed",
327
+ });
328
+
329
+ export type TestProps = HTMLAttributes<HTMLDivElement> & {
330
+ name: string;
331
+ status: TestStatus;
332
+ duration?: number;
333
+ };
334
+
335
+ export const Test = ({
336
+ name,
337
+ status,
338
+ duration,
339
+ className,
340
+ children,
341
+ ...props
342
+ }: TestProps) => (
343
+ <TestContext.Provider value={{ name, status, duration }}>
344
+ <div
345
+ className={cn("flex items-center gap-2 px-4 py-2 text-sm", className)}
346
+ {...props}
347
+ >
348
+ {children ?? (
349
+ <>
350
+ <TestStatus />
351
+ <TestName />
352
+ {duration !== undefined && <TestDuration />}
353
+ </>
354
+ )}
355
+ </div>
356
+ </TestContext.Provider>
357
+ );
358
+
359
+ const statusStyles: Record<TestStatus, string> = {
360
+ passed: "text-green-600 dark:text-green-400",
361
+ failed: "text-red-600 dark:text-red-400",
362
+ skipped: "text-yellow-600 dark:text-yellow-400",
363
+ running: "text-blue-600 dark:text-blue-400",
364
+ };
365
+
366
+ const statusIcons: Record<TestStatus, React.ReactNode> = {
367
+ passed: <CheckCircle2Icon className="size-4" />,
368
+ failed: <XCircleIcon className="size-4" />,
369
+ skipped: <CircleIcon className="size-4" />,
370
+ running: <CircleDotIcon className="size-4 animate-pulse" />,
371
+ };
372
+
373
+ const TestStatusIcon = ({ status }: { status: TestStatus }) => (
374
+ <span className={cn("shrink-0", statusStyles[status])}>
375
+ {statusIcons[status]}
376
+ </span>
377
+ );
378
+
379
+ export type TestStatusProps = HTMLAttributes<HTMLSpanElement>;
380
+
381
+ export const TestStatus = ({
382
+ className,
383
+ children,
384
+ ...props
385
+ }: TestStatusProps) => {
386
+ const { status } = useContext(TestContext);
387
+
388
+ return (
389
+ <span
390
+ className={cn("shrink-0", statusStyles[status], className)}
391
+ {...props}
392
+ >
393
+ {children ?? statusIcons[status]}
394
+ </span>
395
+ );
396
+ };
397
+
398
+ export type TestNameProps = HTMLAttributes<HTMLSpanElement>;
399
+
400
+ export const TestName = ({ className, children, ...props }: TestNameProps) => {
401
+ const { name } = useContext(TestContext);
402
+
403
+ return (
404
+ <span className={cn("flex-1", className)} {...props}>
405
+ {children ?? name}
406
+ </span>
407
+ );
408
+ };
409
+
410
+ export type TestDurationProps = HTMLAttributes<HTMLSpanElement>;
411
+
412
+ export const TestDuration = ({
413
+ className,
414
+ children,
415
+ ...props
416
+ }: TestDurationProps) => {
417
+ const { duration } = useContext(TestContext);
418
+
419
+ if (duration === undefined) {
420
+ return null;
421
+ }
422
+
423
+ return (
424
+ <span
425
+ className={cn("ml-auto text-muted-foreground text-xs", className)}
426
+ {...props}
427
+ >
428
+ {children ?? `${duration}ms`}
429
+ </span>
430
+ );
431
+ };
432
+
433
+ export type TestErrorProps = HTMLAttributes<HTMLDivElement>;
434
+
435
+ export const TestError = ({
436
+ className,
437
+ children,
438
+ ...props
439
+ }: TestErrorProps) => (
440
+ <div
441
+ className={cn(
442
+ "mt-2 rounded-md bg-red-50 p-3 dark:bg-red-900/20",
443
+ className
444
+ )}
445
+ {...props}
446
+ >
447
+ {children}
448
+ </div>
449
+ );
450
+
451
+ export type TestErrorMessageProps = HTMLAttributes<HTMLParagraphElement>;
452
+
453
+ export const TestErrorMessage = ({
454
+ className,
455
+ children,
456
+ ...props
457
+ }: TestErrorMessageProps) => (
458
+ <p
459
+ className={cn(
460
+ "font-medium text-red-700 text-sm dark:text-red-400",
461
+ className
462
+ )}
463
+ {...props}
464
+ >
465
+ {children}
466
+ </p>
467
+ );
468
+
469
+ export type TestErrorStackProps = HTMLAttributes<HTMLPreElement>;
470
+
471
+ export const TestErrorStack = ({
472
+ className,
473
+ children,
474
+ ...props
475
+ }: TestErrorStackProps) => (
476
+ <pre
477
+ className={cn(
478
+ "mt-2 overflow-auto font-mono text-red-600 text-xs dark:text-red-400",
479
+ className
480
+ )}
481
+ {...props}
482
+ >
483
+ {children}
484
+ </pre>
485
+ );
@@ -0,0 +1,174 @@
1
+ "use client";
2
+
3
+ import { Badge } from "@/components/ui/badge";
4
+ import {
5
+ Collapsible,
6
+ CollapsibleContent,
7
+ CollapsibleTrigger,
8
+ } from "@/components/ui/collapsible";
9
+ import { cn } from "@/lib/utils";
10
+ import type { DynamicToolUIPart, ToolUIPart } from "ai";
11
+ import {
12
+ CheckCircleIcon,
13
+ ChevronDownIcon,
14
+ CircleIcon,
15
+ ClockIcon,
16
+ WrenchIcon,
17
+ XCircleIcon,
18
+ } from "lucide-react";
19
+ import type { ComponentProps, ReactNode } from "react";
20
+ import { isValidElement } from "react";
21
+ import { CodeBlock } from "./code-block";
22
+
23
+ export type ToolProps = ComponentProps<typeof Collapsible>;
24
+
25
+ export const Tool = ({ className, ...props }: ToolProps) => (
26
+ <Collapsible
27
+ className={cn("group not-prose mb-4 w-full rounded-md border", className)}
28
+ {...props}
29
+ />
30
+ );
31
+
32
+ export type ToolPart = ToolUIPart | DynamicToolUIPart;
33
+
34
+ export type ToolHeaderProps = {
35
+ title?: string;
36
+ className?: string;
37
+ } & (
38
+ | { type: ToolUIPart["type"]; state: ToolUIPart["state"]; toolName?: never }
39
+ | {
40
+ type: DynamicToolUIPart["type"];
41
+ state: DynamicToolUIPart["state"];
42
+ toolName: string;
43
+ }
44
+ );
45
+
46
+ export const getStatusBadge = (status: ToolPart["state"]) => {
47
+ const labels: Record<ToolPart["state"], string> = {
48
+ "input-streaming": "Pending",
49
+ "input-available": "Running",
50
+ "approval-requested": "Awaiting Approval",
51
+ "approval-responded": "Responded",
52
+ "output-available": "Completed",
53
+ "output-error": "Error",
54
+ "output-denied": "Denied",
55
+ };
56
+
57
+ const icons: Record<ToolPart["state"], ReactNode> = {
58
+ "input-streaming": <CircleIcon className="size-4" />,
59
+ "input-available": <ClockIcon className="size-4 animate-pulse" />,
60
+ "approval-requested": <ClockIcon className="size-4 text-yellow-600" />,
61
+ "approval-responded": <CheckCircleIcon className="size-4 text-blue-600" />,
62
+ "output-available": <CheckCircleIcon className="size-4 text-green-600" />,
63
+ "output-error": <XCircleIcon className="size-4 text-red-600" />,
64
+ "output-denied": <XCircleIcon className="size-4 text-orange-600" />,
65
+ };
66
+
67
+ return (
68
+ <Badge className="gap-1.5 rounded-full text-xs" variant="secondary">
69
+ {icons[status]}
70
+ {labels[status]}
71
+ </Badge>
72
+ );
73
+ };
74
+
75
+ export const ToolHeader = ({
76
+ className,
77
+ title,
78
+ type,
79
+ state,
80
+ toolName,
81
+ ...props
82
+ }: ToolHeaderProps) => {
83
+ const derivedName =
84
+ type === "dynamic-tool" ? toolName : type.split("-").slice(1).join("-");
85
+
86
+ return (
87
+ <CollapsibleTrigger
88
+ className={cn(
89
+ "flex w-full items-center justify-between gap-4 p-3",
90
+ className
91
+ )}
92
+ {...props}
93
+ >
94
+ <div className="flex items-center gap-2">
95
+ <WrenchIcon className="size-4 text-muted-foreground" />
96
+ <span className="font-medium text-sm">{title ?? derivedName}</span>
97
+ {getStatusBadge(state)}
98
+ </div>
99
+ <ChevronDownIcon className="size-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" />
100
+ </CollapsibleTrigger>
101
+ );
102
+ };
103
+
104
+ export type ToolContentProps = ComponentProps<typeof CollapsibleContent>;
105
+
106
+ export const ToolContent = ({ className, ...props }: ToolContentProps) => (
107
+ <CollapsibleContent
108
+ className={cn(
109
+ "data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 space-y-4 p-4 text-popover-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in",
110
+ className
111
+ )}
112
+ {...props}
113
+ />
114
+ );
115
+
116
+ export type ToolInputProps = ComponentProps<"div"> & {
117
+ input: ToolPart["input"];
118
+ };
119
+
120
+ export const ToolInput = ({ className, input, ...props }: ToolInputProps) => (
121
+ <div className={cn("space-y-2 overflow-hidden", className)} {...props}>
122
+ <h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
123
+ Parameters
124
+ </h4>
125
+ <div className="rounded-md bg-muted/50">
126
+ <CodeBlock code={JSON.stringify(input, null, 2)} language="json" />
127
+ </div>
128
+ </div>
129
+ );
130
+
131
+ export type ToolOutputProps = ComponentProps<"div"> & {
132
+ output: ToolPart["output"];
133
+ errorText: ToolPart["errorText"];
134
+ };
135
+
136
+ export const ToolOutput = ({
137
+ className,
138
+ output,
139
+ errorText,
140
+ ...props
141
+ }: ToolOutputProps) => {
142
+ if (!(output || errorText)) {
143
+ return null;
144
+ }
145
+
146
+ let Output = <div>{output as ReactNode}</div>;
147
+
148
+ if (typeof output === "object" && !isValidElement(output)) {
149
+ Output = (
150
+ <CodeBlock code={JSON.stringify(output, null, 2)} language="json" />
151
+ );
152
+ } else if (typeof output === "string") {
153
+ Output = <CodeBlock code={output} language="json" />;
154
+ }
155
+
156
+ return (
157
+ <div className={cn("space-y-2", className)} {...props}>
158
+ <h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
159
+ {errorText ? "Error" : "Result"}
160
+ </h4>
161
+ <div
162
+ className={cn(
163
+ "overflow-x-auto rounded-md text-xs [&_table]:w-full",
164
+ errorText
165
+ ? "bg-destructive/10 text-destructive"
166
+ : "bg-muted/50 text-foreground"
167
+ )}
168
+ >
169
+ {errorText && <div>{errorText}</div>}
170
+ {Output}
171
+ </div>
172
+ </div>
173
+ );
174
+ };
@@ -0,0 +1,16 @@
1
+ import { cn } from "@/lib/utils";
2
+ import { NodeToolbar, Position } from "@xyflow/react";
3
+ import type { ComponentProps } from "react";
4
+
5
+ type ToolbarProps = ComponentProps<typeof NodeToolbar>;
6
+
7
+ export const Toolbar = ({ className, ...props }: ToolbarProps) => (
8
+ <NodeToolbar
9
+ className={cn(
10
+ "flex items-center gap-1 rounded-sm border bg-background p-1.5",
11
+ className
12
+ )}
13
+ position={Position.Bottom}
14
+ {...props}
15
+ />
16
+ );