@vvedantb/ui 0.1.1 → 0.1.2

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 (74) hide show
  1. package/LICENSE +21 -21
  2. package/package.json +3 -2
  3. package/src/ai-elements/artifact.tsx +99 -99
  4. package/src/ai-elements/attachments.tsx +366 -366
  5. package/src/ai-elements/chain-of-thought.tsx +212 -212
  6. package/src/ai-elements/code-block.tsx +478 -478
  7. package/src/ai-elements/conversation.tsx +157 -157
  8. package/src/ai-elements/image.tsx +25 -25
  9. package/src/ai-elements/inline-citation.tsx +252 -252
  10. package/src/ai-elements/message.tsx +269 -269
  11. package/src/ai-elements/model-selector.tsx +211 -211
  12. package/src/ai-elements/prompt-input.tsx +1018 -1018
  13. package/src/ai-elements/queue.tsx +251 -251
  14. package/src/ai-elements/reasoning.tsx +178 -178
  15. package/src/ai-elements/shimmer.tsx +56 -56
  16. package/src/ai-elements/sources.tsx +77 -77
  17. package/src/ai-elements/speech-input.tsx +290 -290
  18. package/src/ai-elements/suggestion.tsx +191 -191
  19. package/src/css.d.ts +2 -2
  20. package/src/index.ts +96 -96
  21. package/src/motion/presets.ts +35 -35
  22. package/src/motion/presets.vibot.ts +105 -105
  23. package/src/ui/_menu-classes.ts +30 -30
  24. package/src/ui/_menu-classes.vibot.ts +33 -33
  25. package/src/ui/accordion.tsx +81 -81
  26. package/src/ui/accordion.verve.tsx +56 -56
  27. package/src/ui/badge.tsx +34 -34
  28. package/src/ui/breadcrumb.tsx +83 -83
  29. package/src/ui/button-group.tsx +84 -84
  30. package/src/ui/button.tsx +57 -57
  31. package/src/ui/card.tsx +27 -27
  32. package/src/ui/carousel.tsx +211 -211
  33. package/src/ui/checkbox.tsx +27 -27
  34. package/src/ui/clear-input.tsx +170 -170
  35. package/src/ui/clear-input.vibot.tsx +76 -76
  36. package/src/ui/clearInputDissolve.ts +214 -214
  37. package/src/ui/collapsible.tsx +7 -7
  38. package/src/ui/command.tsx +148 -148
  39. package/src/ui/context-menu.tsx +173 -173
  40. package/src/ui/dialog.tsx +149 -149
  41. package/src/ui/dropdown-menu.tsx +178 -178
  42. package/src/ui/hover-card.tsx +31 -31
  43. package/src/ui/input-group.tsx +162 -162
  44. package/src/ui/input-group.verve.tsx +150 -150
  45. package/src/ui/input.tsx +21 -21
  46. package/src/ui/kbd.tsx +18 -18
  47. package/src/ui/label.tsx +20 -20
  48. package/src/ui/labeled-switch-row.tsx +53 -53
  49. package/src/ui/modalTransition.ts +85 -85
  50. package/src/ui/pagination.tsx +47 -47
  51. package/src/ui/popover.tsx +33 -33
  52. package/src/ui/progress.tsx +25 -25
  53. package/src/ui/radix-select.tsx +202 -202
  54. package/src/ui/scroll-area.tsx +55 -55
  55. package/src/ui/scroll-area.verve.tsx +43 -43
  56. package/src/ui/search-input.tsx +29 -29
  57. package/src/ui/select.tsx +163 -163
  58. package/src/ui/separator.tsx +28 -28
  59. package/src/ui/sheet.tsx +87 -87
  60. package/src/ui/skeleton.tsx +18 -18
  61. package/src/ui/sonner.css +16 -16
  62. package/src/ui/sonner.tsx +47 -47
  63. package/src/ui/spinner.tsx +27 -27
  64. package/src/ui/surface-classes.ts +55 -55
  65. package/src/ui/switch.tsx +26 -26
  66. package/src/ui/table.tsx +123 -123
  67. package/src/ui/tabs.tsx +140 -140
  68. package/src/ui/tabsSliding.ts +56 -56
  69. package/src/ui/textarea.tsx +21 -21
  70. package/src/ui/time-picker.tsx +170 -170
  71. package/src/ui/tooltip.tsx +25 -25
  72. package/src/ui/visually-hidden.tsx +3 -3
  73. package/src/utils/cn.ts +16 -16
  74. package/src/utils/ref.ts +10 -10
