@mastra/playground-ui 49.0.0 → 49.1.0-alpha.1
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/CHANGELOG.md +55 -0
- package/dist/components/MainSidebar.cjs.js +2 -1
- package/dist/components/MainSidebar.cjs.js.map +1 -1
- package/dist/components/MainSidebar.es.js +2 -1
- package/dist/components/MainSidebar.es.js.map +1 -1
- package/dist/components/MarkdownRenderer.cjs.js +1 -1
- package/dist/components/MarkdownRenderer.es.js +1 -1
- package/dist/components/ai/plan.cjs.js +1 -1
- package/dist/components/ai/plan.es.js +1 -1
- package/dist/components/ai/task-list.cjs.js +49 -34
- package/dist/components/ai/task-list.cjs.js.map +1 -1
- package/dist/components/ai/task-list.es.js +50 -34
- package/dist/components/ai/task-list.es.js.map +1 -1
- package/dist/markdown-renderer-BU7tImQt.cjs +280 -0
- package/dist/markdown-renderer-BU7tImQt.cjs.map +1 -0
- package/dist/markdown-renderer-Bi4HR-Me.js +278 -0
- package/dist/markdown-renderer-Bi4HR-Me.js.map +1 -0
- package/dist/markdown-renderer.css +7 -21
- package/dist/src/ds/components/MainSidebar/main-sidebar-nav-item-classes.d.ts +1 -1
- package/dist/src/ds/components/MarkdownRenderer/arriving.d.ts +19 -0
- package/dist/src/ds/components/MarkdownRenderer/markdown-renderer.d.ts +7 -1
- package/dist/src/ds/components/MarkdownRenderer/markdown-renderer.stories.d.ts +2 -0
- package/dist/src/ds/components/MarkdownRenderer/use-reveal.d.ts +22 -0
- package/dist/src/ds/components/ai/task-list/task-list.d.ts +3 -9
- package/dist/style.css +9 -465
- package/package.json +7 -7
- package/dist/markdown-renderer-BDe_1JLH.js +0 -210
- package/dist/markdown-renderer-BDe_1JLH.js.map +0 -1
- package/dist/markdown-renderer-WHy5Cyu2.cjs +0 -212
- package/dist/markdown-renderer-WHy5Cyu2.cjs.map +0 -1
- package/dist/src/ds/components/MarkdownRenderer/word-spans.d.ts +0 -26
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { jsx } from 'react/jsx-runtime';
|
|
2
|
+
import { useState, useMemo, useRef, useEffect, memo } from 'react';
|
|
3
|
+
import Markdown from 'react-markdown';
|
|
4
|
+
import remarkGfm from 'remark-gfm';
|
|
5
|
+
import remend from 'remend';
|
|
6
|
+
import { C as CodeBlock } from './code-block-V4wpmbud.js';
|
|
7
|
+
import { c as cn } from './cn-BakJzhuv.js';
|
|
8
|
+
|
|
9
|
+
import './markdown-renderer.css';const ARRIVING = "mastra-markdown-arriving";
|
|
10
|
+
const UNBROKEN = /* @__PURE__ */ new Set(["pre", "code"]);
|
|
11
|
+
const wrap = (value, arriving) => ({
|
|
12
|
+
type: "element",
|
|
13
|
+
tagName: "span",
|
|
14
|
+
properties: arriving ? { className: [ARRIVING] } : {},
|
|
15
|
+
children: [{ type: "text", value }]
|
|
16
|
+
});
|
|
17
|
+
function markArriving(node) {
|
|
18
|
+
const classes = node.properties.className;
|
|
19
|
+
node.properties.className = Array.isArray(classes) ? [...classes, ARRIVING] : [ARRIVING];
|
|
20
|
+
}
|
|
21
|
+
function rebuild(nodes, arriving) {
|
|
22
|
+
const out = [];
|
|
23
|
+
for (const node of nodes) {
|
|
24
|
+
if (node.type === "text") {
|
|
25
|
+
for (const piece of node.value.match(/\S+|\s+/g) ?? []) {
|
|
26
|
+
if (/\S/.test(piece)) out.push(wrap(piece, arriving()));
|
|
27
|
+
else out.push({ type: "text", value: piece });
|
|
28
|
+
}
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
out.push(node);
|
|
32
|
+
if (node.type !== "element") continue;
|
|
33
|
+
if (!UNBROKEN.has(node.tagName)) node.children = rebuild(node.children, arriving);
|
|
34
|
+
else if (arriving()) markArriving(node);
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
function rehypeArriving(settled) {
|
|
39
|
+
return function attach() {
|
|
40
|
+
return (tree) => {
|
|
41
|
+
let word = 0;
|
|
42
|
+
tree.children = rebuild(tree.children, () => word++ >= settled);
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const FENCE_OPEN = /^ {0,3}(`{3,}|~{3,})/;
|
|
48
|
+
const FENCE_CLOSE = /^ {0,3}(`{3,}|~{3,})[ \t]*$/;
|
|
49
|
+
const LIST_ITEM = /^ {0,3}(?:[-*+]|\d{1,9}[.)])(?:[ \t]|$)/;
|
|
50
|
+
const INDENTED = /^(?:\t| {2,})/;
|
|
51
|
+
const DEFINITION = /^ {0,3}\[[^\]]+\]:/;
|
|
52
|
+
function fenceOpenedBy(line) {
|
|
53
|
+
return FENCE_OPEN.exec(line)?.[1];
|
|
54
|
+
}
|
|
55
|
+
function closesFence(line, marker) {
|
|
56
|
+
return FENCE_CLOSE.exec(line)?.[1]?.startsWith(marker) ?? false;
|
|
57
|
+
}
|
|
58
|
+
function startsBlock(line, inList) {
|
|
59
|
+
if (INDENTED.test(line)) return false;
|
|
60
|
+
return !(inList && LIST_ITEM.test(line));
|
|
61
|
+
}
|
|
62
|
+
function splitBlocks(markdown) {
|
|
63
|
+
const lines = markdown.split("\n");
|
|
64
|
+
const starts = [0];
|
|
65
|
+
let fence;
|
|
66
|
+
let started = false;
|
|
67
|
+
let broken = false;
|
|
68
|
+
let inList = false;
|
|
69
|
+
for (const [index, line] of lines.entries()) {
|
|
70
|
+
if (fence) {
|
|
71
|
+
if (closesFence(line, fence)) fence = void 0;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (line.trim() === "") {
|
|
75
|
+
broken = started;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (DEFINITION.test(line)) return [markdown];
|
|
79
|
+
if (broken && startsBlock(line, inList)) {
|
|
80
|
+
starts.push(index);
|
|
81
|
+
started = false;
|
|
82
|
+
}
|
|
83
|
+
if (!started) inList = LIST_ITEM.test(line);
|
|
84
|
+
started = true;
|
|
85
|
+
broken = false;
|
|
86
|
+
fence = fenceOpenedBy(line);
|
|
87
|
+
}
|
|
88
|
+
return starts.map((start, position) => {
|
|
89
|
+
const end = starts[position + 1];
|
|
90
|
+
const block = lines.slice(start, end ?? lines.length).join("\n");
|
|
91
|
+
return end === void 0 ? block : `${block}
|
|
92
|
+
`;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const BUFFER_MS = 1e3;
|
|
97
|
+
const CORRECT_MS = 1e3;
|
|
98
|
+
const FLOW_MS = 700;
|
|
99
|
+
const EASE_MS = 400;
|
|
100
|
+
const SLOWEST = 10;
|
|
101
|
+
const FASTEST = 60;
|
|
102
|
+
const MAX_LAG = 300;
|
|
103
|
+
const LEAD_IN = 12;
|
|
104
|
+
function measure(flow, now, gained) {
|
|
105
|
+
flow.rate = flow.rate * Math.exp(-(now - flow.at) / FLOW_MS) + gained / (FLOW_MS / 1e3);
|
|
106
|
+
flow.at = now;
|
|
107
|
+
return flow.rate;
|
|
108
|
+
}
|
|
109
|
+
function prefersReducedMotion() {
|
|
110
|
+
if (typeof window === "undefined" || !window.matchMedia) return false;
|
|
111
|
+
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
112
|
+
}
|
|
113
|
+
function wordStops(text) {
|
|
114
|
+
const stops = [0];
|
|
115
|
+
const word = /\S+/g;
|
|
116
|
+
for (let match = word.exec(text); match; match = word.exec(text)) stops.push(match.index + match[0].length);
|
|
117
|
+
return stops;
|
|
118
|
+
}
|
|
119
|
+
function useReveal(text, streaming) {
|
|
120
|
+
const [calm] = useState(prefersReducedMotion);
|
|
121
|
+
const stops = useMemo(() => wordStops(text), [text]);
|
|
122
|
+
const words = stops.length - 1;
|
|
123
|
+
const [landed, setLanded] = useState(() => streaming && !calm ? Math.max(0, words - LEAD_IN) : words);
|
|
124
|
+
const shown = calm ? words : Math.min(words, Math.max(landed, words - MAX_LAG));
|
|
125
|
+
if (shown !== landed) setLanded(shown);
|
|
126
|
+
const chasing = shown < words;
|
|
127
|
+
const goal = useRef(words);
|
|
128
|
+
const cursor = useRef(shown);
|
|
129
|
+
const flow = useRef({ rate: 0, at: 0 });
|
|
130
|
+
const pace = useRef(SLOWEST);
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
measure(flow.current, performance.now(), Math.max(0, words - goal.current));
|
|
133
|
+
goal.current = words;
|
|
134
|
+
if (cursor.current < shown || cursor.current > words) cursor.current = shown;
|
|
135
|
+
});
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
if (!chasing) return;
|
|
138
|
+
let drawn = performance.now();
|
|
139
|
+
let frame = requestAnimationFrame(function step(now) {
|
|
140
|
+
const elapsed = now - drawn;
|
|
141
|
+
drawn = now;
|
|
142
|
+
const arriving = measure(flow.current, now, 0);
|
|
143
|
+
const from = cursor.current;
|
|
144
|
+
const behind = goal.current - from;
|
|
145
|
+
if (behind > 0) {
|
|
146
|
+
const held = arriving * BUFFER_MS / 1e3;
|
|
147
|
+
const aim = Math.min(FASTEST, Math.max(SLOWEST, arriving + (behind - held) * 1e3 / CORRECT_MS));
|
|
148
|
+
pace.current += (aim - pace.current) * Math.min(1, elapsed / EASE_MS);
|
|
149
|
+
cursor.current = Math.min(goal.current, Math.floor(from) + 1, from + elapsed * pace.current / 1e3);
|
|
150
|
+
if (Math.floor(cursor.current) !== Math.floor(from)) setLanded(Math.floor(cursor.current));
|
|
151
|
+
}
|
|
152
|
+
frame = requestAnimationFrame(step);
|
|
153
|
+
});
|
|
154
|
+
return () => cancelAnimationFrame(frame);
|
|
155
|
+
}, [chasing]);
|
|
156
|
+
if (!chasing) return text;
|
|
157
|
+
return text.slice(0, stops[shown]);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const MarkdownRenderer = memo(function MarkdownRenderer2({
|
|
161
|
+
children,
|
|
162
|
+
className,
|
|
163
|
+
externalLinkTarget = "tab",
|
|
164
|
+
streaming = false
|
|
165
|
+
}) {
|
|
166
|
+
const full = decodeEscapedNewlines(children);
|
|
167
|
+
const shown = useReveal(full, streaming);
|
|
168
|
+
const blocks = useMemo(() => splitBlocks(shown), [shown]);
|
|
169
|
+
const last = blocks.length - 1;
|
|
170
|
+
const components = externalLinkTarget === "window" ? WINDOW_COMPONENTS : COMPONENTS;
|
|
171
|
+
const growing = streaming || shown !== full;
|
|
172
|
+
const [joined] = useState(
|
|
173
|
+
() => streaming ? { blocks: blocks.length, words: countWords(blocks[last] ?? "") } : void 0
|
|
174
|
+
);
|
|
175
|
+
const settledWords = (index) => {
|
|
176
|
+
if (!joined || index < joined.blocks - 1) return void 0;
|
|
177
|
+
return index === joined.blocks - 1 ? joined.words : 0;
|
|
178
|
+
};
|
|
179
|
+
const tail = blocks[last] ?? "";
|
|
180
|
+
const mended = useMemo(() => growing ? remend(tail, REMEND_OPTIONS) : tail, [growing, tail]);
|
|
181
|
+
return /* @__PURE__ */ jsx("div", { className: cn("mastra-markdown", className), children: blocks.map((block, index) => /* @__PURE__ */ jsx(
|
|
182
|
+
MarkdownBlock,
|
|
183
|
+
{
|
|
184
|
+
content: index === last ? mended : block,
|
|
185
|
+
settledWords: settledWords(index),
|
|
186
|
+
components
|
|
187
|
+
},
|
|
188
|
+
index
|
|
189
|
+
)) });
|
|
190
|
+
});
|
|
191
|
+
const MarkdownBlock = memo(function MarkdownBlock2({
|
|
192
|
+
components,
|
|
193
|
+
content,
|
|
194
|
+
settledWords
|
|
195
|
+
}) {
|
|
196
|
+
const rehypePlugins = useMemo(
|
|
197
|
+
() => settledWords === void 0 ? SETTLED : [rehypeArriving(settledWords)],
|
|
198
|
+
[settledWords]
|
|
199
|
+
);
|
|
200
|
+
return /* @__PURE__ */ jsx(Markdown, { remarkPlugins: REMARK_PLUGINS, rehypePlugins, components, children: content });
|
|
201
|
+
});
|
|
202
|
+
const SETTLED = [];
|
|
203
|
+
function countWords(text) {
|
|
204
|
+
return text.match(/\S+/g)?.length ?? 0;
|
|
205
|
+
}
|
|
206
|
+
function decodeEscapedNewlines(text) {
|
|
207
|
+
return text.includes("\n") ? text : text.replace(/\\n/g, "\n");
|
|
208
|
+
}
|
|
209
|
+
function languageOf(node) {
|
|
210
|
+
const classNames = node.properties.className;
|
|
211
|
+
if (!Array.isArray(classNames)) return void 0;
|
|
212
|
+
const language = classNames.find((entry) => typeof entry === "string" && entry.startsWith("language-"));
|
|
213
|
+
return typeof language === "string" ? language.slice("language-".length) : void 0;
|
|
214
|
+
}
|
|
215
|
+
function fencedCode(node) {
|
|
216
|
+
const child = node?.children.find((entry) => entry.type === "element" && entry.tagName === "code");
|
|
217
|
+
if (child?.type !== "element") return void 0;
|
|
218
|
+
const code = child.children.map((entry) => entry.type === "text" ? entry.value : "").join("");
|
|
219
|
+
return { code: code.replace(/\n$/, ""), language: languageOf(child) };
|
|
220
|
+
}
|
|
221
|
+
function MarkdownCodeBlock({
|
|
222
|
+
node,
|
|
223
|
+
children,
|
|
224
|
+
className
|
|
225
|
+
}) {
|
|
226
|
+
const fenced = fencedCode(node);
|
|
227
|
+
if (!fenced) return /* @__PURE__ */ jsx("pre", { className, children });
|
|
228
|
+
return /* @__PURE__ */ jsx(
|
|
229
|
+
CodeBlock,
|
|
230
|
+
{
|
|
231
|
+
code: fenced.code,
|
|
232
|
+
lang: fenced.language,
|
|
233
|
+
overflow: "scroll",
|
|
234
|
+
className: cn("my-3 bg-surface1", className),
|
|
235
|
+
copyMessage: "Copied code to clipboard"
|
|
236
|
+
}
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
const POPUP_WINDOW_FEATURES = "popup=yes,width=720,height=800,resizable=yes,scrollbars=yes";
|
|
240
|
+
function isPlainLeftClick(event) {
|
|
241
|
+
return event.button === 0 && !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey;
|
|
242
|
+
}
|
|
243
|
+
function markdownLink(externalLinkTarget) {
|
|
244
|
+
return function MarkdownLink({ href, title, children }) {
|
|
245
|
+
const isExternal = /^https?:\/\//i.test(href ?? "");
|
|
246
|
+
const openInPopup = (event) => {
|
|
247
|
+
if (!isExternal || externalLinkTarget !== "window" || !isPlainLeftClick(event)) return;
|
|
248
|
+
const popup = window.open(href, "_blank", POPUP_WINDOW_FEATURES);
|
|
249
|
+
if (!popup) return;
|
|
250
|
+
popup.opener = null;
|
|
251
|
+
event.preventDefault();
|
|
252
|
+
};
|
|
253
|
+
return /* @__PURE__ */ jsx(
|
|
254
|
+
"a",
|
|
255
|
+
{
|
|
256
|
+
href,
|
|
257
|
+
title,
|
|
258
|
+
onClick: openInPopup,
|
|
259
|
+
target: isExternal ? "_blank" : void 0,
|
|
260
|
+
rel: isExternal ? "noopener noreferrer" : void 0,
|
|
261
|
+
children
|
|
262
|
+
}
|
|
263
|
+
);
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
const REMARK_PLUGINS = [remarkGfm];
|
|
267
|
+
const REMEND_OPTIONS = { katex: false, linkMode: "text-only" };
|
|
268
|
+
const COMPONENTS = {
|
|
269
|
+
pre: MarkdownCodeBlock,
|
|
270
|
+
a: markdownLink("tab")
|
|
271
|
+
};
|
|
272
|
+
const WINDOW_COMPONENTS = {
|
|
273
|
+
...COMPONENTS,
|
|
274
|
+
a: markdownLink("window")
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
export { MarkdownRenderer as M };
|
|
278
|
+
//# sourceMappingURL=markdown-renderer-Bi4HR-Me.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markdown-renderer-Bi4HR-Me.js","sources":["../src/ds/components/MarkdownRenderer/arriving.ts","../src/ds/components/MarkdownRenderer/blocks.ts","../src/ds/components/MarkdownRenderer/use-reveal.ts","../src/ds/components/MarkdownRenderer/markdown-renderer.tsx"],"sourcesContent":["import type { ExtraProps } from 'react-markdown';\n\ntype MarkdownElement = NonNullable<ExtraProps['node']>;\ntype MarkdownChild = MarkdownElement['children'][number];\n\nconst ARRIVING = 'mastra-markdown-arriving';\n\n/** Fades whole: a fence renders through `CodeBlock`, and inline code carries a background. */\nconst UNBROKEN = new Set(['pre', 'code']);\n\nconst wrap = (value: string, arriving: boolean): MarkdownChild => ({\n type: 'element',\n tagName: 'span',\n properties: arriving ? { className: [ARRIVING] } : {},\n children: [{ type: 'text', value }],\n});\n\nfunction markArriving(node: MarkdownElement): void {\n const classes = node.properties.className;\n\n node.properties.className = Array.isArray(classes) ? [...classes, ARRIVING] : [ARRIVING];\n}\n\nfunction rebuild(nodes: MarkdownChild[], arriving: () => boolean): MarkdownChild[] {\n const out: MarkdownChild[] = [];\n\n for (const node of nodes) {\n if (node.type === 'text') {\n for (const piece of node.value.match(/\\S+|\\s+/g) ?? []) {\n if (/\\S/.test(piece)) out.push(wrap(piece, arriving()));\n else out.push({ type: 'text', value: piece });\n }\n\n continue;\n }\n\n out.push(node);\n\n if (node.type !== 'element') continue;\n\n if (!UNBROKEN.has(node.tagName)) node.children = rebuild(node.children, arriving);\n else if (arriving()) markArriving(node);\n }\n\n return out;\n}\n\n/**\n * Gives every word of a block an element of its own, and the entrance class to\n * those past the first `settled` — the words that were not on screen yet when\n * the reader joined this reply.\n *\n * Every word, not just the arriving ones: react-markdown parses afresh on each\n * landing and React matches the children it already mounted by position, so a\n * wrapper that came and went would land where a bare text node used to be and\n * remount half the paragraph, replaying entrances that had long finished.\n * Wrapping the lot keeps every position fixed — a new word only ever appends —\n * and a word never gains the class it did not mount with, so nothing replays.\n */\nexport function rehypeArriving(settled: number) {\n return function attach() {\n return (tree: { children: MarkdownChild[] }) => {\n let word = 0;\n\n tree.children = rebuild(tree.children, () => word++ >= settled);\n };\n };\n}\n","const FENCE_OPEN = /^ {0,3}(`{3,}|~{3,})/;\nconst FENCE_CLOSE = /^ {0,3}(`{3,}|~{3,})[ \\t]*$/;\nconst LIST_ITEM = /^ {0,3}(?:[-*+]|\\d{1,9}[.)])(?:[ \\t]|$)/;\nconst INDENTED = /^(?:\\t| {2,})/;\nconst DEFINITION = /^ {0,3}\\[[^\\]]+\\]:/;\n\nfunction fenceOpenedBy(line: string): string | undefined {\n return FENCE_OPEN.exec(line)?.[1];\n}\n\n// Both are runs of one character, so `startsWith` is \"same marker, at least as long\".\nfunction closesFence(line: string, marker: string): boolean {\n return FENCE_CLOSE.exec(line)?.[1]?.startsWith(marker) ?? false;\n}\n\nfunction startsBlock(line: string, inList: boolean): boolean {\n if (INDENTED.test(line)) return false;\n\n return !(inList && LIST_ITEM.test(line));\n}\n\n/**\n * Cuts markdown into top-level blocks so a growing reply re-parses its last\n * block instead of all of them. Rejoining the result returns the input, and\n * every block renders as it would have inside the whole.\n *\n * Each rule only ever merges: over-merging costs a re-parse, while cutting a\n * construct in two changes what it renders as. Link reference and footnote\n * definitions resolve across blank lines, which no line-level rule can see, so\n * reaching one outside a fence abandons the split for the whole text.\n */\nexport function splitBlocks(markdown: string): string[] {\n const lines = markdown.split('\\n');\n const starts = [0];\n let fence: string | undefined;\n let started = false;\n let broken = false;\n let inList = false;\n\n for (const [index, line] of lines.entries()) {\n if (fence) {\n if (closesFence(line, fence)) fence = undefined;\n continue;\n }\n\n if (line.trim() === '') {\n broken = started;\n continue;\n }\n\n if (DEFINITION.test(line)) return [markdown];\n\n if (broken && startsBlock(line, inList)) {\n starts.push(index);\n started = false;\n }\n\n if (!started) inList = LIST_ITEM.test(line);\n\n started = true;\n broken = false;\n fence = fenceOpenedBy(line);\n }\n\n return starts.map((start, position) => {\n const end = starts[position + 1];\n const block = lines.slice(start, end ?? lines.length).join('\\n');\n\n return end === undefined ? block : `${block}\\n`;\n });\n}\n","import { useEffect, useMemo, useRef, useState } from 'react';\n\n/** How much of the reply the reveal aims to be holding back, as time at the speed it is playing. */\nconst BUFFER_MS = 1000;\n\n/** How long it takes to work off a buffer that is too full or too empty. */\nconst CORRECT_MS = 1000;\n\n/** How long a word keeps counting towards the speed the reply is arriving at. */\nconst FLOW_MS = 700;\n\n/** How long the reveal takes to adopt a new speed, so a change of pace is never a jolt. */\nconst EASE_MS = 400;\n\n/** Words per second it will not go below: a short burst between two tool calls still reads briskly. */\nconst SLOWEST = 10;\n\n/** Words per second it will not go past: one word a frame, all a screen can draw. */\nconst FASTEST = 60;\n\n/** Words it will never fall further behind than — about five seconds at full speed. */\nconst MAX_LAG = 300;\n\n/** Words a reply already holds on its first render, which it joins rather than retypes. */\nconst LEAD_IN = 12;\n\ninterface Flow {\n /** Words per second the reply is arriving at, as an exponential average. */\n rate: number;\n at: number;\n}\n\n/** Decays what earlier words still count for, then deposits the ones that just arrived. */\nfunction measure(flow: Flow, now: number, gained: number): number {\n flow.rate = flow.rate * Math.exp(-(now - flow.at) / FLOW_MS) + gained / (FLOW_MS / 1000);\n flow.at = now;\n\n return flow.rate;\n}\n\nfunction prefersReducedMotion(): boolean {\n if (typeof window === 'undefined' || !window.matchMedia) return false;\n return window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n}\n\n/** Where the reveal can cut the text: before the first word, then after each one. */\nfunction wordStops(text: string): number[] {\n const stops = [0];\n const word = /\\S+/g;\n\n for (let match = word.exec(text); match; match = word.exec(text)) stops.push(match.index + match[0].length);\n\n return stops;\n}\n\n/**\n * A jitter buffer for a streamed reply, counted in words. Chunks reach the\n * browser unevenly — a proxy flushes, a tool call ends, the model changes pace —\n * and drawing each one on arrival makes a reply lurch: ten words at once, then\n * nothing for a fifth of a second.\n *\n * So the reveal runs on its own clock: a frame loop that chunks never interrupt,\n * laying down at most one word per frame however far behind it is. It plays at\n * the speed the reply is arriving at, measured directly, and leans on that only\n * to work the buffer back to `BUFFER_MS` of held text when it drifts. Measuring\n * the speed rather than inferring it from the buffer is what lets the buffer stay\n * shallow: a reply that lasts two seconds still finds its pace in the first few\n * hundred milliseconds, where a deeper buffer would spend the whole reply filling\n * and then sprint to catch up.\n *\n * Nothing here reads `streaming` past the first render: a reply that stops\n * arriving is one whose measured speed decays, which winds the buffer down on its\n * own. Words, not characters, are what a reader sees land, and they are not the\n * same length: an even flow of characters still drops \"and if we do\" in one frame\n * and spends four on \"implementation\".\n */\nexport function useReveal(text: string, streaming: boolean): string {\n const [calm] = useState(prefersReducedMotion);\n const stops = useMemo(() => wordStops(text), [text]);\n const words = stops.length - 1;\n\n const [landed, setLanded] = useState(() => (streaming && !calm ? Math.max(0, words - LEAD_IN) : words));\n\n // Winds the cursor back when a shorter reply replaces the one on screen, which\n // no key can express: same element, same message, new text.\n const shown = calm ? words : Math.min(words, Math.max(landed, words - MAX_LAG));\n if (shown !== landed) setLanded(shown);\n\n const chasing = shown < words;\n const goal = useRef(words);\n const cursor = useRef(shown);\n const flow = useRef<Flow>({ rate: 0, at: 0 });\n const pace = useRef(SLOWEST);\n\n useEffect(() => {\n measure(flow.current, performance.now(), Math.max(0, words - goal.current));\n goal.current = words;\n\n if (cursor.current < shown || cursor.current > words) cursor.current = shown;\n });\n\n useEffect(() => {\n if (!chasing) return;\n\n let drawn = performance.now();\n let frame = requestAnimationFrame(function step(now) {\n const elapsed = now - drawn;\n drawn = now;\n\n const arriving = measure(flow.current, now, 0);\n const from = cursor.current;\n const behind = goal.current - from;\n\n if (behind > 0) {\n const held = (arriving * BUFFER_MS) / 1000;\n const aim = Math.min(FASTEST, Math.max(SLOWEST, arriving + ((behind - held) * 1000) / CORRECT_MS));\n\n pace.current += (aim - pace.current) * Math.min(1, elapsed / EASE_MS);\n cursor.current = Math.min(goal.current, Math.floor(from) + 1, from + (elapsed * pace.current) / 1000);\n\n if (Math.floor(cursor.current) !== Math.floor(from)) setLanded(Math.floor(cursor.current));\n }\n\n frame = requestAnimationFrame(step);\n });\n\n return () => cancelAnimationFrame(frame);\n }, [chasing]);\n\n if (!chasing) return text;\n\n return text.slice(0, stops[shown]);\n}\n","import { memo, useMemo, useState } from 'react';\nimport type { MouseEvent, MouseEventHandler, ReactNode } from 'react';\nimport Markdown from 'react-markdown';\nimport type { Components, ExtraProps, Options } from 'react-markdown';\nimport remarkGfm from 'remark-gfm';\nimport remend from 'remend';\n\nimport { rehypeArriving } from './arriving';\nimport { splitBlocks } from './blocks';\nimport { useReveal } from './use-reveal';\nimport { CodeBlock } from '@/ds/components/CodeBlock';\nimport { cn } from '@/lib/utils';\n\nimport './markdown-renderer.css';\n\nexport type MarkdownExternalLinkTarget = 'tab' | 'window';\n\nexport interface MarkdownRendererProps {\n children: string;\n className?: string;\n externalLinkTarget?: MarkdownExternalLinkTarget;\n /** The text is still being written: reveal it at a steady pace, word by word. */\n streaming?: boolean;\n}\n\n/**\n * Renders a markdown string. Agent output can carry attacker-influenced text\n * (file contents, tool output, web pages): react-markdown escapes raw HTML and\n * drops dangerous link schemes, so nothing here reaches the DOM as markup.\n *\n * react-markdown re-parses on every render, and a streaming reply re-renders\n * its whole transcript on every delta. Memoizing spares the settled messages;\n * rendering block by block — streaming or not — spares every block of the live\n * one but the last, and lets a reply settle without remounting what is already\n * on screen.\n *\n * A streamed reply is paced here rather than by the caller, so the text a block\n * parses and the text a reader sees are one and the same string. Only what lands\n * after the reader joined plays an entrance: a reply opened part-written is\n * already there, and fading in what someone is halfway through reading would be\n * both a lie and a screenful of animations at once.\n */\nexport const MarkdownRenderer = memo(function MarkdownRenderer({\n children,\n className,\n externalLinkTarget = 'tab',\n streaming = false,\n}: MarkdownRendererProps) {\n const full = decodeEscapedNewlines(children);\n const shown = useReveal(full, streaming);\n const blocks = useMemo(() => splitBlocks(shown), [shown]);\n const last = blocks.length - 1;\n const components = externalLinkTarget === 'window' ? WINDOW_COMPONENTS : COMPONENTS;\n const growing = streaming || shown !== full;\n\n const [joined] = useState(() =>\n streaming ? { blocks: blocks.length, words: countWords(blocks[last] ?? '') } : undefined,\n );\n\n // What a block held when the reader joined, and so never animates. A block\n // already whole by then holds all of itself, which is what `undefined` says:\n // leave it as plain text, no spans at all. Position decides it, so it never\n // changes under a word — and counting the source counts its markers too, so\n // the boundary only ever errs towards leaving a word unanimated.\n const settledWords = (index: number): number | undefined => {\n if (!joined || index < joined.blocks - 1) return undefined;\n\n return index === joined.blocks - 1 ? joined.words : 0;\n };\n\n const tail = blocks[last] ?? '';\n const mended = useMemo(() => (growing ? remend(tail, REMEND_OPTIONS) : tail), [growing, tail]);\n\n return (\n <div className={cn('mastra-markdown', className)}>\n {blocks.map((block, index) => (\n <MarkdownBlock\n key={index}\n content={index === last ? mended : block}\n settledWords={settledWords(index)}\n components={components}\n />\n ))}\n </div>\n );\n});\n\n/** Keyed by position at the call site: a content key remounts on every character. */\nconst MarkdownBlock = memo(function MarkdownBlock({\n components,\n content,\n settledWords,\n}: {\n components: Components;\n content: string;\n settledWords?: number;\n}) {\n const rehypePlugins = useMemo(\n () => (settledWords === undefined ? SETTLED : [rehypeArriving(settledWords)]),\n [settledWords],\n );\n\n return (\n <Markdown remarkPlugins={REMARK_PLUGINS} rehypePlugins={rehypePlugins} components={components}>\n {content}\n </Markdown>\n );\n});\n\nconst SETTLED: Options['rehypePlugins'] = [];\n\nfunction countWords(text: string): number {\n return text.match(/\\S+/g)?.length ?? 0;\n}\n\n// Agent networks emit their text with literal `\\n`. Only unescape when the text\n// has no real newline, otherwise a `\"a\\nb\"` inside a code fence gets shredded.\nfunction decodeEscapedNewlines(text: string): string {\n return text.includes('\\n') ? text : text.replace(/\\\\n/g, '\\n');\n}\n\ntype MarkdownNode = NonNullable<ExtraProps['node']>;\n\nfunction languageOf(node: MarkdownNode): string | undefined {\n const classNames = node.properties.className;\n if (!Array.isArray(classNames)) return undefined;\n\n const language = classNames.find(entry => typeof entry === 'string' && entry.startsWith('language-'));\n return typeof language === 'string' ? language.slice('language-'.length) : undefined;\n}\n\nfunction fencedCode(node: MarkdownNode | undefined): { code: string; language?: string } | undefined {\n const child = node?.children.find(entry => entry.type === 'element' && entry.tagName === 'code');\n if (child?.type !== 'element') return undefined;\n\n const code = child.children.map(entry => (entry.type === 'text' ? entry.value : '')).join('');\n return { code: code.replace(/\\n$/, ''), language: languageOf(child) };\n}\n\nfunction MarkdownCodeBlock({\n node,\n children,\n className,\n}: {\n node?: MarkdownNode;\n children?: ReactNode;\n className?: string;\n}) {\n const fenced = fencedCode(node);\n if (!fenced) return <pre className={className}>{children}</pre>;\n\n return (\n <CodeBlock\n code={fenced.code}\n lang={fenced.language}\n overflow=\"scroll\"\n className={cn('my-3 bg-surface1', className)}\n copyMessage=\"Copied code to clipboard\"\n />\n );\n}\n\nconst POPUP_WINDOW_FEATURES = 'popup=yes,width=720,height=800,resizable=yes,scrollbars=yes';\n\nfunction isPlainLeftClick(event: MouseEvent): boolean {\n return event.button === 0 && !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey;\n}\n\nfunction markdownLink(externalLinkTarget: MarkdownExternalLinkTarget): NonNullable<Components['a']> {\n return function MarkdownLink({ href, title, children }) {\n const isExternal = /^https?:\\/\\//i.test(href ?? '');\n\n const openInPopup: MouseEventHandler<HTMLAnchorElement> = event => {\n if (!isExternal || externalLinkTarget !== 'window' || !isPlainLeftClick(event)) return;\n\n const popup = window.open(href, '_blank', POPUP_WINDOW_FEATURES);\n if (!popup) return;\n\n popup.opener = null;\n event.preventDefault();\n };\n\n return (\n <a\n href={href}\n title={title}\n onClick={openInPopup}\n target={isExternal ? '_blank' : undefined}\n rel={isExternal ? 'noopener noreferrer' : undefined}\n >\n {children}\n </a>\n );\n };\n}\n\nconst REMARK_PLUGINS = [remarkGfm];\n\n// Links stay text until their URL lands: remend's placeholder href would render\n// a live anchor to nowhere. No math is rendered here, so pairing `$$` would only\n// turn one literal into another.\nconst REMEND_OPTIONS = { katex: false, linkMode: 'text-only' } as const;\n\n// Elements are listed one by one: react-markdown also passes its `node`, which\n// React would forward to the DOM as a stray attribute. Everything else is\n// styled from markdown-renderer.css.\nconst COMPONENTS: Components = {\n pre: MarkdownCodeBlock,\n a: markdownLink('tab'),\n};\n\nconst WINDOW_COMPONENTS: Components = {\n ...COMPONENTS,\n a: markdownLink('window'),\n};\n"],"names":["MarkdownRenderer","MarkdownBlock"],"mappings":"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"}
|
|
@@ -179,34 +179,20 @@
|
|
|
179
179
|
border-radius: 8px;
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
/*
|
|
183
|
-
*
|
|
184
|
-
|
|
185
|
-
* filtered raster path antialiases unlike the settled text replacing it. */
|
|
186
|
-
.mastra-markdown-streaming .mastra-markdown-word {
|
|
187
|
-
animation: mastra-markdown-word-in 180ms cubic-bezier(0.2, 0, 0, 1) backwards;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/* The word still being typed. It holds its width so revealing it shifts
|
|
191
|
-
* nothing; gaining the class above is what starts its fade. */
|
|
192
|
-
.mastra-markdown-streaming .mastra-markdown-word-pending {
|
|
193
|
-
opacity: 0;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
@keyframes mastra-markdown-word-in {
|
|
182
|
+
/* A word landing, or a code block appearing whole with its background. Nothing
|
|
183
|
+
* but opacity, so a word stays inline and lines never re-break behind it. */
|
|
184
|
+
@keyframes mastra-markdown-arriving {
|
|
197
185
|
from {
|
|
198
186
|
opacity: 0;
|
|
199
|
-
filter: blur(3px);
|
|
200
187
|
}
|
|
188
|
+
}
|
|
201
189
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
filter: blur(0);
|
|
205
|
-
}
|
|
190
|
+
.mastra-markdown-arriving {
|
|
191
|
+
animation: mastra-markdown-arriving 800ms cubic-bezier(0.33, 1, 0.68, 1) backwards;
|
|
206
192
|
}
|
|
207
193
|
|
|
208
194
|
@media (prefers-reduced-motion: reduce) {
|
|
209
|
-
.mastra-markdown-
|
|
195
|
+
.mastra-markdown-arriving {
|
|
210
196
|
animation: none;
|
|
211
197
|
}
|
|
212
198
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { VariantProps } from 'class-variance-authority';
|
|
2
2
|
declare const navItemVariants: (props?: ({
|
|
3
|
-
size?: "default" | "sm" | null | undefined;
|
|
3
|
+
size?: "default" | "lg" | "sm" | null | undefined;
|
|
4
4
|
} & import('class-variance-authority/types').ClassProp) | undefined) => string;
|
|
5
5
|
type NavItemVariantProps = VariantProps<typeof navItemVariants>;
|
|
6
6
|
export type MainSidebarNavItemSize = NonNullable<NavItemVariantProps['size']>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ExtraProps } from 'react-markdown';
|
|
2
|
+
type MarkdownElement = NonNullable<ExtraProps['node']>;
|
|
3
|
+
type MarkdownChild = MarkdownElement['children'][number];
|
|
4
|
+
/**
|
|
5
|
+
* Gives every word of a block an element of its own, and the entrance class to
|
|
6
|
+
* those past the first `settled` — the words that were not on screen yet when
|
|
7
|
+
* the reader joined this reply.
|
|
8
|
+
*
|
|
9
|
+
* Every word, not just the arriving ones: react-markdown parses afresh on each
|
|
10
|
+
* landing and React matches the children it already mounted by position, so a
|
|
11
|
+
* wrapper that came and went would land where a bare text node used to be and
|
|
12
|
+
* remount half the paragraph, replaying entrances that had long finished.
|
|
13
|
+
* Wrapping the lot keeps every position fixed — a new word only ever appends —
|
|
14
|
+
* and a word never gains the class it did not mount with, so nothing replays.
|
|
15
|
+
*/
|
|
16
|
+
export declare function rehypeArriving(settled: number): () => (tree: {
|
|
17
|
+
children: MarkdownChild[];
|
|
18
|
+
}) => void;
|
|
19
|
+
export {};
|
|
@@ -3,7 +3,7 @@ export interface MarkdownRendererProps {
|
|
|
3
3
|
children: string;
|
|
4
4
|
className?: string;
|
|
5
5
|
externalLinkTarget?: MarkdownExternalLinkTarget;
|
|
6
|
-
/** The text is still being written:
|
|
6
|
+
/** The text is still being written: reveal it at a steady pace, word by word. */
|
|
7
7
|
streaming?: boolean;
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
@@ -16,5 +16,11 @@ export interface MarkdownRendererProps {
|
|
|
16
16
|
* rendering block by block — streaming or not — spares every block of the live
|
|
17
17
|
* one but the last, and lets a reply settle without remounting what is already
|
|
18
18
|
* on screen.
|
|
19
|
+
*
|
|
20
|
+
* A streamed reply is paced here rather than by the caller, so the text a block
|
|
21
|
+
* parses and the text a reader sees are one and the same string. Only what lands
|
|
22
|
+
* after the reader joined plays an entrance: a reply opened part-written is
|
|
23
|
+
* already there, and fading in what someone is halfway through reading would be
|
|
24
|
+
* both a lie and a screenful of animations at once.
|
|
19
25
|
*/
|
|
20
26
|
export declare const MarkdownRenderer: import('react').MemoExoticComponent<({ children, className, externalLinkTarget, streaming, }: MarkdownRendererProps) => import("react").JSX.Element>;
|
|
@@ -12,3 +12,5 @@ export declare const Blockquotes: Story;
|
|
|
12
12
|
export declare const Tables: Story;
|
|
13
13
|
export declare const ComplexDocument: Story;
|
|
14
14
|
export declare const HorizontalRule: Story;
|
|
15
|
+
/** The clumps the replay feeds in should not be readable in the cadence that comes out. */
|
|
16
|
+
export declare const Streaming: Story;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A jitter buffer for a streamed reply, counted in words. Chunks reach the
|
|
3
|
+
* browser unevenly — a proxy flushes, a tool call ends, the model changes pace —
|
|
4
|
+
* and drawing each one on arrival makes a reply lurch: ten words at once, then
|
|
5
|
+
* nothing for a fifth of a second.
|
|
6
|
+
*
|
|
7
|
+
* So the reveal runs on its own clock: a frame loop that chunks never interrupt,
|
|
8
|
+
* laying down at most one word per frame however far behind it is. It plays at
|
|
9
|
+
* the speed the reply is arriving at, measured directly, and leans on that only
|
|
10
|
+
* to work the buffer back to `BUFFER_MS` of held text when it drifts. Measuring
|
|
11
|
+
* the speed rather than inferring it from the buffer is what lets the buffer stay
|
|
12
|
+
* shallow: a reply that lasts two seconds still finds its pace in the first few
|
|
13
|
+
* hundred milliseconds, where a deeper buffer would spend the whole reply filling
|
|
14
|
+
* and then sprint to catch up.
|
|
15
|
+
*
|
|
16
|
+
* Nothing here reads `streaming` past the first render: a reply that stops
|
|
17
|
+
* arriving is one whose measured speed decays, which winds the buffer down on its
|
|
18
|
+
* own. Words, not characters, are what a reader sees land, and they are not the
|
|
19
|
+
* same length: an even flow of characters still drops "and if we do" in one frame
|
|
20
|
+
* and spends four on "implementation".
|
|
21
|
+
*/
|
|
22
|
+
export declare function useReveal(text: string, streaming: boolean): string;
|
|
@@ -4,16 +4,10 @@ import { CollapsibleTrigger } from '../../Collapsible';
|
|
|
4
4
|
export type TaskListItem = TaskItem;
|
|
5
5
|
export declare const TaskListContainer: ({ className, ...props }: ComponentProps<"section">) => import("react").JSX.Element;
|
|
6
6
|
export declare const TaskListHeader: ({ className, ...props }: ComponentProps<typeof CollapsibleTrigger>) => import("react").JSX.Element;
|
|
7
|
-
export interface
|
|
8
|
-
|
|
9
|
-
total: number;
|
|
10
|
-
}
|
|
11
|
-
export declare const TaskListCount: ({ completed, total, className, ...props }: TaskListCountProps) => import("react").JSX.Element;
|
|
12
|
-
export interface TaskListProgressProps extends Omit<ComponentProps<'div'>, 'children'> {
|
|
13
|
-
completed: number;
|
|
14
|
-
total: number;
|
|
7
|
+
export interface TaskListProgressProps extends Omit<ComponentProps<'span'>, 'children'> {
|
|
8
|
+
tasks: TaskListItem[];
|
|
15
9
|
}
|
|
16
|
-
export declare const TaskListProgress: ({
|
|
10
|
+
export declare const TaskListProgress: ({ tasks, className, ...props }: TaskListProgressProps) => import("react").JSX.Element;
|
|
17
11
|
export interface TaskListStatusIconProps extends ComponentProps<'span'> {
|
|
18
12
|
status: TaskListItem['status'];
|
|
19
13
|
}
|