@lobehub/ui
Version:
Lobe UI is an open-source UI component library for building AIGC web apps
135 lines (132 loc) • 5.52 kB
JavaScript
'use client';
import { useMarkdownComponents } from "../../hooks/useMarkdown/useMarkdownComponents.mjs";
import { useMarkdownContent } from "../../hooks/useMarkdown/useMarkdownContent.mjs";
import { useMarkdownRehypePlugins } from "../../hooks/useMarkdown/useMarkdownRehypePlugins.mjs";
import { useMarkdownRemarkPlugins } from "../../hooks/useMarkdown/useMarkdownRemarkPlugins.mjs";
import { rehypeStreamAnimated } from "../plugins/rehypeStreamAnimated.mjs";
import { styles } from "./style.mjs";
import { useStreamQueue } from "./useStreamQueue.mjs";
import { createElement, memo, useEffect, useId, useMemo, useRef } from "react";
import { jsx } from "react/jsx-runtime";
import Markdown from "react-markdown";
import { marked } from "marked";
import remend from "remend";
//#region src/Markdown/SyntaxMarkdown/StreamdownRender.tsx
const STREAM_CHAR_DELAY = 15;
function countChars(text) {
return [...text].length;
}
const isRecord = (value) => typeof value === "object" && value !== null;
const isDeepEqualValue = (a, b) => {
if (a === b) return true;
if (Array.isArray(a) || Array.isArray(b)) {
if (!Array.isArray(a) || !Array.isArray(b)) return false;
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) if (!isDeepEqualValue(a[i], b[i])) return false;
return true;
}
if (!isRecord(a) || !isRecord(b)) return false;
const keysA = Object.keys(a);
const keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
for (const key of keysA) if (!isDeepEqualValue(a[key], b[key])) return false;
return true;
};
const isSamePlugin = (prevPlugin, nextPlugin) => {
const prevTuple = Array.isArray(prevPlugin) ? prevPlugin : [prevPlugin];
const nextTuple = Array.isArray(nextPlugin) ? nextPlugin : [nextPlugin];
if (prevTuple.length !== nextTuple.length) return false;
if (prevTuple[0] !== nextTuple[0]) return false;
return isDeepEqualValue(prevTuple.slice(1), nextTuple.slice(1));
};
const isSamePlugins = (prevPlugins, nextPlugins) => {
if (prevPlugins === nextPlugins) return true;
if (!prevPlugins || !nextPlugins) return !prevPlugins && !nextPlugins;
if (prevPlugins.length !== nextPlugins.length) return false;
for (let i = 0; i < prevPlugins.length; i++) if (!isSamePlugin(prevPlugins[i], nextPlugins[i])) return false;
return true;
};
const useStablePlugins = (plugins) => {
const stableRef = useRef(plugins);
if (!isSamePlugins(stableRef.current, plugins)) stableRef.current = plugins;
return stableRef.current;
};
const StreamdownBlock = memo(({ children, ...rest }) => {
return /* @__PURE__ */ jsx(Markdown, {
...rest,
children
});
}, (prevProps, nextProps) => prevProps.children === nextProps.children && prevProps.components === nextProps.components && isSamePlugins(prevProps.rehypePlugins, nextProps.rehypePlugins) && isSamePlugins(prevProps.remarkPlugins, nextProps.remarkPlugins));
StreamdownBlock.displayName = "StreamdownBlock";
const StreamdownRender = memo(({ children, ...rest }) => {
const escapedContent = useMarkdownContent(children || "");
const components = useMarkdownComponents();
const baseRehypePlugins = useStablePlugins(useMarkdownRehypePlugins());
const remarkPlugins = useStablePlugins(useMarkdownRemarkPlugins());
const generatedId = useId();
const processedContent = useMemo(() => {
return remend(typeof escapedContent === "string" ? escapedContent : "");
}, [escapedContent]);
const blocks = useMemo(() => {
const tokens = marked.lexer(processedContent);
let offset = 0;
return tokens.map((token) => {
const block = {
content: token.raw,
startOffset: offset
};
offset += token.raw.length;
return block;
});
}, [processedContent]);
const { getBlockState, charDelay } = useStreamQueue(blocks);
const staggerPlugins = useMemo(() => [...baseRehypePlugins, [rehypeStreamAnimated, {
baseCharCount: 0,
charDelay
}]], [baseRehypePlugins, charDelay]);
const revealedPlugins = useMemo(() => [...baseRehypePlugins, [rehypeStreamAnimated, { revealed: true }]], [baseRehypePlugins]);
const prevCharCountRef = useRef(0);
const prevStreamOffsetRef = useRef(-1);
useEffect(() => {
const tailIdx = blocks.length - 1;
if (tailIdx >= 0) {
const tail = blocks[tailIdx];
if (tail.startOffset !== prevStreamOffsetRef.current) {
prevStreamOffsetRef.current = tail.startOffset;
prevCharCountRef.current = 0;
}
prevCharCountRef.current = countChars(tail.content);
} else {
prevCharCountRef.current = 0;
prevStreamOffsetRef.current = -1;
}
}, [blocks]);
return /* @__PURE__ */ jsx("div", {
className: styles.animated,
children: blocks.map((block, index) => {
const state = getBlockState(index);
if (state === "queued") return null;
let plugins;
if (state === "streaming") {
const baseCharCount = block.startOffset === prevStreamOffsetRef.current ? prevCharCountRef.current : 0;
plugins = [...baseRehypePlugins, [rehypeStreamAnimated, {
baseCharCount,
charDelay: STREAM_CHAR_DELAY
}]];
} else if (state === "animating") plugins = staggerPlugins;
else plugins = revealedPlugins;
return /* @__PURE__ */ createElement(StreamdownBlock, {
...rest,
components,
key: `${generatedId}-${block.startOffset}`,
rehypePlugins: plugins,
remarkPlugins
}, block.content);
})
});
});
StreamdownRender.displayName = "StreamdownRender";
var StreamdownRender_default = StreamdownRender;
//#endregion
export { StreamdownRender_default as default };
//# sourceMappingURL=StreamdownRender.mjs.map