@gmickel/gno 0.3.4 → 0.4.0

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 (57) hide show
  1. package/README.md +194 -53
  2. package/assets/badges/license.svg +12 -0
  3. package/assets/badges/npm.svg +13 -0
  4. package/assets/badges/twitter.svg +22 -0
  5. package/assets/badges/website.svg +22 -0
  6. package/package.json +30 -1
  7. package/src/cli/commands/ask.ts +11 -186
  8. package/src/cli/commands/models/pull.ts +9 -4
  9. package/src/cli/commands/serve.ts +19 -0
  10. package/src/cli/program.ts +28 -0
  11. package/src/llm/registry.ts +3 -1
  12. package/src/pipeline/answer.ts +191 -0
  13. package/src/serve/CLAUDE.md +91 -0
  14. package/src/serve/bunfig.toml +2 -0
  15. package/src/serve/context.ts +181 -0
  16. package/src/serve/index.ts +7 -0
  17. package/src/serve/public/app.tsx +56 -0
  18. package/src/serve/public/components/ai-elements/code-block.tsx +176 -0
  19. package/src/serve/public/components/ai-elements/conversation.tsx +98 -0
  20. package/src/serve/public/components/ai-elements/inline-citation.tsx +285 -0
  21. package/src/serve/public/components/ai-elements/loader.tsx +96 -0
  22. package/src/serve/public/components/ai-elements/message.tsx +443 -0
  23. package/src/serve/public/components/ai-elements/prompt-input.tsx +1421 -0
  24. package/src/serve/public/components/ai-elements/sources.tsx +75 -0
  25. package/src/serve/public/components/ai-elements/suggestion.tsx +51 -0
  26. package/src/serve/public/components/preset-selector.tsx +403 -0
  27. package/src/serve/public/components/ui/badge.tsx +46 -0
  28. package/src/serve/public/components/ui/button-group.tsx +82 -0
  29. package/src/serve/public/components/ui/button.tsx +62 -0
  30. package/src/serve/public/components/ui/card.tsx +92 -0
  31. package/src/serve/public/components/ui/carousel.tsx +244 -0
  32. package/src/serve/public/components/ui/collapsible.tsx +31 -0
  33. package/src/serve/public/components/ui/command.tsx +181 -0
  34. package/src/serve/public/components/ui/dialog.tsx +141 -0
  35. package/src/serve/public/components/ui/dropdown-menu.tsx +255 -0
  36. package/src/serve/public/components/ui/hover-card.tsx +42 -0
  37. package/src/serve/public/components/ui/input-group.tsx +167 -0
  38. package/src/serve/public/components/ui/input.tsx +21 -0
  39. package/src/serve/public/components/ui/progress.tsx +28 -0
  40. package/src/serve/public/components/ui/scroll-area.tsx +56 -0
  41. package/src/serve/public/components/ui/select.tsx +188 -0
  42. package/src/serve/public/components/ui/separator.tsx +26 -0
  43. package/src/serve/public/components/ui/table.tsx +114 -0
  44. package/src/serve/public/components/ui/textarea.tsx +18 -0
  45. package/src/serve/public/components/ui/tooltip.tsx +59 -0
  46. package/src/serve/public/globals.css +226 -0
  47. package/src/serve/public/hooks/use-api.ts +112 -0
  48. package/src/serve/public/index.html +13 -0
  49. package/src/serve/public/pages/Ask.tsx +442 -0
  50. package/src/serve/public/pages/Browse.tsx +270 -0
  51. package/src/serve/public/pages/Dashboard.tsx +202 -0
  52. package/src/serve/public/pages/DocView.tsx +302 -0
  53. package/src/serve/public/pages/Search.tsx +335 -0
  54. package/src/serve/routes/api.ts +763 -0
  55. package/src/serve/server.ts +249 -0
  56. package/src/store/sqlite/adapter.ts +47 -0
  57. package/src/store/types.ts +10 -0
