@springbrand/message-panel 0.1.3-alpha.1 → 0.1.3-alpha.3
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 +71 -0
- package/cloud-os/chat/activity-indicator.tsx +29 -0
- package/cloud-os/chat/cloud-os-chat-messages.tsx +632 -0
- package/cloud-os/chat/markdown-message.tsx +78 -0
- package/cloud-os/chat/rich-blocks.tsx +672 -0
- package/cloud-os/chat/tool-presentation.ts +584 -0
- package/cloud-os/chat/tool-rows.tsx +216 -0
- package/cloud-os/chat/transcript-model.ts +672 -0
- package/cloud-os/composer/cloud-os-chat-input.tsx +541 -0
- package/cloud-os/index.ts +107 -0
- package/cloud-os/internal/cn.ts +6 -0
- package/cloud-os/internal/theme-context.tsx +16 -0
- package/cloud-os/layout/cloud-os-root.tsx +34 -0
- package/cloud-os/layout/cloud-os-workspace-split.tsx +242 -0
- package/cloud-os/primitives/dropdown-menu.tsx +82 -0
- package/cloud-os/primitives/tooltip.tsx +68 -0
- package/cloud-os/primitives/workshop-controls.tsx +114 -0
- package/cloud-os/styles/cloud-os.css +573 -0
- package/cloud-os/workspace/cloud-os-workspace-panel.tsx +272 -0
- package/demo/camel-chat-showcase.tsx +13 -917
- package/demo/chat-scenarios.ts +926 -0
- package/demo/cloud-os-chat-showcase.tsx +471 -0
- package/demo/index.ts +2 -0
- package/package.json +16 -4
- package/src/camel/camel-chat-messages.tsx +41 -18
- package/src/camel/camel-prompt-input.tsx +2 -1
- package/src/chat-summary-panel.tsx +1 -1
- package/src/composer/chat-composer.tsx +2 -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/message.tsx +0 -8
- package/src/styles/index.css +4 -1
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Brain,
|
|
3
|
+
CaretDown,
|
|
4
|
+
Check,
|
|
5
|
+
File as FileIcon,
|
|
6
|
+
Plug,
|
|
7
|
+
Plus,
|
|
8
|
+
Terminal,
|
|
9
|
+
X,
|
|
10
|
+
} from "@phosphor-icons/react";
|
|
11
|
+
import {
|
|
12
|
+
useCallback,
|
|
13
|
+
useEffect,
|
|
14
|
+
useRef,
|
|
15
|
+
useState,
|
|
16
|
+
type DragEvent as ReactDragEvent,
|
|
17
|
+
type ReactNode,
|
|
18
|
+
} from "react";
|
|
19
|
+
import { DropdownMenu } from "../primitives/dropdown-menu";
|
|
20
|
+
import { Tooltip } from "../primitives/tooltip";
|
|
21
|
+
import { WorkshopIconButton } from "../primitives/workshop-controls";
|
|
22
|
+
|
|
23
|
+
const isComposingKeyEvent = (event: {
|
|
24
|
+
isComposing?: boolean;
|
|
25
|
+
keyCode?: number;
|
|
26
|
+
}) => event.isComposing === true || event.keyCode === 229;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 从 cloudflare-os-main `ChatInterface.tsx` 的 `ChatInput` 拷来。
|
|
30
|
+
*
|
|
31
|
+
* 保留:prompt card 的形与投影、自动增高的 textarea、拖拽落文件的整卡覆盖层、
|
|
32
|
+
* 附件缩略图行、底部左「+ / 添加资源」右「模型 / 发送-停止」的布局、
|
|
33
|
+
* draftUpdateBanner 插槽、blockedReason 占位文案、Enter 发送 / Shift+Enter 换行。
|
|
34
|
+
*
|
|
35
|
+
* 删掉:capsule(URL → 资源胶囊)、slash command picker、ComposerMirror、
|
|
36
|
+
* GatekeeperModal —— 这四样都直挂 gadgets 的 capnweb RPC,在 UIMessage 协议下
|
|
37
|
+
* 没有对应物。「添加资源」按钮保留成一个可选回调,宿主不给就不渲染。
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
export interface CloudOsModelOption {
|
|
41
|
+
id: string;
|
|
42
|
+
name: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface CloudOsComposerCommand {
|
|
46
|
+
name: string;
|
|
47
|
+
description: string;
|
|
48
|
+
/** 选中后填进输入框的起手文本。 */
|
|
49
|
+
prompt: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface CloudOsAttachmentView {
|
|
53
|
+
id: string;
|
|
54
|
+
filename?: string;
|
|
55
|
+
mediaType?: string;
|
|
56
|
+
size?: number;
|
|
57
|
+
previewUrl?: string;
|
|
58
|
+
status: "uploading" | "ready" | "error";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface CloudOsChatInputProps {
|
|
62
|
+
value: string;
|
|
63
|
+
onChange: (value: string) => void;
|
|
64
|
+
onSubmit: () => void;
|
|
65
|
+
onStop?: () => void;
|
|
66
|
+
status: "ready" | "submitted" | "streaming" | "error";
|
|
67
|
+
models?: readonly CloudOsModelOption[];
|
|
68
|
+
selectedModel?: string | null;
|
|
69
|
+
onModelChange?: (modelId: string | null) => void;
|
|
70
|
+
attachments?: readonly CloudOsAttachmentView[];
|
|
71
|
+
onFilesSelected?: (files: readonly File[]) => void;
|
|
72
|
+
onAttachmentRemove?: (id: string) => void;
|
|
73
|
+
onAttachmentRetry?: (id: string) => void;
|
|
74
|
+
/** 底部左侧的额外控件(宿主的 Agent 档位设置等)。 */
|
|
75
|
+
tools?: ReactNode;
|
|
76
|
+
/** 发送键左边的额外控件(宿主的「引导 / 排队」投递方式)。 */
|
|
77
|
+
submitControl?: ReactNode;
|
|
78
|
+
/** 宿主侧判定「此刻不能提交」(缺必填设置、引导不可用…)。 */
|
|
79
|
+
submissionBlocked?: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* 回合是否进行中。宿主的 turn 状态比 status 更准(status 会在 steer 间隙回落),
|
|
82
|
+
* 传了就以它为准来决定显示发送键还是停止键。
|
|
83
|
+
*/
|
|
84
|
+
turnActive?: boolean;
|
|
85
|
+
/**
|
|
86
|
+
* 可用的斜杠命令。cloudflare-os 原版是一个直连 RPC 的行内 picker;这里退成
|
|
87
|
+
* 「+」菜单里的一组条目 —— 能力保住,但**没有**边打字边过滤的行内补全。
|
|
88
|
+
*/
|
|
89
|
+
commands?: readonly CloudOsComposerCommand[];
|
|
90
|
+
/** 有值时渲染「添加资源」按钮。 */
|
|
91
|
+
onAttachResource?: () => void;
|
|
92
|
+
attachLabel?: string;
|
|
93
|
+
showThinkingTraces?: boolean;
|
|
94
|
+
onToggleThinkingTraces?: () => void;
|
|
95
|
+
/** 非空时锁住输入框,并把它当占位文案显示。 */
|
|
96
|
+
blockedReason?: string;
|
|
97
|
+
/** 贴在 prompt card 顶部的横幅(原文件用来放「待接受的改动」)。 */
|
|
98
|
+
banner?: ReactNode;
|
|
99
|
+
placeholder?: string;
|
|
100
|
+
minRows?: number;
|
|
101
|
+
maxRows?: number;
|
|
102
|
+
autoFocus?: boolean;
|
|
103
|
+
/** 输入框下方一行(原文件放 token / cost)。 */
|
|
104
|
+
footnote?: ReactNode;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const MAX_PENDING_ATTACHMENTS = 5;
|
|
108
|
+
|
|
109
|
+
function autoResizeTextarea(
|
|
110
|
+
textarea: HTMLTextAreaElement,
|
|
111
|
+
minRows: number,
|
|
112
|
+
maxRows: number,
|
|
113
|
+
) {
|
|
114
|
+
textarea.style.height = "auto";
|
|
115
|
+
const cs = getComputedStyle(textarea);
|
|
116
|
+
const lineHeight = parseFloat(cs.lineHeight) || parseFloat(cs.fontSize) * 1.5;
|
|
117
|
+
const paddingY = parseFloat(cs.paddingTop) + parseFloat(cs.paddingBottom);
|
|
118
|
+
const borderY =
|
|
119
|
+
parseFloat(cs.borderTopWidth) + parseFloat(cs.borderBottomWidth);
|
|
120
|
+
const minH = lineHeight * minRows + paddingY + borderY;
|
|
121
|
+
const maxH = lineHeight * maxRows + paddingY + borderY;
|
|
122
|
+
textarea.style.height = `${Math.min(Math.max(textarea.scrollHeight, minH), maxH)}px`;
|
|
123
|
+
textarea.style.overflow = textarea.scrollHeight > maxH ? "auto" : "hidden";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function CloudOsChatInput({
|
|
127
|
+
value,
|
|
128
|
+
onChange,
|
|
129
|
+
onSubmit,
|
|
130
|
+
onStop,
|
|
131
|
+
status,
|
|
132
|
+
models = [],
|
|
133
|
+
selectedModel = null,
|
|
134
|
+
onModelChange,
|
|
135
|
+
attachments = [],
|
|
136
|
+
onFilesSelected,
|
|
137
|
+
onAttachmentRemove,
|
|
138
|
+
onAttachmentRetry,
|
|
139
|
+
tools,
|
|
140
|
+
submitControl,
|
|
141
|
+
submissionBlocked = false,
|
|
142
|
+
turnActive,
|
|
143
|
+
commands = [],
|
|
144
|
+
onAttachResource,
|
|
145
|
+
attachLabel,
|
|
146
|
+
showThinkingTraces,
|
|
147
|
+
onToggleThinkingTraces,
|
|
148
|
+
blockedReason,
|
|
149
|
+
banner,
|
|
150
|
+
placeholder,
|
|
151
|
+
minRows = 1,
|
|
152
|
+
maxRows = 6,
|
|
153
|
+
autoFocus = false,
|
|
154
|
+
footnote,
|
|
155
|
+
}: CloudOsChatInputProps) {
|
|
156
|
+
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
|
157
|
+
const attachmentInputRef = useRef<HTMLInputElement>(null);
|
|
158
|
+
const dragDepthRef = useRef(0);
|
|
159
|
+
const [dragActive, setDragActive] = useState(false);
|
|
160
|
+
|
|
161
|
+
const isAgentActive =
|
|
162
|
+
turnActive ?? (status === "submitted" || status === "streaming");
|
|
163
|
+
const isBlocked = Boolean(blockedReason);
|
|
164
|
+
const canAttachMore = attachments.length < MAX_PENDING_ATTACHMENTS;
|
|
165
|
+
const hasReadyAttachment = attachments.some(
|
|
166
|
+
(attachment) => attachment.status === "ready",
|
|
167
|
+
);
|
|
168
|
+
const hasUnreadyAttachment = attachments.some(
|
|
169
|
+
(attachment) => attachment.status !== "ready",
|
|
170
|
+
);
|
|
171
|
+
// 回合进行中时依然可以提交(引导 / 排队),由宿主的 submissionBlocked 说了算。
|
|
172
|
+
// 只有「回合在跑 + 输入框是空的」才把发送键换成停止键 —— 否则用户打了一半字
|
|
173
|
+
// 按回车会变成停止,那是最难受的一种误触。
|
|
174
|
+
const showStopButton =
|
|
175
|
+
isAgentActive && Boolean(onStop) && !value.trim() && !hasReadyAttachment;
|
|
176
|
+
const canSend =
|
|
177
|
+
!isBlocked &&
|
|
178
|
+
!submissionBlocked &&
|
|
179
|
+
(value.trim().length > 0 || hasReadyAttachment) &&
|
|
180
|
+
!hasUnreadyAttachment;
|
|
181
|
+
|
|
182
|
+
const selectedModelLabel =
|
|
183
|
+
selectedModel === null
|
|
184
|
+
? "No agent"
|
|
185
|
+
: (models.find((model) => model.id === selectedModel)?.name ?? selectedModel);
|
|
186
|
+
|
|
187
|
+
useEffect(() => {
|
|
188
|
+
const element = textareaRef.current;
|
|
189
|
+
if (element) autoResizeTextarea(element, minRows, maxRows);
|
|
190
|
+
}, [maxRows, minRows, value]);
|
|
191
|
+
|
|
192
|
+
const addFiles = useCallback(
|
|
193
|
+
(files: readonly File[]) => {
|
|
194
|
+
if (files.length === 0) return;
|
|
195
|
+
onFilesSelected?.(files.slice(0, MAX_PENDING_ATTACHMENTS - attachments.length));
|
|
196
|
+
},
|
|
197
|
+
[attachments.length, onFilesSelected],
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
const handleDragEnter = (event: ReactDragEvent<HTMLDivElement>) => {
|
|
201
|
+
if (!onFilesSelected) return;
|
|
202
|
+
if (!Array.from(event.dataTransfer.types).includes("Files")) return;
|
|
203
|
+
dragDepthRef.current += 1;
|
|
204
|
+
setDragActive(true);
|
|
205
|
+
};
|
|
206
|
+
const handleDragOver = (event: ReactDragEvent<HTMLDivElement>) => {
|
|
207
|
+
if (!onFilesSelected) return;
|
|
208
|
+
if (!Array.from(event.dataTransfer.types).includes("Files")) return;
|
|
209
|
+
event.preventDefault();
|
|
210
|
+
event.dataTransfer.dropEffect = canAttachMore ? "copy" : "none";
|
|
211
|
+
};
|
|
212
|
+
const handleDragLeave = () => {
|
|
213
|
+
dragDepthRef.current = Math.max(0, dragDepthRef.current - 1);
|
|
214
|
+
if (dragDepthRef.current === 0) setDragActive(false);
|
|
215
|
+
};
|
|
216
|
+
const handleDrop = (event: ReactDragEvent<HTMLDivElement>) => {
|
|
217
|
+
if (!onFilesSelected) return;
|
|
218
|
+
event.preventDefault();
|
|
219
|
+
dragDepthRef.current = 0;
|
|
220
|
+
setDragActive(false);
|
|
221
|
+
addFiles(Array.from(event.dataTransfer.files));
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
return (
|
|
225
|
+
// isolation: isolate 把输入框内部的 z-index 关起来,免得盖到 portal 出去的模型下拉。
|
|
226
|
+
<div className="cos-chat-input-root relative isolate px-4 py-4">
|
|
227
|
+
<input
|
|
228
|
+
ref={attachmentInputRef}
|
|
229
|
+
type="file"
|
|
230
|
+
multiple
|
|
231
|
+
className="hidden"
|
|
232
|
+
onChange={(event) => {
|
|
233
|
+
const files = Array.from(event.currentTarget.files ?? []);
|
|
234
|
+
event.currentTarget.value = "";
|
|
235
|
+
addFiles(files);
|
|
236
|
+
}}
|
|
237
|
+
/>
|
|
238
|
+
|
|
239
|
+
{/* Prompt card:比页面底色更亮(kumo-control vs kumo-base),配柔和中性投影,
|
|
240
|
+
让输入框读起来是一块独立表面;聚焦时投影再抬一档。 */}
|
|
241
|
+
<div
|
|
242
|
+
className="themed-prompt-card-shadow relative overflow-visible rounded-2xl border border-kumo-line bg-kumo-control transition-shadow duration-150 ease-out"
|
|
243
|
+
onDragEnter={handleDragEnter}
|
|
244
|
+
onDragOver={handleDragOver}
|
|
245
|
+
onDragLeave={handleDragLeave}
|
|
246
|
+
onDrop={handleDrop}
|
|
247
|
+
>
|
|
248
|
+
{dragActive && (
|
|
249
|
+
<div
|
|
250
|
+
className={`themed-inset-outline pointer-events-none absolute inset-0 z-20 grid place-items-center rounded-2xl border-2 border-dashed p-4 backdrop-blur-[1px] transition-[opacity,transform] duration-150 ease-out ${
|
|
251
|
+
canAttachMore
|
|
252
|
+
? "border-kumo-brand/55 bg-kumo-brand/10"
|
|
253
|
+
: "border-kumo-warning/60 bg-kumo-warning/10"
|
|
254
|
+
}`}
|
|
255
|
+
>
|
|
256
|
+
<div
|
|
257
|
+
className={`themed-floating-shadow flex items-center gap-2 rounded-full border bg-kumo-base/90 px-3 py-2 text-[13px] font-medium leading-4 tracking-[-0.2px] text-kumo-default ${
|
|
258
|
+
canAttachMore ? "border-kumo-brand/25" : "border-kumo-warning/30"
|
|
259
|
+
}`}
|
|
260
|
+
>
|
|
261
|
+
<span
|
|
262
|
+
className={`grid h-7 w-7 place-items-center rounded-full ${
|
|
263
|
+
canAttachMore
|
|
264
|
+
? "bg-kumo-brand/12 text-kumo-brand"
|
|
265
|
+
: "bg-kumo-warning/15 text-kumo-warning"
|
|
266
|
+
}`}
|
|
267
|
+
>
|
|
268
|
+
<FileIcon size={16} weight="duotone" />
|
|
269
|
+
</span>
|
|
270
|
+
{canAttachMore
|
|
271
|
+
? "Drop files to attach"
|
|
272
|
+
: `Messages are limited to ${MAX_PENDING_ATTACHMENTS} attachments`}
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
)}
|
|
276
|
+
|
|
277
|
+
{banner}
|
|
278
|
+
|
|
279
|
+
<div className="relative px-4 pb-1 pt-3">
|
|
280
|
+
<textarea
|
|
281
|
+
ref={(element) => {
|
|
282
|
+
textareaRef.current = element;
|
|
283
|
+
if (element) autoResizeTextarea(element, minRows, maxRows);
|
|
284
|
+
}}
|
|
285
|
+
value={value}
|
|
286
|
+
disabled={isBlocked}
|
|
287
|
+
autoFocus={autoFocus}
|
|
288
|
+
rows={minRows}
|
|
289
|
+
placeholder={
|
|
290
|
+
isBlocked
|
|
291
|
+
? blockedReason
|
|
292
|
+
: isAgentActive
|
|
293
|
+
? "Waiting for agent…"
|
|
294
|
+
: (placeholder ?? "Ask a follow-up…")
|
|
295
|
+
}
|
|
296
|
+
onChange={(event) => {
|
|
297
|
+
onChange(event.target.value);
|
|
298
|
+
autoResizeTextarea(event.target, minRows, maxRows);
|
|
299
|
+
}}
|
|
300
|
+
onPaste={(event) => {
|
|
301
|
+
const files = Array.from(event.clipboardData.items)
|
|
302
|
+
.filter((item) => item.kind === "file")
|
|
303
|
+
.map((item) => item.getAsFile())
|
|
304
|
+
.filter((file): file is File => file !== null);
|
|
305
|
+
if (files.length > 0) {
|
|
306
|
+
event.preventDefault();
|
|
307
|
+
addFiles(files);
|
|
308
|
+
}
|
|
309
|
+
}}
|
|
310
|
+
onKeyDown={(event) => {
|
|
311
|
+
// Enter 发送(按住 Shift 换行); IME 组合中不发送
|
|
312
|
+
if (
|
|
313
|
+
event.key === "Enter" &&
|
|
314
|
+
!event.shiftKey &&
|
|
315
|
+
!isComposingKeyEvent(event.nativeEvent)
|
|
316
|
+
) {
|
|
317
|
+
event.preventDefault();
|
|
318
|
+
if (canSend) onSubmit();
|
|
319
|
+
}
|
|
320
|
+
}}
|
|
321
|
+
className="relative z-[1] w-full 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"
|
|
322
|
+
/>
|
|
323
|
+
</div>
|
|
324
|
+
|
|
325
|
+
{attachments.length > 0 && (
|
|
326
|
+
<div className="flex flex-wrap items-center gap-2 px-3 pb-2 pt-1">
|
|
327
|
+
{attachments.map((attachment) => (
|
|
328
|
+
<div
|
|
329
|
+
key={attachment.id}
|
|
330
|
+
className="relative flex h-14 w-14 items-center justify-center overflow-hidden rounded-lg border border-kumo-line/70 bg-kumo-elevated"
|
|
331
|
+
>
|
|
332
|
+
{attachment.previewUrl ? (
|
|
333
|
+
<img
|
|
334
|
+
src={attachment.previewUrl}
|
|
335
|
+
alt={attachment.filename ?? "Attached file"}
|
|
336
|
+
className="h-full w-full object-cover"
|
|
337
|
+
/>
|
|
338
|
+
) : (
|
|
339
|
+
<FileIcon size={22} className="text-kumo-inactive" />
|
|
340
|
+
)}
|
|
341
|
+
{attachment.status === "uploading" && (
|
|
342
|
+
<div className="absolute inset-0 grid place-items-center rounded-lg bg-black/35 text-[10px] text-white">
|
|
343
|
+
Uploading
|
|
344
|
+
</div>
|
|
345
|
+
)}
|
|
346
|
+
{attachment.status === "error" && (
|
|
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>
|
|
370
|
+
))}
|
|
371
|
+
</div>
|
|
372
|
+
)}
|
|
373
|
+
|
|
374
|
+
{/* 底部一行:左侧选项,右侧模型 + 发送 */}
|
|
375
|
+
<div className="flex items-center justify-between gap-1.5 px-3 pb-1.5">
|
|
376
|
+
<div className="flex min-w-0 flex-1 items-center gap-1.5 overflow-hidden">
|
|
377
|
+
<DropdownMenu>
|
|
378
|
+
<DropdownMenu.Trigger
|
|
379
|
+
render={
|
|
380
|
+
<button
|
|
381
|
+
type="button"
|
|
382
|
+
className="group flex h-8 w-8 flex-shrink-0 cursor-pointer items-center justify-center rounded-lg text-kumo-inactive transition-[background-color,color,transform] duration-150 ease-out hover:bg-kumo-tint hover:text-kumo-subtle focus-visible:bg-kumo-tint focus-visible:text-kumo-subtle focus-visible:outline-none active:scale-[0.96] data-[state=open]:bg-kumo-tint data-[state=open]:text-kumo-subtle"
|
|
383
|
+
aria-label="Open chat options"
|
|
384
|
+
>
|
|
385
|
+
<Plus size={18} />
|
|
386
|
+
</button>
|
|
387
|
+
}
|
|
388
|
+
/>
|
|
389
|
+
<DropdownMenu.Content className="themed-floating-shadow-lg !min-w-[170px] rounded-2xl border border-kumo-line/70 bg-kumo-base p-1">
|
|
390
|
+
{onToggleThinkingTraces && (
|
|
391
|
+
<DropdownMenu.Item
|
|
392
|
+
onClick={onToggleThinkingTraces}
|
|
393
|
+
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"
|
|
394
|
+
>
|
|
395
|
+
<span className="mr-2 inline-flex h-4 w-4 items-center justify-center text-kumo-inactive">
|
|
396
|
+
<Brain size={14} />
|
|
397
|
+
</span>
|
|
398
|
+
<span className="flex-1">
|
|
399
|
+
{showThinkingTraces ? "Hide thinking" : "Show thinking"}
|
|
400
|
+
</span>
|
|
401
|
+
</DropdownMenu.Item>
|
|
402
|
+
)}
|
|
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
|
+
{onFilesSelected && (
|
|
428
|
+
<DropdownMenu.Item
|
|
429
|
+
onClick={() => attachmentInputRef.current?.click()}
|
|
430
|
+
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"
|
|
431
|
+
>
|
|
432
|
+
<span className="mr-2 inline-flex h-4 w-4 items-center justify-center text-kumo-inactive">
|
|
433
|
+
<FileIcon size={14} />
|
|
434
|
+
</span>
|
|
435
|
+
<span className="flex-1">Upload file</span>
|
|
436
|
+
</DropdownMenu.Item>
|
|
437
|
+
)}
|
|
438
|
+
</DropdownMenu.Content>
|
|
439
|
+
</DropdownMenu>
|
|
440
|
+
{onAttachResource && (
|
|
441
|
+
<button
|
|
442
|
+
type="button"
|
|
443
|
+
onClick={onAttachResource}
|
|
444
|
+
className="inline-flex h-8 flex-shrink-0 cursor-pointer items-center gap-1.5 rounded-lg px-2 text-[13px] leading-none tracking-[-0.25px] text-kumo-inactive transition-[background-color,color,transform] duration-150 ease-out hover:bg-kumo-tint hover:text-kumo-subtle focus-visible:bg-kumo-tint focus-visible:text-kumo-subtle focus-visible:outline-none active:scale-[0.97]"
|
|
445
|
+
>
|
|
446
|
+
<Plug size={15} className="flex-shrink-0" />
|
|
447
|
+
<span className="cos-attach-label leading-none">
|
|
448
|
+
{attachLabel ?? "Add resource"}
|
|
449
|
+
</span>
|
|
450
|
+
</button>
|
|
451
|
+
)}
|
|
452
|
+
{tools}
|
|
453
|
+
</div>
|
|
454
|
+
|
|
455
|
+
<div className="ml-auto flex min-w-0 flex-shrink items-center gap-1.5">
|
|
456
|
+
{onModelChange && (
|
|
457
|
+
<DropdownMenu>
|
|
458
|
+
<DropdownMenu.Trigger
|
|
459
|
+
render={
|
|
460
|
+
<button
|
|
461
|
+
type="button"
|
|
462
|
+
className="group inline-flex h-8 min-w-0 max-w-[180px] cursor-pointer items-center gap-1.5 rounded-lg px-2 text-[13px] leading-5 tracking-[-0.25px] text-kumo-subtle transition-[background-color,color,transform] duration-150 ease-out hover:bg-kumo-tint hover:text-kumo-default focus-visible:bg-kumo-tint focus-visible:text-kumo-default focus-visible:outline-none active:scale-[0.97] data-[state=open]:bg-kumo-tint data-[state=open]:text-kumo-default"
|
|
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>
|
|
496
|
+
)}
|
|
497
|
+
{!showStopButton && submitControl}
|
|
498
|
+
{showStopButton ? (
|
|
499
|
+
<WorkshopIconButton
|
|
500
|
+
onClick={onStop}
|
|
501
|
+
tone="primary"
|
|
502
|
+
className="!h-8 !w-8"
|
|
503
|
+
aria-label="Stop agent"
|
|
504
|
+
>
|
|
505
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
|
|
506
|
+
<rect x="5" y="5" width="14" height="14" rx="2" />
|
|
507
|
+
</svg>
|
|
508
|
+
</WorkshopIconButton>
|
|
509
|
+
) : (
|
|
510
|
+
<WorkshopIconButton
|
|
511
|
+
onClick={onSubmit}
|
|
512
|
+
disabled={!canSend}
|
|
513
|
+
tone="primary"
|
|
514
|
+
className="!h-8 !w-8 disabled:cursor-not-allowed disabled:opacity-30"
|
|
515
|
+
aria-label="Send message"
|
|
516
|
+
>
|
|
517
|
+
<svg
|
|
518
|
+
width="16"
|
|
519
|
+
height="16"
|
|
520
|
+
viewBox="0 0 24 24"
|
|
521
|
+
fill="none"
|
|
522
|
+
stroke="currentColor"
|
|
523
|
+
strokeWidth="2.5"
|
|
524
|
+
>
|
|
525
|
+
<line x1="12" y1="19" x2="12" y2="5" />
|
|
526
|
+
<polyline points="5 12 12 5 19 12" />
|
|
527
|
+
</svg>
|
|
528
|
+
</WorkshopIconButton>
|
|
529
|
+
)}
|
|
530
|
+
</div>
|
|
531
|
+
</div>
|
|
532
|
+
</div>
|
|
533
|
+
|
|
534
|
+
{footnote && (
|
|
535
|
+
<div className="-mt-1 flex min-h-[1.25rem] items-start justify-end gap-4 px-4 pb-1 font-mono text-[11px] leading-4 text-kumo-inactive">
|
|
536
|
+
{footnote}
|
|
537
|
+
</div>
|
|
538
|
+
)}
|
|
539
|
+
</div>
|
|
540
|
+
);
|
|
541
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@springbrand/message-panel/cloud-os`
|
|
3
|
+
*
|
|
4
|
+
* 从 cloudflare-os-main(packages/workshop-frontend)搬来的聊天体 + 右侧工作区面板,
|
|
5
|
+
* 适配到当前的 UIMessage 协议。这个文件夹**完全独立**:不 import `../src` 的任何东西,
|
|
6
|
+
* 自带 cn / 主题 / 图标 / markdown / primitives。
|
|
7
|
+
*
|
|
8
|
+
* 边界:
|
|
9
|
+
* - `styles/cloud-os.css` 必须由宿主的 Tailwind 入口 import,并把本目录加进 @source。
|
|
10
|
+
* - 所有组件都要包在 `<CloudOsRoot>` 里,否则拿不到作用域化的 token。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export { CloudOsRoot } from "./layout/cloud-os-root";
|
|
14
|
+
export { CloudOsWorkspaceSplit } from "./layout/cloud-os-workspace-split";
|
|
15
|
+
export type { CloudOsWorkspaceSplitProps } from "./layout/cloud-os-workspace-split";
|
|
16
|
+
|
|
17
|
+
export { CloudOsChatMessages } from "./chat/cloud-os-chat-messages";
|
|
18
|
+
export type {
|
|
19
|
+
CloudOsChatMessagesProps,
|
|
20
|
+
CloudOsChatStatus,
|
|
21
|
+
} from "./chat/cloud-os-chat-messages";
|
|
22
|
+
|
|
23
|
+
export { CloudOsChatInput } from "./composer/cloud-os-chat-input";
|
|
24
|
+
export type {
|
|
25
|
+
CloudOsAttachmentView,
|
|
26
|
+
CloudOsChatInputProps,
|
|
27
|
+
CloudOsComposerCommand,
|
|
28
|
+
CloudOsModelOption,
|
|
29
|
+
} from "./composer/cloud-os-chat-input";
|
|
30
|
+
|
|
31
|
+
export {
|
|
32
|
+
CloudOsWorkspacePanel,
|
|
33
|
+
NoContentPlaceholder,
|
|
34
|
+
PaneLabel,
|
|
35
|
+
PaneTab,
|
|
36
|
+
PaneWorkpieceTabs,
|
|
37
|
+
} from "./workspace/cloud-os-workspace-panel";
|
|
38
|
+
export type {
|
|
39
|
+
CloudOsPaneTab,
|
|
40
|
+
CloudOsWorkpieceTab,
|
|
41
|
+
CloudOsWorkspacePanelProps,
|
|
42
|
+
} from "./workspace/cloud-os-workspace-panel";
|
|
43
|
+
|
|
44
|
+
export { MarkdownMessage } from "./chat/markdown-message";
|
|
45
|
+
export type { CloudOsUrlResolver } from "./chat/markdown-message";
|
|
46
|
+
export {
|
|
47
|
+
ThinkingTraceRow,
|
|
48
|
+
ToolCallDetails,
|
|
49
|
+
ToolGroupRow,
|
|
50
|
+
WorkIcon,
|
|
51
|
+
} from "./chat/tool-rows";
|
|
52
|
+
export {
|
|
53
|
+
ApprovalBlock,
|
|
54
|
+
AskUserBlock,
|
|
55
|
+
ErrorBlock,
|
|
56
|
+
ParallelBlock,
|
|
57
|
+
PlanBlock,
|
|
58
|
+
ScheduleBlock,
|
|
59
|
+
SubAgentsBlock,
|
|
60
|
+
SuggestionsBlock,
|
|
61
|
+
} from "./chat/rich-blocks";
|
|
62
|
+
export type { ApprovalDecision } from "./chat/rich-blocks";
|
|
63
|
+
|
|
64
|
+
export { CloudOsActivityIndicator } from "./chat/activity-indicator";
|
|
65
|
+
|
|
66
|
+
export {
|
|
67
|
+
buildCloudOsEntries,
|
|
68
|
+
cloudOsMetadata,
|
|
69
|
+
deriveTurnActivity,
|
|
70
|
+
formatClockTime,
|
|
71
|
+
formatFullTimestamp,
|
|
72
|
+
messageText,
|
|
73
|
+
rhythmTopClass,
|
|
74
|
+
} from "./chat/transcript-model";
|
|
75
|
+
export type {
|
|
76
|
+
AssistantBlock,
|
|
77
|
+
CloudOsActivity,
|
|
78
|
+
CloudOsAttachment,
|
|
79
|
+
CloudOsEntry,
|
|
80
|
+
ParallelTool,
|
|
81
|
+
PlanStep,
|
|
82
|
+
SubAgentView,
|
|
83
|
+
} from "./chat/transcript-model";
|
|
84
|
+
|
|
85
|
+
export {
|
|
86
|
+
buildToolCallGroups,
|
|
87
|
+
canonicalToolKind,
|
|
88
|
+
getToolCallSummary,
|
|
89
|
+
getToolIcon,
|
|
90
|
+
toCloudOsToolCall,
|
|
91
|
+
toolNameOfPart,
|
|
92
|
+
} from "./chat/tool-presentation";
|
|
93
|
+
export type {
|
|
94
|
+
CloudOsToolCall,
|
|
95
|
+
CloudOsToolKind,
|
|
96
|
+
ToolCallGroup,
|
|
97
|
+
} from "./chat/tool-presentation";
|
|
98
|
+
|
|
99
|
+
export { DropdownMenu } from "./primitives/dropdown-menu";
|
|
100
|
+
export { Tooltip, TooltipProvider } from "./primitives/tooltip";
|
|
101
|
+
export {
|
|
102
|
+
CountBadge,
|
|
103
|
+
WorkshopButton,
|
|
104
|
+
WorkshopIconButton,
|
|
105
|
+
WorkshopInput,
|
|
106
|
+
} from "./primitives/workshop-controls";
|
|
107
|
+
export type { CloudOsMode } from "./internal/theme-context";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
|
|
3
|
+
export type CloudOsMode = "light" | "dark";
|
|
4
|
+
|
|
5
|
+
const CloudOsModeContext = createContext<CloudOsMode>("light");
|
|
6
|
+
|
|
7
|
+
export const CloudOsModeProvider = CloudOsModeContext.Provider;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* portal 出去的浮层(Tooltip / DropdownMenu / Popover)脱离 `.cloud-os-root` 子树,
|
|
11
|
+
* 拿不到作用域里的 CSS 变量。浮层根节点自己带上 data-mode,配合
|
|
12
|
+
* `[data-cloud-os-portal][data-mode="dark"]` 那组规则补齐变量。
|
|
13
|
+
*/
|
|
14
|
+
export function useCloudOsMode(): CloudOsMode {
|
|
15
|
+
return useContext(CloudOsModeContext);
|
|
16
|
+
}
|