@springbrand/message-panel 0.1.3-alpha.3 → 0.1.3-alpha.31
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/cloud-os/README.md +6 -6
- package/cloud-os/assets/followup-arrow.svg +3 -0
- package/cloud-os/assets/loading-corner.svg +3 -0
- package/cloud-os/assets/loading-mark.svg +16 -0
- package/cloud-os/assets/loading-spark.svg +3 -0
- package/cloud-os/assets/model-selected.svg +5 -0
- package/cloud-os/assets/thinking-spark.svg +9 -0
- package/cloud-os/capability-chip.tsx +35 -0
- package/cloud-os/chat/activity-indicator.tsx +2 -2
- package/cloud-os/chat/cloud-os-chat-messages.tsx +374 -63
- package/cloud-os/chat/image-generation.tsx +76 -0
- package/cloud-os/chat/loading-state.tsx +25 -0
- package/cloud-os/chat/markdown-message.tsx +144 -41
- package/cloud-os/chat/rich-blocks.tsx +548 -246
- package/cloud-os/chat/tool-presentation.ts +35 -16
- package/cloud-os/chat/tool-rows.tsx +330 -85
- package/cloud-os/chat/transcript-model.ts +428 -56
- package/cloud-os/composer/cloud-os-chat-input.tsx +228 -142
- package/cloud-os/composer/cloud-os-model-select.tsx +105 -0
- package/cloud-os/file-view.tsx +441 -0
- package/cloud-os/index.ts +35 -3
- package/cloud-os/layout/cloud-os-workspace-split.tsx +58 -15
- package/cloud-os/primitives/workshop-controls.tsx +2 -2
- package/cloud-os/styles/cloud-os.css +100 -20
- package/demo/camel-chat-showcase.tsx +21 -13
- package/demo/chat-scenarios.ts +223 -43
- package/demo/cloud-os-chat-showcase.tsx +47 -14
- package/demo/fixtures.ts +4 -5
- package/demo/message-panel-gallery.tsx +16 -2
- package/package.json +3 -4
- package/src/camel/camel-chat-messages.tsx +78 -78
- package/src/camel/camel-tool-presentation.tsx +19 -15
- package/src/camel/camel-turn.ts +1 -17
- package/src/composer/composer-attachments.tsx +1 -1
- package/src/composer/composer-trigger-popover.tsx +1 -1
- package/src/composer/composer.tsx +2 -2
- package/src/contracts.ts +0 -2
- package/src/message-panel.tsx +1 -24
- package/src/parts/index.tsx +103 -64
|
@@ -1,24 +1,43 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Brain,
|
|
3
|
-
CaretDown,
|
|
4
|
-
Check,
|
|
5
3
|
File as FileIcon,
|
|
6
4
|
Plug,
|
|
7
5
|
Plus,
|
|
8
|
-
Terminal,
|
|
9
|
-
X,
|
|
10
6
|
} from "@phosphor-icons/react";
|
|
11
7
|
import {
|
|
8
|
+
AssistantRuntimeProvider,
|
|
9
|
+
ComposerPrimitive,
|
|
10
|
+
unstable_useSlashCommandAdapter,
|
|
11
|
+
unstable_useTriggerPopoverAriaProps,
|
|
12
|
+
unstable_useTriggerPopoverTriggers,
|
|
13
|
+
useExternalStoreRuntime,
|
|
14
|
+
type AssistantRuntime,
|
|
15
|
+
type ThreadMessage,
|
|
16
|
+
} from "@assistant-ui/react";
|
|
17
|
+
import { ComposerTriggerPopover } from "@springbrand/message-panel/composer";
|
|
18
|
+
import {
|
|
19
|
+
Fragment,
|
|
12
20
|
useCallback,
|
|
13
21
|
useEffect,
|
|
22
|
+
useMemo,
|
|
14
23
|
useRef,
|
|
15
24
|
useState,
|
|
16
25
|
type DragEvent as ReactDragEvent,
|
|
26
|
+
type Ref,
|
|
17
27
|
type ReactNode,
|
|
18
28
|
} from "react";
|
|
19
29
|
import { DropdownMenu } from "../primitives/dropdown-menu";
|
|
20
30
|
import { Tooltip } from "../primitives/tooltip";
|
|
21
31
|
import { WorkshopIconButton } from "../primitives/workshop-controls";
|
|
32
|
+
import { CapabilityChip } from "../capability-chip";
|
|
33
|
+
import {
|
|
34
|
+
renderFileView,
|
|
35
|
+
type FileRenderer,
|
|
36
|
+
} from "../file-view";
|
|
37
|
+
import {
|
|
38
|
+
CloudOsModelSelect,
|
|
39
|
+
type CloudOsModelOption,
|
|
40
|
+
} from "./cloud-os-model-select";
|
|
22
41
|
|
|
23
42
|
const isComposingKeyEvent = (event: {
|
|
24
43
|
isComposing?: boolean;
|
|
@@ -32,21 +51,22 @@ const isComposingKeyEvent = (event: {
|
|
|
32
51
|
* 附件缩略图行、底部左「+ / 添加资源」右「模型 / 发送-停止」的布局、
|
|
33
52
|
* draftUpdateBanner 插槽、blockedReason 占位文案、Enter 发送 / Shift+Enter 换行。
|
|
34
53
|
*
|
|
35
|
-
* 删掉:capsule(URL → 资源胶囊)、
|
|
36
|
-
*
|
|
37
|
-
*
|
|
54
|
+
* 删掉:capsule(URL → 资源胶囊)、ComposerMirror、GatekeeperModal —— 这三样都直挂
|
|
55
|
+
* gadgets 的 capnweb RPC,在 UIMessage 协议下没有对应物。「添加资源」按钮保留成一个
|
|
56
|
+
* 可选回调,宿主不给就不渲染。Command picker 使用 package 内的
|
|
57
|
+
* assistant-ui ComposerTriggerPopover。
|
|
38
58
|
*/
|
|
39
59
|
|
|
40
|
-
export interface
|
|
41
|
-
|
|
60
|
+
export interface CloudOsComposerCommand {
|
|
61
|
+
prefix?: "/" | "$";
|
|
42
62
|
name: string;
|
|
63
|
+
description: string;
|
|
43
64
|
}
|
|
44
65
|
|
|
45
|
-
export interface
|
|
66
|
+
export interface CloudOsCapabilityView {
|
|
67
|
+
kind: "skill" | "tool" | "plan";
|
|
46
68
|
name: string;
|
|
47
|
-
|
|
48
|
-
/** 选中后填进输入框的起手文本。 */
|
|
49
|
-
prompt: string;
|
|
69
|
+
label: string;
|
|
50
70
|
}
|
|
51
71
|
|
|
52
72
|
export interface CloudOsAttachmentView {
|
|
@@ -55,6 +75,7 @@ export interface CloudOsAttachmentView {
|
|
|
55
75
|
mediaType?: string;
|
|
56
76
|
size?: number;
|
|
57
77
|
previewUrl?: string;
|
|
78
|
+
progress?: number;
|
|
58
79
|
status: "uploading" | "ready" | "error";
|
|
59
80
|
}
|
|
60
81
|
|
|
@@ -71,6 +92,8 @@ export interface CloudOsChatInputProps {
|
|
|
71
92
|
onFilesSelected?: (files: readonly File[]) => void;
|
|
72
93
|
onAttachmentRemove?: (id: string) => void;
|
|
73
94
|
onAttachmentRetry?: (id: string) => void;
|
|
95
|
+
/** 接管输入框附件视图;未提供时使用包内 FileView。 */
|
|
96
|
+
renderFile?: FileRenderer;
|
|
74
97
|
/** 底部左侧的额外控件(宿主的 Agent 档位设置等)。 */
|
|
75
98
|
tools?: ReactNode;
|
|
76
99
|
/** 发送键左边的额外控件(宿主的「引导 / 排队」投递方式)。 */
|
|
@@ -82,11 +105,11 @@ export interface CloudOsChatInputProps {
|
|
|
82
105
|
* 传了就以它为准来决定显示发送键还是停止键。
|
|
83
106
|
*/
|
|
84
107
|
turnActive?: boolean;
|
|
85
|
-
/**
|
|
86
|
-
* 可用的斜杠命令。cloudflare-os 原版是一个直连 RPC 的行内 picker;这里退成
|
|
87
|
-
* 「+」菜单里的一组条目 —— 能力保住,但**没有**边打字边过滤的行内补全。
|
|
88
|
-
*/
|
|
108
|
+
/** 可用的斜杠命令。 */
|
|
89
109
|
commands?: readonly CloudOsComposerCommand[];
|
|
110
|
+
capabilities?: readonly CloudOsCapabilityView[];
|
|
111
|
+
onCommandSelect?: (command: CloudOsComposerCommand) => void;
|
|
112
|
+
onCapabilityRemove?: (kind: CloudOsCapabilityView["kind"], name: string) => void;
|
|
90
113
|
/** 有值时渲染「添加资源」按钮。 */
|
|
91
114
|
onAttachResource?: () => void;
|
|
92
115
|
attachLabel?: string;
|
|
@@ -100,11 +123,17 @@ export interface CloudOsChatInputProps {
|
|
|
100
123
|
minRows?: number;
|
|
101
124
|
maxRows?: number;
|
|
102
125
|
autoFocus?: boolean;
|
|
126
|
+
/** Gives the host direct access to the chat textarea. */
|
|
127
|
+
textareaRef?: Ref<HTMLTextAreaElement>;
|
|
103
128
|
/** 输入框下方一行(原文件放 token / cost)。 */
|
|
104
129
|
footnote?: ReactNode;
|
|
105
130
|
}
|
|
106
131
|
|
|
107
|
-
const MAX_PENDING_ATTACHMENTS =
|
|
132
|
+
const MAX_PENDING_ATTACHMENTS = 8;
|
|
133
|
+
|
|
134
|
+
function commandLabel(command: CloudOsComposerCommand): string {
|
|
135
|
+
return `${command.prefix ?? "/"}${command.name.replaceAll("-", " ")}`;
|
|
136
|
+
}
|
|
108
137
|
|
|
109
138
|
function autoResizeTextarea(
|
|
110
139
|
textarea: HTMLTextAreaElement,
|
|
@@ -123,7 +152,7 @@ function autoResizeTextarea(
|
|
|
123
152
|
textarea.style.overflow = textarea.scrollHeight > maxH ? "auto" : "hidden";
|
|
124
153
|
}
|
|
125
154
|
|
|
126
|
-
|
|
155
|
+
function CloudOsChatInputContent({
|
|
127
156
|
value,
|
|
128
157
|
onChange,
|
|
129
158
|
onSubmit,
|
|
@@ -136,11 +165,15 @@ export function CloudOsChatInput({
|
|
|
136
165
|
onFilesSelected,
|
|
137
166
|
onAttachmentRemove,
|
|
138
167
|
onAttachmentRetry,
|
|
168
|
+
renderFile,
|
|
139
169
|
tools,
|
|
140
170
|
submitControl,
|
|
141
171
|
submissionBlocked = false,
|
|
142
172
|
turnActive,
|
|
143
173
|
commands = [],
|
|
174
|
+
capabilities = [],
|
|
175
|
+
onCommandSelect,
|
|
176
|
+
onCapabilityRemove,
|
|
144
177
|
onAttachResource,
|
|
145
178
|
attachLabel,
|
|
146
179
|
showThinkingTraces,
|
|
@@ -151,8 +184,10 @@ export function CloudOsChatInput({
|
|
|
151
184
|
minRows = 1,
|
|
152
185
|
maxRows = 6,
|
|
153
186
|
autoFocus = false,
|
|
187
|
+
textareaRef: forwardedTextareaRef,
|
|
154
188
|
footnote,
|
|
155
|
-
|
|
189
|
+
runtime,
|
|
190
|
+
}: CloudOsChatInputProps & { runtime: AssistantRuntime }) {
|
|
156
191
|
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
|
157
192
|
const attachmentInputRef = useRef<HTMLInputElement>(null);
|
|
158
193
|
const dragDepthRef = useRef(0);
|
|
@@ -179,16 +214,78 @@ export function CloudOsChatInput({
|
|
|
179
214
|
(value.trim().length > 0 || hasReadyAttachment) &&
|
|
180
215
|
!hasUnreadyAttachment;
|
|
181
216
|
|
|
182
|
-
const selectedModelLabel =
|
|
183
|
-
selectedModel === null
|
|
184
|
-
? "No agent"
|
|
185
|
-
: (models.find((model) => model.id === selectedModel)?.name ?? selectedModel);
|
|
186
|
-
|
|
187
217
|
useEffect(() => {
|
|
188
218
|
const element = textareaRef.current;
|
|
189
219
|
if (element) autoResizeTextarea(element, minRows, maxRows);
|
|
190
220
|
}, [maxRows, minRows, value]);
|
|
191
221
|
|
|
222
|
+
const selectCommand = useCallback((
|
|
223
|
+
command: CloudOsComposerCommand,
|
|
224
|
+
clearTrigger: boolean,
|
|
225
|
+
) => {
|
|
226
|
+
onCommandSelect?.(command);
|
|
227
|
+
if (clearTrigger) {
|
|
228
|
+
runtime.thread.composer.setText("");
|
|
229
|
+
onChange("");
|
|
230
|
+
}
|
|
231
|
+
requestAnimationFrame(() => textareaRef.current?.focus());
|
|
232
|
+
}, [onChange, onCommandSelect, runtime]);
|
|
233
|
+
|
|
234
|
+
const slashCommands = useMemo(
|
|
235
|
+
() => commands
|
|
236
|
+
.filter((command) => (command.prefix ?? "/") === "/")
|
|
237
|
+
.map((command) => ({
|
|
238
|
+
id: command.name,
|
|
239
|
+
label: commandLabel(command),
|
|
240
|
+
description: command.description,
|
|
241
|
+
execute: () => selectCommand(command, true),
|
|
242
|
+
})),
|
|
243
|
+
[commands, selectCommand],
|
|
244
|
+
);
|
|
245
|
+
const skillCommands = useMemo(
|
|
246
|
+
() => commands
|
|
247
|
+
.filter((command) => command.prefix === "$")
|
|
248
|
+
.map((command) => ({
|
|
249
|
+
id: command.name,
|
|
250
|
+
label: commandLabel(command),
|
|
251
|
+
description: command.description,
|
|
252
|
+
execute: () => selectCommand(command, true),
|
|
253
|
+
})),
|
|
254
|
+
[commands, selectCommand],
|
|
255
|
+
);
|
|
256
|
+
const slashAdapter = unstable_useSlashCommandAdapter({
|
|
257
|
+
commands: slashCommands,
|
|
258
|
+
removeOnExecute: true,
|
|
259
|
+
});
|
|
260
|
+
const skillAdapter = unstable_useSlashCommandAdapter({
|
|
261
|
+
commands: skillCommands,
|
|
262
|
+
removeOnExecute: true,
|
|
263
|
+
});
|
|
264
|
+
// assistant-ui only re-runs a TriggerPopover search when the adapter changes.
|
|
265
|
+
const slash = useMemo(() => ({
|
|
266
|
+
...slashAdapter,
|
|
267
|
+
adapter: { ...slashAdapter.adapter },
|
|
268
|
+
}), [slashAdapter, slashCommands]);
|
|
269
|
+
const skills = useMemo(() => ({
|
|
270
|
+
...skillAdapter,
|
|
271
|
+
adapter: { ...skillAdapter.adapter },
|
|
272
|
+
}), [skillAdapter, skillCommands]);
|
|
273
|
+
const triggers = unstable_useTriggerPopoverTriggers();
|
|
274
|
+
const triggerAria = unstable_useTriggerPopoverAriaProps();
|
|
275
|
+
|
|
276
|
+
const setTriggerCursor = useCallback(
|
|
277
|
+
(position: number) => {
|
|
278
|
+
for (const trigger of triggers.values()) {
|
|
279
|
+
trigger.resource.setCursorPosition(position);
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
[triggers],
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
useEffect(() => {
|
|
286
|
+
setTriggerCursor(textareaRef.current?.selectionStart ?? value.length);
|
|
287
|
+
}, [commands, setTriggerCursor, value]);
|
|
288
|
+
|
|
192
289
|
const addFiles = useCallback(
|
|
193
290
|
(files: readonly File[]) => {
|
|
194
291
|
if (files.length === 0) return;
|
|
@@ -224,6 +321,20 @@ export function CloudOsChatInput({
|
|
|
224
321
|
return (
|
|
225
322
|
// isolation: isolate 把输入框内部的 z-index 关起来,免得盖到 portal 出去的模型下拉。
|
|
226
323
|
<div className="cos-chat-input-root relative isolate px-4 py-4">
|
|
324
|
+
<ComposerTriggerPopover
|
|
325
|
+
char="/"
|
|
326
|
+
{...slash}
|
|
327
|
+
aria-label="Commands"
|
|
328
|
+
emptyItemsLabel="No matching commands"
|
|
329
|
+
className="!w-72 !border-kumo-line/70 !bg-kumo-base !text-kumo-default themed-floating-shadow-lg"
|
|
330
|
+
/>
|
|
331
|
+
<ComposerTriggerPopover
|
|
332
|
+
char="$"
|
|
333
|
+
{...skills}
|
|
334
|
+
aria-label="Skills"
|
|
335
|
+
emptyItemsLabel="No matching skills"
|
|
336
|
+
className="!w-72 !border-kumo-line/70 !bg-kumo-base !text-kumo-default themed-floating-shadow-lg"
|
|
337
|
+
/>
|
|
227
338
|
<input
|
|
228
339
|
ref={attachmentInputRef}
|
|
229
340
|
type="file"
|
|
@@ -276,10 +387,30 @@ export function CloudOsChatInput({
|
|
|
276
387
|
|
|
277
388
|
{banner}
|
|
278
389
|
|
|
279
|
-
<div className="relative px-4 pb-1 pt-3">
|
|
390
|
+
<div className="relative flex flex-nowrap items-center gap-2 px-4 pb-1 pt-3">
|
|
391
|
+
{capabilities.length > 0 && (
|
|
392
|
+
<div className="flex max-w-[65%] shrink-0 flex-nowrap gap-1.5 overflow-x-auto">
|
|
393
|
+
{capabilities.map((capability) => (
|
|
394
|
+
<CapabilityChip
|
|
395
|
+
key={`${capability.kind}:${capability.name}`}
|
|
396
|
+
kind={capability.kind}
|
|
397
|
+
label={capability.label}
|
|
398
|
+
removable
|
|
399
|
+
onRemove={() =>
|
|
400
|
+
onCapabilityRemove?.(capability.kind, capability.name)
|
|
401
|
+
}
|
|
402
|
+
/>
|
|
403
|
+
))}
|
|
404
|
+
</div>
|
|
405
|
+
)}
|
|
280
406
|
<textarea
|
|
281
407
|
ref={(element) => {
|
|
282
408
|
textareaRef.current = element;
|
|
409
|
+
if (typeof forwardedTextareaRef === "function") {
|
|
410
|
+
forwardedTextareaRef(element);
|
|
411
|
+
} else if (forwardedTextareaRef) {
|
|
412
|
+
forwardedTextareaRef.current = element;
|
|
413
|
+
}
|
|
283
414
|
if (element) autoResizeTextarea(element, minRows, maxRows);
|
|
284
415
|
}}
|
|
285
416
|
value={value}
|
|
@@ -294,9 +425,17 @@ export function CloudOsChatInput({
|
|
|
294
425
|
: (placeholder ?? "Ask a follow-up…")
|
|
295
426
|
}
|
|
296
427
|
onChange={(event) => {
|
|
297
|
-
|
|
428
|
+
const next = event.target.value;
|
|
429
|
+
runtime.thread.composer.setText(next);
|
|
430
|
+
onChange(next);
|
|
431
|
+
setTriggerCursor(event.target.selectionStart ?? next.length);
|
|
298
432
|
autoResizeTextarea(event.target, minRows, maxRows);
|
|
299
433
|
}}
|
|
434
|
+
onSelect={(event) =>
|
|
435
|
+
setTriggerCursor(
|
|
436
|
+
event.currentTarget.selectionStart ?? value.length,
|
|
437
|
+
)
|
|
438
|
+
}
|
|
300
439
|
onPaste={(event) => {
|
|
301
440
|
const files = Array.from(event.clipboardData.items)
|
|
302
441
|
.filter((item) => item.kind === "file")
|
|
@@ -308,6 +447,9 @@ export function CloudOsChatInput({
|
|
|
308
447
|
}
|
|
309
448
|
}}
|
|
310
449
|
onKeyDown={(event) => {
|
|
450
|
+
for (const trigger of triggers.values()) {
|
|
451
|
+
if (trigger.resource.handleKeyDown(event)) return;
|
|
452
|
+
}
|
|
311
453
|
// Enter 发送(按住 Shift 换行); IME 组合中不发送
|
|
312
454
|
if (
|
|
313
455
|
event.key === "Enter" &&
|
|
@@ -318,55 +460,33 @@ export function CloudOsChatInput({
|
|
|
318
460
|
if (canSend) onSubmit();
|
|
319
461
|
}
|
|
320
462
|
}}
|
|
321
|
-
className="relative z-[1] w-
|
|
463
|
+
className="relative z-[1] min-w-0 flex-1 resize-none border-none bg-transparent p-0 text-[14px] leading-[22px] tracking-[-0.25px] text-kumo-default outline-none placeholder:text-kumo-inactive disabled:cursor-not-allowed"
|
|
464
|
+
{...triggerAria}
|
|
322
465
|
/>
|
|
323
466
|
</div>
|
|
324
467
|
|
|
325
468
|
{attachments.length > 0 && (
|
|
326
469
|
<div className="flex flex-wrap items-center gap-2 px-3 pb-2 pt-1">
|
|
327
470
|
{attachments.map((attachment) => (
|
|
328
|
-
<
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
)}
|
|
346
|
-
|
|
347
|
-
<div className="absolute inset-0 grid place-items-center rounded-lg bg-kumo-danger/80 px-1 text-center text-[9px] leading-3 text-white">
|
|
348
|
-
Failed
|
|
349
|
-
</div>
|
|
350
|
-
)}
|
|
351
|
-
{attachment.status === "error" && onAttachmentRetry && (
|
|
352
|
-
<button
|
|
353
|
-
type="button"
|
|
354
|
-
aria-label="Retry upload"
|
|
355
|
-
onClick={() => onAttachmentRetry(attachment.id)}
|
|
356
|
-
className="absolute inset-0 cursor-pointer"
|
|
357
|
-
/>
|
|
358
|
-
)}
|
|
359
|
-
{onAttachmentRemove && (
|
|
360
|
-
<button
|
|
361
|
-
type="button"
|
|
362
|
-
aria-label="Remove attachment"
|
|
363
|
-
onClick={() => onAttachmentRemove(attachment.id)}
|
|
364
|
-
className="absolute right-0.5 top-0.5 flex h-4 w-4 cursor-pointer items-center justify-center rounded-full bg-black/55 text-white hover:bg-black/75 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/80"
|
|
365
|
-
>
|
|
366
|
-
<X size={10} weight="bold" />
|
|
367
|
-
</button>
|
|
368
|
-
)}
|
|
369
|
-
</div>
|
|
471
|
+
<Fragment key={attachment.id}>
|
|
472
|
+
{renderFileView(renderFile, {
|
|
473
|
+
file: {
|
|
474
|
+
url: attachment.previewUrl,
|
|
475
|
+
filename: attachment.filename,
|
|
476
|
+
mediaType: attachment.mediaType,
|
|
477
|
+
size: attachment.size,
|
|
478
|
+
},
|
|
479
|
+
placement: "composer",
|
|
480
|
+
status: attachment.status,
|
|
481
|
+
progress: attachment.progress,
|
|
482
|
+
onRemove: onAttachmentRemove
|
|
483
|
+
? () => onAttachmentRemove(attachment.id)
|
|
484
|
+
: undefined,
|
|
485
|
+
onRetry: onAttachmentRetry
|
|
486
|
+
? () => onAttachmentRetry(attachment.id)
|
|
487
|
+
: undefined,
|
|
488
|
+
})}
|
|
489
|
+
</Fragment>
|
|
370
490
|
))}
|
|
371
491
|
</div>
|
|
372
492
|
)}
|
|
@@ -400,30 +520,6 @@ export function CloudOsChatInput({
|
|
|
400
520
|
</span>
|
|
401
521
|
</DropdownMenu.Item>
|
|
402
522
|
)}
|
|
403
|
-
{commands.length > 0 && (
|
|
404
|
-
<>
|
|
405
|
-
{commands.map((command) => (
|
|
406
|
-
<DropdownMenu.Item
|
|
407
|
-
key={command.name}
|
|
408
|
-
onClick={() => {
|
|
409
|
-
onChange(command.prompt);
|
|
410
|
-
requestAnimationFrame(() =>
|
|
411
|
-
textareaRef.current?.focus(),
|
|
412
|
-
);
|
|
413
|
-
}}
|
|
414
|
-
className="!h-auto rounded-xl !px-2 !py-1.5 text-[12px] leading-4 font-normal tracking-[-0.15px] text-kumo-subtle transition-colors data-highlighted:bg-kumo-tint/70 data-highlighted:text-kumo-default"
|
|
415
|
-
>
|
|
416
|
-
<span className="mr-2 inline-flex h-4 w-4 items-center justify-center text-kumo-inactive">
|
|
417
|
-
<Terminal size={14} />
|
|
418
|
-
</span>
|
|
419
|
-
<span className="min-w-0 flex-1 truncate">
|
|
420
|
-
/{command.name}
|
|
421
|
-
</span>
|
|
422
|
-
</DropdownMenu.Item>
|
|
423
|
-
))}
|
|
424
|
-
<div className="my-1 border-t border-kumo-line/70" />
|
|
425
|
-
</>
|
|
426
|
-
)}
|
|
427
523
|
{onFilesSelected && (
|
|
428
524
|
<DropdownMenu.Item
|
|
429
525
|
onClick={() => attachmentInputRef.current?.click()}
|
|
@@ -454,45 +550,12 @@ export function CloudOsChatInput({
|
|
|
454
550
|
|
|
455
551
|
<div className="ml-auto flex min-w-0 flex-shrink items-center gap-1.5">
|
|
456
552
|
{onModelChange && (
|
|
457
|
-
<
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
aria-label="Select model"
|
|
464
|
-
>
|
|
465
|
-
<span className="min-w-0 truncate">{selectedModelLabel}</span>
|
|
466
|
-
<CaretDown
|
|
467
|
-
size={12}
|
|
468
|
-
weight="bold"
|
|
469
|
-
className="flex-shrink-0 text-kumo-inactive transition-transform duration-150 ease-out group-data-[state=open]:rotate-180"
|
|
470
|
-
/>
|
|
471
|
-
</button>
|
|
472
|
-
}
|
|
473
|
-
/>
|
|
474
|
-
<DropdownMenu.Content
|
|
475
|
-
align="end"
|
|
476
|
-
className="themed-floating-shadow-lg !min-w-[190px] rounded-2xl border border-kumo-line/70 bg-kumo-base p-1"
|
|
477
|
-
>
|
|
478
|
-
{models.map((model) => (
|
|
479
|
-
<DropdownMenu.Item
|
|
480
|
-
key={model.id}
|
|
481
|
-
onClick={() => onModelChange(model.id)}
|
|
482
|
-
className="!h-auto rounded-xl !px-2 !py-1.5 text-[12px] leading-4 font-normal tracking-[-0.15px] text-kumo-subtle transition-colors data-highlighted:bg-kumo-tint/70 data-highlighted:text-kumo-default"
|
|
483
|
-
>
|
|
484
|
-
<span className="min-w-0 flex-1 truncate">{model.name}</span>
|
|
485
|
-
{selectedModel === model.id && (
|
|
486
|
-
<Check
|
|
487
|
-
size={12}
|
|
488
|
-
weight="bold"
|
|
489
|
-
className="ml-3 flex-shrink-0 text-kumo-inactive"
|
|
490
|
-
/>
|
|
491
|
-
)}
|
|
492
|
-
</DropdownMenu.Item>
|
|
493
|
-
))}
|
|
494
|
-
</DropdownMenu.Content>
|
|
495
|
-
</DropdownMenu>
|
|
553
|
+
<CloudOsModelSelect
|
|
554
|
+
models={models}
|
|
555
|
+
selectedModel={selectedModel}
|
|
556
|
+
onModelChange={onModelChange}
|
|
557
|
+
variant="cloud-os"
|
|
558
|
+
/>
|
|
496
559
|
)}
|
|
497
560
|
{!showStopButton && submitControl}
|
|
498
561
|
{showStopButton ? (
|
|
@@ -507,25 +570,27 @@ export function CloudOsChatInput({
|
|
|
507
570
|
</svg>
|
|
508
571
|
</WorkshopIconButton>
|
|
509
572
|
) : (
|
|
510
|
-
<
|
|
573
|
+
<button
|
|
574
|
+
type="button"
|
|
511
575
|
onClick={onSubmit}
|
|
512
576
|
disabled={!canSend}
|
|
513
|
-
|
|
514
|
-
className="!h-8 !w-8 disabled:cursor-not-allowed disabled:opacity-30"
|
|
577
|
+
className="grid size-8 shrink-0 place-items-center rounded-full bg-[var(--primary,var(--color-kumo-brand))] text-[var(--primary-foreground,var(--text-color-kumo-inverse))] shadow-sm transition-colors enabled:hover:bg-[var(--primary-hover,var(--color-kumo-brand-hover))] disabled:cursor-not-allowed disabled:opacity-20"
|
|
515
578
|
aria-label="Send message"
|
|
516
579
|
>
|
|
517
580
|
<svg
|
|
518
|
-
|
|
519
|
-
height="16"
|
|
581
|
+
className="size-4"
|
|
520
582
|
viewBox="0 0 24 24"
|
|
521
583
|
fill="none"
|
|
522
584
|
stroke="currentColor"
|
|
523
|
-
strokeWidth="
|
|
585
|
+
strokeWidth="1.8"
|
|
586
|
+
strokeLinecap="round"
|
|
587
|
+
strokeLinejoin="round"
|
|
588
|
+
aria-hidden="true"
|
|
524
589
|
>
|
|
525
|
-
<
|
|
526
|
-
<
|
|
590
|
+
<path d="m5 12 7-7 7 7" />
|
|
591
|
+
<path d="M12 19V5" />
|
|
527
592
|
</svg>
|
|
528
|
-
</
|
|
593
|
+
</button>
|
|
529
594
|
)}
|
|
530
595
|
</div>
|
|
531
596
|
</div>
|
|
@@ -539,3 +604,24 @@ export function CloudOsChatInput({
|
|
|
539
604
|
</div>
|
|
540
605
|
);
|
|
541
606
|
}
|
|
607
|
+
|
|
608
|
+
const EMPTY_MESSAGES: readonly ThreadMessage[] = [];
|
|
609
|
+
|
|
610
|
+
export function CloudOsChatInput(props: CloudOsChatInputProps) {
|
|
611
|
+
const runtime = useExternalStoreRuntime<ThreadMessage>({
|
|
612
|
+
messages: EMPTY_MESSAGES,
|
|
613
|
+
onNew: async () => undefined,
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
useEffect(() => {
|
|
617
|
+
runtime.thread.composer.setText(props.value);
|
|
618
|
+
}, [props.value, runtime]);
|
|
619
|
+
|
|
620
|
+
return (
|
|
621
|
+
<AssistantRuntimeProvider runtime={runtime}>
|
|
622
|
+
<ComposerPrimitive.Unstable_TriggerPopoverRoot>
|
|
623
|
+
<CloudOsChatInputContent {...props} runtime={runtime} />
|
|
624
|
+
</ComposerPrimitive.Unstable_TriggerPopoverRoot>
|
|
625
|
+
</AssistantRuntimeProvider>
|
|
626
|
+
);
|
|
627
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { CaretDown } from "@phosphor-icons/react";
|
|
2
|
+
import { cn } from "../internal/cn";
|
|
3
|
+
import { DropdownMenu } from "../primitives/dropdown-menu";
|
|
4
|
+
|
|
5
|
+
const selectedIcon = new URL("../assets/model-selected.svg", import.meta.url).href;
|
|
6
|
+
|
|
7
|
+
export interface CloudOsModelOption {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
disabledLabel?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface CloudOsModelSelectProps {
|
|
15
|
+
models: readonly CloudOsModelOption[];
|
|
16
|
+
selectedModel?: string | null;
|
|
17
|
+
onModelChange: (modelId: string) => void;
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
variant?: "default" | "cloud-os";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function CloudOsModelSelect({
|
|
23
|
+
models,
|
|
24
|
+
selectedModel = null,
|
|
25
|
+
onModelChange,
|
|
26
|
+
disabled = false,
|
|
27
|
+
variant = "default",
|
|
28
|
+
}: CloudOsModelSelectProps) {
|
|
29
|
+
if (models.length === 0) return null;
|
|
30
|
+
|
|
31
|
+
const selectedLabel =
|
|
32
|
+
models.find((model) => model.id === selectedModel)?.name ?? "Model";
|
|
33
|
+
const cloudOs = variant === "cloud-os";
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<DropdownMenu>
|
|
37
|
+
<DropdownMenu.Trigger
|
|
38
|
+
render={
|
|
39
|
+
<button
|
|
40
|
+
type="button"
|
|
41
|
+
disabled={disabled}
|
|
42
|
+
aria-label="Select model"
|
|
43
|
+
className={cn(
|
|
44
|
+
"group inline-flex h-8 min-w-0 items-center gap-1.5 rounded-lg transition-[background-color,color,transform] duration-150 ease-out focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-40",
|
|
45
|
+
cloudOs
|
|
46
|
+
? "max-w-[180px] cursor-pointer px-2 text-[13px] leading-5 tracking-[-0.25px] text-kumo-subtle hover:bg-kumo-tint hover:text-kumo-default focus-visible:bg-kumo-tint focus-visible:text-kumo-default active:scale-[0.97] data-[state=open]:bg-kumo-tint data-[state=open]:text-kumo-default"
|
|
47
|
+
: "max-w-[min(11rem,38vw)] px-2.5 text-xs leading-none text-black/55 hover:bg-black/5 hover:text-black focus-visible:ring-2 focus-visible:ring-black/15",
|
|
48
|
+
)}
|
|
49
|
+
>
|
|
50
|
+
<span className="min-w-0 truncate">{selectedLabel}</span>
|
|
51
|
+
<CaretDown
|
|
52
|
+
size={12}
|
|
53
|
+
weight="bold"
|
|
54
|
+
aria-hidden="true"
|
|
55
|
+
className={cn(
|
|
56
|
+
"shrink-0 transition-transform duration-150 ease-out group-data-[state=open]:rotate-180",
|
|
57
|
+
cloudOs ? "text-kumo-inactive" : "text-black/55",
|
|
58
|
+
)}
|
|
59
|
+
/>
|
|
60
|
+
</button>
|
|
61
|
+
}
|
|
62
|
+
/>
|
|
63
|
+
<DropdownMenu.Content
|
|
64
|
+
align="end"
|
|
65
|
+
side="top"
|
|
66
|
+
sideOffset={8}
|
|
67
|
+
data-slot="model-select-content"
|
|
68
|
+
className="cos-model-select min-w-[12.5rem] rounded-lg border p-2"
|
|
69
|
+
>
|
|
70
|
+
{models.map((model) => (
|
|
71
|
+
<DropdownMenu.Item
|
|
72
|
+
key={model.id}
|
|
73
|
+
disabled={model.disabled}
|
|
74
|
+
onClick={model.disabled ? undefined : () => onModelChange(model.id)}
|
|
75
|
+
data-slot="model-select-item"
|
|
76
|
+
className="cos-model-select__item gap-2 rounded px-2 py-2.5 text-[14px] leading-[1.4] font-normal transition-colors"
|
|
77
|
+
>
|
|
78
|
+
<span
|
|
79
|
+
className="cos-model-select__name min-w-0 flex-1 truncate"
|
|
80
|
+
data-muted={selectedModel === model.id || model.disabled ? "" : undefined}
|
|
81
|
+
>
|
|
82
|
+
{model.name}
|
|
83
|
+
</span>
|
|
84
|
+
{model.disabledLabel ? (
|
|
85
|
+
<span
|
|
86
|
+
className="cos-model-select__plan min-w-0 flex-1 truncate text-right"
|
|
87
|
+
data-disabled={model.disabled ? "" : undefined}
|
|
88
|
+
>
|
|
89
|
+
{model.disabledLabel}
|
|
90
|
+
</span>
|
|
91
|
+
) : selectedModel === model.id ? (
|
|
92
|
+
<img
|
|
93
|
+
src={selectedIcon}
|
|
94
|
+
alt=""
|
|
95
|
+
aria-hidden="true"
|
|
96
|
+
data-slot="model-selected-icon"
|
|
97
|
+
className="size-4 shrink-0"
|
|
98
|
+
/>
|
|
99
|
+
) : null}
|
|
100
|
+
</DropdownMenu.Item>
|
|
101
|
+
))}
|
|
102
|
+
</DropdownMenu.Content>
|
|
103
|
+
</DropdownMenu>
|
|
104
|
+
);
|
|
105
|
+
}
|