@springbrand/message-panel 0.1.3-alpha.9 → 0.2.0-alpha.37
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 +4 -3
- 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-star.svg +9 -0
- package/cloud-os/capability-chip.tsx +1 -1
- package/cloud-os/chat/cloud-os-chat-messages.tsx +441 -98
- package/cloud-os/chat/code-runner.tsx +106 -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 +144 -41
- package/cloud-os/chat/range.ts +8 -0
- package/cloud-os/chat/rich-blocks.tsx +285 -227
- package/cloud-os/chat/surfaces.tsx +90 -0
- package/cloud-os/chat/tool-call.tsx +94 -0
- package/cloud-os/chat/tool-presentation.ts +31 -22
- package/cloud-os/chat/tool-rows.tsx +195 -143
- package/cloud-os/chat/tool-timeline.tsx +123 -0
- package/cloud-os/chat/transcript-model.ts +258 -20
- package/cloud-os/chat/web-search.tsx +135 -0
- package/cloud-os/composer/cloud-os-chat-input.tsx +29 -88
- package/cloud-os/composer/cloud-os-model-select.tsx +105 -0
- package/cloud-os/file-view.tsx +195 -12
- package/cloud-os/index.ts +24 -3
- package/cloud-os/layout/cloud-os-workspace-split.tsx +60 -3
- package/cloud-os/primitives/collapsible.tsx +21 -0
- package/cloud-os/primitives/workshop-controls.tsx +2 -2
- package/cloud-os/styles/cloud-os.css +132 -1
- package/demo/camel-chat-showcase.tsx +21 -13
- package/demo/chat-scenarios.ts +214 -40
- package/demo/cloud-os-chat-showcase.tsx +37 -14
- package/demo/fixtures.ts +4 -5
- package/demo/message-panel-gallery.tsx +16 -2
- package/package.json +8 -4
- package/springbrand-pet/LICENSE.bloub +21 -0
- package/springbrand-pet/bot/cycles.test.ts +221 -0
- package/springbrand-pet/bot/cycles.ts +225 -0
- package/springbrand-pet/bot/decor.ts +285 -0
- package/springbrand-pet/bot/engine.test.ts +543 -0
- package/springbrand-pet/bot/engine.ts +558 -0
- package/springbrand-pet/bot/expressions.test.ts +135 -0
- package/springbrand-pet/bot/expressions.ts +192 -0
- package/springbrand-pet/bot/eyefit.ts +455 -0
- package/springbrand-pet/bot/face.test.ts +101 -0
- package/springbrand-pet/bot/face.ts +179 -0
- package/springbrand-pet/bot/math.ts +42 -0
- package/springbrand-pet/bot/profiles.ts +22 -0
- package/springbrand-pet/bot/repere.ts +31 -0
- package/springbrand-pet/bot/shape.test.ts +97 -0
- package/springbrand-pet/bot/shape.ts +310 -0
- package/springbrand-pet/bot/skins.test.ts +325 -0
- package/springbrand-pet/bot/skins.ts +161 -0
- package/springbrand-pet/bot/states.ts +616 -0
- package/springbrand-pet/drag.test.ts +19 -0
- package/springbrand-pet/index.tsx +569 -0
- package/springbrand-pet/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/springbrand-pet/vitest.config.ts +9 -0
- 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.tsx +2 -2
- package/src/contracts.ts +0 -2
- package/src/message-panel.tsx +1 -24
- package/src/parts/index.tsx +103 -64
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Brain,
|
|
3
|
-
CaretDown,
|
|
4
|
-
Check,
|
|
5
3
|
File as FileIcon,
|
|
6
4
|
Plug,
|
|
7
5
|
Plus,
|
|
8
|
-
Terminal,
|
|
9
6
|
} from "@phosphor-icons/react";
|
|
10
7
|
import {
|
|
11
8
|
AssistantRuntimeProvider,
|
|
@@ -37,6 +34,10 @@ import {
|
|
|
37
34
|
renderFileView,
|
|
38
35
|
type FileRenderer,
|
|
39
36
|
} from "../file-view";
|
|
37
|
+
import {
|
|
38
|
+
CloudOsModelSelect,
|
|
39
|
+
type CloudOsModelOption,
|
|
40
|
+
} from "./cloud-os-model-select";
|
|
40
41
|
|
|
41
42
|
const isComposingKeyEvent = (event: {
|
|
42
43
|
isComposing?: boolean;
|
|
@@ -56,11 +57,6 @@ const isComposingKeyEvent = (event: {
|
|
|
56
57
|
* assistant-ui ComposerTriggerPopover。
|
|
57
58
|
*/
|
|
58
59
|
|
|
59
|
-
export interface CloudOsModelOption {
|
|
60
|
-
id: string;
|
|
61
|
-
name: string;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
60
|
export interface CloudOsComposerCommand {
|
|
65
61
|
prefix?: "/" | "$";
|
|
66
62
|
name: string;
|
|
@@ -68,7 +64,7 @@ export interface CloudOsComposerCommand {
|
|
|
68
64
|
}
|
|
69
65
|
|
|
70
66
|
export interface CloudOsCapabilityView {
|
|
71
|
-
kind: "skill" | "plan";
|
|
67
|
+
kind: "skill" | "tool" | "plan";
|
|
72
68
|
name: string;
|
|
73
69
|
label: string;
|
|
74
70
|
}
|
|
@@ -109,7 +105,7 @@ export interface CloudOsChatInputProps {
|
|
|
109
105
|
* 传了就以它为准来决定显示发送键还是停止键。
|
|
110
106
|
*/
|
|
111
107
|
turnActive?: boolean;
|
|
112
|
-
/**
|
|
108
|
+
/** 可用的斜杠命令。 */
|
|
113
109
|
commands?: readonly CloudOsComposerCommand[];
|
|
114
110
|
capabilities?: readonly CloudOsCapabilityView[];
|
|
115
111
|
onCommandSelect?: (command: CloudOsComposerCommand) => void;
|
|
@@ -218,11 +214,6 @@ function CloudOsChatInputContent({
|
|
|
218
214
|
(value.trim().length > 0 || hasReadyAttachment) &&
|
|
219
215
|
!hasUnreadyAttachment;
|
|
220
216
|
|
|
221
|
-
const selectedModelLabel =
|
|
222
|
-
selectedModel === null
|
|
223
|
-
? "No agent"
|
|
224
|
-
: (models.find((model) => model.id === selectedModel)?.name ?? selectedModel);
|
|
225
|
-
|
|
226
217
|
useEffect(() => {
|
|
227
218
|
const element = textareaRef.current;
|
|
228
219
|
if (element) autoResizeTextarea(element, minRows, maxRows);
|
|
@@ -329,7 +320,7 @@ function CloudOsChatInputContent({
|
|
|
329
320
|
|
|
330
321
|
return (
|
|
331
322
|
// isolation: isolate 把输入框内部的 z-index 关起来,免得盖到 portal 出去的模型下拉。
|
|
332
|
-
<div className="cos-chat-input-root relative isolate
|
|
323
|
+
<div className="cos-chat-input-root relative isolate pl-[calc(env(safe-area-inset-left)+1rem)] pr-[calc(env(safe-area-inset-right)+1rem)] pb-[calc(env(safe-area-inset-bottom)+1rem)]">
|
|
333
324
|
<ComposerTriggerPopover
|
|
334
325
|
char="/"
|
|
335
326
|
{...slash}
|
|
@@ -508,7 +499,7 @@ function CloudOsChatInputContent({
|
|
|
508
499
|
render={
|
|
509
500
|
<button
|
|
510
501
|
type="button"
|
|
511
|
-
className="group flex h-
|
|
502
|
+
className="group flex h-11 w-11 flex-shrink-0 cursor-pointer items-center justify-center rounded-lg text-kumo-inactive transition-[background-color,color,transform] duration-150 ease-out hover:bg-kumo-tint hover:text-kumo-subtle focus-visible:bg-kumo-tint focus-visible:text-kumo-subtle focus-visible:outline-none active:scale-[0.96] data-[state=open]:bg-kumo-tint data-[state=open]:text-kumo-subtle md:h-8 md:w-8"
|
|
512
503
|
aria-label="Open chat options"
|
|
513
504
|
>
|
|
514
505
|
<Plus size={18} />
|
|
@@ -519,7 +510,7 @@ function CloudOsChatInputContent({
|
|
|
519
510
|
{onToggleThinkingTraces && (
|
|
520
511
|
<DropdownMenu.Item
|
|
521
512
|
onClick={onToggleThinkingTraces}
|
|
522
|
-
className="
|
|
513
|
+
className="min-h-11 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 md:min-h-0"
|
|
523
514
|
>
|
|
524
515
|
<span className="mr-2 inline-flex h-4 w-4 items-center justify-center text-kumo-inactive">
|
|
525
516
|
<Brain size={14} />
|
|
@@ -529,29 +520,10 @@ function CloudOsChatInputContent({
|
|
|
529
520
|
</span>
|
|
530
521
|
</DropdownMenu.Item>
|
|
531
522
|
)}
|
|
532
|
-
{commands.length > 0 && (
|
|
533
|
-
<>
|
|
534
|
-
{commands.map((command) => (
|
|
535
|
-
<DropdownMenu.Item
|
|
536
|
-
key={`${command.prefix ?? "/"}${command.name}`}
|
|
537
|
-
onClick={() => selectCommand(command, false)}
|
|
538
|
-
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"
|
|
539
|
-
>
|
|
540
|
-
<span className="mr-2 inline-flex h-4 w-4 items-center justify-center text-kumo-inactive">
|
|
541
|
-
<Terminal size={14} />
|
|
542
|
-
</span>
|
|
543
|
-
<span className="min-w-0 flex-1 truncate">
|
|
544
|
-
{commandLabel(command)}
|
|
545
|
-
</span>
|
|
546
|
-
</DropdownMenu.Item>
|
|
547
|
-
))}
|
|
548
|
-
<div className="my-1 border-t border-kumo-line/70" />
|
|
549
|
-
</>
|
|
550
|
-
)}
|
|
551
523
|
{onFilesSelected && (
|
|
552
524
|
<DropdownMenu.Item
|
|
553
525
|
onClick={() => attachmentInputRef.current?.click()}
|
|
554
|
-
className="
|
|
526
|
+
className="min-h-11 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 md:min-h-0"
|
|
555
527
|
>
|
|
556
528
|
<span className="mr-2 inline-flex h-4 w-4 items-center justify-center text-kumo-inactive">
|
|
557
529
|
<FileIcon size={14} />
|
|
@@ -565,7 +537,7 @@ function CloudOsChatInputContent({
|
|
|
565
537
|
<button
|
|
566
538
|
type="button"
|
|
567
539
|
onClick={onAttachResource}
|
|
568
|
-
className="inline-flex h-
|
|
540
|
+
className="inline-flex h-11 flex-shrink-0 cursor-pointer items-center gap-1.5 rounded-lg px-2 text-[13px] leading-none tracking-[-0.25px] text-kumo-inactive transition-[background-color,color,transform] duration-150 ease-out hover:bg-kumo-tint hover:text-kumo-subtle focus-visible:bg-kumo-tint focus-visible:text-kumo-subtle focus-visible:outline-none active:scale-[0.97] md:h-8"
|
|
569
541
|
>
|
|
570
542
|
<Plug size={15} className="flex-shrink-0" />
|
|
571
543
|
<span className="cos-attach-label leading-none">
|
|
@@ -578,52 +550,19 @@ function CloudOsChatInputContent({
|
|
|
578
550
|
|
|
579
551
|
<div className="ml-auto flex min-w-0 flex-shrink items-center gap-1.5">
|
|
580
552
|
{onModelChange && (
|
|
581
|
-
<
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
aria-label="Select model"
|
|
588
|
-
>
|
|
589
|
-
<span className="min-w-0 truncate">{selectedModelLabel}</span>
|
|
590
|
-
<CaretDown
|
|
591
|
-
size={12}
|
|
592
|
-
weight="bold"
|
|
593
|
-
className="flex-shrink-0 text-kumo-inactive transition-transform duration-150 ease-out group-data-[state=open]:rotate-180"
|
|
594
|
-
/>
|
|
595
|
-
</button>
|
|
596
|
-
}
|
|
597
|
-
/>
|
|
598
|
-
<DropdownMenu.Content
|
|
599
|
-
align="end"
|
|
600
|
-
className="themed-floating-shadow-lg !min-w-[190px] rounded-2xl border border-kumo-line/70 bg-kumo-base p-1"
|
|
601
|
-
>
|
|
602
|
-
{models.map((model) => (
|
|
603
|
-
<DropdownMenu.Item
|
|
604
|
-
key={model.id}
|
|
605
|
-
onClick={() => onModelChange(model.id)}
|
|
606
|
-
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"
|
|
607
|
-
>
|
|
608
|
-
<span className="min-w-0 flex-1 truncate">{model.name}</span>
|
|
609
|
-
{selectedModel === model.id && (
|
|
610
|
-
<Check
|
|
611
|
-
size={12}
|
|
612
|
-
weight="bold"
|
|
613
|
-
className="ml-3 flex-shrink-0 text-kumo-inactive"
|
|
614
|
-
/>
|
|
615
|
-
)}
|
|
616
|
-
</DropdownMenu.Item>
|
|
617
|
-
))}
|
|
618
|
-
</DropdownMenu.Content>
|
|
619
|
-
</DropdownMenu>
|
|
553
|
+
<CloudOsModelSelect
|
|
554
|
+
models={models}
|
|
555
|
+
selectedModel={selectedModel}
|
|
556
|
+
onModelChange={onModelChange}
|
|
557
|
+
variant="cloud-os"
|
|
558
|
+
/>
|
|
620
559
|
)}
|
|
621
560
|
{!showStopButton && submitControl}
|
|
622
561
|
{showStopButton ? (
|
|
623
562
|
<WorkshopIconButton
|
|
624
563
|
onClick={onStop}
|
|
625
564
|
tone="primary"
|
|
626
|
-
className="!h-
|
|
565
|
+
className="!h-11 !w-11 md:!h-8 md:!w-8"
|
|
627
566
|
aria-label="Stop agent"
|
|
628
567
|
>
|
|
629
568
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
|
|
@@ -631,25 +570,27 @@ function CloudOsChatInputContent({
|
|
|
631
570
|
</svg>
|
|
632
571
|
</WorkshopIconButton>
|
|
633
572
|
) : (
|
|
634
|
-
<
|
|
573
|
+
<button
|
|
574
|
+
type="button"
|
|
635
575
|
onClick={onSubmit}
|
|
636
576
|
disabled={!canSend}
|
|
637
|
-
|
|
638
|
-
className="!h-8 !w-8 disabled:cursor-not-allowed disabled:opacity-30"
|
|
577
|
+
className="grid size-11 shrink-0 place-items-center rounded-full bg-[var(--primary,var(--color-kumo-brand))] text-[var(--primary-foreground,var(--text-color-kumo-inverse))] shadow-sm transition-colors enabled:hover:bg-[var(--primary-hover,var(--color-kumo-brand-hover))] disabled:cursor-not-allowed disabled:opacity-20 md:size-8"
|
|
639
578
|
aria-label="Send message"
|
|
640
579
|
>
|
|
641
580
|
<svg
|
|
642
|
-
|
|
643
|
-
height="16"
|
|
581
|
+
className="size-4"
|
|
644
582
|
viewBox="0 0 24 24"
|
|
645
583
|
fill="none"
|
|
646
584
|
stroke="currentColor"
|
|
647
|
-
strokeWidth="
|
|
585
|
+
strokeWidth="1.8"
|
|
586
|
+
strokeLinecap="round"
|
|
587
|
+
strokeLinejoin="round"
|
|
588
|
+
aria-hidden="true"
|
|
648
589
|
>
|
|
649
|
-
<
|
|
650
|
-
<
|
|
590
|
+
<path d="m5 12 7-7 7 7" />
|
|
591
|
+
<path d="M12 19V5" />
|
|
651
592
|
</svg>
|
|
652
|
-
</
|
|
593
|
+
</button>
|
|
653
594
|
)}
|
|
654
595
|
</div>
|
|
655
596
|
</div>
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { CaretDown } from "@phosphor-icons/react";
|
|
2
|
+
import { cn } from "../internal/cn";
|
|
3
|
+
import { DropdownMenu } from "../primitives/dropdown-menu";
|
|
4
|
+
|
|
5
|
+
const selectedIcon = new URL("../assets/model-selected.svg", import.meta.url).href;
|
|
6
|
+
|
|
7
|
+
export interface CloudOsModelOption {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
disabledLabel?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface CloudOsModelSelectProps {
|
|
15
|
+
models: readonly CloudOsModelOption[];
|
|
16
|
+
selectedModel?: string | null;
|
|
17
|
+
onModelChange: (modelId: string) => void;
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
variant?: "default" | "cloud-os";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function CloudOsModelSelect({
|
|
23
|
+
models,
|
|
24
|
+
selectedModel = null,
|
|
25
|
+
onModelChange,
|
|
26
|
+
disabled = false,
|
|
27
|
+
variant = "default",
|
|
28
|
+
}: CloudOsModelSelectProps) {
|
|
29
|
+
if (models.length === 0) return null;
|
|
30
|
+
|
|
31
|
+
const selectedLabel =
|
|
32
|
+
models.find((model) => model.id === selectedModel)?.name ?? "Model";
|
|
33
|
+
const cloudOs = variant === "cloud-os";
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<DropdownMenu>
|
|
37
|
+
<DropdownMenu.Trigger
|
|
38
|
+
render={
|
|
39
|
+
<button
|
|
40
|
+
type="button"
|
|
41
|
+
disabled={disabled}
|
|
42
|
+
aria-label="Select model"
|
|
43
|
+
className={cn(
|
|
44
|
+
"group inline-flex h-11 min-w-0 items-center gap-1.5 rounded-lg transition-[background-color,color,transform] duration-150 ease-out focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-40 md:h-8",
|
|
45
|
+
cloudOs
|
|
46
|
+
? "max-w-[180px] cursor-pointer px-2 text-[13px] leading-5 tracking-[-0.25px] text-kumo-subtle hover:bg-kumo-tint hover:text-kumo-default focus-visible:bg-kumo-tint focus-visible:text-kumo-default active:scale-[0.97] data-[state=open]:bg-kumo-tint data-[state=open]:text-kumo-default"
|
|
47
|
+
: "max-w-[min(11rem,38vw)] px-2.5 text-xs leading-none text-black/55 hover:bg-black/5 hover:text-black focus-visible:ring-2 focus-visible:ring-black/15",
|
|
48
|
+
)}
|
|
49
|
+
>
|
|
50
|
+
<span className="min-w-0 truncate">{selectedLabel}</span>
|
|
51
|
+
<CaretDown
|
|
52
|
+
size={12}
|
|
53
|
+
weight="bold"
|
|
54
|
+
aria-hidden="true"
|
|
55
|
+
className={cn(
|
|
56
|
+
"shrink-0 transition-transform duration-150 ease-out group-data-[state=open]:rotate-180",
|
|
57
|
+
cloudOs ? "text-kumo-inactive" : "text-black/55",
|
|
58
|
+
)}
|
|
59
|
+
/>
|
|
60
|
+
</button>
|
|
61
|
+
}
|
|
62
|
+
/>
|
|
63
|
+
<DropdownMenu.Content
|
|
64
|
+
align="end"
|
|
65
|
+
side="top"
|
|
66
|
+
sideOffset={8}
|
|
67
|
+
data-slot="model-select-content"
|
|
68
|
+
className="cos-model-select min-w-[12.5rem] rounded-lg border p-2"
|
|
69
|
+
>
|
|
70
|
+
{models.map((model) => (
|
|
71
|
+
<DropdownMenu.Item
|
|
72
|
+
key={model.id}
|
|
73
|
+
disabled={model.disabled}
|
|
74
|
+
onClick={model.disabled ? undefined : () => onModelChange(model.id)}
|
|
75
|
+
data-slot="model-select-item"
|
|
76
|
+
className="cos-model-select__item gap-2 rounded px-2 py-2.5 text-[14px] leading-[1.4] font-normal transition-colors"
|
|
77
|
+
>
|
|
78
|
+
<span
|
|
79
|
+
className="cos-model-select__name min-w-0 flex-1 truncate"
|
|
80
|
+
data-muted={selectedModel === model.id || model.disabled ? "" : undefined}
|
|
81
|
+
>
|
|
82
|
+
{model.name}
|
|
83
|
+
</span>
|
|
84
|
+
{model.disabledLabel ? (
|
|
85
|
+
<span
|
|
86
|
+
className="cos-model-select__plan min-w-0 flex-1 truncate text-right"
|
|
87
|
+
data-disabled={model.disabled ? "" : undefined}
|
|
88
|
+
>
|
|
89
|
+
{model.disabledLabel}
|
|
90
|
+
</span>
|
|
91
|
+
) : selectedModel === model.id ? (
|
|
92
|
+
<img
|
|
93
|
+
src={selectedIcon}
|
|
94
|
+
alt=""
|
|
95
|
+
aria-hidden="true"
|
|
96
|
+
data-slot="model-selected-icon"
|
|
97
|
+
className="size-4 shrink-0"
|
|
98
|
+
/>
|
|
99
|
+
) : null}
|
|
100
|
+
</DropdownMenu.Item>
|
|
101
|
+
))}
|
|
102
|
+
</DropdownMenu.Content>
|
|
103
|
+
</DropdownMenu>
|
|
104
|
+
);
|
|
105
|
+
}
|
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,8 +57,10 @@ 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;
|
|
63
|
+
onOpen?: () => void;
|
|
37
64
|
onRemove?: () => void;
|
|
38
65
|
onRetry?: () => void;
|
|
39
66
|
}
|
|
@@ -59,10 +86,12 @@ function ImageFileView({
|
|
|
59
86
|
src,
|
|
60
87
|
label,
|
|
61
88
|
placement,
|
|
89
|
+
frameClassName,
|
|
62
90
|
}: {
|
|
63
91
|
src: string;
|
|
64
92
|
label: string;
|
|
65
93
|
placement: Exclude<FileViewPlacement, "composer">;
|
|
94
|
+
frameClassName?: string;
|
|
66
95
|
}) {
|
|
67
96
|
const imageRef = useRef<HTMLImageElement>(null);
|
|
68
97
|
const [loadedSrc, setLoadedSrc] = useState<string>();
|
|
@@ -78,9 +107,11 @@ function ImageFileView({
|
|
|
78
107
|
}, [src]);
|
|
79
108
|
|
|
80
109
|
const assistant = placement === "assistant-message";
|
|
81
|
-
const previewClassName =
|
|
82
|
-
?
|
|
83
|
-
:
|
|
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";
|
|
84
115
|
|
|
85
116
|
return (
|
|
86
117
|
<span
|
|
@@ -93,9 +124,11 @@ function ImageFileView({
|
|
|
93
124
|
src={src}
|
|
94
125
|
alt={label}
|
|
95
126
|
className={`${
|
|
96
|
-
|
|
97
|
-
? "block
|
|
98
|
-
:
|
|
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"
|
|
99
132
|
} ${loaded && !error ? "" : "invisible"}`}
|
|
100
133
|
decoding="async"
|
|
101
134
|
loading="lazy"
|
|
@@ -115,7 +148,7 @@ function ImageFileView({
|
|
|
115
148
|
className="size-6 animate-pulse motion-reduce:animate-none"
|
|
116
149
|
aria-hidden="true"
|
|
117
150
|
/>
|
|
118
|
-
<span className="sr-only">Loading
|
|
151
|
+
<span className="sr-only">Loading preview...</span>
|
|
119
152
|
</span>
|
|
120
153
|
)}
|
|
121
154
|
{error && (
|
|
@@ -123,7 +156,7 @@ function ImageFileView({
|
|
|
123
156
|
className="absolute inset-0 flex min-h-24 min-w-36 items-center justify-center bg-kumo-elevated text-kumo-danger"
|
|
124
157
|
data-image-state="error"
|
|
125
158
|
role="img"
|
|
126
|
-
aria-label=
|
|
159
|
+
aria-label="Unable to load preview"
|
|
127
160
|
>
|
|
128
161
|
<FileImageIcon className="size-6" aria-hidden="true" />
|
|
129
162
|
</span>
|
|
@@ -132,14 +165,114 @@ function ImageFileView({
|
|
|
132
165
|
);
|
|
133
166
|
}
|
|
134
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
|
+
|
|
135
266
|
/**
|
|
136
267
|
* Cloud OS 的默认文件视图。宿主可通过 `renderFile` 在同一 seam 替换它。
|
|
137
268
|
*/
|
|
138
269
|
export function FileView({
|
|
139
270
|
file,
|
|
140
271
|
placement,
|
|
272
|
+
variant = "compact",
|
|
141
273
|
status = "ready",
|
|
142
274
|
progress = 0,
|
|
275
|
+
onOpen,
|
|
143
276
|
onRemove,
|
|
144
277
|
onRetry,
|
|
145
278
|
}: FileViewProps) {
|
|
@@ -165,7 +298,15 @@ export function FileView({
|
|
|
165
298
|
data-state={status === "ready" ? "done" : status}
|
|
166
299
|
>
|
|
167
300
|
<span className="flex size-8 shrink-0 items-center justify-center rounded-[10px] bg-kumo-control text-kumo-inactive themed-thumbnail-shadow">
|
|
168
|
-
|
|
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
|
+
)}
|
|
169
310
|
</span>
|
|
170
311
|
<span className="flex min-w-0 flex-col">
|
|
171
312
|
<span className="max-w-36 truncate text-xs font-medium text-kumo-default">
|
|
@@ -230,10 +371,31 @@ export function FileView({
|
|
|
230
371
|
);
|
|
231
372
|
}
|
|
232
373
|
|
|
233
|
-
if (
|
|
374
|
+
if (variant === "product") {
|
|
234
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 = (
|
|
235
387
|
<ImageFileView src={file.url!} label={label} placement={placement} />
|
|
236
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;
|
|
237
399
|
}
|
|
238
400
|
|
|
239
401
|
return (
|
|
@@ -243,7 +405,28 @@ export function FileView({
|
|
|
243
405
|
data-placement={placement}
|
|
244
406
|
>
|
|
245
407
|
<FileIcon size={13} className="text-kumo-inactive" />
|
|
246
|
-
{label}
|
|
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
|
+
)}
|
|
247
430
|
</span>
|
|
248
431
|
);
|
|
249
432
|
}
|