@sero-ai/ui 0.1.0 → 0.2.1
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.
- package/.turbo/turbo-typecheck.log +1 -1
- package/LICENSE +201 -0
- package/package.json +22 -18
- package/src/components/ai-elements/attachments.tsx +4 -4
- package/src/components/ai-elements/audio-player.tsx +1 -1
- package/src/components/ai-elements/chain-of-thought.tsx +2 -2
- package/src/components/ai-elements/confirmation.tsx +2 -2
- package/src/components/ai-elements/context.tsx +2 -2
- package/src/components/ai-elements/environment-variables.tsx +6 -6
- package/src/components/ai-elements/file-tree.tsx +3 -3
- package/src/components/ai-elements/inline-citation.tsx +2 -2
- package/src/components/ai-elements/jsx-preview.tsx +2 -2
- package/src/components/ai-elements/message.tsx +2 -2
- package/src/components/ai-elements/mic-selector.tsx +8 -8
- package/src/components/ai-elements/open-in-chat.tsx +2 -2
- package/src/components/ai-elements/package-info.tsx +4 -4
- package/src/components/ai-elements/plan.tsx +2 -2
- package/src/components/ai-elements/prompt-input-context.tsx +252 -0
- package/src/components/ai-elements/prompt-input-elements.tsx +371 -0
- package/src/components/ai-elements/prompt-input-textarea.tsx +117 -0
- package/src/components/ai-elements/prompt-input.tsx +59 -990
- package/src/components/ai-elements/queue.tsx +1 -1
- package/src/components/ai-elements/reasoning.tsx +3 -3
- package/src/components/ai-elements/schema-display.tsx +7 -7
- package/src/components/ai-elements/snippet.tsx +3 -3
- package/src/components/ai-elements/sources.tsx +2 -2
- package/src/components/ai-elements/terminal.tsx +5 -5
- package/src/components/ai-elements/test-results.tsx +8 -8
- package/src/components/ai-elements/transcription.tsx +2 -2
- package/src/components/ai-elements/voice-selector-accent.tsx +50 -0
- package/src/components/ai-elements/voice-selector.tsx +8 -177
- package/src/components/ai-elements/web-preview.tsx +5 -6
- package/src/components/model-selection/available-model-picker.tsx +231 -0
- package/src/components/model-selection/model-warning-list.tsx +33 -0
- package/src/components/model-selection/thinking-level-picker.tsx +55 -0
- package/src/components/ui/alert-dialog.tsx +1 -1
- package/src/components/ui/calendar.tsx +1 -1
- package/src/components/ui/carousel.tsx +1 -1
- package/src/components/ui/chart.tsx +3 -3
- package/src/components/ui/command.tsx +2 -2
- package/src/components/ui/dialog.tsx +1 -1
- package/src/components/ui/field.tsx +2 -2
- package/src/components/ui/form.tsx +2 -2
- package/src/components/ui/input-otp.tsx +2 -2
- package/src/components/ui/input.tsx +3 -1
- package/src/components/ui/menubar.tsx +1 -1
- package/src/components/ui/native-select.tsx +2 -0
- package/src/components/ui/navigation-menu.tsx +1 -1
- package/src/components/ui/plugin-safety-disclaimer.tsx +25 -0
- package/src/components/ui/progress.tsx +1 -1
- package/src/components/ui/resizable.tsx +1 -1
- package/src/components/ui/search-input.tsx +39 -0
- package/src/components/ui/slider.tsx +7 -2
- package/src/components/ui/textarea.tsx +3 -1
- package/src/components/ui/toggle-group.tsx +1 -1
- package/src/components/ui/tree.tsx +3 -3
- package/src/index.ts +128 -7
- package/src/styles/globals.css +91 -26
- package/src/styles/plugin.css +4 -0
- package/src/theme/apply-theme.ts +250 -0
- package/src/theme/index.ts +2 -0
- package/src/theme/types.ts +253 -0
|
@@ -1,78 +1,33 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import type { ChatStatus, FileUIPart, SourceDocumentUIPart } from "ai";
|
|
4
3
|
import type {
|
|
5
|
-
ChangeEvent,
|
|
6
|
-
ChangeEventHandler,
|
|
7
|
-
ClipboardEventHandler,
|
|
8
|
-
ComponentProps,
|
|
9
4
|
FormEvent,
|
|
10
5
|
FormEventHandler,
|
|
11
6
|
HTMLAttributes,
|
|
12
|
-
KeyboardEventHandler,
|
|
13
|
-
PropsWithChildren,
|
|
14
|
-
ReactNode,
|
|
15
|
-
RefObject,
|
|
16
7
|
} from "react";
|
|
17
|
-
|
|
18
|
-
import {
|
|
19
|
-
Command,
|
|
20
|
-
CommandEmpty,
|
|
21
|
-
CommandGroup,
|
|
22
|
-
CommandInput,
|
|
23
|
-
CommandItem,
|
|
24
|
-
CommandList,
|
|
25
|
-
CommandSeparator,
|
|
26
|
-
} from "../ui/command";
|
|
27
|
-
import {
|
|
28
|
-
DropdownMenu,
|
|
29
|
-
DropdownMenuContent,
|
|
30
|
-
DropdownMenuItem,
|
|
31
|
-
DropdownMenuTrigger,
|
|
32
|
-
} from "../ui/dropdown-menu";
|
|
33
|
-
import {
|
|
34
|
-
HoverCard,
|
|
35
|
-
HoverCardContent,
|
|
36
|
-
HoverCardTrigger,
|
|
37
|
-
} from "../ui/hover-card";
|
|
38
|
-
import {
|
|
39
|
-
InputGroup,
|
|
40
|
-
InputGroupAddon,
|
|
41
|
-
InputGroupButton,
|
|
42
|
-
InputGroupTextarea,
|
|
43
|
-
} from "../ui/input-group";
|
|
44
|
-
import {
|
|
45
|
-
Select,
|
|
46
|
-
SelectContent,
|
|
47
|
-
SelectItem,
|
|
48
|
-
SelectTrigger,
|
|
49
|
-
SelectValue,
|
|
50
|
-
} from "../ui/select";
|
|
51
|
-
import { Spinner } from "../ui/spinner";
|
|
52
|
-
import {
|
|
53
|
-
Tooltip,
|
|
54
|
-
TooltipContent,
|
|
55
|
-
TooltipTrigger,
|
|
56
|
-
} from "../ui/tooltip";
|
|
8
|
+
import type { FileUIPart, SourceDocumentUIPart } from "ai";
|
|
9
|
+
import { InputGroup } from "../ui/input-group";
|
|
57
10
|
import { cn } from "../../lib/utils";
|
|
58
|
-
import {
|
|
59
|
-
CornerDownLeftIcon,
|
|
60
|
-
ImageIcon,
|
|
61
|
-
PlusIcon,
|
|
62
|
-
SquareIcon,
|
|
63
|
-
XIcon,
|
|
64
|
-
} from "lucide-react";
|
|
65
11
|
import { nanoid } from "nanoid";
|
|
66
12
|
import {
|
|
67
|
-
Children,
|
|
68
|
-
createContext,
|
|
69
13
|
useCallback,
|
|
70
|
-
useContext,
|
|
71
14
|
useEffect,
|
|
72
15
|
useMemo,
|
|
73
16
|
useRef,
|
|
74
17
|
useState,
|
|
75
18
|
} from "react";
|
|
19
|
+
import {
|
|
20
|
+
useOptionalPromptInputController,
|
|
21
|
+
LocalAttachmentsContext,
|
|
22
|
+
LocalReferencedSourcesContext,
|
|
23
|
+
type AttachmentsContext,
|
|
24
|
+
type ReferencedSourcesContext,
|
|
25
|
+
} from "./prompt-input-context";
|
|
26
|
+
|
|
27
|
+
// Re-export everything so consumers can keep importing from this single path
|
|
28
|
+
export * from "./prompt-input-context";
|
|
29
|
+
export * from "./prompt-input-textarea";
|
|
30
|
+
export * from "./prompt-input-elements";
|
|
76
31
|
|
|
77
32
|
// ============================================================================
|
|
78
33
|
// Helpers
|
|
@@ -98,266 +53,9 @@ const convertBlobUrlToDataUrl = async (url: string): Promise<string | null> => {
|
|
|
98
53
|
};
|
|
99
54
|
|
|
100
55
|
// ============================================================================
|
|
101
|
-
//
|
|
102
|
-
// ============================================================================
|
|
103
|
-
|
|
104
|
-
export interface AttachmentsContext {
|
|
105
|
-
files: (FileUIPart & { id: string })[];
|
|
106
|
-
add: (files: File[] | FileList) => void;
|
|
107
|
-
remove: (id: string) => void;
|
|
108
|
-
clear: () => void;
|
|
109
|
-
openFileDialog: () => void;
|
|
110
|
-
fileInputRef: RefObject<HTMLInputElement | null>;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export interface TextInputContext {
|
|
114
|
-
value: string;
|
|
115
|
-
setInput: (v: string) => void;
|
|
116
|
-
clear: () => void;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export interface PromptInputControllerProps {
|
|
120
|
-
textInput: TextInputContext;
|
|
121
|
-
attachments: AttachmentsContext;
|
|
122
|
-
/** INTERNAL: Allows PromptInput to register its file textInput + "open" callback */
|
|
123
|
-
__registerFileInput: (
|
|
124
|
-
ref: RefObject<HTMLInputElement | null>,
|
|
125
|
-
open: () => void
|
|
126
|
-
) => void;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const PromptInputController = createContext<PromptInputControllerProps | null>(
|
|
130
|
-
null
|
|
131
|
-
);
|
|
132
|
-
const ProviderAttachmentsContext = createContext<AttachmentsContext | null>(
|
|
133
|
-
null
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
export const usePromptInputController = () => {
|
|
137
|
-
const ctx = useContext(PromptInputController);
|
|
138
|
-
if (!ctx) {
|
|
139
|
-
throw new Error(
|
|
140
|
-
"Wrap your component inside <PromptInputProvider> to use usePromptInputController()."
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
return ctx;
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
// Optional variants (do NOT throw). Useful for dual-mode components.
|
|
147
|
-
const useOptionalPromptInputController = () =>
|
|
148
|
-
useContext(PromptInputController);
|
|
149
|
-
|
|
150
|
-
export const useProviderAttachments = () => {
|
|
151
|
-
const ctx = useContext(ProviderAttachmentsContext);
|
|
152
|
-
if (!ctx) {
|
|
153
|
-
throw new Error(
|
|
154
|
-
"Wrap your component inside <PromptInputProvider> to use useProviderAttachments()."
|
|
155
|
-
);
|
|
156
|
-
}
|
|
157
|
-
return ctx;
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
const useOptionalProviderAttachments = () =>
|
|
161
|
-
useContext(ProviderAttachmentsContext);
|
|
162
|
-
|
|
163
|
-
export type PromptInputProviderProps = PropsWithChildren<{
|
|
164
|
-
initialInput?: string;
|
|
165
|
-
}>;
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Optional global provider that lifts PromptInput state outside of PromptInput.
|
|
169
|
-
* If you don't use it, PromptInput stays fully self-managed.
|
|
170
|
-
*/
|
|
171
|
-
export const PromptInputProvider = ({
|
|
172
|
-
initialInput: initialTextInput = "",
|
|
173
|
-
children,
|
|
174
|
-
}: PromptInputProviderProps) => {
|
|
175
|
-
// ----- textInput state
|
|
176
|
-
const [textInput, setTextInput] = useState(initialTextInput);
|
|
177
|
-
const clearInput = useCallback(() => setTextInput(""), []);
|
|
178
|
-
|
|
179
|
-
// ----- attachments state (global when wrapped)
|
|
180
|
-
const [attachmentFiles, setAttachmentFiles] = useState<
|
|
181
|
-
(FileUIPart & { id: string })[]
|
|
182
|
-
>([]);
|
|
183
|
-
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
|
184
|
-
// oxlint-disable-next-line eslint(no-empty-function)
|
|
185
|
-
const openRef = useRef<() => void>(() => {});
|
|
186
|
-
|
|
187
|
-
const add = useCallback((files: File[] | FileList) => {
|
|
188
|
-
const incoming = [...files];
|
|
189
|
-
if (incoming.length === 0) {
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
setAttachmentFiles((prev) => [
|
|
194
|
-
...prev,
|
|
195
|
-
...incoming.map((file) => ({
|
|
196
|
-
filename: file.name,
|
|
197
|
-
id: nanoid(),
|
|
198
|
-
mediaType: file.type,
|
|
199
|
-
type: "file" as const,
|
|
200
|
-
url: URL.createObjectURL(file),
|
|
201
|
-
})),
|
|
202
|
-
]);
|
|
203
|
-
}, []);
|
|
204
|
-
|
|
205
|
-
const remove = useCallback((id: string) => {
|
|
206
|
-
setAttachmentFiles((prev) => {
|
|
207
|
-
const found = prev.find((f) => f.id === id);
|
|
208
|
-
if (found?.url) {
|
|
209
|
-
URL.revokeObjectURL(found.url);
|
|
210
|
-
}
|
|
211
|
-
return prev.filter((f) => f.id !== id);
|
|
212
|
-
});
|
|
213
|
-
}, []);
|
|
214
|
-
|
|
215
|
-
const clear = useCallback(() => {
|
|
216
|
-
setAttachmentFiles((prev) => {
|
|
217
|
-
for (const f of prev) {
|
|
218
|
-
if (f.url) {
|
|
219
|
-
URL.revokeObjectURL(f.url);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
return [];
|
|
223
|
-
});
|
|
224
|
-
}, []);
|
|
225
|
-
|
|
226
|
-
// Keep a ref to attachments for cleanup on unmount (avoids stale closure)
|
|
227
|
-
const attachmentsRef = useRef(attachmentFiles);
|
|
228
|
-
|
|
229
|
-
useEffect(() => {
|
|
230
|
-
attachmentsRef.current = attachmentFiles;
|
|
231
|
-
}, [attachmentFiles]);
|
|
232
|
-
|
|
233
|
-
// Cleanup blob URLs on unmount to prevent memory leaks
|
|
234
|
-
useEffect(
|
|
235
|
-
() => () => {
|
|
236
|
-
for (const f of attachmentsRef.current) {
|
|
237
|
-
if (f.url) {
|
|
238
|
-
URL.revokeObjectURL(f.url);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
},
|
|
242
|
-
[]
|
|
243
|
-
);
|
|
244
|
-
|
|
245
|
-
const openFileDialog = useCallback(() => {
|
|
246
|
-
openRef.current?.();
|
|
247
|
-
}, []);
|
|
248
|
-
|
|
249
|
-
const attachments = useMemo<AttachmentsContext>(
|
|
250
|
-
() => ({
|
|
251
|
-
add,
|
|
252
|
-
clear,
|
|
253
|
-
fileInputRef,
|
|
254
|
-
files: attachmentFiles,
|
|
255
|
-
openFileDialog,
|
|
256
|
-
remove,
|
|
257
|
-
}),
|
|
258
|
-
[attachmentFiles, add, remove, clear, openFileDialog]
|
|
259
|
-
);
|
|
260
|
-
|
|
261
|
-
const __registerFileInput = useCallback(
|
|
262
|
-
(ref: RefObject<HTMLInputElement | null>, open: () => void) => {
|
|
263
|
-
fileInputRef.current = ref.current;
|
|
264
|
-
openRef.current = open;
|
|
265
|
-
},
|
|
266
|
-
[]
|
|
267
|
-
);
|
|
268
|
-
|
|
269
|
-
const controller = useMemo<PromptInputControllerProps>(
|
|
270
|
-
() => ({
|
|
271
|
-
__registerFileInput,
|
|
272
|
-
attachments,
|
|
273
|
-
textInput: {
|
|
274
|
-
clear: clearInput,
|
|
275
|
-
setInput: setTextInput,
|
|
276
|
-
value: textInput,
|
|
277
|
-
},
|
|
278
|
-
}),
|
|
279
|
-
[textInput, clearInput, attachments, __registerFileInput]
|
|
280
|
-
);
|
|
281
|
-
|
|
282
|
-
return (
|
|
283
|
-
<PromptInputController.Provider value={controller}>
|
|
284
|
-
<ProviderAttachmentsContext.Provider value={attachments}>
|
|
285
|
-
{children}
|
|
286
|
-
</ProviderAttachmentsContext.Provider>
|
|
287
|
-
</PromptInputController.Provider>
|
|
288
|
-
);
|
|
289
|
-
};
|
|
290
|
-
|
|
291
|
-
// ============================================================================
|
|
292
|
-
// Component Context & Hooks
|
|
293
|
-
// ============================================================================
|
|
294
|
-
|
|
295
|
-
const LocalAttachmentsContext = createContext<AttachmentsContext | null>(null);
|
|
296
|
-
|
|
297
|
-
export const usePromptInputAttachments = () => {
|
|
298
|
-
// Prefer local context (inside PromptInput) as it has validation, fall back to provider
|
|
299
|
-
const provider = useOptionalProviderAttachments();
|
|
300
|
-
const local = useContext(LocalAttachmentsContext);
|
|
301
|
-
const context = local ?? provider;
|
|
302
|
-
if (!context) {
|
|
303
|
-
throw new Error(
|
|
304
|
-
"usePromptInputAttachments must be used within a PromptInput or PromptInputProvider"
|
|
305
|
-
);
|
|
306
|
-
}
|
|
307
|
-
return context;
|
|
308
|
-
};
|
|
309
|
-
|
|
310
|
-
// ============================================================================
|
|
311
|
-
// Referenced Sources (Local to PromptInput)
|
|
56
|
+
// PromptInput
|
|
312
57
|
// ============================================================================
|
|
313
58
|
|
|
314
|
-
export interface ReferencedSourcesContext {
|
|
315
|
-
sources: (SourceDocumentUIPart & { id: string })[];
|
|
316
|
-
add: (sources: SourceDocumentUIPart[] | SourceDocumentUIPart) => void;
|
|
317
|
-
remove: (id: string) => void;
|
|
318
|
-
clear: () => void;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
export const LocalReferencedSourcesContext =
|
|
322
|
-
createContext<ReferencedSourcesContext | null>(null);
|
|
323
|
-
|
|
324
|
-
export const usePromptInputReferencedSources = () => {
|
|
325
|
-
const ctx = useContext(LocalReferencedSourcesContext);
|
|
326
|
-
if (!ctx) {
|
|
327
|
-
throw new Error(
|
|
328
|
-
"usePromptInputReferencedSources must be used within a LocalReferencedSourcesContext.Provider"
|
|
329
|
-
);
|
|
330
|
-
}
|
|
331
|
-
return ctx;
|
|
332
|
-
};
|
|
333
|
-
|
|
334
|
-
export type PromptInputActionAddAttachmentsProps = ComponentProps<
|
|
335
|
-
typeof DropdownMenuItem
|
|
336
|
-
> & {
|
|
337
|
-
label?: string;
|
|
338
|
-
};
|
|
339
|
-
|
|
340
|
-
export const PromptInputActionAddAttachments = ({
|
|
341
|
-
label = "Add photos or files",
|
|
342
|
-
...props
|
|
343
|
-
}: PromptInputActionAddAttachmentsProps) => {
|
|
344
|
-
const attachments = usePromptInputAttachments();
|
|
345
|
-
|
|
346
|
-
const handleSelect = useCallback(
|
|
347
|
-
(e: Event) => {
|
|
348
|
-
e.preventDefault();
|
|
349
|
-
attachments.openFileDialog();
|
|
350
|
-
},
|
|
351
|
-
[attachments]
|
|
352
|
-
);
|
|
353
|
-
|
|
354
|
-
return (
|
|
355
|
-
<DropdownMenuItem {...props} onSelect={handleSelect}>
|
|
356
|
-
<ImageIcon className="mr-2 size-4" /> {label}
|
|
357
|
-
</DropdownMenuItem>
|
|
358
|
-
);
|
|
359
|
-
};
|
|
360
|
-
|
|
361
59
|
export interface PromptInputMessage {
|
|
362
60
|
text: string;
|
|
363
61
|
files: FileUIPart[];
|
|
@@ -401,11 +99,9 @@ export const PromptInput = ({
|
|
|
401
99
|
children,
|
|
402
100
|
...props
|
|
403
101
|
}: PromptInputProps) => {
|
|
404
|
-
// Try to use a provider controller if present
|
|
405
102
|
const controller = useOptionalPromptInputController();
|
|
406
103
|
const usingProvider = !!controller;
|
|
407
104
|
|
|
408
|
-
// Refs
|
|
409
105
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
410
106
|
const formRef = useRef<HTMLFormElement | null>(null);
|
|
411
107
|
|
|
@@ -420,29 +116,16 @@ export const PromptInput = ({
|
|
|
420
116
|
|
|
421
117
|
// Keep a ref to files for cleanup on unmount (avoids stale closure)
|
|
422
118
|
const filesRef = useRef(files);
|
|
119
|
+
useEffect(() => { filesRef.current = files; }, [files]);
|
|
423
120
|
|
|
424
|
-
|
|
425
|
-
filesRef.current = files;
|
|
426
|
-
}, [files]);
|
|
427
|
-
|
|
428
|
-
const openFileDialogLocal = useCallback(() => {
|
|
429
|
-
inputRef.current?.click();
|
|
430
|
-
}, []);
|
|
121
|
+
const openFileDialogLocal = useCallback(() => { inputRef.current?.click(); }, []);
|
|
431
122
|
|
|
432
123
|
const matchesAccept = useCallback(
|
|
433
124
|
(f: File) => {
|
|
434
|
-
if (!accept || accept.trim() === "")
|
|
435
|
-
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
const patterns = accept
|
|
439
|
-
.split(",")
|
|
440
|
-
.map((s) => s.trim())
|
|
441
|
-
.filter(Boolean);
|
|
442
|
-
|
|
125
|
+
if (!accept || accept.trim() === "") return true;
|
|
126
|
+
const patterns = accept.split(",").map((s) => s.trim()).filter(Boolean);
|
|
443
127
|
return patterns.some((pattern) => {
|
|
444
128
|
if (pattern.endsWith("/*")) {
|
|
445
|
-
// e.g: image/* -> image/
|
|
446
129
|
const prefix = pattern.slice(0, -1);
|
|
447
130
|
return f.type.startsWith(prefix);
|
|
448
131
|
}
|
|
@@ -457,45 +140,25 @@ export const PromptInput = ({
|
|
|
457
140
|
const incoming = [...fileList];
|
|
458
141
|
const accepted = incoming.filter((f) => matchesAccept(f));
|
|
459
142
|
if (incoming.length && accepted.length === 0) {
|
|
460
|
-
onError?.({
|
|
461
|
-
code: "accept",
|
|
462
|
-
message: "No files match the accepted types.",
|
|
463
|
-
});
|
|
143
|
+
onError?.({ code: "accept", message: "No files match the accepted types." });
|
|
464
144
|
return;
|
|
465
145
|
}
|
|
466
|
-
const withinSize = (f: File) =>
|
|
467
|
-
maxFileSize ? f.size <= maxFileSize : true;
|
|
146
|
+
const withinSize = (f: File) => maxFileSize ? f.size <= maxFileSize : true;
|
|
468
147
|
const sized = accepted.filter(withinSize);
|
|
469
148
|
if (accepted.length > 0 && sized.length === 0) {
|
|
470
|
-
onError?.({
|
|
471
|
-
code: "max_file_size",
|
|
472
|
-
message: "All files exceed the maximum size.",
|
|
473
|
-
});
|
|
149
|
+
onError?.({ code: "max_file_size", message: "All files exceed the maximum size." });
|
|
474
150
|
return;
|
|
475
151
|
}
|
|
476
152
|
|
|
477
153
|
setItems((prev) => {
|
|
478
|
-
const capacity =
|
|
479
|
-
|
|
480
|
-
? Math.max(0, maxFiles - prev.length)
|
|
481
|
-
: undefined;
|
|
482
|
-
const capped =
|
|
483
|
-
typeof capacity === "number" ? sized.slice(0, capacity) : sized;
|
|
154
|
+
const capacity = typeof maxFiles === "number" ? Math.max(0, maxFiles - prev.length) : undefined;
|
|
155
|
+
const capped = typeof capacity === "number" ? sized.slice(0, capacity) : sized;
|
|
484
156
|
if (typeof capacity === "number" && sized.length > capacity) {
|
|
485
|
-
onError?.({
|
|
486
|
-
code: "max_files",
|
|
487
|
-
message: "Too many files. Some were not added.",
|
|
488
|
-
});
|
|
157
|
+
onError?.({ code: "max_files", message: "Too many files. Some were not added." });
|
|
489
158
|
}
|
|
490
159
|
const next: (FileUIPart & { id: string })[] = [];
|
|
491
160
|
for (const file of capped) {
|
|
492
|
-
next.push({
|
|
493
|
-
filename: file.name,
|
|
494
|
-
id: nanoid(),
|
|
495
|
-
mediaType: file.type,
|
|
496
|
-
type: "file",
|
|
497
|
-
url: URL.createObjectURL(file),
|
|
498
|
-
});
|
|
161
|
+
next.push({ filename: file.name, id: nanoid(), mediaType: file.type, type: "file", url: URL.createObjectURL(file) });
|
|
499
162
|
}
|
|
500
163
|
return [...prev, ...next];
|
|
501
164
|
});
|
|
@@ -507,9 +170,7 @@ export const PromptInput = ({
|
|
|
507
170
|
(id: string) =>
|
|
508
171
|
setItems((prev) => {
|
|
509
172
|
const found = prev.find((file) => file.id === id);
|
|
510
|
-
if (found?.url)
|
|
511
|
-
URL.revokeObjectURL(found.url);
|
|
512
|
-
}
|
|
173
|
+
if (found?.url) URL.revokeObjectURL(found.url);
|
|
513
174
|
return prev.filter((file) => file.id !== id);
|
|
514
175
|
}),
|
|
515
176
|
[]
|
|
@@ -521,40 +182,21 @@ export const PromptInput = ({
|
|
|
521
182
|
const incoming = [...fileList];
|
|
522
183
|
const accepted = incoming.filter((f) => matchesAccept(f));
|
|
523
184
|
if (incoming.length && accepted.length === 0) {
|
|
524
|
-
onError?.({
|
|
525
|
-
code: "accept",
|
|
526
|
-
message: "No files match the accepted types.",
|
|
527
|
-
});
|
|
185
|
+
onError?.({ code: "accept", message: "No files match the accepted types." });
|
|
528
186
|
return;
|
|
529
187
|
}
|
|
530
|
-
const withinSize = (f: File) =>
|
|
531
|
-
maxFileSize ? f.size <= maxFileSize : true;
|
|
188
|
+
const withinSize = (f: File) => maxFileSize ? f.size <= maxFileSize : true;
|
|
532
189
|
const sized = accepted.filter(withinSize);
|
|
533
190
|
if (accepted.length > 0 && sized.length === 0) {
|
|
534
|
-
onError?.({
|
|
535
|
-
code: "max_file_size",
|
|
536
|
-
message: "All files exceed the maximum size.",
|
|
537
|
-
});
|
|
191
|
+
onError?.({ code: "max_file_size", message: "All files exceed the maximum size." });
|
|
538
192
|
return;
|
|
539
193
|
}
|
|
540
|
-
|
|
541
|
-
const
|
|
542
|
-
const capacity =
|
|
543
|
-
typeof maxFiles === "number"
|
|
544
|
-
? Math.max(0, maxFiles - currentCount)
|
|
545
|
-
: undefined;
|
|
546
|
-
const capped =
|
|
547
|
-
typeof capacity === "number" ? sized.slice(0, capacity) : sized;
|
|
194
|
+
const capacity = typeof maxFiles === "number" ? Math.max(0, maxFiles - files.length) : undefined;
|
|
195
|
+
const capped = typeof capacity === "number" ? sized.slice(0, capacity) : sized;
|
|
548
196
|
if (typeof capacity === "number" && sized.length > capacity) {
|
|
549
|
-
onError?.({
|
|
550
|
-
code: "max_files",
|
|
551
|
-
message: "Too many files. Some were not added.",
|
|
552
|
-
});
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
if (capped.length > 0) {
|
|
556
|
-
controller?.attachments.add(capped);
|
|
197
|
+
onError?.({ code: "max_files", message: "Too many files. Some were not added." });
|
|
557
198
|
}
|
|
199
|
+
if (capped.length > 0) controller?.attachments.add(capped);
|
|
558
200
|
},
|
|
559
201
|
[matchesAccept, maxFileSize, maxFiles, onError, files.length, controller]
|
|
560
202
|
);
|
|
@@ -565,25 +207,18 @@ export const PromptInput = ({
|
|
|
565
207
|
? controller?.attachments.clear()
|
|
566
208
|
: setItems((prev) => {
|
|
567
209
|
for (const file of prev) {
|
|
568
|
-
if (file.url)
|
|
569
|
-
URL.revokeObjectURL(file.url);
|
|
570
|
-
}
|
|
210
|
+
if (file.url) URL.revokeObjectURL(file.url);
|
|
571
211
|
}
|
|
572
212
|
return [];
|
|
573
213
|
}),
|
|
574
214
|
[usingProvider, controller]
|
|
575
215
|
);
|
|
576
216
|
|
|
577
|
-
const clearReferencedSources = useCallback(
|
|
578
|
-
() => setReferencedSources([]),
|
|
579
|
-
[]
|
|
580
|
-
);
|
|
217
|
+
const clearReferencedSources = useCallback(() => setReferencedSources([]), []);
|
|
581
218
|
|
|
582
219
|
const add = usingProvider ? addWithProviderValidation : addLocal;
|
|
583
220
|
const remove = usingProvider ? controller.attachments.remove : removeLocal;
|
|
584
|
-
const openFileDialog = usingProvider
|
|
585
|
-
? controller.attachments.openFileDialog
|
|
586
|
-
: openFileDialogLocal;
|
|
221
|
+
const openFileDialog = usingProvider ? controller.attachments.openFileDialog : openFileDialogLocal;
|
|
587
222
|
|
|
588
223
|
const clear = useCallback(() => {
|
|
589
224
|
clearAttachments();
|
|
@@ -592,9 +227,7 @@ export const PromptInput = ({
|
|
|
592
227
|
|
|
593
228
|
// Let provider know about our hidden file input so external menus can call openFileDialog()
|
|
594
229
|
useEffect(() => {
|
|
595
|
-
if (!usingProvider)
|
|
596
|
-
return;
|
|
597
|
-
}
|
|
230
|
+
if (!usingProvider) return;
|
|
598
231
|
controller.__registerFileInput(inputRef, () => inputRef.current?.click());
|
|
599
232
|
}, [usingProvider, controller]);
|
|
600
233
|
|
|
@@ -606,29 +239,17 @@ export const PromptInput = ({
|
|
|
606
239
|
}
|
|
607
240
|
}, [files, syncHiddenInput]);
|
|
608
241
|
|
|
609
|
-
// Attach drop handlers on
|
|
242
|
+
// Attach drop handlers on form (local) or document (opt-in global)
|
|
610
243
|
useEffect(() => {
|
|
611
244
|
const form = formRef.current;
|
|
612
|
-
if (!form)
|
|
613
|
-
return;
|
|
614
|
-
}
|
|
615
|
-
if (globalDrop) {
|
|
616
|
-
// when global drop is on, let the document-level handler own drops
|
|
617
|
-
return;
|
|
618
|
-
}
|
|
245
|
+
if (!form || globalDrop) return;
|
|
619
246
|
|
|
620
247
|
const onDragOver = (e: DragEvent) => {
|
|
621
|
-
if (e.dataTransfer?.types?.includes("Files"))
|
|
622
|
-
e.preventDefault();
|
|
623
|
-
}
|
|
248
|
+
if (e.dataTransfer?.types?.includes("Files")) e.preventDefault();
|
|
624
249
|
};
|
|
625
250
|
const onDrop = (e: DragEvent) => {
|
|
626
|
-
if (e.dataTransfer?.types?.includes("Files"))
|
|
627
|
-
|
|
628
|
-
}
|
|
629
|
-
if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {
|
|
630
|
-
add(e.dataTransfer.files);
|
|
631
|
-
}
|
|
251
|
+
if (e.dataTransfer?.types?.includes("Files")) e.preventDefault();
|
|
252
|
+
if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) add(e.dataTransfer.files);
|
|
632
253
|
};
|
|
633
254
|
form.addEventListener("dragover", onDragOver);
|
|
634
255
|
form.addEventListener("drop", onDrop);
|
|
@@ -639,22 +260,13 @@ export const PromptInput = ({
|
|
|
639
260
|
}, [add, globalDrop]);
|
|
640
261
|
|
|
641
262
|
useEffect(() => {
|
|
642
|
-
if (!globalDrop)
|
|
643
|
-
return;
|
|
644
|
-
}
|
|
645
|
-
|
|
263
|
+
if (!globalDrop) return;
|
|
646
264
|
const onDragOver = (e: DragEvent) => {
|
|
647
|
-
if (e.dataTransfer?.types?.includes("Files"))
|
|
648
|
-
e.preventDefault();
|
|
649
|
-
}
|
|
265
|
+
if (e.dataTransfer?.types?.includes("Files")) e.preventDefault();
|
|
650
266
|
};
|
|
651
267
|
const onDrop = (e: DragEvent) => {
|
|
652
|
-
if (e.dataTransfer?.types?.includes("Files"))
|
|
653
|
-
|
|
654
|
-
}
|
|
655
|
-
if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {
|
|
656
|
-
add(e.dataTransfer.files);
|
|
657
|
-
}
|
|
268
|
+
if (e.dataTransfer?.types?.includes("Files")) e.preventDefault();
|
|
269
|
+
if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) add(e.dataTransfer.files);
|
|
658
270
|
};
|
|
659
271
|
document.addEventListener("dragover", onDragOver);
|
|
660
272
|
document.addEventListener("drop", onDrop);
|
|
@@ -664,13 +276,12 @@ export const PromptInput = ({
|
|
|
664
276
|
};
|
|
665
277
|
}, [add, globalDrop]);
|
|
666
278
|
|
|
279
|
+
// Cleanup blob URLs on unmount
|
|
667
280
|
useEffect(
|
|
668
281
|
() => () => {
|
|
669
282
|
if (!usingProvider) {
|
|
670
283
|
for (const f of filesRef.current) {
|
|
671
|
-
if (f.url)
|
|
672
|
-
URL.revokeObjectURL(f.url);
|
|
673
|
-
}
|
|
284
|
+
if (f.url) URL.revokeObjectURL(f.url);
|
|
674
285
|
}
|
|
675
286
|
}
|
|
676
287
|
},
|
|
@@ -678,11 +289,9 @@ export const PromptInput = ({
|
|
|
678
289
|
[usingProvider]
|
|
679
290
|
);
|
|
680
291
|
|
|
681
|
-
const handleChange
|
|
682
|
-
(event) => {
|
|
683
|
-
if (event.currentTarget.files)
|
|
684
|
-
add(event.currentTarget.files);
|
|
685
|
-
}
|
|
292
|
+
const handleChange = useCallback(
|
|
293
|
+
(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
294
|
+
if (event.currentTarget.files) add(event.currentTarget.files);
|
|
686
295
|
// Reset input value to allow selecting files that were previously removed
|
|
687
296
|
event.currentTarget.value = "";
|
|
688
297
|
},
|
|
@@ -733,9 +342,7 @@ export const PromptInput = ({
|
|
|
733
342
|
|
|
734
343
|
// Reset form immediately after capturing text to avoid race condition
|
|
735
344
|
// where user input during async blob conversion would be lost
|
|
736
|
-
if (!usingProvider)
|
|
737
|
-
form.reset();
|
|
738
|
-
}
|
|
345
|
+
if (!usingProvider) form.reset();
|
|
739
346
|
|
|
740
347
|
try {
|
|
741
348
|
// Convert blob URLs to data URLs asynchronously
|
|
@@ -743,11 +350,7 @@ export const PromptInput = ({
|
|
|
743
350
|
files.map(async ({ id: _id, ...item }) => {
|
|
744
351
|
if (item.url?.startsWith("blob:")) {
|
|
745
352
|
const dataUrl = await convertBlobUrlToDataUrl(item.url);
|
|
746
|
-
|
|
747
|
-
return {
|
|
748
|
-
...item,
|
|
749
|
-
url: dataUrl ?? item.url,
|
|
750
|
-
};
|
|
353
|
+
return { ...item, url: dataUrl ?? item.url };
|
|
751
354
|
}
|
|
752
355
|
return item;
|
|
753
356
|
})
|
|
@@ -755,23 +358,17 @@ export const PromptInput = ({
|
|
|
755
358
|
|
|
756
359
|
const result = onSubmit({ files: convertedFiles, text }, event);
|
|
757
360
|
|
|
758
|
-
// Handle both sync and async onSubmit
|
|
759
361
|
if (result instanceof Promise) {
|
|
760
362
|
try {
|
|
761
363
|
await result;
|
|
762
364
|
clear();
|
|
763
|
-
if (usingProvider)
|
|
764
|
-
controller.textInput.clear();
|
|
765
|
-
}
|
|
365
|
+
if (usingProvider) controller.textInput.clear();
|
|
766
366
|
} catch {
|
|
767
367
|
// Don't clear on error - user may want to retry
|
|
768
368
|
}
|
|
769
369
|
} else {
|
|
770
|
-
// Sync function completed without throwing, clear inputs
|
|
771
370
|
clear();
|
|
772
|
-
if (usingProvider)
|
|
773
|
-
controller.textInput.clear();
|
|
774
|
-
}
|
|
371
|
+
if (usingProvider) controller.textInput.clear();
|
|
775
372
|
}
|
|
776
373
|
} catch {
|
|
777
374
|
// Don't clear on error - user may want to retry
|
|
@@ -780,7 +377,6 @@ export const PromptInput = ({
|
|
|
780
377
|
[usingProvider, controller, files, onSubmit, clear]
|
|
781
378
|
);
|
|
782
379
|
|
|
783
|
-
// Render with or without local provider
|
|
784
380
|
const inner = (
|
|
785
381
|
<>
|
|
786
382
|
<input
|
|
@@ -804,538 +400,11 @@ export const PromptInput = ({
|
|
|
804
400
|
</>
|
|
805
401
|
);
|
|
806
402
|
|
|
807
|
-
const withReferencedSources = (
|
|
808
|
-
<LocalReferencedSourcesContext.Provider value={refsCtx}>
|
|
809
|
-
{inner}
|
|
810
|
-
</LocalReferencedSourcesContext.Provider>
|
|
811
|
-
);
|
|
812
|
-
|
|
813
|
-
// Always provide LocalAttachmentsContext so children get validated add function
|
|
814
403
|
return (
|
|
815
404
|
<LocalAttachmentsContext.Provider value={attachmentsCtx}>
|
|
816
|
-
{
|
|
405
|
+
<LocalReferencedSourcesContext.Provider value={refsCtx}>
|
|
406
|
+
{inner}
|
|
407
|
+
</LocalReferencedSourcesContext.Provider>
|
|
817
408
|
</LocalAttachmentsContext.Provider>
|
|
818
409
|
);
|
|
819
410
|
};
|
|
820
|
-
|
|
821
|
-
export type PromptInputBodyProps = HTMLAttributes<HTMLDivElement>;
|
|
822
|
-
|
|
823
|
-
export const PromptInputBody = ({
|
|
824
|
-
className,
|
|
825
|
-
...props
|
|
826
|
-
}: PromptInputBodyProps) => (
|
|
827
|
-
<div className={cn("contents", className)} {...props} />
|
|
828
|
-
);
|
|
829
|
-
|
|
830
|
-
export type PromptInputTextareaProps = ComponentProps<
|
|
831
|
-
typeof InputGroupTextarea
|
|
832
|
-
>;
|
|
833
|
-
|
|
834
|
-
export const PromptInputTextarea = ({
|
|
835
|
-
onChange,
|
|
836
|
-
onKeyDown,
|
|
837
|
-
className,
|
|
838
|
-
placeholder = "What would you like to know?",
|
|
839
|
-
...props
|
|
840
|
-
}: PromptInputTextareaProps) => {
|
|
841
|
-
const controller = useOptionalPromptInputController();
|
|
842
|
-
const attachments = usePromptInputAttachments();
|
|
843
|
-
const [isComposing, setIsComposing] = useState(false);
|
|
844
|
-
|
|
845
|
-
const handleKeyDown: KeyboardEventHandler<HTMLTextAreaElement> = useCallback(
|
|
846
|
-
(e) => {
|
|
847
|
-
// Call the external onKeyDown handler first
|
|
848
|
-
onKeyDown?.(e);
|
|
849
|
-
|
|
850
|
-
// If the external handler prevented default, don't run internal logic
|
|
851
|
-
if (e.defaultPrevented) {
|
|
852
|
-
return;
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
if (e.key === "Enter") {
|
|
856
|
-
if (isComposing || e.nativeEvent.isComposing) {
|
|
857
|
-
return;
|
|
858
|
-
}
|
|
859
|
-
if (e.shiftKey) {
|
|
860
|
-
return;
|
|
861
|
-
}
|
|
862
|
-
e.preventDefault();
|
|
863
|
-
|
|
864
|
-
// Check if the submit button is disabled before submitting
|
|
865
|
-
const { form } = e.currentTarget;
|
|
866
|
-
const submitButton = form?.querySelector(
|
|
867
|
-
'button[type="submit"]'
|
|
868
|
-
) as HTMLButtonElement | null;
|
|
869
|
-
if (submitButton?.disabled) {
|
|
870
|
-
return;
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
form?.requestSubmit();
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
// Remove last attachment when Backspace is pressed and textarea is empty
|
|
877
|
-
if (
|
|
878
|
-
e.key === "Backspace" &&
|
|
879
|
-
e.currentTarget.value === "" &&
|
|
880
|
-
attachments.files.length > 0
|
|
881
|
-
) {
|
|
882
|
-
e.preventDefault();
|
|
883
|
-
const lastAttachment = attachments.files.at(-1);
|
|
884
|
-
if (lastAttachment) {
|
|
885
|
-
attachments.remove(lastAttachment.id);
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
},
|
|
889
|
-
[onKeyDown, isComposing, attachments]
|
|
890
|
-
);
|
|
891
|
-
|
|
892
|
-
const handlePaste: ClipboardEventHandler<HTMLTextAreaElement> = useCallback(
|
|
893
|
-
(event) => {
|
|
894
|
-
const items = event.clipboardData?.items;
|
|
895
|
-
|
|
896
|
-
if (!items) {
|
|
897
|
-
return;
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
const files: File[] = [];
|
|
901
|
-
|
|
902
|
-
for (const item of items) {
|
|
903
|
-
if (item.kind === "file") {
|
|
904
|
-
const file = item.getAsFile();
|
|
905
|
-
if (file) {
|
|
906
|
-
files.push(file);
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
if (files.length > 0) {
|
|
912
|
-
event.preventDefault();
|
|
913
|
-
attachments.add(files);
|
|
914
|
-
}
|
|
915
|
-
},
|
|
916
|
-
[attachments]
|
|
917
|
-
);
|
|
918
|
-
|
|
919
|
-
const handleCompositionEnd = useCallback(() => setIsComposing(false), []);
|
|
920
|
-
const handleCompositionStart = useCallback(() => setIsComposing(true), []);
|
|
921
|
-
|
|
922
|
-
const controlledProps = controller
|
|
923
|
-
? {
|
|
924
|
-
onChange: (e: ChangeEvent<HTMLTextAreaElement>) => {
|
|
925
|
-
controller.textInput.setInput(e.currentTarget.value);
|
|
926
|
-
onChange?.(e);
|
|
927
|
-
},
|
|
928
|
-
value: controller.textInput.value,
|
|
929
|
-
}
|
|
930
|
-
: {
|
|
931
|
-
onChange,
|
|
932
|
-
};
|
|
933
|
-
|
|
934
|
-
return (
|
|
935
|
-
<InputGroupTextarea
|
|
936
|
-
className={cn("field-sizing-content max-h-48 min-h-16", className)}
|
|
937
|
-
name="message"
|
|
938
|
-
onCompositionEnd={handleCompositionEnd}
|
|
939
|
-
onCompositionStart={handleCompositionStart}
|
|
940
|
-
onKeyDown={handleKeyDown}
|
|
941
|
-
onPaste={handlePaste}
|
|
942
|
-
placeholder={placeholder}
|
|
943
|
-
{...props}
|
|
944
|
-
{...controlledProps}
|
|
945
|
-
/>
|
|
946
|
-
);
|
|
947
|
-
};
|
|
948
|
-
|
|
949
|
-
export type PromptInputHeaderProps = Omit<
|
|
950
|
-
ComponentProps<typeof InputGroupAddon>,
|
|
951
|
-
"align"
|
|
952
|
-
>;
|
|
953
|
-
|
|
954
|
-
export const PromptInputHeader = ({
|
|
955
|
-
className,
|
|
956
|
-
...props
|
|
957
|
-
}: PromptInputHeaderProps) => (
|
|
958
|
-
<InputGroupAddon
|
|
959
|
-
align="block-end"
|
|
960
|
-
className={cn("order-first flex-wrap gap-1", className)}
|
|
961
|
-
{...props}
|
|
962
|
-
/>
|
|
963
|
-
);
|
|
964
|
-
|
|
965
|
-
export type PromptInputFooterProps = Omit<
|
|
966
|
-
ComponentProps<typeof InputGroupAddon>,
|
|
967
|
-
"align"
|
|
968
|
-
>;
|
|
969
|
-
|
|
970
|
-
export const PromptInputFooter = ({
|
|
971
|
-
className,
|
|
972
|
-
...props
|
|
973
|
-
}: PromptInputFooterProps) => (
|
|
974
|
-
<InputGroupAddon
|
|
975
|
-
align="block-end"
|
|
976
|
-
className={cn("justify-between gap-1", className)}
|
|
977
|
-
{...props}
|
|
978
|
-
/>
|
|
979
|
-
);
|
|
980
|
-
|
|
981
|
-
export type PromptInputToolsProps = HTMLAttributes<HTMLDivElement>;
|
|
982
|
-
|
|
983
|
-
export const PromptInputTools = ({
|
|
984
|
-
className,
|
|
985
|
-
...props
|
|
986
|
-
}: PromptInputToolsProps) => (
|
|
987
|
-
<div
|
|
988
|
-
className={cn("flex min-w-0 items-center gap-1", className)}
|
|
989
|
-
{...props}
|
|
990
|
-
/>
|
|
991
|
-
);
|
|
992
|
-
|
|
993
|
-
export type PromptInputButtonTooltip =
|
|
994
|
-
| string
|
|
995
|
-
| {
|
|
996
|
-
content: ReactNode;
|
|
997
|
-
shortcut?: string;
|
|
998
|
-
side?: ComponentProps<typeof TooltipContent>["side"];
|
|
999
|
-
};
|
|
1000
|
-
|
|
1001
|
-
export type PromptInputButtonProps = ComponentProps<typeof InputGroupButton> & {
|
|
1002
|
-
tooltip?: PromptInputButtonTooltip;
|
|
1003
|
-
};
|
|
1004
|
-
|
|
1005
|
-
export const PromptInputButton = ({
|
|
1006
|
-
variant = "ghost",
|
|
1007
|
-
className,
|
|
1008
|
-
size,
|
|
1009
|
-
tooltip,
|
|
1010
|
-
...props
|
|
1011
|
-
}: PromptInputButtonProps) => {
|
|
1012
|
-
const newSize =
|
|
1013
|
-
size ?? (Children.count(props.children) > 1 ? "sm" : "icon-sm");
|
|
1014
|
-
|
|
1015
|
-
const button = (
|
|
1016
|
-
<InputGroupButton
|
|
1017
|
-
className={cn(className)}
|
|
1018
|
-
size={newSize}
|
|
1019
|
-
type="button"
|
|
1020
|
-
variant={variant}
|
|
1021
|
-
{...props}
|
|
1022
|
-
/>
|
|
1023
|
-
);
|
|
1024
|
-
|
|
1025
|
-
if (!tooltip) {
|
|
1026
|
-
return button;
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
const tooltipContent =
|
|
1030
|
-
typeof tooltip === "string" ? tooltip : tooltip.content;
|
|
1031
|
-
const shortcut = typeof tooltip === "string" ? undefined : tooltip.shortcut;
|
|
1032
|
-
const side = typeof tooltip === "string" ? "top" : (tooltip.side ?? "top");
|
|
1033
|
-
|
|
1034
|
-
return (
|
|
1035
|
-
<Tooltip>
|
|
1036
|
-
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
|
1037
|
-
<TooltipContent side={side}>
|
|
1038
|
-
{tooltipContent}
|
|
1039
|
-
{shortcut && (
|
|
1040
|
-
<span className="ml-2 text-muted-foreground">{shortcut}</span>
|
|
1041
|
-
)}
|
|
1042
|
-
</TooltipContent>
|
|
1043
|
-
</Tooltip>
|
|
1044
|
-
);
|
|
1045
|
-
};
|
|
1046
|
-
|
|
1047
|
-
export type PromptInputActionMenuProps = ComponentProps<typeof DropdownMenu>;
|
|
1048
|
-
export const PromptInputActionMenu = (props: PromptInputActionMenuProps) => (
|
|
1049
|
-
<DropdownMenu {...props} />
|
|
1050
|
-
);
|
|
1051
|
-
|
|
1052
|
-
export type PromptInputActionMenuTriggerProps = PromptInputButtonProps;
|
|
1053
|
-
|
|
1054
|
-
export const PromptInputActionMenuTrigger = ({
|
|
1055
|
-
className,
|
|
1056
|
-
children,
|
|
1057
|
-
...props
|
|
1058
|
-
}: PromptInputActionMenuTriggerProps) => (
|
|
1059
|
-
<DropdownMenuTrigger asChild>
|
|
1060
|
-
<PromptInputButton className={className} {...props}>
|
|
1061
|
-
{children ?? <PlusIcon className="size-4" />}
|
|
1062
|
-
</PromptInputButton>
|
|
1063
|
-
</DropdownMenuTrigger>
|
|
1064
|
-
);
|
|
1065
|
-
|
|
1066
|
-
export type PromptInputActionMenuContentProps = ComponentProps<
|
|
1067
|
-
typeof DropdownMenuContent
|
|
1068
|
-
>;
|
|
1069
|
-
export const PromptInputActionMenuContent = ({
|
|
1070
|
-
className,
|
|
1071
|
-
...props
|
|
1072
|
-
}: PromptInputActionMenuContentProps) => (
|
|
1073
|
-
<DropdownMenuContent align="start" className={cn(className)} {...props} />
|
|
1074
|
-
);
|
|
1075
|
-
|
|
1076
|
-
export type PromptInputActionMenuItemProps = ComponentProps<
|
|
1077
|
-
typeof DropdownMenuItem
|
|
1078
|
-
>;
|
|
1079
|
-
export const PromptInputActionMenuItem = ({
|
|
1080
|
-
className,
|
|
1081
|
-
...props
|
|
1082
|
-
}: PromptInputActionMenuItemProps) => (
|
|
1083
|
-
<DropdownMenuItem className={cn(className)} {...props} />
|
|
1084
|
-
);
|
|
1085
|
-
|
|
1086
|
-
// Note: Actions that perform side-effects (like opening a file dialog)
|
|
1087
|
-
// are provided in opt-in modules (e.g., prompt-input-attachments).
|
|
1088
|
-
|
|
1089
|
-
export type PromptInputSubmitProps = ComponentProps<typeof InputGroupButton> & {
|
|
1090
|
-
status?: ChatStatus;
|
|
1091
|
-
onStop?: () => void;
|
|
1092
|
-
};
|
|
1093
|
-
|
|
1094
|
-
export const PromptInputSubmit = ({
|
|
1095
|
-
className,
|
|
1096
|
-
variant = "default",
|
|
1097
|
-
size = "icon-sm",
|
|
1098
|
-
status,
|
|
1099
|
-
onStop,
|
|
1100
|
-
onClick,
|
|
1101
|
-
children,
|
|
1102
|
-
...props
|
|
1103
|
-
}: PromptInputSubmitProps) => {
|
|
1104
|
-
const isGenerating = status === "submitted" || status === "streaming";
|
|
1105
|
-
|
|
1106
|
-
let Icon = <CornerDownLeftIcon className="size-4" />;
|
|
1107
|
-
|
|
1108
|
-
if (status === "submitted") {
|
|
1109
|
-
Icon = <Spinner />;
|
|
1110
|
-
} else if (status === "streaming") {
|
|
1111
|
-
Icon = <SquareIcon className="size-4" />;
|
|
1112
|
-
} else if (status === "error") {
|
|
1113
|
-
Icon = <XIcon className="size-4" />;
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
const handleClick = useCallback(
|
|
1117
|
-
(e: React.MouseEvent<HTMLButtonElement>) => {
|
|
1118
|
-
if (isGenerating && onStop) {
|
|
1119
|
-
e.preventDefault();
|
|
1120
|
-
onStop();
|
|
1121
|
-
return;
|
|
1122
|
-
}
|
|
1123
|
-
onClick?.(e);
|
|
1124
|
-
},
|
|
1125
|
-
[isGenerating, onStop, onClick]
|
|
1126
|
-
);
|
|
1127
|
-
|
|
1128
|
-
return (
|
|
1129
|
-
<InputGroupButton
|
|
1130
|
-
aria-label={isGenerating ? "Stop" : "Submit"}
|
|
1131
|
-
className={cn(className)}
|
|
1132
|
-
onClick={handleClick}
|
|
1133
|
-
size={size}
|
|
1134
|
-
type={isGenerating && onStop ? "button" : "submit"}
|
|
1135
|
-
variant={variant}
|
|
1136
|
-
{...props}
|
|
1137
|
-
>
|
|
1138
|
-
{children ?? Icon}
|
|
1139
|
-
</InputGroupButton>
|
|
1140
|
-
);
|
|
1141
|
-
};
|
|
1142
|
-
|
|
1143
|
-
export type PromptInputSelectProps = ComponentProps<typeof Select>;
|
|
1144
|
-
|
|
1145
|
-
export const PromptInputSelect = (props: PromptInputSelectProps) => (
|
|
1146
|
-
<Select {...props} />
|
|
1147
|
-
);
|
|
1148
|
-
|
|
1149
|
-
export type PromptInputSelectTriggerProps = ComponentProps<
|
|
1150
|
-
typeof SelectTrigger
|
|
1151
|
-
>;
|
|
1152
|
-
|
|
1153
|
-
export const PromptInputSelectTrigger = ({
|
|
1154
|
-
className,
|
|
1155
|
-
...props
|
|
1156
|
-
}: PromptInputSelectTriggerProps) => (
|
|
1157
|
-
<SelectTrigger
|
|
1158
|
-
className={cn(
|
|
1159
|
-
"border-none bg-transparent font-medium text-muted-foreground shadow-none transition-colors",
|
|
1160
|
-
"hover:bg-accent hover:text-foreground aria-expanded:bg-accent aria-expanded:text-foreground",
|
|
1161
|
-
className
|
|
1162
|
-
)}
|
|
1163
|
-
{...props}
|
|
1164
|
-
/>
|
|
1165
|
-
);
|
|
1166
|
-
|
|
1167
|
-
export type PromptInputSelectContentProps = ComponentProps<
|
|
1168
|
-
typeof SelectContent
|
|
1169
|
-
>;
|
|
1170
|
-
|
|
1171
|
-
export const PromptInputSelectContent = ({
|
|
1172
|
-
className,
|
|
1173
|
-
...props
|
|
1174
|
-
}: PromptInputSelectContentProps) => (
|
|
1175
|
-
<SelectContent className={cn(className)} {...props} />
|
|
1176
|
-
);
|
|
1177
|
-
|
|
1178
|
-
export type PromptInputSelectItemProps = ComponentProps<typeof SelectItem>;
|
|
1179
|
-
|
|
1180
|
-
export const PromptInputSelectItem = ({
|
|
1181
|
-
className,
|
|
1182
|
-
...props
|
|
1183
|
-
}: PromptInputSelectItemProps) => (
|
|
1184
|
-
<SelectItem className={cn(className)} {...props} />
|
|
1185
|
-
);
|
|
1186
|
-
|
|
1187
|
-
export type PromptInputSelectValueProps = ComponentProps<typeof SelectValue>;
|
|
1188
|
-
|
|
1189
|
-
export const PromptInputSelectValue = ({
|
|
1190
|
-
className,
|
|
1191
|
-
...props
|
|
1192
|
-
}: PromptInputSelectValueProps) => (
|
|
1193
|
-
<SelectValue className={cn(className)} {...props} />
|
|
1194
|
-
);
|
|
1195
|
-
|
|
1196
|
-
export type PromptInputHoverCardProps = ComponentProps<typeof HoverCard>;
|
|
1197
|
-
|
|
1198
|
-
export const PromptInputHoverCard = ({
|
|
1199
|
-
openDelay = 0,
|
|
1200
|
-
closeDelay = 0,
|
|
1201
|
-
...props
|
|
1202
|
-
}: PromptInputHoverCardProps) => (
|
|
1203
|
-
<HoverCard closeDelay={closeDelay} openDelay={openDelay} {...props} />
|
|
1204
|
-
);
|
|
1205
|
-
|
|
1206
|
-
export type PromptInputHoverCardTriggerProps = ComponentProps<
|
|
1207
|
-
typeof HoverCardTrigger
|
|
1208
|
-
>;
|
|
1209
|
-
|
|
1210
|
-
export const PromptInputHoverCardTrigger = (
|
|
1211
|
-
props: PromptInputHoverCardTriggerProps
|
|
1212
|
-
) => <HoverCardTrigger {...props} />;
|
|
1213
|
-
|
|
1214
|
-
export type PromptInputHoverCardContentProps = ComponentProps<
|
|
1215
|
-
typeof HoverCardContent
|
|
1216
|
-
>;
|
|
1217
|
-
|
|
1218
|
-
export const PromptInputHoverCardContent = ({
|
|
1219
|
-
align = "start",
|
|
1220
|
-
...props
|
|
1221
|
-
}: PromptInputHoverCardContentProps) => (
|
|
1222
|
-
<HoverCardContent align={align} {...props} />
|
|
1223
|
-
);
|
|
1224
|
-
|
|
1225
|
-
export type PromptInputTabsListProps = HTMLAttributes<HTMLDivElement>;
|
|
1226
|
-
|
|
1227
|
-
export const PromptInputTabsList = ({
|
|
1228
|
-
className,
|
|
1229
|
-
...props
|
|
1230
|
-
}: PromptInputTabsListProps) => <div className={cn(className)} {...props} />;
|
|
1231
|
-
|
|
1232
|
-
export type PromptInputTabProps = HTMLAttributes<HTMLDivElement>;
|
|
1233
|
-
|
|
1234
|
-
export const PromptInputTab = ({
|
|
1235
|
-
className,
|
|
1236
|
-
...props
|
|
1237
|
-
}: PromptInputTabProps) => <div className={cn(className)} {...props} />;
|
|
1238
|
-
|
|
1239
|
-
export type PromptInputTabLabelProps = HTMLAttributes<HTMLHeadingElement>;
|
|
1240
|
-
|
|
1241
|
-
export const PromptInputTabLabel = ({
|
|
1242
|
-
className,
|
|
1243
|
-
...props
|
|
1244
|
-
}: PromptInputTabLabelProps) => (
|
|
1245
|
-
// Content provided via children in props
|
|
1246
|
-
// oxlint-disable-next-line eslint-plugin-jsx-a11y(heading-has-content)
|
|
1247
|
-
<h3
|
|
1248
|
-
className={cn(
|
|
1249
|
-
"mb-2 px-3 font-medium text-muted-foreground text-xs",
|
|
1250
|
-
className
|
|
1251
|
-
)}
|
|
1252
|
-
{...props}
|
|
1253
|
-
/>
|
|
1254
|
-
);
|
|
1255
|
-
|
|
1256
|
-
export type PromptInputTabBodyProps = HTMLAttributes<HTMLDivElement>;
|
|
1257
|
-
|
|
1258
|
-
export const PromptInputTabBody = ({
|
|
1259
|
-
className,
|
|
1260
|
-
...props
|
|
1261
|
-
}: PromptInputTabBodyProps) => (
|
|
1262
|
-
<div className={cn("space-y-1", className)} {...props} />
|
|
1263
|
-
);
|
|
1264
|
-
|
|
1265
|
-
export type PromptInputTabItemProps = HTMLAttributes<HTMLDivElement>;
|
|
1266
|
-
|
|
1267
|
-
export const PromptInputTabItem = ({
|
|
1268
|
-
className,
|
|
1269
|
-
...props
|
|
1270
|
-
}: PromptInputTabItemProps) => (
|
|
1271
|
-
<div
|
|
1272
|
-
className={cn(
|
|
1273
|
-
"flex items-center gap-2 px-3 py-2 text-xs hover:bg-accent",
|
|
1274
|
-
className
|
|
1275
|
-
)}
|
|
1276
|
-
{...props}
|
|
1277
|
-
/>
|
|
1278
|
-
);
|
|
1279
|
-
|
|
1280
|
-
export type PromptInputCommandProps = ComponentProps<typeof Command>;
|
|
1281
|
-
|
|
1282
|
-
export const PromptInputCommand = ({
|
|
1283
|
-
className,
|
|
1284
|
-
...props
|
|
1285
|
-
}: PromptInputCommandProps) => <Command className={cn(className)} {...props} />;
|
|
1286
|
-
|
|
1287
|
-
export type PromptInputCommandInputProps = ComponentProps<typeof CommandInput>;
|
|
1288
|
-
|
|
1289
|
-
export const PromptInputCommandInput = ({
|
|
1290
|
-
className,
|
|
1291
|
-
...props
|
|
1292
|
-
}: PromptInputCommandInputProps) => (
|
|
1293
|
-
<CommandInput className={cn(className)} {...props} />
|
|
1294
|
-
);
|
|
1295
|
-
|
|
1296
|
-
export type PromptInputCommandListProps = ComponentProps<typeof CommandList>;
|
|
1297
|
-
|
|
1298
|
-
export const PromptInputCommandList = ({
|
|
1299
|
-
className,
|
|
1300
|
-
...props
|
|
1301
|
-
}: PromptInputCommandListProps) => (
|
|
1302
|
-
<CommandList className={cn(className)} {...props} />
|
|
1303
|
-
);
|
|
1304
|
-
|
|
1305
|
-
export type PromptInputCommandEmptyProps = ComponentProps<typeof CommandEmpty>;
|
|
1306
|
-
|
|
1307
|
-
export const PromptInputCommandEmpty = ({
|
|
1308
|
-
className,
|
|
1309
|
-
...props
|
|
1310
|
-
}: PromptInputCommandEmptyProps) => (
|
|
1311
|
-
<CommandEmpty className={cn(className)} {...props} />
|
|
1312
|
-
);
|
|
1313
|
-
|
|
1314
|
-
export type PromptInputCommandGroupProps = ComponentProps<typeof CommandGroup>;
|
|
1315
|
-
|
|
1316
|
-
export const PromptInputCommandGroup = ({
|
|
1317
|
-
className,
|
|
1318
|
-
...props
|
|
1319
|
-
}: PromptInputCommandGroupProps) => (
|
|
1320
|
-
<CommandGroup className={cn(className)} {...props} />
|
|
1321
|
-
);
|
|
1322
|
-
|
|
1323
|
-
export type PromptInputCommandItemProps = ComponentProps<typeof CommandItem>;
|
|
1324
|
-
|
|
1325
|
-
export const PromptInputCommandItem = ({
|
|
1326
|
-
className,
|
|
1327
|
-
...props
|
|
1328
|
-
}: PromptInputCommandItemProps) => (
|
|
1329
|
-
<CommandItem className={cn(className)} {...props} />
|
|
1330
|
-
);
|
|
1331
|
-
|
|
1332
|
-
export type PromptInputCommandSeparatorProps = ComponentProps<
|
|
1333
|
-
typeof CommandSeparator
|
|
1334
|
-
>;
|
|
1335
|
-
|
|
1336
|
-
export const PromptInputCommandSeparator = ({
|
|
1337
|
-
className,
|
|
1338
|
-
...props
|
|
1339
|
-
}: PromptInputCommandSeparatorProps) => (
|
|
1340
|
-
<CommandSeparator className={cn(className)} {...props} />
|
|
1341
|
-
);
|