@@ -1,1018 +1,1018 @@
1
- "use client";
2
- import type { ChatStatus, FileUIPart, SourceDocumentUIPart } from "ai";
3
- import type {
4
- ChangeEvent,
5
- ChangeEventHandler,
6
- ClipboardEventHandler,
7
- ComponentProps,
8
- FormEvent,
9
- FormEventHandler,
10
- HTMLAttributes,
11
- KeyboardEventHandler,
12
- MouseEvent,
13
- PropsWithChildren,
14
- ReactNode,
15
- RefObject,
16
- } from "react";
17
- import {
18
- Command,
19
- CommandEmpty,
20
- CommandGroup,
21
- CommandInput,
22
- CommandItem,
23
- CommandList,
24
- CommandSeparator,
25
- } from "../ui/command";
26
- import {
27
- DropdownMenu as Dropdown,
28
- DropdownMenuTrigger as DropdownTrigger,
29
- DropdownMenuContent as DropdownMenu,
30
- DropdownMenuItem as DropdownItem,
31
- } from "../ui/dropdown-menu";
32
- import {
33
- HoverCard,
34
- HoverCardContent,
35
- HoverCardTrigger,
36
- } from "../ui/hover-card";
37
- import {
38
- InputGroup,
39
- InputGroupAddon,
40
- InputGroupButton,
41
- InputGroupTextarea,
42
- } from "../ui/input-group";
43
- import {
44
- Select,
45
- SelectContent,
46
- SelectItem,
47
- SelectTrigger,
48
- SelectValue,
49
- } from "../ui/radix-select";
50
- import { Spinner } from "../ui/spinner";
51
- import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
52
- import { cn } from "../utils/cn";
53
- import { segmentExpandedStateClass } from "../ui/surface-classes";
54
- import {
55
- IconCornerDownLeft,
56
- IconPhoto,
57
- IconPlus,
58
- IconSquare,
59
- IconX,
60
- } from "@tabler/icons-react";
61
- import { nanoid } from "nanoid";
62
- import {
63
- Children,
64
- createContext,
65
- useContext,
66
- useEffect,
67
- useRef,
68
- useState,
69
- } from "react";
70
- const convertBlobUrlToDataUrl = async (url: string): Promise<string | null> => {
71
- try {
72
- const response = await fetch(url);
73
- const blob = await response.blob();
74
- return new Promise((resolve) => {
75
- const reader = new FileReader();
76
- reader.onloadend = () => resolve(reader.result as string);
77
- reader.onerror = () => resolve(null);
78
- reader.readAsDataURL(blob);
79
- });
80
- } catch {
81
- return null;
82
- }
83
- };
84
- export interface AttachmentsContext {
85
- files: (FileUIPart & {
86
- id: string;
87
- })[];
88
- add: (files: File[] | FileList) => void;
89
- remove: (id: string) => void;
90
- clear: () => void;
91
- openFileDialog: () => void;
92
- fileInputRef: RefObject<HTMLInputElement | null>;
93
- }
94
- export interface TextInputContext {
95
- value: string;
96
- setInput: (v: string) => void;
97
- clear: () => void;
98
- }
99
- export interface PromptInputControllerProps {
100
- textInput: TextInputContext;
101
- attachments: AttachmentsContext;
102
- __registerFileInput: (
103
- ref: RefObject<HTMLInputElement | null>,
104
- open: () => void,
105
- ) => void;
106
- }
107
- const PromptInputController = createContext<PromptInputControllerProps | null>(
108
- null,
109
- );
110
- const ProviderAttachmentsContext = createContext<AttachmentsContext | null>(
111
- null,
112
- );
113
- export const usePromptInputController = () => {
114
- const ctx = useContext(PromptInputController);
115
- if (!ctx) {
116
- throw new Error(
117
- "Wrap your component inside <PromptInputProvider> to use usePromptInputController().",
118
- );
119
- }
120
- return ctx;
121
- };
122
- const useOptionalPromptInputController = () =>
123
- useContext(PromptInputController);
124
- export const useProviderAttachments = () => {
125
- const ctx = useContext(ProviderAttachmentsContext);
126
- if (!ctx) {
127
- throw new Error(
128
- "Wrap your component inside <PromptInputProvider> to use useProviderAttachments().",
129
- );
130
- }
131
- return ctx;
132
- };
133
- const useOptionalProviderAttachments = () =>
134
- useContext(ProviderAttachmentsContext);
135
- export type PromptInputProviderProps = PropsWithChildren<{
136
- initialInput?: string;
137
- }>;
138
- export const PromptInputProvider = ({
139
- initialInput: initialTextInput = "",
140
- children,
141
- }: PromptInputProviderProps) => {
142
- const [textInput, setTextInput] = useState(initialTextInput);
143
- const clearInput = () => setTextInput("");
144
- const [attachmentFiles, setAttachmentFiles] = useState<
145
- (FileUIPart & {
146
- id: string;
147
- })[]
148
- >([]);
149
- const fileInputRef = useRef<HTMLInputElement | null>(null);
150
- const openRef = useRef<() => void>(() => {});
151
- const add = (files: File[] | FileList) => {
152
- const incoming = [...files];
153
- if (incoming.length === 0) return;
154
- setAttachmentFiles((prev) => [
155
- ...prev,
156
- ...incoming.map((file) => ({
157
- filename: file.name,
158
- id: nanoid(),
159
- mediaType: file.type,
160
- type: "file" as const,
161
- url: URL.createObjectURL(file),
162
- })),
163
- ]);
164
- };
165
- const remove = (id: string) => {
166
- setAttachmentFiles((prev) => {
167
- const found = prev.find((f) => f.id === id);
168
- if (found?.url) URL.revokeObjectURL(found.url);
169
- return prev.filter((f) => f.id !== id);
170
- });
171
- };
172
- const clear = () => {
173
- setAttachmentFiles((prev) => {
174
- for (const f of prev) {
175
- if (f.url) URL.revokeObjectURL(f.url);
176
- }
177
- return [];
178
- });
179
- };
180
- const attachmentsRef = useRef(attachmentFiles);
181
- useEffect(() => {
182
- attachmentsRef.current = attachmentFiles;
183
- }, [attachmentFiles]);
184
- useEffect(
185
- () => () => {
186
- for (const f of attachmentsRef.current) {
187
- if (f.url) URL.revokeObjectURL(f.url);
188
- }
189
- },
190
- [],
191
- );
192
- const openFileDialog = () => {
193
- openRef.current?.();
194
- };
195
- const attachments = {
196
- add,
197
- clear,
198
- fileInputRef,
199
- files: attachmentFiles,
200
- openFileDialog,
201
- remove,
202
- };
203
- const __registerFileInput = (
204
- ref: RefObject<HTMLInputElement | null>,
205
- open: () => void,
206
- ) => {
207
- fileInputRef.current = ref.current;
208
- openRef.current = open;
209
- };
210
- const controller = {
211
- __registerFileInput,
212
- attachments,
213
- textInput: {
214
- clear: clearInput,
215
- setInput: setTextInput,
216
- value: textInput,
217
- },
218
- };
219
- return (
220
- <PromptInputController.Provider value={controller}>
221
- <ProviderAttachmentsContext.Provider value={attachments}>
222
- {children}
223
- </ProviderAttachmentsContext.Provider>
224
- </PromptInputController.Provider>
225
- );
226
- };
227
- const LocalAttachmentsContext = createContext<AttachmentsContext | null>(null);
228
- export const usePromptInputAttachments = () => {
229
- const provider = useOptionalProviderAttachments();
230
- const local = useContext(LocalAttachmentsContext);
231
- const context = local ?? provider;
232
- if (!context) {
233
- throw new Error(
234
- "usePromptInputAttachments must be used within a PromptInput or PromptInputProvider",
235
- );
236
- }
237
- return context;
238
- };
239
- export interface ReferencedSourcesContext {
240
- sources: (SourceDocumentUIPart & {
241
- id: string;
242
- })[];
243
- add: (sources: SourceDocumentUIPart[] | SourceDocumentUIPart) => void;
244
- remove: (id: string) => void;
245
- clear: () => void;
246
- }
247
- export const LocalReferencedSourcesContext =
248
- createContext<ReferencedSourcesContext | null>(null);
249
- export const usePromptInputReferencedSources = () => {
250
- const ctx = useContext(LocalReferencedSourcesContext);
251
- if (!ctx) {
252
- throw new Error(
253
- "usePromptInputReferencedSources must be used within a LocalReferencedSourcesContext.Provider",
254
- );
255
- }
256
- return ctx;
257
- };
258
- export type PromptInputActionAddAttachmentsProps = ComponentProps<
259
- typeof DropdownItem
260
- > & {
261
- label?: string;
262
- };
263
- export const PromptInputActionAddAttachments = ({
264
- label = "Add photos or files",
265
- ...props
266
- }: PromptInputActionAddAttachmentsProps) => {
267
- const attachments = usePromptInputAttachments();
268
- const handleClick = (e: MouseEvent<HTMLDivElement>) => {
269
- e.preventDefault();
270
- attachments.openFileDialog();
271
- };
272
- return (
273
- <DropdownItem {...props} onClick={handleClick}>
274
- <IconPhoto className="mr-2 size-4" /> {label}
275
- </DropdownItem>
276
- );
277
- };
278
- export interface PromptInputMessage {
279
- text: string;
280
- files: FileUIPart[];
281
- }
282
- export type PromptInputProps = Omit<
283
- HTMLAttributes<HTMLFormElement>,
284
- "onSubmit" | "onError"
285
- > & {
286
- accept?: string;
287
- multiple?: boolean;
288
- globalDrop?: boolean;
289
- syncHiddenInput?: boolean;
290
- maxFiles?: number;
291
- maxFileSize?: number;
292
- containerClassName?: string;
293
- onError?: (err: {
294
- code: "max_files" | "max_file_size" | "accept";
295
- message: string;
296
- }) => void;
297
- onSubmit: (
298
- message: PromptInputMessage,
299
- event: FormEvent<HTMLFormElement>,
300
- ) => void | Promise<void>;
301
- };
302
- export const PromptInput = ({
303
- className,
304
- accept,
305
- multiple,
306
- globalDrop,
307
- syncHiddenInput,
308
- maxFiles,
309
- maxFileSize,
310
- containerClassName,
311
- onError,
312
- onSubmit,
313
- children,
314
- ...props
315
- }: PromptInputProps) => {
316
- const controller = useOptionalPromptInputController();
317
- const usingProvider = !!controller;
318
- const inputRef = useRef<HTMLInputElement | null>(null);
319
- const formRef = useRef<HTMLFormElement | null>(null);
320
- const [items, setItems] = useState<
321
- (FileUIPart & {
322
- id: string;
323
- })[]
324
- >([]);
325
- const files = usingProvider ? controller.attachments.files : items;
326
- const [referencedSources, setReferencedSources] = useState<
327
- (SourceDocumentUIPart & {
328
- id: string;
329
- })[]
330
- >([]);
331
- const filesRef = useRef(files);
332
- useEffect(() => {
333
- filesRef.current = files;
334
- }, [files]);
335
- const openFileDialogLocal = () => {
336
- inputRef.current?.click();
337
- };
338
- const matchesAccept = (f: File) => {
339
- if (!accept || accept.trim() === "") return true;
340
- const patterns = accept
341
- .split(",")
342
- .map((s) => s.trim())
343
- .filter(Boolean);
344
- return patterns.some((pattern) => {
345
- if (pattern.endsWith("/*")) {
346
- const prefix = pattern.slice(0, -1);
347
- return f.type.startsWith(prefix);
348
- }
349
- return f.type === pattern;
350
- });
351
- };
352
- const addLocal = (fileList: File[] | FileList) => {
353
- const incoming = [...fileList];
354
- const accepted = incoming.filter((f) => matchesAccept(f));
355
- if (incoming.length && accepted.length === 0) {
356
- onError?.({
357
- code: "accept",
358
- message: "No files match the accepted types.",
359
- });
360
- return;
361
- }
362
- const withinSize = (f: File) =>
363
- maxFileSize ? f.size <= maxFileSize : true;
364
- const sized = accepted.filter(withinSize);
365
- if (accepted.length > 0 && sized.length === 0) {
366
- onError?.({
367
- code: "max_file_size",
368
- message: "All files exceed the maximum size.",
369
- });
370
- return;
371
- }
372
- setItems((prev) => {
373
- const capacity =
374
- typeof maxFiles === "number"
375
- ? Math.max(0, maxFiles - prev.length)
376
- : undefined;
377
- const capped =
378
- typeof capacity === "number" ? sized.slice(0, capacity) : sized;
379
- if (typeof capacity === "number" && sized.length > capacity) {
380
- onError?.({
381
- code: "max_files",
382
- message: "Too many files. Some were not added.",
383
- });
384
- }
385
- const next: (FileUIPart & {
386
- id: string;
387
- })[] = [];
388
- for (const file of capped) {
389
- next.push({
390
- filename: file.name,
391
- id: nanoid(),
392
- mediaType: file.type,
393
- type: "file",
394
- url: URL.createObjectURL(file),
395
- });
396
- }
397
- return [...prev, ...next];
398
- });
399
- };
400
- const removeLocal = (id: string) =>
401
- setItems((prev) => {
402
- const found = prev.find((file) => file.id === id);
403
- if (found?.url) URL.revokeObjectURL(found.url);
404
- return prev.filter((file) => file.id !== id);
405
- });
406
- const addWithProviderValidation = (fileList: File[] | FileList) => {
407
- const incoming = [...fileList];
408
- const accepted = incoming.filter((f) => matchesAccept(f));
409
- if (incoming.length && accepted.length === 0) {
410
- onError?.({
411
- code: "accept",
412
- message: "No files match the accepted types.",
413
- });
414
- return;
415
- }
416
- const withinSize = (f: File) =>
417
- maxFileSize ? f.size <= maxFileSize : true;
418
- const sized = accepted.filter(withinSize);
419
- if (accepted.length > 0 && sized.length === 0) {
420
- onError?.({
421
- code: "max_file_size",
422
- message: "All files exceed the maximum size.",
423
- });
424
- return;
425
- }
426
- const currentCount = files.length;
427
- const capacity =
428
- typeof maxFiles === "number"
429
- ? Math.max(0, maxFiles - currentCount)
430
- : undefined;
431
- const capped =
432
- typeof capacity === "number" ? sized.slice(0, capacity) : sized;
433
- if (typeof capacity === "number" && sized.length > capacity) {
434
- onError?.({
435
- code: "max_files",
436
- message: "Too many files. Some were not added.",
437
- });
438
- }
439
- if (capped.length > 0) {
440
- controller?.attachments.add(capped);
441
- }
442
- };
443
- const clearAttachments = () =>
444
- usingProvider
445
- ? controller?.attachments.clear()
446
- : setItems((prev) => {
447
- for (const file of prev) {
448
- if (file.url) URL.revokeObjectURL(file.url);
449
- }
450
- return [];
451
- });
452
- const clearReferencedSources = () => setReferencedSources([]);
453
- const add = usingProvider ? addWithProviderValidation : addLocal;
454
- const remove = usingProvider ? controller.attachments.remove : removeLocal;
455
- const openFileDialog = usingProvider
456
- ? controller.attachments.openFileDialog
457
- : openFileDialogLocal;
458
- const clear = () => {
459
- clearAttachments();
460
- clearReferencedSources();
461
- };
462
- useEffect(() => {
463
- if (!usingProvider) return;
464
- controller.__registerFileInput(inputRef, () => inputRef.current?.click());
465
- }, [usingProvider, controller]);
466
- useEffect(() => {
467
- if (syncHiddenInput && inputRef.current && files.length === 0) {
468
- inputRef.current.value = "";
469
- }
470
- }, [files, syncHiddenInput]);
471
- useEffect(() => {
472
- const form = formRef.current;
473
- if (!form) return;
474
- if (globalDrop) return;
475
- const onDragOver = (e: DragEvent) => {
476
- if (e.dataTransfer?.types?.includes("Files")) e.preventDefault();
477
- };
478
- const onDrop = (e: DragEvent) => {
479
- if (e.dataTransfer?.types?.includes("Files")) e.preventDefault();
480
- if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {
481
- add(e.dataTransfer.files);
482
- }
483
- };
484
- form.addEventListener("dragover", onDragOver);
485
- form.addEventListener("drop", onDrop);
486
- return () => {
487
- form.removeEventListener("dragover", onDragOver);
488
- form.removeEventListener("drop", onDrop);
489
- };
490
- }, [add, globalDrop]);
491
- useEffect(() => {
492
- if (!globalDrop) return;
493
- const onDragOver = (e: DragEvent) => {
494
- if (e.dataTransfer?.types?.includes("Files")) e.preventDefault();
495
- };
496
- const onDrop = (e: DragEvent) => {
497
- if (e.dataTransfer?.types?.includes("Files")) e.preventDefault();
498
- if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {
499
- add(e.dataTransfer.files);
500
- }
501
- };
502
- document.addEventListener("dragover", onDragOver);
503
- document.addEventListener("drop", onDrop);
504
- return () => {
505
- document.removeEventListener("dragover", onDragOver);
506
- document.removeEventListener("drop", onDrop);
507
- };
508
- }, [add, globalDrop]);
509
- useEffect(
510
- () => () => {
511
- if (!usingProvider) {
512
- for (const f of filesRef.current) {
513
- if (f.url) URL.revokeObjectURL(f.url);
514
- }
515
- }
516
- },
517
- [usingProvider],
518
- );
519
- const handleChange: ChangeEventHandler<HTMLInputElement> = (event) => {
520
- if (event.currentTarget.files) add(event.currentTarget.files);
521
- event.currentTarget.value = "";
522
- };
523
- const attachmentsCtx = {
524
- add,
525
- clear: clearAttachments,
526
- fileInputRef: inputRef,
527
- files: files.map((item) => ({ ...item, id: item.id })),
528
- openFileDialog,
529
- remove,
530
- };
531
- const refsCtx = {
532
- add: (incoming: SourceDocumentUIPart[] | SourceDocumentUIPart) => {
533
- const array = Array.isArray(incoming) ? incoming : [incoming];
534
- setReferencedSources((prev) => [
535
- ...prev,
536
- ...array.map((s) => ({ ...s, id: nanoid() })),
537
- ]);
538
- },
539
- clear: clearReferencedSources,
540
- remove: (id: string) => {
541
- setReferencedSources((prev) => prev.filter((s) => s.id !== id));
542
- },
543
- sources: referencedSources,
544
- };
545
- const handleSubmit: FormEventHandler<HTMLFormElement> = async (event) => {
546
- event.preventDefault();
547
- const form = event.currentTarget;
548
- const text = usingProvider
549
- ? controller.textInput.value
550
- : (() => {
551
- const formData = new FormData(form);
552
- return (formData.get("message") as string) || "";
553
- })();
554
- if (!usingProvider) form.reset();
555
- try {
556
- const convertedFiles: FileUIPart[] = await Promise.all(
557
- files.map(async ({ id: _id, ...item }) => {
558
- if (item.url?.startsWith("blob:")) {
559
- const dataUrl = await convertBlobUrlToDataUrl(item.url);
560
- return { ...item, url: dataUrl ?? item.url };
561
- }
562
- return item;
563
- }),
564
- );
565
- const result = onSubmit({ files: convertedFiles, text }, event);
566
- if (result instanceof Promise) {
567
- try {
568
- await result;
569
- clear();
570
- if (usingProvider) controller.textInput.clear();
571
- } catch {
572
- /* Don't clear on error */
573
- }
574
- } else {
575
- clear();
576
- if (usingProvider) controller.textInput.clear();
577
- }
578
- } catch {
579
- /* Don't clear on error */
580
- }
581
- };
582
- const inner = (
583
- <>
584
- <input
585
- accept={accept}
586
- aria-label="Upload files"
587
- className="hidden"
588
- multiple={multiple}
589
- onChange={handleChange}
590
- ref={inputRef}
591
- title="Upload files"
592
- type="file"
593
- />
594
- <form
595
- className={cn("w-full", className)}
596
- onSubmit={handleSubmit}
597
- ref={formRef}
598
- {...props}
599
- >
600
- <InputGroup className={cn("overflow-hidden", containerClassName)}>
601
- {children}
602
- </InputGroup>
603
- </form>
604
- </>
605
- );
606
- const withReferencedSources = (
607
- <LocalReferencedSourcesContext.Provider value={refsCtx}>
608
- {inner}
609
- </LocalReferencedSourcesContext.Provider>
610
- );
611
- return (
612
- <LocalAttachmentsContext.Provider value={attachmentsCtx}>
613
- {withReferencedSources}
614
- </LocalAttachmentsContext.Provider>
615
- );
616
- };
617
- export type PromptInputBodyProps = HTMLAttributes<HTMLDivElement>;
618
- export const PromptInputBody = ({
619
- className,
620
- ...props
621
- }: PromptInputBodyProps) => (
622
- <div className={cn("contents", className)} {...props} />
623
- );
624
- export type PromptInputTextareaProps = ComponentProps<
625
- typeof InputGroupTextarea
626
- >;
627
- export const PromptInputTextarea = ({
628
- onChange,
629
- onKeyDown,
630
- className,
631
- placeholder = "What would you like to know?",
632
- ...props
633
- }: PromptInputTextareaProps) => {
634
- const controller = useOptionalPromptInputController();
635
- const attachments = usePromptInputAttachments();
636
- const [isComposing, setIsComposing] = useState(false);
637
- const handleKeyDown: KeyboardEventHandler<HTMLTextAreaElement> = (e) => {
638
- onKeyDown?.(e);
639
- if (e.defaultPrevented) return;
640
- if (e.key === "Enter") {
641
- if (isComposing || e.nativeEvent.isComposing) return;
642
- if (e.shiftKey) return;
643
- e.preventDefault();
644
- const { form } = e.currentTarget;
645
- const submitButton = form?.querySelector(
646
- 'button[type="submit"]',
647
- ) as HTMLButtonElement | null;
648
- if (submitButton?.disabled) return;
649
- form?.requestSubmit();
650
- }
651
- if (
652
- e.key === "Backspace" &&
653
- e.currentTarget.value === "" &&
654
- attachments.files.length > 0
655
- ) {
656
- e.preventDefault();
657
- const lastAttachment = attachments.files.at(-1);
658
- if (lastAttachment) attachments.remove(lastAttachment.id);
659
- }
660
- };
661
- const handlePaste: ClipboardEventHandler<HTMLTextAreaElement> = (event) => {
662
- const items = event.clipboardData?.items;
663
- if (!items) return;
664
- const pastedFiles: File[] = [];
665
- for (const item of items) {
666
- if (item.kind === "file") {
667
- const file = item.getAsFile();
668
- if (file) pastedFiles.push(file);
669
- }
670
- }
671
- if (pastedFiles.length > 0) {
672
- event.preventDefault();
673
- attachments.add(pastedFiles);
674
- }
675
- };
676
- const handleCompositionEnd = () => setIsComposing(false);
677
- const handleCompositionStart = () => setIsComposing(true);
678
- const controlledProps = controller
679
- ? {
680
- onChange: (e: ChangeEvent<HTMLTextAreaElement>) => {
681
- controller.textInput.setInput(e.currentTarget.value);
682
- onChange?.(e);
683
- },
684
- value: controller.textInput.value,
685
- }
686
- : { onChange };
687
- return (
688
- <InputGroupTextarea
689
- className={cn("field-sizing-content max-h-48 min-h-16", className)}
690
- name="message"
691
- onCompositionEnd={handleCompositionEnd}
692
- onCompositionStart={handleCompositionStart}
693
- onKeyDown={handleKeyDown}
694
- onPaste={handlePaste}
695
- placeholder={placeholder}
696
- {...props}
697
- {...controlledProps}
698
- />
699
- );
700
- };
701
- export type PromptInputHeaderProps = Omit<
702
- ComponentProps<typeof InputGroupAddon>,
703
- "align"
704
- >;
705
- export const PromptInputHeader = ({
706
- className,
707
- ...props
708
- }: PromptInputHeaderProps) => (
709
- <InputGroupAddon
710
- align="block-end"
711
- className={cn("order-first flex-wrap gap-1", className)}
712
- {...props}
713
- />
714
- );
715
- export type PromptInputFooterProps = Omit<
716
- ComponentProps<typeof InputGroupAddon>,
717
- "align"
718
- >;
719
- export const PromptInputFooter = ({
720
- className,
721
- ...props
722
- }: PromptInputFooterProps) => (
723
- <InputGroupAddon
724
- align="block-end"
725
- className={cn("justify-between gap-1", className)}
726
- {...props}
727
- />
728
- );
729
- export type PromptInputToolsProps = HTMLAttributes<HTMLDivElement>;
730
- export const PromptInputTools = ({
731
- className,
732
- ...props
733
- }: PromptInputToolsProps) => (
734
- <div
735
- className={cn("flex min-w-0 items-center gap-1", className)}
736
- {...props}
737
- />
738
- );
739
- export type PromptInputButtonTooltip =
740
- | string
741
- | {
742
- content: ReactNode;
743
- shortcut?: string;
744
- side?: ComponentProps<typeof TooltipContent>["side"];
745
- };
746
- export type PromptInputButtonProps = ComponentProps<typeof InputGroupButton> & {
747
- tooltip?: PromptInputButtonTooltip;
748
- };
749
- export const PromptInputButton = ({
750
- variant = "ghost",
751
- className,
752
- size,
753
- tooltip,
754
- ...props
755
- }: PromptInputButtonProps) => {
756
- const newSize =
757
- size ?? (Children.count(props.children) > 1 ? "sm" : "icon-sm");
758
- const button = (
759
- <InputGroupButton
760
- className={cn(className)}
761
- size={newSize}
762
- type="button"
763
- variant={variant}
764
- {...props}
765
- />
766
- );
767
- if (!tooltip) return button;
768
- const tooltipContent =
769
- typeof tooltip === "string" ? tooltip : tooltip.content;
770
- const shortcut = typeof tooltip === "string" ? undefined : tooltip.shortcut;
771
- const side = typeof tooltip === "string" ? "top" : tooltip.side ?? "top";
772
- return (
773
- <Tooltip>
774
- <TooltipTrigger asChild>{button}</TooltipTrigger>
775
- <TooltipContent side={side}>
776
- {tooltipContent}
777
- {shortcut && <span className="ml-2 text-muted">{shortcut}</span>}
778
- </TooltipContent>
779
- </Tooltip>
780
- );
781
- };
782
- export type PromptInputActionMenuProps = ComponentProps<typeof Dropdown>;
783
- export const PromptInputActionMenu = (props: PromptInputActionMenuProps) => (
784
- <Dropdown {...props} />
785
- );
786
- export type PromptInputActionMenuTriggerProps = PromptInputButtonProps;
787
- export const PromptInputActionMenuTrigger = ({
788
- className,
789
- children,
790
- ...props
791
- }: PromptInputActionMenuTriggerProps) => (
792
- <DropdownTrigger>
793
- <PromptInputButton className={className} {...props}>
794
- {children ?? <IconPlus className="size-4" />}
795
- </PromptInputButton>
796
- </DropdownTrigger>
797
- );
798
- export type PromptInputActionMenuContentProps = ComponentProps<
799
- typeof DropdownMenu
800
- >;
801
- export const PromptInputActionMenuContent = ({
802
- className,
803
- ...props
804
- }: PromptInputActionMenuContentProps) => (
805
- <DropdownMenu align="start" className={cn(className)} {...props} />
806
- );
807
- export type PromptInputActionMenuItemProps = ComponentProps<
808
- typeof DropdownItem
809
- >;
810
- export const PromptInputActionMenuItem = ({
811
- className,
812
- ...props
813
- }: PromptInputActionMenuItemProps) => (
814
- <DropdownItem className={cn(className)} {...props} />
815
- );
816
- export type PromptInputSubmitProps = ComponentProps<typeof InputGroupButton> & {
817
- status?: ChatStatus;
818
- onStop?: () => void;
819
- };
820
- export const PromptInputSubmit = ({
821
- className,
822
- variant = "default",
823
- size = "icon-sm",
824
- status,
825
- onStop,
826
- onClick,
827
- children,
828
- ...props
829
- }: PromptInputSubmitProps) => {
830
- const isGenerating = status === "submitted" || status === "streaming";
831
- let Icon = <IconCornerDownLeft className="size-4" />;
832
- if (status === "submitted") {
833
- Icon = <Spinner size="sm" />;
834
- } else if (status === "streaming") {
835
- Icon = <IconSquare className="size-4" />;
836
- } else if (status === "error") {
837
- Icon = <IconX className="size-4" />;
838
- }
839
- const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
840
- if (isGenerating && onStop) {
841
- e.preventDefault();
842
- onStop();
843
- return;
844
- }
845
- onClick?.(e);
846
- };
847
- return (
848
- <InputGroupButton
849
- aria-label={isGenerating ? "Stop" : "Submit"}
850
- className={cn(className)}
851
- onClick={handleClick}
852
- size={size}
853
- type={isGenerating && onStop ? "button" : "submit"}
854
- variant={variant}
855
- {...props}
856
- >
857
- {children ?? Icon}
858
- </InputGroupButton>
859
- );
860
- };
861
- export type PromptInputModelSelectProps = ComponentProps<typeof Select>;
862
- export const PromptInputModelSelect = (props: PromptInputModelSelectProps) => (
863
- <Select {...props} />
864
- );
865
- export type PromptInputModelSelectTriggerProps = ComponentProps<
866
- typeof SelectTrigger
867
- >;
868
- export const PromptInputModelSelectTrigger = ({
869
- className,
870
- ...props
871
- }: PromptInputModelSelectTriggerProps) => (
872
- <SelectTrigger
873
- className={cn(
874
- "border-none bg-transparent font-medium text-muted shadow-none transition-[color,transform] duration-150 ease-out active:scale-[0.97]",
875
- "hover:bg-surface-secondary hover:text-foreground",
876
- segmentExpandedStateClass,
877
- className,
878
- )}
879
- {...props}
880
- />
881
- );
882
- export type PromptInputModelSelectContentProps = ComponentProps<
883
- typeof SelectContent
884
- >;
885
- export const PromptInputModelSelectContent = ({
886
- className,
887
- ...props
888
- }: PromptInputModelSelectContentProps) => (
889
- <SelectContent className={cn(className)} {...props} />
890
- );
891
- export type PromptInputModelSelectItemProps = ComponentProps<typeof SelectItem>;
892
- export const PromptInputModelSelectItem = ({
893
- className,
894
- ...props
895
- }: PromptInputModelSelectItemProps) => (
896
- <SelectItem className={cn(className)} {...props} />
897
- );
898
- export type PromptInputModelSelectValueProps = ComponentProps<
899
- typeof SelectValue
900
- >;
901
- export const PromptInputModelSelectValue = ({
902
- className,
903
- ...props
904
- }: PromptInputModelSelectValueProps) => (
905
- <SelectValue className={cn(className)} {...props} />
906
- );
907
- export type PromptInputHoverCardProps = ComponentProps<typeof HoverCard>;
908
- export const PromptInputHoverCard = ({
909
- openDelay = 0,
910
- closeDelay = 0,
911
- ...props
912
- }: PromptInputHoverCardProps) => (
913
- <HoverCard closeDelay={closeDelay} openDelay={openDelay} {...props} />
914
- );
915
- export type PromptInputHoverCardTriggerProps = ComponentProps<
916
- typeof HoverCardTrigger
917
- >;
918
- export const PromptInputHoverCardTrigger = (
919
- props: PromptInputHoverCardTriggerProps,
920
- ) => <HoverCardTrigger {...props} />;
921
- export type PromptInputHoverCardContentProps = ComponentProps<
922
- typeof HoverCardContent
923
- >;
924
- export const PromptInputHoverCardContent = ({
925
- align = "start",
926
- ...props
927
- }: PromptInputHoverCardContentProps) => (
928
- <HoverCardContent align={align} {...props} />
929
- );
930
- export type PromptInputTabsListProps = HTMLAttributes<HTMLDivElement>;
931
- export const PromptInputTabsList = ({
932
- className,
933
- ...props
934
- }: PromptInputTabsListProps) => <div className={cn(className)} {...props} />;
935
- export type PromptInputTabProps = HTMLAttributes<HTMLDivElement>;
936
- export const PromptInputTab = ({
937
- className,
938
- ...props
939
- }: PromptInputTabProps) => <div className={cn(className)} {...props} />;
940
- export type PromptInputTabLabelProps = HTMLAttributes<HTMLHeadingElement>;
941
- export const PromptInputTabLabel = ({
942
- className,
943
- ...props
944
- }: PromptInputTabLabelProps) => (
945
- <h3
946
- className={cn("mb-2 px-3 font-medium text-muted text-xs", className)}
947
- {...props}
948
- />
949
- );
950
- export type PromptInputTabBodyProps = HTMLAttributes<HTMLDivElement>;
951
- export const PromptInputTabBody = ({
952
- className,
953
- ...props
954
- }: PromptInputTabBodyProps) => (
955
- <div className={cn("space-y-1", className)} {...props} />
956
- );
957
- export type PromptInputTabItemProps = HTMLAttributes<HTMLDivElement>;
958
- export const PromptInputTabItem = ({
959
- className,
960
- ...props
961
- }: PromptInputTabItemProps) => (
962
- <div
963
- className={cn(
964
- "flex items-center gap-2 rounded-md px-3 py-2 text-xs transition-[background-color,transform] duration-150 ease-out active:scale-[0.99] hover:bg-surface-secondary",
965
- className,
966
- )}
967
- {...props}
968
- />
969
- );
970
- export type PromptInputCommandProps = ComponentProps<typeof Command>;
971
- export const PromptInputCommand = ({
972
- className,
973
- ...props
974
- }: PromptInputCommandProps) => <Command className={cn(className)} {...props} />;
975
- export type PromptInputCommandInputProps = ComponentProps<typeof CommandInput>;
976
- export const PromptInputCommandInput = ({
977
- className,
978
- ...props
979
- }: PromptInputCommandInputProps) => (
980
- <CommandInput className={cn(className)} {...props} />
981
- );
982
- export type PromptInputCommandListProps = ComponentProps<typeof CommandList>;
983
- export const PromptInputCommandList = ({
984
- className,
985
- ...props
986
- }: PromptInputCommandListProps) => (
987
- <CommandList className={cn(className)} {...props} />
988
- );
989
- export type PromptInputCommandEmptyProps = ComponentProps<typeof CommandEmpty>;
990
- export const PromptInputCommandEmpty = ({
991
- className,
992
- ...props
993
- }: PromptInputCommandEmptyProps) => (
994
- <CommandEmpty className={cn(className)} {...props} />
995
- );
996
- export type PromptInputCommandGroupProps = ComponentProps<typeof CommandGroup>;
997
- export const PromptInputCommandGroup = ({
998
- className,
999
- ...props
1000
- }: PromptInputCommandGroupProps) => (
1001
- <CommandGroup className={cn(className)} {...props} />
1002
- );
1003
- export type PromptInputCommandItemProps = ComponentProps<typeof CommandItem>;
1004
- export const PromptInputCommandItem = ({
1005
- className,
1006
- ...props
1007
- }: PromptInputCommandItemProps) => (
1008
- <CommandItem className={cn(className)} {...props} />
1009
- );
1010
- export type PromptInputCommandSeparatorProps = ComponentProps<
1011
- typeof CommandSeparator
1012
- >;
1013
- export const PromptInputCommandSeparator = ({
1014
- className,
1015
- ...props
1016
- }: PromptInputCommandSeparatorProps) => (
1017
- <CommandSeparator className={cn(className)} {...props} />
1018
- );
1
+ "use client";
2
+ import type { ChatStatus, FileUIPart, SourceDocumentUIPart } from "ai";
3
+ import type {
4
+ ChangeEvent,
5
+ ChangeEventHandler,
6
+ ClipboardEventHandler,
7
+ ComponentProps,
8
+ FormEvent,
9
+ FormEventHandler,
10
+ HTMLAttributes,
11
+ KeyboardEventHandler,
12
+ MouseEvent,
13
+ PropsWithChildren,
14
+ ReactNode,
15
+ RefObject,
16
+ } from "react";
17
+ import {
18
+ Command,
19
+ CommandEmpty,
20
+ CommandGroup,
21
+ CommandInput,
22
+ CommandItem,
23
+ CommandList,
24
+ CommandSeparator,
25
+ } from "../ui/command";
26
+ import {
27
+ DropdownMenu as Dropdown,
28
+ DropdownMenuTrigger as DropdownTrigger,
29
+ DropdownMenuContent as DropdownMenu,
30
+ DropdownMenuItem as DropdownItem,
31
+ } from "../ui/dropdown-menu";
32
+ import {
33
+ HoverCard,
34
+ HoverCardContent,
35
+ HoverCardTrigger,
36
+ } from "../ui/hover-card";
37
+ import {
38
+ InputGroup,
39
+ InputGroupAddon,
40
+ InputGroupButton,
41
+ InputGroupTextarea,
42
+ } from "../ui/input-group";
43
+ import {
44
+ Select,
45
+ SelectContent,
46
+ SelectItem,
47
+ SelectTrigger,
48
+ SelectValue,
49
+ } from "../ui/radix-select";
50
+ import { Spinner } from "../ui/spinner";
51
+ import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
52
+ import { cn } from "../utils/cn";
53
+ import { segmentExpandedStateClass } from "../ui/surface-classes";
54
+ import {
55
+ IconCornerDownLeft,
56
+ IconPhoto,
57
+ IconPlus,
58
+ IconSquare,
59
+ IconX,
60
+ } from "@tabler/icons-react";
61
+ import { nanoid } from "nanoid";
62
+ import {
63
+ Children,
64
+ createContext,
65
+ useContext,
66
+ useEffect,
67
+ useRef,
68
+ useState,
69
+ } from "react";
70
+ const convertBlobUrlToDataUrl = async (url: string): Promise<string | null> => {
71
+ try {
72
+ const response = await fetch(url);
73
+ const blob = await response.blob();
74
+ return new Promise((resolve) => {
75
+ const reader = new FileReader();
76
+ reader.onloadend = () => resolve(reader.result as string);
77
+ reader.onerror = () => resolve(null);
78
+ reader.readAsDataURL(blob);
79
+ });
80
+ } catch {
81
+ return null;
82
+ }
83
+ };
84
+ export interface AttachmentsContext {
85
+ files: (FileUIPart & {
86
+ id: string;
87
+ })[];
88
+ add: (files: File[] | FileList) => void;
89
+ remove: (id: string) => void;
90
+ clear: () => void;
91
+ openFileDialog: () => void;
92
+ fileInputRef: RefObject<HTMLInputElement | null>;
93
+ }
94
+ export interface TextInputContext {
95
+ value: string;
96
+ setInput: (v: string) => void;
97
+ clear: () => void;
98
+ }
99
+ export interface PromptInputControllerProps {
100
+ textInput: TextInputContext;
101
+ attachments: AttachmentsContext;
102
+ __registerFileInput: (
103
+ ref: RefObject<HTMLInputElement | null>,
104
+ open: () => void,
105
+ ) => void;
106
+ }
107
+ const PromptInputController = createContext<PromptInputControllerProps | null>(
108
+ null,
109
+ );
110
+ const ProviderAttachmentsContext = createContext<AttachmentsContext | null>(
111
+ null,
112
+ );
113
+ export const usePromptInputController = () => {
114
+ const ctx = useContext(PromptInputController);
115
+ if (!ctx) {
116
+ throw new Error(
117
+ "Wrap your component inside <PromptInputProvider> to use usePromptInputController().",
118
+ );
119
+ }
120
+ return ctx;
121
+ };
122
+ const useOptionalPromptInputController = () =>
123
+ useContext(PromptInputController);
124
+ export const useProviderAttachments = () => {
125
+ const ctx = useContext(ProviderAttachmentsContext);
126
+ if (!ctx) {
127
+ throw new Error(
128
+ "Wrap your component inside <PromptInputProvider> to use useProviderAttachments().",
129
+ );
130
+ }
131
+ return ctx;
132
+ };
133
+ const useOptionalProviderAttachments = () =>
134
+ useContext(ProviderAttachmentsContext);
135
+ export type PromptInputProviderProps = PropsWithChildren<{
136
+ initialInput?: string;
137
+ }>;
138
+ export const PromptInputProvider = ({
139
+ initialInput: initialTextInput = "",
140
+ children,
141
+ }: PromptInputProviderProps) => {
142
+ const [textInput, setTextInput] = useState(initialTextInput);
143
+ const clearInput = () => setTextInput("");
144
+ const [attachmentFiles, setAttachmentFiles] = useState<
145
+ (FileUIPart & {
146
+ id: string;
147
+ })[]
148
+ >([]);
149
+ const fileInputRef = useRef<HTMLInputElement | null>(null);
150
+ const openRef = useRef<() => void>(() => {});
151
+ const add = (files: File[] | FileList) => {
152
+ const incoming = [...files];
153
+ if (incoming.length === 0) return;
154
+ setAttachmentFiles((prev) => [
155
+ ...prev,
156
+ ...incoming.map((file) => ({
157
+ filename: file.name,
158
+ id: nanoid(),
159
+ mediaType: file.type,
160
+ type: "file" as const,
161
+ url: URL.createObjectURL(file),
162
+ })),
163
+ ]);
164
+ };
165
+ const remove = (id: string) => {
166
+ setAttachmentFiles((prev) => {
167
+ const found = prev.find((f) => f.id === id);
168
+ if (found?.url) URL.revokeObjectURL(found.url);
169
+ return prev.filter((f) => f.id !== id);
170
+ });
171
+ };
172
+ const clear = () => {
173
+ setAttachmentFiles((prev) => {
174
+ for (const f of prev) {
175
+ if (f.url) URL.revokeObjectURL(f.url);
176
+ }
177
+ return [];
178
+ });
179
+ };
180
+ const attachmentsRef = useRef(attachmentFiles);
181
+ useEffect(() => {
182
+ attachmentsRef.current = attachmentFiles;
183
+ }, [attachmentFiles]);
184
+ useEffect(
185
+ () => () => {
186
+ for (const f of attachmentsRef.current) {
187
+ if (f.url) URL.revokeObjectURL(f.url);
188
+ }
189
+ },
190
+ [],
191
+ );
192
+ const openFileDialog = () => {
193
+ openRef.current?.();
194
+ };
195
+ const attachments = {
196
+ add,
197
+ clear,
198
+ fileInputRef,
199
+ files: attachmentFiles,
200
+ openFileDialog,
201
+ remove,
202
+ };
203
+ const __registerFileInput = (
204
+ ref: RefObject<HTMLInputElement | null>,
205
+ open: () => void,
206
+ ) => {
207
+ fileInputRef.current = ref.current;
208
+ openRef.current = open;
209
+ };
210
+ const controller = {
211
+ __registerFileInput,
212
+ attachments,
213
+ textInput: {
214
+ clear: clearInput,
215
+ setInput: setTextInput,
216
+ value: textInput,
217
+ },
218
+ };
219
+ return (
220
+ <PromptInputController.Provider value={controller}>
221
+ <ProviderAttachmentsContext.Provider value={attachments}>
222
+ {children}
223
+ </ProviderAttachmentsContext.Provider>
224
+ </PromptInputController.Provider>
225
+ );
226
+ };
227
+ const LocalAttachmentsContext = createContext<AttachmentsContext | null>(null);
228
+ export const usePromptInputAttachments = () => {
229
+ const provider = useOptionalProviderAttachments();
230
+ const local = useContext(LocalAttachmentsContext);
231
+ const context = local ?? provider;
232
+ if (!context) {
233
+ throw new Error(
234
+ "usePromptInputAttachments must be used within a PromptInput or PromptInputProvider",
235
+ );
236
+ }
237
+ return context;
238
+ };
239
+ export interface ReferencedSourcesContext {
240
+ sources: (SourceDocumentUIPart & {
241
+ id: string;
242
+ })[];
243
+ add: (sources: SourceDocumentUIPart[] | SourceDocumentUIPart) => void;
244
+ remove: (id: string) => void;
245
+ clear: () => void;
246
+ }
247
+ export const LocalReferencedSourcesContext =
248
+ createContext<ReferencedSourcesContext | null>(null);
249
+ export const usePromptInputReferencedSources = () => {
250
+ const ctx = useContext(LocalReferencedSourcesContext);
251
+ if (!ctx) {
252
+ throw new Error(
253
+ "usePromptInputReferencedSources must be used within a LocalReferencedSourcesContext.Provider",
254
+ );
255
+ }
256
+ return ctx;
257
+ };
258
+ export type PromptInputActionAddAttachmentsProps = ComponentProps<
259
+ typeof DropdownItem
260
+ > & {
261
+ label?: string;
262
+ };
263
+ export const PromptInputActionAddAttachments = ({
264
+ label = "Add photos or files",
265
+ ...props
266
+ }: PromptInputActionAddAttachmentsProps) => {
267
+ const attachments = usePromptInputAttachments();
268
+ const handleClick = (e: MouseEvent<HTMLDivElement>) => {
269
+ e.preventDefault();
270
+ attachments.openFileDialog();
271
+ };
272
+ return (
273
+ <DropdownItem {...props} onClick={handleClick}>
274
+ <IconPhoto className="mr-2 size-4" /> {label}
275
+ </DropdownItem>
276
+ );
277
+ };
278
+ export interface PromptInputMessage {
279
+ text: string;
280
+ files: FileUIPart[];
281
+ }
282
+ export type PromptInputProps = Omit<
283
+ HTMLAttributes<HTMLFormElement>,
284
+ "onSubmit" | "onError"
285
+ > & {
286
+ accept?: string;
287
+ multiple?: boolean;
288
+ globalDrop?: boolean;
289
+ syncHiddenInput?: boolean;
290
+ maxFiles?: number;
291
+ maxFileSize?: number;
292
+ containerClassName?: string;
293
+ onError?: (err: {
294
+ code: "max_files" | "max_file_size" | "accept";
295
+ message: string;
296
+ }) => void;
297
+ onSubmit: (
298
+ message: PromptInputMessage,
299
+ event: FormEvent<HTMLFormElement>,
300
+ ) => void | Promise<void>;
301
+ };
302
+ export const PromptInput = ({
303
+ className,
304
+ accept,
305
+ multiple,
306
+ globalDrop,
307
+ syncHiddenInput,
308
+ maxFiles,
309
+ maxFileSize,
310
+ containerClassName,
311
+ onError,
312
+ onSubmit,
313
+ children,
314
+ ...props
315
+ }: PromptInputProps) => {
316
+ const controller = useOptionalPromptInputController();
317
+ const usingProvider = !!controller;
318
+ const inputRef = useRef<HTMLInputElement | null>(null);
319
+ const formRef = useRef<HTMLFormElement | null>(null);
320
+ const [items, setItems] = useState<
321
+ (FileUIPart & {
322
+ id: string;
323
+ })[]
324
+ >([]);
325
+ const files = usingProvider ? controller.attachments.files : items;
326
+ const [referencedSources, setReferencedSources] = useState<
327
+ (SourceDocumentUIPart & {
328
+ id: string;
329
+ })[]
330
+ >([]);
331
+ const filesRef = useRef(files);
332
+ useEffect(() => {
333
+ filesRef.current = files;
334
+ }, [files]);
335
+ const openFileDialogLocal = () => {
336
+ inputRef.current?.click();
337
+ };
338
+ const matchesAccept = (f: File) => {
339
+ if (!accept || accept.trim() === "") return true;
340
+ const patterns = accept
341
+ .split(",")
342
+ .map((s) => s.trim())
343
+ .filter(Boolean);
344
+ return patterns.some((pattern) => {
345
+ if (pattern.endsWith("/*")) {
346
+ const prefix = pattern.slice(0, -1);
347
+ return f.type.startsWith(prefix);
348
+ }
349
+ return f.type === pattern;
350
+ });
351
+ };
352
+ const addLocal = (fileList: File[] | FileList) => {
353
+ const incoming = [...fileList];
354
+ const accepted = incoming.filter((f) => matchesAccept(f));
355
+ if (incoming.length && accepted.length === 0) {
356
+ onError?.({
357
+ code: "accept",
358
+ message: "No files match the accepted types.",
359
+ });
360
+ return;
361
+ }
362
+ const withinSize = (f: File) =>
363
+ maxFileSize ? f.size <= maxFileSize : true;
364
+ const sized = accepted.filter(withinSize);
365
+ if (accepted.length > 0 && sized.length === 0) {
366
+ onError?.({
367
+ code: "max_file_size",
368
+ message: "All files exceed the maximum size.",
369
+ });
370
+ return;
371
+ }
372
+ setItems((prev) => {
373
+ const capacity =
374
+ typeof maxFiles === "number"
375
+ ? Math.max(0, maxFiles - prev.length)
376
+ : undefined;
377
+ const capped =
378
+ typeof capacity === "number" ? sized.slice(0, capacity) : sized;
379
+ if (typeof capacity === "number" && sized.length > capacity) {
380
+ onError?.({
381
+ code: "max_files",
382
+ message: "Too many files. Some were not added.",
383
+ });
384
+ }
385
+ const next: (FileUIPart & {
386
+ id: string;
387
+ })[] = [];
388
+ for (const file of capped) {
389
+ next.push({
390
+ filename: file.name,
391
+ id: nanoid(),
392
+ mediaType: file.type,
393
+ type: "file",
394
+ url: URL.createObjectURL(file),
395
+ });
396
+ }
397
+ return [...prev, ...next];
398
+ });
399
+ };
400
+ const removeLocal = (id: string) =>
401
+ setItems((prev) => {
402
+ const found = prev.find((file) => file.id === id);
403
+ if (found?.url) URL.revokeObjectURL(found.url);
404
+ return prev.filter((file) => file.id !== id);
405
+ });
406
+ const addWithProviderValidation = (fileList: File[] | FileList) => {
407
+ const incoming = [...fileList];
408
+ const accepted = incoming.filter((f) => matchesAccept(f));
409
+ if (incoming.length && accepted.length === 0) {
410
+ onError?.({
411
+ code: "accept",
412
+ message: "No files match the accepted types.",
413
+ });
414
+ return;
415
+ }
416
+ const withinSize = (f: File) =>
417
+ maxFileSize ? f.size <= maxFileSize : true;
418
+ const sized = accepted.filter(withinSize);
419
+ if (accepted.length > 0 && sized.length === 0) {
420
+ onError?.({
421
+ code: "max_file_size",
422
+ message: "All files exceed the maximum size.",
423
+ });
424
+ return;
425
+ }
426
+ const currentCount = files.length;
427
+ const capacity =
428
+ typeof maxFiles === "number"
429
+ ? Math.max(0, maxFiles - currentCount)
430
+ : undefined;
431
+ const capped =
432
+ typeof capacity === "number" ? sized.slice(0, capacity) : sized;
433
+ if (typeof capacity === "number" && sized.length > capacity) {
434
+ onError?.({
435
+ code: "max_files",
436
+ message: "Too many files. Some were not added.",
437
+ });
438
+ }
439
+ if (capped.length > 0) {
440
+ controller?.attachments.add(capped);
441
+ }
442
+ };
443
+ const clearAttachments = () =>
444
+ usingProvider
445
+ ? controller?.attachments.clear()
446
+ : setItems((prev) => {
447
+ for (const file of prev) {
448
+ if (file.url) URL.revokeObjectURL(file.url);
449
+ }
450
+ return [];
451
+ });
452
+ const clearReferencedSources = () => setReferencedSources([]);
453
+ const add = usingProvider ? addWithProviderValidation : addLocal;
454
+ const remove = usingProvider ? controller.attachments.remove : removeLocal;
455
+ const openFileDialog = usingProvider
456
+ ? controller.attachments.openFileDialog
457
+ : openFileDialogLocal;
458
+ const clear = () => {
459
+ clearAttachments();
460
+ clearReferencedSources();
461
+ };
462
+ useEffect(() => {
463
+ if (!usingProvider) return;
464
+ controller.__registerFileInput(inputRef, () => inputRef.current?.click());
465
+ }, [usingProvider, controller]);
466
+ useEffect(() => {
467
+ if (syncHiddenInput && inputRef.current && files.length === 0) {
468
+ inputRef.current.value = "";
469
+ }
470
+ }, [files, syncHiddenInput]);
471
+ useEffect(() => {
472
+ const form = formRef.current;
473
+ if (!form) return;
474
+ if (globalDrop) return;
475
+ const onDragOver = (e: DragEvent) => {
476
+ if (e.dataTransfer?.types?.includes("Files")) e.preventDefault();
477
+ };
478
+ const onDrop = (e: DragEvent) => {
479
+ if (e.dataTransfer?.types?.includes("Files")) e.preventDefault();
480
+ if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {
481
+ add(e.dataTransfer.files);
482
+ }
483
+ };
484
+ form.addEventListener("dragover", onDragOver);
485
+ form.addEventListener("drop", onDrop);
486
+ return () => {
487
+ form.removeEventListener("dragover", onDragOver);
488
+ form.removeEventListener("drop", onDrop);
489
+ };
490
+ }, [add, globalDrop]);
491
+ useEffect(() => {
492
+ if (!globalDrop) return;
493
+ const onDragOver = (e: DragEvent) => {
494
+ if (e.dataTransfer?.types?.includes("Files")) e.preventDefault();
495
+ };
496
+ const onDrop = (e: DragEvent) => {
497
+ if (e.dataTransfer?.types?.includes("Files")) e.preventDefault();
498
+ if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {
499
+ add(e.dataTransfer.files);
500
+ }
501
+ };
502
+ document.addEventListener("dragover", onDragOver);
503
+ document.addEventListener("drop", onDrop);
504
+ return () => {
505
+ document.removeEventListener("dragover", onDragOver);
506
+ document.removeEventListener("drop", onDrop);
507
+ };
508
+ }, [add, globalDrop]);
509
+ useEffect(
510
+ () => () => {
511
+ if (!usingProvider) {
512
+ for (const f of filesRef.current) {
513
+ if (f.url) URL.revokeObjectURL(f.url);
514
+ }
515
+ }
516
+ },
517
+ [usingProvider],
518
+ );
519
+ const handleChange: ChangeEventHandler<HTMLInputElement> = (event) => {
520
+ if (event.currentTarget.files) add(event.currentTarget.files);
521
+ event.currentTarget.value = "";
522
+ };
523
+ const attachmentsCtx = {
524
+ add,
525
+ clear: clearAttachments,
526
+ fileInputRef: inputRef,
527
+ files: files.map((item) => ({ ...item, id: item.id })),
528
+ openFileDialog,
529
+ remove,
530
+ };
531
+ const refsCtx = {
532
+ add: (incoming: SourceDocumentUIPart[] | SourceDocumentUIPart) => {
533
+ const array = Array.isArray(incoming) ? incoming : [incoming];
534
+ setReferencedSources((prev) => [
535
+ ...prev,
536
+ ...array.map((s) => ({ ...s, id: nanoid() })),
537
+ ]);
538
+ },
539
+ clear: clearReferencedSources,
540
+ remove: (id: string) => {
541
+ setReferencedSources((prev) => prev.filter((s) => s.id !== id));
542
+ },
543
+ sources: referencedSources,
544
+ };
545
+ const handleSubmit: FormEventHandler<HTMLFormElement> = async (event) => {
546
+ event.preventDefault();
547
+ const form = event.currentTarget;
548
+ const text = usingProvider
549
+ ? controller.textInput.value
550
+ : (() => {
551
+ const formData = new FormData(form);
552
+ return (formData.get("message") as string) || "";
553
+ })();
554
+ if (!usingProvider) form.reset();
555
+ try {
556
+ const convertedFiles: FileUIPart[] = await Promise.all(
557
+ files.map(async ({ id: _id, ...item }) => {
558
+ if (item.url?.startsWith("blob:")) {
559
+ const dataUrl = await convertBlobUrlToDataUrl(item.url);
560
+ return { ...item, url: dataUrl ?? item.url };
561
+ }
562
+ return item;
563
+ }),
564
+ );
565
+ const result = onSubmit({ files: convertedFiles, text }, event);
566
+ if (result instanceof Promise) {
567
+ try {
568
+ await result;
569
+ clear();
570
+ if (usingProvider) controller.textInput.clear();
571
+ } catch {
572
+ /* Don't clear on error */
573
+ }
574
+ } else {
575
+ clear();
576
+ if (usingProvider) controller.textInput.clear();
577
+ }
578
+ } catch {
579
+ /* Don't clear on error */
580
+ }
581
+ };
582
+ const inner = (
583
+ <>
584
+ <input
585
+ accept={accept}
586
+ aria-label="Upload files"
587
+ className="hidden"
588
+ multiple={multiple}
589
+ onChange={handleChange}
590
+ ref={inputRef}
591
+ title="Upload files"
592
+ type="file"
593
+ />
594
+ <form
595
+ className={cn("w-full", className)}
596
+ onSubmit={handleSubmit}
597
+ ref={formRef}
598
+ {...props}
599
+ >
600
+ <InputGroup className={cn("overflow-hidden", containerClassName)}>
601
+ {children}
602
+ </InputGroup>
603
+ </form>
604
+ </>
605
+ );
606
+ const withReferencedSources = (
607
+ <LocalReferencedSourcesContext.Provider value={refsCtx}>
608
+ {inner}
609
+ </LocalReferencedSourcesContext.Provider>
610
+ );
611
+ return (
612
+ <LocalAttachmentsContext.Provider value={attachmentsCtx}>
613
+ {withReferencedSources}
614
+ </LocalAttachmentsContext.Provider>
615
+ );
616
+ };
617
+ export type PromptInputBodyProps = HTMLAttributes<HTMLDivElement>;
618
+ export const PromptInputBody = ({
619
+ className,
620
+ ...props
621
+ }: PromptInputBodyProps) => (
622
+ <div className={cn("contents", className)} {...props} />
623
+ );
624
+ export type PromptInputTextareaProps = ComponentProps<
625
+ typeof InputGroupTextarea
626
+ >;
627
+ export const PromptInputTextarea = ({
628
+ onChange,
629
+ onKeyDown,
630
+ className,
631
+ placeholder = "What would you like to know?",
632
+ ...props
633
+ }: PromptInputTextareaProps) => {
634
+ const controller = useOptionalPromptInputController();
635
+ const attachments = usePromptInputAttachments();
636
+ const [isComposing, setIsComposing] = useState(false);
637
+ const handleKeyDown: KeyboardEventHandler<HTMLTextAreaElement> = (e) => {
638
+ onKeyDown?.(e);
639
+ if (e.defaultPrevented) return;
640
+ if (e.key === "Enter") {
641
+ if (isComposing || e.nativeEvent.isComposing) return;
642
+ if (e.shiftKey) return;
643
+ e.preventDefault();
644
+ const { form } = e.currentTarget;
645
+ const submitButton = form?.querySelector(
646
+ 'button[type="submit"]',
647
+ ) as HTMLButtonElement | null;
648
+ if (submitButton?.disabled) return;
649
+ form?.requestSubmit();
650
+ }
651
+ if (
652
+ e.key === "Backspace" &&
653
+ e.currentTarget.value === "" &&
654
+ attachments.files.length > 0
655
+ ) {
656
+ e.preventDefault();
657
+ const lastAttachment = attachments.files.at(-1);
658
+ if (lastAttachment) attachments.remove(lastAttachment.id);
659
+ }
660
+ };
661
+ const handlePaste: ClipboardEventHandler<HTMLTextAreaElement> = (event) => {
662
+ const items = event.clipboardData?.items;
663
+ if (!items) return;
664
+ const pastedFiles: File[] = [];
665
+ for (const item of items) {
666
+ if (item.kind === "file") {
667
+ const file = item.getAsFile();
668
+ if (file) pastedFiles.push(file);
669
+ }
670
+ }
671
+ if (pastedFiles.length > 0) {
672
+ event.preventDefault();
673
+ attachments.add(pastedFiles);
674
+ }
675
+ };
676
+ const handleCompositionEnd = () => setIsComposing(false);
677
+ const handleCompositionStart = () => setIsComposing(true);
678
+ const controlledProps = controller
679
+ ? {
680
+ onChange: (e: ChangeEvent<HTMLTextAreaElement>) => {
681
+ controller.textInput.setInput(e.currentTarget.value);
682
+ onChange?.(e);
683
+ },
684
+ value: controller.textInput.value,
685
+ }
686
+ : { onChange };
687
+ return (
688
+ <InputGroupTextarea
689
+ className={cn("field-sizing-content max-h-48 min-h-16", className)}
690
+ name="message"
691
+ onCompositionEnd={handleCompositionEnd}
692
+ onCompositionStart={handleCompositionStart}
693
+ onKeyDown={handleKeyDown}
694
+ onPaste={handlePaste}
695
+ placeholder={placeholder}
696
+ {...props}
697
+ {...controlledProps}
698
+ />
699
+ );
700
+ };
701
+ export type PromptInputHeaderProps = Omit<
702
+ ComponentProps<typeof InputGroupAddon>,
703
+ "align"
704
+ >;
705
+ export const PromptInputHeader = ({
706
+ className,
707
+ ...props
708
+ }: PromptInputHeaderProps) => (
709
+ <InputGroupAddon
710
+ align="block-end"
711
+ className={cn("order-first flex-wrap gap-1", className)}
712
+ {...props}
713
+ />
714
+ );
715
+ export type PromptInputFooterProps = Omit<
716
+ ComponentProps<typeof InputGroupAddon>,
717
+ "align"
718
+ >;
719
+ export const PromptInputFooter = ({
720
+ className,
721
+ ...props
722
+ }: PromptInputFooterProps) => (
723
+ <InputGroupAddon
724
+ align="block-end"
725
+ className={cn("justify-between gap-1", className)}
726
+ {...props}
727
+ />
728
+ );
729
+ export type PromptInputToolsProps = HTMLAttributes<HTMLDivElement>;
730
+ export const PromptInputTools = ({
731
+ className,
732
+ ...props
733
+ }: PromptInputToolsProps) => (
734
+ <div
735
+ className={cn("flex min-w-0 items-center gap-1", className)}
736
+ {...props}
737
+ />
738
+ );
739
+ export type PromptInputButtonTooltip =
740
+ | string
741
+ | {
742
+ content: ReactNode;
743
+ shortcut?: string;
744
+ side?: ComponentProps<typeof TooltipContent>["side"];
745
+ };
746
+ export type PromptInputButtonProps = ComponentProps<typeof InputGroupButton> & {
747
+ tooltip?: PromptInputButtonTooltip;
748
+ };
749
+ export const PromptInputButton = ({
750
+ variant = "ghost",
751
+ className,
752
+ size,
753
+ tooltip,
754
+ ...props
755
+ }: PromptInputButtonProps) => {
756
+ const newSize =
757
+ size ?? (Children.count(props.children) > 1 ? "sm" : "icon-sm");
758
+ const button = (
759
+ <InputGroupButton
760
+ className={cn(className)}
761
+ size={newSize}
762
+ type="button"
763
+ variant={variant}
764
+ {...props}
765
+ />
766
+ );
767
+ if (!tooltip) return button;
768
+ const tooltipContent =
769
+ typeof tooltip === "string" ? tooltip : tooltip.content;
770
+ const shortcut = typeof tooltip === "string" ? undefined : tooltip.shortcut;
771
+ const side = typeof tooltip === "string" ? "top" : tooltip.side ?? "top";
772
+ return (
773
+ <Tooltip>
774
+ <TooltipTrigger asChild>{button}</TooltipTrigger>
775
+ <TooltipContent side={side}>
776
+ {tooltipContent}
777
+ {shortcut && <span className="ml-2 text-muted">{shortcut}</span>}
778
+ </TooltipContent>
779
+ </Tooltip>
780
+ );
781
+ };
782
+ export type PromptInputActionMenuProps = ComponentProps<typeof Dropdown>;
783
+ export const PromptInputActionMenu = (props: PromptInputActionMenuProps) => (
784
+ <Dropdown {...props} />
785
+ );
786
+ export type PromptInputActionMenuTriggerProps = PromptInputButtonProps;
787
+ export const PromptInputActionMenuTrigger = ({
788
+ className,
789
+ children,
790
+ ...props
791
+ }: PromptInputActionMenuTriggerProps) => (
792
+ <DropdownTrigger>
793
+ <PromptInputButton className={className} {...props}>
794
+ {children ?? <IconPlus className="size-4" />}
795
+ </PromptInputButton>
796
+ </DropdownTrigger>
797
+ );
798
+ export type PromptInputActionMenuContentProps = ComponentProps<
799
+ typeof DropdownMenu
800
+ >;
801
+ export const PromptInputActionMenuContent = ({
802
+ className,
803
+ ...props
804
+ }: PromptInputActionMenuContentProps) => (
805
+ <DropdownMenu align="start" className={cn(className)} {...props} />
806
+ );
807
+ export type PromptInputActionMenuItemProps = ComponentProps<
808
+ typeof DropdownItem
809
+ >;
810
+ export const PromptInputActionMenuItem = ({
811
+ className,
812
+ ...props
813
+ }: PromptInputActionMenuItemProps) => (
814
+ <DropdownItem className={cn(className)} {...props} />
815
+ );
816
+ export type PromptInputSubmitProps = ComponentProps<typeof InputGroupButton> & {
817
+ status?: ChatStatus;
818
+ onStop?: () => void;
819
+ };
820
+ export const PromptInputSubmit = ({
821
+ className,
822
+ variant = "default",
823
+ size = "icon-sm",
824
+ status,
825
+ onStop,
826
+ onClick,
827
+ children,
828
+ ...props
829
+ }: PromptInputSubmitProps) => {
830
+ const isGenerating = status === "submitted" || status === "streaming";
831
+ let Icon = <IconCornerDownLeft className="size-4" />;
832
+ if (status === "submitted") {
833
+ Icon = <Spinner size="sm" />;
834
+ } else if (status === "streaming") {
835
+ Icon = <IconSquare className="size-4" />;
836
+ } else if (status === "error") {
837
+ Icon = <IconX className="size-4" />;
838
+ }
839
+ const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
840
+ if (isGenerating && onStop) {
841
+ e.preventDefault();
842
+ onStop();
843
+ return;
844
+ }
845
+ onClick?.(e);
846
+ };
847
+ return (
848
+ <InputGroupButton
849
+ aria-label={isGenerating ? "Stop" : "Submit"}
850
+ className={cn(className)}
851
+ onClick={handleClick}
852
+ size={size}
853
+ type={isGenerating && onStop ? "button" : "submit"}
854
+ variant={variant}
855
+ {...props}
856
+ >
857
+ {children ?? Icon}
858
+ </InputGroupButton>
859
+ );
860
+ };
861
+ export type PromptInputModelSelectProps = ComponentProps<typeof Select>;
862
+ export const PromptInputModelSelect = (props: PromptInputModelSelectProps) => (
863
+ <Select {...props} />
864
+ );
865
+ export type PromptInputModelSelectTriggerProps = ComponentProps<
866
+ typeof SelectTrigger
867
+ >;
868
+ export const PromptInputModelSelectTrigger = ({
869
+ className,
870
+ ...props
871
+ }: PromptInputModelSelectTriggerProps) => (
872
+ <SelectTrigger
873
+ className={cn(
874
+ "border-none bg-transparent font-medium text-muted shadow-none transition-[color,transform] duration-150 ease-out active:scale-[0.97]",
875
+ "hover:bg-surface-secondary hover:text-foreground",
876
+ segmentExpandedStateClass,
877
+ className,
878
+ )}
879
+ {...props}
880
+ />
881
+ );
882
+ export type PromptInputModelSelectContentProps = ComponentProps<
883
+ typeof SelectContent
884
+ >;
885
+ export const PromptInputModelSelectContent = ({
886
+ className,
887
+ ...props
888
+ }: PromptInputModelSelectContentProps) => (
889
+ <SelectContent className={cn(className)} {...props} />
890
+ );
891
+ export type PromptInputModelSelectItemProps = ComponentProps<typeof SelectItem>;
892
+ export const PromptInputModelSelectItem = ({
893
+ className,
894
+ ...props
895
+ }: PromptInputModelSelectItemProps) => (
896
+ <SelectItem className={cn(className)} {...props} />
897
+ );
898
+ export type PromptInputModelSelectValueProps = ComponentProps<
899
+ typeof SelectValue
900
+ >;
901
+ export const PromptInputModelSelectValue = ({
902
+ className,
903
+ ...props
904
+ }: PromptInputModelSelectValueProps) => (
905
+ <SelectValue className={cn(className)} {...props} />
906
+ );
907
+ export type PromptInputHoverCardProps = ComponentProps<typeof HoverCard>;
908
+ export const PromptInputHoverCard = ({
909
+ openDelay = 0,
910
+ closeDelay = 0,
911
+ ...props
912
+ }: PromptInputHoverCardProps) => (
913
+ <HoverCard closeDelay={closeDelay} openDelay={openDelay} {...props} />
914
+ );
915
+ export type PromptInputHoverCardTriggerProps = ComponentProps<
916
+ typeof HoverCardTrigger
917
+ >;
918
+ export const PromptInputHoverCardTrigger = (
919
+ props: PromptInputHoverCardTriggerProps,
920
+ ) => <HoverCardTrigger {...props} />;
921
+ export type PromptInputHoverCardContentProps = ComponentProps<
922
+ typeof HoverCardContent
923
+ >;
924
+ export const PromptInputHoverCardContent = ({
925
+ align = "start",
926
+ ...props
927
+ }: PromptInputHoverCardContentProps) => (
928
+ <HoverCardContent align={align} {...props} />
929
+ );
930
+ export type PromptInputTabsListProps = HTMLAttributes<HTMLDivElement>;
931
+ export const PromptInputTabsList = ({
932
+ className,
933
+ ...props
934
+ }: PromptInputTabsListProps) => <div className={cn(className)} {...props} />;
935
+ export type PromptInputTabProps = HTMLAttributes<HTMLDivElement>;
936
+ export const PromptInputTab = ({
937
+ className,
938
+ ...props
939
+ }: PromptInputTabProps) => <div className={cn(className)} {...props} />;
940
+ export type PromptInputTabLabelProps = HTMLAttributes<HTMLHeadingElement>;
941
+ export const PromptInputTabLabel = ({
942
+ className,
943
+ ...props
944
+ }: PromptInputTabLabelProps) => (
945
+ <h3
946
+ className={cn("mb-2 px-3 font-medium text-muted text-xs", className)}
947
+ {...props}
948
+ />
949
+ );
950
+ export type PromptInputTabBodyProps = HTMLAttributes<HTMLDivElement>;
951
+ export const PromptInputTabBody = ({
952
+ className,
953
+ ...props
954
+ }: PromptInputTabBodyProps) => (
955
+ <div className={cn("space-y-1", className)} {...props} />
956
+ );
957
+ export type PromptInputTabItemProps = HTMLAttributes<HTMLDivElement>;
958
+ export const PromptInputTabItem = ({
959
+ className,
960
+ ...props
961
+ }: PromptInputTabItemProps) => (
962
+ <div
963
+ className={cn(
964
+ "flex items-center gap-2 rounded-md px-3 py-2 text-xs transition-[background-color,transform] duration-150 ease-out active:scale-[0.99] hover:bg-surface-secondary",
965
+ className,
966
+ )}
967
+ {...props}
968
+ />
969
+ );
970
+ export type PromptInputCommandProps = ComponentProps<typeof Command>;
971
+ export const PromptInputCommand = ({
972
+ className,
973
+ ...props
974
+ }: PromptInputCommandProps) => <Command className={cn(className)} {...props} />;
975
+ export type PromptInputCommandInputProps = ComponentProps<typeof CommandInput>;
976
+ export const PromptInputCommandInput = ({
977
+ className,
978
+ ...props
979
+ }: PromptInputCommandInputProps) => (
980
+ <CommandInput className={cn(className)} {...props} />
981
+ );
982
+ export type PromptInputCommandListProps = ComponentProps<typeof CommandList>;
983
+ export const PromptInputCommandList = ({
984
+ className,
985
+ ...props
986
+ }: PromptInputCommandListProps) => (
987
+ <CommandList className={cn(className)} {...props} />
988
+ );
989
+ export type PromptInputCommandEmptyProps = ComponentProps<typeof CommandEmpty>;
990
+ export const PromptInputCommandEmpty = ({
991
+ className,
992
+ ...props
993
+ }: PromptInputCommandEmptyProps) => (
994
+ <CommandEmpty className={cn(className)} {...props} />
995
+ );
996
+ export type PromptInputCommandGroupProps = ComponentProps<typeof CommandGroup>;
997
+ export const PromptInputCommandGroup = ({
998
+ className,
999
+ ...props
1000
+ }: PromptInputCommandGroupProps) => (
1001
+ <CommandGroup className={cn(className)} {...props} />
1002
+ );
1003
+ export type PromptInputCommandItemProps = ComponentProps<typeof CommandItem>;
1004
+ export const PromptInputCommandItem = ({
1005
+ className,
1006
+ ...props
1007
+ }: PromptInputCommandItemProps) => (
1008
+ <CommandItem className={cn(className)} {...props} />
1009
+ );
1010
+ export type PromptInputCommandSeparatorProps = ComponentProps<
1011
+ typeof CommandSeparator
1012
+ >;
1013
+ export const PromptInputCommandSeparator = ({
1014
+ className,
1015
+ ...props
1016
+ }: PromptInputCommandSeparatorProps) => (
1017
+ <CommandSeparator className={cn(className)} {...props} />
1018
+ );