@springbrand/message-panel 0.1.2 → 0.1.3-alpha.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/demo/camel-chat-showcase.tsx +1265 -0
- package/demo/fixtures.ts +292 -0
- package/demo/message-panel-chat-showcase.tsx +250 -0
- package/demo/message-panel-gallery.tsx +389 -0
- package/package.json +6 -16
- package/src/adapters/ai-sdk.ts +32 -0
- package/src/camel/camel-chat-messages.tsx +1423 -0
- package/src/camel/camel-model-select.tsx +88 -0
- package/src/camel/camel-plan-bar.tsx +115 -0
- package/src/camel/camel-prompt-input.tsx +547 -0
- package/src/camel/camel-tool-presentation.tsx +1270 -0
- package/src/camel/camel-turn.ts +316 -0
- package/src/camel/turn-summary-bar.tsx +81 -0
- package/src/chat-summary-panel.tsx +229 -0
- package/src/composer/chat-composer.tsx +216 -0
- package/src/composer/composer-attachments.tsx +138 -0
- package/src/composer/composer-trigger-popover.tsx +236 -0
- package/src/composer/composer.tsx +425 -0
- package/src/composer/drop-controller.ts +80 -0
- package/src/composer/types.ts +79 -0
- package/src/contracts.ts +138 -0
- package/src/conversation.tsx +76 -0
- package/src/internal/cn.ts +3 -0
- package/src/message-panel.tsx +368 -0
- package/src/message.tsx +254 -0
- package/src/parts/index.tsx +1149 -0
- package/src/parts/kind.ts +67 -0
- package/src/parts/plan.ts +63 -0
- package/src/parts/primitives.tsx +439 -0
- package/src/registry.ts +44 -0
- package/src/status.tsx +132 -0
- package/dist/demo/camel-chat-showcase.d.ts +0 -1
- package/dist/demo/camel-chat-showcase.js +0 -1007
- package/dist/demo/fixtures.d.ts +0 -32
- package/dist/demo/fixtures.js +0 -250
- package/dist/demo/index.js +0 -4
- package/dist/demo/message-panel-chat-showcase.d.ts +0 -1
- package/dist/demo/message-panel-chat-showcase.js +0 -114
- package/dist/demo/message-panel-gallery.d.ts +0 -1
- package/dist/demo/message-panel-gallery.js +0 -130
- package/dist/src/adapters/ai-sdk.d.ts +0 -4
- package/dist/src/adapters/ai-sdk.js +0 -25
- package/dist/src/camel/camel-chat-messages.d.ts +0 -44
- package/dist/src/camel/camel-chat-messages.js +0 -421
- package/dist/src/camel/camel-model-select.d.ts +0 -13
- package/dist/src/camel/camel-model-select.js +0 -7
- package/dist/src/camel/camel-plan-bar.d.ts +0 -5
- package/dist/src/camel/camel-plan-bar.js +0 -24
- package/dist/src/camel/camel-prompt-input.d.ts +0 -32
- package/dist/src/camel/camel-prompt-input.js +0 -182
- package/dist/src/camel/camel-tool-presentation.d.ts +0 -21
- package/dist/src/camel/camel-tool-presentation.js +0 -784
- package/dist/src/camel/camel-turn.d.ts +0 -57
- package/dist/src/camel/camel-turn.js +0 -203
- package/dist/src/camel/index.js +0 -7
- package/dist/src/camel/turn-summary-bar.d.ts +0 -8
- package/dist/src/camel/turn-summary-bar.js +0 -12
- package/dist/src/chat-summary-panel.d.ts +0 -22
- package/dist/src/chat-summary-panel.js +0 -26
- package/dist/src/composer/chat-composer.d.ts +0 -4
- package/dist/src/composer/chat-composer.js +0 -90
- package/dist/src/composer/composer-attachments.d.ts +0 -13
- package/dist/src/composer/composer-attachments.js +0 -47
- package/dist/src/composer/composer-trigger-popover.d.ts +0 -31
- package/dist/src/composer/composer-trigger-popover.js +0 -29
- package/dist/src/composer/composer.d.ts +0 -15
- package/dist/src/composer/composer.js +0 -180
- package/dist/src/composer/drop-controller.d.ts +0 -8
- package/dist/src/composer/drop-controller.js +0 -69
- package/dist/src/composer/index.js +0 -6
- package/dist/src/composer/types.d.ts +0 -62
- package/dist/src/composer/types.js +0 -1
- package/dist/src/contracts.d.ts +0 -94
- package/dist/src/contracts.js +0 -1
- package/dist/src/conversation.d.ts +0 -13
- package/dist/src/conversation.js +0 -18
- package/dist/src/index.js +0 -12
- package/dist/src/internal/cn.d.ts +0 -1
- package/dist/src/internal/cn.js +0 -3
- package/dist/src/message-panel.d.ts +0 -4
- package/dist/src/message-panel.js +0 -144
- package/dist/src/message.d.ts +0 -36
- package/dist/src/message.js +0 -83
- package/dist/src/parts/index.d.ts +0 -187
- package/dist/src/parts/index.js +0 -332
- package/dist/src/parts/kind.d.ts +0 -3
- package/dist/src/parts/kind.js +0 -69
- package/dist/src/parts/plan.d.ts +0 -10
- package/dist/src/parts/plan.js +0 -40
- package/dist/src/parts/primitives.d.ts +0 -70
- package/dist/src/parts/primitives.js +0 -153
- package/dist/src/registry.d.ts +0 -4
- package/dist/src/registry.js +0 -31
- package/dist/src/status.d.ts +0 -26
- package/dist/src/status.js +0 -74
- /package/{dist/demo/index.d.ts → demo/index.ts} +0 -0
- /package/{dist/src/camel/index.d.ts → src/camel/index.ts} +0 -0
- /package/{dist/src/composer/index.d.ts → src/composer/index.ts} +0 -0
- /package/{dist/src/index.d.ts → src/index.ts} +0 -0
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AssistantRuntimeProvider,
|
|
3
|
+
ComposerPrimitive,
|
|
4
|
+
unstable_useSlashCommandAdapter,
|
|
5
|
+
unstable_useTriggerPopoverAriaProps,
|
|
6
|
+
unstable_useTriggerPopoverTriggers,
|
|
7
|
+
useExternalStoreRuntime,
|
|
8
|
+
type AssistantRuntime,
|
|
9
|
+
type ThreadMessage,
|
|
10
|
+
} from "@assistant-ui/react";
|
|
11
|
+
import {
|
|
12
|
+
ArrowUpIcon,
|
|
13
|
+
ClockIcon,
|
|
14
|
+
FileIcon,
|
|
15
|
+
FileTextIcon,
|
|
16
|
+
GlobeIcon,
|
|
17
|
+
ListChecksIcon,
|
|
18
|
+
Loader2Icon,
|
|
19
|
+
PlugIcon,
|
|
20
|
+
PlusIcon,
|
|
21
|
+
RotateCcwIcon,
|
|
22
|
+
SquareIcon,
|
|
23
|
+
TerminalIcon,
|
|
24
|
+
WrenchIcon,
|
|
25
|
+
XIcon,
|
|
26
|
+
} from "lucide-react";
|
|
27
|
+
import {
|
|
28
|
+
forwardRef,
|
|
29
|
+
useCallback,
|
|
30
|
+
useEffect,
|
|
31
|
+
useMemo,
|
|
32
|
+
useRef,
|
|
33
|
+
useState,
|
|
34
|
+
type ChangeEvent,
|
|
35
|
+
type ClipboardEvent,
|
|
36
|
+
type DragEvent,
|
|
37
|
+
type FormEvent,
|
|
38
|
+
type KeyboardEvent,
|
|
39
|
+
type ReactNode,
|
|
40
|
+
} from "react";
|
|
41
|
+
import { ComposerTriggerPopover } from "../composer/composer-trigger-popover";
|
|
42
|
+
import type { ComposerCommand } from "../composer/types";
|
|
43
|
+
import { cn } from "../internal/cn";
|
|
44
|
+
|
|
45
|
+
export interface CamelAttachmentView {
|
|
46
|
+
id: string;
|
|
47
|
+
filename?: string;
|
|
48
|
+
mediaType?: string;
|
|
49
|
+
size?: number;
|
|
50
|
+
url: string;
|
|
51
|
+
status: "uploading" | "ready" | "error";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const COMMAND_ICON_MAP = {
|
|
55
|
+
plan: ListChecksIcon,
|
|
56
|
+
research: GlobeIcon,
|
|
57
|
+
file: FileTextIcon,
|
|
58
|
+
time: ClockIcon,
|
|
59
|
+
tool: WrenchIcon,
|
|
60
|
+
mcp: PlugIcon,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
64
|
+
size?: "sm" | "icon-xs" | "icon-sm";
|
|
65
|
+
variant?: "default" | "outline" | "ghost" | "destructive";
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
function Button({
|
|
69
|
+
className,
|
|
70
|
+
size = "sm",
|
|
71
|
+
variant = "default",
|
|
72
|
+
type = "button",
|
|
73
|
+
...props
|
|
74
|
+
}: ButtonProps) {
|
|
75
|
+
return (
|
|
76
|
+
<button
|
|
77
|
+
type={type}
|
|
78
|
+
className={cn(
|
|
79
|
+
"inline-flex shrink-0 items-center justify-center gap-1 rounded-md text-xs font-medium transition-all outline-none disabled:pointer-events-none disabled:opacity-50",
|
|
80
|
+
variant === "default" &&
|
|
81
|
+
"bg-primary text-primary-foreground hover:bg-primary/80",
|
|
82
|
+
variant === "outline" &&
|
|
83
|
+
"border border-border bg-background hover:bg-muted",
|
|
84
|
+
variant === "ghost" && "hover:bg-muted hover:text-foreground",
|
|
85
|
+
variant === "destructive" &&
|
|
86
|
+
"bg-destructive/10 text-destructive hover:bg-destructive/20",
|
|
87
|
+
size === "sm" && "h-7 px-2",
|
|
88
|
+
size === "icon-xs" && "size-6 p-0",
|
|
89
|
+
size === "icon-sm" && "size-8 p-0",
|
|
90
|
+
className,
|
|
91
|
+
)}
|
|
92
|
+
{...props}
|
|
93
|
+
/>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function InputGroup({
|
|
98
|
+
className,
|
|
99
|
+
...props
|
|
100
|
+
}: React.HTMLAttributes<HTMLDivElement>) {
|
|
101
|
+
return (
|
|
102
|
+
<div
|
|
103
|
+
role="group"
|
|
104
|
+
className={cn(
|
|
105
|
+
"group/input-group relative flex w-full min-w-0 flex-col items-center rounded-md border border-input bg-input/20 outline-none transition-colors dark:bg-input/30",
|
|
106
|
+
"has-[[data-camel-input=true]:focus-visible]:border-ring has-[[data-camel-input=true]:focus-visible]:ring-2 has-[[data-camel-input=true]:focus-visible]:ring-ring/30",
|
|
107
|
+
className,
|
|
108
|
+
)}
|
|
109
|
+
{...props}
|
|
110
|
+
/>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function InputGroupAddon({
|
|
115
|
+
className,
|
|
116
|
+
align,
|
|
117
|
+
...props
|
|
118
|
+
}: React.HTMLAttributes<HTMLDivElement> & {
|
|
119
|
+
align: "block-start" | "block-end";
|
|
120
|
+
}) {
|
|
121
|
+
return (
|
|
122
|
+
<div
|
|
123
|
+
role="group"
|
|
124
|
+
data-align={align}
|
|
125
|
+
className={cn(
|
|
126
|
+
"flex h-auto w-full cursor-text items-center gap-1 px-2 text-xs font-medium text-muted-foreground",
|
|
127
|
+
align === "block-start"
|
|
128
|
+
? "order-first justify-start pt-2"
|
|
129
|
+
: "order-last pb-2",
|
|
130
|
+
className,
|
|
131
|
+
)}
|
|
132
|
+
{...props}
|
|
133
|
+
/>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function InputGroupButton(props: ButtonProps) {
|
|
138
|
+
return <Button {...props} />;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const InputGroupTextarea = forwardRef<
|
|
142
|
+
HTMLTextAreaElement,
|
|
143
|
+
React.TextareaHTMLAttributes<HTMLTextAreaElement>
|
|
144
|
+
>(({ className, ...props }, ref) => (
|
|
145
|
+
<textarea
|
|
146
|
+
ref={ref}
|
|
147
|
+
data-camel-input="true"
|
|
148
|
+
className={cn(
|
|
149
|
+
"w-full flex-1 resize-none rounded-none border-0 bg-transparent py-2 shadow-none outline-none ring-0 placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
|
150
|
+
className,
|
|
151
|
+
)}
|
|
152
|
+
{...props}
|
|
153
|
+
/>
|
|
154
|
+
));
|
|
155
|
+
InputGroupTextarea.displayName = "InputGroupTextarea";
|
|
156
|
+
|
|
157
|
+
interface CamelPromptInputProps {
|
|
158
|
+
value: string;
|
|
159
|
+
onChange: (value: string) => void;
|
|
160
|
+
onSubmit: () => void;
|
|
161
|
+
onStop: () => void;
|
|
162
|
+
status: "ready" | "submitted" | "streaming" | "error";
|
|
163
|
+
attachments: readonly CamelAttachmentView[];
|
|
164
|
+
commands: readonly ComposerCommand[];
|
|
165
|
+
onFilesSelected?: (files: readonly File[]) => void;
|
|
166
|
+
onAttachmentRemove: (id: string) => void;
|
|
167
|
+
onAttachmentRetry: (id: string) => void;
|
|
168
|
+
tools?: ReactNode;
|
|
169
|
+
/** Compact delivery selector rendered immediately beside the submit button. */
|
|
170
|
+
submitControl?: ReactNode;
|
|
171
|
+
placeholder?: string;
|
|
172
|
+
/** Durable Turn activity can outlive the local chat stream status. */
|
|
173
|
+
turnActive?: boolean;
|
|
174
|
+
/** Blocks new submissions without disabling the stop control for an active Turn. */
|
|
175
|
+
submissionBlocked?: boolean;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function formatBytes(size?: number): string {
|
|
179
|
+
if (!size) return "";
|
|
180
|
+
if (size < 1024) return `${size} B`;
|
|
181
|
+
if (size < 1024 * 1024) return `${Math.round(size / 1024)} KB`;
|
|
182
|
+
return `${(size / 1024 / 1024).toFixed(1)} MB`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function AttachmentList({
|
|
186
|
+
attachments,
|
|
187
|
+
onRemove,
|
|
188
|
+
onRetry,
|
|
189
|
+
}: {
|
|
190
|
+
attachments: readonly CamelAttachmentView[];
|
|
191
|
+
onRemove: (id: string) => void;
|
|
192
|
+
onRetry: (id: string) => void;
|
|
193
|
+
}) {
|
|
194
|
+
return (
|
|
195
|
+
<div className="flex w-full flex-wrap gap-2">
|
|
196
|
+
{attachments.map((attachment) => (
|
|
197
|
+
<div
|
|
198
|
+
key={attachment.id}
|
|
199
|
+
className="group/attachment flex max-w-64 items-center gap-2 rounded-lg border border-border bg-muted/30 p-2"
|
|
200
|
+
data-attachment-status={attachment.status}
|
|
201
|
+
>
|
|
202
|
+
{attachment.mediaType?.startsWith("image/") ? (
|
|
203
|
+
<img
|
|
204
|
+
src={attachment.url}
|
|
205
|
+
alt=""
|
|
206
|
+
className="size-9 shrink-0 rounded-md object-cover"
|
|
207
|
+
/>
|
|
208
|
+
) : (
|
|
209
|
+
<span className="grid size-9 shrink-0 place-items-center rounded-md bg-muted">
|
|
210
|
+
<FileIcon className="size-4 text-muted-foreground" />
|
|
211
|
+
</span>
|
|
212
|
+
)}
|
|
213
|
+
<div className="min-w-0 flex-1">
|
|
214
|
+
<div className="truncate text-xs text-foreground">
|
|
215
|
+
{attachment.filename ?? "Attachment"}
|
|
216
|
+
</div>
|
|
217
|
+
<div className="mt-0.5 flex items-center gap-1 text-[10px] text-muted-foreground">
|
|
218
|
+
{attachment.status === "uploading" && (
|
|
219
|
+
<Loader2Icon className="size-3 animate-spin" />
|
|
220
|
+
)}
|
|
221
|
+
<span>
|
|
222
|
+
{attachment.status === "uploading"
|
|
223
|
+
? "Uploading"
|
|
224
|
+
: attachment.status === "error"
|
|
225
|
+
? "Upload failed"
|
|
226
|
+
: formatBytes(attachment.size)}
|
|
227
|
+
</span>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
{attachment.status === "error" && (
|
|
231
|
+
<Button
|
|
232
|
+
type="button"
|
|
233
|
+
variant="ghost"
|
|
234
|
+
size="icon-xs"
|
|
235
|
+
onClick={() => onRetry(attachment.id)}
|
|
236
|
+
aria-label="重试上传"
|
|
237
|
+
>
|
|
238
|
+
<RotateCcwIcon className="size-3" />
|
|
239
|
+
</Button>
|
|
240
|
+
)}
|
|
241
|
+
<Button
|
|
242
|
+
type="button"
|
|
243
|
+
variant="ghost"
|
|
244
|
+
size="icon-xs"
|
|
245
|
+
onClick={() => onRemove(attachment.id)}
|
|
246
|
+
aria-label="移除附件"
|
|
247
|
+
>
|
|
248
|
+
<XIcon className="size-3" />
|
|
249
|
+
</Button>
|
|
250
|
+
</div>
|
|
251
|
+
))}
|
|
252
|
+
</div>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function CamelPromptInputContent({
|
|
257
|
+
value,
|
|
258
|
+
onChange,
|
|
259
|
+
onSubmit,
|
|
260
|
+
onStop,
|
|
261
|
+
status,
|
|
262
|
+
attachments,
|
|
263
|
+
commands,
|
|
264
|
+
onFilesSelected,
|
|
265
|
+
onAttachmentRemove,
|
|
266
|
+
onAttachmentRetry,
|
|
267
|
+
tools,
|
|
268
|
+
submitControl,
|
|
269
|
+
placeholder = "Type a message...",
|
|
270
|
+
submissionBlocked = false,
|
|
271
|
+
turnActive,
|
|
272
|
+
runtime,
|
|
273
|
+
}: CamelPromptInputProps & { runtime: AssistantRuntime }) {
|
|
274
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
275
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
276
|
+
const [isDragOver, setIsDragOver] = useState(false);
|
|
277
|
+
const isRunning = turnActive ??
|
|
278
|
+
(status === "submitted" || status === "streaming");
|
|
279
|
+
const hasUploading = attachments.some(
|
|
280
|
+
(attachment) => attachment.status === "uploading",
|
|
281
|
+
);
|
|
282
|
+
const hasReady = attachments.some(
|
|
283
|
+
(attachment) => attachment.status === "ready",
|
|
284
|
+
);
|
|
285
|
+
const slashCommands = useMemo(
|
|
286
|
+
() =>
|
|
287
|
+
commands.map((command) => ({
|
|
288
|
+
id: command.name,
|
|
289
|
+
description: command.description,
|
|
290
|
+
icon: command.icon,
|
|
291
|
+
execute: () => {
|
|
292
|
+
runtime.thread.composer.setText(command.prompt);
|
|
293
|
+
onChange(command.prompt);
|
|
294
|
+
requestAnimationFrame(() => textareaRef.current?.focus());
|
|
295
|
+
},
|
|
296
|
+
})),
|
|
297
|
+
[commands, onChange, runtime],
|
|
298
|
+
);
|
|
299
|
+
const slash = unstable_useSlashCommandAdapter({
|
|
300
|
+
commands: slashCommands,
|
|
301
|
+
removeOnExecute: true,
|
|
302
|
+
iconMap: COMMAND_ICON_MAP,
|
|
303
|
+
fallbackIcon: TerminalIcon,
|
|
304
|
+
});
|
|
305
|
+
const triggers = unstable_useTriggerPopoverTriggers();
|
|
306
|
+
const triggerAria = unstable_useTriggerPopoverAriaProps();
|
|
307
|
+
|
|
308
|
+
const setTriggerCursor = useCallback(
|
|
309
|
+
(position: number) => {
|
|
310
|
+
for (const trigger of triggers.values()) {
|
|
311
|
+
trigger.resource.setCursorPosition(position);
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
[triggers],
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
useEffect(() => {
|
|
318
|
+
const textarea = textareaRef.current;
|
|
319
|
+
if (!textarea) return;
|
|
320
|
+
textarea.style.height = "auto";
|
|
321
|
+
textarea.style.height = `${Math.min(textarea.scrollHeight, 384)}px`;
|
|
322
|
+
}, [value]);
|
|
323
|
+
|
|
324
|
+
const submit = useCallback(() => {
|
|
325
|
+
if (isRunning && !value.trim() && !hasReady) {
|
|
326
|
+
onStop();
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (submissionBlocked) return;
|
|
330
|
+
if (!hasUploading && (value.trim() || hasReady)) onSubmit();
|
|
331
|
+
}, [
|
|
332
|
+
hasReady,
|
|
333
|
+
hasUploading,
|
|
334
|
+
isRunning,
|
|
335
|
+
onStop,
|
|
336
|
+
onSubmit,
|
|
337
|
+
submissionBlocked,
|
|
338
|
+
value,
|
|
339
|
+
]);
|
|
340
|
+
|
|
341
|
+
const handleKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
342
|
+
for (const trigger of triggers.values()) {
|
|
343
|
+
if (trigger.resource.handleKeyDown(event)) return;
|
|
344
|
+
}
|
|
345
|
+
if (
|
|
346
|
+
event.key === "Enter" &&
|
|
347
|
+
!event.shiftKey &&
|
|
348
|
+
!event.nativeEvent.isComposing
|
|
349
|
+
) {
|
|
350
|
+
event.preventDefault();
|
|
351
|
+
submit();
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const handlePaste = (event: ClipboardEvent<HTMLTextAreaElement>) => {
|
|
356
|
+
if (!onFilesSelected) return;
|
|
357
|
+
const files = Array.from(event.clipboardData.files);
|
|
358
|
+
if (files.length > 0) {
|
|
359
|
+
event.preventDefault();
|
|
360
|
+
onFilesSelected(files);
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
const handleFiles = (event: ChangeEvent<HTMLInputElement>) => {
|
|
365
|
+
const files = Array.from(event.currentTarget.files ?? []);
|
|
366
|
+
if (files.length > 0) onFilesSelected?.(files);
|
|
367
|
+
event.currentTarget.value = "";
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
const handleDragOver = (event: DragEvent<HTMLDivElement>) => {
|
|
371
|
+
if (!onFilesSelected) return;
|
|
372
|
+
event.preventDefault();
|
|
373
|
+
event.stopPropagation();
|
|
374
|
+
setIsDragOver(true);
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
const handleDragLeave = (event: DragEvent<HTMLDivElement>) => {
|
|
378
|
+
event.preventDefault();
|
|
379
|
+
event.stopPropagation();
|
|
380
|
+
const nextTarget = event.relatedTarget;
|
|
381
|
+
if (!(nextTarget instanceof Node) || !event.currentTarget.contains(nextTarget)) {
|
|
382
|
+
setIsDragOver(false);
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
const handleDrop = (event: DragEvent<HTMLDivElement>) => {
|
|
387
|
+
if (!onFilesSelected) return;
|
|
388
|
+
event.preventDefault();
|
|
389
|
+
event.stopPropagation();
|
|
390
|
+
setIsDragOver(false);
|
|
391
|
+
const files = Array.from(event.dataTransfer.files);
|
|
392
|
+
if (files.length > 0) onFilesSelected(files);
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
const handleFormSubmit = (event: FormEvent) => {
|
|
396
|
+
event.preventDefault();
|
|
397
|
+
submit();
|
|
398
|
+
};
|
|
399
|
+
const showStopButton = isRunning && !value.trim() && !hasReady;
|
|
400
|
+
const submitDisabled =
|
|
401
|
+
hasUploading || (
|
|
402
|
+
!showStopButton &&
|
|
403
|
+
(submissionBlocked || (!value.trim() && !hasReady))
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
return (
|
|
407
|
+
<form
|
|
408
|
+
onSubmit={handleFormSubmit}
|
|
409
|
+
className="relative shrink-0"
|
|
410
|
+
data-camel-prompt-input=""
|
|
411
|
+
data-drop-scope="document"
|
|
412
|
+
>
|
|
413
|
+
{onFilesSelected && (
|
|
414
|
+
<input
|
|
415
|
+
ref={fileInputRef}
|
|
416
|
+
type="file"
|
|
417
|
+
multiple
|
|
418
|
+
onChange={handleFiles}
|
|
419
|
+
className="hidden"
|
|
420
|
+
aria-hidden="true"
|
|
421
|
+
/>
|
|
422
|
+
)}
|
|
423
|
+
<ComposerTriggerPopover
|
|
424
|
+
char="/"
|
|
425
|
+
{...slash}
|
|
426
|
+
aria-label="Commands"
|
|
427
|
+
backLabel="Back"
|
|
428
|
+
emptyItemsLabel="No matching commands"
|
|
429
|
+
className="w-full"
|
|
430
|
+
/>
|
|
431
|
+
<div
|
|
432
|
+
onDragOver={handleDragOver}
|
|
433
|
+
onDragLeave={handleDragLeave}
|
|
434
|
+
onDrop={handleDrop}
|
|
435
|
+
className={cn(
|
|
436
|
+
"relative rounded-2xl transition-all duration-200",
|
|
437
|
+
isDragOver && "ring-2 ring-primary ring-offset-2",
|
|
438
|
+
)}
|
|
439
|
+
>
|
|
440
|
+
{isDragOver && (
|
|
441
|
+
<div className="absolute inset-0 z-20 flex items-center justify-center rounded-2xl border-2 border-dashed border-primary bg-primary/10">
|
|
442
|
+
<span className="text-sm font-medium text-primary">
|
|
443
|
+
Drop files here
|
|
444
|
+
</span>
|
|
445
|
+
</div>
|
|
446
|
+
)}
|
|
447
|
+
<InputGroup className="cursor-text rounded-2xl border-border bg-background shadow-sm transition-all duration-200 hover:shadow-md focus-within:border-ring focus-within:shadow-md">
|
|
448
|
+
{attachments.length > 0 && (
|
|
449
|
+
<InputGroupAddon
|
|
450
|
+
align="block-start"
|
|
451
|
+
className="border-b border-border"
|
|
452
|
+
>
|
|
453
|
+
<AttachmentList
|
|
454
|
+
attachments={attachments}
|
|
455
|
+
onRemove={onAttachmentRemove}
|
|
456
|
+
onRetry={onAttachmentRetry}
|
|
457
|
+
/>
|
|
458
|
+
</InputGroupAddon>
|
|
459
|
+
)}
|
|
460
|
+
<InputGroupTextarea
|
|
461
|
+
ref={textareaRef}
|
|
462
|
+
value={value}
|
|
463
|
+
onChange={(event) => {
|
|
464
|
+
const next = event.currentTarget.value;
|
|
465
|
+
runtime.thread.composer.setText(next);
|
|
466
|
+
onChange(next);
|
|
467
|
+
setTriggerCursor(
|
|
468
|
+
event.currentTarget.selectionStart ?? next.length,
|
|
469
|
+
);
|
|
470
|
+
}}
|
|
471
|
+
onKeyDown={handleKeyDown}
|
|
472
|
+
onSelect={(event) =>
|
|
473
|
+
setTriggerCursor(
|
|
474
|
+
event.currentTarget.selectionStart ?? value.length,
|
|
475
|
+
)
|
|
476
|
+
}
|
|
477
|
+
onPaste={handlePaste}
|
|
478
|
+
placeholder={placeholder}
|
|
479
|
+
autoFocus
|
|
480
|
+
className="relative z-10 max-h-96 min-h-11 overflow-y-auto bg-transparent p-3.5 text-sm selection:bg-primary/30"
|
|
481
|
+
style={{ caretColor: "var(--foreground)" }}
|
|
482
|
+
{...triggerAria}
|
|
483
|
+
/>
|
|
484
|
+
<InputGroupAddon
|
|
485
|
+
align="block-end"
|
|
486
|
+
className="justify-between px-3 pb-3"
|
|
487
|
+
>
|
|
488
|
+
<div className="flex items-center gap-1.5">
|
|
489
|
+
{onFilesSelected && (
|
|
490
|
+
<InputGroupButton
|
|
491
|
+
type="button"
|
|
492
|
+
size="icon-sm"
|
|
493
|
+
variant="ghost"
|
|
494
|
+
onClick={() => fileInputRef.current?.click()}
|
|
495
|
+
className="rounded-full text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
496
|
+
aria-label="Upload file"
|
|
497
|
+
>
|
|
498
|
+
<PlusIcon className="size-4" />
|
|
499
|
+
</InputGroupButton>
|
|
500
|
+
)}
|
|
501
|
+
{tools}
|
|
502
|
+
</div>
|
|
503
|
+
<div className="flex items-center gap-1">
|
|
504
|
+
{!showStopButton && submitControl}
|
|
505
|
+
<InputGroupButton
|
|
506
|
+
type={showStopButton ? "button" : "submit"}
|
|
507
|
+
size="icon-sm"
|
|
508
|
+
variant="default"
|
|
509
|
+
disabled={submitDisabled}
|
|
510
|
+
onClick={showStopButton ? onStop : undefined}
|
|
511
|
+
className="rounded-full"
|
|
512
|
+
aria-label={showStopButton ? "停止" : "发送"}
|
|
513
|
+
>
|
|
514
|
+
{showStopButton ? (
|
|
515
|
+
<SquareIcon className="size-3" />
|
|
516
|
+
) : (
|
|
517
|
+
<ArrowUpIcon className="size-4" />
|
|
518
|
+
)}
|
|
519
|
+
</InputGroupButton>
|
|
520
|
+
</div>
|
|
521
|
+
</InputGroupAddon>
|
|
522
|
+
</InputGroup>
|
|
523
|
+
</div>
|
|
524
|
+
</form>
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const EMPTY_MESSAGES: readonly ThreadMessage[] = [];
|
|
529
|
+
|
|
530
|
+
export function CamelPromptInput(props: CamelPromptInputProps) {
|
|
531
|
+
const runtime = useExternalStoreRuntime<ThreadMessage>({
|
|
532
|
+
messages: EMPTY_MESSAGES,
|
|
533
|
+
onNew: async () => undefined,
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
useEffect(() => {
|
|
537
|
+
runtime.thread.composer.setText(props.value);
|
|
538
|
+
}, [props.value, runtime]);
|
|
539
|
+
|
|
540
|
+
return (
|
|
541
|
+
<AssistantRuntimeProvider runtime={runtime}>
|
|
542
|
+
<ComposerPrimitive.Unstable_TriggerPopoverRoot>
|
|
543
|
+
<CamelPromptInputContent {...props} runtime={runtime} />
|
|
544
|
+
</ComposerPrimitive.Unstable_TriggerPopoverRoot>
|
|
545
|
+
</AssistantRuntimeProvider>
|
|
546
|
+
);
|
|
547
|
+
}
|