@@ -0,0 +1,1421 @@
1
+ import type { ChatStatus, FileUIPart } from 'ai';
2
+ import {
3
+ CornerDownLeftIcon,
4
+ ImageIcon,
5
+ Loader2Icon,
6
+ MicIcon,
7
+ PaperclipIcon,
8
+ PlusIcon,
9
+ SquareIcon,
10
+ XIcon,
11
+ } from 'lucide-react';
12
+ import { nanoid } from 'nanoid';
13
+ import {
14
+ type ChangeEvent,
15
+ type ChangeEventHandler,
16
+ Children,
17
+ type ClipboardEventHandler,
18
+ type ComponentProps,
19
+ createContext,
20
+ type FormEvent,
21
+ type FormEventHandler,
22
+ Fragment,
23
+ type HTMLAttributes,
24
+ type KeyboardEventHandler,
25
+ type PropsWithChildren,
26
+ type ReactNode,
27
+ type RefObject,
28
+ useCallback,
29
+ useContext,
30
+ useEffect,
31
+ useMemo,
32
+ useRef,
33
+ useState,
34
+ } from 'react';
35
+ import { cn } from '../../lib/utils';
36
+ import { Button } from '../ui/button';
37
+ import {
38
+ Command,
39
+ CommandEmpty,
40
+ CommandGroup,
41
+ CommandInput,
42
+ CommandItem,
43
+ CommandList,
44
+ CommandSeparator,
45
+ } from '../ui/command';
46
+ import {
47
+ DropdownMenu,
48
+ DropdownMenuContent,
49
+ DropdownMenuItem,
50
+ DropdownMenuTrigger,
51
+ } from '../ui/dropdown-menu';
52
+ import {
53
+ HoverCard,
54
+ HoverCardContent,
55
+ HoverCardTrigger,
56
+ } from '../ui/hover-card';
57
+ import {
58
+ InputGroup,
59
+ InputGroupAddon,
60
+ InputGroupButton,
61
+ InputGroupTextarea,
62
+ } from '../ui/input-group';
63
+ import {
64
+ Select,
65
+ SelectContent,
66
+ SelectItem,
67
+ SelectTrigger,
68
+ SelectValue,
69
+ } from '../ui/select';
70
+
71
+ // ============================================================================
72
+ // Provider Context & Types
73
+ // ============================================================================
74
+
75
+ export interface AttachmentsContext {
76
+ files: (FileUIPart & { id: string })[];
77
+ add: (files: File[] | FileList) => void;
78
+ remove: (id: string) => void;
79
+ clear: () => void;
80
+ openFileDialog: () => void;
81
+ fileInputRef: RefObject<HTMLInputElement | null>;
82
+ }
83
+
84
+ export interface TextInputContext {
85
+ value: string;
86
+ setInput: (v: string) => void;
87
+ clear: () => void;
88
+ }
89
+
90
+ export interface PromptInputControllerProps {
91
+ textInput: TextInputContext;
92
+ attachments: AttachmentsContext;
93
+ /** INTERNAL: Allows PromptInput to register its file textInput + "open" callback */
94
+ __registerFileInput: (
95
+ ref: RefObject<HTMLInputElement | null>,
96
+ open: () => void
97
+ ) => void;
98
+ }
99
+
100
+ const PromptInputController = createContext<PromptInputControllerProps | null>(
101
+ null
102
+ );
103
+ const ProviderAttachmentsContext = createContext<AttachmentsContext | null>(
104
+ null
105
+ );
106
+
107
+ export const usePromptInputController = () => {
108
+ const ctx = useContext(PromptInputController);
109
+ if (!ctx) {
110
+ throw new Error(
111
+ 'Wrap your component inside <PromptInputProvider> to use usePromptInputController().'
112
+ );
113
+ }
114
+ return ctx;
115
+ };
116
+
117
+ // Optional variants (do NOT throw). Useful for dual-mode components.
118
+ const useOptionalPromptInputController = () =>
119
+ useContext(PromptInputController);
120
+
121
+ export const useProviderAttachments = () => {
122
+ const ctx = useContext(ProviderAttachmentsContext);
123
+ if (!ctx) {
124
+ throw new Error(
125
+ 'Wrap your component inside <PromptInputProvider> to use useProviderAttachments().'
126
+ );
127
+ }
128
+ return ctx;
129
+ };
130
+
131
+ const useOptionalProviderAttachments = () =>
132
+ useContext(ProviderAttachmentsContext);
133
+
134
+ export type PromptInputProviderProps = PropsWithChildren<{
135
+ initialInput?: string;
136
+ }>;
137
+
138
+ /**
139
+ * Optional global provider that lifts PromptInput state outside of PromptInput.
140
+ * If you don't use it, PromptInput stays fully self-managed.
141
+ */
142
+ export function PromptInputProvider({
143
+ initialInput: initialTextInput = '',
144
+ children,
145
+ }: PromptInputProviderProps) {
146
+ // ----- textInput state
147
+ const [textInput, setTextInput] = useState(initialTextInput);
148
+ const clearInput = useCallback(() => setTextInput(''), []);
149
+
150
+ // ----- attachments state (global when wrapped)
151
+ const [attachmentFiles, setAttachmentFiles] = useState<
152
+ (FileUIPart & { id: string })[]
153
+ >([]);
154
+ const fileInputRef = useRef<HTMLInputElement | null>(null);
155
+ const openRef = useRef<() => void>(() => {});
156
+
157
+ const add = useCallback((files: File[] | FileList) => {
158
+ const incoming = Array.from(files);
159
+ if (incoming.length === 0) {
160
+ return;
161
+ }
162
+
163
+ setAttachmentFiles((prev) =>
164
+ prev.concat(
165
+ incoming.map((file) => ({
166
+ id: nanoid(),
167
+ type: 'file' as const,
168
+ url: URL.createObjectURL(file),
169
+ mediaType: file.type,
170
+ filename: file.name,
171
+ }))
172
+ )
173
+ );
174
+ }, []);
175
+
176
+ const remove = useCallback((id: string) => {
177
+ setAttachmentFiles((prev) => {
178
+ const found = prev.find((f) => f.id === id);
179
+ if (found?.url) {
180
+ URL.revokeObjectURL(found.url);
181
+ }
182
+ return prev.filter((f) => f.id !== id);
183
+ });
184
+ }, []);
185
+
186
+ const clear = useCallback(() => {
187
+ setAttachmentFiles((prev) => {
188
+ for (const f of prev) {
189
+ if (f.url) {
190
+ URL.revokeObjectURL(f.url);
191
+ }
192
+ }
193
+ return [];
194
+ });
195
+ }, []);
196
+
197
+ // Keep a ref to attachments for cleanup on unmount (avoids stale closure)
198
+ const attachmentsRef = useRef(attachmentFiles);
199
+ attachmentsRef.current = attachmentFiles;
200
+
201
+ // Cleanup blob URLs on unmount to prevent memory leaks
202
+ useEffect(() => {
203
+ return () => {
204
+ for (const f of attachmentsRef.current) {
205
+ if (f.url) {
206
+ URL.revokeObjectURL(f.url);
207
+ }
208
+ }
209
+ };
210
+ }, []);
211
+
212
+ const openFileDialog = useCallback(() => {
213
+ openRef.current?.();
214
+ }, []);
215
+
216
+ const attachments = useMemo<AttachmentsContext>(
217
+ () => ({
218
+ files: attachmentFiles,
219
+ add,
220
+ remove,
221
+ clear,
222
+ openFileDialog,
223
+ fileInputRef,
224
+ }),
225
+ [attachmentFiles, add, remove, clear, openFileDialog]
226
+ );
227
+
228
+ const __registerFileInput = useCallback(
229
+ (ref: RefObject<HTMLInputElement | null>, open: () => void) => {
230
+ fileInputRef.current = ref.current;
231
+ openRef.current = open;
232
+ },
233
+ []
234
+ );
235
+
236
+ const controller = useMemo<PromptInputControllerProps>(
237
+ () => ({
238
+ textInput: {
239
+ value: textInput,
240
+ setInput: setTextInput,
241
+ clear: clearInput,
242
+ },
243
+ attachments,
244
+ __registerFileInput,
245
+ }),
246
+ [textInput, clearInput, attachments, __registerFileInput]
247
+ );
248
+
249
+ return (
250
+ <PromptInputController.Provider value={controller}>
251
+ <ProviderAttachmentsContext.Provider value={attachments}>
252
+ {children}
253
+ </ProviderAttachmentsContext.Provider>
254
+ </PromptInputController.Provider>
255
+ );
256
+ }
257
+
258
+ // ============================================================================
259
+ // Component Context & Hooks
260
+ // ============================================================================
261
+
262
+ const LocalAttachmentsContext = createContext<AttachmentsContext | null>(null);
263
+
264
+ export const usePromptInputAttachments = () => {
265
+ // Dual-mode: prefer provider if present, otherwise use local
266
+ const provider = useOptionalProviderAttachments();
267
+ const local = useContext(LocalAttachmentsContext);
268
+ const context = provider ?? local;
269
+ if (!context) {
270
+ throw new Error(
271
+ 'usePromptInputAttachments must be used within a PromptInput or PromptInputProvider'
272
+ );
273
+ }
274
+ return context;
275
+ };
276
+
277
+ export type PromptInputAttachmentProps = HTMLAttributes<HTMLDivElement> & {
278
+ data: FileUIPart & { id: string };
279
+ className?: string;
280
+ };
281
+
282
+ export function PromptInputAttachment({
283
+ data,
284
+ className,
285
+ ...props
286
+ }: PromptInputAttachmentProps) {
287
+ const attachments = usePromptInputAttachments();
288
+
289
+ const filename = data.filename || '';
290
+
291
+ const mediaType =
292
+ data.mediaType?.startsWith('image/') && data.url ? 'image' : 'file';
293
+ const isImage = mediaType === 'image';
294
+
295
+ const attachmentLabel = filename || (isImage ? 'Image' : 'Attachment');
296
+
297
+ return (
298
+ <PromptInputHoverCard>
299
+ <HoverCardTrigger asChild>
300
+ <div
301
+ className={cn(
302
+ 'group relative flex h-8 cursor-pointer select-none items-center gap-1.5 rounded-md border border-border px-1.5 font-medium text-sm transition-all hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
303
+ className
304
+ )}
305
+ key={data.id}
306
+ {...props}
307
+ >
308
+ <div className="relative size-5 shrink-0">
309
+ <div className="absolute inset-0 flex size-5 items-center justify-center overflow-hidden rounded bg-background transition-opacity group-hover:opacity-0">
310
+ {isImage ? (
311
+ <img
312
+ alt={filename || 'attachment'}
313
+ className="size-5 object-cover"
314
+ height={20}
315
+ src={data.url}
316
+ width={20}
317
+ />
318
+ ) : (
319
+ <div className="flex size-5 items-center justify-center text-muted-foreground">
320
+ <PaperclipIcon className="size-3" />
321
+ </div>
322
+ )}
323
+ </div>
324
+ <Button
325
+ aria-label="Remove attachment"
326
+ className="absolute inset-0 size-5 cursor-pointer rounded p-0 opacity-0 transition-opacity group-hover:pointer-events-auto group-hover:opacity-100 [&>svg]:size-2.5"
327
+ onClick={(e) => {
328
+ e.stopPropagation();
329
+ attachments.remove(data.id);
330
+ }}
331
+ type="button"
332
+ variant="ghost"
333
+ >
334
+ <XIcon />
335
+ <span className="sr-only">Remove</span>
336
+ </Button>
337
+ </div>
338
+
339
+ <span className="flex-1 truncate">{attachmentLabel}</span>
340
+ </div>
341
+ </HoverCardTrigger>
342
+ <PromptInputHoverCardContent className="w-auto p-2">
343
+ <div className="w-auto space-y-3">
344
+ {isImage && (
345
+ <div className="flex max-h-96 w-96 items-center justify-center overflow-hidden rounded-md border">
346
+ <img
347
+ alt={filename || 'attachment preview'}
348
+ className="max-h-full max-w-full object-contain"
349
+ height={384}
350
+ src={data.url}
351
+ width={448}
352
+ />
353
+ </div>
354
+ )}
355
+ <div className="flex items-center gap-2.5">
356
+ <div className="min-w-0 flex-1 space-y-1 px-0.5">
357
+ <h4 className="truncate font-semibold text-sm leading-none">
358
+ {filename || (isImage ? 'Image' : 'Attachment')}
359
+ </h4>
360
+ {data.mediaType && (
361
+ <p className="truncate font-mono text-muted-foreground text-xs">
362
+ {data.mediaType}
363
+ </p>
364
+ )}
365
+ </div>
366
+ </div>
367
+ </div>
368
+ </PromptInputHoverCardContent>
369
+ </PromptInputHoverCard>
370
+ );
371
+ }
372
+
373
+ export type PromptInputAttachmentsProps = Omit<
374
+ HTMLAttributes<HTMLDivElement>,
375
+ 'children'
376
+ > & {
377
+ children: (attachment: FileUIPart & { id: string }) => ReactNode;
378
+ };
379
+
380
+ export function PromptInputAttachments({
381
+ children,
382
+ className,
383
+ ...props
384
+ }: PromptInputAttachmentsProps) {
385
+ const attachments = usePromptInputAttachments();
386
+
387
+ if (!attachments.files.length) {
388
+ return null;
389
+ }
390
+
391
+ return (
392
+ <div
393
+ className={cn('flex w-full flex-wrap items-center gap-2 p-3', className)}
394
+ {...props}
395
+ >
396
+ {attachments.files.map((file) => (
397
+ <Fragment key={file.id}>{children(file)}</Fragment>
398
+ ))}
399
+ </div>
400
+ );
401
+ }
402
+
403
+ export type PromptInputActionAddAttachmentsProps = ComponentProps<
404
+ typeof DropdownMenuItem
405
+ > & {
406
+ label?: string;
407
+ };
408
+
409
+ export const PromptInputActionAddAttachments = ({
410
+ label = 'Add photos or files',
411
+ ...props
412
+ }: PromptInputActionAddAttachmentsProps) => {
413
+ const attachments = usePromptInputAttachments();
414
+
415
+ return (
416
+ <DropdownMenuItem
417
+ {...props}
418
+ onSelect={(e) => {
419
+ e.preventDefault();
420
+ attachments.openFileDialog();
421
+ }}
422
+ >
423
+ <ImageIcon className="mr-2 size-4" /> {label}
424
+ </DropdownMenuItem>
425
+ );
426
+ };
427
+
428
+ export interface PromptInputMessage {
429
+ text: string;
430
+ files: FileUIPart[];
431
+ }
432
+
433
+ export type PromptInputProps = Omit<
434
+ HTMLAttributes<HTMLFormElement>,
435
+ 'onSubmit' | 'onError'
436
+ > & {
437
+ accept?: string; // e.g., "image/*" or leave undefined for any
438
+ multiple?: boolean;
439
+ // When true, accepts drops anywhere on document. Default false (opt-in).
440
+ globalDrop?: boolean;
441
+ // Render a hidden input with given name and keep it in sync for native form posts. Default false.
442
+ syncHiddenInput?: boolean;
443
+ // Minimal constraints
444
+ maxFiles?: number;
445
+ maxFileSize?: number; // bytes
446
+ onError?: (err: {
447
+ code: 'max_files' | 'max_file_size' | 'accept';
448
+ message: string;
449
+ }) => void;
450
+ onSubmit: (
451
+ message: PromptInputMessage,
452
+ event: FormEvent<HTMLFormElement>
453
+ ) => void | Promise<void>;
454
+ };
455
+
456
+ export const PromptInput = ({
457
+ className,
458
+ accept,
459
+ multiple,
460
+ globalDrop,
461
+ syncHiddenInput,
462
+ maxFiles,
463
+ maxFileSize,
464
+ onError,
465
+ onSubmit,
466
+ children,
467
+ ...props
468
+ }: PromptInputProps) => {
469
+ // Try to use a provider controller if present
470
+ const controller = useOptionalPromptInputController();
471
+ const usingProvider = !!controller;
472
+
473
+ // Refs
474
+ const inputRef = useRef<HTMLInputElement | null>(null);
475
+ const formRef = useRef<HTMLFormElement | null>(null);
476
+
477
+ // ----- Local attachments (only used when no provider)
478
+ const [items, setItems] = useState<(FileUIPart & { id: string })[]>([]);
479
+ const files = usingProvider ? controller.attachments.files : items;
480
+
481
+ // Keep a ref to files for cleanup on unmount (avoids stale closure)
482
+ const filesRef = useRef(files);
483
+ filesRef.current = files;
484
+
485
+ const openFileDialogLocal = useCallback(() => {
486
+ inputRef.current?.click();
487
+ }, []);
488
+
489
+ const matchesAccept = useCallback(
490
+ (f: File) => {
491
+ if (!accept || accept.trim() === '') {
492
+ return true;
493
+ }
494
+
495
+ const patterns = accept
496
+ .split(',')
497
+ .map((s) => s.trim())
498
+ .filter(Boolean);
499
+
500
+ return patterns.some((pattern) => {
501
+ if (pattern.endsWith('/*')) {
502
+ const prefix = pattern.slice(0, -1); // e.g: image/* -> image/
503
+ return f.type.startsWith(prefix);
504
+ }
505
+ return f.type === pattern;
506
+ });
507
+ },
508
+ [accept]
509
+ );
510
+
511
+ const addLocal = useCallback(
512
+ (fileList: File[] | FileList) => {
513
+ const incoming = Array.from(fileList);
514
+ const accepted = incoming.filter((f) => matchesAccept(f));
515
+ if (incoming.length && accepted.length === 0) {
516
+ onError?.({
517
+ code: 'accept',
518
+ message: 'No files match the accepted types.',
519
+ });
520
+ return;
521
+ }
522
+ const withinSize = (f: File) =>
523
+ maxFileSize ? f.size <= maxFileSize : true;
524
+ const sized = accepted.filter(withinSize);
525
+ if (accepted.length > 0 && sized.length === 0) {
526
+ onError?.({
527
+ code: 'max_file_size',
528
+ message: 'All files exceed the maximum size.',
529
+ });
530
+ return;
531
+ }
532
+
533
+ setItems((prev) => {
534
+ const capacity =
535
+ typeof maxFiles === 'number'
536
+ ? Math.max(0, maxFiles - prev.length)
537
+ : undefined;
538
+ const capped =
539
+ typeof capacity === 'number' ? sized.slice(0, capacity) : sized;
540
+ if (typeof capacity === 'number' && sized.length > capacity) {
541
+ onError?.({
542
+ code: 'max_files',
543
+ message: 'Too many files. Some were not added.',
544
+ });
545
+ }
546
+ const next: (FileUIPart & { id: string })[] = [];
547
+ for (const file of capped) {
548
+ next.push({
549
+ id: nanoid(),
550
+ type: 'file',
551
+ url: URL.createObjectURL(file),
552
+ mediaType: file.type,
553
+ filename: file.name,
554
+ });
555
+ }
556
+ return prev.concat(next);
557
+ });
558
+ },
559
+ [matchesAccept, maxFiles, maxFileSize, onError]
560
+ );
561
+
562
+ const removeLocal = useCallback(
563
+ (id: string) =>
564
+ setItems((prev) => {
565
+ const found = prev.find((file) => file.id === id);
566
+ if (found?.url) {
567
+ URL.revokeObjectURL(found.url);
568
+ }
569
+ return prev.filter((file) => file.id !== id);
570
+ }),
571
+ []
572
+ );
573
+
574
+ const clearLocal = useCallback(
575
+ () =>
576
+ setItems((prev) => {
577
+ for (const file of prev) {
578
+ if (file.url) {
579
+ URL.revokeObjectURL(file.url);
580
+ }
581
+ }
582
+ return [];
583
+ }),
584
+ []
585
+ );
586
+
587
+ const add = usingProvider ? controller.attachments.add : addLocal;
588
+ const remove = usingProvider ? controller.attachments.remove : removeLocal;
589
+ const clear = usingProvider ? controller.attachments.clear : clearLocal;
590
+ const openFileDialog = usingProvider
591
+ ? controller.attachments.openFileDialog
592
+ : openFileDialogLocal;
593
+
594
+ // Let provider know about our hidden file input so external menus can call openFileDialog()
595
+ useEffect(() => {
596
+ if (!usingProvider) {
597
+ return;
598
+ }
599
+ controller.__registerFileInput(inputRef, () => inputRef.current?.click());
600
+ }, [usingProvider, controller]);
601
+
602
+ // Note: File input cannot be programmatically set for security reasons
603
+ // The syncHiddenInput prop is no longer functional
604
+ useEffect(() => {
605
+ if (syncHiddenInput && inputRef.current && files.length === 0) {
606
+ inputRef.current.value = '';
607
+ }
608
+ }, [files, syncHiddenInput]);
609
+
610
+ // Attach drop handlers on nearest form and document (opt-in)
611
+ useEffect(() => {
612
+ const form = formRef.current;
613
+ if (!form) {
614
+ return;
615
+ }
616
+ if (globalDrop) {
617
+ return; // when global drop is on, let the document-level handler own drops
618
+ }
619
+
620
+ const onDragOver = (e: DragEvent) => {
621
+ if (e.dataTransfer?.types?.includes('Files')) {
622
+ e.preventDefault();
623
+ }
624
+ };
625
+ const onDrop = (e: DragEvent) => {
626
+ if (e.dataTransfer?.types?.includes('Files')) {
627
+ e.preventDefault();
628
+ }
629
+ if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {
630
+ add(e.dataTransfer.files);
631
+ }
632
+ };
633
+ form.addEventListener('dragover', onDragOver);
634
+ form.addEventListener('drop', onDrop);
635
+ return () => {
636
+ form.removeEventListener('dragover', onDragOver);
637
+ form.removeEventListener('drop', onDrop);
638
+ };
639
+ }, [add, globalDrop]);
640
+
641
+ useEffect(() => {
642
+ if (!globalDrop) {
643
+ return;
644
+ }
645
+
646
+ const onDragOver = (e: DragEvent) => {
647
+ if (e.dataTransfer?.types?.includes('Files')) {
648
+ e.preventDefault();
649
+ }
650
+ };
651
+ const onDrop = (e: DragEvent) => {
652
+ if (e.dataTransfer?.types?.includes('Files')) {
653
+ e.preventDefault();
654
+ }
655
+ if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {
656
+ add(e.dataTransfer.files);
657
+ }
658
+ };
659
+ document.addEventListener('dragover', onDragOver);
660
+ document.addEventListener('drop', onDrop);
661
+ return () => {
662
+ document.removeEventListener('dragover', onDragOver);
663
+ document.removeEventListener('drop', onDrop);
664
+ };
665
+ }, [add, globalDrop]);
666
+
667
+ useEffect(
668
+ () => () => {
669
+ if (!usingProvider) {
670
+ for (const f of filesRef.current) {
671
+ if (f.url) {
672
+ URL.revokeObjectURL(f.url);
673
+ }
674
+ }
675
+ }
676
+ },
677
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- cleanup only on unmount; filesRef always current
678
+ [usingProvider]
679
+ );
680
+
681
+ const handleChange: ChangeEventHandler<HTMLInputElement> = (event) => {
682
+ if (event.currentTarget.files) {
683
+ add(event.currentTarget.files);
684
+ }
685
+ // Reset input value to allow selecting files that were previously removed
686
+ event.currentTarget.value = '';
687
+ };
688
+
689
+ const convertBlobUrlToDataUrl = async (
690
+ url: string
691
+ ): Promise<string | null> => {
692
+ try {
693
+ const response = await fetch(url);
694
+ const blob = await response.blob();
695
+ return new Promise((resolve) => {
696
+ const reader = new FileReader();
697
+ reader.onloadend = () => resolve(reader.result as string);
698
+ reader.onerror = () => resolve(null);
699
+ reader.readAsDataURL(blob);
700
+ });
701
+ } catch {
702
+ return null;
703
+ }
704
+ };
705
+
706
+ const ctx = useMemo<AttachmentsContext>(
707
+ () => ({
708
+ files: files.map((item) => ({ ...item, id: item.id })),
709
+ add,
710
+ remove,
711
+ clear,
712
+ openFileDialog,
713
+ fileInputRef: inputRef,
714
+ }),
715
+ [files, add, remove, clear, openFileDialog]
716
+ );
717
+
718
+ const handleSubmit: FormEventHandler<HTMLFormElement> = (event) => {
719
+ event.preventDefault();
720
+
721
+ const form = event.currentTarget;
722
+ const text = usingProvider
723
+ ? controller.textInput.value
724
+ : (() => {
725
+ const formData = new FormData(form);
726
+ return (formData.get('message') as string) || '';
727
+ })();
728
+
729
+ // Reset form immediately after capturing text to avoid race condition
730
+ // where user input during async blob conversion would be lost
731
+ if (!usingProvider) {
732
+ form.reset();
733
+ }
734
+
735
+ // Convert blob URLs to data URLs asynchronously
736
+ Promise.all(
737
+ files.map(async ({ id, ...item }) => {
738
+ if (item.url?.startsWith('blob:')) {
739
+ const dataUrl = await convertBlobUrlToDataUrl(item.url);
740
+ // If conversion failed, keep the original blob URL
741
+ return {
742
+ ...item,
743
+ url: dataUrl ?? item.url,
744
+ };
745
+ }
746
+ return item;
747
+ })
748
+ )
749
+ .then((convertedFiles: FileUIPart[]) => {
750
+ try {
751
+ const result = onSubmit({ text, files: convertedFiles }, event);
752
+
753
+ // Handle both sync and async onSubmit
754
+ if (result instanceof Promise) {
755
+ result
756
+ .then(() => {
757
+ clear();
758
+ if (usingProvider) {
759
+ controller.textInput.clear();
760
+ }
761
+ })
762
+ .catch(() => {
763
+ // Don't clear on error - user may want to retry
764
+ });
765
+ } else {
766
+ // Sync function completed without throwing, clear attachments
767
+ clear();
768
+ if (usingProvider) {
769
+ controller.textInput.clear();
770
+ }
771
+ }
772
+ } catch {
773
+ // Don't clear on error - user may want to retry
774
+ }
775
+ })
776
+ .catch(() => {
777
+ // Don't clear on error - user may want to retry
778
+ });
779
+ };
780
+
781
+ // Render with or without local provider
782
+ const inner = (
783
+ <>
784
+ <input
785
+ accept={accept}
786
+ aria-label="Upload files"
787
+ className="hidden"
788
+ multiple={multiple}
789
+ onChange={handleChange}
790
+ ref={inputRef}
791
+ title="Upload files"
792
+ type="file"
793
+ />
794
+ <form
795
+ className={cn('w-full', className)}
796
+ onSubmit={handleSubmit}
797
+ ref={formRef}
798
+ {...props}
799
+ >
800
+ <InputGroup className="overflow-hidden">{children}</InputGroup>
801
+ </form>
802
+ </>
803
+ );
804
+
805
+ return usingProvider ? (
806
+ inner
807
+ ) : (
808
+ <LocalAttachmentsContext.Provider value={ctx}>
809
+ {inner}
810
+ </LocalAttachmentsContext.Provider>
811
+ );
812
+ };
813
+
814
+ export type PromptInputBodyProps = HTMLAttributes<HTMLDivElement>;
815
+
816
+ export const PromptInputBody = ({
817
+ className,
818
+ ...props
819
+ }: PromptInputBodyProps) => (
820
+ <div className={cn('contents', className)} {...props} />
821
+ );
822
+
823
+ export type PromptInputTextareaProps = ComponentProps<
824
+ typeof InputGroupTextarea
825
+ >;
826
+
827
+ export const PromptInputTextarea = ({
828
+ onChange,
829
+ className,
830
+ placeholder = 'What would you like to know?',
831
+ ...props
832
+ }: PromptInputTextareaProps) => {
833
+ const controller = useOptionalPromptInputController();
834
+ const attachments = usePromptInputAttachments();
835
+ const [isComposing, setIsComposing] = useState(false);
836
+
837
+ const handleKeyDown: KeyboardEventHandler<HTMLTextAreaElement> = (e) => {
838
+ if (e.key === 'Enter') {
839
+ if (isComposing || e.nativeEvent.isComposing) {
840
+ return;
841
+ }
842
+ if (e.shiftKey) {
843
+ return;
844
+ }
845
+ e.preventDefault();
846
+
847
+ // Check if the submit button is disabled before submitting
848
+ const form = e.currentTarget.form;
849
+ const submitButton = form?.querySelector(
850
+ 'button[type="submit"]'
851
+ ) as HTMLButtonElement | null;
852
+ if (submitButton?.disabled) {
853
+ return;
854
+ }
855
+
856
+ form?.requestSubmit();
857
+ }
858
+
859
+ // Remove last attachment when Backspace is pressed and textarea is empty
860
+ if (
861
+ e.key === 'Backspace' &&
862
+ e.currentTarget.value === '' &&
863
+ attachments.files.length > 0
864
+ ) {
865
+ e.preventDefault();
866
+ const lastAttachment = attachments.files.at(-1);
867
+ if (lastAttachment) {
868
+ attachments.remove(lastAttachment.id);
869
+ }
870
+ }
871
+ };
872
+
873
+ const handlePaste: ClipboardEventHandler<HTMLTextAreaElement> = (event) => {
874
+ const items = event.clipboardData?.items;
875
+
876
+ if (!items) {
877
+ return;
878
+ }
879
+
880
+ const files: File[] = [];
881
+
882
+ for (const item of items) {
883
+ if (item.kind === 'file') {
884
+ const file = item.getAsFile();
885
+ if (file) {
886
+ files.push(file);
887
+ }
888
+ }
889
+ }
890
+
891
+ if (files.length > 0) {
892
+ event.preventDefault();
893
+ attachments.add(files);
894
+ }
895
+ };
896
+
897
+ const controlledProps = controller
898
+ ? {
899
+ value: controller.textInput.value,
900
+ onChange: (e: ChangeEvent<HTMLTextAreaElement>) => {
901
+ controller.textInput.setInput(e.currentTarget.value);
902
+ onChange?.(e);
903
+ },
904
+ }
905
+ : {
906
+ onChange,
907
+ };
908
+
909
+ return (
910
+ <InputGroupTextarea
911
+ className={cn('field-sizing-content max-h-48 min-h-16', className)}
912
+ name="message"
913
+ onCompositionEnd={() => setIsComposing(false)}
914
+ onCompositionStart={() => setIsComposing(true)}
915
+ onKeyDown={handleKeyDown}
916
+ onPaste={handlePaste}
917
+ placeholder={placeholder}
918
+ {...props}
919
+ {...controlledProps}
920
+ />
921
+ );
922
+ };
923
+
924
+ export type PromptInputHeaderProps = Omit<
925
+ ComponentProps<typeof InputGroupAddon>,
926
+ 'align'
927
+ >;
928
+
929
+ export const PromptInputHeader = ({
930
+ className,
931
+ ...props
932
+ }: PromptInputHeaderProps) => (
933
+ <InputGroupAddon
934
+ align="block-end"
935
+ className={cn('order-first flex-wrap gap-1', className)}
936
+ {...props}
937
+ />
938
+ );
939
+
940
+ export type PromptInputFooterProps = Omit<
941
+ ComponentProps<typeof InputGroupAddon>,
942
+ 'align'
943
+ >;
944
+
945
+ export const PromptInputFooter = ({
946
+ className,
947
+ ...props
948
+ }: PromptInputFooterProps) => (
949
+ <InputGroupAddon
950
+ align="block-end"
951
+ className={cn('justify-between gap-1', className)}
952
+ {...props}
953
+ />
954
+ );
955
+
956
+ export type PromptInputToolsProps = HTMLAttributes<HTMLDivElement>;
957
+
958
+ export const PromptInputTools = ({
959
+ className,
960
+ ...props
961
+ }: PromptInputToolsProps) => (
962
+ <div className={cn('flex items-center gap-1', className)} {...props} />
963
+ );
964
+
965
+ export type PromptInputButtonProps = ComponentProps<typeof InputGroupButton>;
966
+
967
+ export const PromptInputButton = ({
968
+ variant = 'ghost',
969
+ className,
970
+ size,
971
+ ...props
972
+ }: PromptInputButtonProps) => {
973
+ const newSize =
974
+ size ?? (Children.count(props.children) > 1 ? 'sm' : 'icon-sm');
975
+
976
+ return (
977
+ <InputGroupButton
978
+ className={cn(className)}
979
+ size={newSize}
980
+ type="button"
981
+ variant={variant}
982
+ {...props}
983
+ />
984
+ );
985
+ };
986
+
987
+ export type PromptInputActionMenuProps = ComponentProps<typeof DropdownMenu>;
988
+ export const PromptInputActionMenu = (props: PromptInputActionMenuProps) => (
989
+ <DropdownMenu {...props} />
990
+ );
991
+
992
+ export type PromptInputActionMenuTriggerProps = PromptInputButtonProps;
993
+
994
+ export const PromptInputActionMenuTrigger = ({
995
+ className,
996
+ children,
997
+ ...props
998
+ }: PromptInputActionMenuTriggerProps) => (
999
+ <DropdownMenuTrigger asChild>
1000
+ <PromptInputButton className={className} {...props}>
1001
+ {children ?? <PlusIcon className="size-4" />}
1002
+ </PromptInputButton>
1003
+ </DropdownMenuTrigger>
1004
+ );
1005
+
1006
+ export type PromptInputActionMenuContentProps = ComponentProps<
1007
+ typeof DropdownMenuContent
1008
+ >;
1009
+ export const PromptInputActionMenuContent = ({
1010
+ className,
1011
+ ...props
1012
+ }: PromptInputActionMenuContentProps) => (
1013
+ <DropdownMenuContent align="start" className={cn(className)} {...props} />
1014
+ );
1015
+
1016
+ export type PromptInputActionMenuItemProps = ComponentProps<
1017
+ typeof DropdownMenuItem
1018
+ >;
1019
+ export const PromptInputActionMenuItem = ({
1020
+ className,
1021
+ ...props
1022
+ }: PromptInputActionMenuItemProps) => (
1023
+ <DropdownMenuItem className={cn(className)} {...props} />
1024
+ );
1025
+
1026
+ // Note: Actions that perform side-effects (like opening a file dialog)
1027
+ // are provided in opt-in modules (e.g., prompt-input-attachments).
1028
+
1029
+ export type PromptInputSubmitProps = ComponentProps<typeof InputGroupButton> & {
1030
+ status?: ChatStatus;
1031
+ };
1032
+
1033
+ export const PromptInputSubmit = ({
1034
+ className,
1035
+ variant = 'default',
1036
+ size = 'icon-sm',
1037
+ status,
1038
+ children,
1039
+ ...props
1040
+ }: PromptInputSubmitProps) => {
1041
+ let Icon = <CornerDownLeftIcon className="size-4" />;
1042
+
1043
+ if (status === 'submitted') {
1044
+ Icon = <Loader2Icon className="size-4 animate-spin" />;
1045
+ } else if (status === 'streaming') {
1046
+ Icon = <SquareIcon className="size-4" />;
1047
+ } else if (status === 'error') {
1048
+ Icon = <XIcon className="size-4" />;
1049
+ }
1050
+
1051
+ return (
1052
+ <InputGroupButton
1053
+ aria-label="Submit"
1054
+ className={cn(className)}
1055
+ size={size}
1056
+ type="submit"
1057
+ variant={variant}
1058
+ {...props}
1059
+ >
1060
+ {children ?? Icon}
1061
+ </InputGroupButton>
1062
+ );
1063
+ };
1064
+
1065
+ interface SpeechRecognition extends EventTarget {
1066
+ continuous: boolean;
1067
+ interimResults: boolean;
1068
+ lang: string;
1069
+ start(): void;
1070
+ stop(): void;
1071
+ onstart: ((this: SpeechRecognition, ev: Event) => any) | null;
1072
+ onend: ((this: SpeechRecognition, ev: Event) => any) | null;
1073
+ onresult:
1074
+ | ((this: SpeechRecognition, ev: SpeechRecognitionEvent) => any)
1075
+ | null;
1076
+ onerror:
1077
+ | ((this: SpeechRecognition, ev: SpeechRecognitionErrorEvent) => any)
1078
+ | null;
1079
+ }
1080
+
1081
+ interface SpeechRecognitionEvent extends Event {
1082
+ results: SpeechRecognitionResultList;
1083
+ resultIndex: number;
1084
+ }
1085
+
1086
+ interface SpeechRecognitionResultList {
1087
+ readonly length: number;
1088
+ item(index: number): SpeechRecognitionResult;
1089
+ [index: number]: SpeechRecognitionResult;
1090
+ }
1091
+
1092
+ interface SpeechRecognitionResult {
1093
+ readonly length: number;
1094
+ item(index: number): SpeechRecognitionAlternative;
1095
+ [index: number]: SpeechRecognitionAlternative;
1096
+ isFinal: boolean;
1097
+ }
1098
+
1099
+ interface SpeechRecognitionAlternative {
1100
+ transcript: string;
1101
+ confidence: number;
1102
+ }
1103
+
1104
+ interface SpeechRecognitionErrorEvent extends Event {
1105
+ error: string;
1106
+ }
1107
+
1108
+ declare global {
1109
+ interface Window {
1110
+ SpeechRecognition: {
1111
+ new (): SpeechRecognition;
1112
+ };
1113
+ webkitSpeechRecognition: {
1114
+ new (): SpeechRecognition;
1115
+ };
1116
+ }
1117
+ }
1118
+
1119
+ export type PromptInputSpeechButtonProps = ComponentProps<
1120
+ typeof PromptInputButton
1121
+ > & {
1122
+ textareaRef?: RefObject<HTMLTextAreaElement | null>;
1123
+ onTranscriptionChange?: (text: string) => void;
1124
+ };
1125
+
1126
+ export const PromptInputSpeechButton = ({
1127
+ className,
1128
+ textareaRef,
1129
+ onTranscriptionChange,
1130
+ ...props
1131
+ }: PromptInputSpeechButtonProps) => {
1132
+ const [isListening, setIsListening] = useState(false);
1133
+ const [recognition, setRecognition] = useState<SpeechRecognition | null>(
1134
+ null
1135
+ );
1136
+ const recognitionRef = useRef<SpeechRecognition | null>(null);
1137
+
1138
+ useEffect(() => {
1139
+ if (
1140
+ typeof window !== 'undefined' &&
1141
+ ('SpeechRecognition' in window || 'webkitSpeechRecognition' in window)
1142
+ ) {
1143
+ const SpeechRecognition =
1144
+ window.SpeechRecognition || window.webkitSpeechRecognition;
1145
+ const speechRecognition = new SpeechRecognition();
1146
+
1147
+ speechRecognition.continuous = true;
1148
+ speechRecognition.interimResults = true;
1149
+ speechRecognition.lang = 'en-US';
1150
+
1151
+ speechRecognition.onstart = () => {
1152
+ setIsListening(true);
1153
+ };
1154
+
1155
+ speechRecognition.onend = () => {
1156
+ setIsListening(false);
1157
+ };
1158
+
1159
+ speechRecognition.onresult = (event) => {
1160
+ let finalTranscript = '';
1161
+
1162
+ for (let i = event.resultIndex; i < event.results.length; i++) {
1163
+ const result = event.results[i];
1164
+ if (result.isFinal) {
1165
+ finalTranscript += result[0]?.transcript ?? '';
1166
+ }
1167
+ }
1168
+
1169
+ if (finalTranscript && textareaRef?.current) {
1170
+ const textarea = textareaRef.current;
1171
+ const currentValue = textarea.value;
1172
+ const newValue =
1173
+ currentValue + (currentValue ? ' ' : '') + finalTranscript;
1174
+
1175
+ textarea.value = newValue;
1176
+ textarea.dispatchEvent(new Event('input', { bubbles: true }));
1177
+ onTranscriptionChange?.(newValue);
1178
+ }
1179
+ };
1180
+
1181
+ speechRecognition.onerror = (event) => {
1182
+ console.error('Speech recognition error:', event.error);
1183
+ setIsListening(false);
1184
+ };
1185
+
1186
+ recognitionRef.current = speechRecognition;
1187
+ setRecognition(speechRecognition);
1188
+ }
1189
+
1190
+ return () => {
1191
+ if (recognitionRef.current) {
1192
+ recognitionRef.current.stop();
1193
+ }
1194
+ };
1195
+ }, [textareaRef, onTranscriptionChange]);
1196
+
1197
+ const toggleListening = useCallback(() => {
1198
+ if (!recognition) {
1199
+ return;
1200
+ }
1201
+
1202
+ if (isListening) {
1203
+ recognition.stop();
1204
+ } else {
1205
+ recognition.start();
1206
+ }
1207
+ }, [recognition, isListening]);
1208
+
1209
+ return (
1210
+ <PromptInputButton
1211
+ className={cn(
1212
+ 'relative transition-all duration-200',
1213
+ isListening && 'animate-pulse bg-accent text-accent-foreground',
1214
+ className
1215
+ )}
1216
+ disabled={!recognition}
1217
+ onClick={toggleListening}
1218
+ {...props}
1219
+ >
1220
+ <MicIcon className="size-4" />
1221
+ </PromptInputButton>
1222
+ );
1223
+ };
1224
+
1225
+ export type PromptInputSelectProps = ComponentProps<typeof Select>;
1226
+
1227
+ export const PromptInputSelect = (props: PromptInputSelectProps) => (
1228
+ <Select {...props} />
1229
+ );
1230
+
1231
+ export type PromptInputSelectTriggerProps = ComponentProps<
1232
+ typeof SelectTrigger
1233
+ >;
1234
+
1235
+ export const PromptInputSelectTrigger = ({
1236
+ className,
1237
+ ...props
1238
+ }: PromptInputSelectTriggerProps) => (
1239
+ <SelectTrigger
1240
+ className={cn(
1241
+ 'border-none bg-transparent font-medium text-muted-foreground shadow-none transition-colors',
1242
+ 'hover:bg-accent hover:text-foreground aria-expanded:bg-accent aria-expanded:text-foreground',
1243
+ className
1244
+ )}
1245
+ {...props}
1246
+ />
1247
+ );
1248
+
1249
+ export type PromptInputSelectContentProps = ComponentProps<
1250
+ typeof SelectContent
1251
+ >;
1252
+
1253
+ export const PromptInputSelectContent = ({
1254
+ className,
1255
+ ...props
1256
+ }: PromptInputSelectContentProps) => (
1257
+ <SelectContent className={cn(className)} {...props} />
1258
+ );
1259
+
1260
+ export type PromptInputSelectItemProps = ComponentProps<typeof SelectItem>;
1261
+
1262
+ export const PromptInputSelectItem = ({
1263
+ className,
1264
+ ...props
1265
+ }: PromptInputSelectItemProps) => (
1266
+ <SelectItem className={cn(className)} {...props} />
1267
+ );
1268
+
1269
+ export type PromptInputSelectValueProps = ComponentProps<typeof SelectValue>;
1270
+
1271
+ export const PromptInputSelectValue = ({
1272
+ className,
1273
+ ...props
1274
+ }: PromptInputSelectValueProps) => (
1275
+ <SelectValue className={cn(className)} {...props} />
1276
+ );
1277
+
1278
+ export type PromptInputHoverCardProps = ComponentProps<typeof HoverCard>;
1279
+
1280
+ export const PromptInputHoverCard = ({
1281
+ openDelay = 0,
1282
+ closeDelay = 0,
1283
+ ...props
1284
+ }: PromptInputHoverCardProps) => (
1285
+ <HoverCard closeDelay={closeDelay} openDelay={openDelay} {...props} />
1286
+ );
1287
+
1288
+ export type PromptInputHoverCardTriggerProps = ComponentProps<
1289
+ typeof HoverCardTrigger
1290
+ >;
1291
+
1292
+ export const PromptInputHoverCardTrigger = (
1293
+ props: PromptInputHoverCardTriggerProps
1294
+ ) => <HoverCardTrigger {...props} />;
1295
+
1296
+ export type PromptInputHoverCardContentProps = ComponentProps<
1297
+ typeof HoverCardContent
1298
+ >;
1299
+
1300
+ export const PromptInputHoverCardContent = ({
1301
+ align = 'start',
1302
+ ...props
1303
+ }: PromptInputHoverCardContentProps) => (
1304
+ <HoverCardContent align={align} {...props} />
1305
+ );
1306
+
1307
+ export type PromptInputTabsListProps = HTMLAttributes<HTMLDivElement>;
1308
+
1309
+ export const PromptInputTabsList = ({
1310
+ className,
1311
+ ...props
1312
+ }: PromptInputTabsListProps) => <div className={cn(className)} {...props} />;
1313
+
1314
+ export type PromptInputTabProps = HTMLAttributes<HTMLDivElement>;
1315
+
1316
+ export const PromptInputTab = ({
1317
+ className,
1318
+ ...props
1319
+ }: PromptInputTabProps) => <div className={cn(className)} {...props} />;
1320
+
1321
+ export type PromptInputTabLabelProps = HTMLAttributes<HTMLHeadingElement>;
1322
+
1323
+ export const PromptInputTabLabel = ({
1324
+ className,
1325
+ ...props
1326
+ }: PromptInputTabLabelProps) => (
1327
+ <h3
1328
+ className={cn(
1329
+ 'mb-2 px-3 font-medium text-muted-foreground text-xs',
1330
+ className
1331
+ )}
1332
+ {...props}
1333
+ />
1334
+ );
1335
+
1336
+ export type PromptInputTabBodyProps = HTMLAttributes<HTMLDivElement>;
1337
+
1338
+ export const PromptInputTabBody = ({
1339
+ className,
1340
+ ...props
1341
+ }: PromptInputTabBodyProps) => (
1342
+ <div className={cn('space-y-1', className)} {...props} />
1343
+ );
1344
+
1345
+ export type PromptInputTabItemProps = HTMLAttributes<HTMLDivElement>;
1346
+
1347
+ export const PromptInputTabItem = ({
1348
+ className,
1349
+ ...props
1350
+ }: PromptInputTabItemProps) => (
1351
+ <div
1352
+ className={cn(
1353
+ 'flex items-center gap-2 px-3 py-2 text-xs hover:bg-accent',
1354
+ className
1355
+ )}
1356
+ {...props}
1357
+ />
1358
+ );
1359
+
1360
+ export type PromptInputCommandProps = ComponentProps<typeof Command>;
1361
+
1362
+ export const PromptInputCommand = ({
1363
+ className,
1364
+ ...props
1365
+ }: PromptInputCommandProps) => <Command className={cn(className)} {...props} />;
1366
+
1367
+ export type PromptInputCommandInputProps = ComponentProps<typeof CommandInput>;
1368
+
1369
+ export const PromptInputCommandInput = ({
1370
+ className,
1371
+ ...props
1372
+ }: PromptInputCommandInputProps) => (
1373
+ <CommandInput className={cn(className)} {...props} />
1374
+ );
1375
+
1376
+ export type PromptInputCommandListProps = ComponentProps<typeof CommandList>;
1377
+
1378
+ export const PromptInputCommandList = ({
1379
+ className,
1380
+ ...props
1381
+ }: PromptInputCommandListProps) => (
1382
+ <CommandList className={cn(className)} {...props} />
1383
+ );
1384
+
1385
+ export type PromptInputCommandEmptyProps = ComponentProps<typeof CommandEmpty>;
1386
+
1387
+ export const PromptInputCommandEmpty = ({
1388
+ className,
1389
+ ...props
1390
+ }: PromptInputCommandEmptyProps) => (
1391
+ <CommandEmpty className={cn(className)} {...props} />
1392
+ );
1393
+
1394
+ export type PromptInputCommandGroupProps = ComponentProps<typeof CommandGroup>;
1395
+
1396
+ export const PromptInputCommandGroup = ({
1397
+ className,
1398
+ ...props
1399
+ }: PromptInputCommandGroupProps) => (
1400
+ <CommandGroup className={cn(className)} {...props} />
1401
+ );
1402
+
1403
+ export type PromptInputCommandItemProps = ComponentProps<typeof CommandItem>;
1404
+
1405
+ export const PromptInputCommandItem = ({
1406
+ className,
1407
+ ...props
1408
+ }: PromptInputCommandItemProps) => (
1409
+ <CommandItem className={cn(className)} {...props} />
1410
+ );
1411
+
1412
+ export type PromptInputCommandSeparatorProps = ComponentProps<
1413
+ typeof CommandSeparator
1414
+ >;
1415
+
1416
+ export const PromptInputCommandSeparator = ({
1417
+ className,
1418
+ ...props
1419
+ }: PromptInputCommandSeparatorProps) => (
1420
+ <CommandSeparator className={cn(className)} {...props} />
1421
+ );