@lobehub/ui 5.15.15 → 5.15.17
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/es/Markdown/Markdown.mjs +2 -1
- package/es/Markdown/Markdown.mjs.map +1 -1
- package/es/Markdown/SyntaxMarkdown/CachedMarkdown.mjs +82 -0
- package/es/Markdown/SyntaxMarkdown/CachedMarkdown.mjs.map +1 -0
- package/es/Markdown/SyntaxMarkdown/StreamdownRender.mjs +127 -97
- package/es/Markdown/SyntaxMarkdown/StreamdownRender.mjs.map +1 -1
- package/es/Markdown/SyntaxMarkdown/style.mjs +1 -2
- package/es/Markdown/SyntaxMarkdown/style.mjs.map +1 -1
- package/es/Markdown/SyntaxMarkdown/useSmoothStreamContent.mjs +32 -14
- package/es/Markdown/SyntaxMarkdown/useSmoothStreamContent.mjs.map +1 -1
- package/es/Markdown/SyntaxMarkdown/useStreamQueue.mjs +2 -5
- package/es/Markdown/SyntaxMarkdown/useStreamQueue.mjs.map +1 -1
- package/es/Markdown/components/MarkdownProvider.mjs +2 -1
- package/es/Markdown/components/MarkdownProvider.mjs.map +1 -1
- package/es/Markdown/index.d.mts +2 -2
- package/es/Markdown/plugins/rehypeStreamAnimated.d.mts +21 -0
- package/es/Markdown/plugins/rehypeStreamAnimated.mjs +61 -25
- package/es/Markdown/plugins/rehypeStreamAnimated.mjs.map +1 -1
- package/es/Markdown/type.d.mts +3 -1
- package/es/base-ui/Modal/style.mjs +4 -4
- package/es/base-ui/Modal/style.mjs.map +1 -1
- package/es/utils/getNow.mjs +8 -0
- package/es/utils/getNow.mjs.map +1 -0
- package/package.json +6 -1
package/es/Markdown/Markdown.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import { jsx } from "react/jsx-runtime";
|
|
|
12
12
|
import { cx } from "antd-style";
|
|
13
13
|
//#region src/Markdown/Markdown.tsx
|
|
14
14
|
const Markdown = memo((props) => {
|
|
15
|
-
const { ref, children = "", className, style, fullFeaturedCodeBlock, onDoubleClick, animated, enableHtmlPreview = false, enableLatex = true, enableMermaid = true, enableImageGallery, enableCustomFootnotes, enableGithubAlert, enableStream = true, componentProps, rehypePluginsAhead, allowHtml, borderRadius, fontSize = props.variant === "chat" ? 14 : void 0, headerMultiple = props.variant === "chat" ? .25 : void 0, marginMultiple = props.variant === "chat" ? 1 : void 0, variant = "default", reactMarkdownProps, lineHeight = props.variant === "chat" ? 1.6 : void 0, rehypePlugins, remarkPlugins, remarkPluginsAhead, components = {}, customRender, showFootnotes = true, streamSmoothingPreset, citations, ...rest } = props;
|
|
15
|
+
const { ref, children = "", className, style, fullFeaturedCodeBlock, onDoubleClick, animated, enableHtmlPreview = false, enableLatex = true, enableMermaid = true, enableImageGallery, enableCustomFootnotes, enableGithubAlert, enableStream = true, componentProps, rehypePluginsAhead, allowHtml, borderRadius, fontSize = props.variant === "chat" ? 14 : void 0, headerMultiple = props.variant === "chat" ? .25 : void 0, marginMultiple = props.variant === "chat" ? 1 : void 0, variant = "default", reactMarkdownProps, lineHeight = props.variant === "chat" ? 1.6 : void 0, rehypePlugins, remarkPlugins, remarkPluginsAhead, components = {}, customRender, showFootnotes = true, streamAnimationGranularity, streamSmoothingPreset, citations, ...rest } = props;
|
|
16
16
|
const delayedAnimated = useDelayedAnimated(animated);
|
|
17
17
|
const stableComponentProps = useStableValue(componentProps);
|
|
18
18
|
const stableComponents = useStableValue(components);
|
|
@@ -57,6 +57,7 @@ const Markdown = memo((props) => {
|
|
|
57
57
|
remarkPlugins,
|
|
58
58
|
remarkPluginsAhead,
|
|
59
59
|
showFootnotes,
|
|
60
|
+
streamAnimationGranularity,
|
|
60
61
|
streamSmoothingPreset,
|
|
61
62
|
variant,
|
|
62
63
|
children: /* @__PURE__ */ jsx(Render, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Markdown.mjs","names":["MarkdownRender"],"sources":["../../src/Markdown/Markdown.tsx"],"sourcesContent":["'use client';\n\nimport { cx } from 'antd-style';\nimport { memo, useCallback } from 'react';\n\nimport { useStableValue } from '@/hooks/useStableValue';\nimport { PreviewGroup } from '@/Image';\n\nimport { MarkdownProvider } from './components/MarkdownProvider';\nimport { useDelayedAnimated } from './components/useDelayedAnimated';\nimport { variants } from './style';\nimport { MarkdownRender, StreamdownRender } from './SyntaxMarkdown';\nimport { type MarkdownProps } from './type';\nimport Typography from './Typography';\n\nconst Markdown = memo<MarkdownProps>((props) => {\n const {\n ref,\n children = '',\n className,\n style,\n fullFeaturedCodeBlock,\n onDoubleClick,\n animated,\n enableHtmlPreview = false,\n enableLatex = true,\n enableMermaid = true,\n enableImageGallery,\n enableCustomFootnotes,\n enableGithubAlert,\n enableStream = true,\n componentProps,\n rehypePluginsAhead,\n allowHtml,\n borderRadius,\n fontSize = props.variant === 'chat' ? 14 : undefined,\n headerMultiple = props.variant === 'chat' ? 0.25 : undefined,\n marginMultiple = props.variant === 'chat' ? 1 : undefined,\n variant = 'default',\n reactMarkdownProps,\n lineHeight = props.variant === 'chat' ? 1.6 : undefined,\n rehypePlugins,\n remarkPlugins,\n remarkPluginsAhead,\n components = {},\n customRender,\n showFootnotes = true,\n streamSmoothingPreset,\n citations,\n ...rest\n } = props;\n\n const delayedAnimated = useDelayedAnimated(animated);\n\n // Stabilise structural props so an inline `componentProps={{ html:\n // {...} }}` from the caller doesn't cascade through MarkdownProvider →\n // useMarkdownComponents → react-markdown's `components` and remount the\n // entire code-block subtree (including the HtmlPreview iframe) on\n // every parent render. The deep-equal compare treats inline literals\n // as the same object as long as their structure matches. Inline\n // callbacks (`actionsRender`, custom `components`) still need\n // `useCallback` at the call site — function bodies aren't compared.\n const stableComponentProps = useStableValue(componentProps);\n const stableComponents = useStableValue(components);\n\n const Render = useCallback(\n ({\n enableStream,\n children,\n reactMarkdownProps,\n }: Pick<MarkdownProps, 'children' | 'enableStream' | 'reactMarkdownProps'>) => {\n const DefaultRender = enableStream ? StreamdownRender : MarkdownRender;\n const defaultDOM = <DefaultRender {...reactMarkdownProps}>{children}</DefaultRender>;\n return customRender ? customRender(defaultDOM, { text: children }) : defaultDOM;\n },\n [customRender],\n );\n\n return (\n <PreviewGroup enable={enableImageGallery}>\n <Typography\n borderRadius={borderRadius}\n className={cx(variants({ enableLatex, variant }), className)}\n data-code-type=\"markdown\"\n fontSize={fontSize}\n headerMultiple={headerMultiple}\n lineHeight={lineHeight}\n marginMultiple={marginMultiple}\n ref={ref}\n style={style}\n onDoubleClick={onDoubleClick}\n {...rest}\n >\n <MarkdownProvider\n allowHtml={allowHtml}\n animated={delayedAnimated}\n citations={citations}\n componentProps={stableComponentProps}\n components={stableComponents}\n enableCustomFootnotes={enableCustomFootnotes}\n enableGithubAlert={enableGithubAlert}\n enableHtmlPreview={enableHtmlPreview}\n enableLatex={enableLatex}\n enableMermaid={enableMermaid}\n fullFeaturedCodeBlock={fullFeaturedCodeBlock}\n rehypePlugins={rehypePlugins}\n rehypePluginsAhead={rehypePluginsAhead}\n remarkPlugins={remarkPlugins}\n remarkPluginsAhead={remarkPluginsAhead}\n showFootnotes={showFootnotes}\n streamSmoothingPreset={streamSmoothingPreset}\n variant={variant}\n >\n <Render\n enableStream={enableStream && delayedAnimated}\n reactMarkdownProps={reactMarkdownProps}\n >\n {children}\n </Render>\n </MarkdownProvider>\n </Typography>\n </PreviewGroup>\n );\n});\n\nMarkdown.displayName = 'Markdown';\n\nexport default Markdown;\n"],"mappings":";;;;;;;;;;;;;AAeA,MAAM,WAAW,MAAqB,UAAU;CAC9C,MAAM,EACJ,KACA,WAAW,IACX,WACA,OACA,uBACA,eACA,UACA,oBAAoB,OACpB,cAAc,MACd,gBAAgB,MAChB,oBACA,uBACA,mBACA,eAAe,MACf,gBACA,oBACA,WACA,cACA,WAAW,MAAM,YAAY,SAAS,KAAK,KAAA,GAC3C,iBAAiB,MAAM,YAAY,SAAS,MAAO,KAAA,GACnD,iBAAiB,MAAM,YAAY,SAAS,IAAI,KAAA,GAChD,UAAU,WACV,oBACA,aAAa,MAAM,YAAY,SAAS,MAAM,KAAA,GAC9C,eACA,eACA,oBACA,aAAa,EAAE,EACf,cACA,gBAAgB,MAChB,uBACA,WACA,GAAG,SACD;CAEJ,MAAM,kBAAkB,mBAAmB,SAAS;CAUpD,MAAM,uBAAuB,eAAe,eAAe;CAC3D,MAAM,mBAAmB,eAAe,WAAW;CAEnD,MAAM,SAAS,aACZ,EACC,cACA,UACA,yBAC6E;EAE7E,MAAM,aAAa,oBADG,eAAe,mBAAmBA,kBACrC;GAAe,GAAI;GAAqB;GAAyB,CAAA;AACpF,SAAO,eAAe,aAAa,YAAY,EAAE,MAAM,UAAU,CAAC,GAAG;IAEvE,CAAC,aAAa,CACf;AAED,QACE,oBAAC,cAAD;EAAc,QAAQ;YACpB,oBAAC,YAAD;GACgB;GACd,WAAW,GAAG,SAAS;IAAE;IAAa;IAAS,CAAC,EAAE,UAAU;GAC5D,kBAAe;GACL;GACM;GACJ;GACI;GACX;GACE;GACQ;GACf,GAAI;aAEJ,oBAAC,kBAAD;IACa;IACX,UAAU;IACC;IACX,gBAAgB;IAChB,YAAY;IACW;IACJ;IACA;IACN;IACE;IACQ;IACR;IACK;IACL;IACK;IACL;
|
|
1
|
+
{"version":3,"file":"Markdown.mjs","names":["MarkdownRender"],"sources":["../../src/Markdown/Markdown.tsx"],"sourcesContent":["'use client';\n\nimport { cx } from 'antd-style';\nimport { memo, useCallback } from 'react';\n\nimport { useStableValue } from '@/hooks/useStableValue';\nimport { PreviewGroup } from '@/Image';\n\nimport { MarkdownProvider } from './components/MarkdownProvider';\nimport { useDelayedAnimated } from './components/useDelayedAnimated';\nimport { variants } from './style';\nimport { MarkdownRender, StreamdownRender } from './SyntaxMarkdown';\nimport { type MarkdownProps } from './type';\nimport Typography from './Typography';\n\nconst Markdown = memo<MarkdownProps>((props) => {\n const {\n ref,\n children = '',\n className,\n style,\n fullFeaturedCodeBlock,\n onDoubleClick,\n animated,\n enableHtmlPreview = false,\n enableLatex = true,\n enableMermaid = true,\n enableImageGallery,\n enableCustomFootnotes,\n enableGithubAlert,\n enableStream = true,\n componentProps,\n rehypePluginsAhead,\n allowHtml,\n borderRadius,\n fontSize = props.variant === 'chat' ? 14 : undefined,\n headerMultiple = props.variant === 'chat' ? 0.25 : undefined,\n marginMultiple = props.variant === 'chat' ? 1 : undefined,\n variant = 'default',\n reactMarkdownProps,\n lineHeight = props.variant === 'chat' ? 1.6 : undefined,\n rehypePlugins,\n remarkPlugins,\n remarkPluginsAhead,\n components = {},\n customRender,\n showFootnotes = true,\n streamAnimationGranularity,\n streamSmoothingPreset,\n citations,\n ...rest\n } = props;\n\n const delayedAnimated = useDelayedAnimated(animated);\n\n // Stabilise structural props so an inline `componentProps={{ html:\n // {...} }}` from the caller doesn't cascade through MarkdownProvider →\n // useMarkdownComponents → react-markdown's `components` and remount the\n // entire code-block subtree (including the HtmlPreview iframe) on\n // every parent render. The deep-equal compare treats inline literals\n // as the same object as long as their structure matches. Inline\n // callbacks (`actionsRender`, custom `components`) still need\n // `useCallback` at the call site — function bodies aren't compared.\n const stableComponentProps = useStableValue(componentProps);\n const stableComponents = useStableValue(components);\n\n const Render = useCallback(\n ({\n enableStream,\n children,\n reactMarkdownProps,\n }: Pick<MarkdownProps, 'children' | 'enableStream' | 'reactMarkdownProps'>) => {\n const DefaultRender = enableStream ? StreamdownRender : MarkdownRender;\n const defaultDOM = <DefaultRender {...reactMarkdownProps}>{children}</DefaultRender>;\n return customRender ? customRender(defaultDOM, { text: children }) : defaultDOM;\n },\n [customRender],\n );\n\n return (\n <PreviewGroup enable={enableImageGallery}>\n <Typography\n borderRadius={borderRadius}\n className={cx(variants({ enableLatex, variant }), className)}\n data-code-type=\"markdown\"\n fontSize={fontSize}\n headerMultiple={headerMultiple}\n lineHeight={lineHeight}\n marginMultiple={marginMultiple}\n ref={ref}\n style={style}\n onDoubleClick={onDoubleClick}\n {...rest}\n >\n <MarkdownProvider\n allowHtml={allowHtml}\n animated={delayedAnimated}\n citations={citations}\n componentProps={stableComponentProps}\n components={stableComponents}\n enableCustomFootnotes={enableCustomFootnotes}\n enableGithubAlert={enableGithubAlert}\n enableHtmlPreview={enableHtmlPreview}\n enableLatex={enableLatex}\n enableMermaid={enableMermaid}\n fullFeaturedCodeBlock={fullFeaturedCodeBlock}\n rehypePlugins={rehypePlugins}\n rehypePluginsAhead={rehypePluginsAhead}\n remarkPlugins={remarkPlugins}\n remarkPluginsAhead={remarkPluginsAhead}\n showFootnotes={showFootnotes}\n streamAnimationGranularity={streamAnimationGranularity}\n streamSmoothingPreset={streamSmoothingPreset}\n variant={variant}\n >\n <Render\n enableStream={enableStream && delayedAnimated}\n reactMarkdownProps={reactMarkdownProps}\n >\n {children}\n </Render>\n </MarkdownProvider>\n </Typography>\n </PreviewGroup>\n );\n});\n\nMarkdown.displayName = 'Markdown';\n\nexport default Markdown;\n"],"mappings":";;;;;;;;;;;;;AAeA,MAAM,WAAW,MAAqB,UAAU;CAC9C,MAAM,EACJ,KACA,WAAW,IACX,WACA,OACA,uBACA,eACA,UACA,oBAAoB,OACpB,cAAc,MACd,gBAAgB,MAChB,oBACA,uBACA,mBACA,eAAe,MACf,gBACA,oBACA,WACA,cACA,WAAW,MAAM,YAAY,SAAS,KAAK,KAAA,GAC3C,iBAAiB,MAAM,YAAY,SAAS,MAAO,KAAA,GACnD,iBAAiB,MAAM,YAAY,SAAS,IAAI,KAAA,GAChD,UAAU,WACV,oBACA,aAAa,MAAM,YAAY,SAAS,MAAM,KAAA,GAC9C,eACA,eACA,oBACA,aAAa,EAAE,EACf,cACA,gBAAgB,MAChB,4BACA,uBACA,WACA,GAAG,SACD;CAEJ,MAAM,kBAAkB,mBAAmB,SAAS;CAUpD,MAAM,uBAAuB,eAAe,eAAe;CAC3D,MAAM,mBAAmB,eAAe,WAAW;CAEnD,MAAM,SAAS,aACZ,EACC,cACA,UACA,yBAC6E;EAE7E,MAAM,aAAa,oBADG,eAAe,mBAAmBA,kBACrC;GAAe,GAAI;GAAqB;GAAyB,CAAA;AACpF,SAAO,eAAe,aAAa,YAAY,EAAE,MAAM,UAAU,CAAC,GAAG;IAEvE,CAAC,aAAa,CACf;AAED,QACE,oBAAC,cAAD;EAAc,QAAQ;YACpB,oBAAC,YAAD;GACgB;GACd,WAAW,GAAG,SAAS;IAAE;IAAa;IAAS,CAAC,EAAE,UAAU;GAC5D,kBAAe;GACL;GACM;GACJ;GACI;GACX;GACE;GACQ;GACf,GAAI;aAEJ,oBAAC,kBAAD;IACa;IACX,UAAU;IACC;IACX,gBAAgB;IAChB,YAAY;IACW;IACJ;IACA;IACN;IACE;IACQ;IACR;IACK;IACL;IACK;IACL;IACa;IACL;IACd;cAET,oBAAC,QAAD;KACE,cAAc,gBAAgB;KACV;KAEnB;KACM,CAAA;IACQ,CAAA;GACR,CAAA;EACA,CAAA;EAEjB;AAEF,SAAS,cAAc"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { visit } from "../../node_modules/unist-util-visit/lib/index.mjs";
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
import { defaultUrlTransform } from "react-markdown";
|
|
6
|
+
import { toJsxRuntime } from "hast-util-to-jsx-runtime";
|
|
7
|
+
import { urlAttributes } from "html-url-attributes";
|
|
8
|
+
import remarkParse from "remark-parse";
|
|
9
|
+
import remarkRehype from "remark-rehype";
|
|
10
|
+
import { unified } from "unified";
|
|
11
|
+
import { VFile } from "vfile";
|
|
12
|
+
//#region src/Markdown/SyntaxMarkdown/CachedMarkdown.tsx
|
|
13
|
+
const EMPTY_PLUGINS = [];
|
|
14
|
+
const DEFAULT_REMARK_REHYPE_OPTIONS = { allowDangerousHtml: true };
|
|
15
|
+
/**
|
|
16
|
+
* Render-equivalent of react-markdown's synchronous `<Markdown>` that keeps
|
|
17
|
+
* the unified processor across renders. react-markdown rebuilds the whole
|
|
18
|
+
* plugin chain on every render; the streaming tail block re-renders on every
|
|
19
|
+
* reveal commit (~20/s) with identical plugin identities, so caching the
|
|
20
|
+
* processor removes the per-commit chain construction. `post`/`transform`
|
|
21
|
+
* mirror react-markdown@10 — keep them in sync when upgrading it.
|
|
22
|
+
*/
|
|
23
|
+
const CachedMarkdown = (options) => {
|
|
24
|
+
const { children, rehypePlugins, remarkPlugins, remarkRehypeOptions } = options;
|
|
25
|
+
const processor = useMemo(() => {
|
|
26
|
+
return unified().use(remarkParse).use(remarkPlugins || EMPTY_PLUGINS).use(remarkRehype, remarkRehypeOptions ? {
|
|
27
|
+
...remarkRehypeOptions,
|
|
28
|
+
...DEFAULT_REMARK_REHYPE_OPTIONS
|
|
29
|
+
} : DEFAULT_REMARK_REHYPE_OPTIONS).use(rehypePlugins || EMPTY_PLUGINS);
|
|
30
|
+
}, [
|
|
31
|
+
rehypePlugins,
|
|
32
|
+
remarkPlugins,
|
|
33
|
+
remarkRehypeOptions
|
|
34
|
+
]);
|
|
35
|
+
const file = new VFile();
|
|
36
|
+
file.value = typeof children === "string" ? children : "";
|
|
37
|
+
return post(processor.runSync(processor.parse(file), file), options);
|
|
38
|
+
};
|
|
39
|
+
function post(tree, options) {
|
|
40
|
+
const { allowedElements, allowElement, components, disallowedElements, skipHtml, unwrapDisallowed } = options;
|
|
41
|
+
const urlTransform = options.urlTransform || defaultUrlTransform;
|
|
42
|
+
if (allowedElements && disallowedElements) throw new Error("Unexpected combined `allowedElements` and `disallowedElements`, expected one or the other");
|
|
43
|
+
const transform = (node, index, parent) => {
|
|
44
|
+
if (node.type === "raw" && parent && typeof index === "number") {
|
|
45
|
+
if (skipHtml) parent.children.splice(index, 1);
|
|
46
|
+
else parent.children[index] = {
|
|
47
|
+
type: "text",
|
|
48
|
+
value: node.value
|
|
49
|
+
};
|
|
50
|
+
return index;
|
|
51
|
+
}
|
|
52
|
+
if (node.type === "element") {
|
|
53
|
+
let key;
|
|
54
|
+
for (key in urlAttributes) if (Object.hasOwn(urlAttributes, key) && Object.hasOwn(node.properties, key)) {
|
|
55
|
+
const value = node.properties[key];
|
|
56
|
+
const test = urlAttributes[key];
|
|
57
|
+
if (test === null || test.includes(node.tagName)) node.properties[key] = urlTransform(String(value || ""), key, node);
|
|
58
|
+
}
|
|
59
|
+
let remove = allowedElements ? !allowedElements.includes(node.tagName) : disallowedElements ? disallowedElements.includes(node.tagName) : false;
|
|
60
|
+
if (!remove && allowElement && typeof index === "number") remove = !allowElement(node, index, parent);
|
|
61
|
+
if (remove && parent && typeof index === "number") {
|
|
62
|
+
if (unwrapDisallowed && node.children) parent.children.splice(index, 1, ...node.children);
|
|
63
|
+
else parent.children.splice(index, 1);
|
|
64
|
+
return index;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
visit(tree, transform);
|
|
69
|
+
return toJsxRuntime(tree, {
|
|
70
|
+
Fragment: Fragment$1,
|
|
71
|
+
components,
|
|
72
|
+
ignoreInvalidStyle: true,
|
|
73
|
+
jsx,
|
|
74
|
+
jsxs,
|
|
75
|
+
passKeys: true,
|
|
76
|
+
passNode: true
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
//#endregion
|
|
80
|
+
export { CachedMarkdown };
|
|
81
|
+
|
|
82
|
+
//# sourceMappingURL=CachedMarkdown.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CachedMarkdown.mjs","names":[],"sources":["../../../src/Markdown/SyntaxMarkdown/CachedMarkdown.tsx"],"sourcesContent":["'use client';\n\nimport { type Nodes, type Root } from 'hast';\nimport { toJsxRuntime } from 'hast-util-to-jsx-runtime';\nimport { urlAttributes } from 'html-url-attributes';\nimport { useMemo } from 'react';\nimport { Fragment, jsx, jsxs } from 'react/jsx-runtime';\nimport { defaultUrlTransform, type Options } from 'react-markdown';\nimport remarkParse from 'remark-parse';\nimport remarkRehype from 'remark-rehype';\nimport { type PluggableList, unified } from 'unified';\nimport { type BuildVisitor, visit } from 'unist-util-visit';\nimport { VFile } from 'vfile';\n\nconst EMPTY_PLUGINS: PluggableList = [];\nconst DEFAULT_REMARK_REHYPE_OPTIONS = { allowDangerousHtml: true };\n\n/**\n * Render-equivalent of react-markdown's synchronous `<Markdown>` that keeps\n * the unified processor across renders. react-markdown rebuilds the whole\n * plugin chain on every render; the streaming tail block re-renders on every\n * reveal commit (~20/s) with identical plugin identities, so caching the\n * processor removes the per-commit chain construction. `post`/`transform`\n * mirror react-markdown@10 — keep them in sync when upgrading it.\n */\nexport const CachedMarkdown = (options: Options) => {\n const { children, rehypePlugins, remarkPlugins, remarkRehypeOptions } = options;\n\n const processor = useMemo(() => {\n return unified()\n .use(remarkParse)\n .use(remarkPlugins || EMPTY_PLUGINS)\n .use(\n remarkRehype,\n remarkRehypeOptions\n ? { ...remarkRehypeOptions, ...DEFAULT_REMARK_REHYPE_OPTIONS }\n : DEFAULT_REMARK_REHYPE_OPTIONS,\n )\n .use(rehypePlugins || EMPTY_PLUGINS);\n }, [rehypePlugins, remarkPlugins, remarkRehypeOptions]);\n\n const file = new VFile();\n file.value = typeof children === 'string' ? children : '';\n\n return post(processor.runSync(processor.parse(file), file) as Nodes, options);\n};\n\nfunction post(tree: Nodes, options: Options) {\n const {\n allowedElements,\n allowElement,\n components,\n disallowedElements,\n skipHtml,\n unwrapDisallowed,\n } = options;\n const urlTransform = options.urlTransform || defaultUrlTransform;\n\n if (allowedElements && disallowedElements) {\n throw new Error(\n 'Unexpected combined `allowedElements` and `disallowedElements`, expected one or the other',\n );\n }\n\n const transform: BuildVisitor<Root> = (node, index, parent) => {\n if (node.type === 'raw' && parent && typeof index === 'number') {\n if (skipHtml) {\n parent.children.splice(index, 1);\n } else {\n parent.children[index] = { type: 'text', value: node.value };\n }\n\n return index;\n }\n\n if (node.type === 'element') {\n let key: string;\n\n for (key in urlAttributes) {\n if (Object.hasOwn(urlAttributes, key) && Object.hasOwn(node.properties, key)) {\n const value = node.properties[key];\n const test = urlAttributes[key];\n if (test === null || test.includes(node.tagName)) {\n node.properties[key] = urlTransform(String(value || ''), key, node);\n }\n }\n }\n\n let remove = allowedElements\n ? !allowedElements.includes(node.tagName)\n : disallowedElements\n ? disallowedElements.includes(node.tagName)\n : false;\n\n if (!remove && allowElement && typeof index === 'number') {\n remove = !allowElement(node, index, parent);\n }\n\n if (remove && parent && typeof index === 'number') {\n if (unwrapDisallowed && node.children) {\n parent.children.splice(index, 1, ...node.children);\n } else {\n parent.children.splice(index, 1);\n }\n\n return index;\n }\n }\n };\n\n visit(tree as Root, transform);\n\n return toJsxRuntime(tree, {\n Fragment,\n components,\n ignoreInvalidStyle: true,\n jsx,\n jsxs,\n passKeys: true,\n passNode: true,\n });\n}\n"],"mappings":";;;;;;;;;;;;AAcA,MAAM,gBAA+B,EAAE;AACvC,MAAM,gCAAgC,EAAE,oBAAoB,MAAM;;;;;;;;;AAUlE,MAAa,kBAAkB,YAAqB;CAClD,MAAM,EAAE,UAAU,eAAe,eAAe,wBAAwB;CAExE,MAAM,YAAY,cAAc;AAC9B,SAAO,SAAS,CACb,IAAI,YAAY,CAChB,IAAI,iBAAiB,cAAc,CACnC,IACC,cACA,sBACI;GAAE,GAAG;GAAqB,GAAG;GAA+B,GAC5D,8BACL,CACA,IAAI,iBAAiB,cAAc;IACrC;EAAC;EAAe;EAAe;EAAoB,CAAC;CAEvD,MAAM,OAAO,IAAI,OAAO;AACxB,MAAK,QAAQ,OAAO,aAAa,WAAW,WAAW;AAEvD,QAAO,KAAK,UAAU,QAAQ,UAAU,MAAM,KAAK,EAAE,KAAK,EAAW,QAAQ;;AAG/E,SAAS,KAAK,MAAa,SAAkB;CAC3C,MAAM,EACJ,iBACA,cACA,YACA,oBACA,UACA,qBACE;CACJ,MAAM,eAAe,QAAQ,gBAAgB;AAE7C,KAAI,mBAAmB,mBACrB,OAAM,IAAI,MACR,4FACD;CAGH,MAAM,aAAiC,MAAM,OAAO,WAAW;AAC7D,MAAI,KAAK,SAAS,SAAS,UAAU,OAAO,UAAU,UAAU;AAC9D,OAAI,SACF,QAAO,SAAS,OAAO,OAAO,EAAE;OAEhC,QAAO,SAAS,SAAS;IAAE,MAAM;IAAQ,OAAO,KAAK;IAAO;AAG9D,UAAO;;AAGT,MAAI,KAAK,SAAS,WAAW;GAC3B,IAAI;AAEJ,QAAK,OAAO,cACV,KAAI,OAAO,OAAO,eAAe,IAAI,IAAI,OAAO,OAAO,KAAK,YAAY,IAAI,EAAE;IAC5E,MAAM,QAAQ,KAAK,WAAW;IAC9B,MAAM,OAAO,cAAc;AAC3B,QAAI,SAAS,QAAQ,KAAK,SAAS,KAAK,QAAQ,CAC9C,MAAK,WAAW,OAAO,aAAa,OAAO,SAAS,GAAG,EAAE,KAAK,KAAK;;GAKzE,IAAI,SAAS,kBACT,CAAC,gBAAgB,SAAS,KAAK,QAAQ,GACvC,qBACE,mBAAmB,SAAS,KAAK,QAAQ,GACzC;AAEN,OAAI,CAAC,UAAU,gBAAgB,OAAO,UAAU,SAC9C,UAAS,CAAC,aAAa,MAAM,OAAO,OAAO;AAG7C,OAAI,UAAU,UAAU,OAAO,UAAU,UAAU;AACjD,QAAI,oBAAoB,KAAK,SAC3B,QAAO,SAAS,OAAO,OAAO,GAAG,GAAG,KAAK,SAAS;QAElD,QAAO,SAAS,OAAO,OAAO,EAAE;AAGlC,WAAO;;;;AAKb,OAAM,MAAc,UAAU;AAE9B,QAAO,aAAa,MAAM;EACxB,UAAA;EACA;EACA,oBAAoB;EACpB;EACA;EACA,UAAU;EACV,UAAU;EACX,CAAC"}
|
|
@@ -1,30 +1,24 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { isDeepEqual } from "../../utils/isDeepEqual.mjs";
|
|
3
|
+
import { useStableValue } from "../../hooks/useStableValue.mjs";
|
|
3
4
|
import { useMarkdownContext } from "../components/MarkdownProvider.mjs";
|
|
4
5
|
import { useMarkdownComponents } from "../../hooks/useMarkdown/useMarkdownComponents.mjs";
|
|
5
6
|
import { useMarkdownContent } from "../../hooks/useMarkdown/useMarkdownContent.mjs";
|
|
6
7
|
import { useMarkdownRehypePlugins } from "../../hooks/useMarkdown/useMarkdownRehypePlugins.mjs";
|
|
7
8
|
import { useMarkdownRemarkPlugins } from "../../hooks/useMarkdown/useMarkdownRemarkPlugins.mjs";
|
|
9
|
+
import { getNow } from "../../utils/getNow.mjs";
|
|
8
10
|
import { rehypeStreamAnimated } from "../plugins/rehypeStreamAnimated.mjs";
|
|
9
11
|
import { useStreamdownProfiler } from "../streamProfiler/StreamdownProfilerProvider.mjs";
|
|
12
|
+
import { CachedMarkdown } from "./CachedMarkdown.mjs";
|
|
10
13
|
import { resolveBlockAnimationMeta } from "./streamAnimationMeta.mjs";
|
|
11
14
|
import { styles } from "./style.mjs";
|
|
12
|
-
import { useSmoothStreamContent } from "./useSmoothStreamContent.mjs";
|
|
15
|
+
import { countChars, useSmoothStreamContent } from "./useSmoothStreamContent.mjs";
|
|
13
16
|
import { useStreamQueue } from "./useStreamQueue.mjs";
|
|
14
17
|
import { Profiler, createElement, memo, useCallback, useEffect, useId, useMemo, useRef } from "react";
|
|
15
18
|
import { jsx } from "react/jsx-runtime";
|
|
16
|
-
import Markdown from "react-markdown";
|
|
17
19
|
import { marked } from "marked";
|
|
18
20
|
import remend from "remend";
|
|
19
21
|
//#region src/Markdown/SyntaxMarkdown/StreamdownRender.tsx
|
|
20
|
-
const STREAM_FADE_DURATION = 280;
|
|
21
|
-
const REVEALED_STREAM_PLUGIN = [rehypeStreamAnimated, { revealed: true }];
|
|
22
|
-
function countChars(text) {
|
|
23
|
-
return [...text].length;
|
|
24
|
-
}
|
|
25
|
-
function getNow() {
|
|
26
|
-
return typeof performance === "undefined" ? Date.now() : performance.now();
|
|
27
|
-
}
|
|
28
22
|
const isSamePlugin = (prevPlugin, nextPlugin) => {
|
|
29
23
|
const prevTuple = Array.isArray(prevPlugin) ? prevPlugin : [prevPlugin];
|
|
30
24
|
const nextTuple = Array.isArray(nextPlugin) ? nextPlugin : [nextPlugin];
|
|
@@ -45,21 +39,90 @@ const useStablePlugins = (plugins) => {
|
|
|
45
39
|
return stableRef.current;
|
|
46
40
|
};
|
|
47
41
|
const StreamdownBlock = memo(({ children, ...rest }) => {
|
|
48
|
-
return /* @__PURE__ */ jsx(
|
|
42
|
+
return /* @__PURE__ */ jsx(CachedMarkdown, {
|
|
49
43
|
...rest,
|
|
50
44
|
children
|
|
51
45
|
});
|
|
52
46
|
}, (prevProps, nextProps) => prevProps.children === nextProps.children && prevProps.components === nextProps.components && isSamePlugins(prevProps.rehypePlugins, nextProps.rehypePlugins) && isSamePlugins(prevProps.remarkPlugins, nextProps.remarkPlugins));
|
|
53
47
|
StreamdownBlock.displayName = "StreamdownBlock";
|
|
54
|
-
const
|
|
55
|
-
|
|
48
|
+
const MIN_STREAM_CHAR_PACE_MS = 2;
|
|
49
|
+
const MAX_REVEAL_GAP_MS = 160;
|
|
50
|
+
const updateBlockAnimation = ({ blocks, charDelay, getBlockState, pluginsCache, renderNow, revealClock, runtimes }) => {
|
|
51
|
+
const blockAnimationMeta = /* @__PURE__ */ new Map();
|
|
52
|
+
const alive = /* @__PURE__ */ new Set();
|
|
53
|
+
let revealedNewChars = false;
|
|
54
|
+
for (const [index, block] of blocks.entries()) {
|
|
55
|
+
alive.add(block.startOffset);
|
|
56
|
+
const state = getBlockState(index);
|
|
57
|
+
if (state === "queued") continue;
|
|
58
|
+
let runtime = runtimes.get(block.startOffset);
|
|
59
|
+
if (!runtime) {
|
|
60
|
+
runtime = {
|
|
61
|
+
births: [],
|
|
62
|
+
charCount: 0,
|
|
63
|
+
rawLength: -1,
|
|
64
|
+
settled: false,
|
|
65
|
+
styles: []
|
|
66
|
+
};
|
|
67
|
+
runtimes.set(block.startOffset, runtime);
|
|
68
|
+
}
|
|
69
|
+
if (runtime.rawLength !== block.content.length) {
|
|
70
|
+
runtime.charCount = countChars(block.content);
|
|
71
|
+
runtime.rawLength = block.content.length;
|
|
72
|
+
}
|
|
73
|
+
const blockCharCount = runtime.charCount;
|
|
74
|
+
const births = runtime.births;
|
|
75
|
+
if (births.length > blockCharCount) {
|
|
76
|
+
births.length = blockCharCount;
|
|
77
|
+
runtime.styles.length = blockCharCount;
|
|
78
|
+
}
|
|
79
|
+
if (births.length < blockCharCount) {
|
|
80
|
+
const newChars = blockCharCount - births.length;
|
|
81
|
+
let pace = charDelay;
|
|
82
|
+
let cap = renderNow + 180;
|
|
83
|
+
if (state === "streaming") {
|
|
84
|
+
revealedNewChars = true;
|
|
85
|
+
const gapMs = Math.min(Math.max(renderNow - revealClock.lastTs, 16), MAX_REVEAL_GAP_MS);
|
|
86
|
+
pace = Math.min(charDelay, Math.max(gapMs / newChars, MIN_STREAM_CHAR_PACE_MS));
|
|
87
|
+
cap = renderNow + gapMs + 180;
|
|
88
|
+
}
|
|
89
|
+
for (let i = births.length; i < blockCharCount; i++) {
|
|
90
|
+
const chained = (i > 0 ? births[i - 1] : renderNow - pace) + pace;
|
|
91
|
+
births.push(Math.min(cap, Math.max(chained, renderNow)));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
let meta;
|
|
95
|
+
if (runtime.settled) meta = {
|
|
96
|
+
charDelay: runtime.charDelay ?? charDelay,
|
|
97
|
+
settled: true
|
|
98
|
+
};
|
|
99
|
+
else {
|
|
100
|
+
meta = resolveBlockAnimationMeta({
|
|
101
|
+
currentCharDelay: charDelay,
|
|
102
|
+
fadeDuration: 180,
|
|
103
|
+
lastElapsedMs: renderNow - (births.length > 0 ? births.at(-1) ?? renderNow : renderNow),
|
|
104
|
+
previousCharDelay: runtime.charDelay,
|
|
105
|
+
state
|
|
106
|
+
});
|
|
107
|
+
runtime.settled = meta.settled;
|
|
108
|
+
}
|
|
109
|
+
runtime.charDelay = meta.charDelay;
|
|
110
|
+
blockAnimationMeta.set(block.startOffset, meta);
|
|
111
|
+
}
|
|
112
|
+
if (revealedNewChars) revealClock.lastTs = renderNow;
|
|
113
|
+
for (const key of runtimes.keys()) if (!alive.has(key)) {
|
|
114
|
+
runtimes.delete(key);
|
|
115
|
+
pluginsCache.delete(key);
|
|
116
|
+
}
|
|
117
|
+
return blockAnimationMeta;
|
|
118
|
+
};
|
|
119
|
+
const StreamdownBlocks = memo(({ content: smoothedContent, markdownOptions: rest }) => {
|
|
120
|
+
const { streamAnimationGranularity = "char" } = useMarkdownContext();
|
|
56
121
|
const profiler = useStreamdownProfiler();
|
|
57
|
-
const escapedContent = useMarkdownContent(children || "");
|
|
58
122
|
const components = useMarkdownComponents();
|
|
59
123
|
const baseRehypePlugins = useStablePlugins(useMarkdownRehypePlugins());
|
|
60
124
|
const remarkPlugins = useStablePlugins(useMarkdownRemarkPlugins());
|
|
61
125
|
const generatedId = useId();
|
|
62
|
-
const smoothedContent = useSmoothStreamContent(typeof escapedContent === "string" ? escapedContent : "", { preset: streamSmoothingPreset });
|
|
63
126
|
const processedContentResult = useMemo(() => {
|
|
64
127
|
const start = profiler ? getNow() : 0;
|
|
65
128
|
const value = remend(smoothedContent);
|
|
@@ -88,43 +151,21 @@ const StreamdownRender = memo(({ children, ...rest }) => {
|
|
|
88
151
|
}, [processedContent, profiler]);
|
|
89
152
|
const blocks = blocksResult.value;
|
|
90
153
|
const { getBlockState, charDelay } = useStreamQueue(blocks);
|
|
91
|
-
const
|
|
92
|
-
const
|
|
154
|
+
const blockRuntimesRef = useRef(/* @__PURE__ */ new Map());
|
|
155
|
+
const blockPluginsRef = useRef(/* @__PURE__ */ new Map());
|
|
156
|
+
const revealClockRef = useRef({ lastTs: 0 });
|
|
93
157
|
const renderNow = getNow();
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
const nextBirths = /* @__PURE__ */ new Map();
|
|
97
|
-
const prevBirths = blockBirthsRef.current;
|
|
98
|
-
for (const [index, block] of blocks.entries()) {
|
|
99
|
-
if (getBlockState(index) === "queued") continue;
|
|
100
|
-
const blockCharCount = countChars(block.content);
|
|
101
|
-
const prev = prevBirths.get(block.startOffset);
|
|
102
|
-
let arr;
|
|
103
|
-
if (prev && prev.length === blockCharCount) arr = prev;
|
|
104
|
-
else if (prev && prev.length > blockCharCount) arr = prev.slice(0, blockCharCount);
|
|
105
|
-
else {
|
|
106
|
-
arr = prev ? prev.slice() : [];
|
|
107
|
-
const startIdx = arr.length;
|
|
108
|
-
const cap = renderNow + STREAM_FADE_DURATION;
|
|
109
|
-
for (let i = startIdx; i < blockCharCount; i++) {
|
|
110
|
-
const chained = (i > 0 ? arr[i - 1] : renderNow - charDelay) + charDelay;
|
|
111
|
-
arr.push(Math.min(cap, Math.max(chained, renderNow)));
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
nextBirths.set(block.startOffset, arr);
|
|
115
|
-
}
|
|
116
|
-
return {
|
|
117
|
-
durationMs: profiler ? getNow() - start : 0,
|
|
118
|
-
value: nextBirths
|
|
119
|
-
};
|
|
120
|
-
}, [
|
|
158
|
+
const animationStart = profiler ? getNow() : 0;
|
|
159
|
+
const blockAnimationMeta = updateBlockAnimation({
|
|
121
160
|
blocks,
|
|
122
161
|
charDelay,
|
|
123
162
|
getBlockState,
|
|
124
|
-
|
|
125
|
-
renderNow
|
|
126
|
-
|
|
127
|
-
|
|
163
|
+
pluginsCache: blockPluginsRef.current,
|
|
164
|
+
renderNow,
|
|
165
|
+
revealClock: revealClockRef.current,
|
|
166
|
+
runtimes: blockRuntimesRef.current
|
|
167
|
+
});
|
|
168
|
+
const blockAnimationDurationMs = profiler ? getNow() - animationStart : 0;
|
|
128
169
|
useEffect(() => {
|
|
129
170
|
if (!profiler) return;
|
|
130
171
|
profiler.recordCalculation({
|
|
@@ -154,48 +195,35 @@ const StreamdownRender = memo(({ children, ...rest }) => {
|
|
|
154
195
|
useEffect(() => {
|
|
155
196
|
if (!profiler) return;
|
|
156
197
|
profiler.recordCalculation({
|
|
157
|
-
durationMs:
|
|
198
|
+
durationMs: blockAnimationDurationMs,
|
|
158
199
|
itemCount: blocks.length,
|
|
159
200
|
name: "block-births",
|
|
160
201
|
textLength: processedContent.length
|
|
161
202
|
});
|
|
162
203
|
}, [
|
|
163
|
-
|
|
204
|
+
blockAnimationDurationMs,
|
|
164
205
|
blocks.length,
|
|
165
206
|
processedContent.length,
|
|
166
207
|
profiler
|
|
167
208
|
]);
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
return
|
|
185
|
-
|
|
186
|
-
blockCharDelay: nextBlockCharDelay
|
|
187
|
-
};
|
|
188
|
-
}, [
|
|
189
|
-
birthsForRender,
|
|
190
|
-
blocks,
|
|
191
|
-
charDelay,
|
|
192
|
-
getBlockState,
|
|
193
|
-
renderNow
|
|
194
|
-
]);
|
|
195
|
-
useEffect(() => {
|
|
196
|
-
blockCharDelayRef.current = blockAnimationMetaResult.blockCharDelay;
|
|
197
|
-
blockBirthsRef.current = birthsForRender;
|
|
198
|
-
}, [birthsForRender, blockAnimationMetaResult.blockCharDelay]);
|
|
209
|
+
const resolveBlockPlugins = (startOffset, settled) => {
|
|
210
|
+
if (settled) return baseRehypePlugins;
|
|
211
|
+
const cache = blockPluginsRef.current;
|
|
212
|
+
const entry = cache.get(startOffset);
|
|
213
|
+
if (entry && entry.base === baseRehypePlugins && entry.granularity === streamAnimationGranularity) return entry.value;
|
|
214
|
+
const runtime = blockRuntimesRef.current.get(startOffset);
|
|
215
|
+
const value = [...baseRehypePlugins, [rehypeStreamAnimated, {
|
|
216
|
+
fadeDuration: 180,
|
|
217
|
+
granularity: streamAnimationGranularity,
|
|
218
|
+
runtime
|
|
219
|
+
}]];
|
|
220
|
+
cache.set(startOffset, {
|
|
221
|
+
base: baseRehypePlugins,
|
|
222
|
+
granularity: streamAnimationGranularity,
|
|
223
|
+
value
|
|
224
|
+
});
|
|
225
|
+
return value;
|
|
226
|
+
};
|
|
199
227
|
const handleRootRender = useCallback((_, phase, actualDuration, baseDuration) => {
|
|
200
228
|
profiler?.recordRootCommit({
|
|
201
229
|
actualDuration,
|
|
@@ -233,23 +261,10 @@ const StreamdownRender = memo(({ children, ...rest }) => {
|
|
|
233
261
|
const content = /* @__PURE__ */ jsx("div", {
|
|
234
262
|
className: styles.animated,
|
|
235
263
|
children: blocks.map((block, index) => {
|
|
236
|
-
|
|
237
|
-
const animationMeta = blockAnimationMetaResult.blockAnimationMeta.get(block.startOffset);
|
|
264
|
+
const animationMeta = blockAnimationMeta.get(block.startOffset);
|
|
238
265
|
if (!animationMeta) return null;
|
|
239
|
-
const
|
|
240
|
-
const plugins = animationMeta.settled ? [...baseRehypePlugins, REVEALED_STREAM_PLUGIN] : [...baseRehypePlugins, [rehypeStreamAnimated, {
|
|
241
|
-
births,
|
|
242
|
-
fadeDuration: STREAM_FADE_DURATION,
|
|
243
|
-
nowMs: renderNow
|
|
244
|
-
}]];
|
|
266
|
+
const plugins = resolveBlockPlugins(block.startOffset, animationMeta.settled);
|
|
245
267
|
const key = `${generatedId}-${block.startOffset}`;
|
|
246
|
-
const blockNode = /* @__PURE__ */ jsx(StreamdownBlock, {
|
|
247
|
-
...rest,
|
|
248
|
-
components,
|
|
249
|
-
rehypePlugins: plugins,
|
|
250
|
-
remarkPlugins,
|
|
251
|
-
children: block.content
|
|
252
|
-
});
|
|
253
268
|
if (!profiler) return /* @__PURE__ */ createElement(StreamdownBlock, {
|
|
254
269
|
...rest,
|
|
255
270
|
components,
|
|
@@ -260,7 +275,13 @@ const StreamdownRender = memo(({ children, ...rest }) => {
|
|
|
260
275
|
return /* @__PURE__ */ jsx(Profiler, {
|
|
261
276
|
id: `streamdown-block:${index}:${block.startOffset}`,
|
|
262
277
|
onRender: handleBlockRender,
|
|
263
|
-
children:
|
|
278
|
+
children: /* @__PURE__ */ jsx(StreamdownBlock, {
|
|
279
|
+
...rest,
|
|
280
|
+
components,
|
|
281
|
+
rehypePlugins: plugins,
|
|
282
|
+
remarkPlugins,
|
|
283
|
+
children: block.content
|
|
284
|
+
})
|
|
264
285
|
}, key);
|
|
265
286
|
})
|
|
266
287
|
});
|
|
@@ -271,6 +292,15 @@ const StreamdownRender = memo(({ children, ...rest }) => {
|
|
|
271
292
|
children: content
|
|
272
293
|
});
|
|
273
294
|
});
|
|
295
|
+
StreamdownBlocks.displayName = "StreamdownBlocks";
|
|
296
|
+
const StreamdownRender = memo(({ children, ...rest }) => {
|
|
297
|
+
const { streamSmoothingPreset = "balanced" } = useMarkdownContext();
|
|
298
|
+
const escapedContent = useMarkdownContent(children || "");
|
|
299
|
+
return /* @__PURE__ */ jsx(StreamdownBlocks, {
|
|
300
|
+
content: useSmoothStreamContent(typeof escapedContent === "string" ? escapedContent : "", { preset: streamSmoothingPreset }),
|
|
301
|
+
markdownOptions: useStableValue(rest)
|
|
302
|
+
});
|
|
303
|
+
});
|
|
274
304
|
StreamdownRender.displayName = "StreamdownRender";
|
|
275
305
|
//#endregion
|
|
276
306
|
export { StreamdownRender as default };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StreamdownRender.mjs","names":[],"sources":["../../../src/Markdown/SyntaxMarkdown/StreamdownRender.tsx"],"sourcesContent":["'use client';\n\nimport { marked } from 'marked';\nimport {\n memo,\n Profiler,\n type ProfilerOnRenderCallback,\n useCallback,\n useEffect,\n useId,\n useMemo,\n useRef,\n} from 'react';\nimport Markdown, { type Options } from 'react-markdown';\nimport remend from 'remend';\nimport type { Pluggable, PluggableList } from 'unified';\n\nimport {\n useMarkdownComponents,\n useMarkdownContent,\n useMarkdownRehypePlugins,\n useMarkdownRemarkPlugins,\n} from '@/hooks/useMarkdown';\nimport { useMarkdownContext } from '@/Markdown/components/MarkdownProvider';\nimport { rehypeStreamAnimated } from '@/Markdown/plugins/rehypeStreamAnimated';\nimport { useStreamdownProfiler } from '@/Markdown/streamProfiler';\nimport { isDeepEqual } from '@/utils/isDeepEqual';\n\nimport { resolveBlockAnimationMeta } from './streamAnimationMeta';\nimport { styles } from './style';\nimport { useSmoothStreamContent } from './useSmoothStreamContent';\nimport { type BlockInfo, useStreamQueue } from './useStreamQueue';\n\nconst STREAM_FADE_DURATION = 280;\nconst REVEALED_STREAM_PLUGIN: Pluggable = [rehypeStreamAnimated, { revealed: true }];\n\nfunction countChars(text: string): number {\n return [...text].length;\n}\n\nfunction getNow(): number {\n return typeof performance === 'undefined' ? Date.now() : performance.now();\n}\n\nconst isSamePlugin = (prevPlugin: Pluggable, nextPlugin: Pluggable): boolean => {\n const prevTuple = Array.isArray(prevPlugin) ? prevPlugin : [prevPlugin];\n const nextTuple = Array.isArray(nextPlugin) ? nextPlugin : [nextPlugin];\n\n if (prevTuple.length !== nextTuple.length) return false;\n if (prevTuple[0] !== nextTuple[0]) return false;\n\n return isDeepEqual(prevTuple.slice(1), nextTuple.slice(1));\n};\n\nconst isSamePlugins = (\n prevPlugins?: PluggableList | null,\n nextPlugins?: PluggableList | null,\n): boolean => {\n if (prevPlugins === nextPlugins) return true;\n if (!prevPlugins || !nextPlugins) return !prevPlugins && !nextPlugins;\n if (prevPlugins.length !== nextPlugins.length) return false;\n\n for (let i = 0; i < prevPlugins.length; i++) {\n if (!isSamePlugin(prevPlugins[i], nextPlugins[i])) return false;\n }\n\n return true;\n};\n\nconst useStablePlugins = (plugins: PluggableList): PluggableList => {\n const stableRef = useRef<PluggableList>(plugins);\n\n if (!isSamePlugins(stableRef.current, plugins)) {\n stableRef.current = plugins;\n }\n\n return stableRef.current;\n};\n\nconst StreamdownBlock = memo<Options>(\n ({ children, ...rest }) => {\n return <Markdown {...rest}>{children}</Markdown>;\n },\n (prevProps, nextProps) =>\n prevProps.children === nextProps.children &&\n prevProps.components === nextProps.components &&\n isSamePlugins(prevProps.rehypePlugins, nextProps.rehypePlugins) &&\n isSamePlugins(prevProps.remarkPlugins, nextProps.remarkPlugins),\n);\n\nStreamdownBlock.displayName = 'StreamdownBlock';\n\nexport const StreamdownRender = memo<Options>(({ children, ...rest }) => {\n const { streamSmoothingPreset = 'balanced' } = useMarkdownContext();\n const profiler = useStreamdownProfiler();\n const escapedContent = useMarkdownContent(children || '');\n const components = useMarkdownComponents();\n const baseRehypePlugins = useStablePlugins(useMarkdownRehypePlugins());\n const remarkPlugins = useStablePlugins(useMarkdownRemarkPlugins());\n const generatedId = useId();\n const smoothedContent = useSmoothStreamContent(\n typeof escapedContent === 'string' ? escapedContent : '',\n { preset: streamSmoothingPreset },\n );\n\n const processedContentResult = useMemo(() => {\n const start = profiler ? getNow() : 0;\n const value = remend(smoothedContent);\n\n return {\n durationMs: profiler ? getNow() - start : 0,\n value,\n };\n }, [profiler, smoothedContent]);\n const processedContent = processedContentResult.value;\n\n const blocksResult = useMemo(() => {\n const start = profiler ? getNow() : 0;\n const tokens = marked.lexer(processedContent);\n let offset = 0;\n\n const value = tokens.map((token) => {\n const block = { content: token.raw, startOffset: offset };\n offset += token.raw.length;\n return block;\n });\n\n return {\n durationMs: profiler ? getNow() - start : 0,\n value,\n };\n }, [processedContent, profiler]);\n const blocks: BlockInfo[] = blocksResult.value;\n\n const { getBlockState, charDelay } = useStreamQueue(blocks);\n const blockCharDelayRef = useRef<Map<number, number>>(new Map());\n const blockBirthsRef = useRef<Map<number, number[]>>(new Map());\n\n const renderNow = getNow();\n\n const birthsResult = useMemo(() => {\n const start = profiler ? getNow() : 0;\n const nextBirths = new Map<number, number[]>();\n const prevBirths = blockBirthsRef.current;\n\n for (const [index, block] of blocks.entries()) {\n const state = getBlockState(index);\n // Queued blocks are not rendered. Defer birth assignment so that\n // when the block later transitions to animating/streaming, its\n // chars start fading from that moment instead of having already\n // \"aged out\" of the fade window.\n if (state === 'queued') continue;\n\n const blockCharCount = countChars(block.content);\n const prev = prevBirths.get(block.startOffset);\n let arr: number[];\n\n if (prev && prev.length === blockCharCount) {\n arr = prev;\n } else if (prev && prev.length > blockCharCount) {\n // Block content shrunk (stream restart or upstream rewrite).\n arr = prev.slice(0, blockCharCount);\n } else {\n arr = prev ? prev.slice() : [];\n const startIdx = arr.length;\n // Chain each new char monotonically after the previous one so fades\n // never race out of order. Cap how far the fade queue can run ahead\n // of renderNow to prevent stream-faster-than-fade producing seconds\n // of invisible backlog at the tail.\n const cap = renderNow + STREAM_FADE_DURATION;\n for (let i = startIdx; i < blockCharCount; i++) {\n const prevBirth = i > 0 ? (arr[i - 1] as number) : renderNow - charDelay;\n const chained = prevBirth + charDelay;\n arr.push(Math.min(cap, Math.max(chained, renderNow)));\n }\n }\n\n nextBirths.set(block.startOffset, arr);\n }\n\n return {\n durationMs: profiler ? getNow() - start : 0,\n value: nextBirths,\n };\n }, [blocks, charDelay, getBlockState, profiler, renderNow]);\n const birthsForRender = birthsResult.value;\n\n useEffect(() => {\n if (!profiler) return;\n\n profiler.recordCalculation({\n durationMs: processedContentResult.durationMs,\n name: 'content-normalize',\n textLength: processedContent.length,\n });\n }, [processedContent.length, processedContentResult.durationMs, profiler]);\n\n useEffect(() => {\n if (!profiler) return;\n\n profiler.recordCalculation({\n durationMs: blocksResult.durationMs,\n itemCount: blocks.length,\n name: 'block-lex',\n textLength: processedContent.length,\n });\n }, [blocks.length, blocksResult.durationMs, processedContent.length, profiler]);\n\n useEffect(() => {\n if (!profiler) return;\n\n profiler.recordCalculation({\n durationMs: birthsResult.durationMs,\n itemCount: blocks.length,\n name: 'block-births',\n textLength: processedContent.length,\n });\n }, [birthsResult.durationMs, blocks.length, processedContent.length, profiler]);\n\n const blockAnimationMetaResult = useMemo(() => {\n const nextBlockCharDelay = new Map<number, number>();\n const blockAnimationMeta = new Map<number, ReturnType<typeof resolveBlockAnimationMeta>>();\n\n for (const [index, block] of blocks.entries()) {\n const state = getBlockState(index);\n const births = birthsForRender.get(block.startOffset);\n const lastBirthTs = births && births.length > 0 ? (births.at(-1) ?? renderNow) : renderNow;\n const lastElapsedMs = renderNow - lastBirthTs;\n const animationMeta = resolveBlockAnimationMeta({\n currentCharDelay: charDelay,\n fadeDuration: STREAM_FADE_DURATION,\n lastElapsedMs,\n previousCharDelay: blockCharDelayRef.current.get(block.startOffset),\n state,\n });\n\n nextBlockCharDelay.set(block.startOffset, animationMeta.charDelay);\n blockAnimationMeta.set(block.startOffset, animationMeta);\n }\n\n return {\n blockAnimationMeta,\n blockCharDelay: nextBlockCharDelay,\n };\n }, [birthsForRender, blocks, charDelay, getBlockState, renderNow]);\n\n useEffect(() => {\n blockCharDelayRef.current = blockAnimationMetaResult.blockCharDelay;\n blockBirthsRef.current = birthsForRender;\n }, [birthsForRender, blockAnimationMetaResult.blockCharDelay]);\n\n const handleRootRender = useCallback<ProfilerOnRenderCallback>(\n (_, phase, actualDuration, baseDuration) => {\n profiler?.recordRootCommit({\n actualDuration,\n baseDuration,\n blockCount: blocks.length,\n phase,\n textLength: processedContent.length,\n });\n },\n [blocks.length, processedContent.length, profiler],\n );\n\n const handleBlockRender = useCallback<ProfilerOnRenderCallback>(\n (id, phase, actualDuration, baseDuration) => {\n if (!profiler) return;\n\n const [, indexText, offsetText] = id.split(':');\n const blockIndex = Number(indexText);\n\n if (!Number.isFinite(blockIndex)) return;\n\n const block = blocks[blockIndex];\n if (!block) return;\n\n profiler.recordBlockCommit({\n actualDuration,\n baseDuration,\n blockChars: countChars(block.content),\n blockIndex,\n blockKey: offsetText ?? String(block.startOffset),\n phase,\n state: getBlockState(blockIndex),\n });\n },\n [blocks, getBlockState, profiler],\n );\n\n const content = (\n <div className={styles.animated}>\n {blocks.map((block, index) => {\n const state = getBlockState(index);\n if (state === 'queued') return null;\n const animationMeta = blockAnimationMetaResult.blockAnimationMeta.get(block.startOffset);\n if (!animationMeta) return null;\n\n const births = birthsForRender.get(block.startOffset);\n const plugins: Pluggable[] = animationMeta.settled\n ? [...baseRehypePlugins, REVEALED_STREAM_PLUGIN]\n : [\n ...baseRehypePlugins,\n [\n rehypeStreamAnimated,\n {\n births,\n fadeDuration: STREAM_FADE_DURATION,\n nowMs: renderNow,\n },\n ],\n ];\n\n const key = `${generatedId}-${block.startOffset}`;\n const blockNode = (\n <StreamdownBlock\n {...rest}\n components={components}\n rehypePlugins={plugins}\n remarkPlugins={remarkPlugins}\n >\n {block.content}\n </StreamdownBlock>\n );\n\n if (!profiler) {\n return (\n <StreamdownBlock\n {...rest}\n components={components}\n key={key}\n rehypePlugins={plugins}\n remarkPlugins={remarkPlugins}\n >\n {block.content}\n </StreamdownBlock>\n );\n }\n\n return (\n <Profiler\n id={`streamdown-block:${index}:${block.startOffset}`}\n key={key}\n onRender={handleBlockRender}\n >\n {blockNode}\n </Profiler>\n );\n })}\n </div>\n );\n\n if (!profiler) return content;\n\n return (\n <Profiler id={'streamdown-root'} onRender={handleRootRender}>\n {content}\n </Profiler>\n );\n});\n\nStreamdownRender.displayName = 'StreamdownRender';\n\nexport default StreamdownRender;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"StreamdownRender.mjs","names":[],"sources":["../../../src/Markdown/SyntaxMarkdown/StreamdownRender.tsx"],"sourcesContent":["'use client';\n\nimport { marked } from 'marked';\nimport {\n memo,\n Profiler,\n type ProfilerOnRenderCallback,\n useCallback,\n useEffect,\n useId,\n useMemo,\n useRef,\n} from 'react';\nimport { type Options } from 'react-markdown';\nimport remend from 'remend';\nimport type { Pluggable, PluggableList } from 'unified';\n\nimport {\n useMarkdownComponents,\n useMarkdownContent,\n useMarkdownRehypePlugins,\n useMarkdownRemarkPlugins,\n} from '@/hooks/useMarkdown';\nimport { useStableValue } from '@/hooks/useStableValue';\nimport { useMarkdownContext } from '@/Markdown/components/MarkdownProvider';\nimport {\n rehypeStreamAnimated,\n type StreamAnimatedRuntime,\n} from '@/Markdown/plugins/rehypeStreamAnimated';\nimport { useStreamdownProfiler } from '@/Markdown/streamProfiler';\nimport { type StreamAnimationGranularity } from '@/Markdown/type';\nimport { getNow } from '@/utils/getNow';\nimport { isDeepEqual } from '@/utils/isDeepEqual';\n\nimport { CachedMarkdown } from './CachedMarkdown';\nimport { type BlockAnimationMeta, resolveBlockAnimationMeta } from './streamAnimationMeta';\nimport { STREAM_FADE_DURATION, styles } from './style';\nimport { countChars, useSmoothStreamContent } from './useSmoothStreamContent';\nimport { type BlockInfo, type BlockState, useStreamQueue } from './useStreamQueue';\n\nconst isSamePlugin = (prevPlugin: Pluggable, nextPlugin: Pluggable): boolean => {\n const prevTuple = Array.isArray(prevPlugin) ? prevPlugin : [prevPlugin];\n const nextTuple = Array.isArray(nextPlugin) ? nextPlugin : [nextPlugin];\n\n if (prevTuple.length !== nextTuple.length) return false;\n if (prevTuple[0] !== nextTuple[0]) return false;\n\n return isDeepEqual(prevTuple.slice(1), nextTuple.slice(1));\n};\n\nconst isSamePlugins = (\n prevPlugins?: PluggableList | null,\n nextPlugins?: PluggableList | null,\n): boolean => {\n if (prevPlugins === nextPlugins) return true;\n if (!prevPlugins || !nextPlugins) return !prevPlugins && !nextPlugins;\n if (prevPlugins.length !== nextPlugins.length) return false;\n\n for (let i = 0; i < prevPlugins.length; i++) {\n if (!isSamePlugin(prevPlugins[i], nextPlugins[i])) return false;\n }\n\n return true;\n};\n\nconst useStablePlugins = (plugins: PluggableList): PluggableList => {\n const stableRef = useRef<PluggableList>(plugins);\n\n if (!isSamePlugins(stableRef.current, plugins)) {\n stableRef.current = plugins;\n }\n\n return stableRef.current;\n};\n\nconst StreamdownBlock = memo<Options>(\n ({ children, ...rest }) => {\n return <CachedMarkdown {...rest}>{children}</CachedMarkdown>;\n },\n (prevProps, nextProps) =>\n prevProps.children === nextProps.children &&\n prevProps.components === nextProps.components &&\n isSamePlugins(prevProps.rehypePlugins, nextProps.rehypePlugins) &&\n isSamePlugins(prevProps.remarkPlugins, nextProps.remarkPlugins),\n);\n\nStreamdownBlock.displayName = 'StreamdownBlock';\n\ninterface BlockRuntime extends StreamAnimatedRuntime {\n charCount: number;\n charDelay?: number;\n rawLength: number;\n settled: boolean;\n}\n\ninterface BlockPluginsCacheEntry {\n base: PluggableList;\n granularity: StreamAnimationGranularity;\n value: PluggableList;\n}\n\ninterface UpdateBlockAnimationArgs {\n blocks: BlockInfo[];\n charDelay: number;\n getBlockState: (index: number) => BlockState;\n pluginsCache: Map<number, BlockPluginsCacheEntry>;\n renderNow: number;\n revealClock: { lastTs: number };\n runtimes: Map<number, BlockRuntime>;\n}\n\nconst MIN_STREAM_CHAR_PACE_MS = 2;\nconst MAX_REVEAL_GAP_MS = 160;\n\n// Runs in the render phase: extends each visible block's birth timeline in\n// place and resolves its animation meta in one pass. Mutations are\n// idempotent for a given block content/length, so discarded or StrictMode\n// double renders re-derive the same state.\nconst updateBlockAnimation = ({\n blocks,\n charDelay,\n getBlockState,\n pluginsCache,\n renderNow,\n revealClock,\n runtimes,\n}: UpdateBlockAnimationArgs): Map<number, BlockAnimationMeta> => {\n const blockAnimationMeta = new Map<number, BlockAnimationMeta>();\n const alive = new Set<number>();\n let revealedNewChars = false;\n\n for (const [index, block] of blocks.entries()) {\n alive.add(block.startOffset);\n\n // Queued blocks are not rendered. Defer birth assignment so that\n // when the block later transitions to animating/streaming, its\n // chars start fading from that moment instead of having already\n // \"aged out\" of the fade window.\n const state = getBlockState(index);\n if (state === 'queued') continue;\n\n let runtime = runtimes.get(block.startOffset);\n if (!runtime) {\n runtime = { births: [], charCount: 0, rawLength: -1, settled: false, styles: [] };\n runtimes.set(block.startOffset, runtime);\n }\n\n if (runtime.rawLength !== block.content.length) {\n runtime.charCount = countChars(block.content);\n runtime.rawLength = block.content.length;\n }\n\n const blockCharCount = runtime.charCount;\n const births = runtime.births;\n\n if (births.length > blockCharCount) {\n // Block content shrunk (stream restart or upstream rewrite).\n births.length = blockCharCount;\n runtime.styles.length = blockCharCount;\n }\n\n if (births.length < blockCharCount) {\n // Chain each new char monotonically after the previous one so fades\n // never race out of order. Cap how far the fade queue can run ahead\n // of renderNow to prevent stream-faster-than-fade producing seconds\n // of invisible backlog at the tail.\n //\n // The streaming tail paces its stagger from the observed reveal-commit\n // gap instead of the queue's fixed charDelay: a commit's chars are\n // spread to land exactly until the next commit arrives, so the flow\n // stays per-char continuous no matter how far apart the throttled\n // commits are.\n const newChars = blockCharCount - births.length;\n let pace = charDelay;\n let cap = renderNow + STREAM_FADE_DURATION;\n if (state === 'streaming') {\n revealedNewChars = true;\n const gapMs = Math.min(Math.max(renderNow - revealClock.lastTs, 16), MAX_REVEAL_GAP_MS);\n pace = Math.min(charDelay, Math.max(gapMs / newChars, MIN_STREAM_CHAR_PACE_MS));\n cap = renderNow + gapMs + STREAM_FADE_DURATION;\n }\n for (let i = births.length; i < blockCharCount; i++) {\n const prevBirth = i > 0 ? births[i - 1] : renderNow - pace;\n const chained = prevBirth + pace;\n births.push(Math.min(cap, Math.max(chained, renderNow)));\n }\n }\n\n let meta: BlockAnimationMeta;\n if (runtime.settled) {\n // Settled is monotone: a revealed block's births are frozen, so once\n // its last fade completed it stays settled until the runtime is\n // pruned by a stream restart.\n meta = { charDelay: runtime.charDelay ?? charDelay, settled: true };\n } else {\n const lastBirthTs = births.length > 0 ? (births.at(-1) ?? renderNow) : renderNow;\n meta = resolveBlockAnimationMeta({\n currentCharDelay: charDelay,\n fadeDuration: STREAM_FADE_DURATION,\n lastElapsedMs: renderNow - lastBirthTs,\n previousCharDelay: runtime.charDelay,\n state,\n });\n runtime.settled = meta.settled;\n }\n runtime.charDelay = meta.charDelay;\n\n blockAnimationMeta.set(block.startOffset, meta);\n }\n\n if (revealedNewChars) {\n revealClock.lastTs = renderNow;\n }\n\n for (const key of runtimes.keys()) {\n if (!alive.has(key)) {\n runtimes.delete(key);\n pluginsCache.delete(key);\n }\n }\n\n return blockAnimationMeta;\n};\n\ninterface StreamdownBlocksProps {\n content: string;\n markdownOptions: Omit<Options, 'children'>;\n}\n\nconst StreamdownBlocks = memo<StreamdownBlocksProps>(\n ({ content: smoothedContent, markdownOptions: rest }) => {\n const { streamAnimationGranularity = 'char' } = useMarkdownContext();\n const profiler = useStreamdownProfiler();\n const components = useMarkdownComponents();\n const baseRehypePlugins = useStablePlugins(useMarkdownRehypePlugins());\n const remarkPlugins = useStablePlugins(useMarkdownRemarkPlugins());\n const generatedId = useId();\n\n const processedContentResult = useMemo(() => {\n const start = profiler ? getNow() : 0;\n const value = remend(smoothedContent);\n\n return {\n durationMs: profiler ? getNow() - start : 0,\n value,\n };\n }, [profiler, smoothedContent]);\n const processedContent = processedContentResult.value;\n\n const blocksResult = useMemo(() => {\n const start = profiler ? getNow() : 0;\n const tokens = marked.lexer(processedContent);\n let offset = 0;\n\n const value = tokens.map((token) => {\n const block = { content: token.raw, startOffset: offset };\n offset += token.raw.length;\n return block;\n });\n\n return {\n durationMs: profiler ? getNow() - start : 0,\n value,\n };\n }, [processedContent, profiler]);\n const blocks: BlockInfo[] = blocksResult.value;\n\n const { getBlockState, charDelay } = useStreamQueue(blocks);\n const blockRuntimesRef = useRef<Map<number, BlockRuntime>>(new Map());\n const blockPluginsRef = useRef<Map<number, BlockPluginsCacheEntry>>(new Map());\n const revealClockRef = useRef<{ lastTs: number }>({ lastTs: 0 });\n\n const renderNow = getNow();\n\n const animationStart = profiler ? getNow() : 0;\n const blockAnimationMeta = updateBlockAnimation({\n blocks,\n charDelay,\n getBlockState,\n pluginsCache: blockPluginsRef.current,\n renderNow,\n revealClock: revealClockRef.current,\n runtimes: blockRuntimesRef.current,\n });\n const blockAnimationDurationMs = profiler ? getNow() - animationStart : 0;\n\n useEffect(() => {\n if (!profiler) return;\n\n profiler.recordCalculation({\n durationMs: processedContentResult.durationMs,\n name: 'content-normalize',\n textLength: processedContent.length,\n });\n }, [processedContent.length, processedContentResult.durationMs, profiler]);\n\n useEffect(() => {\n if (!profiler) return;\n\n profiler.recordCalculation({\n durationMs: blocksResult.durationMs,\n itemCount: blocks.length,\n name: 'block-lex',\n textLength: processedContent.length,\n });\n }, [blocks.length, blocksResult.durationMs, processedContent.length, profiler]);\n\n useEffect(() => {\n if (!profiler) return;\n\n profiler.recordCalculation({\n durationMs: blockAnimationDurationMs,\n itemCount: blocks.length,\n name: 'block-births',\n textLength: processedContent.length,\n });\n }, [blockAnimationDurationMs, blocks.length, processedContent.length, profiler]);\n\n const resolveBlockPlugins = (startOffset: number, settled: boolean): PluggableList => {\n if (settled) return baseRehypePlugins;\n\n const cache = blockPluginsRef.current;\n const entry = cache.get(startOffset);\n if (\n entry &&\n entry.base === baseRehypePlugins &&\n entry.granularity === streamAnimationGranularity\n ) {\n return entry.value;\n }\n\n const runtime = blockRuntimesRef.current.get(startOffset);\n const value: PluggableList = [\n ...baseRehypePlugins,\n [\n rehypeStreamAnimated,\n {\n fadeDuration: STREAM_FADE_DURATION,\n granularity: streamAnimationGranularity,\n runtime,\n },\n ],\n ];\n cache.set(startOffset, {\n base: baseRehypePlugins,\n granularity: streamAnimationGranularity,\n value,\n });\n return value;\n };\n\n const handleRootRender = useCallback<ProfilerOnRenderCallback>(\n (_, phase, actualDuration, baseDuration) => {\n profiler?.recordRootCommit({\n actualDuration,\n baseDuration,\n blockCount: blocks.length,\n phase,\n textLength: processedContent.length,\n });\n },\n [blocks.length, processedContent.length, profiler],\n );\n\n const handleBlockRender = useCallback<ProfilerOnRenderCallback>(\n (id, phase, actualDuration, baseDuration) => {\n if (!profiler) return;\n\n const [, indexText, offsetText] = id.split(':');\n const blockIndex = Number(indexText);\n\n if (!Number.isFinite(blockIndex)) return;\n\n const block = blocks[blockIndex];\n if (!block) return;\n\n profiler.recordBlockCommit({\n actualDuration,\n baseDuration,\n blockChars: countChars(block.content),\n blockIndex,\n blockKey: offsetText ?? String(block.startOffset),\n phase,\n state: getBlockState(blockIndex),\n });\n },\n [blocks, getBlockState, profiler],\n );\n\n const content = (\n <div className={styles.animated}>\n {blocks.map((block, index) => {\n const animationMeta = blockAnimationMeta.get(block.startOffset);\n if (!animationMeta) return null;\n\n const plugins = resolveBlockPlugins(block.startOffset, animationMeta.settled);\n const key = `${generatedId}-${block.startOffset}`;\n\n if (!profiler) {\n return (\n <StreamdownBlock\n {...rest}\n components={components}\n key={key}\n rehypePlugins={plugins}\n remarkPlugins={remarkPlugins}\n >\n {block.content}\n </StreamdownBlock>\n );\n }\n\n return (\n <Profiler\n id={`streamdown-block:${index}:${block.startOffset}`}\n key={key}\n onRender={handleBlockRender}\n >\n <StreamdownBlock\n {...rest}\n components={components}\n rehypePlugins={plugins}\n remarkPlugins={remarkPlugins}\n >\n {block.content}\n </StreamdownBlock>\n </Profiler>\n );\n })}\n </div>\n );\n\n if (!profiler) return content;\n\n return (\n <Profiler id={'streamdown-root'} onRender={handleRootRender}>\n {content}\n </Profiler>\n );\n },\n);\n\nStreamdownBlocks.displayName = 'StreamdownBlocks';\n\n// The outer component absorbs the upstream per-chunk prop churn: every\n// streamed chunk re-renders it, but it only feeds the smoother and renders\n// a memoized child keyed on the smoother's output — so the expensive block\n// pipeline runs per reveal commit, not per chunk AND per commit.\nexport const StreamdownRender = memo<Options>(({ children, ...rest }) => {\n const { streamSmoothingPreset = 'balanced' } = useMarkdownContext();\n const escapedContent = useMarkdownContent(children || '');\n const smoothedContent = useSmoothStreamContent(\n typeof escapedContent === 'string' ? escapedContent : '',\n { preset: streamSmoothingPreset },\n );\n const markdownOptions = useStableValue(rest);\n\n return <StreamdownBlocks content={smoothedContent} markdownOptions={markdownOptions} />;\n});\n\nStreamdownRender.displayName = 'StreamdownRender';\n\nexport default StreamdownRender;\n"],"mappings":"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"}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { fadeIn } from "../../styles/animations.mjs";
|
|
2
2
|
import { createStaticStyles } from "antd-style";
|
|
3
|
-
//#region src/Markdown/SyntaxMarkdown/style.ts
|
|
4
3
|
const styles = createStaticStyles(({ css }) => {
|
|
5
4
|
return { animated: css`
|
|
6
5
|
.stream-char {
|
|
7
6
|
opacity: 0;
|
|
8
7
|
|
|
9
8
|
animation-name: ${fadeIn};
|
|
10
|
-
animation-duration:
|
|
9
|
+
animation-duration: ${180}ms;
|
|
11
10
|
animation-timing-function: cubic-bezier(0.33, 0, 0.67, 1);
|
|
12
11
|
animation-fill-mode: forwards;
|
|
13
12
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"style.mjs","names":[],"sources":["../../../src/Markdown/SyntaxMarkdown/style.ts"],"sourcesContent":["import { createStaticStyles } from 'antd-style';\n\nimport { fadeIn } from '@/styles/animations';\n\nexport const styles = createStaticStyles(({ css }) => {\n return {\n animated: css`\n .stream-char {\n opacity: 0;\n\n animation-name: ${fadeIn};\n animation-duration:
|
|
1
|
+
{"version":3,"file":"style.mjs","names":[],"sources":["../../../src/Markdown/SyntaxMarkdown/style.ts"],"sourcesContent":["import { createStaticStyles } from 'antd-style';\n\nimport { fadeIn } from '@/styles/animations';\n\nexport const STREAM_FADE_DURATION = 180;\n\nexport const styles = createStaticStyles(({ css }) => {\n return {\n animated: css`\n .stream-char {\n opacity: 0;\n\n animation-name: ${fadeIn};\n animation-duration: ${STREAM_FADE_DURATION}ms;\n animation-timing-function: cubic-bezier(0.33, 0, 0.67, 1);\n animation-fill-mode: forwards;\n }\n\n .stream-char-revealed {\n opacity: 1;\n animation: none;\n }\n\n .katex-display .katex-html span {\n mask: none !important;\n animation: none !important;\n }\n `,\n };\n});\n"],"mappings":";;AAMA,MAAa,SAAS,oBAAoB,EAAE,UAAU;AACpD,QAAO,EACL,UAAU,GAAG;;;;0BAIS,OAAO;kCACkB;;;;;;;;;;;;;;OAehD;EACD"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getNow } from "../../utils/getNow.mjs";
|
|
1
2
|
import { useStreamdownProfiler } from "../streamProfiler/StreamdownProfilerProvider.mjs";
|
|
2
3
|
import { findOpenFenceLanguage } from "./fenceState.mjs";
|
|
3
4
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
@@ -14,6 +15,7 @@ const PRESET_CONFIG = {
|
|
|
14
15
|
maxActiveCps: 132,
|
|
15
16
|
maxCps: 72,
|
|
16
17
|
maxFlushCps: 280,
|
|
18
|
+
minCommitIntervalMs: 48,
|
|
17
19
|
minCps: 18,
|
|
18
20
|
settleAfterMs: 360,
|
|
19
21
|
settleDrainMaxMs: 520,
|
|
@@ -30,6 +32,7 @@ const PRESET_CONFIG = {
|
|
|
30
32
|
maxActiveCps: 180,
|
|
31
33
|
maxCps: 96,
|
|
32
34
|
maxFlushCps: 360,
|
|
35
|
+
minCommitIntervalMs: 32,
|
|
33
36
|
minCps: 24,
|
|
34
37
|
settleAfterMs: 260,
|
|
35
38
|
settleDrainMaxMs: 360,
|
|
@@ -46,6 +49,7 @@ const PRESET_CONFIG = {
|
|
|
46
49
|
maxActiveCps: 102,
|
|
47
50
|
maxCps: 56,
|
|
48
51
|
maxFlushCps: 220,
|
|
52
|
+
minCommitIntervalMs: 56,
|
|
49
53
|
minCps: 14,
|
|
50
54
|
settleAfterMs: 460,
|
|
51
55
|
settleDrainMaxMs: 680,
|
|
@@ -56,11 +60,16 @@ const PRESET_CONFIG = {
|
|
|
56
60
|
const clamp = (value, min, max) => {
|
|
57
61
|
return Math.min(max, Math.max(min, value));
|
|
58
62
|
};
|
|
59
|
-
const
|
|
60
|
-
|
|
63
|
+
const MAX_COMMIT_INTERVAL_MS = 96;
|
|
64
|
+
const COMMIT_INTERVAL_TAIL_SCALE_UNITS = 256;
|
|
65
|
+
const findTailUnits = (content) => {
|
|
66
|
+
const boundary = content.lastIndexOf("\n\n");
|
|
67
|
+
return boundary === -1 ? content.length : content.length - boundary - 2;
|
|
61
68
|
};
|
|
62
69
|
const countChars = (text) => {
|
|
63
|
-
|
|
70
|
+
let count = 0;
|
|
71
|
+
for (const _char of text) count += 1;
|
|
72
|
+
return count;
|
|
64
73
|
};
|
|
65
74
|
const useSmoothStreamContent = (content, { enabled = true, preset = "balanced" } = {}) => {
|
|
66
75
|
const config = PRESET_CONFIG[preset];
|
|
@@ -72,6 +81,7 @@ const useSmoothStreamContent = (content, { enabled = true, preset = "balanced" }
|
|
|
72
81
|
const targetCharsRef = useRef([...content]);
|
|
73
82
|
const targetCountRef = useRef(targetCharsRef.current.length);
|
|
74
83
|
const emaCpsRef = useRef(config.defaultCps);
|
|
84
|
+
const tailUnitsRef = useRef(findTailUnits(content));
|
|
75
85
|
const lastInputTsRef = useRef(0);
|
|
76
86
|
const lastInputCountRef = useRef(targetCountRef.current);
|
|
77
87
|
const chunkSizeEmaRef = useRef(1);
|
|
@@ -117,6 +127,7 @@ const useSmoothStreamContent = (content, { enabled = true, preset = "balanced" }
|
|
|
117
127
|
emaCpsRef.current = config.defaultCps;
|
|
118
128
|
chunkSizeEmaRef.current = 1;
|
|
119
129
|
arrivalCpsEmaRef.current = config.defaultCps;
|
|
130
|
+
tailUnitsRef.current = findTailUnits(nextContent);
|
|
120
131
|
lastInputTsRef.current = now;
|
|
121
132
|
lastInputCountRef.current = chars.length;
|
|
122
133
|
}, [config.defaultCps, stopScheduling]);
|
|
@@ -124,23 +135,28 @@ const useSmoothStreamContent = (content, { enabled = true, preset = "balanced" }
|
|
|
124
135
|
clearWakeTimer();
|
|
125
136
|
if (rafRef.current !== null) return;
|
|
126
137
|
const tick = (ts) => {
|
|
127
|
-
const
|
|
138
|
+
const targetCount = targetCountRef.current;
|
|
139
|
+
const displayedCount = displayedCountRef.current;
|
|
140
|
+
const backlog = targetCount - displayedCount;
|
|
141
|
+
if (backlog <= 0) {
|
|
142
|
+
stopFrameLoop();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
128
145
|
if (lastFrameTsRef.current === null) {
|
|
129
146
|
lastFrameTsRef.current = ts;
|
|
130
147
|
rafRef.current = requestAnimationFrame(tick);
|
|
131
148
|
return;
|
|
132
149
|
}
|
|
150
|
+
const commitIntervalMs = Math.min(MAX_COMMIT_INTERVAL_MS, config.minCommitIntervalMs * (1 + tailUnitsRef.current / COMMIT_INTERVAL_TAIL_SCALE_UNITS));
|
|
133
151
|
const frameIntervalMs = Math.max(0, ts - lastFrameTsRef.current);
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const targetCount = targetCountRef.current;
|
|
137
|
-
const displayedCount = displayedCountRef.current;
|
|
138
|
-
const backlog = targetCount - displayedCount;
|
|
139
|
-
if (backlog <= 0) {
|
|
140
|
-
stopFrameLoop();
|
|
152
|
+
if (frameIntervalMs < commitIntervalMs) {
|
|
153
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
141
154
|
return;
|
|
142
155
|
}
|
|
143
|
-
const
|
|
156
|
+
const frameStart = getNow();
|
|
157
|
+
const dtSeconds = Math.max(.001, Math.min(frameIntervalMs / 1e3, .12));
|
|
158
|
+
lastFrameTsRef.current = ts;
|
|
159
|
+
const idleMs = frameStart - lastInputTsRef.current;
|
|
144
160
|
const inputActive = idleMs <= config.activeInputWindowMs;
|
|
145
161
|
const settling = !inputActive && idleMs >= config.settleAfterMs;
|
|
146
162
|
const baseCps = clamp(emaCpsRef.current, config.minCps, config.maxCps);
|
|
@@ -210,6 +226,7 @@ const useSmoothStreamContent = (content, { enabled = true, preset = "balanced" }
|
|
|
210
226
|
config.maxActiveCps,
|
|
211
227
|
config.maxCps,
|
|
212
228
|
config.maxFlushCps,
|
|
229
|
+
config.minCommitIntervalMs,
|
|
213
230
|
config.minCps,
|
|
214
231
|
config.settleAfterMs,
|
|
215
232
|
config.settleDrainMaxMs,
|
|
@@ -249,8 +266,9 @@ const useSmoothStreamContent = (content, { enabled = true, preset = "balanced" }
|
|
|
249
266
|
return;
|
|
250
267
|
}
|
|
251
268
|
targetContentRef.current = content;
|
|
252
|
-
targetCharsRef.current
|
|
269
|
+
targetCharsRef.current.push(...appendedChars);
|
|
253
270
|
targetCountRef.current += appendedCount;
|
|
271
|
+
tailUnitsRef.current = findTailUnits(content);
|
|
254
272
|
const deltaChars = targetCountRef.current - lastInputCountRef.current;
|
|
255
273
|
const deltaMs = Math.max(1, now - lastInputTsRef.current);
|
|
256
274
|
if (deltaChars > 0) {
|
|
@@ -287,6 +305,6 @@ const useSmoothStreamContent = (content, { enabled = true, preset = "balanced" }
|
|
|
287
305
|
return displayedContent;
|
|
288
306
|
};
|
|
289
307
|
//#endregion
|
|
290
|
-
export { useSmoothStreamContent };
|
|
308
|
+
export { countChars, useSmoothStreamContent };
|
|
291
309
|
|
|
292
310
|
//# sourceMappingURL=useSmoothStreamContent.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSmoothStreamContent.mjs","names":[],"sources":["../../../src/Markdown/SyntaxMarkdown/useSmoothStreamContent.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\n\nimport { useStreamdownProfiler } from '@/Markdown/streamProfiler';\nimport { type StreamSmoothingPreset } from '@/Markdown/type';\n\nimport { findOpenFenceLanguage } from './fenceState';\n\ninterface StreamSmoothingPresetConfig {\n activeInputWindowMs: number;\n /**\n * Code-fence languages whose contents bypass smoothing entirely. While\n * the input ends inside an open fence in this set, every chunk is\n * `syncImmediate`-d to the display so downstream consumers (the\n * HtmlPreview iframe in particular) see partial HTML at the demo's\n * actual production rate instead of waiting on the smoother's\n * ~maxFlushCps budget. As soon as the fence closes — or another\n * non-bypass fence opens — smoothing resumes for the rest of the\n * stream. Default `['html']` covers the artifact case that motivated\n * this; tune via preset if you need to bypass `mermaid`, `svg`, etc.\n */\n bypassFencedLanguages: readonly string[];\n defaultCps: number;\n emaAlpha: number;\n flushCps: number;\n largeAppendChars: number;\n maxActiveCps: number;\n maxCps: number;\n maxFlushCps: number;\n minCps: number;\n settleAfterMs: number;\n settleDrainMaxMs: number;\n settleDrainMinMs: number;\n targetBufferMs: number;\n}\n\nconst DEFAULT_BYPASS_LANGUAGES = ['html'] as const;\n\nconst PRESET_CONFIG: Record<StreamSmoothingPreset, StreamSmoothingPresetConfig> = {\n balanced: {\n activeInputWindowMs: 220,\n bypassFencedLanguages: DEFAULT_BYPASS_LANGUAGES,\n defaultCps: 38,\n emaAlpha: 0.2,\n flushCps: 120,\n largeAppendChars: 120,\n maxActiveCps: 132,\n maxCps: 72,\n maxFlushCps: 280,\n minCps: 18,\n settleAfterMs: 360,\n settleDrainMaxMs: 520,\n settleDrainMinMs: 180,\n targetBufferMs: 120,\n },\n realtime: {\n activeInputWindowMs: 140,\n bypassFencedLanguages: DEFAULT_BYPASS_LANGUAGES,\n defaultCps: 50,\n emaAlpha: 0.3,\n flushCps: 170,\n largeAppendChars: 180,\n maxActiveCps: 180,\n maxCps: 96,\n maxFlushCps: 360,\n minCps: 24,\n settleAfterMs: 260,\n settleDrainMaxMs: 360,\n settleDrainMinMs: 140,\n targetBufferMs: 40,\n },\n silky: {\n activeInputWindowMs: 320,\n bypassFencedLanguages: DEFAULT_BYPASS_LANGUAGES,\n defaultCps: 28,\n emaAlpha: 0.14,\n flushCps: 96,\n largeAppendChars: 100,\n maxActiveCps: 102,\n maxCps: 56,\n maxFlushCps: 220,\n minCps: 14,\n settleAfterMs: 460,\n settleDrainMaxMs: 680,\n settleDrainMinMs: 240,\n targetBufferMs: 170,\n },\n};\n\nconst clamp = (value: number, min: number, max: number): number => {\n return Math.min(max, Math.max(min, value));\n};\n\nconst getNow = () => {\n return typeof performance === 'undefined' ? Date.now() : performance.now();\n};\n\nexport const countChars = (text: string): number => {\n return [...text].length;\n};\n\ninterface UseSmoothStreamContentOptions {\n enabled?: boolean;\n preset?: StreamSmoothingPreset;\n}\n\nexport const useSmoothStreamContent = (\n content: string,\n { enabled = true, preset = 'balanced' }: UseSmoothStreamContentOptions = {},\n): string => {\n const config = PRESET_CONFIG[preset];\n const profiler = useStreamdownProfiler();\n const [displayedContent, setDisplayedContent] = useState(content);\n\n const displayedContentRef = useRef(content);\n const displayedCountRef = useRef(countChars(content));\n\n const targetContentRef = useRef(content);\n const targetCharsRef = useRef([...content]);\n const targetCountRef = useRef(targetCharsRef.current.length);\n\n const emaCpsRef = useRef(config.defaultCps);\n const lastInputTsRef = useRef(0);\n const lastInputCountRef = useRef(targetCountRef.current);\n const chunkSizeEmaRef = useRef(1);\n const arrivalCpsEmaRef = useRef(config.defaultCps);\n\n const rafRef = useRef<number | null>(null);\n const lastFrameTsRef = useRef<number | null>(null);\n const wakeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const clearWakeTimer = useCallback(() => {\n if (wakeTimerRef.current !== null) {\n clearTimeout(wakeTimerRef.current);\n wakeTimerRef.current = null;\n }\n }, []);\n\n const stopFrameLoop = useCallback(() => {\n if (rafRef.current !== null) {\n cancelAnimationFrame(rafRef.current);\n rafRef.current = null;\n }\n lastFrameTsRef.current = null;\n }, []);\n\n const stopScheduling = useCallback(() => {\n stopFrameLoop();\n clearWakeTimer();\n }, [clearWakeTimer, stopFrameLoop]);\n\n const startFrameLoopRef = useRef<() => void>(() => {});\n\n const scheduleFrameWake = useCallback(\n (delayMs: number) => {\n clearWakeTimer();\n\n wakeTimerRef.current = setTimeout(\n () => {\n wakeTimerRef.current = null;\n startFrameLoopRef.current();\n },\n Math.max(1, Math.ceil(delayMs)),\n );\n },\n [clearWakeTimer],\n );\n\n const syncImmediate = useCallback(\n (nextContent: string) => {\n stopScheduling();\n\n const chars = [...nextContent];\n const now = getNow();\n\n targetContentRef.current = nextContent;\n targetCharsRef.current = chars;\n targetCountRef.current = chars.length;\n\n displayedContentRef.current = nextContent;\n displayedCountRef.current = chars.length;\n setDisplayedContent(nextContent);\n\n emaCpsRef.current = config.defaultCps;\n chunkSizeEmaRef.current = 1;\n arrivalCpsEmaRef.current = config.defaultCps;\n lastInputTsRef.current = now;\n lastInputCountRef.current = chars.length;\n },\n [config.defaultCps, stopScheduling],\n );\n\n const startFrameLoop = useCallback(() => {\n clearWakeTimer();\n if (rafRef.current !== null) return;\n\n const tick = (ts: number) => {\n const frameStart = getNow();\n\n if (lastFrameTsRef.current === null) {\n lastFrameTsRef.current = ts;\n rafRef.current = requestAnimationFrame(tick);\n return;\n }\n\n const frameIntervalMs = Math.max(0, ts - lastFrameTsRef.current);\n const dtSeconds = Math.max(0.001, Math.min(frameIntervalMs / 1000, 0.05));\n lastFrameTsRef.current = ts;\n\n const targetCount = targetCountRef.current;\n const displayedCount = displayedCountRef.current;\n const backlog = targetCount - displayedCount;\n\n if (backlog <= 0) {\n stopFrameLoop();\n return;\n }\n\n const now = getNow();\n const idleMs = now - lastInputTsRef.current;\n const inputActive = idleMs <= config.activeInputWindowMs;\n const settling = !inputActive && idleMs >= config.settleAfterMs;\n\n const baseCps = clamp(emaCpsRef.current, config.minCps, config.maxCps);\n const baseLagChars = Math.max(1, Math.round((baseCps * config.targetBufferMs) / 1000));\n const lagUpperBound = Math.max(baseLagChars + 2, baseLagChars * 3);\n const targetLagChars = inputActive\n ? Math.round(\n clamp(baseLagChars + chunkSizeEmaRef.current * 0.35, baseLagChars, lagUpperBound),\n )\n : 0;\n const desiredDisplayed = Math.max(0, targetCount - targetLagChars);\n\n let currentCps: number;\n if (inputActive) {\n const backlogPressure = targetLagChars > 0 ? backlog / targetLagChars : 1;\n const chunkPressure = targetLagChars > 0 ? chunkSizeEmaRef.current / targetLagChars : 1;\n const arrivalPressure = arrivalCpsEmaRef.current / Math.max(baseCps, 1);\n const combinedPressure = clamp(\n backlogPressure * 0.6 + chunkPressure * 0.25 + arrivalPressure * 0.15,\n 1,\n 4.5,\n );\n const activeCap = clamp(\n config.maxActiveCps + chunkSizeEmaRef.current * 6,\n config.maxActiveCps,\n config.maxFlushCps,\n );\n currentCps = clamp(baseCps * combinedPressure, config.minCps, activeCap);\n } else if (settling) {\n // If upstream likely ended, cap the remaining tail duration so\n // we do not keep replaying old backlog for seconds.\n const drainTargetMs = clamp(backlog * 8, config.settleDrainMinMs, config.settleDrainMaxMs);\n const settleCps = (backlog * 1000) / drainTargetMs;\n currentCps = clamp(settleCps, config.flushCps, config.maxFlushCps);\n } else {\n const idleFlushCps = Math.max(\n config.flushCps,\n baseCps * 1.8,\n arrivalCpsEmaRef.current * 0.8,\n );\n currentCps = clamp(idleFlushCps, config.flushCps, config.maxFlushCps);\n }\n\n const urgentBacklog = inputActive && targetLagChars > 0 && backlog > targetLagChars * 2.2;\n const burstyInput = inputActive && chunkSizeEmaRef.current >= targetLagChars * 0.9;\n const minRevealChars = inputActive ? (urgentBacklog || burstyInput ? 2 : 1) : 2;\n let revealChars = Math.max(minRevealChars, Math.round(currentCps * dtSeconds));\n\n if (inputActive) {\n const shortfall = desiredDisplayed - displayedCount;\n if (shortfall <= 0) {\n stopFrameLoop();\n scheduleFrameWake(config.activeInputWindowMs - idleMs);\n\n profiler?.recordAnimationFrame({\n backlog,\n durationMs: getNow() - frameStart,\n frameIntervalMs,\n inputActive,\n revealChars: 0,\n settling,\n });\n return;\n }\n revealChars = Math.min(revealChars, shortfall, backlog);\n } else {\n revealChars = Math.min(revealChars, backlog);\n }\n\n const nextCount = displayedCount + revealChars;\n const segment = targetCharsRef.current.slice(displayedCount, nextCount).join('');\n\n if (segment) {\n const nextDisplayed = displayedContentRef.current + segment;\n displayedContentRef.current = nextDisplayed;\n displayedCountRef.current = nextCount;\n setDisplayedContent(nextDisplayed);\n } else {\n displayedContentRef.current = targetContentRef.current;\n displayedCountRef.current = targetCount;\n setDisplayedContent(targetContentRef.current);\n }\n\n profiler?.recordAnimationFrame({\n backlog,\n durationMs: getNow() - frameStart,\n frameIntervalMs,\n inputActive,\n revealChars: segment ? revealChars : backlog,\n settling,\n });\n\n rafRef.current = requestAnimationFrame(tick);\n };\n\n rafRef.current = requestAnimationFrame(tick);\n }, [\n clearWakeTimer,\n config.activeInputWindowMs,\n config.flushCps,\n config.maxActiveCps,\n config.maxCps,\n config.maxFlushCps,\n config.minCps,\n config.settleAfterMs,\n config.settleDrainMaxMs,\n config.settleDrainMinMs,\n config.targetBufferMs,\n scheduleFrameWake,\n stopFrameLoop,\n ]);\n startFrameLoopRef.current = startFrameLoop;\n\n useEffect(() => {\n if (!enabled) {\n syncImmediate(content);\n return;\n }\n\n const prevTargetContent = targetContentRef.current;\n if (content === prevTargetContent) return;\n\n const now = getNow();\n const appendOnly = content.startsWith(prevTargetContent);\n\n if (!appendOnly) {\n syncImmediate(content);\n return;\n }\n\n // Bypass smoothing entirely while the input ends inside an open\n // fence whose language is opted out — see the preset config.\n // Without this, a 5 KB inline `<style>` block can keep `</style>`\n // (and therefore the HtmlPreview head-close trigger) trapped in the\n // smoother's buffer for tens of seconds, leaving the iframe pinned\n // on the loading placeholder while the artifact \"looks stuck\".\n if (config.bypassFencedLanguages.length > 0) {\n const openLang = findOpenFenceLanguage(content);\n if (openLang !== null && config.bypassFencedLanguages.includes(openLang)) {\n syncImmediate(content);\n return;\n }\n }\n\n const appended = content.slice(prevTargetContent.length);\n const appendedChars = [...appended];\n const appendedCount = appendedChars.length;\n\n profiler?.recordInputAppend({\n appendedChars: appendedCount,\n contentLength: countChars(content),\n });\n\n if (appendedCount > config.largeAppendChars) {\n syncImmediate(content);\n return;\n }\n\n targetContentRef.current = content;\n targetCharsRef.current = [...targetCharsRef.current, ...appendedChars];\n targetCountRef.current += appendedCount;\n\n const deltaChars = targetCountRef.current - lastInputCountRef.current;\n const deltaMs = Math.max(1, now - lastInputTsRef.current);\n\n if (deltaChars > 0) {\n const instantCps = (deltaChars * 1000) / deltaMs;\n const normalizedInstantCps = clamp(instantCps, config.minCps, config.maxFlushCps * 2);\n const chunkEmaAlpha = 0.35;\n chunkSizeEmaRef.current =\n chunkSizeEmaRef.current * (1 - chunkEmaAlpha) + appendedCount * chunkEmaAlpha;\n arrivalCpsEmaRef.current =\n arrivalCpsEmaRef.current * (1 - chunkEmaAlpha) + normalizedInstantCps * chunkEmaAlpha;\n\n const clampedCps = clamp(instantCps, config.minCps, config.maxActiveCps);\n emaCpsRef.current = emaCpsRef.current * (1 - config.emaAlpha) + clampedCps * config.emaAlpha;\n }\n\n lastInputTsRef.current = now;\n lastInputCountRef.current = targetCountRef.current;\n\n startFrameLoop();\n }, [\n config.bypassFencedLanguages,\n config.emaAlpha,\n config.largeAppendChars,\n config.maxActiveCps,\n config.maxCps,\n config.maxFlushCps,\n config.minCps,\n content,\n enabled,\n startFrameLoop,\n syncImmediate,\n profiler,\n ]);\n\n useEffect(() => {\n return () => {\n stopScheduling();\n };\n }, [stopScheduling]);\n\n return displayedContent;\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useSmoothStreamContent.mjs","names":["now"],"sources":["../../../src/Markdown/SyntaxMarkdown/useSmoothStreamContent.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\n\nimport { useStreamdownProfiler } from '@/Markdown/streamProfiler';\nimport { type StreamSmoothingPreset } from '@/Markdown/type';\nimport { getNow } from '@/utils/getNow';\n\nimport { findOpenFenceLanguage } from './fenceState';\n\ninterface StreamSmoothingPresetConfig {\n activeInputWindowMs: number;\n /**\n * Code-fence languages whose contents bypass smoothing entirely. While\n * the input ends inside an open fence in this set, every chunk is\n * `syncImmediate`-d to the display so downstream consumers (the\n * HtmlPreview iframe in particular) see partial HTML at the demo's\n * actual production rate instead of waiting on the smoother's\n * ~maxFlushCps budget. As soon as the fence closes — or another\n * non-bypass fence opens — smoothing resumes for the rest of the\n * stream. Default `['html']` covers the artifact case that motivated\n * this; tune via preset if you need to bypass `mermaid`, `svg`, etc.\n */\n bypassFencedLanguages: readonly string[];\n defaultCps: number;\n emaAlpha: number;\n flushCps: number;\n largeAppendChars: number;\n maxActiveCps: number;\n maxCps: number;\n maxFlushCps: number;\n minCommitIntervalMs: number;\n minCps: number;\n settleAfterMs: number;\n settleDrainMaxMs: number;\n settleDrainMinMs: number;\n targetBufferMs: number;\n}\n\nconst DEFAULT_BYPASS_LANGUAGES = ['html'] as const;\n\nconst PRESET_CONFIG: Record<StreamSmoothingPreset, StreamSmoothingPresetConfig> = {\n balanced: {\n activeInputWindowMs: 220,\n bypassFencedLanguages: DEFAULT_BYPASS_LANGUAGES,\n defaultCps: 38,\n emaAlpha: 0.2,\n flushCps: 120,\n largeAppendChars: 120,\n maxActiveCps: 132,\n maxCps: 72,\n maxFlushCps: 280,\n minCommitIntervalMs: 48,\n minCps: 18,\n settleAfterMs: 360,\n settleDrainMaxMs: 520,\n settleDrainMinMs: 180,\n targetBufferMs: 120,\n },\n realtime: {\n activeInputWindowMs: 140,\n bypassFencedLanguages: DEFAULT_BYPASS_LANGUAGES,\n defaultCps: 50,\n emaAlpha: 0.3,\n flushCps: 170,\n largeAppendChars: 180,\n maxActiveCps: 180,\n maxCps: 96,\n maxFlushCps: 360,\n minCommitIntervalMs: 32,\n minCps: 24,\n settleAfterMs: 260,\n settleDrainMaxMs: 360,\n settleDrainMinMs: 140,\n targetBufferMs: 40,\n },\n silky: {\n activeInputWindowMs: 320,\n bypassFencedLanguages: DEFAULT_BYPASS_LANGUAGES,\n defaultCps: 28,\n emaAlpha: 0.14,\n flushCps: 96,\n largeAppendChars: 100,\n maxActiveCps: 102,\n maxCps: 56,\n maxFlushCps: 220,\n minCommitIntervalMs: 56,\n minCps: 14,\n settleAfterMs: 460,\n settleDrainMaxMs: 680,\n settleDrainMinMs: 240,\n targetBufferMs: 170,\n },\n};\n\nconst clamp = (value: number, min: number, max: number): number => {\n return Math.min(max, Math.max(min, value));\n};\n\n// Every reveal commit re-parses and re-wraps the entire trailing block, so\n// the per-commit cost grows linearly with how far the content is from the\n// last block boundary. Widen the commit interval as that distance grows —\n// long paragraphs/code fences keep total work bounded while short blocks\n// stay at the preset's snappy interval. Per-char animation-delay stagger\n// hides the lower commit rate.\nconst MAX_COMMIT_INTERVAL_MS = 96;\nconst COMMIT_INTERVAL_TAIL_SCALE_UNITS = 256;\n\nconst findTailUnits = (content: string): number => {\n const boundary = content.lastIndexOf('\\n\\n');\n return boundary === -1 ? content.length : content.length - boundary - 2;\n};\n\nexport const countChars = (text: string): number => {\n let count = 0;\n for (const _char of text) count += 1;\n return count;\n};\n\ninterface UseSmoothStreamContentOptions {\n enabled?: boolean;\n preset?: StreamSmoothingPreset;\n}\n\nexport const useSmoothStreamContent = (\n content: string,\n { enabled = true, preset = 'balanced' }: UseSmoothStreamContentOptions = {},\n): string => {\n const config = PRESET_CONFIG[preset];\n const profiler = useStreamdownProfiler();\n const [displayedContent, setDisplayedContent] = useState(content);\n\n const displayedContentRef = useRef(content);\n const displayedCountRef = useRef(countChars(content));\n\n const targetContentRef = useRef(content);\n const targetCharsRef = useRef([...content]);\n const targetCountRef = useRef(targetCharsRef.current.length);\n\n const emaCpsRef = useRef(config.defaultCps);\n const tailUnitsRef = useRef(findTailUnits(content));\n const lastInputTsRef = useRef(0);\n const lastInputCountRef = useRef(targetCountRef.current);\n const chunkSizeEmaRef = useRef(1);\n const arrivalCpsEmaRef = useRef(config.defaultCps);\n\n const rafRef = useRef<number | null>(null);\n const lastFrameTsRef = useRef<number | null>(null);\n const wakeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const clearWakeTimer = useCallback(() => {\n if (wakeTimerRef.current !== null) {\n clearTimeout(wakeTimerRef.current);\n wakeTimerRef.current = null;\n }\n }, []);\n\n const stopFrameLoop = useCallback(() => {\n if (rafRef.current !== null) {\n cancelAnimationFrame(rafRef.current);\n rafRef.current = null;\n }\n lastFrameTsRef.current = null;\n }, []);\n\n const stopScheduling = useCallback(() => {\n stopFrameLoop();\n clearWakeTimer();\n }, [clearWakeTimer, stopFrameLoop]);\n\n const startFrameLoopRef = useRef<() => void>(() => {});\n\n const scheduleFrameWake = useCallback(\n (delayMs: number) => {\n clearWakeTimer();\n\n wakeTimerRef.current = setTimeout(\n () => {\n wakeTimerRef.current = null;\n startFrameLoopRef.current();\n },\n Math.max(1, Math.ceil(delayMs)),\n );\n },\n [clearWakeTimer],\n );\n\n const syncImmediate = useCallback(\n (nextContent: string) => {\n stopScheduling();\n\n const chars = [...nextContent];\n const now = getNow();\n\n targetContentRef.current = nextContent;\n targetCharsRef.current = chars;\n targetCountRef.current = chars.length;\n\n displayedContentRef.current = nextContent;\n displayedCountRef.current = chars.length;\n setDisplayedContent(nextContent);\n\n emaCpsRef.current = config.defaultCps;\n chunkSizeEmaRef.current = 1;\n arrivalCpsEmaRef.current = config.defaultCps;\n tailUnitsRef.current = findTailUnits(nextContent);\n lastInputTsRef.current = now;\n lastInputCountRef.current = chars.length;\n },\n [config.defaultCps, stopScheduling],\n );\n\n const startFrameLoop = useCallback(() => {\n clearWakeTimer();\n if (rafRef.current !== null) return;\n\n const tick = (ts: number) => {\n const targetCount = targetCountRef.current;\n const displayedCount = displayedCountRef.current;\n const backlog = targetCount - displayedCount;\n\n if (backlog <= 0) {\n stopFrameLoop();\n return;\n }\n\n if (lastFrameTsRef.current === null) {\n lastFrameTsRef.current = ts;\n rafRef.current = requestAnimationFrame(tick);\n return;\n }\n\n // Reveal commits are throttled below the display refresh rate: each\n // commit re-renders the tail block and re-runs remend + lexing, so\n // committing at 60-120fps burns CPU without visible benefit — the\n // per-char stagger inside a commit is carried by animation-delay.\n const commitIntervalMs = Math.min(\n MAX_COMMIT_INTERVAL_MS,\n config.minCommitIntervalMs * (1 + tailUnitsRef.current / COMMIT_INTERVAL_TAIL_SCALE_UNITS),\n );\n const frameIntervalMs = Math.max(0, ts - lastFrameTsRef.current);\n if (frameIntervalMs < commitIntervalMs) {\n rafRef.current = requestAnimationFrame(tick);\n return;\n }\n\n const frameStart = getNow();\n const dtSeconds = Math.max(0.001, Math.min(frameIntervalMs / 1000, 0.12));\n lastFrameTsRef.current = ts;\n\n const now = frameStart;\n const idleMs = now - lastInputTsRef.current;\n const inputActive = idleMs <= config.activeInputWindowMs;\n const settling = !inputActive && idleMs >= config.settleAfterMs;\n\n const baseCps = clamp(emaCpsRef.current, config.minCps, config.maxCps);\n const baseLagChars = Math.max(1, Math.round((baseCps * config.targetBufferMs) / 1000));\n const lagUpperBound = Math.max(baseLagChars + 2, baseLagChars * 3);\n const targetLagChars = inputActive\n ? Math.round(\n clamp(baseLagChars + chunkSizeEmaRef.current * 0.35, baseLagChars, lagUpperBound),\n )\n : 0;\n const desiredDisplayed = Math.max(0, targetCount - targetLagChars);\n\n let currentCps: number;\n if (inputActive) {\n const backlogPressure = targetLagChars > 0 ? backlog / targetLagChars : 1;\n const chunkPressure = targetLagChars > 0 ? chunkSizeEmaRef.current / targetLagChars : 1;\n const arrivalPressure = arrivalCpsEmaRef.current / Math.max(baseCps, 1);\n const combinedPressure = clamp(\n backlogPressure * 0.6 + chunkPressure * 0.25 + arrivalPressure * 0.15,\n 1,\n 4.5,\n );\n const activeCap = clamp(\n config.maxActiveCps + chunkSizeEmaRef.current * 6,\n config.maxActiveCps,\n config.maxFlushCps,\n );\n currentCps = clamp(baseCps * combinedPressure, config.minCps, activeCap);\n } else if (settling) {\n // If upstream likely ended, cap the remaining tail duration so\n // we do not keep replaying old backlog for seconds.\n const drainTargetMs = clamp(backlog * 8, config.settleDrainMinMs, config.settleDrainMaxMs);\n const settleCps = (backlog * 1000) / drainTargetMs;\n currentCps = clamp(settleCps, config.flushCps, config.maxFlushCps);\n } else {\n const idleFlushCps = Math.max(\n config.flushCps,\n baseCps * 1.8,\n arrivalCpsEmaRef.current * 0.8,\n );\n currentCps = clamp(idleFlushCps, config.flushCps, config.maxFlushCps);\n }\n\n const urgentBacklog = inputActive && targetLagChars > 0 && backlog > targetLagChars * 2.2;\n const burstyInput = inputActive && chunkSizeEmaRef.current >= targetLagChars * 0.9;\n const minRevealChars = inputActive ? (urgentBacklog || burstyInput ? 2 : 1) : 2;\n let revealChars = Math.max(minRevealChars, Math.round(currentCps * dtSeconds));\n\n if (inputActive) {\n const shortfall = desiredDisplayed - displayedCount;\n if (shortfall <= 0) {\n stopFrameLoop();\n scheduleFrameWake(config.activeInputWindowMs - idleMs);\n\n profiler?.recordAnimationFrame({\n backlog,\n durationMs: getNow() - frameStart,\n frameIntervalMs,\n inputActive,\n revealChars: 0,\n settling,\n });\n return;\n }\n revealChars = Math.min(revealChars, shortfall, backlog);\n } else {\n revealChars = Math.min(revealChars, backlog);\n }\n\n const nextCount = displayedCount + revealChars;\n const segment = targetCharsRef.current.slice(displayedCount, nextCount).join('');\n\n if (segment) {\n const nextDisplayed = displayedContentRef.current + segment;\n displayedContentRef.current = nextDisplayed;\n displayedCountRef.current = nextCount;\n setDisplayedContent(nextDisplayed);\n } else {\n displayedContentRef.current = targetContentRef.current;\n displayedCountRef.current = targetCount;\n setDisplayedContent(targetContentRef.current);\n }\n\n profiler?.recordAnimationFrame({\n backlog,\n durationMs: getNow() - frameStart,\n frameIntervalMs,\n inputActive,\n revealChars: segment ? revealChars : backlog,\n settling,\n });\n\n rafRef.current = requestAnimationFrame(tick);\n };\n\n rafRef.current = requestAnimationFrame(tick);\n }, [\n clearWakeTimer,\n config.activeInputWindowMs,\n config.flushCps,\n config.maxActiveCps,\n config.maxCps,\n config.maxFlushCps,\n config.minCommitIntervalMs,\n config.minCps,\n config.settleAfterMs,\n config.settleDrainMaxMs,\n config.settleDrainMinMs,\n config.targetBufferMs,\n scheduleFrameWake,\n stopFrameLoop,\n ]);\n startFrameLoopRef.current = startFrameLoop;\n\n useEffect(() => {\n if (!enabled) {\n syncImmediate(content);\n return;\n }\n\n const prevTargetContent = targetContentRef.current;\n if (content === prevTargetContent) return;\n\n const now = getNow();\n const appendOnly = content.startsWith(prevTargetContent);\n\n if (!appendOnly) {\n syncImmediate(content);\n return;\n }\n\n // Bypass smoothing entirely while the input ends inside an open\n // fence whose language is opted out — see the preset config.\n // Without this, a 5 KB inline `<style>` block can keep `</style>`\n // (and therefore the HtmlPreview head-close trigger) trapped in the\n // smoother's buffer for tens of seconds, leaving the iframe pinned\n // on the loading placeholder while the artifact \"looks stuck\".\n if (config.bypassFencedLanguages.length > 0) {\n const openLang = findOpenFenceLanguage(content);\n if (openLang !== null && config.bypassFencedLanguages.includes(openLang)) {\n syncImmediate(content);\n return;\n }\n }\n\n const appended = content.slice(prevTargetContent.length);\n const appendedChars = [...appended];\n const appendedCount = appendedChars.length;\n\n profiler?.recordInputAppend({\n appendedChars: appendedCount,\n contentLength: countChars(content),\n });\n\n if (appendedCount > config.largeAppendChars) {\n syncImmediate(content);\n return;\n }\n\n targetContentRef.current = content;\n targetCharsRef.current.push(...appendedChars);\n targetCountRef.current += appendedCount;\n\n tailUnitsRef.current = findTailUnits(content);\n\n const deltaChars = targetCountRef.current - lastInputCountRef.current;\n const deltaMs = Math.max(1, now - lastInputTsRef.current);\n\n if (deltaChars > 0) {\n const instantCps = (deltaChars * 1000) / deltaMs;\n const normalizedInstantCps = clamp(instantCps, config.minCps, config.maxFlushCps * 2);\n const chunkEmaAlpha = 0.35;\n chunkSizeEmaRef.current =\n chunkSizeEmaRef.current * (1 - chunkEmaAlpha) + appendedCount * chunkEmaAlpha;\n arrivalCpsEmaRef.current =\n arrivalCpsEmaRef.current * (1 - chunkEmaAlpha) + normalizedInstantCps * chunkEmaAlpha;\n\n const clampedCps = clamp(instantCps, config.minCps, config.maxActiveCps);\n emaCpsRef.current = emaCpsRef.current * (1 - config.emaAlpha) + clampedCps * config.emaAlpha;\n }\n\n lastInputTsRef.current = now;\n lastInputCountRef.current = targetCountRef.current;\n\n startFrameLoop();\n }, [\n config.bypassFencedLanguages,\n config.emaAlpha,\n config.largeAppendChars,\n config.maxActiveCps,\n config.maxCps,\n config.maxFlushCps,\n config.minCps,\n content,\n enabled,\n startFrameLoop,\n syncImmediate,\n profiler,\n ]);\n\n useEffect(() => {\n return () => {\n stopScheduling();\n };\n }, [stopScheduling]);\n\n return displayedContent;\n};\n"],"mappings":"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"}
|
|
@@ -1,12 +1,9 @@
|
|
|
1
|
+
import { countChars } from "./useSmoothStreamContent.mjs";
|
|
1
2
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
3
|
//#region src/Markdown/SyntaxMarkdown/useStreamQueue.ts
|
|
3
4
|
const BASE_DELAY = 18;
|
|
4
5
|
const ACCELERATION_FACTOR = .3;
|
|
5
6
|
const MAX_BLOCK_DURATION = 3e3;
|
|
6
|
-
const FADE_DURATION = 280;
|
|
7
|
-
function countChars(text) {
|
|
8
|
-
return [...text].length;
|
|
9
|
-
}
|
|
10
7
|
function computeCharDelay(queueLength, charCount) {
|
|
11
8
|
let delay = BASE_DELAY / (1 + queueLength * ACCELERATION_FACTOR);
|
|
12
9
|
delay = Math.min(delay, MAX_BLOCK_DURATION / Math.max(charCount, 1));
|
|
@@ -64,7 +61,7 @@ function useStreamQueue(blocks) {
|
|
|
64
61
|
timerRef.current = null;
|
|
65
62
|
}
|
|
66
63
|
if (animatingIndex < 0) return;
|
|
67
|
-
const totalTime = Math.max(0, (animatingCharCount - 1) * charDelay) +
|
|
64
|
+
const totalTime = Math.max(0, (animatingCharCount - 1) * charDelay) + 180;
|
|
68
65
|
timerRef.current = setTimeout(onAnimationDone, totalTime);
|
|
69
66
|
return () => {
|
|
70
67
|
if (timerRef.current) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useStreamQueue.mjs","names":[],"sources":["../../../src/Markdown/SyntaxMarkdown/useStreamQueue.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\n\nexport interface BlockInfo {\n content: string;\n startOffset: number;\n}\n\nexport type BlockState = 'revealed' | 'animating' | 'streaming' | 'queued';\n\nconst BASE_DELAY = 18;\nconst ACCELERATION_FACTOR = 0.3;\nconst MAX_BLOCK_DURATION = 3000;\
|
|
1
|
+
{"version":3,"file":"useStreamQueue.mjs","names":[],"sources":["../../../src/Markdown/SyntaxMarkdown/useStreamQueue.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\n\nimport { STREAM_FADE_DURATION } from './style';\nimport { countChars } from './useSmoothStreamContent';\n\nexport interface BlockInfo {\n content: string;\n startOffset: number;\n}\n\nexport type BlockState = 'revealed' | 'animating' | 'streaming' | 'queued';\n\nconst BASE_DELAY = 18;\nconst ACCELERATION_FACTOR = 0.3;\nconst MAX_BLOCK_DURATION = 3000;\n\nfunction computeCharDelay(queueLength: number, charCount: number): number {\n const acceleration = 1 + queueLength * ACCELERATION_FACTOR;\n let delay = BASE_DELAY / acceleration;\n delay = Math.min(delay, MAX_BLOCK_DURATION / Math.max(charCount, 1));\n return delay;\n}\n\nexport interface UseStreamQueueReturn {\n charDelay: number;\n getBlockState: (index: number) => BlockState;\n queueLength: number;\n}\n\nexport function useStreamQueue(blocks: BlockInfo[]): UseStreamQueueReturn {\n const [revealedCount, setRevealedCount] = useState(0);\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const prevBlocksLenRef = useRef(0);\n const minRevealedRef = useRef(0);\n\n // Synchronous auto-reveal during render.\n // When blocks grow, the previous tail (streaming block) is instantly\n // promoted to revealed — its chars are already visible via stream-mode\n // animation. This runs during render (not in effect) so there is NO\n // intermediate frame where the old streaming block enters 'animating'\n // state and gets stagger plugins that would restart its animations.\n if (blocks.length === 0 && prevBlocksLenRef.current !== 0) {\n minRevealedRef.current = 0;\n }\n if (blocks.length > prevBlocksLenRef.current && prevBlocksLenRef.current > 0) {\n const prevTail = prevBlocksLenRef.current - 1;\n minRevealedRef.current = Math.max(minRevealedRef.current, prevTail + 1);\n }\n prevBlocksLenRef.current = blocks.length;\n\n // State reset when stream restarts (blocks empty)\n useEffect(() => {\n if (blocks.length === 0) {\n setRevealedCount(0);\n minRevealedRef.current = 0;\n if (timerRef.current) {\n clearTimeout(timerRef.current);\n timerRef.current = null;\n }\n }\n }, [blocks.length]);\n\n const effectiveRevealedCount = Math.max(revealedCount, minRevealedRef.current);\n const tailIndex = blocks.length - 1;\n\n const getBlockState = useCallback(\n (index: number): BlockState => {\n if (index < effectiveRevealedCount) return 'revealed';\n if (index === effectiveRevealedCount && index < tailIndex) return 'animating';\n if (index === effectiveRevealedCount && index === tailIndex) return 'streaming';\n return 'queued';\n },\n [effectiveRevealedCount, tailIndex],\n );\n\n const queueLength = Math.max(0, tailIndex - effectiveRevealedCount - 1);\n\n const animatingIndex = effectiveRevealedCount < tailIndex ? effectiveRevealedCount : -1;\n const animatingCharCount =\n animatingIndex >= 0 ? countChars(blocks[animatingIndex]?.content ?? '') : 0;\n\n const streamingIndex = animatingIndex < 0 && tailIndex >= effectiveRevealedCount ? tailIndex : -1;\n const activeIndex = animatingIndex >= 0 ? animatingIndex : streamingIndex;\n const activeCharCount = activeIndex >= 0 ? countChars(blocks[activeIndex]?.content ?? '') : 0;\n\n // Freeze charDelay when entering a new active block (animating or streaming)\n const frozenRef = useRef({ delay: BASE_DELAY, index: -1 });\n if (activeIndex >= 0 && activeIndex !== frozenRef.current.index) {\n frozenRef.current = {\n delay: computeCharDelay(queueLength, activeCharCount),\n index: activeIndex,\n };\n }\n const charDelay = activeIndex >= 0 ? frozenRef.current.delay : BASE_DELAY;\n\n const onAnimationDone = useCallback(() => {\n setRevealedCount(effectiveRevealedCount + 1);\n }, [effectiveRevealedCount]);\n\n useEffect(() => {\n if (timerRef.current) {\n clearTimeout(timerRef.current);\n timerRef.current = null;\n }\n\n if (animatingIndex < 0) return;\n\n const totalTime = Math.max(0, (animatingCharCount - 1) * charDelay) + STREAM_FADE_DURATION;\n timerRef.current = setTimeout(onAnimationDone, totalTime);\n\n return () => {\n if (timerRef.current) {\n clearTimeout(timerRef.current);\n timerRef.current = null;\n }\n };\n }, [animatingIndex, animatingCharCount, charDelay, onAnimationDone]);\n\n return { charDelay, getBlockState, queueLength };\n}\n"],"mappings":";;;AAYA,MAAM,aAAa;AACnB,MAAM,sBAAsB;AAC5B,MAAM,qBAAqB;AAE3B,SAAS,iBAAiB,aAAqB,WAA2B;CAExE,IAAI,QAAQ,cADS,IAAI,cAAc;AAEvC,SAAQ,KAAK,IAAI,OAAO,qBAAqB,KAAK,IAAI,WAAW,EAAE,CAAC;AACpE,QAAO;;AAST,SAAgB,eAAe,QAA2C;CACxE,MAAM,CAAC,eAAe,oBAAoB,SAAS,EAAE;CACrD,MAAM,WAAW,OAA6C,KAAK;CACnE,MAAM,mBAAmB,OAAO,EAAE;CAClC,MAAM,iBAAiB,OAAO,EAAE;AAQhC,KAAI,OAAO,WAAW,KAAK,iBAAiB,YAAY,EACtD,gBAAe,UAAU;AAE3B,KAAI,OAAO,SAAS,iBAAiB,WAAW,iBAAiB,UAAU,GAAG;EAC5E,MAAM,WAAW,iBAAiB,UAAU;AAC5C,iBAAe,UAAU,KAAK,IAAI,eAAe,SAAS,WAAW,EAAE;;AAEzE,kBAAiB,UAAU,OAAO;AAGlC,iBAAgB;AACd,MAAI,OAAO,WAAW,GAAG;AACvB,oBAAiB,EAAE;AACnB,kBAAe,UAAU;AACzB,OAAI,SAAS,SAAS;AACpB,iBAAa,SAAS,QAAQ;AAC9B,aAAS,UAAU;;;IAGtB,CAAC,OAAO,OAAO,CAAC;CAEnB,MAAM,yBAAyB,KAAK,IAAI,eAAe,eAAe,QAAQ;CAC9E,MAAM,YAAY,OAAO,SAAS;CAElC,MAAM,gBAAgB,aACnB,UAA8B;AAC7B,MAAI,QAAQ,uBAAwB,QAAO;AAC3C,MAAI,UAAU,0BAA0B,QAAQ,UAAW,QAAO;AAClE,MAAI,UAAU,0BAA0B,UAAU,UAAW,QAAO;AACpE,SAAO;IAET,CAAC,wBAAwB,UAAU,CACpC;CAED,MAAM,cAAc,KAAK,IAAI,GAAG,YAAY,yBAAyB,EAAE;CAEvE,MAAM,iBAAiB,yBAAyB,YAAY,yBAAyB;CACrF,MAAM,qBACJ,kBAAkB,IAAI,WAAW,OAAO,iBAAiB,WAAW,GAAG,GAAG;CAG5E,MAAM,cAAc,kBAAkB,IAAI,iBADnB,iBAAiB,KAAK,aAAa,yBAAyB,YAAY;CAE/F,MAAM,kBAAkB,eAAe,IAAI,WAAW,OAAO,cAAc,WAAW,GAAG,GAAG;CAG5F,MAAM,YAAY,OAAO;EAAE,OAAO;EAAY,OAAO;EAAI,CAAC;AAC1D,KAAI,eAAe,KAAK,gBAAgB,UAAU,QAAQ,MACxD,WAAU,UAAU;EAClB,OAAO,iBAAiB,aAAa,gBAAgB;EACrD,OAAO;EACR;CAEH,MAAM,YAAY,eAAe,IAAI,UAAU,QAAQ,QAAQ;CAE/D,MAAM,kBAAkB,kBAAkB;AACxC,mBAAiB,yBAAyB,EAAE;IAC3C,CAAC,uBAAuB,CAAC;AAE5B,iBAAgB;AACd,MAAI,SAAS,SAAS;AACpB,gBAAa,SAAS,QAAQ;AAC9B,YAAS,UAAU;;AAGrB,MAAI,iBAAiB,EAAG;EAExB,MAAM,YAAY,KAAK,IAAI,IAAI,qBAAqB,KAAK,UAAU,GAAA;AACnE,WAAS,UAAU,WAAW,iBAAiB,UAAU;AAEzD,eAAa;AACX,OAAI,SAAS,SAAS;AACpB,iBAAa,SAAS,QAAQ;AAC9B,aAAS,UAAU;;;IAGtB;EAAC;EAAgB;EAAoB;EAAW;EAAgB,CAAC;AAEpE,QAAO;EAAE;EAAW;EAAe;EAAa"}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import { useStableValue } from "../../hooks/useStableValue.mjs";
|
|
2
3
|
import { createContext, memo, use } from "react";
|
|
3
4
|
import { jsx } from "react/jsx-runtime";
|
|
4
5
|
//#region src/Markdown/components/MarkdownProvider.tsx
|
|
5
6
|
const MarkdownContext = createContext({});
|
|
6
7
|
const MarkdownProvider = memo(({ children, ...config }) => {
|
|
7
8
|
return /* @__PURE__ */ jsx(MarkdownContext, {
|
|
8
|
-
value: config,
|
|
9
|
+
value: useStableValue(config),
|
|
9
10
|
children
|
|
10
11
|
});
|
|
11
12
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MarkdownProvider.mjs","names":[],"sources":["../../../src/Markdown/components/MarkdownProvider.tsx"],"sourcesContent":["'use client';\n\nimport { createContext, memo, type PropsWithChildren, use } from 'react';\n\nimport { type SyntaxMarkdownProps } from '../type';\n\nexport type MarkdownContentConfig = Omit<SyntaxMarkdownProps, 'children' | 'reactMarkdownProps'>;\n\nexport const MarkdownContext = createContext<MarkdownContentConfig>({});\n\nexport const MarkdownProvider = memo<PropsWithChildren<MarkdownContentConfig>>(\n ({ children, ...config }) => {\n return <MarkdownContext value={
|
|
1
|
+
{"version":3,"file":"MarkdownProvider.mjs","names":[],"sources":["../../../src/Markdown/components/MarkdownProvider.tsx"],"sourcesContent":["'use client';\n\nimport { createContext, memo, type PropsWithChildren, use } from 'react';\n\nimport { useStableValue } from '@/hooks/useStableValue';\n\nimport { type SyntaxMarkdownProps } from '../type';\n\nexport type MarkdownContentConfig = Omit<SyntaxMarkdownProps, 'children' | 'reactMarkdownProps'>;\n\nexport const MarkdownContext = createContext<MarkdownContentConfig>({});\n\nexport const MarkdownProvider = memo<PropsWithChildren<MarkdownContentConfig>>(\n ({ children, ...config }) => {\n // The rest-spread builds a fresh object on every render while `children`\n // changes on every streamed chunk, so without stabilisation each chunk\n // swaps the context identity and re-renders every consumer inside every\n // block, bypassing the per-block memo entirely.\n const stableConfig = useStableValue(config);\n\n return <MarkdownContext value={stableConfig}>{children}</MarkdownContext>;\n },\n);\n\nexport const useMarkdownContext = () => {\n return use(MarkdownContext);\n};\n"],"mappings":";;;;;AAUA,MAAa,kBAAkB,cAAqC,EAAE,CAAC;AAEvE,MAAa,mBAAmB,MAC7B,EAAE,UAAU,GAAG,aAAa;AAO3B,QAAO,oBAAC,iBAAD;EAAiB,OAFH,eAAe,OAEO;EAAG;EAA2B,CAAA;EAE5E;AAED,MAAa,2BAA2B;AACtC,QAAO,IAAI,gBAAgB"}
|
package/es/Markdown/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MarkdownProps, StreamSmoothingPreset, SyntaxMarkdownProps, TypographyProps } from "./type.mjs";
|
|
1
|
+
import { MarkdownProps, StreamAnimationGranularity, StreamSmoothingPreset, SyntaxMarkdownProps, TypographyProps } from "./type.mjs";
|
|
2
2
|
import { Markdown } from "./Markdown.mjs";
|
|
3
3
|
import { Typography } from "./Typography.mjs";
|
|
4
|
-
export { MarkdownProps, StreamSmoothingPreset, SyntaxMarkdownProps, Typography, TypographyProps, Markdown as default };
|
|
4
|
+
export { MarkdownProps, StreamAnimationGranularity, StreamSmoothingPreset, SyntaxMarkdownProps, Typography, TypographyProps, Markdown as default };
|
|
@@ -1,11 +1,32 @@
|
|
|
1
1
|
import { Root } from "../../node_modules/@types/hast/index.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/Markdown/plugins/rehypeStreamAnimated.d.ts
|
|
4
|
+
interface StreamAnimatedRuntime {
|
|
5
|
+
births: number[];
|
|
6
|
+
/**
|
|
7
|
+
* Write-once per-char render cache, indexed like `births`:
|
|
8
|
+
* `undefined` = char not rendered yet, `null` = born fully revealed,
|
|
9
|
+
* string = inline style frozen at first render.
|
|
10
|
+
* Freezing the style keeps span props referentially stable across the
|
|
11
|
+
* tail block's re-renders, so React never rewrites `animation-delay`
|
|
12
|
+
* on an in-flight fade (a rewrite restarts the CSS animation).
|
|
13
|
+
*/
|
|
14
|
+
styles: (string | null | undefined)[];
|
|
15
|
+
}
|
|
4
16
|
interface StreamAnimatedOptions {
|
|
5
17
|
births?: number[];
|
|
6
18
|
fadeDuration?: number;
|
|
19
|
+
/**
|
|
20
|
+
* `'word'` wraps whitespace-delimited runs in one span instead of one
|
|
21
|
+
* span per char. Every concurrent CSS animation keeps the compositor
|
|
22
|
+
* producing frames and fires animationstart/end through React's root
|
|
23
|
+
* event delegation, so animating ~5x fewer nodes is the main CPU lever —
|
|
24
|
+
* char-level remains available for the finer-grained look.
|
|
25
|
+
*/
|
|
26
|
+
granularity?: 'char' | 'word';
|
|
7
27
|
nowMs?: number;
|
|
8
28
|
revealed?: boolean;
|
|
29
|
+
runtime?: StreamAnimatedRuntime;
|
|
9
30
|
}
|
|
10
31
|
declare const rehypeStreamAnimated: (options?: StreamAnimatedOptions) => (tree: Root) => void;
|
|
11
32
|
//#endregion
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { visit } from "../../node_modules/unist-util-visit/lib/index.mjs";
|
|
2
|
+
import { getNow } from "../../utils/getNow.mjs";
|
|
2
3
|
//#region src/Markdown/plugins/rehypeStreamAnimated.ts
|
|
4
|
+
const WORD_SEGMENT_RE = /\s+|\S+/g;
|
|
5
|
+
const wordSegmenter = typeof Intl !== "undefined" && "Segmenter" in Intl ? new Intl.Segmenter(void 0, { granularity: "word" }) : null;
|
|
6
|
+
const segmentWords = (value) => {
|
|
7
|
+
if (!wordSegmenter) return value.match(WORD_SEGMENT_RE) ?? [];
|
|
8
|
+
const segments = [];
|
|
9
|
+
for (const item of wordSegmenter.segment(value)) segments.push(item.segment);
|
|
10
|
+
return segments;
|
|
11
|
+
};
|
|
3
12
|
const BLOCK_TAGS = new Set([
|
|
4
13
|
"p",
|
|
5
14
|
"h1",
|
|
@@ -23,39 +32,66 @@ function hasClass(node, cls) {
|
|
|
23
32
|
return false;
|
|
24
33
|
}
|
|
25
34
|
const rehypeStreamAnimated = (options = {}) => {
|
|
26
|
-
const { births, fadeDuration = 150, nowMs, revealed = false } = options;
|
|
27
|
-
const
|
|
35
|
+
const { births, fadeDuration = 150, granularity = "char", nowMs, revealed = false, runtime } = options;
|
|
36
|
+
const resolvedRuntime = revealed ? void 0 : runtime ?? (Array.isArray(births) && typeof nowMs === "number" ? {
|
|
37
|
+
births,
|
|
38
|
+
styles: []
|
|
39
|
+
} : void 0);
|
|
40
|
+
const nowOverride = runtime ? void 0 : nowMs;
|
|
28
41
|
return (tree) => {
|
|
29
42
|
let globalCharIndex = 0;
|
|
43
|
+
const now = nowOverride ?? (resolvedRuntime ? getNow() : 0);
|
|
30
44
|
const shouldSkip = (node) => {
|
|
31
45
|
return SKIP_TAGS.has(node.tagName) || hasClass(node, "katex");
|
|
32
46
|
};
|
|
47
|
+
const resolveStyle = (index) => {
|
|
48
|
+
const styles = resolvedRuntime.styles;
|
|
49
|
+
const cached = styles[index];
|
|
50
|
+
if (cached !== void 0) return cached;
|
|
51
|
+
const birthTs = resolvedRuntime.births[index];
|
|
52
|
+
let resolved;
|
|
53
|
+
if (birthTs === void 0) resolved = null;
|
|
54
|
+
else {
|
|
55
|
+
const elapsed = now - birthTs;
|
|
56
|
+
resolved = elapsed >= fadeDuration ? null : `animation-delay:${-elapsed}ms`;
|
|
57
|
+
}
|
|
58
|
+
styles[index] = resolved;
|
|
59
|
+
return resolved;
|
|
60
|
+
};
|
|
61
|
+
const buildSpan = (value, startIndex) => {
|
|
62
|
+
let className = "stream-char";
|
|
63
|
+
let style;
|
|
64
|
+
if (revealed) className = "stream-char stream-char-revealed";
|
|
65
|
+
else if (resolvedRuntime) {
|
|
66
|
+
const resolved = resolveStyle(startIndex);
|
|
67
|
+
if (resolved === null) className = "stream-char stream-char-revealed";
|
|
68
|
+
else style = resolved;
|
|
69
|
+
}
|
|
70
|
+
const properties = { className };
|
|
71
|
+
if (style !== void 0) properties.style = style;
|
|
72
|
+
return {
|
|
73
|
+
children: [{
|
|
74
|
+
type: "text",
|
|
75
|
+
value
|
|
76
|
+
}],
|
|
77
|
+
properties,
|
|
78
|
+
tagName: "span",
|
|
79
|
+
type: "element"
|
|
80
|
+
};
|
|
81
|
+
};
|
|
33
82
|
const wrapText = (node) => {
|
|
34
83
|
const newChildren = [];
|
|
35
|
-
for (const child of node.children) if (child.type === "text") for (const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (birthTs === void 0) className = "stream-char stream-char-revealed";
|
|
42
|
-
else {
|
|
43
|
-
const elapsed = nowMs - birthTs;
|
|
44
|
-
if (elapsed >= fadeDuration) className = "stream-char stream-char-revealed";
|
|
45
|
-
else delay = -elapsed;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
const properties = { className };
|
|
49
|
-
if (delay !== void 0 && delay !== 0) properties.style = `animation-delay:${delay}ms`;
|
|
50
|
-
newChildren.push({
|
|
51
|
-
children: [{
|
|
52
|
-
type: "text",
|
|
53
|
-
value: char
|
|
54
|
-
}],
|
|
55
|
-
properties,
|
|
56
|
-
tagName: "span",
|
|
57
|
-
type: "element"
|
|
84
|
+
for (const child of node.children) if (child.type === "text") if (granularity === "word") for (const segment of segmentWords(child.value)) {
|
|
85
|
+
const startIndex = globalCharIndex;
|
|
86
|
+
for (const _char of segment) globalCharIndex++;
|
|
87
|
+
if (segment.trim() === "") newChildren.push({
|
|
88
|
+
type: "text",
|
|
89
|
+
value: segment
|
|
58
90
|
});
|
|
91
|
+
else newChildren.push(buildSpan(segment, startIndex));
|
|
92
|
+
}
|
|
93
|
+
else for (const char of child.value) {
|
|
94
|
+
newChildren.push(buildSpan(char, globalCharIndex));
|
|
59
95
|
globalCharIndex++;
|
|
60
96
|
}
|
|
61
97
|
else if (child.type === "element") {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rehypeStreamAnimated.mjs","names":[],"sources":["../../../src/Markdown/plugins/rehypeStreamAnimated.ts"],"sourcesContent":["import { type Element, type ElementContent, type Root } from 'hast';\nimport { type BuildVisitor } from 'unist-util-visit';\nimport { visit } from 'unist-util-visit';\n\nexport interface StreamAnimatedOptions {\n births?: number[];\n fadeDuration?: number;\n nowMs?: number;\n revealed?: boolean;\n}\n\nconst BLOCK_TAGS = new Set(['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li']);\nconst SKIP_TAGS = new Set(['pre', 'code', 'table', 'svg']);\n\nfunction hasClass(node: Element, cls: string): boolean {\n const cn = node.properties?.className;\n if (Array.isArray(cn)) return cn.some((c) => String(c).includes(cls));\n if (typeof cn === 'string') return cn.includes(cls);\n return false;\n}\n\nexport const rehypeStreamAnimated = (options: StreamAnimatedOptions = {}) => {\n const {
|
|
1
|
+
{"version":3,"file":"rehypeStreamAnimated.mjs","names":[],"sources":["../../../src/Markdown/plugins/rehypeStreamAnimated.ts"],"sourcesContent":["import { type Element, type ElementContent, type Root } from 'hast';\nimport { type BuildVisitor } from 'unist-util-visit';\nimport { visit } from 'unist-util-visit';\n\nimport { getNow } from '@/utils/getNow';\n\nexport interface StreamAnimatedRuntime {\n births: number[];\n /**\n * Write-once per-char render cache, indexed like `births`:\n * `undefined` = char not rendered yet, `null` = born fully revealed,\n * string = inline style frozen at first render.\n * Freezing the style keeps span props referentially stable across the\n * tail block's re-renders, so React never rewrites `animation-delay`\n * on an in-flight fade (a rewrite restarts the CSS animation).\n */\n styles: (string | null | undefined)[];\n}\n\nexport interface StreamAnimatedOptions {\n births?: number[];\n fadeDuration?: number;\n /**\n * `'word'` wraps whitespace-delimited runs in one span instead of one\n * span per char. Every concurrent CSS animation keeps the compositor\n * producing frames and fires animationstart/end through React's root\n * event delegation, so animating ~5x fewer nodes is the main CPU lever —\n * char-level remains available for the finer-grained look.\n */\n granularity?: 'char' | 'word';\n nowMs?: number;\n revealed?: boolean;\n runtime?: StreamAnimatedRuntime;\n}\n\n// Intl.Segmenter splits CJK runs into words too — the whitespace regex\n// fallback would otherwise fade an entire unspaced CJK paragraph as one\n// unit.\nconst WORD_SEGMENT_RE = /\\s+|\\S+/g;\n\nconst wordSegmenter =\n typeof Intl !== 'undefined' && 'Segmenter' in Intl\n ? new Intl.Segmenter(undefined, { granularity: 'word' })\n : null;\n\nconst segmentWords = (value: string): string[] => {\n if (!wordSegmenter) return value.match(WORD_SEGMENT_RE) ?? [];\n\n const segments: string[] = [];\n for (const item of wordSegmenter.segment(value)) {\n segments.push(item.segment);\n }\n return segments;\n};\n\nconst BLOCK_TAGS = new Set(['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li']);\nconst SKIP_TAGS = new Set(['pre', 'code', 'table', 'svg']);\n\nfunction hasClass(node: Element, cls: string): boolean {\n const cn = node.properties?.className;\n if (Array.isArray(cn)) return cn.some((c) => String(c).includes(cls));\n if (typeof cn === 'string') return cn.includes(cls);\n return false;\n}\n\nexport const rehypeStreamAnimated = (options: StreamAnimatedOptions = {}) => {\n const {\n births,\n fadeDuration = 150,\n granularity = 'char',\n nowMs,\n revealed = false,\n runtime,\n } = options;\n // Legacy births/nowMs callers share the runtime path through a throwaway\n // cache: the plugin factory runs once per render, so their styles are\n // recomputed against the caller's nowMs each run, exactly as before.\n const resolvedRuntime = revealed\n ? undefined\n : (runtime ??\n (Array.isArray(births) && typeof nowMs === 'number' ? { births, styles: [] } : undefined));\n const nowOverride = runtime ? undefined : nowMs;\n\n return (tree: Root) => {\n let globalCharIndex = 0;\n const now = nowOverride ?? (resolvedRuntime ? getNow() : 0);\n\n const shouldSkip = (node: Element): boolean => {\n return SKIP_TAGS.has(node.tagName) || hasClass(node, 'katex');\n };\n\n const resolveStyle = (index: number): string | null => {\n const styles = resolvedRuntime!.styles;\n const cached = styles[index];\n if (cached !== undefined) return cached;\n\n const birthTs = resolvedRuntime!.births[index];\n let resolved: string | null;\n if (birthTs === undefined) {\n resolved = null;\n } else {\n const elapsed = now - birthTs;\n // Negative delay = already elapsed ms into the fade. Positive\n // delay = not started yet (char born in the future, i.e.\n // staggered within the same commit).\n resolved = elapsed >= fadeDuration ? null : `animation-delay:${-elapsed}ms`;\n }\n styles[index] = resolved;\n return resolved;\n };\n\n const buildSpan = (value: string, startIndex: number): ElementContent => {\n let className = 'stream-char';\n let style: string | undefined;\n\n if (revealed) {\n className = 'stream-char stream-char-revealed';\n } else if (resolvedRuntime) {\n const resolved = resolveStyle(startIndex);\n if (resolved === null) {\n className = 'stream-char stream-char-revealed';\n } else {\n style = resolved;\n }\n }\n\n const properties: Record<string, any> = { className };\n if (style !== undefined) {\n properties.style = style;\n }\n return {\n children: [{ type: 'text', value }],\n properties,\n tagName: 'span',\n type: 'element',\n };\n };\n\n const wrapText = (node: Element) => {\n const newChildren: ElementContent[] = [];\n for (const child of node.children) {\n if (child.type === 'text') {\n if (granularity === 'word') {\n for (const segment of segmentWords(child.value)) {\n const startIndex = globalCharIndex;\n for (const _char of segment) globalCharIndex++;\n\n if (segment.trim() === '') {\n newChildren.push({ type: 'text', value: segment });\n } else {\n newChildren.push(buildSpan(segment, startIndex));\n }\n }\n } else {\n for (const char of child.value) {\n newChildren.push(buildSpan(char, globalCharIndex));\n globalCharIndex++;\n }\n }\n } else if (child.type === 'element') {\n if (!shouldSkip(child)) {\n wrapText(child);\n }\n newChildren.push(child);\n } else {\n newChildren.push(child);\n }\n }\n node.children = newChildren;\n };\n\n visit(tree, 'element', ((node: Element) => {\n if (shouldSkip(node)) return 'skip';\n if (BLOCK_TAGS.has(node.tagName)) {\n wrapText(node);\n return 'skip';\n }\n }) as BuildVisitor<Root, 'element'>);\n };\n};\n"],"mappings":"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"}
|
package/es/Markdown/type.d.mts
CHANGED
|
@@ -21,6 +21,7 @@ interface TypographyProps extends DivProps {
|
|
|
21
21
|
ref?: Ref<HTMLDivElement>;
|
|
22
22
|
}
|
|
23
23
|
type StreamSmoothingPreset = 'realtime' | 'balanced' | 'silky';
|
|
24
|
+
type StreamAnimationGranularity = 'char' | 'word';
|
|
24
25
|
interface SyntaxMarkdownProps {
|
|
25
26
|
allowHtml?: boolean;
|
|
26
27
|
allowHtmlList?: ElementType[];
|
|
@@ -50,6 +51,7 @@ interface SyntaxMarkdownProps {
|
|
|
50
51
|
remarkPlugins?: Pluggable[];
|
|
51
52
|
remarkPluginsAhead?: Pluggable[];
|
|
52
53
|
showFootnotes?: boolean;
|
|
54
|
+
streamAnimationGranularity?: StreamAnimationGranularity;
|
|
53
55
|
streamSmoothingPreset?: StreamSmoothingPreset;
|
|
54
56
|
variant?: 'default' | 'chat';
|
|
55
57
|
}
|
|
@@ -64,5 +66,5 @@ interface MarkdownProps extends SyntaxMarkdownProps, Omit<TypographyProps, 'chil
|
|
|
64
66
|
style?: CSSProperties;
|
|
65
67
|
}
|
|
66
68
|
//#endregion
|
|
67
|
-
export { MarkdownProps, StreamSmoothingPreset, SyntaxMarkdownProps, TypographyProps };
|
|
69
|
+
export { MarkdownProps, StreamAnimationGranularity, StreamSmoothingPreset, SyntaxMarkdownProps, TypographyProps };
|
|
68
70
|
//# sourceMappingURL=type.d.mts.map
|
|
@@ -30,7 +30,7 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
|
|
|
30
30
|
height: 32px;
|
|
31
31
|
padding: 0;
|
|
32
32
|
border: none;
|
|
33
|
-
border-radius:
|
|
33
|
+
border-radius: 12px;
|
|
34
34
|
|
|
35
35
|
color: ${cssVar.colorTextTertiary};
|
|
36
36
|
|
|
@@ -60,7 +60,7 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
|
|
|
60
60
|
height: 32px;
|
|
61
61
|
padding: 0;
|
|
62
62
|
border: none;
|
|
63
|
-
border-radius:
|
|
63
|
+
border-radius: 12px;
|
|
64
64
|
|
|
65
65
|
color: ${cssVar.colorTextTertiary};
|
|
66
66
|
|
|
@@ -183,7 +183,7 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
|
|
|
183
183
|
height: 100dvh !important;
|
|
184
184
|
max-height: 100dvh !important;
|
|
185
185
|
border: none;
|
|
186
|
-
border-radius:
|
|
186
|
+
border-radius: 12px;
|
|
187
187
|
`,
|
|
188
188
|
fullscreenToggle: css`
|
|
189
189
|
cursor: pointer;
|
|
@@ -196,7 +196,7 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
|
|
|
196
196
|
height: 28px;
|
|
197
197
|
padding: 0;
|
|
198
198
|
border: none;
|
|
199
|
-
border-radius:
|
|
199
|
+
border-radius: 12px;
|
|
200
200
|
|
|
201
201
|
color: ${cssVar.colorTextTertiary};
|
|
202
202
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"style.mjs","names":[],"sources":["../../../src/base-ui/Modal/style.ts"],"sourcesContent":["import { createStaticStyles } from 'antd-style';\n\nexport const styles = createStaticStyles(({ css, cssVar }) => ({\n backdrop: css`\n position: fixed;\n z-index: 1200;\n inset: 0;\n\n background: color-mix(in srgb, ${cssVar.colorBgContainer} 60%, transparent);\n\n transition: opacity 180ms cubic-bezier(0.32, 0.72, 0, 1);\n\n &[data-starting-style],\n &[data-ending-style] {\n opacity: 0;\n }\n `,\n\n close: css`\n cursor: pointer;\n\n position: absolute;\n inset-block-start: 12px;\n inset-inline-end: 12px;\n\n display: flex;\n align-items: center;\n justify-content: center;\n\n width: 32px;\n height: 32px;\n padding: 0;\n border: none;\n border-radius:
|
|
1
|
+
{"version":3,"file":"style.mjs","names":[],"sources":["../../../src/base-ui/Modal/style.ts"],"sourcesContent":["import { createStaticStyles } from 'antd-style';\n\nexport const styles = createStaticStyles(({ css, cssVar }) => ({\n backdrop: css`\n position: fixed;\n z-index: 1200;\n inset: 0;\n\n background: color-mix(in srgb, ${cssVar.colorBgContainer} 60%, transparent);\n\n transition: opacity 180ms cubic-bezier(0.32, 0.72, 0, 1);\n\n &[data-starting-style],\n &[data-ending-style] {\n opacity: 0;\n }\n `,\n\n close: css`\n cursor: pointer;\n\n position: absolute;\n inset-block-start: 12px;\n inset-inline-end: 12px;\n\n display: flex;\n align-items: center;\n justify-content: center;\n\n width: 32px;\n height: 32px;\n padding: 0;\n border: none;\n border-radius: 12px;\n\n color: ${cssVar.colorTextTertiary};\n\n background: transparent;\n\n transition: all 160ms cubic-bezier(0.32, 0.72, 0, 1);\n\n &:hover {\n transform: scale(1.04);\n color: ${cssVar.colorText};\n background: ${cssVar.colorFillSecondary};\n }\n\n &:focus-visible {\n outline: none;\n box-shadow: 0 0 0 2px ${cssVar.colorPrimaryBorder};\n }\n `,\n\n closeInline: css`\n cursor: pointer;\n\n display: flex;\n align-items: center;\n justify-content: center;\n\n width: 32px;\n height: 32px;\n padding: 0;\n border: none;\n border-radius: 12px;\n\n color: ${cssVar.colorTextTertiary};\n\n background: transparent;\n\n transition: all 160ms cubic-bezier(0.32, 0.72, 0, 1);\n\n &:hover {\n transform: scale(1.04);\n color: ${cssVar.colorText};\n background: ${cssVar.colorFillSecondary};\n }\n\n &:focus-visible {\n outline: none;\n box-shadow: 0 0 0 2px ${cssVar.colorPrimaryBorder};\n }\n `,\n\n content: css`\n overflow: hidden auto;\n padding-block: 12px;\n padding-inline: 16px;\n `,\n\n footer: css`\n display: flex;\n gap: 8px;\n align-items: center;\n justify-content: flex-end;\n\n padding-block: 12px;\n padding-inline: 16px;\n `,\n\n header: css`\n display: flex;\n align-items: center;\n justify-content: space-between;\n\n padding-block: 12px;\n padding-inline: 16px;\n `,\n\n headerDraggable: css`\n cursor: default;\n user-select: none;\n `,\n\n popup: css`\n pointer-events: none;\n\n position: fixed;\n z-index: 1201;\n inset: 0;\n\n display: flex;\n align-items: center;\n justify-content: center;\n `,\n\n popupInner: css`\n pointer-events: auto;\n\n position: relative;\n\n overflow: hidden;\n display: flex;\n flex-direction: column;\n\n box-sizing: border-box;\n width: calc(100% - 32px);\n max-width: 520px;\n max-height: calc(100dvh - 64px);\n border: 1px solid ${cssVar.colorBorderSecondary};\n border-radius: 12px;\n\n background: ${cssVar.colorBgElevated};\n box-shadow: ${cssVar.boxShadow};\n\n transition:\n transform 220ms cubic-bezier(0.32, 0.72, 0, 1),\n opacity 220ms cubic-bezier(0.32, 0.72, 0, 1);\n\n &[data-starting-style] {\n transform: scale(0.97);\n opacity: 0;\n }\n\n &[data-ending-style] {\n transform: scale(0.98);\n opacity: 0;\n transition-timing-function: cubic-bezier(0.4, 0, 1, 1);\n transition-duration: 120ms;\n }\n `,\n\n title: css`\n margin: 0;\n\n font-size: 17px;\n font-weight: 600;\n line-height: 1.4;\n color: ${cssVar.colorText};\n letter-spacing: -0.005em;\n `,\n\n loadingSpinner: css`\n @keyframes modal-spin {\n to {\n transform: rotate(360deg);\n }\n }\n\n display: inline-block;\n\n width: 14px;\n height: 14px;\n border: 2px solid currentcolor;\n border-block-start-color: transparent;\n border-radius: 50%;\n\n animation: modal-spin 0.6s linear infinite;\n `,\n\n fullscreenPopupInner: css`\n width: 100% !important;\n max-width: 100% !important;\n height: 100dvh !important;\n max-height: 100dvh !important;\n border: none;\n border-radius: 12px;\n `,\n\n fullscreenToggle: css`\n cursor: pointer;\n\n display: flex;\n align-items: center;\n justify-content: center;\n\n width: 28px;\n height: 28px;\n padding: 0;\n border: none;\n border-radius: 12px;\n\n color: ${cssVar.colorTextTertiary};\n\n background: transparent;\n\n transition: all 160ms cubic-bezier(0.32, 0.72, 0, 1);\n\n &:hover {\n transform: scale(1.04);\n color: ${cssVar.colorText};\n background: ${cssVar.colorFillSecondary};\n }\n\n &:focus-visible {\n outline: none;\n box-shadow: 0 0 0 2px ${cssVar.colorPrimaryBorder};\n }\n `,\n\n headerActions: css`\n display: flex;\n gap: 4px;\n align-items: center;\n margin-inline-end: -4px;\n `,\n\n denyAnimation: css`\n @keyframes modal-deny {\n 0%,\n 100% {\n transform: translateX(0);\n }\n\n 20% {\n transform: translateX(-5px);\n }\n\n 40% {\n transform: translateX(5px);\n }\n\n 60% {\n transform: translateX(-3px);\n }\n\n 80% {\n transform: translateX(2px);\n }\n }\n\n animation: modal-deny 280ms cubic-bezier(0.36, 0.66, 0.04, 1);\n `,\n\n viewport: css`\n position: fixed;\n z-index: 1200;\n inset: 0;\n overflow: auto;\n `,\n}));\n"],"mappings":";;AAEA,MAAa,SAAS,oBAAoB,EAAE,KAAK,cAAc;CAC7D,UAAU,GAAG;;;;;qCAKsB,OAAO,iBAAiB;;;;;;;;;CAU3D,OAAO,GAAG;;;;;;;;;;;;;;;;;aAiBC,OAAO,kBAAkB;;;;;;;;eAQvB,OAAO,UAAU;oBACZ,OAAO,mBAAmB;;;;;8BAKhB,OAAO,mBAAmB;;;CAItD,aAAa,GAAG;;;;;;;;;;;;;aAaL,OAAO,kBAAkB;;;;;;;;eAQvB,OAAO,UAAU;oBACZ,OAAO,mBAAmB;;;;;8BAKhB,OAAO,mBAAmB;;;CAItD,SAAS,GAAG;;;;;CAMZ,QAAQ,GAAG;;;;;;;;;CAUX,QAAQ,GAAG;;;;;;;;CASX,iBAAiB,GAAG;;;;CAKpB,OAAO,GAAG;;;;;;;;;;;CAYV,YAAY,GAAG;;;;;;;;;;;;;wBAaO,OAAO,qBAAqB;;;kBAGlC,OAAO,gBAAgB;kBACvB,OAAO,UAAU;;;;;;;;;;;;;;;;;;CAmBjC,OAAO,GAAG;;;;;;aAMC,OAAO,UAAU;;;CAI5B,gBAAgB,GAAG;;;;;;;;;;;;;;;;;CAkBnB,sBAAsB,GAAG;;;;;;;;CASzB,kBAAkB,GAAG;;;;;;;;;;;;;aAaV,OAAO,kBAAkB;;;;;;;;eAQvB,OAAO,UAAU;oBACZ,OAAO,mBAAmB;;;;;8BAKhB,OAAO,mBAAmB;;;CAItD,eAAe,GAAG;;;;;;CAOlB,eAAe,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;CA2BlB,UAAU,GAAG;;;;;;CAMd,EAAE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getNow.mjs","names":[],"sources":["../../src/utils/getNow.ts"],"sourcesContent":["export const getNow = (): number => {\n return typeof performance === 'undefined' ? Date.now() : performance.now();\n};\n"],"mappings":";AAAA,MAAa,eAAuB;AAClC,QAAO,OAAO,gBAAgB,cAAc,KAAK,KAAK,GAAG,YAAY,KAAK"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/ui",
|
|
3
|
-
"version": "5.15.
|
|
3
|
+
"version": "5.15.17",
|
|
4
4
|
"description": "Lobe UI is an open-source UI component library for building AIGC web apps",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"lobehub",
|
|
@@ -162,6 +162,8 @@
|
|
|
162
162
|
"emoji-mart": "^5.6.0",
|
|
163
163
|
"es-toolkit": "^1.47.0",
|
|
164
164
|
"fast-deep-equal": "^3.1.3",
|
|
165
|
+
"hast-util-to-jsx-runtime": "^2.3.6",
|
|
166
|
+
"html-url-attributes": "^3.0.1",
|
|
165
167
|
"immer": "^11.1.8",
|
|
166
168
|
"katex": "^0.16.47",
|
|
167
169
|
"leva": "^0.10.1",
|
|
@@ -193,6 +195,8 @@
|
|
|
193
195
|
"remark-gfm": "^4.0.1",
|
|
194
196
|
"remark-github": "^12.0.0",
|
|
195
197
|
"remark-math": "^6.0.0",
|
|
198
|
+
"remark-parse": "^11.0.0",
|
|
199
|
+
"remark-rehype": "^11.1.2",
|
|
196
200
|
"remend": "^1.3.0",
|
|
197
201
|
"shiki": "^4.2.0",
|
|
198
202
|
"shiki-stream": "^0.1.5",
|
|
@@ -202,6 +206,7 @@
|
|
|
202
206
|
"url-join": "^5.0.0",
|
|
203
207
|
"use-merge-value": "^1.2.0",
|
|
204
208
|
"uuid": "^13.0.2",
|
|
209
|
+
"vfile": "^6.0.3",
|
|
205
210
|
"virtua": "^0.49.1"
|
|
206
211
|
},
|
|
207
212
|
"devDependencies": {
|