@springbrand/message-panel 0.1.3-alpha.8 → 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 +38 -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,
|
|
@@ -26,6 +23,7 @@ import {
|
|
|
26
23
|
useRef,
|
|
27
24
|
useState,
|
|
28
25
|
type DragEvent as ReactDragEvent,
|
|
26
|
+
type Ref,
|
|
29
27
|
type ReactNode,
|
|
30
28
|
} from "react";
|
|
31
29
|
import { DropdownMenu } from "../primitives/dropdown-menu";
|
|
@@ -36,6 +34,10 @@ import {
|
|
|
36
34
|
renderFileView,
|
|
37
35
|
type FileRenderer,
|
|
38
36
|
} from "../file-view";
|
|
37
|
+
import {
|
|
38
|
+
CloudOsModelSelect,
|
|
39
|
+
type CloudOsModelOption,
|
|
40
|
+
} from "./cloud-os-model-select";
|
|
39
41
|
|
|
40
42
|
const isComposingKeyEvent = (event: {
|
|
41
43
|
isComposing?: boolean;
|
|
@@ -55,11 +57,6 @@ const isComposingKeyEvent = (event: {
|
|
|
55
57
|
* assistant-ui ComposerTriggerPopover。
|
|
56
58
|
*/
|
|
57
59
|
|
|
58
|
-
export interface CloudOsModelOption {
|
|
59
|
-
id: string;
|
|
60
|
-
name: string;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
60
|
export interface CloudOsComposerCommand {
|
|
64
61
|
prefix?: "/" | "$";
|
|
65
62
|
name: string;
|
|
@@ -67,7 +64,7 @@ export interface CloudOsComposerCommand {
|
|
|
67
64
|
}
|
|
68
65
|
|
|
69
66
|
export interface CloudOsCapabilityView {
|
|
70
|
-
kind: "skill" | "plan";
|
|
67
|
+
kind: "skill" | "tool" | "plan";
|
|
71
68
|
name: string;
|
|
72
69
|
label: string;
|
|
73
70
|
}
|
|
@@ -108,7 +105,7 @@ export interface CloudOsChatInputProps {
|
|
|
108
105
|
* 传了就以它为准来决定显示发送键还是停止键。
|
|
109
106
|
*/
|
|
110
107
|
turnActive?: boolean;
|
|
111
|
-
/**
|
|
108
|
+
/** 可用的斜杠命令。 */
|
|
112
109
|
commands?: readonly CloudOsComposerCommand[];
|
|
113
110
|
capabilities?: readonly CloudOsCapabilityView[];
|
|
114
111
|
onCommandSelect?: (command: CloudOsComposerCommand) => void;
|
|
@@ -126,6 +123,8 @@ export interface CloudOsChatInputProps {
|
|
|
126
123
|
minRows?: number;
|
|
127
124
|
maxRows?: number;
|
|
128
125
|
autoFocus?: boolean;
|
|
126
|
+
/** Gives the host direct access to the chat textarea. */
|
|
127
|
+
textareaRef?: Ref<HTMLTextAreaElement>;
|
|
129
128
|
/** 输入框下方一行(原文件放 token / cost)。 */
|
|
130
129
|
footnote?: ReactNode;
|
|
131
130
|
}
|
|
@@ -185,6 +184,7 @@ function CloudOsChatInputContent({
|
|
|
185
184
|
minRows = 1,
|
|
186
185
|
maxRows = 6,
|
|
187
186
|
autoFocus = false,
|
|
187
|
+
textareaRef: forwardedTextareaRef,
|
|
188
188
|
footnote,
|
|
189
189
|
runtime,
|
|
190
190
|
}: CloudOsChatInputProps & { runtime: AssistantRuntime }) {
|
|
@@ -214,11 +214,6 @@ function CloudOsChatInputContent({
|
|
|
214
214
|
(value.trim().length > 0 || hasReadyAttachment) &&
|
|
215
215
|
!hasUnreadyAttachment;
|
|
216
216
|
|
|
217
|
-
const selectedModelLabel =
|
|
218
|
-
selectedModel === null
|
|
219
|
-
? "No agent"
|
|
220
|
-
: (models.find((model) => model.id === selectedModel)?.name ?? selectedModel);
|
|
221
|
-
|
|
222
217
|
useEffect(() => {
|
|
223
218
|
const element = textareaRef.current;
|
|
224
219
|
if (element) autoResizeTextarea(element, minRows, maxRows);
|
|
@@ -325,7 +320,7 @@ function CloudOsChatInputContent({
|
|
|
325
320
|
|
|
326
321
|
return (
|
|
327
322
|
// isolation: isolate 把输入框内部的 z-index 关起来,免得盖到 portal 出去的模型下拉。
|
|
328
|
-
<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)]">
|
|
329
324
|
<ComposerTriggerPopover
|
|
330
325
|
char="/"
|
|
331
326
|
{...slash}
|
|
@@ -411,6 +406,11 @@ function CloudOsChatInputContent({
|
|
|
411
406
|
<textarea
|
|
412
407
|
ref={(element) => {
|
|
413
408
|
textareaRef.current = element;
|
|
409
|
+
if (typeof forwardedTextareaRef === "function") {
|
|
410
|
+
forwardedTextareaRef(element);
|
|
411
|
+
} else if (forwardedTextareaRef) {
|
|
412
|
+
forwardedTextareaRef.current = element;
|
|
413
|
+
}
|
|
414
414
|
if (element) autoResizeTextarea(element, minRows, maxRows);
|
|
415
415
|
}}
|
|
416
416
|
value={value}
|
|
@@ -499,7 +499,7 @@ function CloudOsChatInputContent({
|
|
|
499
499
|
render={
|
|
500
500
|
<button
|
|
501
501
|
type="button"
|
|
502
|
-
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"
|
|
503
503
|
aria-label="Open chat options"
|
|
504
504
|
>
|
|
505
505
|
<Plus size={18} />
|
|
@@ -510,7 +510,7 @@ function CloudOsChatInputContent({
|
|
|
510
510
|
{onToggleThinkingTraces && (
|
|
511
511
|
<DropdownMenu.Item
|
|
512
512
|
onClick={onToggleThinkingTraces}
|
|
513
|
-
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"
|
|
514
514
|
>
|
|
515
515
|
<span className="mr-2 inline-flex h-4 w-4 items-center justify-center text-kumo-inactive">
|
|
516
516
|
<Brain size={14} />
|
|
@@ -520,29 +520,10 @@ function CloudOsChatInputContent({
|
|
|
520
520
|
</span>
|
|
521
521
|
</DropdownMenu.Item>
|
|
522
522
|
)}
|
|
523
|
-
{commands.length > 0 && (
|
|
524
|
-
<>
|
|
525
|
-
{commands.map((command) => (
|
|
526
|
-
<DropdownMenu.Item
|
|
527
|
-
key={`${command.prefix ?? "/"}${command.name}`}
|
|
528
|
-
onClick={() => selectCommand(command, false)}
|
|
529
|
-
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"
|
|
530
|
-
>
|
|
531
|
-
<span className="mr-2 inline-flex h-4 w-4 items-center justify-center text-kumo-inactive">
|
|
532
|
-
<Terminal size={14} />
|
|
533
|
-
</span>
|
|
534
|
-
<span className="min-w-0 flex-1 truncate">
|
|
535
|
-
{commandLabel(command)}
|
|
536
|
-
</span>
|
|
537
|
-
</DropdownMenu.Item>
|
|
538
|
-
))}
|
|
539
|
-
<div className="my-1 border-t border-kumo-line/70" />
|
|
540
|
-
</>
|
|
541
|
-
)}
|
|
542
523
|
{onFilesSelected && (
|
|
543
524
|
<DropdownMenu.Item
|
|
544
525
|
onClick={() => attachmentInputRef.current?.click()}
|
|
545
|
-
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"
|
|
546
527
|
>
|
|
547
528
|
<span className="mr-2 inline-flex h-4 w-4 items-center justify-center text-kumo-inactive">
|
|
548
529
|
<FileIcon size={14} />
|
|
@@ -556,7 +537,7 @@ function CloudOsChatInputContent({
|
|
|
556
537
|
<button
|
|
557
538
|
type="button"
|
|
558
539
|
onClick={onAttachResource}
|
|
559
|
-
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"
|
|
560
541
|
>
|
|
561
542
|
<Plug size={15} className="flex-shrink-0" />
|
|
562
543
|
<span className="cos-attach-label leading-none">
|
|
@@ -569,52 +550,19 @@ function CloudOsChatInputContent({
|
|
|
569
550
|
|
|
570
551
|
<div className="ml-auto flex min-w-0 flex-shrink items-center gap-1.5">
|
|
571
552
|
{onModelChange && (
|
|
572
|
-
<
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
aria-label="Select model"
|
|
579
|
-
>
|
|
580
|
-
<span className="min-w-0 truncate">{selectedModelLabel}</span>
|
|
581
|
-
<CaretDown
|
|
582
|
-
size={12}
|
|
583
|
-
weight="bold"
|
|
584
|
-
className="flex-shrink-0 text-kumo-inactive transition-transform duration-150 ease-out group-data-[state=open]:rotate-180"
|
|
585
|
-
/>
|
|
586
|
-
</button>
|
|
587
|
-
}
|
|
588
|
-
/>
|
|
589
|
-
<DropdownMenu.Content
|
|
590
|
-
align="end"
|
|
591
|
-
className="themed-floating-shadow-lg !min-w-[190px] rounded-2xl border border-kumo-line/70 bg-kumo-base p-1"
|
|
592
|
-
>
|
|
593
|
-
{models.map((model) => (
|
|
594
|
-
<DropdownMenu.Item
|
|
595
|
-
key={model.id}
|
|
596
|
-
onClick={() => onModelChange(model.id)}
|
|
597
|
-
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"
|
|
598
|
-
>
|
|
599
|
-
<span className="min-w-0 flex-1 truncate">{model.name}</span>
|
|
600
|
-
{selectedModel === model.id && (
|
|
601
|
-
<Check
|
|
602
|
-
size={12}
|
|
603
|
-
weight="bold"
|
|
604
|
-
className="ml-3 flex-shrink-0 text-kumo-inactive"
|
|
605
|
-
/>
|
|
606
|
-
)}
|
|
607
|
-
</DropdownMenu.Item>
|
|
608
|
-
))}
|
|
609
|
-
</DropdownMenu.Content>
|
|
610
|
-
</DropdownMenu>
|
|
553
|
+
<CloudOsModelSelect
|
|
554
|
+
models={models}
|
|
555
|
+
selectedModel={selectedModel}
|
|
556
|
+
onModelChange={onModelChange}
|
|
557
|
+
variant="cloud-os"
|
|
558
|
+
/>
|
|
611
559
|
)}
|
|
612
560
|
{!showStopButton && submitControl}
|
|
613
561
|
{showStopButton ? (
|
|
614
562
|
<WorkshopIconButton
|
|
615
563
|
onClick={onStop}
|
|
616
564
|
tone="primary"
|
|
617
|
-
className="!h-
|
|
565
|
+
className="!h-11 !w-11 md:!h-8 md:!w-8"
|
|
618
566
|
aria-label="Stop agent"
|
|
619
567
|
>
|
|
620
568
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
|
|
@@ -622,25 +570,27 @@ function CloudOsChatInputContent({
|
|
|
622
570
|
</svg>
|
|
623
571
|
</WorkshopIconButton>
|
|
624
572
|
) : (
|
|
625
|
-
<
|
|
573
|
+
<button
|
|
574
|
+
type="button"
|
|
626
575
|
onClick={onSubmit}
|
|
627
576
|
disabled={!canSend}
|
|
628
|
-
|
|
629
|
-
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"
|
|
630
578
|
aria-label="Send message"
|
|
631
579
|
>
|
|
632
580
|
<svg
|
|
633
|
-
|
|
634
|
-
height="16"
|
|
581
|
+
className="size-4"
|
|
635
582
|
viewBox="0 0 24 24"
|
|
636
583
|
fill="none"
|
|
637
584
|
stroke="currentColor"
|
|
638
|
-
strokeWidth="
|
|
585
|
+
strokeWidth="1.8"
|
|
586
|
+
strokeLinecap="round"
|
|
587
|
+
strokeLinejoin="round"
|
|
588
|
+
aria-hidden="true"
|
|
639
589
|
>
|
|
640
|
-
<
|
|
641
|
-
<
|
|
590
|
+
<path d="m5 12 7-7 7 7" />
|
|
591
|
+
<path d="M12 19V5" />
|
|
642
592
|
</svg>
|
|
643
|
-
</
|
|
593
|
+
</button>
|
|
644
594
|
)}
|
|
645
595
|
</div>
|
|
646
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
|
}
|