@chat-js/cli 0.6.1 → 0.6.3

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