@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
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<svg preserveAspectRatio="none" overflow="visible" width="12.2369" height="12.2369" viewBox="0 0 12.2369 12.2369" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M5.18061 0.652963C5.50276 -0.217655 6.73415 -0.217654 7.05631 0.652964L7.64089 2.23277C8.04603 3.32764 8.90927 4.19089 10.0042 4.59603L11.584 5.18061C12.4546 5.50276 12.4546 6.73415 11.584 7.05631L10.0041 7.64089C8.90927 8.04603 8.04603 8.90927 7.64089 10.0042L7.05631 11.584C6.73415 12.4546 5.50276 12.4546 5.18061 11.584L4.59603 10.0041C4.19089 8.90927 3.32764 8.04603 2.23277 7.64089L0.652963 7.05631C-0.217655 6.73415 -0.217654 5.50276 0.652964 5.18061L2.23277 4.59603C3.32764 4.19089 4.19089 3.32764 4.59603 2.23277L5.18061 0.652963Z" fill="url(#thinking-spark-gradient)"/>
|
|
3
|
+
<defs>
|
|
4
|
+
<radialGradient id="thinking-spark-gradient" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(6.11846 2.11846) rotate(90) scale(12)">
|
|
5
|
+
<stop stop-color="#FF9938"/>
|
|
6
|
+
<stop offset="1" stop-color="#FFE46C"/>
|
|
7
|
+
</radialGradient>
|
|
8
|
+
</defs>
|
|
9
|
+
</svg>
|
|
@@ -33,8 +33,14 @@ import {
|
|
|
33
33
|
SuggestionsBlock,
|
|
34
34
|
type ApprovalDecision,
|
|
35
35
|
} from "./rich-blocks";
|
|
36
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
ToolGroupRow,
|
|
38
|
+
WorkDescriptionRow,
|
|
39
|
+
WorkTraceDisclosure,
|
|
40
|
+
} from "./tool-rows";
|
|
37
41
|
import { CloudOsActivityIndicator } from "./activity-indicator";
|
|
42
|
+
import { ProductGeneration } from "./image-generation";
|
|
43
|
+
import { GenerationLoader } from "./loading-state";
|
|
38
44
|
import { CapabilityChip } from "../capability-chip";
|
|
39
45
|
import {
|
|
40
46
|
renderFileView,
|
|
@@ -45,8 +51,10 @@ import {
|
|
|
45
51
|
deriveTurnActivity,
|
|
46
52
|
formatClockTime,
|
|
47
53
|
formatFullTimestamp,
|
|
54
|
+
partitionAssistantBlocks,
|
|
48
55
|
rhythmTopClass,
|
|
49
56
|
type CloudOsEntry,
|
|
57
|
+
type CloudOsActionPresentation,
|
|
50
58
|
} from "./transcript-model";
|
|
51
59
|
|
|
52
60
|
export type CloudOsChatStatus = "ready" | "submitted" | "streaming" | "error";
|
|
@@ -100,6 +108,18 @@ export interface CloudOsChatMessagesProps {
|
|
|
100
108
|
onCopy?: (text: string) => void;
|
|
101
109
|
}
|
|
102
110
|
|
|
111
|
+
export function collapseEntriesWithNewNarrative(
|
|
112
|
+
expanded: ReadonlySet<string>,
|
|
113
|
+
previousNarrativeKeys: ReadonlySet<string>,
|
|
114
|
+
narrativeKeys: ReadonlySet<string>,
|
|
115
|
+
): ReadonlySet<string> {
|
|
116
|
+
const next = new Set(expanded);
|
|
117
|
+
for (const key of narrativeKeys) {
|
|
118
|
+
if (!previousNarrativeKeys.has(key)) next.delete(key);
|
|
119
|
+
}
|
|
120
|
+
return next.size === expanded.size ? expanded : next;
|
|
121
|
+
}
|
|
122
|
+
|
|
103
123
|
function useCopyAction(): {
|
|
104
124
|
copied: string | null;
|
|
105
125
|
copy: (text: string) => void;
|
|
@@ -200,6 +220,83 @@ function UserBubble({
|
|
|
200
220
|
);
|
|
201
221
|
}
|
|
202
222
|
|
|
223
|
+
function ActionPresentationBlock({
|
|
224
|
+
presentation,
|
|
225
|
+
renderFile,
|
|
226
|
+
resolveUrl,
|
|
227
|
+
}: {
|
|
228
|
+
presentation: CloudOsActionPresentation;
|
|
229
|
+
renderFile?: FileRenderer;
|
|
230
|
+
resolveUrl?: CloudOsUrlResolver;
|
|
231
|
+
}) {
|
|
232
|
+
const { state } = presentation;
|
|
233
|
+
if (state === "ready") {
|
|
234
|
+
return (
|
|
235
|
+
<div
|
|
236
|
+
data-action-presentation={state}
|
|
237
|
+
role="status"
|
|
238
|
+
aria-label="Completed"
|
|
239
|
+
className="space-y-2"
|
|
240
|
+
>
|
|
241
|
+
{presentation.content.length === 0
|
|
242
|
+
? <p className="m-0 text-[14px] leading-5 text-kumo-subtle">Completed</p>
|
|
243
|
+
: presentation.content.map((file, index) => (
|
|
244
|
+
<Fragment key={`${file.url}:${index}`}>
|
|
245
|
+
{renderFileView(renderFile, {
|
|
246
|
+
file: {
|
|
247
|
+
...file,
|
|
248
|
+
filename: "Generated file",
|
|
249
|
+
url: resolveUrl?.(file.url) ?? file.url,
|
|
250
|
+
},
|
|
251
|
+
placement: "assistant-message",
|
|
252
|
+
variant: "product",
|
|
253
|
+
})}
|
|
254
|
+
</Fragment>
|
|
255
|
+
))}
|
|
256
|
+
</div>
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const product = presentation.produces;
|
|
261
|
+
const productName = product?.type ?? "other";
|
|
262
|
+
const isFileProduct = productName !== "data" && productName !== "other";
|
|
263
|
+
const body = state === "running"
|
|
264
|
+
? isFileProduct
|
|
265
|
+
? `Generating ${productName}${product?.mediaType ? ` (${product.mediaType})` : ""}`
|
|
266
|
+
: "Generating"
|
|
267
|
+
: state === "outcome_unknown"
|
|
268
|
+
? "The provider may have completed this action. Check the status before trying again."
|
|
269
|
+
: undefined;
|
|
270
|
+
if (state === "running") {
|
|
271
|
+
return (
|
|
272
|
+
<div
|
|
273
|
+
data-action-presentation={state}
|
|
274
|
+
data-product-type={productName}
|
|
275
|
+
role="status"
|
|
276
|
+
aria-label={body}
|
|
277
|
+
>
|
|
278
|
+
{isFileProduct
|
|
279
|
+
? <ProductGeneration type={productName} mediaType={product?.mediaType} />
|
|
280
|
+
: <GenerationLoader />}
|
|
281
|
+
</div>
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
return (
|
|
285
|
+
<div
|
|
286
|
+
data-action-presentation={state}
|
|
287
|
+
role={state === "failed" ? "alert" : "status"}
|
|
288
|
+
className="inline-flex max-w-72 items-start rounded-xl border border-kumo-line bg-kumo-elevated px-3 py-2 text-[13px] leading-5"
|
|
289
|
+
>
|
|
290
|
+
<span>
|
|
291
|
+
<strong className="block font-medium text-kumo-default">
|
|
292
|
+
{state === "failed" ? "Generation failed" : "Status unavailable"}
|
|
293
|
+
</strong>
|
|
294
|
+
{body && <span className="block text-kumo-subtle">{body}</span>}
|
|
295
|
+
</span>
|
|
296
|
+
</div>
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
203
300
|
export function CloudOsChatMessages({
|
|
204
301
|
messages,
|
|
205
302
|
status,
|
|
@@ -231,6 +328,9 @@ export function CloudOsChatMessages({
|
|
|
231
328
|
const [expandedToolCalls, setExpandedToolCalls] = useState<ReadonlySet<string>>(
|
|
232
329
|
() => new Set(),
|
|
233
330
|
);
|
|
331
|
+
const [expandedWorkEntries, setExpandedWorkEntries] = useState<
|
|
332
|
+
ReadonlySet<string>
|
|
333
|
+
>(() => new Set());
|
|
234
334
|
const [expandedCompactions, setExpandedCompactions] = useState<
|
|
235
335
|
ReadonlySet<string>
|
|
236
336
|
>(() => new Set());
|
|
@@ -248,6 +348,31 @@ export function CloudOsChatMessages({
|
|
|
248
348
|
}),
|
|
249
349
|
[approvalIdOf, isActive, messages, showThinkingTraces],
|
|
250
350
|
);
|
|
351
|
+
const narrativeEntryKeys = useMemo(
|
|
352
|
+
() =>
|
|
353
|
+
new Set(
|
|
354
|
+
entries.flatMap((entry) =>
|
|
355
|
+
entry.type === "assistant" &&
|
|
356
|
+
partitionAssistantBlocks(entry.blocks).content.some(
|
|
357
|
+
(block) => block.kind === "text",
|
|
358
|
+
)
|
|
359
|
+
? [entry.key]
|
|
360
|
+
: [],
|
|
361
|
+
),
|
|
362
|
+
),
|
|
363
|
+
[entries],
|
|
364
|
+
);
|
|
365
|
+
const previousNarrativeEntryKeys = useRef<ReadonlySet<string>>(
|
|
366
|
+
narrativeEntryKeys,
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
useEffect(() => {
|
|
370
|
+
const previous = previousNarrativeEntryKeys.current;
|
|
371
|
+
previousNarrativeEntryKeys.current = narrativeEntryKeys;
|
|
372
|
+
setExpandedWorkEntries((expanded) =>
|
|
373
|
+
collapseEntriesWithNewNarrative(expanded, previous, narrativeEntryKeys),
|
|
374
|
+
);
|
|
375
|
+
}, [narrativeEntryKeys]);
|
|
251
376
|
|
|
252
377
|
const entryTopClasses = useMemo(
|
|
253
378
|
() =>
|
|
@@ -266,6 +391,15 @@ export function CloudOsChatMessages({
|
|
|
266
391
|
});
|
|
267
392
|
}, []);
|
|
268
393
|
|
|
394
|
+
const toggleWorkEntry = useCallback((key: string) => {
|
|
395
|
+
setExpandedWorkEntries((current) => {
|
|
396
|
+
const next = new Set(current);
|
|
397
|
+
if (next.has(key)) next.delete(key);
|
|
398
|
+
else next.add(key);
|
|
399
|
+
return next;
|
|
400
|
+
});
|
|
401
|
+
}, []);
|
|
402
|
+
|
|
269
403
|
const toggleCompaction = useCallback((key: string) => {
|
|
270
404
|
setExpandedCompactions((current) => {
|
|
271
405
|
const next = new Set(current);
|
|
@@ -413,7 +547,7 @@ export function CloudOsChatMessages({
|
|
|
413
547
|
<span className="h-px flex-1 bg-kumo-line" aria-hidden="true" />
|
|
414
548
|
</div>
|
|
415
549
|
{expanded && (
|
|
416
|
-
<div className="
|
|
550
|
+
<div className="cos-markdown mt-3 rounded-2xl border border-kumo-line/70 bg-kumo-elevated/45 p-3.5 text-[13px] leading-[19px]">
|
|
417
551
|
<MarkdownMessage
|
|
418
552
|
message={entry.summary}
|
|
419
553
|
resolveUrl={resolveUrl}
|
|
@@ -424,24 +558,90 @@ export function CloudOsChatMessages({
|
|
|
424
558
|
);
|
|
425
559
|
}
|
|
426
560
|
|
|
427
|
-
const
|
|
561
|
+
const { work: workBlocks, content: contentBlocks } =
|
|
562
|
+
partitionAssistantBlocks(entry.blocks);
|
|
563
|
+
const workGroups = workBlocks.flatMap((block) =>
|
|
564
|
+
block.kind === "toolGroup" ? [block.group] : [],
|
|
565
|
+
);
|
|
566
|
+
const hasNarrative = contentBlocks.some(
|
|
428
567
|
(block) => block.kind === "text",
|
|
429
568
|
);
|
|
430
569
|
|
|
431
570
|
return (
|
|
432
571
|
<div
|
|
433
572
|
key={entry.key}
|
|
434
|
-
className={`${topClass} min-w-0 w-full max-w-[860px]
|
|
573
|
+
className={`${topClass} min-w-0 w-full max-w-[860px]`}
|
|
435
574
|
>
|
|
436
|
-
<div className="group/agentMessage relative space-y-
|
|
437
|
-
{
|
|
575
|
+
<div className="group/agentMessage relative space-y-4">
|
|
576
|
+
{workBlocks.length > 0 && (
|
|
577
|
+
<WorkTraceDisclosure
|
|
578
|
+
durationMs={entry.durationMs}
|
|
579
|
+
groups={workGroups}
|
|
580
|
+
completed={!isActive}
|
|
581
|
+
open={expandedWorkEntries.has(entry.key)}
|
|
582
|
+
onToggle={() => toggleWorkEntry(entry.key)}
|
|
583
|
+
>
|
|
584
|
+
{workBlocks.map((block) => {
|
|
585
|
+
switch (block.kind) {
|
|
586
|
+
case "reasoning":
|
|
587
|
+
case "text":
|
|
588
|
+
return (
|
|
589
|
+
<WorkDescriptionRow
|
|
590
|
+
key={block.key}
|
|
591
|
+
text={block.text}
|
|
592
|
+
resolveUrl={resolveUrl}
|
|
593
|
+
/>
|
|
594
|
+
);
|
|
595
|
+
case "toolGroup":
|
|
596
|
+
return (
|
|
597
|
+
<ToolGroupRow
|
|
598
|
+
key={block.key}
|
|
599
|
+
group={block.group}
|
|
600
|
+
open={expandedToolCalls.has(block.group.key)}
|
|
601
|
+
expandedKeys={expandedToolCalls}
|
|
602
|
+
onToggle={toggleToolCall}
|
|
603
|
+
/>
|
|
604
|
+
);
|
|
605
|
+
case "plan":
|
|
606
|
+
return (
|
|
607
|
+
<PlanBlock
|
|
608
|
+
key={block.key}
|
|
609
|
+
steps={block.steps}
|
|
610
|
+
running={block.running}
|
|
611
|
+
defaultOpen={false}
|
|
612
|
+
showCollapsedSummary={false}
|
|
613
|
+
/>
|
|
614
|
+
);
|
|
615
|
+
case "parallel":
|
|
616
|
+
return (
|
|
617
|
+
<ParallelBlock
|
|
618
|
+
key={block.key}
|
|
619
|
+
label={block.label}
|
|
620
|
+
tools={block.tools}
|
|
621
|
+
/>
|
|
622
|
+
);
|
|
623
|
+
case "subagents":
|
|
624
|
+
return (
|
|
625
|
+
<SubAgentsBlock
|
|
626
|
+
key={block.key}
|
|
627
|
+
agents={block.agents}
|
|
628
|
+
/>
|
|
629
|
+
);
|
|
630
|
+
default:
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
633
|
+
})}
|
|
634
|
+
</WorkTraceDisclosure>
|
|
635
|
+
)}
|
|
636
|
+
|
|
637
|
+
{contentBlocks.map((block) => {
|
|
438
638
|
switch (block.kind) {
|
|
439
639
|
case "reasoning":
|
|
440
640
|
return (
|
|
441
|
-
<
|
|
641
|
+
<WorkDescriptionRow
|
|
442
642
|
key={block.key}
|
|
443
|
-
|
|
444
|
-
|
|
643
|
+
text={block.text}
|
|
644
|
+
resolveUrl={resolveUrl}
|
|
445
645
|
/>
|
|
446
646
|
);
|
|
447
647
|
case "text": {
|
|
@@ -564,6 +764,15 @@ export function CloudOsChatMessages({
|
|
|
564
764
|
})}
|
|
565
765
|
</Fragment>
|
|
566
766
|
);
|
|
767
|
+
case "actionPresentation":
|
|
768
|
+
return (
|
|
769
|
+
<ActionPresentationBlock
|
|
770
|
+
key={block.key}
|
|
771
|
+
presentation={block.presentation}
|
|
772
|
+
renderFile={renderFile}
|
|
773
|
+
resolveUrl={resolveUrl}
|
|
774
|
+
/>
|
|
775
|
+
);
|
|
567
776
|
case "error":
|
|
568
777
|
return (
|
|
569
778
|
<ErrorBlock
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { CloudOsActionPresentation } from "./transcript-model";
|
|
2
|
+
|
|
3
|
+
export type ProductType = NonNullable<CloudOsActionPresentation["produces"]>["type"];
|
|
4
|
+
|
|
5
|
+
export const PRODUCT_PRESETS = {
|
|
6
|
+
image: { frame: "aspect-square w-52", columns: 6, rows: 6 },
|
|
7
|
+
video: { frame: "aspect-video w-64", columns: 8, rows: 4 },
|
|
8
|
+
audio: { frame: "h-24 w-64", columns: 9, rows: 2 },
|
|
9
|
+
document: { frame: "aspect-[4/5] w-44", columns: 5, rows: 7 },
|
|
10
|
+
archive: { frame: "h-28 w-44", columns: 5, rows: 3 },
|
|
11
|
+
data: { frame: "h-32 w-52", columns: 6, rows: 3 },
|
|
12
|
+
other: { frame: "h-28 w-44", columns: 5, rows: 3 },
|
|
13
|
+
} satisfies Record<ProductType, { frame: string; columns: number; rows: number }>;
|
|
14
|
+
|
|
15
|
+
export function productTypeForMediaType(mediaType?: string): ProductType {
|
|
16
|
+
const type = mediaType?.toLowerCase() ?? "";
|
|
17
|
+
if (type.startsWith("image/")) return "image";
|
|
18
|
+
if (type.startsWith("audio/")) return "audio";
|
|
19
|
+
if (type.startsWith("video/")) return "video";
|
|
20
|
+
if (/zip|archive|compressed|gzip|tar/.test(type)) return "archive";
|
|
21
|
+
if (/json|csv|spreadsheet|excel|parquet|sql/.test(type)) return "data";
|
|
22
|
+
if (type.startsWith("text/") || /pdf|document|word|rtf|presentation/.test(type)) {
|
|
23
|
+
return "document";
|
|
24
|
+
}
|
|
25
|
+
return "other";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Product-aware adaptation of https://www.assistant-ui.com/elements/image-generation. */
|
|
29
|
+
export function ProductGeneration({
|
|
30
|
+
type,
|
|
31
|
+
mediaType,
|
|
32
|
+
}: {
|
|
33
|
+
type: ProductType;
|
|
34
|
+
mediaType?: string;
|
|
35
|
+
}) {
|
|
36
|
+
const { frame, columns, rows } = PRODUCT_PRESETS[type];
|
|
37
|
+
const dots = Array.from({ length: columns * rows }, (_, index) => index);
|
|
38
|
+
return (
|
|
39
|
+
<div className="flex w-fit max-w-full flex-col gap-2.5" data-product-generation={type}>
|
|
40
|
+
<div className={`themed-thumbnail-shadow relative max-w-full overflow-hidden rounded-2xl border border-kumo-line bg-kumo-elevated ${frame}`}>
|
|
41
|
+
<div
|
|
42
|
+
className="absolute inset-0 grid place-items-center p-5"
|
|
43
|
+
data-generation-grid={`${columns}x${rows}`}
|
|
44
|
+
style={{
|
|
45
|
+
gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`,
|
|
46
|
+
gridTemplateRows: `repeat(${rows}, minmax(0, 1fr))`,
|
|
47
|
+
}}
|
|
48
|
+
aria-hidden="true"
|
|
49
|
+
>
|
|
50
|
+
{dots.map((dot) => {
|
|
51
|
+
const row = Math.floor(dot / columns);
|
|
52
|
+
const column = dot % columns;
|
|
53
|
+
return (
|
|
54
|
+
<span
|
|
55
|
+
key={dot}
|
|
56
|
+
data-generation-dot=""
|
|
57
|
+
className="size-1 rounded-full bg-kumo-fill-hover animate-pulse motion-reduce:animate-none"
|
|
58
|
+
style={{ animationDelay: `${(row + column) * 90}ms` }}
|
|
59
|
+
/>
|
|
60
|
+
);
|
|
61
|
+
})}
|
|
62
|
+
</div>
|
|
63
|
+
{mediaType && (
|
|
64
|
+
<span className="absolute end-2.5 top-2.5 font-mono text-[10px] text-kumo-inactive">
|
|
65
|
+
{mediaType}
|
|
66
|
+
</span>
|
|
67
|
+
)}
|
|
68
|
+
</div>
|
|
69
|
+
<p className="m-0 min-w-0 truncate text-xs text-kumo-inactive">
|
|
70
|
+
<span className="capitalize text-kumo-subtle">{type}</span>
|
|
71
|
+
<span aria-hidden="true"> · </span>
|
|
72
|
+
<span className="cos-thinking-shimmer motion-reduce:animate-none">Generating</span>
|
|
73
|
+
</p>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const CELLS = Array.from({ length: 9 }, (_, index) => index);
|
|
2
|
+
|
|
3
|
+
/** CSS-driven adaptation of https://www.assistant-ui.com/elements/loading-state. */
|
|
4
|
+
export function GenerationLoader() {
|
|
5
|
+
return (
|
|
6
|
+
<div
|
|
7
|
+
data-slot="generation-loader"
|
|
8
|
+
className="inline-flex flex-col items-center gap-3 py-2"
|
|
9
|
+
>
|
|
10
|
+
<div aria-hidden="true" className="grid grid-cols-3 gap-1">
|
|
11
|
+
{CELLS.map((cell) => (
|
|
12
|
+
<span
|
|
13
|
+
key={cell}
|
|
14
|
+
data-loading-cell=""
|
|
15
|
+
className="size-2 animate-pulse rounded-full bg-[var(--text-color-kumo-default)] motion-reduce:animate-none"
|
|
16
|
+
style={{ animationDelay: `${cell * 100}ms` }}
|
|
17
|
+
/>
|
|
18
|
+
))}
|
|
19
|
+
</div>
|
|
20
|
+
<span className="cos-thinking-shimmer text-sm text-kumo-inactive motion-reduce:animate-none">
|
|
21
|
+
Generating
|
|
22
|
+
</span>
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TextMessagePartProvider,
|
|
3
|
+
useMessagePartText,
|
|
4
|
+
useSmooth,
|
|
5
|
+
type SmoothOptions,
|
|
6
|
+
} from "@assistant-ui/react";
|
|
1
7
|
import { memo, useMemo, type ReactNode } from "react";
|
|
2
8
|
import {
|
|
3
9
|
defaultRehypePlugins,
|
|
@@ -14,7 +20,10 @@ import {
|
|
|
14
20
|
export type CloudOsUrlResolver = (url: string) => string | null | undefined;
|
|
15
21
|
|
|
16
22
|
const ALLOWED_PROTOCOLS = new Set(["http:", "https:", "mailto:"]);
|
|
17
|
-
const
|
|
23
|
+
const SMOOTH_STREAMING = {
|
|
24
|
+
maxCharsPerFrame: 12,
|
|
25
|
+
minCommitMs: 32,
|
|
26
|
+
} satisfies SmoothOptions;
|
|
18
27
|
|
|
19
28
|
export function safeExternalUrl(href: string | undefined): string | undefined {
|
|
20
29
|
if (!href) return undefined;
|
|
@@ -101,6 +110,20 @@ export const MarkdownMessage = memo(function MarkdownMessage({
|
|
|
101
110
|
resolveUrl?: CloudOsUrlResolver;
|
|
102
111
|
streaming?: boolean;
|
|
103
112
|
}): ReactNode {
|
|
113
|
+
return (
|
|
114
|
+
<TextMessagePartProvider text={message} isRunning={streaming}>
|
|
115
|
+
<MarkdownMessageContent resolveUrl={resolveUrl} />
|
|
116
|
+
</TextMessagePartProvider>
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
function MarkdownMessageContent({
|
|
121
|
+
resolveUrl,
|
|
122
|
+
}: {
|
|
123
|
+
resolveUrl?: CloudOsUrlResolver;
|
|
124
|
+
}) {
|
|
125
|
+
const part = useSmooth(useMessagePartText(), SMOOTH_STREAMING);
|
|
126
|
+
const streaming = part.status.type === "running";
|
|
104
127
|
const rehypePlugins = useMemo(
|
|
105
128
|
() =>
|
|
106
129
|
resolveUrl
|
|
@@ -110,13 +133,7 @@ export const MarkdownMessage = memo(function MarkdownMessage({
|
|
|
110
133
|
);
|
|
111
134
|
return (
|
|
112
135
|
<Streamdown
|
|
113
|
-
animated={
|
|
114
|
-
streaming
|
|
115
|
-
? CJK_TEXT.test(message)
|
|
116
|
-
? { sep: "char", duration: 100, stagger: 5 }
|
|
117
|
-
: true
|
|
118
|
-
: false
|
|
119
|
-
}
|
|
136
|
+
animated={false}
|
|
120
137
|
className="space-y-0"
|
|
121
138
|
controls={false}
|
|
122
139
|
mode={streaming ? "streaming" : "static"}
|
|
@@ -125,7 +142,7 @@ export const MarkdownMessage = memo(function MarkdownMessage({
|
|
|
125
142
|
skipHtml
|
|
126
143
|
components={DEFAULT_COMPONENTS}
|
|
127
144
|
>
|
|
128
|
-
{
|
|
145
|
+
{part.text}
|
|
129
146
|
</Streamdown>
|
|
130
147
|
);
|
|
131
|
-
}
|
|
148
|
+
}
|