@sero-ai/ui 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-typecheck.log +1 -1
- package/package.json +6 -5
- 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/voice-selector-accent.tsx +50 -0
- package/src/components/ai-elements/voice-selector.tsx +6 -175
- 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/command.tsx +1 -1
- package/src/components/ui/dialog.tsx +1 -1
- package/src/components/ui/plugin-safety-disclaimer.tsx +25 -0
- package/src/components/ui/search-input.tsx +40 -0
- package/src/index.ts +125 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sero-ai/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Shared UI components, AI elements, and design tokens for the Sero platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -24,11 +24,12 @@
|
|
|
24
24
|
"@radix-ui/react-use-controllable-state": "^1.2.2",
|
|
25
25
|
"@remixicon/react": "^4.9.0",
|
|
26
26
|
"@rive-app/react-webgl2": "^4.27.1",
|
|
27
|
+
"@sero-ai/common": "workspace:*",
|
|
27
28
|
"@streamdown/cjk": "^1.0.2",
|
|
28
29
|
"@streamdown/code": "^1.1.0",
|
|
29
30
|
"@streamdown/math": "^1.0.2",
|
|
30
31
|
"@streamdown/mermaid": "^1.0.2",
|
|
31
|
-
"@xyflow/react": "^12.10.
|
|
32
|
+
"@xyflow/react": "^12.10.2",
|
|
32
33
|
"ai": "^6.0.116",
|
|
33
34
|
"ansi-to-react": "^6.2.6",
|
|
34
35
|
"class-variance-authority": "^0.7.1",
|
|
@@ -40,7 +41,7 @@
|
|
|
40
41
|
"lucide-react": "catalog:",
|
|
41
42
|
"media-chrome": "^4.18.0",
|
|
42
43
|
"motion": "catalog:",
|
|
43
|
-
"nanoid": "^5.1.
|
|
44
|
+
"nanoid": "^5.1.11",
|
|
44
45
|
"next-themes": "^0.4.6",
|
|
45
46
|
"radix-ui": "^1.4.3",
|
|
46
47
|
"react-day-picker": "^9.14.0",
|
|
@@ -51,10 +52,10 @@
|
|
|
51
52
|
"recharts": "2.15.4",
|
|
52
53
|
"shiki": "^3.23.0",
|
|
53
54
|
"sonner": "^2.0.7",
|
|
54
|
-
"streamdown": "
|
|
55
|
+
"streamdown": "catalog:",
|
|
55
56
|
"tailwind-merge": "^3.5.0",
|
|
56
57
|
"tokenlens": "^1.3.1",
|
|
57
|
-
"use-stick-to-bottom": "^1.1.
|
|
58
|
+
"use-stick-to-bottom": "^1.1.4",
|
|
58
59
|
"vaul": "^1.1.2"
|
|
59
60
|
},
|
|
60
61
|
"devDependencies": {
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
ComponentProps,
|
|
5
|
+
PropsWithChildren,
|
|
6
|
+
RefObject,
|
|
7
|
+
} from "react";
|
|
8
|
+
import type { FileUIPart, SourceDocumentUIPart } from "ai";
|
|
9
|
+
import {
|
|
10
|
+
DropdownMenuItem,
|
|
11
|
+
} from "../ui/dropdown-menu";
|
|
12
|
+
import { ImageIcon } from "lucide-react";
|
|
13
|
+
import { nanoid } from "nanoid";
|
|
14
|
+
import {
|
|
15
|
+
createContext,
|
|
16
|
+
useCallback,
|
|
17
|
+
useContext,
|
|
18
|
+
useEffect,
|
|
19
|
+
useMemo,
|
|
20
|
+
useRef,
|
|
21
|
+
useState,
|
|
22
|
+
} from "react";
|
|
23
|
+
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// Types
|
|
26
|
+
// ============================================================================
|
|
27
|
+
|
|
28
|
+
export interface AttachmentsContext {
|
|
29
|
+
files: (FileUIPart & { id: string })[];
|
|
30
|
+
add: (files: File[] | FileList) => void;
|
|
31
|
+
remove: (id: string) => void;
|
|
32
|
+
clear: () => void;
|
|
33
|
+
openFileDialog: () => void;
|
|
34
|
+
fileInputRef: RefObject<HTMLInputElement | null>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface TextInputContext {
|
|
38
|
+
value: string;
|
|
39
|
+
setInput: (v: string) => void;
|
|
40
|
+
clear: () => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface PromptInputControllerProps {
|
|
44
|
+
textInput: TextInputContext;
|
|
45
|
+
attachments: AttachmentsContext;
|
|
46
|
+
/** INTERNAL: Allows PromptInput to register its file textInput + "open" callback */
|
|
47
|
+
__registerFileInput: (
|
|
48
|
+
ref: RefObject<HTMLInputElement | null>,
|
|
49
|
+
open: () => void
|
|
50
|
+
) => void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ReferencedSourcesContext {
|
|
54
|
+
sources: (SourceDocumentUIPart & { id: string })[];
|
|
55
|
+
add: (sources: SourceDocumentUIPart[] | SourceDocumentUIPart) => void;
|
|
56
|
+
remove: (id: string) => void;
|
|
57
|
+
clear: () => void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ============================================================================
|
|
61
|
+
// Contexts
|
|
62
|
+
// ============================================================================
|
|
63
|
+
|
|
64
|
+
const PromptInputController = createContext<PromptInputControllerProps | null>(null);
|
|
65
|
+
const ProviderAttachmentsContext = createContext<AttachmentsContext | null>(null);
|
|
66
|
+
export const LocalAttachmentsContext = createContext<AttachmentsContext | null>(null);
|
|
67
|
+
export const LocalReferencedSourcesContext = createContext<ReferencedSourcesContext | null>(null);
|
|
68
|
+
|
|
69
|
+
// ============================================================================
|
|
70
|
+
// Hooks
|
|
71
|
+
// ============================================================================
|
|
72
|
+
|
|
73
|
+
export const usePromptInputController = () => {
|
|
74
|
+
const ctx = useContext(PromptInputController);
|
|
75
|
+
if (!ctx) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
"Wrap your component inside <PromptInputProvider> to use usePromptInputController()."
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
return ctx;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/** Optional variant — does NOT throw. Useful for dual-mode components. */
|
|
84
|
+
export const useOptionalPromptInputController = () =>
|
|
85
|
+
useContext(PromptInputController);
|
|
86
|
+
|
|
87
|
+
export const useProviderAttachments = () => {
|
|
88
|
+
const ctx = useContext(ProviderAttachmentsContext);
|
|
89
|
+
if (!ctx) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
"Wrap your component inside <PromptInputProvider> to use useProviderAttachments()."
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
return ctx;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const useOptionalProviderAttachments = () =>
|
|
98
|
+
useContext(ProviderAttachmentsContext);
|
|
99
|
+
|
|
100
|
+
export const usePromptInputAttachments = () => {
|
|
101
|
+
// Prefer local context (inside PromptInput) as it has validation, fall back to provider
|
|
102
|
+
const provider = useOptionalProviderAttachments();
|
|
103
|
+
const local = useContext(LocalAttachmentsContext);
|
|
104
|
+
const context = local ?? provider;
|
|
105
|
+
if (!context) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
"usePromptInputAttachments must be used within a PromptInput or PromptInputProvider"
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
return context;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export const usePromptInputReferencedSources = () => {
|
|
114
|
+
const ctx = useContext(LocalReferencedSourcesContext);
|
|
115
|
+
if (!ctx) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
"usePromptInputReferencedSources must be used within a LocalReferencedSourcesContext.Provider"
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
return ctx;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// ============================================================================
|
|
124
|
+
// PromptInputProvider
|
|
125
|
+
// ============================================================================
|
|
126
|
+
|
|
127
|
+
export type PromptInputProviderProps = PropsWithChildren<{
|
|
128
|
+
initialInput?: string;
|
|
129
|
+
}>;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Optional global provider that lifts PromptInput state outside of PromptInput.
|
|
133
|
+
* If you don't use it, PromptInput stays fully self-managed.
|
|
134
|
+
*/
|
|
135
|
+
export const PromptInputProvider = ({
|
|
136
|
+
initialInput: initialTextInput = "",
|
|
137
|
+
children,
|
|
138
|
+
}: PromptInputProviderProps) => {
|
|
139
|
+
const [textInput, setTextInput] = useState(initialTextInput);
|
|
140
|
+
const clearInput = useCallback(() => setTextInput(""), []);
|
|
141
|
+
|
|
142
|
+
const [attachmentFiles, setAttachmentFiles] = useState<
|
|
143
|
+
(FileUIPart & { id: string })[]
|
|
144
|
+
>([]);
|
|
145
|
+
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
|
146
|
+
// oxlint-disable-next-line eslint(no-empty-function)
|
|
147
|
+
const openRef = useRef<() => void>(() => {});
|
|
148
|
+
|
|
149
|
+
const add = useCallback((files: File[] | FileList) => {
|
|
150
|
+
const incoming = [...files];
|
|
151
|
+
if (incoming.length === 0) return;
|
|
152
|
+
setAttachmentFiles((prev) => [
|
|
153
|
+
...prev,
|
|
154
|
+
...incoming.map((file) => ({
|
|
155
|
+
filename: file.name,
|
|
156
|
+
id: nanoid(),
|
|
157
|
+
mediaType: file.type,
|
|
158
|
+
type: "file" as const,
|
|
159
|
+
url: URL.createObjectURL(file),
|
|
160
|
+
})),
|
|
161
|
+
]);
|
|
162
|
+
}, []);
|
|
163
|
+
|
|
164
|
+
const remove = useCallback((id: string) => {
|
|
165
|
+
setAttachmentFiles((prev) => {
|
|
166
|
+
const found = prev.find((f) => f.id === id);
|
|
167
|
+
if (found?.url) URL.revokeObjectURL(found.url);
|
|
168
|
+
return prev.filter((f) => f.id !== id);
|
|
169
|
+
});
|
|
170
|
+
}, []);
|
|
171
|
+
|
|
172
|
+
const clear = useCallback(() => {
|
|
173
|
+
setAttachmentFiles((prev) => {
|
|
174
|
+
for (const f of prev) {
|
|
175
|
+
if (f.url) URL.revokeObjectURL(f.url);
|
|
176
|
+
}
|
|
177
|
+
return [];
|
|
178
|
+
});
|
|
179
|
+
}, []);
|
|
180
|
+
|
|
181
|
+
// Keep ref current for cleanup on unmount (avoids stale closure)
|
|
182
|
+
const attachmentsRef = useRef(attachmentFiles);
|
|
183
|
+
useEffect(() => { attachmentsRef.current = attachmentFiles; }, [attachmentFiles]);
|
|
184
|
+
|
|
185
|
+
// Cleanup blob URLs on unmount
|
|
186
|
+
useEffect(() => () => {
|
|
187
|
+
for (const f of attachmentsRef.current) {
|
|
188
|
+
if (f.url) URL.revokeObjectURL(f.url);
|
|
189
|
+
}
|
|
190
|
+
}, []);
|
|
191
|
+
|
|
192
|
+
const openFileDialog = useCallback(() => { openRef.current?.(); }, []);
|
|
193
|
+
|
|
194
|
+
const attachments = useMemo<AttachmentsContext>(
|
|
195
|
+
() => ({ add, clear, fileInputRef, files: attachmentFiles, openFileDialog, remove }),
|
|
196
|
+
[attachmentFiles, add, remove, clear, openFileDialog]
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
const __registerFileInput = useCallback(
|
|
200
|
+
(ref: RefObject<HTMLInputElement | null>, open: () => void) => {
|
|
201
|
+
fileInputRef.current = ref.current;
|
|
202
|
+
openRef.current = open;
|
|
203
|
+
},
|
|
204
|
+
[]
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const controller = useMemo<PromptInputControllerProps>(
|
|
208
|
+
() => ({
|
|
209
|
+
__registerFileInput,
|
|
210
|
+
attachments,
|
|
211
|
+
textInput: { clear: clearInput, setInput: setTextInput, value: textInput },
|
|
212
|
+
}),
|
|
213
|
+
[textInput, clearInput, attachments, __registerFileInput]
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
return (
|
|
217
|
+
<PromptInputController.Provider value={controller}>
|
|
218
|
+
<ProviderAttachmentsContext.Provider value={attachments}>
|
|
219
|
+
{children}
|
|
220
|
+
</ProviderAttachmentsContext.Provider>
|
|
221
|
+
</PromptInputController.Provider>
|
|
222
|
+
);
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// ============================================================================
|
|
226
|
+
// PromptInputActionAddAttachments
|
|
227
|
+
// ============================================================================
|
|
228
|
+
|
|
229
|
+
export type PromptInputActionAddAttachmentsProps = ComponentProps<
|
|
230
|
+
typeof DropdownMenuItem
|
|
231
|
+
> & { label?: string };
|
|
232
|
+
|
|
233
|
+
export const PromptInputActionAddAttachments = ({
|
|
234
|
+
label = "Add photos or files",
|
|
235
|
+
...props
|
|
236
|
+
}: PromptInputActionAddAttachmentsProps) => {
|
|
237
|
+
const attachments = usePromptInputAttachments();
|
|
238
|
+
|
|
239
|
+
const handleSelect = useCallback(
|
|
240
|
+
(e: Event) => {
|
|
241
|
+
e.preventDefault();
|
|
242
|
+
attachments.openFileDialog();
|
|
243
|
+
},
|
|
244
|
+
[attachments]
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
return (
|
|
248
|
+
<DropdownMenuItem {...props} onSelect={handleSelect}>
|
|
249
|
+
<ImageIcon className="mr-2 size-4" /> {label}
|
|
250
|
+
</DropdownMenuItem>
|
|
251
|
+
);
|
|
252
|
+
};
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
ComponentProps,
|
|
5
|
+
HTMLAttributes,
|
|
6
|
+
ReactNode,
|
|
7
|
+
} from "react";
|
|
8
|
+
import {
|
|
9
|
+
Command,
|
|
10
|
+
CommandEmpty,
|
|
11
|
+
CommandGroup,
|
|
12
|
+
CommandInput,
|
|
13
|
+
CommandItem,
|
|
14
|
+
CommandList,
|
|
15
|
+
CommandSeparator,
|
|
16
|
+
} from "../ui/command";
|
|
17
|
+
import {
|
|
18
|
+
DropdownMenu,
|
|
19
|
+
DropdownMenuContent,
|
|
20
|
+
DropdownMenuItem,
|
|
21
|
+
DropdownMenuTrigger,
|
|
22
|
+
} from "../ui/dropdown-menu";
|
|
23
|
+
import {
|
|
24
|
+
HoverCard,
|
|
25
|
+
HoverCardContent,
|
|
26
|
+
HoverCardTrigger,
|
|
27
|
+
} from "../ui/hover-card";
|
|
28
|
+
import {
|
|
29
|
+
InputGroupAddon,
|
|
30
|
+
InputGroupButton,
|
|
31
|
+
} from "../ui/input-group";
|
|
32
|
+
import {
|
|
33
|
+
Select,
|
|
34
|
+
SelectContent,
|
|
35
|
+
SelectItem,
|
|
36
|
+
SelectTrigger,
|
|
37
|
+
SelectValue,
|
|
38
|
+
} from "../ui/select";
|
|
39
|
+
import { Spinner } from "../ui/spinner";
|
|
40
|
+
import {
|
|
41
|
+
Tooltip,
|
|
42
|
+
TooltipContent,
|
|
43
|
+
TooltipTrigger,
|
|
44
|
+
} from "../ui/tooltip";
|
|
45
|
+
import { cn } from "../../lib/utils";
|
|
46
|
+
import {
|
|
47
|
+
CornerDownLeftIcon,
|
|
48
|
+
SquareIcon,
|
|
49
|
+
XIcon,
|
|
50
|
+
PlusIcon,
|
|
51
|
+
} from "lucide-react";
|
|
52
|
+
import { Children, useCallback } from "react";
|
|
53
|
+
|
|
54
|
+
// ── Body ──────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
export type PromptInputBodyProps = HTMLAttributes<HTMLDivElement>;
|
|
57
|
+
|
|
58
|
+
export const PromptInputBody = ({
|
|
59
|
+
className,
|
|
60
|
+
...props
|
|
61
|
+
}: PromptInputBodyProps) => (
|
|
62
|
+
<div className={cn("contents", className)} {...props} />
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// ── Header / Footer / Tools ───────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
export type PromptInputHeaderProps = Omit<ComponentProps<typeof InputGroupAddon>, "align">;
|
|
68
|
+
|
|
69
|
+
export const PromptInputHeader = ({
|
|
70
|
+
className,
|
|
71
|
+
...props
|
|
72
|
+
}: PromptInputHeaderProps) => (
|
|
73
|
+
<InputGroupAddon
|
|
74
|
+
align="block-end"
|
|
75
|
+
className={cn("order-first flex-wrap gap-1", className)}
|
|
76
|
+
{...props}
|
|
77
|
+
/>
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
export type PromptInputFooterProps = Omit<ComponentProps<typeof InputGroupAddon>, "align">;
|
|
81
|
+
|
|
82
|
+
export const PromptInputFooter = ({
|
|
83
|
+
className,
|
|
84
|
+
...props
|
|
85
|
+
}: PromptInputFooterProps) => (
|
|
86
|
+
<InputGroupAddon
|
|
87
|
+
align="block-end"
|
|
88
|
+
className={cn("justify-between gap-1", className)}
|
|
89
|
+
{...props}
|
|
90
|
+
/>
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
export type PromptInputToolsProps = HTMLAttributes<HTMLDivElement>;
|
|
94
|
+
|
|
95
|
+
export const PromptInputTools = ({
|
|
96
|
+
className,
|
|
97
|
+
...props
|
|
98
|
+
}: PromptInputToolsProps) => (
|
|
99
|
+
<div className={cn("flex min-w-0 items-center gap-1", className)} {...props} />
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// ── Button ────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
export type PromptInputButtonTooltip =
|
|
105
|
+
| string
|
|
106
|
+
| {
|
|
107
|
+
content: ReactNode;
|
|
108
|
+
shortcut?: string;
|
|
109
|
+
side?: ComponentProps<typeof TooltipContent>["side"];
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export type PromptInputButtonProps = ComponentProps<typeof InputGroupButton> & {
|
|
113
|
+
tooltip?: PromptInputButtonTooltip;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export const PromptInputButton = ({
|
|
117
|
+
variant = "ghost",
|
|
118
|
+
className,
|
|
119
|
+
size,
|
|
120
|
+
tooltip,
|
|
121
|
+
...props
|
|
122
|
+
}: PromptInputButtonProps) => {
|
|
123
|
+
const newSize = size ?? (Children.count(props.children) > 1 ? "sm" : "icon-sm");
|
|
124
|
+
|
|
125
|
+
const button = (
|
|
126
|
+
<InputGroupButton
|
|
127
|
+
className={cn(className)}
|
|
128
|
+
size={newSize}
|
|
129
|
+
type="button"
|
|
130
|
+
variant={variant}
|
|
131
|
+
{...props}
|
|
132
|
+
/>
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
if (!tooltip) return button;
|
|
136
|
+
|
|
137
|
+
const tooltipContent = typeof tooltip === "string" ? tooltip : tooltip.content;
|
|
138
|
+
const shortcut = typeof tooltip === "string" ? undefined : tooltip.shortcut;
|
|
139
|
+
const side = typeof tooltip === "string" ? "top" : (tooltip.side ?? "top");
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<Tooltip>
|
|
143
|
+
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
|
144
|
+
<TooltipContent side={side}>
|
|
145
|
+
{tooltipContent}
|
|
146
|
+
{shortcut && (
|
|
147
|
+
<span className="ml-2 text-muted-foreground">{shortcut}</span>
|
|
148
|
+
)}
|
|
149
|
+
</TooltipContent>
|
|
150
|
+
</Tooltip>
|
|
151
|
+
);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// ── Submit ────────────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
export type PromptInputSubmitProps = ComponentProps<typeof InputGroupButton> & {
|
|
157
|
+
status?: import("ai").ChatStatus;
|
|
158
|
+
onStop?: () => void;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
export const PromptInputSubmit = ({
|
|
162
|
+
className,
|
|
163
|
+
variant = "default",
|
|
164
|
+
size = "icon-sm",
|
|
165
|
+
status,
|
|
166
|
+
onStop,
|
|
167
|
+
onClick,
|
|
168
|
+
children,
|
|
169
|
+
...props
|
|
170
|
+
}: PromptInputSubmitProps) => {
|
|
171
|
+
const isGenerating = status === "submitted" || status === "streaming";
|
|
172
|
+
|
|
173
|
+
let Icon = <CornerDownLeftIcon className="size-4" />;
|
|
174
|
+
if (status === "submitted") Icon = <Spinner />;
|
|
175
|
+
else if (status === "streaming") Icon = <SquareIcon className="size-4" />;
|
|
176
|
+
else if (status === "error") Icon = <XIcon className="size-4" />;
|
|
177
|
+
|
|
178
|
+
const handleClick = useCallback(
|
|
179
|
+
(e: React.MouseEvent<HTMLButtonElement>) => {
|
|
180
|
+
if (isGenerating && onStop) {
|
|
181
|
+
e.preventDefault();
|
|
182
|
+
onStop();
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
onClick?.(e);
|
|
186
|
+
},
|
|
187
|
+
[isGenerating, onStop, onClick]
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
return (
|
|
191
|
+
<InputGroupButton
|
|
192
|
+
aria-label={isGenerating ? "Stop" : "Submit"}
|
|
193
|
+
className={cn(className)}
|
|
194
|
+
onClick={handleClick}
|
|
195
|
+
size={size}
|
|
196
|
+
type={isGenerating && onStop ? "button" : "submit"}
|
|
197
|
+
variant={variant}
|
|
198
|
+
{...props}
|
|
199
|
+
>
|
|
200
|
+
{children ?? Icon}
|
|
201
|
+
</InputGroupButton>
|
|
202
|
+
);
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// ── ActionMenu ────────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
export type PromptInputActionMenuProps = ComponentProps<typeof DropdownMenu>;
|
|
208
|
+
export const PromptInputActionMenu = (props: PromptInputActionMenuProps) => (
|
|
209
|
+
<DropdownMenu {...props} />
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
export type PromptInputActionMenuTriggerProps = PromptInputButtonProps;
|
|
213
|
+
|
|
214
|
+
export const PromptInputActionMenuTrigger = ({
|
|
215
|
+
className,
|
|
216
|
+
children,
|
|
217
|
+
...props
|
|
218
|
+
}: PromptInputActionMenuTriggerProps) => (
|
|
219
|
+
<DropdownMenuTrigger asChild>
|
|
220
|
+
<PromptInputButton className={className} {...props}>
|
|
221
|
+
{children ?? <PlusIcon className="size-4" />}
|
|
222
|
+
</PromptInputButton>
|
|
223
|
+
</DropdownMenuTrigger>
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
export type PromptInputActionMenuContentProps = ComponentProps<typeof DropdownMenuContent>;
|
|
227
|
+
export const PromptInputActionMenuContent = ({
|
|
228
|
+
className,
|
|
229
|
+
...props
|
|
230
|
+
}: PromptInputActionMenuContentProps) => (
|
|
231
|
+
<DropdownMenuContent align="start" className={cn(className)} {...props} />
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
export type PromptInputActionMenuItemProps = ComponentProps<typeof DropdownMenuItem>;
|
|
235
|
+
export const PromptInputActionMenuItem = ({
|
|
236
|
+
className,
|
|
237
|
+
...props
|
|
238
|
+
}: PromptInputActionMenuItemProps) => (
|
|
239
|
+
<DropdownMenuItem className={cn(className)} {...props} />
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
// ── Select ────────────────────────────────────────────────────────
|
|
243
|
+
|
|
244
|
+
export type PromptInputSelectProps = ComponentProps<typeof Select>;
|
|
245
|
+
export const PromptInputSelect = (props: PromptInputSelectProps) => <Select {...props} />;
|
|
246
|
+
|
|
247
|
+
export type PromptInputSelectTriggerProps = ComponentProps<typeof SelectTrigger>;
|
|
248
|
+
export const PromptInputSelectTrigger = ({
|
|
249
|
+
className,
|
|
250
|
+
...props
|
|
251
|
+
}: PromptInputSelectTriggerProps) => (
|
|
252
|
+
<SelectTrigger
|
|
253
|
+
className={cn(
|
|
254
|
+
"border-none bg-transparent font-medium text-muted-foreground shadow-none transition-colors",
|
|
255
|
+
"hover:bg-accent hover:text-foreground aria-expanded:bg-accent aria-expanded:text-foreground",
|
|
256
|
+
className
|
|
257
|
+
)}
|
|
258
|
+
{...props}
|
|
259
|
+
/>
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
export type PromptInputSelectContentProps = ComponentProps<typeof SelectContent>;
|
|
263
|
+
export const PromptInputSelectContent = ({ className, ...props }: PromptInputSelectContentProps) => (
|
|
264
|
+
<SelectContent className={cn(className)} {...props} />
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
export type PromptInputSelectItemProps = ComponentProps<typeof SelectItem>;
|
|
268
|
+
export const PromptInputSelectItem = ({ className, ...props }: PromptInputSelectItemProps) => (
|
|
269
|
+
<SelectItem className={cn(className)} {...props} />
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
export type PromptInputSelectValueProps = ComponentProps<typeof SelectValue>;
|
|
273
|
+
export const PromptInputSelectValue = ({ className, ...props }: PromptInputSelectValueProps) => (
|
|
274
|
+
<SelectValue className={cn(className)} {...props} />
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
// ── HoverCard ─────────────────────────────────────────────────────
|
|
278
|
+
|
|
279
|
+
export type PromptInputHoverCardProps = ComponentProps<typeof HoverCard>;
|
|
280
|
+
export const PromptInputHoverCard = ({
|
|
281
|
+
openDelay = 0,
|
|
282
|
+
closeDelay = 0,
|
|
283
|
+
...props
|
|
284
|
+
}: PromptInputHoverCardProps) => (
|
|
285
|
+
<HoverCard closeDelay={closeDelay} openDelay={openDelay} {...props} />
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
export type PromptInputHoverCardTriggerProps = ComponentProps<typeof HoverCardTrigger>;
|
|
289
|
+
export const PromptInputHoverCardTrigger = (props: PromptInputHoverCardTriggerProps) => (
|
|
290
|
+
<HoverCardTrigger {...props} />
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
export type PromptInputHoverCardContentProps = ComponentProps<typeof HoverCardContent>;
|
|
294
|
+
export const PromptInputHoverCardContent = ({
|
|
295
|
+
align = "start",
|
|
296
|
+
...props
|
|
297
|
+
}: PromptInputHoverCardContentProps) => (
|
|
298
|
+
<HoverCardContent align={align} {...props} />
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
// ── Tabs ──────────────────────────────────────────────────────────
|
|
302
|
+
|
|
303
|
+
export type PromptInputTabsListProps = HTMLAttributes<HTMLDivElement>;
|
|
304
|
+
export const PromptInputTabsList = ({ className, ...props }: PromptInputTabsListProps) => (
|
|
305
|
+
<div className={cn(className)} {...props} />
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
export type PromptInputTabProps = HTMLAttributes<HTMLDivElement>;
|
|
309
|
+
export const PromptInputTab = ({ className, ...props }: PromptInputTabProps) => (
|
|
310
|
+
<div className={cn(className)} {...props} />
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
export type PromptInputTabLabelProps = HTMLAttributes<HTMLHeadingElement>;
|
|
314
|
+
export const PromptInputTabLabel = ({ className, ...props }: PromptInputTabLabelProps) => (
|
|
315
|
+
// Content provided via children in props
|
|
316
|
+
// oxlint-disable-next-line eslint-plugin-jsx-a11y(heading-has-content)
|
|
317
|
+
<h3 className={cn("mb-2 px-3 font-medium text-muted-foreground text-xs", className)} {...props} />
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
export type PromptInputTabBodyProps = HTMLAttributes<HTMLDivElement>;
|
|
321
|
+
export const PromptInputTabBody = ({ className, ...props }: PromptInputTabBodyProps) => (
|
|
322
|
+
<div className={cn("space-y-1", className)} {...props} />
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
export type PromptInputTabItemProps = HTMLAttributes<HTMLDivElement>;
|
|
326
|
+
export const PromptInputTabItem = ({ className, ...props }: PromptInputTabItemProps) => (
|
|
327
|
+
<div
|
|
328
|
+
className={cn("flex items-center gap-2 px-3 py-2 text-xs hover:bg-accent", className)}
|
|
329
|
+
{...props}
|
|
330
|
+
/>
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
// ── Command ───────────────────────────────────────────────────────
|
|
334
|
+
|
|
335
|
+
export type PromptInputCommandProps = ComponentProps<typeof Command>;
|
|
336
|
+
export const PromptInputCommand = ({ className, ...props }: PromptInputCommandProps) => (
|
|
337
|
+
<Command className={cn(className)} {...props} />
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
export type PromptInputCommandInputProps = ComponentProps<typeof CommandInput>;
|
|
341
|
+
export const PromptInputCommandInput = ({ className, ...props }: PromptInputCommandInputProps) => (
|
|
342
|
+
<CommandInput className={cn(className)} {...props} />
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
export type PromptInputCommandListProps = ComponentProps<typeof CommandList>;
|
|
346
|
+
export const PromptInputCommandList = ({ className, ...props }: PromptInputCommandListProps) => (
|
|
347
|
+
<CommandList className={cn(className)} {...props} />
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
export type PromptInputCommandEmptyProps = ComponentProps<typeof CommandEmpty>;
|
|
351
|
+
export const PromptInputCommandEmpty = ({ className, ...props }: PromptInputCommandEmptyProps) => (
|
|
352
|
+
<CommandEmpty className={cn(className)} {...props} />
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
export type PromptInputCommandGroupProps = ComponentProps<typeof CommandGroup>;
|
|
356
|
+
export const PromptInputCommandGroup = ({ className, ...props }: PromptInputCommandGroupProps) => (
|
|
357
|
+
<CommandGroup className={cn(className)} {...props} />
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
export type PromptInputCommandItemProps = ComponentProps<typeof CommandItem>;
|
|
361
|
+
export const PromptInputCommandItem = ({ className, ...props }: PromptInputCommandItemProps) => (
|
|
362
|
+
<CommandItem className={cn(className)} {...props} />
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
export type PromptInputCommandSeparatorProps = ComponentProps<typeof CommandSeparator>;
|
|
366
|
+
export const PromptInputCommandSeparator = ({
|
|
367
|
+
className,
|
|
368
|
+
...props
|
|
369
|
+
}: PromptInputCommandSeparatorProps) => (
|
|
370
|
+
<CommandSeparator className={cn(className)} {...props} />
|
|
371
|
+
);
|