@springbrand/message-panel 0.1.3-alpha.3 → 0.1.3-alpha.30
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
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Database,
|
|
3
|
+
File as FileIcon,
|
|
4
|
+
FileArchive,
|
|
5
|
+
FileAudio,
|
|
6
|
+
FileImage,
|
|
7
|
+
FileText,
|
|
8
|
+
FileVideo,
|
|
9
|
+
type Icon,
|
|
10
|
+
} from "@phosphor-icons/react";
|
|
11
|
+
import {
|
|
12
|
+
CheckIcon,
|
|
13
|
+
FileArchiveIcon,
|
|
14
|
+
FileImageIcon,
|
|
15
|
+
FileTextIcon,
|
|
16
|
+
Loader2Icon,
|
|
17
|
+
RotateCcwIcon,
|
|
18
|
+
XIcon,
|
|
19
|
+
} from "lucide-react";
|
|
20
|
+
import {
|
|
21
|
+
useEffect,
|
|
22
|
+
useRef,
|
|
23
|
+
useState,
|
|
24
|
+
type ComponentType,
|
|
25
|
+
} from "react";
|
|
26
|
+
import {
|
|
27
|
+
PRODUCT_PRESETS,
|
|
28
|
+
productTypeForMediaType,
|
|
29
|
+
type ProductType,
|
|
30
|
+
} from "./chat/image-generation";
|
|
31
|
+
|
|
32
|
+
const PRODUCT_ICONS = {
|
|
33
|
+
image: FileImage,
|
|
34
|
+
audio: FileAudio,
|
|
35
|
+
video: FileVideo,
|
|
36
|
+
document: FileText,
|
|
37
|
+
archive: FileArchive,
|
|
38
|
+
data: Database,
|
|
39
|
+
other: FileIcon,
|
|
40
|
+
} satisfies Record<ProductType, Icon>;
|
|
41
|
+
|
|
42
|
+
export type FileViewPlacement =
|
|
43
|
+
| "assistant-message"
|
|
44
|
+
| "composer"
|
|
45
|
+
| "user-message";
|
|
46
|
+
|
|
47
|
+
export type FileViewStatus = "error" | "ready" | "uploading";
|
|
48
|
+
export type FileViewVariant = "compact" | "product";
|
|
49
|
+
|
|
50
|
+
export interface FileViewFile {
|
|
51
|
+
url?: string;
|
|
52
|
+
filename?: string;
|
|
53
|
+
mediaType?: string;
|
|
54
|
+
size?: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface FileViewProps {
|
|
58
|
+
file: FileViewFile;
|
|
59
|
+
placement: FileViewPlacement;
|
|
60
|
+
variant?: FileViewVariant;
|
|
61
|
+
status?: FileViewStatus;
|
|
62
|
+
progress?: number;
|
|
63
|
+
onOpen?: () => void;
|
|
64
|
+
onRemove?: () => void;
|
|
65
|
+
onRetry?: () => void;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type FileRenderer = ComponentType<FileViewProps>;
|
|
69
|
+
|
|
70
|
+
function fileMeta(file: FileViewFile): string {
|
|
71
|
+
if (file.size === undefined) return "Ready";
|
|
72
|
+
if (file.size < 1024) return `${file.size} B`;
|
|
73
|
+
if (file.size < 1024 * 1024) return `${Math.round(file.size / 1024)} KB`;
|
|
74
|
+
return `${(file.size / 1024 / 1024).toFixed(1)} MB`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function attachmentIcon(file: FileViewFile) {
|
|
78
|
+
if (file.mediaType?.startsWith("image/")) return FileImageIcon;
|
|
79
|
+
if (/zip|archive|compressed/i.test(file.mediaType ?? "")) {
|
|
80
|
+
return FileArchiveIcon;
|
|
81
|
+
}
|
|
82
|
+
return FileTextIcon;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function ImageFileView({
|
|
86
|
+
src,
|
|
87
|
+
label,
|
|
88
|
+
placement,
|
|
89
|
+
frameClassName,
|
|
90
|
+
}: {
|
|
91
|
+
src: string;
|
|
92
|
+
label: string;
|
|
93
|
+
placement: Exclude<FileViewPlacement, "composer">;
|
|
94
|
+
frameClassName?: string;
|
|
95
|
+
}) {
|
|
96
|
+
const imageRef = useRef<HTMLImageElement>(null);
|
|
97
|
+
const [loadedSrc, setLoadedSrc] = useState<string>();
|
|
98
|
+
const [errorSrc, setErrorSrc] = useState<string>();
|
|
99
|
+
const loaded = loadedSrc === src;
|
|
100
|
+
const error = errorSrc === src;
|
|
101
|
+
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
const image = imageRef.current;
|
|
104
|
+
if (!image?.complete) return;
|
|
105
|
+
if (image.naturalWidth > 0) setLoadedSrc(src);
|
|
106
|
+
else setErrorSrc(src);
|
|
107
|
+
}, [src]);
|
|
108
|
+
|
|
109
|
+
const assistant = placement === "assistant-message";
|
|
110
|
+
const previewClassName = frameClassName
|
|
111
|
+
? `themed-thumbnail-shadow relative inline-block max-w-full overflow-hidden rounded-2xl border border-kumo-line align-bottom ${frameClassName}`
|
|
112
|
+
: assistant
|
|
113
|
+
? "relative inline-block max-w-full overflow-hidden rounded-xl border border-kumo-line align-bottom"
|
|
114
|
+
: "themed-thumbnail-shadow relative inline-block max-w-64 overflow-hidden rounded-xl border border-kumo-line align-bottom";
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<span
|
|
118
|
+
className={`${previewClassName} ${loaded && !error ? "" : "min-h-24 min-w-36"}`}
|
|
119
|
+
data-file-view=""
|
|
120
|
+
data-placement={placement}
|
|
121
|
+
>
|
|
122
|
+
<img
|
|
123
|
+
ref={imageRef}
|
|
124
|
+
src={src}
|
|
125
|
+
alt={label}
|
|
126
|
+
className={`${
|
|
127
|
+
frameClassName
|
|
128
|
+
? "block size-full object-contain"
|
|
129
|
+
: assistant
|
|
130
|
+
? "block max-h-[28rem] max-w-full object-contain"
|
|
131
|
+
: "block max-h-52 max-w-64 object-cover"
|
|
132
|
+
} ${loaded && !error ? "" : "invisible"}`}
|
|
133
|
+
decoding="async"
|
|
134
|
+
loading="lazy"
|
|
135
|
+
onLoad={() => {
|
|
136
|
+
setLoadedSrc(src);
|
|
137
|
+
setErrorSrc(undefined);
|
|
138
|
+
}}
|
|
139
|
+
onError={() => setErrorSrc(src)}
|
|
140
|
+
/>
|
|
141
|
+
{!loaded && !error && (
|
|
142
|
+
<span
|
|
143
|
+
className="absolute inset-0 flex min-h-24 min-w-36 items-center justify-center bg-kumo-elevated text-kumo-inactive"
|
|
144
|
+
data-image-state="loading"
|
|
145
|
+
role="status"
|
|
146
|
+
>
|
|
147
|
+
<FileImageIcon
|
|
148
|
+
className="size-6 animate-pulse motion-reduce:animate-none"
|
|
149
|
+
aria-hidden="true"
|
|
150
|
+
/>
|
|
151
|
+
<span className="sr-only">Loading preview...</span>
|
|
152
|
+
</span>
|
|
153
|
+
)}
|
|
154
|
+
{error && (
|
|
155
|
+
<span
|
|
156
|
+
className="absolute inset-0 flex min-h-24 min-w-36 items-center justify-center bg-kumo-elevated text-kumo-danger"
|
|
157
|
+
data-image-state="error"
|
|
158
|
+
role="img"
|
|
159
|
+
aria-label="Unable to load preview"
|
|
160
|
+
>
|
|
161
|
+
<FileImageIcon className="size-6" aria-hidden="true" />
|
|
162
|
+
</span>
|
|
163
|
+
)}
|
|
164
|
+
</span>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function ProductFileView({
|
|
169
|
+
file,
|
|
170
|
+
label,
|
|
171
|
+
placement,
|
|
172
|
+
onOpen,
|
|
173
|
+
}: {
|
|
174
|
+
file: FileViewFile;
|
|
175
|
+
label: string;
|
|
176
|
+
placement: Exclude<FileViewPlacement, "composer">;
|
|
177
|
+
onOpen?: () => void;
|
|
178
|
+
}) {
|
|
179
|
+
const type = productTypeForMediaType(file.mediaType);
|
|
180
|
+
const { frame } = PRODUCT_PRESETS[type];
|
|
181
|
+
const image = type === "image" && file.url;
|
|
182
|
+
const video = type === "video" && file.url;
|
|
183
|
+
const ProductIcon = PRODUCT_ICONS[type];
|
|
184
|
+
const preview = image
|
|
185
|
+
? (
|
|
186
|
+
<ImageFileView
|
|
187
|
+
src={file.url!}
|
|
188
|
+
label={label}
|
|
189
|
+
placement={placement}
|
|
190
|
+
frameClassName={frame}
|
|
191
|
+
/>
|
|
192
|
+
)
|
|
193
|
+
: video
|
|
194
|
+
? (
|
|
195
|
+
<video
|
|
196
|
+
src={file.url}
|
|
197
|
+
controls
|
|
198
|
+
preload="metadata"
|
|
199
|
+
playsInline
|
|
200
|
+
aria-label={label}
|
|
201
|
+
className={`themed-thumbnail-shadow block max-w-full rounded-2xl border border-kumo-line bg-kumo-elevated object-contain ${frame}`}
|
|
202
|
+
data-video-preview=""
|
|
203
|
+
/>
|
|
204
|
+
)
|
|
205
|
+
: (
|
|
206
|
+
<span className={`themed-thumbnail-shadow relative inline-grid max-w-full place-items-center overflow-hidden rounded-2xl border border-kumo-line bg-kumo-elevated ${frame}`}>
|
|
207
|
+
<ProductIcon
|
|
208
|
+
size={24}
|
|
209
|
+
weight="light"
|
|
210
|
+
className="text-kumo-subtle"
|
|
211
|
+
aria-hidden="true"
|
|
212
|
+
data-product-icon={type}
|
|
213
|
+
/>
|
|
214
|
+
{file.mediaType && (
|
|
215
|
+
<span className="absolute end-2.5 top-2.5 font-mono text-[10px] text-kumo-inactive">
|
|
216
|
+
{file.mediaType}
|
|
217
|
+
</span>
|
|
218
|
+
)}
|
|
219
|
+
</span>
|
|
220
|
+
);
|
|
221
|
+
const content = (
|
|
222
|
+
<span className="flex w-fit max-w-full flex-col gap-2.5" data-product-file-view={type}>
|
|
223
|
+
{preview}
|
|
224
|
+
<span className="flex min-w-0 max-w-full items-center gap-1 text-xs text-kumo-inactive">
|
|
225
|
+
<span className="capitalize text-kumo-subtle">{type}</span>
|
|
226
|
+
<span aria-hidden="true">·</span>
|
|
227
|
+
<span className="min-w-0 truncate">{label}</span>
|
|
228
|
+
</span>
|
|
229
|
+
</span>
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
return onOpen ? (
|
|
233
|
+
<button
|
|
234
|
+
type="button"
|
|
235
|
+
onClick={onOpen}
|
|
236
|
+
aria-label={`Open ${label} in Creation View`}
|
|
237
|
+
className="inline-block max-w-full cursor-pointer rounded-2xl text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-kumo-brand/40"
|
|
238
|
+
>
|
|
239
|
+
{content}
|
|
240
|
+
</button>
|
|
241
|
+
) : file.url && !video ? (
|
|
242
|
+
<a
|
|
243
|
+
href={file.url}
|
|
244
|
+
target="_blank"
|
|
245
|
+
rel="noopener noreferrer"
|
|
246
|
+
aria-label={`Open ${label}`}
|
|
247
|
+
className="inline-block max-w-full rounded-2xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-kumo-brand/40"
|
|
248
|
+
data-file-view={image ? undefined : ""}
|
|
249
|
+
data-placement={placement}
|
|
250
|
+
data-file-variant="product"
|
|
251
|
+
>
|
|
252
|
+
{content}
|
|
253
|
+
</a>
|
|
254
|
+
) : (
|
|
255
|
+
<span
|
|
256
|
+
className="inline-block max-w-full"
|
|
257
|
+
data-file-view=""
|
|
258
|
+
data-placement={placement}
|
|
259
|
+
data-file-variant="product"
|
|
260
|
+
>
|
|
261
|
+
{content}
|
|
262
|
+
</span>
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Cloud OS 的默认文件视图。宿主可通过 `renderFile` 在同一 seam 替换它。
|
|
268
|
+
*/
|
|
269
|
+
export function FileView({
|
|
270
|
+
file,
|
|
271
|
+
placement,
|
|
272
|
+
variant = "compact",
|
|
273
|
+
status = "ready",
|
|
274
|
+
progress = 0,
|
|
275
|
+
onOpen,
|
|
276
|
+
onRemove,
|
|
277
|
+
onRetry,
|
|
278
|
+
}: FileViewProps) {
|
|
279
|
+
const isImage = Boolean(file.url && file.mediaType?.startsWith("image/"));
|
|
280
|
+
const label = file.filename ?? "Attachment";
|
|
281
|
+
|
|
282
|
+
if (placement === "composer") {
|
|
283
|
+
const AttachmentIcon = attachmentIcon(file);
|
|
284
|
+
const meta =
|
|
285
|
+
status === "uploading"
|
|
286
|
+
? "Uploading"
|
|
287
|
+
: status === "error"
|
|
288
|
+
? "Upload failed"
|
|
289
|
+
: fileMeta(file);
|
|
290
|
+
|
|
291
|
+
return (
|
|
292
|
+
<div
|
|
293
|
+
className="relative flex items-center gap-2.5 overflow-hidden rounded-[14px] bg-kumo-elevated py-1.5 ps-1.5 pe-2.5"
|
|
294
|
+
data-file-view=""
|
|
295
|
+
data-placement={placement}
|
|
296
|
+
data-status={status}
|
|
297
|
+
data-slot="composer-attachment"
|
|
298
|
+
data-state={status === "ready" ? "done" : status}
|
|
299
|
+
>
|
|
300
|
+
<span className="flex size-8 shrink-0 items-center justify-center rounded-[10px] bg-kumo-control text-kumo-inactive themed-thumbnail-shadow">
|
|
301
|
+
{isImage ? (
|
|
302
|
+
<img
|
|
303
|
+
src={file.url}
|
|
304
|
+
alt=""
|
|
305
|
+
className="size-8 object-cover"
|
|
306
|
+
/>
|
|
307
|
+
) : (
|
|
308
|
+
<AttachmentIcon className="size-4" aria-hidden="true" />
|
|
309
|
+
)}
|
|
310
|
+
</span>
|
|
311
|
+
<span className="flex min-w-0 flex-col">
|
|
312
|
+
<span className="max-w-36 truncate text-xs font-medium text-kumo-default">
|
|
313
|
+
{label}
|
|
314
|
+
</span>
|
|
315
|
+
<span
|
|
316
|
+
className={
|
|
317
|
+
status === "error"
|
|
318
|
+
? "text-[11px] text-kumo-danger"
|
|
319
|
+
: "text-[11px] text-kumo-inactive"
|
|
320
|
+
}
|
|
321
|
+
>
|
|
322
|
+
{meta}
|
|
323
|
+
</span>
|
|
324
|
+
</span>
|
|
325
|
+
<span className="ms-1 flex w-5 items-center justify-end">
|
|
326
|
+
{status === "uploading" ? (
|
|
327
|
+
<Loader2Icon
|
|
328
|
+
className="size-3.5 animate-spin text-kumo-inactive motion-reduce:animate-none"
|
|
329
|
+
aria-label="Uploading"
|
|
330
|
+
/>
|
|
331
|
+
) : status === "error" && onRetry ? (
|
|
332
|
+
<button
|
|
333
|
+
type="button"
|
|
334
|
+
aria-label={`Retry ${label}`}
|
|
335
|
+
onClick={onRetry}
|
|
336
|
+
className="grid size-5 place-items-center rounded-full text-kumo-danger hover:bg-kumo-tint focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-kumo-brand/40"
|
|
337
|
+
>
|
|
338
|
+
<RotateCcwIcon className="size-3" aria-hidden="true" />
|
|
339
|
+
</button>
|
|
340
|
+
) : status === "ready" && onRemove ? (
|
|
341
|
+
<button
|
|
342
|
+
type="button"
|
|
343
|
+
aria-label={`Remove ${label}`}
|
|
344
|
+
onClick={onRemove}
|
|
345
|
+
className="grid size-5 place-items-center rounded-full text-kumo-inactive hover:bg-kumo-tint hover:text-kumo-default focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-kumo-brand/40"
|
|
346
|
+
>
|
|
347
|
+
<XIcon className="size-3" aria-hidden="true" />
|
|
348
|
+
</button>
|
|
349
|
+
) : status === "ready" ? (
|
|
350
|
+
<CheckIcon className="size-3.5 text-kumo-success" aria-label="Ready" />
|
|
351
|
+
) : null}
|
|
352
|
+
</span>
|
|
353
|
+
{status !== "ready" && onRemove && (
|
|
354
|
+
<button
|
|
355
|
+
type="button"
|
|
356
|
+
aria-label={`Remove ${label}`}
|
|
357
|
+
onClick={onRemove}
|
|
358
|
+
className="grid size-5 place-items-center rounded-full text-kumo-inactive hover:bg-kumo-tint hover:text-kumo-default focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-kumo-brand/40"
|
|
359
|
+
>
|
|
360
|
+
<XIcon className="size-3" aria-hidden="true" />
|
|
361
|
+
</button>
|
|
362
|
+
)}
|
|
363
|
+
{status === "uploading" && (
|
|
364
|
+
<span
|
|
365
|
+
aria-hidden="true"
|
|
366
|
+
className="absolute inset-x-0 bottom-0 h-0.5 bg-kumo-brand/70 transition-[width] duration-300"
|
|
367
|
+
style={{ width: `${Math.min(100, Math.max(0, progress * 100))}%` }}
|
|
368
|
+
/>
|
|
369
|
+
)}
|
|
370
|
+
</div>
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (variant === "product") {
|
|
375
|
+
return (
|
|
376
|
+
<ProductFileView
|
|
377
|
+
file={file}
|
|
378
|
+
label={label}
|
|
379
|
+
placement={placement}
|
|
380
|
+
onOpen={onOpen}
|
|
381
|
+
/>
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (isImage) {
|
|
386
|
+
const image = (
|
|
387
|
+
<ImageFileView src={file.url!} label={label} placement={placement} />
|
|
388
|
+
);
|
|
389
|
+
return onOpen ? (
|
|
390
|
+
<button
|
|
391
|
+
type="button"
|
|
392
|
+
onClick={onOpen}
|
|
393
|
+
aria-label={`Open ${label} in Creation View`}
|
|
394
|
+
className="inline-block max-w-full cursor-pointer rounded-xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-kumo-brand/40"
|
|
395
|
+
>
|
|
396
|
+
{image}
|
|
397
|
+
</button>
|
|
398
|
+
) : image;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return (
|
|
402
|
+
<span
|
|
403
|
+
className="inline-flex items-center gap-1.5 rounded-lg border border-kumo-line bg-kumo-elevated px-2.5 py-1.5 text-[12px] leading-4 text-kumo-subtle"
|
|
404
|
+
data-file-view=""
|
|
405
|
+
data-placement={placement}
|
|
406
|
+
>
|
|
407
|
+
<FileIcon size={13} className="text-kumo-inactive" />
|
|
408
|
+
<span className="min-w-0 truncate">{label}</span>
|
|
409
|
+
{file.url && (
|
|
410
|
+
<span className="ms-1 inline-flex items-center gap-1.5 border-s border-kumo-line ps-2">
|
|
411
|
+
<a
|
|
412
|
+
href={file.url}
|
|
413
|
+
target="_blank"
|
|
414
|
+
rel="noopener noreferrer"
|
|
415
|
+
aria-label={`Open ${label}`}
|
|
416
|
+
className="rounded text-kumo-default hover:text-kumo-default-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-kumo-brand/40"
|
|
417
|
+
>
|
|
418
|
+
Open
|
|
419
|
+
</a>
|
|
420
|
+
<a
|
|
421
|
+
href={file.url}
|
|
422
|
+
download={label}
|
|
423
|
+
aria-label={`Download ${label}`}
|
|
424
|
+
className="rounded text-kumo-default hover:text-kumo-default-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-kumo-brand/40"
|
|
425
|
+
>
|
|
426
|
+
Download
|
|
427
|
+
</a>
|
|
428
|
+
</span>
|
|
429
|
+
)}
|
|
430
|
+
</span>
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** 宿主没有接管时回退到包内默认视图。 */
|
|
435
|
+
export function renderFileView(
|
|
436
|
+
renderFile: FileRenderer | undefined,
|
|
437
|
+
props: FileViewProps,
|
|
438
|
+
) {
|
|
439
|
+
const Renderer = renderFile ?? FileView;
|
|
440
|
+
return <Renderer {...props} />;
|
|
441
|
+
}
|
package/cloud-os/index.ts
CHANGED
|
@@ -11,7 +11,21 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
export { CloudOsRoot } from "./layout/cloud-os-root";
|
|
14
|
-
export {
|
|
14
|
+
export { CapabilityChip } from "./capability-chip";
|
|
15
|
+
export type { CapabilityChipProps } from "./capability-chip";
|
|
16
|
+
export { FileView } from "./file-view";
|
|
17
|
+
export type {
|
|
18
|
+
FileRenderer,
|
|
19
|
+
FileViewFile,
|
|
20
|
+
FileViewPlacement,
|
|
21
|
+
FileViewProps,
|
|
22
|
+
FileViewStatus,
|
|
23
|
+
FileViewVariant,
|
|
24
|
+
} from "./file-view";
|
|
25
|
+
export {
|
|
26
|
+
CloudOsConversationLoading,
|
|
27
|
+
CloudOsWorkspaceSplit,
|
|
28
|
+
} from "./layout/cloud-os-workspace-split";
|
|
15
29
|
export type { CloudOsWorkspaceSplitProps } from "./layout/cloud-os-workspace-split";
|
|
16
30
|
|
|
17
31
|
export { CloudOsChatMessages } from "./chat/cloud-os-chat-messages";
|
|
@@ -23,10 +37,15 @@ export type {
|
|
|
23
37
|
export { CloudOsChatInput } from "./composer/cloud-os-chat-input";
|
|
24
38
|
export type {
|
|
25
39
|
CloudOsAttachmentView,
|
|
40
|
+
CloudOsCapabilityView,
|
|
26
41
|
CloudOsChatInputProps,
|
|
27
42
|
CloudOsComposerCommand,
|
|
28
|
-
CloudOsModelOption,
|
|
29
43
|
} from "./composer/cloud-os-chat-input";
|
|
44
|
+
export { CloudOsModelSelect } from "./composer/cloud-os-model-select";
|
|
45
|
+
export type {
|
|
46
|
+
CloudOsModelOption,
|
|
47
|
+
CloudOsModelSelectProps,
|
|
48
|
+
} from "./composer/cloud-os-model-select";
|
|
30
49
|
|
|
31
50
|
export {
|
|
32
51
|
CloudOsWorkspacePanel,
|
|
@@ -42,12 +61,19 @@ export type {
|
|
|
42
61
|
} from "./workspace/cloud-os-workspace-panel";
|
|
43
62
|
|
|
44
63
|
export { MarkdownMessage } from "./chat/markdown-message";
|
|
45
|
-
export type {
|
|
64
|
+
export type {
|
|
65
|
+
CloudOsArtifactResolver,
|
|
66
|
+
CloudOsUrlResolver,
|
|
67
|
+
} from "./chat/markdown-message";
|
|
46
68
|
export {
|
|
47
69
|
ThinkingTraceRow,
|
|
48
70
|
ToolCallDetails,
|
|
71
|
+
ToolGroupDetails,
|
|
49
72
|
ToolGroupRow,
|
|
73
|
+
WorkDescriptionRow,
|
|
50
74
|
WorkIcon,
|
|
75
|
+
WorkTraceDisclosure,
|
|
76
|
+
formatWorkDuration,
|
|
51
77
|
} from "./chat/tool-rows";
|
|
52
78
|
export {
|
|
53
79
|
ApprovalBlock,
|
|
@@ -55,7 +81,9 @@ export {
|
|
|
55
81
|
ErrorBlock,
|
|
56
82
|
ParallelBlock,
|
|
57
83
|
PlanBlock,
|
|
84
|
+
PermissionGrant,
|
|
58
85
|
ScheduleBlock,
|
|
86
|
+
ScheduleConfirmation,
|
|
59
87
|
SubAgentsBlock,
|
|
60
88
|
SuggestionsBlock,
|
|
61
89
|
} from "./chat/rich-blocks";
|
|
@@ -70,6 +98,8 @@ export {
|
|
|
70
98
|
formatClockTime,
|
|
71
99
|
formatFullTimestamp,
|
|
72
100
|
messageText,
|
|
101
|
+
partitionAssistantBlocks,
|
|
102
|
+
requestedCapabilitiesOf,
|
|
73
103
|
rhythmTopClass,
|
|
74
104
|
} from "./chat/transcript-model";
|
|
75
105
|
export type {
|
|
@@ -77,6 +107,7 @@ export type {
|
|
|
77
107
|
CloudOsActivity,
|
|
78
108
|
CloudOsAttachment,
|
|
79
109
|
CloudOsEntry,
|
|
110
|
+
CloudOsRequestedCapability,
|
|
80
111
|
ParallelTool,
|
|
81
112
|
PlanStep,
|
|
82
113
|
SubAgentView,
|
|
@@ -87,6 +118,7 @@ export {
|
|
|
87
118
|
canonicalToolKind,
|
|
88
119
|
getToolCallSummary,
|
|
89
120
|
getToolIcon,
|
|
121
|
+
humanizeToolName,
|
|
90
122
|
toCloudOsToolCall,
|
|
91
123
|
toolNameOfPart,
|
|
92
124
|
} from "./chat/tool-presentation";
|
|
@@ -6,11 +6,12 @@ import {
|
|
|
6
6
|
type PointerEvent as ReactPointerEvent,
|
|
7
7
|
type ReactNode,
|
|
8
8
|
} from "react";
|
|
9
|
+
import { motion, useReducedMotion } from "motion/react";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* 左聊天 / 右工作区的双栏骨架。从 cloudflare-os-main `GadgetEditor.tsx` 的 BODY
|
|
12
13
|
* 段拷来:同一条 1px 的 kumo-line 分隔即拖拽把手、同一套 pointer capture 拖拽
|
|
13
|
-
* (拖过 iframe 也不丢)、同一个 200ms
|
|
14
|
+
* (拖过 iframe 也不丢)、同一个 200ms 的收起/展开过渡。
|
|
14
15
|
*
|
|
15
16
|
* 适配点:原文件把左右两侧的内容写死成 ChatInterface / GadgetUI,这里换成
|
|
16
17
|
* children 插槽;chatWidth 的 localStorage key 换成 cloud-os 自己的。
|
|
@@ -24,6 +25,56 @@ const WORKSPACE_TRANSITION_MS = 200;
|
|
|
24
25
|
|
|
25
26
|
const isBrowser = typeof window !== "undefined";
|
|
26
27
|
|
|
28
|
+
const loadingAssets = {
|
|
29
|
+
spark: new URL("../assets/loading-spark.svg", import.meta.url).href,
|
|
30
|
+
corner: new URL("../assets/loading-corner.svg", import.meta.url).href,
|
|
31
|
+
mark: new URL("../assets/loading-mark.svg", import.meta.url).href,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const loadingSpring = (t: number) =>
|
|
35
|
+
1 - Math.exp(-t * 7.6657) * (Math.cos(t * 6.7605) + 1.1339 * Math.sin(t * 6.7605));
|
|
36
|
+
|
|
37
|
+
export function CloudOsConversationLoading() {
|
|
38
|
+
const reducedMotion = useReducedMotion();
|
|
39
|
+
const repeat = reducedMotion ? 0 : Infinity;
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="absolute inset-0 z-30 grid place-items-center bg-kumo-base/90">
|
|
43
|
+
<div role="status" aria-label="Loading conversation..." className="flex flex-col items-center gap-4">
|
|
44
|
+
<div className="relative h-[49px] w-[49px] overflow-hidden" aria-hidden="true">
|
|
45
|
+
<motion.img
|
|
46
|
+
src={loadingAssets.spark}
|
|
47
|
+
alt=""
|
|
48
|
+
className="absolute bottom-[18.5%] right-[61.46%] h-[20.04%] w-[20.04%] will-change-transform"
|
|
49
|
+
initial={reducedMotion ? false : { opacity: 0, rotate: 200, scaleX: 0.5, scaleY: 0.5, x: -140, y: 120 }}
|
|
50
|
+
animate={reducedMotion ? { opacity: 1, rotate: 0, scaleX: 1, scaleY: 1, x: 0, y: 0 } : { opacity: [0, 0, 1, 1, 0, 0], rotate: [200, 200, 10, 0, 0, 200, 200], scaleX: [0.5, 0.5, 1.3, 0.8, 1, 1], scaleY: [0.5, 0.5, 0.75, 1.2, 1, 1], x: [-140, -140, 2, 0, 0, -140, -140], y: [120, 120, -2, 0, 0, 120, 120] }}
|
|
51
|
+
transition={{ opacity: { duration: 1.68, times: [0, 0.2083, 0.25, 0.9998, 0.9999, 1], ease: ["linear", "linear", "linear", "easeIn", "linear"], repeat }, rotate: { duration: 1.68, times: [0, 0.2083, 0.3869, 0.506, 0.9998, 0.9999, 1], ease: ["linear", [0, 0, 0.15, 1], loadingSpring, "linear", "easeIn", "linear"], repeat }, scaleX: { duration: 1.68, times: [0, 0.2083, 0.3274, 0.3869, 0.506, 1], ease: ["linear", [0, 0, 0.15, 1], "easeOut", loadingSpring, "linear"], repeat }, scaleY: { duration: 1.68, times: [0, 0.2083, 0.3274, 0.3869, 0.506, 1], ease: ["linear", [0, 0, 0.15, 1], "easeOut", loadingSpring, "linear"], repeat }, x: { duration: 1.68, times: [0, 0.2083, 0.3869, 0.506, 0.9998, 0.9999, 1], ease: ["linear", [0, 0, 0.15, 1], loadingSpring, "linear", "easeIn", "linear"], repeat }, y: { duration: 1.68, times: [0, 0.2083, 0.3869, 0.506, 0.9998, 0.9999, 1], ease: ["linear", [0, 0, 0.15, 1], loadingSpring, "linear", "easeIn", "linear"], repeat } }}
|
|
52
|
+
/>
|
|
53
|
+
<motion.img
|
|
54
|
+
src={loadingAssets.corner}
|
|
55
|
+
alt=""
|
|
56
|
+
className="absolute bottom-[11.65%] right-[43.79%] h-[44.74%] w-[44.67%] will-change-transform"
|
|
57
|
+
initial={reducedMotion ? false : { opacity: 0, scaleX: 1, scaleY: 1, x: 0, y: 0 }}
|
|
58
|
+
animate={reducedMotion ? { opacity: 1, scaleX: 1, scaleY: 1, x: 0, y: 0 } : { opacity: [0, 1, 1, 0, 0], scaleX: [1, 1, 1.06, 1, 1], scaleY: [1, 1, 0.95, 1, 1], x: [0, 0, 4, 0, 0], y: [0, 0, -2, 0, 0] }}
|
|
59
|
+
transition={{ opacity: { duration: 1.68, times: [0, 0.1786, 0.9998, 0.9999, 1], ease: [[0, 0, 0.3, 1], "linear", "easeIn", "linear"], repeat }, scaleX: { duration: 1.68, times: [0, 0.3988, 0.4345, 0.6548, 1], ease: ["linear", [0.2, 0.9, 0.3, 1], loadingSpring, "linear"], repeat }, scaleY: { duration: 1.68, times: [0, 0.3988, 0.4345, 0.6548, 1], ease: ["linear", [0.2, 0.9, 0.3, 1], loadingSpring, "linear"], repeat }, x: { duration: 1.68, times: [0, 0.3988, 0.4345, 0.6845, 1], ease: ["linear", [0.2, 0.9, 0.3, 1], loadingSpring, "linear"], repeat }, y: { duration: 1.68, times: [0, 0.3988, 0.4345, 0.6845, 1], ease: ["linear", [0.2, 0.9, 0.3, 1], loadingSpring, "linear"], repeat } }}
|
|
60
|
+
/>
|
|
61
|
+
<motion.img
|
|
62
|
+
src={loadingAssets.mark}
|
|
63
|
+
alt=""
|
|
64
|
+
className="absolute bottom-[19.14%] right-[11.53%] h-[69.22%] w-[69.33%] will-change-transform"
|
|
65
|
+
initial={reducedMotion ? false : { opacity: 0, rotate: 0, scaleX: 0.85, scaleY: 0.85, x: 0, y: 0 }}
|
|
66
|
+
animate={reducedMotion ? { opacity: 1, rotate: 0, scaleX: 1, scaleY: 1, x: 0, y: 0 } : { opacity: [0, 1, 1, 0, 0], rotate: [0, 0, -3, 0, 0], scaleX: [0.85, 1, 1, 1.1, 0.95, 1, 1], scaleY: [0.85, 1, 1, 0.92, 1.05, 1, 1], x: [0, 0, 6, 0, 0], y: [0, 0, -4, 0, 0] }}
|
|
67
|
+
transition={{ opacity: { duration: 1.68, times: [0, 0.2083, 0.9998, 0.9999, 1], ease: [[0, 0, 0.3, 1], "linear", "easeIn", "linear"], repeat }, rotate: { duration: 1.68, times: [0, 0.3869, 0.4286, 0.6548, 1], ease: ["linear", [0.2, 0.9, 0.3, 1], loadingSpring, "linear"], repeat }, scaleX: { duration: 1.68, times: [0, 0.2083, 0.3869, 0.4167, 0.4643, 0.6548, 1], ease: [[0, 0, 0.3, 1], "linear", [0.2, 0.9, 0.3, 1], "easeOut", loadingSpring, "linear"], repeat }, scaleY: { duration: 1.68, times: [0, 0.2083, 0.3869, 0.4167, 0.4643, 0.6548, 1], ease: [[0, 0, 0.3, 1], "linear", [0.2, 0.9, 0.3, 1], "easeOut", loadingSpring, "linear"], repeat }, x: { duration: 1.68, times: [0, 0.3869, 0.4167, 0.6548, 1], ease: ["linear", [0.2, 0.9, 0.3, 1], loadingSpring, "linear"], repeat }, y: { duration: 1.68, times: [0, 0.3869, 0.4167, 0.6548, 1], ease: ["linear", [0.2, 0.9, 0.3, 1], loadingSpring, "linear"], repeat } }}
|
|
68
|
+
/>
|
|
69
|
+
</div>
|
|
70
|
+
<p className="m-0 text-center text-[14px] font-normal leading-[1.4] text-kumo-default">
|
|
71
|
+
Loading conversation...
|
|
72
|
+
</p>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
27
78
|
function clampChatWidth(width: number, containerWidth?: number) {
|
|
28
79
|
if (!isBrowser) {
|
|
29
80
|
return Math.max(MIN_CHAT_WIDTH, Math.min(DEFAULT_CHAT_WIDTH, width));
|
|
@@ -59,20 +110,20 @@ export interface CloudOsWorkspaceSplitProps {
|
|
|
59
110
|
workspace: ReactNode;
|
|
60
111
|
/** 关掉右栏时聊天占满整宽。 */
|
|
61
112
|
workspaceOpen: boolean;
|
|
62
|
-
/** 顶部那条推进条:回合进行中显示。 */
|
|
63
|
-
isAgentActive?: boolean;
|
|
64
113
|
/** 右栏收起时依然保留的窄边栏宽度(原文件的 Outputs rail)。 */
|
|
65
114
|
railWidth?: number;
|
|
66
115
|
rail?: ReactNode;
|
|
116
|
+
/** 覆盖整个聊天 + 工作区主体;页面级导航应放在分栏外。 */
|
|
117
|
+
isLoading?: boolean;
|
|
67
118
|
}
|
|
68
119
|
|
|
69
120
|
export function CloudOsWorkspaceSplit({
|
|
70
121
|
chat,
|
|
71
122
|
workspace,
|
|
72
123
|
workspaceOpen,
|
|
73
|
-
isAgentActive = false,
|
|
74
124
|
railWidth = 0,
|
|
75
125
|
rail,
|
|
126
|
+
isLoading = false,
|
|
76
127
|
}: CloudOsWorkspaceSplitProps) {
|
|
77
128
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
78
129
|
const resizeSessionRef = useRef<ResizeSession | null>(null);
|
|
@@ -187,18 +238,8 @@ export function CloudOsWorkspaceSplit({
|
|
|
187
238
|
<div
|
|
188
239
|
ref={containerRef}
|
|
189
240
|
className="relative flex h-full min-h-0 flex-1 overflow-hidden bg-kumo-base"
|
|
241
|
+
aria-busy={isLoading}
|
|
190
242
|
>
|
|
191
|
-
{isAgentActive && (
|
|
192
|
-
<div
|
|
193
|
-
className="absolute left-0 z-10 h-0"
|
|
194
|
-
style={{ top: 0, right: railWidth }}
|
|
195
|
-
>
|
|
196
|
-
<div className="absolute left-0 right-0 h-0.5 overflow-hidden bg-kumo-fill">
|
|
197
|
-
<div className="cos-progress-sweep absolute inset-y-0 w-1/3 bg-kumo-brand" />
|
|
198
|
-
</div>
|
|
199
|
-
</div>
|
|
200
|
-
)}
|
|
201
|
-
|
|
202
243
|
{/* ── 左:聊天 ──────────────────────────────────────────────────────── */}
|
|
203
244
|
<div
|
|
204
245
|
className={`flex h-full min-h-0 flex-shrink-0 flex-col ${transitionClass} ${
|
|
@@ -237,6 +278,8 @@ export function CloudOsWorkspaceSplit({
|
|
|
237
278
|
</div>
|
|
238
279
|
|
|
239
280
|
{rail}
|
|
281
|
+
|
|
282
|
+
{isLoading && <CloudOsConversationLoading />}
|
|
240
283
|
</div>
|
|
241
284
|
);
|
|
242
285
|
}
|
|
@@ -11,7 +11,7 @@ const buttonBaseClassName =
|
|
|
11
11
|
|
|
12
12
|
const buttonToneClassNames = {
|
|
13
13
|
primary:
|
|
14
|
-
"!h-9 bg-
|
|
14
|
+
"!h-9 bg-gradient-to-br from-[#FFD077] to-[#FF9938] px-3 text-white enabled:hover:opacity-90 disabled:opacity-50",
|
|
15
15
|
secondary:
|
|
16
16
|
"!h-8 border border-kumo-line bg-kumo-base px-3 text-kumo-default enabled:hover:bg-kumo-elevated disabled:opacity-40",
|
|
17
17
|
danger:
|
|
@@ -59,7 +59,7 @@ export function WorkshopIconButton({
|
|
|
59
59
|
}: WorkshopIconButtonProps) {
|
|
60
60
|
const toneClassName =
|
|
61
61
|
tone === "primary"
|
|
62
|
-
? "bg-kumo-
|
|
62
|
+
? "bg-[var(--primary,var(--color-kumo-brand))] text-[var(--primary-foreground,var(--text-color-kumo-inverse))] enabled:hover:bg-[var(--primary-hover,var(--color-kumo-brand-hover))]"
|
|
63
63
|
: danger
|
|
64
64
|
? "text-kumo-subtle enabled:hover:bg-kumo-danger-tint enabled:hover:text-kumo-danger"
|
|
65
65
|
: "text-kumo-subtle enabled:hover:bg-kumo-tint enabled:hover:text-kumo-default";
|