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