@openconsole/shadcn 0.2.4 → 0.2.5

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