@springbrand/message-panel 0.1.3-alpha.22 → 0.1.3-alpha.24
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/chat/cloud-os-chat-messages.tsx +89 -0
- 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 +8 -0
- package/cloud-os/chat/transcript-model.ts +143 -1
- package/cloud-os/composer/cloud-os-chat-input.tsx +1 -21
- package/cloud-os/file-view.tsx +155 -10
- package/cloud-os/index.ts +1 -0
- package/cloud-os/styles/cloud-os.css +4 -0
- package/demo/chat-scenarios.ts +114 -0
- package/package.json +1 -1
|
@@ -35,6 +35,8 @@ import {
|
|
|
35
35
|
} from "./rich-blocks";
|
|
36
36
|
import { ThinkingTraceRow, ToolGroupRow } from "./tool-rows";
|
|
37
37
|
import { CloudOsActivityIndicator } from "./activity-indicator";
|
|
38
|
+
import { ProductGeneration } from "./image-generation";
|
|
39
|
+
import { GenerationLoader } from "./loading-state";
|
|
38
40
|
import { CapabilityChip } from "../capability-chip";
|
|
39
41
|
import {
|
|
40
42
|
renderFileView,
|
|
@@ -47,6 +49,7 @@ import {
|
|
|
47
49
|
formatFullTimestamp,
|
|
48
50
|
rhythmTopClass,
|
|
49
51
|
type CloudOsEntry,
|
|
52
|
+
type CloudOsActionPresentation,
|
|
50
53
|
} from "./transcript-model";
|
|
51
54
|
|
|
52
55
|
export type CloudOsChatStatus = "ready" | "submitted" | "streaming" | "error";
|
|
@@ -200,6 +203,83 @@ function UserBubble({
|
|
|
200
203
|
);
|
|
201
204
|
}
|
|
202
205
|
|
|
206
|
+
function ActionPresentationBlock({
|
|
207
|
+
presentation,
|
|
208
|
+
renderFile,
|
|
209
|
+
resolveUrl,
|
|
210
|
+
}: {
|
|
211
|
+
presentation: CloudOsActionPresentation;
|
|
212
|
+
renderFile?: FileRenderer;
|
|
213
|
+
resolveUrl?: CloudOsUrlResolver;
|
|
214
|
+
}) {
|
|
215
|
+
const { state } = presentation;
|
|
216
|
+
if (state === "ready") {
|
|
217
|
+
return (
|
|
218
|
+
<div
|
|
219
|
+
data-action-presentation={state}
|
|
220
|
+
role="status"
|
|
221
|
+
aria-label="Completed"
|
|
222
|
+
className="space-y-2"
|
|
223
|
+
>
|
|
224
|
+
{presentation.content.length === 0
|
|
225
|
+
? <p className="m-0 text-[14px] leading-5 text-kumo-subtle">Completed</p>
|
|
226
|
+
: presentation.content.map((file, index) => (
|
|
227
|
+
<Fragment key={`${file.url}:${index}`}>
|
|
228
|
+
{renderFileView(renderFile, {
|
|
229
|
+
file: {
|
|
230
|
+
...file,
|
|
231
|
+
filename: "Generated file",
|
|
232
|
+
url: resolveUrl?.(file.url) ?? file.url,
|
|
233
|
+
},
|
|
234
|
+
placement: "assistant-message",
|
|
235
|
+
variant: "product",
|
|
236
|
+
})}
|
|
237
|
+
</Fragment>
|
|
238
|
+
))}
|
|
239
|
+
</div>
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const product = presentation.produces;
|
|
244
|
+
const productName = product?.type ?? "other";
|
|
245
|
+
const isFileProduct = productName !== "data" && productName !== "other";
|
|
246
|
+
const body = state === "running"
|
|
247
|
+
? isFileProduct
|
|
248
|
+
? `Generating ${productName}${product?.mediaType ? ` (${product.mediaType})` : ""}`
|
|
249
|
+
: "Generating"
|
|
250
|
+
: state === "outcome_unknown"
|
|
251
|
+
? "The provider may have completed this action. Check the status before trying again."
|
|
252
|
+
: undefined;
|
|
253
|
+
if (state === "running") {
|
|
254
|
+
return (
|
|
255
|
+
<div
|
|
256
|
+
data-action-presentation={state}
|
|
257
|
+
data-product-type={productName}
|
|
258
|
+
role="status"
|
|
259
|
+
aria-label={body}
|
|
260
|
+
>
|
|
261
|
+
{isFileProduct
|
|
262
|
+
? <ProductGeneration type={productName} mediaType={product?.mediaType} />
|
|
263
|
+
: <GenerationLoader />}
|
|
264
|
+
</div>
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
return (
|
|
268
|
+
<div
|
|
269
|
+
data-action-presentation={state}
|
|
270
|
+
role={state === "failed" ? "alert" : "status"}
|
|
271
|
+
className="inline-flex max-w-72 items-start rounded-xl border border-kumo-line bg-kumo-elevated px-3 py-2 text-[13px] leading-5"
|
|
272
|
+
>
|
|
273
|
+
<span>
|
|
274
|
+
<strong className="block font-medium text-kumo-default">
|
|
275
|
+
{state === "failed" ? "Generation failed" : "Status unavailable"}
|
|
276
|
+
</strong>
|
|
277
|
+
{body && <span className="block text-kumo-subtle">{body}</span>}
|
|
278
|
+
</span>
|
|
279
|
+
</div>
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
203
283
|
export function CloudOsChatMessages({
|
|
204
284
|
messages,
|
|
205
285
|
status,
|
|
@@ -564,6 +644,15 @@ export function CloudOsChatMessages({
|
|
|
564
644
|
})}
|
|
565
645
|
</Fragment>
|
|
566
646
|
);
|
|
647
|
+
case "actionPresentation":
|
|
648
|
+
return (
|
|
649
|
+
<ActionPresentationBlock
|
|
650
|
+
key={block.key}
|
|
651
|
+
presentation={block.presentation}
|
|
652
|
+
renderFile={renderFile}
|
|
653
|
+
resolveUrl={resolveUrl}
|
|
654
|
+
/>
|
|
655
|
+
);
|
|
567
656
|
case "error":
|
|
568
657
|
return (
|
|
569
658
|
<ErrorBlock
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { CloudOsActionPresentation } from "./transcript-model";
|
|
2
|
+
|
|
3
|
+
export type ProductType = NonNullable<CloudOsActionPresentation["produces"]>["type"];
|
|
4
|
+
|
|
5
|
+
export const PRODUCT_PRESETS = {
|
|
6
|
+
image: { frame: "aspect-square w-52", columns: 6, rows: 6 },
|
|
7
|
+
video: { frame: "aspect-video w-64", columns: 8, rows: 4 },
|
|
8
|
+
audio: { frame: "h-24 w-64", columns: 9, rows: 2 },
|
|
9
|
+
document: { frame: "aspect-[4/5] w-44", columns: 5, rows: 7 },
|
|
10
|
+
archive: { frame: "h-28 w-44", columns: 5, rows: 3 },
|
|
11
|
+
data: { frame: "h-32 w-52", columns: 6, rows: 3 },
|
|
12
|
+
other: { frame: "h-28 w-44", columns: 5, rows: 3 },
|
|
13
|
+
} satisfies Record<ProductType, { frame: string; columns: number; rows: number }>;
|
|
14
|
+
|
|
15
|
+
export function productTypeForMediaType(mediaType?: string): ProductType {
|
|
16
|
+
const type = mediaType?.toLowerCase() ?? "";
|
|
17
|
+
if (type.startsWith("image/")) return "image";
|
|
18
|
+
if (type.startsWith("audio/")) return "audio";
|
|
19
|
+
if (type.startsWith("video/")) return "video";
|
|
20
|
+
if (/zip|archive|compressed|gzip|tar/.test(type)) return "archive";
|
|
21
|
+
if (/json|csv|spreadsheet|excel|parquet|sql/.test(type)) return "data";
|
|
22
|
+
if (type.startsWith("text/") || /pdf|document|word|rtf|presentation/.test(type)) {
|
|
23
|
+
return "document";
|
|
24
|
+
}
|
|
25
|
+
return "other";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Product-aware adaptation of https://www.assistant-ui.com/elements/image-generation. */
|
|
29
|
+
export function ProductGeneration({
|
|
30
|
+
type,
|
|
31
|
+
mediaType,
|
|
32
|
+
}: {
|
|
33
|
+
type: ProductType;
|
|
34
|
+
mediaType?: string;
|
|
35
|
+
}) {
|
|
36
|
+
const { frame, columns, rows } = PRODUCT_PRESETS[type];
|
|
37
|
+
const dots = Array.from({ length: columns * rows }, (_, index) => index);
|
|
38
|
+
return (
|
|
39
|
+
<div className="flex w-fit max-w-full flex-col gap-2.5" data-product-generation={type}>
|
|
40
|
+
<div className={`themed-thumbnail-shadow relative max-w-full overflow-hidden rounded-2xl border border-kumo-line bg-kumo-elevated ${frame}`}>
|
|
41
|
+
<div
|
|
42
|
+
className="absolute inset-0 grid place-items-center p-5"
|
|
43
|
+
data-generation-grid={`${columns}x${rows}`}
|
|
44
|
+
style={{
|
|
45
|
+
gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`,
|
|
46
|
+
gridTemplateRows: `repeat(${rows}, minmax(0, 1fr))`,
|
|
47
|
+
}}
|
|
48
|
+
aria-hidden="true"
|
|
49
|
+
>
|
|
50
|
+
{dots.map((dot) => {
|
|
51
|
+
const row = Math.floor(dot / columns);
|
|
52
|
+
const column = dot % columns;
|
|
53
|
+
return (
|
|
54
|
+
<span
|
|
55
|
+
key={dot}
|
|
56
|
+
data-generation-dot=""
|
|
57
|
+
className="size-1 rounded-full bg-kumo-fill-hover animate-pulse motion-reduce:animate-none"
|
|
58
|
+
style={{ animationDelay: `${(row + column) * 90}ms` }}
|
|
59
|
+
/>
|
|
60
|
+
);
|
|
61
|
+
})}
|
|
62
|
+
</div>
|
|
63
|
+
{mediaType && (
|
|
64
|
+
<span className="absolute end-2.5 top-2.5 font-mono text-[10px] text-kumo-inactive">
|
|
65
|
+
{mediaType}
|
|
66
|
+
</span>
|
|
67
|
+
)}
|
|
68
|
+
</div>
|
|
69
|
+
<p className="m-0 min-w-0 truncate text-xs text-kumo-inactive">
|
|
70
|
+
<span className="capitalize text-kumo-subtle">{type}</span>
|
|
71
|
+
<span aria-hidden="true"> · </span>
|
|
72
|
+
<span className="cos-thinking-shimmer motion-reduce:animate-none">Generating</span>
|
|
73
|
+
</p>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const CELLS = Array.from({ length: 9 }, (_, index) => index);
|
|
2
|
+
|
|
3
|
+
/** CSS-driven adaptation of https://www.assistant-ui.com/elements/loading-state. */
|
|
4
|
+
export function GenerationLoader() {
|
|
5
|
+
return (
|
|
6
|
+
<div
|
|
7
|
+
data-slot="generation-loader"
|
|
8
|
+
className="inline-flex flex-col items-center gap-3 py-2"
|
|
9
|
+
>
|
|
10
|
+
<div aria-hidden="true" className="grid grid-cols-3 gap-1">
|
|
11
|
+
{CELLS.map((cell) => (
|
|
12
|
+
<span
|
|
13
|
+
key={cell}
|
|
14
|
+
data-loading-cell=""
|
|
15
|
+
className="size-2 animate-pulse rounded-full bg-[var(--text-color-kumo-default)] motion-reduce:animate-none"
|
|
16
|
+
style={{ animationDelay: `${cell * 100}ms` }}
|
|
17
|
+
/>
|
|
18
|
+
))}
|
|
19
|
+
</div>
|
|
20
|
+
<span className="cos-thinking-shimmer text-sm text-kumo-inactive motion-reduce:animate-none">
|
|
21
|
+
Generating
|
|
22
|
+
</span>
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
export type CloudOsUrlResolver = (url: string) => string | null | undefined;
|
|
15
15
|
|
|
16
16
|
const ALLOWED_PROTOCOLS = new Set(["http:", "https:", "mailto:"]);
|
|
17
|
+
const CJK_TEXT = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u;
|
|
17
18
|
|
|
18
19
|
export function safeExternalUrl(href: string | undefined): string | undefined {
|
|
19
20
|
if (!href) return undefined;
|
|
@@ -109,6 +110,13 @@ export const MarkdownMessage = memo(function MarkdownMessage({
|
|
|
109
110
|
);
|
|
110
111
|
return (
|
|
111
112
|
<Streamdown
|
|
113
|
+
animated={
|
|
114
|
+
streaming
|
|
115
|
+
? CJK_TEXT.test(message)
|
|
116
|
+
? { sep: "char", duration: 100, stagger: 5 }
|
|
117
|
+
: true
|
|
118
|
+
: false
|
|
119
|
+
}
|
|
112
120
|
className="space-y-0"
|
|
113
121
|
controls={false}
|
|
114
122
|
mode={streaming ? "streaming" : "static"}
|
|
@@ -45,6 +45,25 @@ export interface CloudOsAttachment {
|
|
|
45
45
|
mediaType?: string;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
interface CloudOsActionPresentationFile {
|
|
49
|
+
type: "file";
|
|
50
|
+
url: string;
|
|
51
|
+
mediaType?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface CloudOsActionProduces {
|
|
55
|
+
type: "image" | "audio" | "video" | "document" | "archive" | "data" | "other";
|
|
56
|
+
mediaType?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface CloudOsActionPresentation {
|
|
60
|
+
type: "action-execution";
|
|
61
|
+
executionId: string;
|
|
62
|
+
state: "running" | "ready" | "failed" | "outcome_unknown";
|
|
63
|
+
produces?: CloudOsActionProduces;
|
|
64
|
+
content: CloudOsActionPresentationFile[];
|
|
65
|
+
}
|
|
66
|
+
|
|
48
67
|
export interface PlanStep {
|
|
49
68
|
text: string;
|
|
50
69
|
status: "pending" | "in_progress" | "done";
|
|
@@ -79,6 +98,11 @@ export type AssistantBlock =
|
|
|
79
98
|
mediaType?: string;
|
|
80
99
|
}
|
|
81
100
|
| { kind: "file"; key: string; file: CloudOsAttachment }
|
|
101
|
+
| {
|
|
102
|
+
kind: "actionPresentation";
|
|
103
|
+
key: string;
|
|
104
|
+
presentation: CloudOsActionPresentation;
|
|
105
|
+
}
|
|
82
106
|
| { kind: "error"; key: string; title: string; body?: string };
|
|
83
107
|
|
|
84
108
|
export interface ParallelTool {
|
|
@@ -244,6 +268,71 @@ function normalizeStatus(value: unknown): ParallelTool["status"] {
|
|
|
244
268
|
return "pending";
|
|
245
269
|
}
|
|
246
270
|
|
|
271
|
+
function actionPresentationOf(value: unknown): CloudOsActionPresentation | null {
|
|
272
|
+
const presentation = recordOf(recordOf(value).presentation);
|
|
273
|
+
const state = presentation.state;
|
|
274
|
+
if (
|
|
275
|
+
presentation.type !== "action-execution" ||
|
|
276
|
+
typeof presentation.executionId !== "string" ||
|
|
277
|
+
!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
|
|
278
|
+
presentation.executionId,
|
|
279
|
+
) ||
|
|
280
|
+
(state !== "running" &&
|
|
281
|
+
state !== "ready" &&
|
|
282
|
+
state !== "failed" &&
|
|
283
|
+
state !== "outcome_unknown") ||
|
|
284
|
+
!Array.isArray(presentation.content)
|
|
285
|
+
) return null;
|
|
286
|
+
|
|
287
|
+
let produces: CloudOsActionProduces | undefined;
|
|
288
|
+
if (presentation.produces !== undefined) {
|
|
289
|
+
const hint = recordOf(presentation.produces);
|
|
290
|
+
if (
|
|
291
|
+
!["image", "audio", "video", "document", "archive", "data", "other"].includes(
|
|
292
|
+
String(hint.type),
|
|
293
|
+
) ||
|
|
294
|
+
(hint.mediaType !== undefined &&
|
|
295
|
+
(typeof hint.mediaType !== "string" || !/^[^\s/]+\/[^\s/]+$/.test(hint.mediaType)))
|
|
296
|
+
) return null;
|
|
297
|
+
produces = {
|
|
298
|
+
type: hint.type as CloudOsActionProduces["type"],
|
|
299
|
+
...(hint.mediaType === undefined ? {} : { mediaType: hint.mediaType as string }),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
if (state !== "running" && produces) return null;
|
|
303
|
+
|
|
304
|
+
const content: CloudOsActionPresentationFile[] = [];
|
|
305
|
+
for (const value of presentation.content) {
|
|
306
|
+
const file = recordOf(value);
|
|
307
|
+
if (file.type !== "file" || typeof file.url !== "string") return null;
|
|
308
|
+
try {
|
|
309
|
+
if (new URL(file.url).protocol !== "https:") return null;
|
|
310
|
+
} catch {
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
if (
|
|
314
|
+
file.mediaType !== undefined &&
|
|
315
|
+
(typeof file.mediaType !== "string" || !file.mediaType.trim())
|
|
316
|
+
) return null;
|
|
317
|
+
content.push({
|
|
318
|
+
type: "file" as const,
|
|
319
|
+
url: file.url,
|
|
320
|
+
...(file.mediaType !== undefined
|
|
321
|
+
? { mediaType: file.mediaType }
|
|
322
|
+
: {}),
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
if (state !== "ready" && content.length > 0) return null;
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
type: "action-execution",
|
|
329
|
+
executionId: presentation.executionId,
|
|
330
|
+
state,
|
|
331
|
+
...(produces ? { produces } : {}),
|
|
332
|
+
content,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
247
336
|
// ── 独立消息(非 assistant 回合)判定 ────────────────────────────────────────
|
|
248
337
|
|
|
249
338
|
function standaloneEntry(
|
|
@@ -427,6 +516,13 @@ function assistantBlocks(
|
|
|
427
516
|
if (toolName === null) return;
|
|
428
517
|
|
|
429
518
|
const call = toCloudOsToolCall(part, key, isActive);
|
|
519
|
+
const presentation = actionPresentationOf(call.output);
|
|
520
|
+
if (presentation) {
|
|
521
|
+
pendingCalls.push(call);
|
|
522
|
+
flush();
|
|
523
|
+
blocks.push({ kind: "actionPresentation", key, presentation });
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
430
526
|
|
|
431
527
|
if (call.awaitingApproval) {
|
|
432
528
|
const approvalId = approvalIdOf(part);
|
|
@@ -587,7 +683,53 @@ export function buildCloudOsEntries({
|
|
|
587
683
|
});
|
|
588
684
|
});
|
|
589
685
|
|
|
590
|
-
return dropSupersededPlans(entries);
|
|
686
|
+
return dropSupersededActionPresentations(dropSupersededPlans(entries));
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function dropSupersededActionPresentations(entries: CloudOsEntry[]): CloudOsEntry[] {
|
|
690
|
+
const snapshots = new Map<string, {
|
|
691
|
+
entryIndex: number;
|
|
692
|
+
blockIndex: number;
|
|
693
|
+
presentation: CloudOsActionPresentation;
|
|
694
|
+
}>();
|
|
695
|
+
entries.forEach((entry, entryIndex) => {
|
|
696
|
+
if (entry.type !== "assistant") return;
|
|
697
|
+
entry.blocks.forEach((block, blockIndex) => {
|
|
698
|
+
if (block.kind !== "actionPresentation") return;
|
|
699
|
+
const current = snapshots.get(block.presentation.executionId);
|
|
700
|
+
if (!current) {
|
|
701
|
+
snapshots.set(block.presentation.executionId, {
|
|
702
|
+
entryIndex,
|
|
703
|
+
blockIndex,
|
|
704
|
+
presentation: block.presentation,
|
|
705
|
+
});
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
if (block.presentation.state !== "running") {
|
|
709
|
+
current.presentation = block.presentation;
|
|
710
|
+
} else if (current.presentation.state === "running") {
|
|
711
|
+
current.presentation = {
|
|
712
|
+
...block.presentation,
|
|
713
|
+
produces: block.presentation.produces ?? current.presentation.produces,
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
return entries.map((entry, entryIndex) =>
|
|
720
|
+
entry.type !== "assistant"
|
|
721
|
+
? entry
|
|
722
|
+
: {
|
|
723
|
+
...entry,
|
|
724
|
+
blocks: entry.blocks.flatMap<AssistantBlock>((block, blockIndex) => {
|
|
725
|
+
if (block.kind !== "actionPresentation") return [block];
|
|
726
|
+
const snapshot = snapshots.get(block.presentation.executionId)!;
|
|
727
|
+
return snapshot.entryIndex === entryIndex && snapshot.blockIndex === blockIndex
|
|
728
|
+
? [{ ...block, presentation: snapshot.presentation }]
|
|
729
|
+
: [];
|
|
730
|
+
}),
|
|
731
|
+
}
|
|
732
|
+
);
|
|
591
733
|
}
|
|
592
734
|
|
|
593
735
|
/**
|
|
@@ -3,7 +3,6 @@ import {
|
|
|
3
3
|
File as FileIcon,
|
|
4
4
|
Plug,
|
|
5
5
|
Plus,
|
|
6
|
-
Terminal,
|
|
7
6
|
} from "@phosphor-icons/react";
|
|
8
7
|
import {
|
|
9
8
|
AssistantRuntimeProvider,
|
|
@@ -106,7 +105,7 @@ export interface CloudOsChatInputProps {
|
|
|
106
105
|
* 传了就以它为准来决定显示发送键还是停止键。
|
|
107
106
|
*/
|
|
108
107
|
turnActive?: boolean;
|
|
109
|
-
/**
|
|
108
|
+
/** 可用的斜杠命令。 */
|
|
110
109
|
commands?: readonly CloudOsComposerCommand[];
|
|
111
110
|
capabilities?: readonly CloudOsCapabilityView[];
|
|
112
111
|
onCommandSelect?: (command: CloudOsComposerCommand) => void;
|
|
@@ -521,25 +520,6 @@ function CloudOsChatInputContent({
|
|
|
521
520
|
</span>
|
|
522
521
|
</DropdownMenu.Item>
|
|
523
522
|
)}
|
|
524
|
-
{commands.length > 0 && (
|
|
525
|
-
<>
|
|
526
|
-
{commands.map((command) => (
|
|
527
|
-
<DropdownMenu.Item
|
|
528
|
-
key={`${command.prefix ?? "/"}${command.name}`}
|
|
529
|
-
onClick={() => selectCommand(command, false)}
|
|
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"
|
|
531
|
-
>
|
|
532
|
-
<span className="mr-2 inline-flex h-4 w-4 items-center justify-center text-kumo-inactive">
|
|
533
|
-
<Terminal size={14} />
|
|
534
|
-
</span>
|
|
535
|
-
<span className="min-w-0 flex-1 truncate">
|
|
536
|
-
{commandLabel(command)}
|
|
537
|
-
</span>
|
|
538
|
-
</DropdownMenu.Item>
|
|
539
|
-
))}
|
|
540
|
-
<div className="my-1 border-t border-kumo-line/70" />
|
|
541
|
-
</>
|
|
542
|
-
)}
|
|
543
523
|
{onFilesSelected && (
|
|
544
524
|
<DropdownMenu.Item
|
|
545
525
|
onClick={() => attachmentInputRef.current?.click()}
|
package/cloud-os/file-view.tsx
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import {
|
|
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";
|
|
2
11
|
import {
|
|
3
12
|
CheckIcon,
|
|
4
13
|
FileArchiveIcon,
|
|
@@ -14,6 +23,21 @@ import {
|
|
|
14
23
|
useState,
|
|
15
24
|
type ComponentType,
|
|
16
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>;
|
|
17
41
|
|
|
18
42
|
export type FileViewPlacement =
|
|
19
43
|
| "assistant-message"
|
|
@@ -21,6 +45,7 @@ export type FileViewPlacement =
|
|
|
21
45
|
| "user-message";
|
|
22
46
|
|
|
23
47
|
export type FileViewStatus = "error" | "ready" | "uploading";
|
|
48
|
+
export type FileViewVariant = "compact" | "product";
|
|
24
49
|
|
|
25
50
|
export interface FileViewFile {
|
|
26
51
|
url?: string;
|
|
@@ -32,6 +57,7 @@ export interface FileViewFile {
|
|
|
32
57
|
export interface FileViewProps {
|
|
33
58
|
file: FileViewFile;
|
|
34
59
|
placement: FileViewPlacement;
|
|
60
|
+
variant?: FileViewVariant;
|
|
35
61
|
status?: FileViewStatus;
|
|
36
62
|
progress?: number;
|
|
37
63
|
onRemove?: () => void;
|
|
@@ -59,10 +85,12 @@ function ImageFileView({
|
|
|
59
85
|
src,
|
|
60
86
|
label,
|
|
61
87
|
placement,
|
|
88
|
+
frameClassName,
|
|
62
89
|
}: {
|
|
63
90
|
src: string;
|
|
64
91
|
label: string;
|
|
65
92
|
placement: Exclude<FileViewPlacement, "composer">;
|
|
93
|
+
frameClassName?: string;
|
|
66
94
|
}) {
|
|
67
95
|
const imageRef = useRef<HTMLImageElement>(null);
|
|
68
96
|
const [loadedSrc, setLoadedSrc] = useState<string>();
|
|
@@ -78,9 +106,11 @@ function ImageFileView({
|
|
|
78
106
|
}, [src]);
|
|
79
107
|
|
|
80
108
|
const assistant = placement === "assistant-message";
|
|
81
|
-
const previewClassName =
|
|
82
|
-
?
|
|
83
|
-
:
|
|
109
|
+
const previewClassName = frameClassName
|
|
110
|
+
? `themed-thumbnail-shadow relative inline-block max-w-full overflow-hidden rounded-2xl border border-kumo-line align-bottom ${frameClassName}`
|
|
111
|
+
: assistant
|
|
112
|
+
? "relative inline-block max-w-full overflow-hidden rounded-xl border border-kumo-line align-bottom"
|
|
113
|
+
: "themed-thumbnail-shadow relative inline-block max-w-64 overflow-hidden rounded-xl border border-kumo-line align-bottom";
|
|
84
114
|
|
|
85
115
|
return (
|
|
86
116
|
<span
|
|
@@ -93,9 +123,11 @@ function ImageFileView({
|
|
|
93
123
|
src={src}
|
|
94
124
|
alt={label}
|
|
95
125
|
className={`${
|
|
96
|
-
|
|
97
|
-
? "block
|
|
98
|
-
:
|
|
126
|
+
frameClassName
|
|
127
|
+
? "block size-full object-contain"
|
|
128
|
+
: assistant
|
|
129
|
+
? "block max-h-[28rem] max-w-full object-contain"
|
|
130
|
+
: "block max-h-52 max-w-64 object-cover"
|
|
99
131
|
} ${loaded && !error ? "" : "invisible"}`}
|
|
100
132
|
decoding="async"
|
|
101
133
|
loading="lazy"
|
|
@@ -115,7 +147,7 @@ function ImageFileView({
|
|
|
115
147
|
className="size-6 animate-pulse motion-reduce:animate-none"
|
|
116
148
|
aria-hidden="true"
|
|
117
149
|
/>
|
|
118
|
-
<span className="sr-only">Loading
|
|
150
|
+
<span className="sr-only">Loading preview...</span>
|
|
119
151
|
</span>
|
|
120
152
|
)}
|
|
121
153
|
{error && (
|
|
@@ -123,7 +155,7 @@ function ImageFileView({
|
|
|
123
155
|
className="absolute inset-0 flex min-h-24 min-w-36 items-center justify-center bg-kumo-elevated text-kumo-danger"
|
|
124
156
|
data-image-state="error"
|
|
125
157
|
role="img"
|
|
126
|
-
aria-label=
|
|
158
|
+
aria-label="Unable to load preview"
|
|
127
159
|
>
|
|
128
160
|
<FileImageIcon className="size-6" aria-hidden="true" />
|
|
129
161
|
</span>
|
|
@@ -132,12 +164,100 @@ function ImageFileView({
|
|
|
132
164
|
);
|
|
133
165
|
}
|
|
134
166
|
|
|
167
|
+
function ProductFileView({
|
|
168
|
+
file,
|
|
169
|
+
label,
|
|
170
|
+
placement,
|
|
171
|
+
}: {
|
|
172
|
+
file: FileViewFile;
|
|
173
|
+
label: string;
|
|
174
|
+
placement: Exclude<FileViewPlacement, "composer">;
|
|
175
|
+
}) {
|
|
176
|
+
const type = productTypeForMediaType(file.mediaType);
|
|
177
|
+
const { frame } = PRODUCT_PRESETS[type];
|
|
178
|
+
const image = type === "image" && file.url;
|
|
179
|
+
const video = type === "video" && file.url;
|
|
180
|
+
const ProductIcon = PRODUCT_ICONS[type];
|
|
181
|
+
const preview = image
|
|
182
|
+
? (
|
|
183
|
+
<ImageFileView
|
|
184
|
+
src={file.url!}
|
|
185
|
+
label={label}
|
|
186
|
+
placement={placement}
|
|
187
|
+
frameClassName={frame}
|
|
188
|
+
/>
|
|
189
|
+
)
|
|
190
|
+
: video
|
|
191
|
+
? (
|
|
192
|
+
<video
|
|
193
|
+
src={file.url}
|
|
194
|
+
controls
|
|
195
|
+
preload="metadata"
|
|
196
|
+
playsInline
|
|
197
|
+
aria-label={label}
|
|
198
|
+
className={`themed-thumbnail-shadow block max-w-full rounded-2xl border border-kumo-line bg-kumo-elevated object-contain ${frame}`}
|
|
199
|
+
data-video-preview=""
|
|
200
|
+
/>
|
|
201
|
+
)
|
|
202
|
+
: (
|
|
203
|
+
<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}`}>
|
|
204
|
+
<ProductIcon
|
|
205
|
+
size={24}
|
|
206
|
+
weight="light"
|
|
207
|
+
className="text-kumo-subtle"
|
|
208
|
+
aria-hidden="true"
|
|
209
|
+
data-product-icon={type}
|
|
210
|
+
/>
|
|
211
|
+
{file.mediaType && (
|
|
212
|
+
<span className="absolute end-2.5 top-2.5 font-mono text-[10px] text-kumo-inactive">
|
|
213
|
+
{file.mediaType}
|
|
214
|
+
</span>
|
|
215
|
+
)}
|
|
216
|
+
</span>
|
|
217
|
+
);
|
|
218
|
+
const content = (
|
|
219
|
+
<span className="flex w-fit max-w-full flex-col gap-2.5" data-product-file-view={type}>
|
|
220
|
+
{preview}
|
|
221
|
+
<span className="flex min-w-0 max-w-full items-center gap-1 text-xs text-kumo-inactive">
|
|
222
|
+
<span className="capitalize text-kumo-subtle">{type}</span>
|
|
223
|
+
<span aria-hidden="true">·</span>
|
|
224
|
+
<span className="min-w-0 truncate">{label}</span>
|
|
225
|
+
</span>
|
|
226
|
+
</span>
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
return file.url && !video ? (
|
|
230
|
+
<a
|
|
231
|
+
href={file.url}
|
|
232
|
+
target="_blank"
|
|
233
|
+
rel="noopener noreferrer"
|
|
234
|
+
aria-label={`Open ${label}`}
|
|
235
|
+
className="inline-block max-w-full rounded-2xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-kumo-brand/40"
|
|
236
|
+
data-file-view={image ? undefined : ""}
|
|
237
|
+
data-placement={placement}
|
|
238
|
+
data-file-variant="product"
|
|
239
|
+
>
|
|
240
|
+
{content}
|
|
241
|
+
</a>
|
|
242
|
+
) : (
|
|
243
|
+
<span
|
|
244
|
+
className="inline-block max-w-full"
|
|
245
|
+
data-file-view=""
|
|
246
|
+
data-placement={placement}
|
|
247
|
+
data-file-variant="product"
|
|
248
|
+
>
|
|
249
|
+
{content}
|
|
250
|
+
</span>
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
135
254
|
/**
|
|
136
255
|
* Cloud OS 的默认文件视图。宿主可通过 `renderFile` 在同一 seam 替换它。
|
|
137
256
|
*/
|
|
138
257
|
export function FileView({
|
|
139
258
|
file,
|
|
140
259
|
placement,
|
|
260
|
+
variant = "compact",
|
|
141
261
|
status = "ready",
|
|
142
262
|
progress = 0,
|
|
143
263
|
onRemove,
|
|
@@ -238,6 +358,10 @@ export function FileView({
|
|
|
238
358
|
);
|
|
239
359
|
}
|
|
240
360
|
|
|
361
|
+
if (variant === "product") {
|
|
362
|
+
return <ProductFileView file={file} label={label} placement={placement} />;
|
|
363
|
+
}
|
|
364
|
+
|
|
241
365
|
if (isImage) {
|
|
242
366
|
return (
|
|
243
367
|
<ImageFileView src={file.url!} label={label} placement={placement} />
|
|
@@ -251,7 +375,28 @@ export function FileView({
|
|
|
251
375
|
data-placement={placement}
|
|
252
376
|
>
|
|
253
377
|
<FileIcon size={13} className="text-kumo-inactive" />
|
|
254
|
-
{label}
|
|
378
|
+
<span className="min-w-0 truncate">{label}</span>
|
|
379
|
+
{file.url && (
|
|
380
|
+
<span className="ms-1 inline-flex items-center gap-1.5 border-s border-kumo-line ps-2">
|
|
381
|
+
<a
|
|
382
|
+
href={file.url}
|
|
383
|
+
target="_blank"
|
|
384
|
+
rel="noopener noreferrer"
|
|
385
|
+
aria-label={`Open ${label}`}
|
|
386
|
+
className="rounded text-kumo-default hover:text-kumo-default-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-kumo-brand/40"
|
|
387
|
+
>
|
|
388
|
+
Open
|
|
389
|
+
</a>
|
|
390
|
+
<a
|
|
391
|
+
href={file.url}
|
|
392
|
+
download={label}
|
|
393
|
+
aria-label={`Download ${label}`}
|
|
394
|
+
className="rounded text-kumo-default hover:text-kumo-default-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-kumo-brand/40"
|
|
395
|
+
>
|
|
396
|
+
Download
|
|
397
|
+
</a>
|
|
398
|
+
</span>
|
|
399
|
+
)}
|
|
255
400
|
</span>
|
|
256
401
|
);
|
|
257
402
|
}
|
package/cloud-os/index.ts
CHANGED
package/demo/chat-scenarios.ts
CHANGED
|
@@ -37,6 +37,8 @@ export type DemoScenario =
|
|
|
37
37
|
| "temporary_agent"
|
|
38
38
|
| "agent_tools"
|
|
39
39
|
| "background_agent"
|
|
40
|
+
| "action_loading"
|
|
41
|
+
| "action_files"
|
|
40
42
|
| "schedule_create"
|
|
41
43
|
| "schedule"
|
|
42
44
|
| "schedule_cancel"
|
|
@@ -119,6 +121,18 @@ export const scenarios: Record<DemoScenario, DemoScenarioMeta> = {
|
|
|
119
121
|
status: "ready",
|
|
120
122
|
flowStep: 3,
|
|
121
123
|
},
|
|
124
|
+
action_loading: {
|
|
125
|
+
label: "Action product loading",
|
|
126
|
+
description: "Preview loading UI for every Action product type.",
|
|
127
|
+
status: "streaming",
|
|
128
|
+
flowStep: 2,
|
|
129
|
+
},
|
|
130
|
+
action_files: {
|
|
131
|
+
label: "Action file results",
|
|
132
|
+
description: "Preview image, audio, video, document, spreadsheet, archive, and unknown files.",
|
|
133
|
+
status: "ready",
|
|
134
|
+
flowStep: 3,
|
|
135
|
+
},
|
|
122
136
|
schedule_create: {
|
|
123
137
|
label: "Create scheduled task",
|
|
124
138
|
description: "Review the task, cadence, and timezone before creating the schedule.",
|
|
@@ -246,6 +260,11 @@ function userMessage(prompt: string): UIMessage {
|
|
|
246
260
|
} as UIMessage;
|
|
247
261
|
}
|
|
248
262
|
|
|
263
|
+
function actionUserMessage(prompt: string): UIMessage {
|
|
264
|
+
const message = userMessage(prompt);
|
|
265
|
+
return { ...message, parts: message.parts.filter((part) => part.type === "text") };
|
|
266
|
+
}
|
|
267
|
+
|
|
249
268
|
export interface DemoAskUserResponse {
|
|
250
269
|
answers: Array<{ selections: string[]; text?: string }>;
|
|
251
270
|
}
|
|
@@ -503,6 +522,95 @@ const backgroundResult = {
|
|
|
503
522
|
],
|
|
504
523
|
} as unknown as UIMessage;
|
|
505
524
|
|
|
525
|
+
const actionProductHints = [
|
|
526
|
+
{ type: "image", mediaType: "image/png" },
|
|
527
|
+
{ type: "audio", mediaType: "audio/mpeg" },
|
|
528
|
+
{ type: "video", mediaType: "video/mp4" },
|
|
529
|
+
{ type: "document", mediaType: "application/pdf" },
|
|
530
|
+
{ type: "archive", mediaType: "application/zip" },
|
|
531
|
+
{ type: "data", mediaType: "application/json" },
|
|
532
|
+
{ type: "other" },
|
|
533
|
+
] as const;
|
|
534
|
+
|
|
535
|
+
function demoExecutionId(index: number) {
|
|
536
|
+
return `00000000-0000-4000-8000-${String(index + 1).padStart(12, "0")}`;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const actionProductLoading = {
|
|
540
|
+
id: "camel-demo-action-product-loading",
|
|
541
|
+
role: "assistant",
|
|
542
|
+
parts: actionProductHints.map((produces, index) => ({
|
|
543
|
+
type: "dynamic-tool",
|
|
544
|
+
toolCallId: `camel-demo-action-loading-${index}`,
|
|
545
|
+
toolName: `action_generate_${produces.type}`,
|
|
546
|
+
state: "output-available",
|
|
547
|
+
input: {},
|
|
548
|
+
output: {
|
|
549
|
+
status: "running",
|
|
550
|
+
executionId: demoExecutionId(index),
|
|
551
|
+
presentation: {
|
|
552
|
+
type: "action-execution",
|
|
553
|
+
executionId: demoExecutionId(index),
|
|
554
|
+
state: "running",
|
|
555
|
+
produces,
|
|
556
|
+
content: [],
|
|
557
|
+
},
|
|
558
|
+
},
|
|
559
|
+
})),
|
|
560
|
+
} as unknown as UIMessage;
|
|
561
|
+
|
|
562
|
+
const actionFileFixtures = [
|
|
563
|
+
{
|
|
564
|
+
name: "image",
|
|
565
|
+
mediaType: "image/png",
|
|
566
|
+
url: "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?auto=format&fit=crop&w=1024&q=80",
|
|
567
|
+
},
|
|
568
|
+
{ name: "audio", mediaType: "audio/mpeg", url: "https://files.example/action-demo/audio.mp3" },
|
|
569
|
+
{ name: "video", mediaType: "video/mp4", url: "https://files.example/action-demo/video.mp4" },
|
|
570
|
+
{ name: "pdf", mediaType: "application/pdf", url: "https://files.example/action-demo/report.pdf" },
|
|
571
|
+
{ name: "markdown", mediaType: "text/markdown", url: "https://files.example/action-demo/report.md" },
|
|
572
|
+
{
|
|
573
|
+
name: "spreadsheet",
|
|
574
|
+
mediaType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
575
|
+
url: "https://files.example/action-demo/data.xlsx",
|
|
576
|
+
},
|
|
577
|
+
{ name: "archive", mediaType: "application/zip", url: "https://files.example/action-demo/files.zip" },
|
|
578
|
+
{
|
|
579
|
+
name: "unknown",
|
|
580
|
+
mediaType: "application/octet-stream",
|
|
581
|
+
url: "https://files.example/action-demo/result.bin",
|
|
582
|
+
},
|
|
583
|
+
] as const;
|
|
584
|
+
|
|
585
|
+
const actionFileResults = {
|
|
586
|
+
id: "camel-demo-action-file-results",
|
|
587
|
+
role: "assistant",
|
|
588
|
+
parts: [
|
|
589
|
+
...actionFileFixtures.map((file, index) => ({
|
|
590
|
+
type: "dynamic-tool",
|
|
591
|
+
toolCallId: `camel-demo-action-file-${index}`,
|
|
592
|
+
toolName: `action_generate_${file.name}`,
|
|
593
|
+
state: "output-available",
|
|
594
|
+
input: {},
|
|
595
|
+
output: {
|
|
596
|
+
status: "succeeded",
|
|
597
|
+
executionId: demoExecutionId(index + actionProductHints.length),
|
|
598
|
+
result: { kind: "file", content_type: file.mediaType, url: file.url },
|
|
599
|
+
presentation: {
|
|
600
|
+
type: "action-execution",
|
|
601
|
+
executionId: demoExecutionId(index + actionProductHints.length),
|
|
602
|
+
state: "ready",
|
|
603
|
+
content: [{ type: "file", url: file.url, mediaType: file.mediaType }],
|
|
604
|
+
},
|
|
605
|
+
},
|
|
606
|
+
})),
|
|
607
|
+
{
|
|
608
|
+
type: "text",
|
|
609
|
+
text: "All Action file result types are rendered above from structured output.",
|
|
610
|
+
},
|
|
611
|
+
],
|
|
612
|
+
} as unknown as UIMessage;
|
|
613
|
+
|
|
506
614
|
export const demoScheduleInput = {
|
|
507
615
|
label: "Weekly competitor report",
|
|
508
616
|
prompt: "Generate and send this week's competitor report.",
|
|
@@ -917,6 +1025,12 @@ export function messagesForScenario(
|
|
|
917
1025
|
if (scenario === "background_agent") {
|
|
918
1026
|
return [...conversation, backgroundDispatch, backgroundResult];
|
|
919
1027
|
}
|
|
1028
|
+
if (scenario === "action_loading") {
|
|
1029
|
+
return [noticeMessage(scenario), actionUserMessage(prompt), actionProductLoading];
|
|
1030
|
+
}
|
|
1031
|
+
if (scenario === "action_files") {
|
|
1032
|
+
return [noticeMessage(scenario), actionUserMessage(prompt), actionFileResults];
|
|
1033
|
+
}
|
|
920
1034
|
if (scenario === "schedule_create") {
|
|
921
1035
|
return [...conversation, scheduleRequest];
|
|
922
1036
|
}
|