UNPKG

@lobehub/ui

Version:

Lobe UI is an open-source UI component library for building AIGC web apps

135 lines (132 loc) 5.52 kB
'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