dreamboard 0.1.21 → 0.1.23

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