@springbrand/message-panel 0.1.3-alpha.23 → 0.1.3-alpha.25
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/assets/thinking-spark.svg +9 -0
- package/cloud-os/chat/cloud-os-chat-messages.tsx +218 -9
- 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 +27 -10
- package/cloud-os/chat/rich-blocks.tsx +88 -58
- package/cloud-os/chat/tool-rows.tsx +254 -53
- package/cloud-os/chat/transcript-model.ts +250 -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 +6 -0
- package/cloud-os/styles/cloud-os.css +12 -0
- package/demo/chat-scenarios.ts +123 -3
- package/package.json +1 -1
|
@@ -27,6 +27,7 @@ export interface CloudOsMessageMetadata extends UnknownRecord {
|
|
|
27
27
|
createdAt?: number;
|
|
28
28
|
error?: string;
|
|
29
29
|
interruptedByUser?: boolean;
|
|
30
|
+
turnId?: string;
|
|
30
31
|
turnDurationMs?: number;
|
|
31
32
|
turnStartedAt?: number;
|
|
32
33
|
turnStatus?: string;
|
|
@@ -45,6 +46,25 @@ export interface CloudOsAttachment {
|
|
|
45
46
|
mediaType?: string;
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
interface CloudOsActionPresentationFile {
|
|
50
|
+
type: "file";
|
|
51
|
+
url: string;
|
|
52
|
+
mediaType?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface CloudOsActionProduces {
|
|
56
|
+
type: "image" | "audio" | "video" | "document" | "archive" | "data" | "other";
|
|
57
|
+
mediaType?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface CloudOsActionPresentation {
|
|
61
|
+
type: "action-execution";
|
|
62
|
+
executionId: string;
|
|
63
|
+
state: "running" | "ready" | "failed" | "outcome_unknown";
|
|
64
|
+
produces?: CloudOsActionProduces;
|
|
65
|
+
content: CloudOsActionPresentationFile[];
|
|
66
|
+
}
|
|
67
|
+
|
|
48
68
|
export interface PlanStep {
|
|
49
69
|
text: string;
|
|
50
70
|
status: "pending" | "in_progress" | "done";
|
|
@@ -79,6 +99,11 @@ export type AssistantBlock =
|
|
|
79
99
|
mediaType?: string;
|
|
80
100
|
}
|
|
81
101
|
| { kind: "file"; key: string; file: CloudOsAttachment }
|
|
102
|
+
| {
|
|
103
|
+
kind: "actionPresentation";
|
|
104
|
+
key: string;
|
|
105
|
+
presentation: CloudOsActionPresentation;
|
|
106
|
+
}
|
|
82
107
|
| { kind: "error"; key: string; title: string; body?: string };
|
|
83
108
|
|
|
84
109
|
export interface ParallelTool {
|
|
@@ -118,8 +143,10 @@ export type CloudOsEntry =
|
|
|
118
143
|
type: "assistant";
|
|
119
144
|
key: string;
|
|
120
145
|
messageId: string;
|
|
146
|
+
turnId?: string;
|
|
121
147
|
blocks: AssistantBlock[];
|
|
122
148
|
copyText: string;
|
|
149
|
+
durationMs?: number;
|
|
123
150
|
timestamp?: number;
|
|
124
151
|
terminal?: { kind: "interrupted" | "error"; title: string; body?: string };
|
|
125
152
|
};
|
|
@@ -143,6 +170,10 @@ function nonNegativeNumber(value: unknown): number | undefined {
|
|
|
143
170
|
: undefined;
|
|
144
171
|
}
|
|
145
172
|
|
|
173
|
+
function nonEmptyString(value: unknown): string | undefined {
|
|
174
|
+
return typeof value === "string" && value.trim() ? value : undefined;
|
|
175
|
+
}
|
|
176
|
+
|
|
146
177
|
export function cloudOsMetadata(message: UIMessage): CloudOsMessageMetadata {
|
|
147
178
|
return recordOf(message.metadata) as CloudOsMessageMetadata;
|
|
148
179
|
}
|
|
@@ -197,6 +228,54 @@ export function formatFullTimestamp(timestamp: number): string {
|
|
|
197
228
|
});
|
|
198
229
|
}
|
|
199
230
|
|
|
231
|
+
function turnDuration(metadata: CloudOsMessageMetadata): number | undefined {
|
|
232
|
+
const explicit = nonNegativeNumber(metadata.turnDurationMs);
|
|
233
|
+
if (explicit !== undefined) return explicit;
|
|
234
|
+
const startedAt = nonNegativeNumber(metadata.turnStartedAt);
|
|
235
|
+
const completedAt = nonNegativeNumber(metadata.completedAt);
|
|
236
|
+
return startedAt !== undefined && completedAt !== undefined
|
|
237
|
+
? Math.max(0, completedAt - startedAt)
|
|
238
|
+
: undefined;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function isWorkBlock(block: AssistantBlock): boolean {
|
|
242
|
+
return (
|
|
243
|
+
block.kind === "reasoning" ||
|
|
244
|
+
block.kind === "toolGroup" ||
|
|
245
|
+
block.kind === "plan" ||
|
|
246
|
+
block.kind === "parallel" ||
|
|
247
|
+
block.kind === "subagents"
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* 把一条 assistant 消息拆成可折叠的工作过程与始终可见的最终内容。
|
|
253
|
+
* 最后一段工作之前的 text 是过程旁白;最后一次工作之后的 text 才是正文。
|
|
254
|
+
*/
|
|
255
|
+
export function partitionAssistantBlocks(blocks: readonly AssistantBlock[]): {
|
|
256
|
+
work: AssistantBlock[];
|
|
257
|
+
content: AssistantBlock[];
|
|
258
|
+
} {
|
|
259
|
+
let lastWorkIndex = -1;
|
|
260
|
+
blocks.forEach((block, index) => {
|
|
261
|
+
if (isWorkBlock(block)) lastWorkIndex = index;
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const work: AssistantBlock[] = [];
|
|
265
|
+
const content: AssistantBlock[] = [];
|
|
266
|
+
blocks.forEach((block, index) => {
|
|
267
|
+
if (
|
|
268
|
+
isWorkBlock(block) ||
|
|
269
|
+
(block.kind === "text" && index < lastWorkIndex)
|
|
270
|
+
) {
|
|
271
|
+
work.push(block);
|
|
272
|
+
} else {
|
|
273
|
+
content.push(block);
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
return { work, content };
|
|
277
|
+
}
|
|
278
|
+
|
|
200
279
|
function toolNameOf(part: unknown): string | null {
|
|
201
280
|
const type = String(recordOf(part).type ?? "");
|
|
202
281
|
if (type === "dynamic-tool") {
|
|
@@ -244,6 +323,71 @@ function normalizeStatus(value: unknown): ParallelTool["status"] {
|
|
|
244
323
|
return "pending";
|
|
245
324
|
}
|
|
246
325
|
|
|
326
|
+
function actionPresentationOf(value: unknown): CloudOsActionPresentation | null {
|
|
327
|
+
const presentation = recordOf(recordOf(value).presentation);
|
|
328
|
+
const state = presentation.state;
|
|
329
|
+
if (
|
|
330
|
+
presentation.type !== "action-execution" ||
|
|
331
|
+
typeof presentation.executionId !== "string" ||
|
|
332
|
+
!/^[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(
|
|
333
|
+
presentation.executionId,
|
|
334
|
+
) ||
|
|
335
|
+
(state !== "running" &&
|
|
336
|
+
state !== "ready" &&
|
|
337
|
+
state !== "failed" &&
|
|
338
|
+
state !== "outcome_unknown") ||
|
|
339
|
+
!Array.isArray(presentation.content)
|
|
340
|
+
) return null;
|
|
341
|
+
|
|
342
|
+
let produces: CloudOsActionProduces | undefined;
|
|
343
|
+
if (presentation.produces !== undefined) {
|
|
344
|
+
const hint = recordOf(presentation.produces);
|
|
345
|
+
if (
|
|
346
|
+
!["image", "audio", "video", "document", "archive", "data", "other"].includes(
|
|
347
|
+
String(hint.type),
|
|
348
|
+
) ||
|
|
349
|
+
(hint.mediaType !== undefined &&
|
|
350
|
+
(typeof hint.mediaType !== "string" || !/^[^\s/]+\/[^\s/]+$/.test(hint.mediaType)))
|
|
351
|
+
) return null;
|
|
352
|
+
produces = {
|
|
353
|
+
type: hint.type as CloudOsActionProduces["type"],
|
|
354
|
+
...(hint.mediaType === undefined ? {} : { mediaType: hint.mediaType as string }),
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
if (state !== "running" && produces) return null;
|
|
358
|
+
|
|
359
|
+
const content: CloudOsActionPresentationFile[] = [];
|
|
360
|
+
for (const value of presentation.content) {
|
|
361
|
+
const file = recordOf(value);
|
|
362
|
+
if (file.type !== "file" || typeof file.url !== "string") return null;
|
|
363
|
+
try {
|
|
364
|
+
if (new URL(file.url).protocol !== "https:") return null;
|
|
365
|
+
} catch {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
if (
|
|
369
|
+
file.mediaType !== undefined &&
|
|
370
|
+
(typeof file.mediaType !== "string" || !file.mediaType.trim())
|
|
371
|
+
) return null;
|
|
372
|
+
content.push({
|
|
373
|
+
type: "file" as const,
|
|
374
|
+
url: file.url,
|
|
375
|
+
...(file.mediaType !== undefined
|
|
376
|
+
? { mediaType: file.mediaType }
|
|
377
|
+
: {}),
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
if (state !== "ready" && content.length > 0) return null;
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
type: "action-execution",
|
|
384
|
+
executionId: presentation.executionId,
|
|
385
|
+
state,
|
|
386
|
+
...(produces ? { produces } : {}),
|
|
387
|
+
content,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
247
391
|
// ── 独立消息(非 assistant 回合)判定 ────────────────────────────────────────
|
|
248
392
|
|
|
249
393
|
function standaloneEntry(
|
|
@@ -427,6 +571,13 @@ function assistantBlocks(
|
|
|
427
571
|
if (toolName === null) return;
|
|
428
572
|
|
|
429
573
|
const call = toCloudOsToolCall(part, key, isActive);
|
|
574
|
+
const presentation = actionPresentationOf(call.output);
|
|
575
|
+
if (presentation) {
|
|
576
|
+
pendingCalls.push(call);
|
|
577
|
+
flush();
|
|
578
|
+
blocks.push({ kind: "actionPresentation", key, presentation });
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
430
581
|
|
|
431
582
|
if (call.awaitingApproval) {
|
|
432
583
|
const approvalId = approvalIdOf(part);
|
|
@@ -524,6 +675,9 @@ export function buildCloudOsEntries({
|
|
|
524
675
|
}
|
|
525
676
|
return -1;
|
|
526
677
|
})();
|
|
678
|
+
const activeTurnId = isActive && lastAssistantIndex >= 0
|
|
679
|
+
? nonEmptyString(cloudOsMetadata(messages[lastAssistantIndex]!).turnId)
|
|
680
|
+
: undefined;
|
|
527
681
|
|
|
528
682
|
messages.forEach((message, index) => {
|
|
529
683
|
if (message.role === "system") return;
|
|
@@ -578,8 +732,10 @@ export function buildCloudOsEntries({
|
|
|
578
732
|
type: "assistant",
|
|
579
733
|
key: `assistant-${message.id}-${index}`,
|
|
580
734
|
messageId: message.id,
|
|
735
|
+
turnId: nonEmptyString(metadata.turnId),
|
|
581
736
|
blocks,
|
|
582
737
|
copyText: messageText(message),
|
|
738
|
+
durationMs: turnDuration(metadata),
|
|
583
739
|
timestamp:
|
|
584
740
|
nonNegativeNumber(metadata.completedAt) ??
|
|
585
741
|
nonNegativeNumber(metadata.createdAt),
|
|
@@ -587,7 +743,100 @@ export function buildCloudOsEntries({
|
|
|
587
743
|
});
|
|
588
744
|
});
|
|
589
745
|
|
|
590
|
-
return
|
|
746
|
+
return aggregateCompletedTurnWork(
|
|
747
|
+
dropSupersededActionPresentations(dropSupersededPlans(entries)),
|
|
748
|
+
activeTurnId,
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/** Collapse every completed Turn's assistant segments into one display entry. */
|
|
753
|
+
function aggregateCompletedTurnWork(
|
|
754
|
+
entries: CloudOsEntry[],
|
|
755
|
+
activeTurnId: string | undefined,
|
|
756
|
+
): CloudOsEntry[] {
|
|
757
|
+
const indexesByTurn = new Map<string, number[]>();
|
|
758
|
+
entries.forEach((entry, index) => {
|
|
759
|
+
if (entry.type !== "assistant" || !entry.turnId) return;
|
|
760
|
+
const indexes = indexesByTurn.get(entry.turnId) ?? [];
|
|
761
|
+
indexes.push(index);
|
|
762
|
+
indexesByTurn.set(entry.turnId, indexes);
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
const hidden = new Set<number>();
|
|
766
|
+
const replacements = new Map<number, CloudOsEntry>();
|
|
767
|
+
for (const [turnId, indexes] of indexesByTurn) {
|
|
768
|
+
if (turnId === activeTurnId || indexes.length < 2) continue;
|
|
769
|
+
const assistantEntries = indexes.map(
|
|
770
|
+
(index) => entries[index] as Extract<CloudOsEntry, { type: "assistant" }>,
|
|
771
|
+
);
|
|
772
|
+
const anchorIndex = indexes[indexes.length - 1]!;
|
|
773
|
+
const anchor = assistantEntries[assistantEntries.length - 1]!;
|
|
774
|
+
const durations = assistantEntries.flatMap((entry) =>
|
|
775
|
+
entry.durationMs === undefined ? [] : [entry.durationMs]
|
|
776
|
+
);
|
|
777
|
+
const timestamps = assistantEntries.flatMap((entry) =>
|
|
778
|
+
entry.timestamp === undefined ? [] : [entry.timestamp]
|
|
779
|
+
);
|
|
780
|
+
|
|
781
|
+
indexes.slice(0, -1).forEach((index) => hidden.add(index));
|
|
782
|
+
replacements.set(anchorIndex, {
|
|
783
|
+
...anchor,
|
|
784
|
+
key: `assistant-turn-${turnId}`,
|
|
785
|
+
blocks: assistantEntries.flatMap((entry) => entry.blocks),
|
|
786
|
+
...(durations.length > 0 ? { durationMs: Math.max(...durations) } : {}),
|
|
787
|
+
...(timestamps.length > 0 ? { timestamp: Math.max(...timestamps) } : {}),
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
return entries.flatMap((entry, index) =>
|
|
792
|
+
hidden.has(index) ? [] : [replacements.get(index) ?? entry]
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function dropSupersededActionPresentations(entries: CloudOsEntry[]): CloudOsEntry[] {
|
|
797
|
+
const snapshots = new Map<string, {
|
|
798
|
+
entryIndex: number;
|
|
799
|
+
blockIndex: number;
|
|
800
|
+
presentation: CloudOsActionPresentation;
|
|
801
|
+
}>();
|
|
802
|
+
entries.forEach((entry, entryIndex) => {
|
|
803
|
+
if (entry.type !== "assistant") return;
|
|
804
|
+
entry.blocks.forEach((block, blockIndex) => {
|
|
805
|
+
if (block.kind !== "actionPresentation") return;
|
|
806
|
+
const current = snapshots.get(block.presentation.executionId);
|
|
807
|
+
if (!current) {
|
|
808
|
+
snapshots.set(block.presentation.executionId, {
|
|
809
|
+
entryIndex,
|
|
810
|
+
blockIndex,
|
|
811
|
+
presentation: block.presentation,
|
|
812
|
+
});
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
if (block.presentation.state !== "running") {
|
|
816
|
+
current.presentation = block.presentation;
|
|
817
|
+
} else if (current.presentation.state === "running") {
|
|
818
|
+
current.presentation = {
|
|
819
|
+
...block.presentation,
|
|
820
|
+
produces: block.presentation.produces ?? current.presentation.produces,
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
});
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
return entries.map((entry, entryIndex) =>
|
|
827
|
+
entry.type !== "assistant"
|
|
828
|
+
? entry
|
|
829
|
+
: {
|
|
830
|
+
...entry,
|
|
831
|
+
blocks: entry.blocks.flatMap<AssistantBlock>((block, blockIndex) => {
|
|
832
|
+
if (block.kind !== "actionPresentation") return [block];
|
|
833
|
+
const snapshot = snapshots.get(block.presentation.executionId)!;
|
|
834
|
+
return snapshot.entryIndex === entryIndex && snapshot.blockIndex === blockIndex
|
|
835
|
+
? [{ ...block, presentation: snapshot.presentation }]
|
|
836
|
+
: [];
|
|
837
|
+
}),
|
|
838
|
+
}
|
|
839
|
+
);
|
|
591
840
|
}
|
|
592
841
|
|
|
593
842
|
/**
|
|
@@ -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
|
@@ -20,6 +20,7 @@ export type {
|
|
|
20
20
|
FileViewPlacement,
|
|
21
21
|
FileViewProps,
|
|
22
22
|
FileViewStatus,
|
|
23
|
+
FileViewVariant,
|
|
23
24
|
} from "./file-view";
|
|
24
25
|
export {
|
|
25
26
|
CloudOsConversationLoading,
|
|
@@ -64,8 +65,12 @@ export type { CloudOsUrlResolver } from "./chat/markdown-message";
|
|
|
64
65
|
export {
|
|
65
66
|
ThinkingTraceRow,
|
|
66
67
|
ToolCallDetails,
|
|
68
|
+
ToolGroupDetails,
|
|
67
69
|
ToolGroupRow,
|
|
70
|
+
WorkDescriptionRow,
|
|
68
71
|
WorkIcon,
|
|
72
|
+
WorkTraceDisclosure,
|
|
73
|
+
formatWorkDuration,
|
|
69
74
|
} from "./chat/tool-rows";
|
|
70
75
|
export {
|
|
71
76
|
ApprovalBlock,
|
|
@@ -90,6 +95,7 @@ export {
|
|
|
90
95
|
formatClockTime,
|
|
91
96
|
formatFullTimestamp,
|
|
92
97
|
messageText,
|
|
98
|
+
partitionAssistantBlocks,
|
|
93
99
|
requestedCapabilitiesOf,
|
|
94
100
|
rhythmTopClass,
|
|
95
101
|
} from "./chat/transcript-model";
|
|
@@ -51,6 +51,8 @@
|
|
|
51
51
|
--color-kumo-success: oklch(72.3% 0.219 149.579);
|
|
52
52
|
--color-kumo-success-tint: oklch(92% 0.08 150);
|
|
53
53
|
|
|
54
|
+
--color-operation-base: #ffffff;
|
|
55
|
+
|
|
54
56
|
--color-kumo-tip-shadow: #14111014;
|
|
55
57
|
--color-kumo-tip-stroke: transparent;
|
|
56
58
|
|
|
@@ -73,6 +75,10 @@
|
|
|
73
75
|
--color-cos-status-live: oklch(72.3% 0.219 149.579);
|
|
74
76
|
--color-cos-status-draft: oklch(68.1% 0.162 75.834);
|
|
75
77
|
--color-cos-status-building: oklch(62.3% 0.214 259.815);
|
|
78
|
+
--text-color-cos-work-description: rgba(0, 0, 0, 0.5);
|
|
79
|
+
--text-color-cos-work-description-subtle: rgba(0, 0, 0, 0.7);
|
|
80
|
+
--text-color-cos-tool-group: rgba(0, 0, 0, 0.5);
|
|
81
|
+
--text-color-cos-output: #000000b3;
|
|
76
82
|
}
|
|
77
83
|
|
|
78
84
|
/* ── 作用域根:通用 token 只在 cloud-os 子树内生效 ─────────────────────────── */
|
|
@@ -152,6 +158,8 @@
|
|
|
152
158
|
--color-kumo-success: oklch(52.7% 0.154 150.069);
|
|
153
159
|
--color-kumo-success-tint: oklch(0.25 0.06 150.069);
|
|
154
160
|
|
|
161
|
+
--color-operation-base: var(--color-kumo-base);
|
|
162
|
+
|
|
155
163
|
--color-kumo-tip-shadow: #00000066;
|
|
156
164
|
--color-kumo-tip-stroke: oklch(0.34 0.022 285);
|
|
157
165
|
|
|
@@ -172,6 +180,10 @@
|
|
|
172
180
|
--color-cos-status-live: oklch(79.2% 0.209 151.711);
|
|
173
181
|
--color-cos-status-draft: oklch(82.8% 0.189 84.429);
|
|
174
182
|
--color-cos-status-building: oklch(74% 0.16 250);
|
|
183
|
+
--text-color-cos-work-description: var(--text-color-kumo-subtle);
|
|
184
|
+
--text-color-cos-work-description-subtle: var(--text-color-kumo-subtle);
|
|
185
|
+
--text-color-cos-tool-group: var(--text-color-kumo-inactive);
|
|
186
|
+
--text-color-cos-output: var(--text-color-kumo-subtle);
|
|
175
187
|
|
|
176
188
|
--color-selection-bg: color-mix(in srgb, var(--color-kumo-brand) 28%, transparent);
|
|
177
189
|
--color-selection-text: oklch(0.97 0.006 285);
|