@springbrand/message-panel 0.1.3-alpha.28 → 0.1.3-alpha.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -20,6 +20,7 @@ import { Tooltip } from "../primitives/tooltip";
|
|
|
20
20
|
import { WorkshopIconButton } from "../primitives/workshop-controls";
|
|
21
21
|
import {
|
|
22
22
|
MarkdownMessage,
|
|
23
|
+
type CloudOsArtifactResolver,
|
|
23
24
|
type CloudOsUrlResolver,
|
|
24
25
|
} from "./markdown-message";
|
|
25
26
|
import {
|
|
@@ -92,6 +93,10 @@ export interface CloudOsChatMessagesProps {
|
|
|
92
93
|
renderFile?: FileRenderer;
|
|
93
94
|
/** 把消息里的 `sandbox:/workspace/...` 之类地址翻译成可访问 URL。 */
|
|
94
95
|
resolveUrl?: CloudOsUrlResolver;
|
|
96
|
+
/** 把图片 URL 关联到宿主的持久产物。 */
|
|
97
|
+
resolveArtifactId?: CloudOsArtifactResolver;
|
|
98
|
+
/** 打开宿主拥有的产物视图。 */
|
|
99
|
+
onOpenArtifact?: (artifactId: string) => void;
|
|
95
100
|
/** 消息区宽度是否收敛到 920px 居中(GadgetEditor 里叫 constrainChatWidth)。 */
|
|
96
101
|
constrainWidth?: boolean;
|
|
97
102
|
emptyTitle?: string;
|
|
@@ -148,12 +153,16 @@ function UserBubble({
|
|
|
148
153
|
copied,
|
|
149
154
|
renderFile,
|
|
150
155
|
resolveUrl,
|
|
156
|
+
resolveArtifactId,
|
|
157
|
+
onOpenArtifact,
|
|
151
158
|
}: {
|
|
152
159
|
entry: Extract<CloudOsEntry, { type: "user" }>;
|
|
153
160
|
onCopy: (text: string) => void;
|
|
154
161
|
copied: boolean;
|
|
155
162
|
renderFile?: FileRenderer;
|
|
156
163
|
resolveUrl?: CloudOsUrlResolver;
|
|
164
|
+
resolveArtifactId?: CloudOsArtifactResolver;
|
|
165
|
+
onOpenArtifact?: (artifactId: string) => void;
|
|
157
166
|
}) {
|
|
158
167
|
return (
|
|
159
168
|
<div className="group/message relative flex flex-col items-end">
|
|
@@ -193,7 +202,12 @@ function UserBubble({
|
|
|
193
202
|
)}
|
|
194
203
|
{entry.text && (
|
|
195
204
|
<div className="themed-user-bubble-shadow cos-markdown cos-user-markdown w-fit max-w-[min(680px,78%)] rounded-[12px] border border-transparent bg-kumo-bubble-user px-4 py-2.5 text-[14px] leading-[22px] tracking-[-0.25px] text-kumo-default">
|
|
196
|
-
<MarkdownMessage
|
|
205
|
+
<MarkdownMessage
|
|
206
|
+
message={entry.text}
|
|
207
|
+
resolveUrl={resolveUrl}
|
|
208
|
+
resolveArtifactId={resolveArtifactId}
|
|
209
|
+
onOpenArtifact={onOpenArtifact}
|
|
210
|
+
/>
|
|
197
211
|
</div>
|
|
198
212
|
)}
|
|
199
213
|
<div className="mt-0.5 flex items-center justify-end gap-2 pr-1 text-[11px] leading-4 text-kumo-inactive opacity-0 transition-opacity duration-150 ease-out group-hover/message:opacity-100 group-focus-within/message:opacity-100">
|
|
@@ -222,12 +236,20 @@ function UserBubble({
|
|
|
222
236
|
|
|
223
237
|
function ActionPresentationBlock({
|
|
224
238
|
presentation,
|
|
239
|
+
failureCount = 1,
|
|
240
|
+
partial = false,
|
|
225
241
|
renderFile,
|
|
226
242
|
resolveUrl,
|
|
243
|
+
resolveArtifactId,
|
|
244
|
+
onOpenArtifact,
|
|
227
245
|
}: {
|
|
228
246
|
presentation: CloudOsActionPresentation;
|
|
247
|
+
failureCount?: number;
|
|
248
|
+
partial?: boolean;
|
|
229
249
|
renderFile?: FileRenderer;
|
|
230
250
|
resolveUrl?: CloudOsUrlResolver;
|
|
251
|
+
resolveArtifactId?: CloudOsArtifactResolver;
|
|
252
|
+
onOpenArtifact?: (artifactId: string) => void;
|
|
231
253
|
}) {
|
|
232
254
|
const { state } = presentation;
|
|
233
255
|
if (state === "ready") {
|
|
@@ -240,19 +262,26 @@ function ActionPresentationBlock({
|
|
|
240
262
|
>
|
|
241
263
|
{presentation.content.length === 0
|
|
242
264
|
? <p className="m-0 text-[14px] leading-5 text-kumo-subtle">Completed</p>
|
|
243
|
-
: presentation.content.map((file, index) =>
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
265
|
+
: presentation.content.map((file, index) => {
|
|
266
|
+
const url = resolveUrl?.(file.url) ?? file.url;
|
|
267
|
+
const artifactId = resolveArtifactId?.(url);
|
|
268
|
+
return (
|
|
269
|
+
<Fragment key={`${file.url}:${index}`}>
|
|
270
|
+
{renderFileView(renderFile, {
|
|
271
|
+
file: {
|
|
272
|
+
...file,
|
|
273
|
+
filename: "Generated file",
|
|
274
|
+
url,
|
|
275
|
+
},
|
|
276
|
+
placement: "assistant-message",
|
|
277
|
+
variant: "product",
|
|
278
|
+
onOpen: file.mediaType?.startsWith("image/") && artifactId && onOpenArtifact
|
|
279
|
+
? () => onOpenArtifact(artifactId)
|
|
280
|
+
: undefined,
|
|
281
|
+
})}
|
|
282
|
+
</Fragment>
|
|
283
|
+
);
|
|
284
|
+
})}
|
|
256
285
|
</div>
|
|
257
286
|
);
|
|
258
287
|
}
|
|
@@ -260,13 +289,17 @@ function ActionPresentationBlock({
|
|
|
260
289
|
const product = presentation.produces;
|
|
261
290
|
const productName = product?.type ?? "other";
|
|
262
291
|
const isFileProduct = productName !== "data" && productName !== "other";
|
|
292
|
+
const insufficientCredits = state === "failed" &&
|
|
293
|
+
presentation.failureCode === "insufficient_credits";
|
|
263
294
|
const body = state === "running"
|
|
264
295
|
? isFileProduct
|
|
265
296
|
? `Generating ${productName}${product?.mediaType ? ` (${product.mediaType})` : ""}`
|
|
266
297
|
: "Generating"
|
|
267
298
|
: state === "outcome_unknown"
|
|
268
299
|
? "The provider may have completed this action. Check the status before trying again."
|
|
269
|
-
:
|
|
300
|
+
: insufficientCredits
|
|
301
|
+
? `${failureCount} API call${failureCount === 1 ? " was" : "s were"} not started because there weren't enough Credits.`
|
|
302
|
+
: undefined;
|
|
270
303
|
if (state === "running") {
|
|
271
304
|
return (
|
|
272
305
|
<div
|
|
@@ -289,7 +322,9 @@ function ActionPresentationBlock({
|
|
|
289
322
|
>
|
|
290
323
|
<span>
|
|
291
324
|
<strong className="block font-medium text-kumo-default">
|
|
292
|
-
{
|
|
325
|
+
{insufficientCredits
|
|
326
|
+
? partial ? "Partially completed" : "Not completed"
|
|
327
|
+
: state === "failed" ? "Action failed" : "Status unavailable"}
|
|
293
328
|
</strong>
|
|
294
329
|
{body && <span className="block text-kumo-subtle">{body}</span>}
|
|
295
330
|
</span>
|
|
@@ -313,6 +348,7 @@ export function CloudOsChatMessages({
|
|
|
313
348
|
renderApproval,
|
|
314
349
|
renderFile,
|
|
315
350
|
resolveUrl,
|
|
351
|
+
resolveArtifactId,
|
|
316
352
|
constrainWidth = true,
|
|
317
353
|
emptyTitle = "What are we working on?",
|
|
318
354
|
onRetry,
|
|
@@ -320,6 +356,7 @@ export function CloudOsChatMessages({
|
|
|
320
356
|
onSuggestion,
|
|
321
357
|
onRespond,
|
|
322
358
|
onOpenSchedule,
|
|
359
|
+
onOpenArtifact,
|
|
323
360
|
onCopy,
|
|
324
361
|
}: CloudOsChatMessagesProps) {
|
|
325
362
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
@@ -473,6 +510,8 @@ export function CloudOsChatMessages({
|
|
|
473
510
|
copied={copied === entry.text}
|
|
474
511
|
renderFile={renderFile}
|
|
475
512
|
resolveUrl={resolveUrl}
|
|
513
|
+
resolveArtifactId={resolveArtifactId}
|
|
514
|
+
onOpenArtifact={onOpenArtifact}
|
|
476
515
|
/>
|
|
477
516
|
</div>
|
|
478
517
|
);
|
|
@@ -551,6 +590,8 @@ export function CloudOsChatMessages({
|
|
|
551
590
|
<MarkdownMessage
|
|
552
591
|
message={entry.summary}
|
|
553
592
|
resolveUrl={resolveUrl}
|
|
593
|
+
resolveArtifactId={resolveArtifactId}
|
|
594
|
+
onOpenArtifact={onOpenArtifact}
|
|
554
595
|
/>
|
|
555
596
|
</div>
|
|
556
597
|
)}
|
|
@@ -566,6 +607,16 @@ export function CloudOsChatMessages({
|
|
|
566
607
|
const hasNarrative = contentBlocks.some(
|
|
567
608
|
(block) => block.kind === "text",
|
|
568
609
|
);
|
|
610
|
+
const insufficientFailures = contentBlocks.filter(
|
|
611
|
+
(block) => block.kind === "actionPresentation" &&
|
|
612
|
+
block.presentation.state === "failed" &&
|
|
613
|
+
block.presentation.failureCode === "insufficient_credits",
|
|
614
|
+
);
|
|
615
|
+
const firstInsufficientFailure = insufficientFailures[0];
|
|
616
|
+
const hasCompletedAction = contentBlocks.some(
|
|
617
|
+
(block) => block.kind === "actionPresentation" &&
|
|
618
|
+
block.presentation.state === "ready",
|
|
619
|
+
);
|
|
569
620
|
|
|
570
621
|
return (
|
|
571
622
|
<div
|
|
@@ -657,6 +708,8 @@ export function CloudOsChatMessages({
|
|
|
657
708
|
<MarkdownMessage
|
|
658
709
|
message={block.text}
|
|
659
710
|
resolveUrl={resolveUrl}
|
|
711
|
+
resolveArtifactId={resolveArtifactId}
|
|
712
|
+
onOpenArtifact={onOpenArtifact}
|
|
660
713
|
streaming={streaming}
|
|
661
714
|
/>
|
|
662
715
|
</div>
|
|
@@ -739,18 +792,25 @@ export function CloudOsChatMessages({
|
|
|
739
792
|
<SubAgentsBlock key={block.key} agents={block.agents} />
|
|
740
793
|
);
|
|
741
794
|
case "image":
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
795
|
+
{
|
|
796
|
+
const url = resolveUrl?.(block.src) ?? block.src;
|
|
797
|
+
const artifactId = resolveArtifactId?.(url);
|
|
798
|
+
return (
|
|
799
|
+
<Fragment key={block.key}>
|
|
800
|
+
{renderFileView(renderFile, {
|
|
801
|
+
file: {
|
|
802
|
+
url,
|
|
803
|
+
filename: block.alt,
|
|
804
|
+
mediaType: block.mediaType ?? "image/*",
|
|
805
|
+
},
|
|
806
|
+
placement: "assistant-message",
|
|
807
|
+
onOpen: artifactId && onOpenArtifact
|
|
808
|
+
? () => onOpenArtifact(artifactId)
|
|
809
|
+
: undefined,
|
|
810
|
+
})}
|
|
811
|
+
</Fragment>
|
|
812
|
+
);
|
|
813
|
+
}
|
|
754
814
|
case "file":
|
|
755
815
|
return (
|
|
756
816
|
<Fragment key={block.key}>
|
|
@@ -765,12 +825,20 @@ export function CloudOsChatMessages({
|
|
|
765
825
|
</Fragment>
|
|
766
826
|
);
|
|
767
827
|
case "actionPresentation":
|
|
828
|
+
if (
|
|
829
|
+
block.presentation.failureCode === "insufficient_credits" &&
|
|
830
|
+
block !== firstInsufficientFailure
|
|
831
|
+
) return null;
|
|
768
832
|
return (
|
|
769
833
|
<ActionPresentationBlock
|
|
770
834
|
key={block.key}
|
|
771
835
|
presentation={block.presentation}
|
|
836
|
+
failureCount={insufficientFailures.length}
|
|
837
|
+
partial={hasCompletedAction}
|
|
772
838
|
renderFile={renderFile}
|
|
773
839
|
resolveUrl={resolveUrl}
|
|
840
|
+
resolveArtifactId={resolveArtifactId}
|
|
841
|
+
onOpenArtifact={onOpenArtifact}
|
|
774
842
|
/>
|
|
775
843
|
);
|
|
776
844
|
case "error":
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
export type CloudOsUrlResolver = (url: string) => string | null | undefined;
|
|
21
|
+
export type CloudOsArtifactResolver = (url: string) => string | undefined;
|
|
21
22
|
|
|
22
23
|
const ALLOWED_PROTOCOLS = new Set(["http:", "https:", "mailto:"]);
|
|
23
24
|
const SMOOTH_STREAMING = {
|
|
@@ -104,23 +105,35 @@ const DEFAULT_COMPONENTS: Components = {
|
|
|
104
105
|
export const MarkdownMessage = memo(function MarkdownMessage({
|
|
105
106
|
message,
|
|
106
107
|
resolveUrl,
|
|
108
|
+
resolveArtifactId,
|
|
109
|
+
onOpenArtifact,
|
|
107
110
|
streaming = false,
|
|
108
111
|
}: {
|
|
109
112
|
message: string;
|
|
110
113
|
resolveUrl?: CloudOsUrlResolver;
|
|
114
|
+
resolveArtifactId?: CloudOsArtifactResolver;
|
|
115
|
+
onOpenArtifact?: (artifactId: string) => void;
|
|
111
116
|
streaming?: boolean;
|
|
112
117
|
}): ReactNode {
|
|
113
118
|
return (
|
|
114
119
|
<TextMessagePartProvider text={message} isRunning={streaming}>
|
|
115
|
-
<MarkdownMessageContent
|
|
120
|
+
<MarkdownMessageContent
|
|
121
|
+
resolveUrl={resolveUrl}
|
|
122
|
+
resolveArtifactId={resolveArtifactId}
|
|
123
|
+
onOpenArtifact={onOpenArtifact}
|
|
124
|
+
/>
|
|
116
125
|
</TextMessagePartProvider>
|
|
117
126
|
);
|
|
118
127
|
});
|
|
119
128
|
|
|
120
129
|
function MarkdownMessageContent({
|
|
121
130
|
resolveUrl,
|
|
131
|
+
resolveArtifactId,
|
|
132
|
+
onOpenArtifact,
|
|
122
133
|
}: {
|
|
123
134
|
resolveUrl?: CloudOsUrlResolver;
|
|
135
|
+
resolveArtifactId?: CloudOsArtifactResolver;
|
|
136
|
+
onOpenArtifact?: (artifactId: string) => void;
|
|
124
137
|
}) {
|
|
125
138
|
const part = useSmooth(useMessagePartText(), SMOOTH_STREAMING);
|
|
126
139
|
const streaming = part.status.type === "running";
|
|
@@ -131,6 +144,26 @@ function MarkdownMessageContent({
|
|
|
131
144
|
: STREAMDOWN_REHYPE_PLUGINS,
|
|
132
145
|
[resolveUrl],
|
|
133
146
|
);
|
|
147
|
+
const components = useMemo<Components>(() => ({
|
|
148
|
+
...DEFAULT_COMPONENTS,
|
|
149
|
+
img: ({ node: _node, src, alt, ...props }) => {
|
|
150
|
+
if (!src) return null;
|
|
151
|
+
const image = <img {...props} src={src} alt={alt} />;
|
|
152
|
+
const artifactId = resolveArtifactId?.(src);
|
|
153
|
+
return artifactId && onOpenArtifact
|
|
154
|
+
? (
|
|
155
|
+
<button
|
|
156
|
+
type="button"
|
|
157
|
+
onClick={() => onOpenArtifact(artifactId)}
|
|
158
|
+
aria-label={`Open ${alt || "image"} in Creation View`}
|
|
159
|
+
className="inline-block max-w-full cursor-pointer rounded-xl align-bottom focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-kumo-brand/40"
|
|
160
|
+
>
|
|
161
|
+
{image}
|
|
162
|
+
</button>
|
|
163
|
+
)
|
|
164
|
+
: image;
|
|
165
|
+
},
|
|
166
|
+
}), [onOpenArtifact, resolveArtifactId]);
|
|
134
167
|
return (
|
|
135
168
|
<Streamdown
|
|
136
169
|
animated={false}
|
|
@@ -140,7 +173,7 @@ function MarkdownMessageContent({
|
|
|
140
173
|
isAnimating={streaming}
|
|
141
174
|
rehypePlugins={rehypePlugins}
|
|
142
175
|
skipHtml
|
|
143
|
-
components={
|
|
176
|
+
components={components}
|
|
144
177
|
>
|
|
145
178
|
{part.text}
|
|
146
179
|
</Streamdown>
|
|
@@ -61,6 +61,7 @@ export interface CloudOsActionPresentation {
|
|
|
61
61
|
type: "action-execution";
|
|
62
62
|
executionId: string;
|
|
63
63
|
state: "running" | "ready" | "failed" | "outcome_unknown";
|
|
64
|
+
failureCode?: "insufficient_credits";
|
|
64
65
|
produces?: CloudOsActionProduces;
|
|
65
66
|
content: CloudOsActionPresentationFile[];
|
|
66
67
|
}
|
|
@@ -383,6 +384,9 @@ function actionPresentationOf(value: unknown): CloudOsActionPresentation | null
|
|
|
383
384
|
type: "action-execution",
|
|
384
385
|
executionId: presentation.executionId,
|
|
385
386
|
state,
|
|
387
|
+
...(state === "failed" && presentation.failureCode === "insufficient_credits"
|
|
388
|
+
? { failureCode: presentation.failureCode }
|
|
389
|
+
: {}),
|
|
386
390
|
...(produces ? { produces } : {}),
|
|
387
391
|
content,
|
|
388
392
|
};
|
package/cloud-os/file-view.tsx
CHANGED
|
@@ -60,6 +60,7 @@ export interface FileViewProps {
|
|
|
60
60
|
variant?: FileViewVariant;
|
|
61
61
|
status?: FileViewStatus;
|
|
62
62
|
progress?: number;
|
|
63
|
+
onOpen?: () => void;
|
|
63
64
|
onRemove?: () => void;
|
|
64
65
|
onRetry?: () => void;
|
|
65
66
|
}
|
|
@@ -168,10 +169,12 @@ function ProductFileView({
|
|
|
168
169
|
file,
|
|
169
170
|
label,
|
|
170
171
|
placement,
|
|
172
|
+
onOpen,
|
|
171
173
|
}: {
|
|
172
174
|
file: FileViewFile;
|
|
173
175
|
label: string;
|
|
174
176
|
placement: Exclude<FileViewPlacement, "composer">;
|
|
177
|
+
onOpen?: () => void;
|
|
175
178
|
}) {
|
|
176
179
|
const type = productTypeForMediaType(file.mediaType);
|
|
177
180
|
const { frame } = PRODUCT_PRESETS[type];
|
|
@@ -226,7 +229,16 @@ function ProductFileView({
|
|
|
226
229
|
</span>
|
|
227
230
|
);
|
|
228
231
|
|
|
229
|
-
return
|
|
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 ? (
|
|
230
242
|
<a
|
|
231
243
|
href={file.url}
|
|
232
244
|
target="_blank"
|
|
@@ -260,6 +272,7 @@ export function FileView({
|
|
|
260
272
|
variant = "compact",
|
|
261
273
|
status = "ready",
|
|
262
274
|
progress = 0,
|
|
275
|
+
onOpen,
|
|
263
276
|
onRemove,
|
|
264
277
|
onRetry,
|
|
265
278
|
}: FileViewProps) {
|
|
@@ -359,13 +372,30 @@ export function FileView({
|
|
|
359
372
|
}
|
|
360
373
|
|
|
361
374
|
if (variant === "product") {
|
|
362
|
-
return
|
|
375
|
+
return (
|
|
376
|
+
<ProductFileView
|
|
377
|
+
file={file}
|
|
378
|
+
label={label}
|
|
379
|
+
placement={placement}
|
|
380
|
+
onOpen={onOpen}
|
|
381
|
+
/>
|
|
382
|
+
);
|
|
363
383
|
}
|
|
364
384
|
|
|
365
385
|
if (isImage) {
|
|
366
|
-
|
|
386
|
+
const image = (
|
|
367
387
|
<ImageFileView src={file.url!} label={label} placement={placement} />
|
|
368
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;
|
|
369
399
|
}
|
|
370
400
|
|
|
371
401
|
return (
|
package/cloud-os/index.ts
CHANGED
|
@@ -61,7 +61,10 @@ export type {
|
|
|
61
61
|
} from "./workspace/cloud-os-workspace-panel";
|
|
62
62
|
|
|
63
63
|
export { MarkdownMessage } from "./chat/markdown-message";
|
|
64
|
-
export type {
|
|
64
|
+
export type {
|
|
65
|
+
CloudOsArtifactResolver,
|
|
66
|
+
CloudOsUrlResolver,
|
|
67
|
+
} from "./chat/markdown-message";
|
|
65
68
|
export {
|
|
66
69
|
ThinkingTraceRow,
|
|
67
70
|
ToolCallDetails,
|