@lobehub/ui 5.2.2 → 5.4.0
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/Accordion/Accordion.d.mts +2 -2
- package/es/Accordion/AccordionItem.d.mts +2 -2
- package/es/ActionIcon/ActionIcon.d.mts +2 -2
- package/es/Alert/Alert.d.mts +2 -2
- package/es/AutoComplete/Select.d.mts +2 -2
- package/es/Avatar/AvatarGroup/index.d.mts +2 -2
- package/es/Burger/Burger.d.mts +2 -2
- package/es/CodeDiff/CodeDiff.d.mts +2 -2
- package/es/CodeDiff/PatchDiff.d.mts +2 -2
- package/es/CodeEditor/CodeEditor.d.mts +2 -2
- package/es/Collapse/Collapse.d.mts +2 -2
- package/es/ConfigProvider/index.d.mts +2 -2
- package/es/CopyButton/CopyButton.d.mts +2 -2
- package/es/DatePicker/DatePicker.d.mts +2 -2
- package/es/DraggablePanel/components/DraggablePanelBody.d.mts +2 -2
- package/es/DraggablePanel/components/DraggablePanelContainer.d.mts +2 -2
- package/es/DraggablePanel/components/DraggablePanelFooter.d.mts +2 -2
- package/es/DraggablePanel/components/DraggablePanelHeader.d.mts +2 -2
- package/es/DraggableSideNav/DraggableSideNav.d.mts +2 -2
- package/es/Drawer/Drawer.d.mts +2 -2
- package/es/Dropdown/Dropdown.d.mts +2 -2
- package/es/EditableText/EditableText.d.mts +2 -2
- package/es/EditorSlashMenu/atoms.d.mts +13 -13
- package/es/EmojiPicker/EmojiPicker.d.mts +2 -2
- package/es/Flex/FlexBasic.d.mts +2 -2
- package/es/FontLoader/index.d.mts +2 -2
- package/es/Footer/Footer.d.mts +2 -2
- package/es/Form/components/FormGroup.d.mts +2 -2
- package/es/Form/components/FormItem.d.mts +2 -2
- package/es/Form/components/FormSubmitFooter.d.mts +2 -2
- package/es/FormModal/FormModal.d.mts +2 -2
- package/es/Freeze/Freeze.d.mts +2 -2
- package/es/GuideCard/GuideCard.d.mts +2 -2
- package/es/Header/Header.d.mts +2 -2
- package/es/Highlighter/Highlighter.d.mts +2 -2
- package/es/Highlighter/SyntaxHighlighter/index.d.mts +2 -2
- package/es/Hotkey/Hotkey.d.mts +2 -2
- package/es/HotkeyInput/HotkeyInput.d.mts +2 -2
- package/es/HotkeyInput/HotkeyInput.mjs +30 -8
- package/es/HotkeyInput/HotkeyInput.mjs.map +1 -1
- package/es/HotkeyInput/type.d.mts +3 -0
- package/es/Icon/Icon.d.mts +2 -2
- package/es/Icon/components/IconProvider.d.mts +3 -3
- package/es/Image/Image.mjs +2 -2
- package/es/Image/Image.mjs.map +1 -1
- package/es/Image/PreviewGroup.d.mts +2 -2
- package/es/Image/components/Toolbar.mjs +116 -36
- package/es/Image/components/Toolbar.mjs.map +1 -1
- package/es/Input/Input.d.mts +2 -2
- package/es/Input/InputOPT.d.mts +2 -2
- package/es/Input/InputPassword.d.mts +2 -2
- package/es/Input/TextArea.d.mts +2 -2
- package/es/Layout/components/LayoutFooter.d.mts +2 -2
- package/es/Layout/components/LayoutHeader.d.mts +2 -2
- package/es/Layout/components/LayoutMain.d.mts +2 -2
- package/es/Layout/components/LayoutSidebar.d.mts +2 -2
- package/es/Layout/components/LayoutSidebarInner.d.mts +2 -2
- package/es/Layout/components/LayoutToc.d.mts +2 -2
- package/es/List/ListItem/index.d.mts +2 -2
- package/es/Markdown/Markdown.d.mts +2 -2
- package/es/Markdown/Markdown.mjs +2 -1
- package/es/Markdown/Markdown.mjs.map +1 -1
- package/es/Markdown/SyntaxMarkdown/StreamdownRender.mjs +56 -30
- package/es/Markdown/SyntaxMarkdown/StreamdownRender.mjs.map +1 -1
- package/es/Markdown/SyntaxMarkdown/style.mjs +2 -2
- package/es/Markdown/SyntaxMarkdown/style.mjs.map +1 -1
- package/es/Markdown/SyntaxMarkdown/useSmoothStreamContent.mjs +229 -0
- package/es/Markdown/SyntaxMarkdown/useSmoothStreamContent.mjs.map +1 -0
- package/es/Markdown/SyntaxMarkdown/useStreamQueue.mjs +8 -6
- package/es/Markdown/SyntaxMarkdown/useStreamQueue.mjs.map +1 -1
- package/es/Markdown/Typography.d.mts +2 -2
- package/es/Markdown/components/SearchResultCards/index.d.mts +2 -2
- package/es/Markdown/index.d.mts +2 -2
- package/es/Markdown/plugins/rehypeStreamAnimated.d.mts +2 -0
- package/es/Markdown/plugins/rehypeStreamAnimated.mjs +17 -5
- package/es/Markdown/plugins/rehypeStreamAnimated.mjs.map +1 -1
- package/es/Markdown/type.d.mts +3 -1
- package/es/MaskShadow/MaskShadow.d.mts +2 -2
- package/es/Menu/Menu.d.mts +2 -2
- package/es/Mermaid/Mermaid.d.mts +2 -2
- package/es/Mermaid/SyntaxMermaid/index.d.mts +2 -2
- package/es/Modal/Modal.d.mts +2 -2
- package/es/Modal/ModalProvider.d.mts +2 -2
- package/es/Modal/imperative.d.mts +2 -2
- package/es/MotionProvider/index.d.mts +2 -2
- package/es/Popover/ArrowIcon.d.mts +2 -2
- package/es/Popover/atoms.d.mts +2 -2
- package/es/Popover/context.d.mts +2 -2
- package/es/SearchBar/SearchBar.d.mts +2 -2
- package/es/Segmented/Segmented.d.mts +2 -2
- package/es/Select/Select.d.mts +2 -2
- package/es/SideNav/SideNav.d.mts +2 -2
- package/es/SliderWithInput/SliderWithInput.d.mts +2 -2
- package/es/SortableList/components/DragHandle.d.mts +2 -2
- package/es/SortableList/components/SortableItem.d.mts +2 -2
- package/es/ThemeProvider/ThemeProvider.d.mts +2 -2
- package/es/Toc/Toc.d.mts +2 -2
- package/es/Tooltip/TooltipGroup.mjs +4 -3
- package/es/Tooltip/TooltipGroup.mjs.map +1 -1
- package/es/Tooltip/TooltipStandalone.mjs +18 -5
- package/es/Tooltip/TooltipStandalone.mjs.map +1 -1
- package/es/Tooltip/type.d.mts +7 -0
- package/es/Tooltip/useMergedTooltipProps.mjs +4 -1
- package/es/Tooltip/useMergedTooltipProps.mjs.map +1 -1
- package/es/Video/index.d.mts +2 -2
- package/es/awesome/AuroraBackground/AuroraBackground.d.mts +2 -2
- package/es/awesome/BottomGradientButton/BottomGradientButton.d.mts +2 -2
- package/es/awesome/Features/Features.d.mts +2 -2
- package/es/awesome/Giscus/Giscus.d.mts +2 -2
- package/es/awesome/GradientButton/GradientButton.d.mts +2 -2
- package/es/awesome/GridBackground/GridBackground.d.mts +2 -2
- package/es/awesome/GridBackground/GridShowcase.d.mts +2 -2
- package/es/awesome/Hero/Hero.d.mts +2 -2
- package/es/awesome/Spline/Spine.d.mts +2 -2
- package/es/awesome/Spotlight/Spotlight.d.mts +2 -2
- package/es/awesome/SpotlightCard/SpotlightCard.d.mts +2 -2
- package/es/awesome/TypewriterEffect/TypewriterEffect.d.mts +2 -2
- package/es/base-ui/DropdownMenu/DropdownMenu.d.mts +2 -2
- package/es/base-ui/DropdownMenu/atoms.d.mts +18 -18
- package/es/base-ui/Modal/atoms.d.mts +12 -12
- package/es/base-ui/Modal/context.d.mts +2 -2
- package/es/base-ui/Modal/imperative.d.mts +2 -2
- package/es/base-ui/ScrollArea/atoms.d.mts +7 -7
- package/es/base-ui/Select/Select.d.mts +2 -2
- package/es/base-ui/Select/atoms.d.mts +3 -3
- package/es/base-ui/Switch/Switch.d.mts +2 -2
- package/es/base-ui/Switch/atoms.d.mts +4 -4
- package/es/base-ui/Toast/imperative.d.mts +2 -2
- package/es/brand/LobeChat/index.d.mts +2 -2
- package/es/brand/LobeHub/index.d.mts +2 -2
- package/es/brand/LogoThree/LogoSpline.d.mts +2 -2
- package/es/brand/LogoThree/index.d.mts +2 -2
- package/es/chat/BackBottom/BackBottom.d.mts +2 -2
- package/es/chat/ChatInputArea/components/ChatInputAreaInner.d.mts +2 -2
- package/es/chat/ChatItem/ChatItem.d.mts +2 -2
- package/es/chat/ChatList/ChatList.d.mts +2 -2
- package/es/chat/EditableMessage/EditableMessage.d.mts +2 -2
- package/es/chat/EditableMessageList/EditableMessageList.d.mts +2 -2
- package/es/chat/MessageInput/MessageInput.d.mts +2 -2
- package/es/chat/MessageModal/MessageModal.d.mts +2 -2
- package/es/color/ColorScales/index.d.mts +2 -2
- package/es/color/CssVar/index.d.mts +2 -2
- package/es/i18n/context.d.mts +2 -2
- package/es/i18n/resources/en/hotkey.d.mts +1 -0
- package/es/i18n/resources/en/hotkey.mjs +1 -0
- package/es/i18n/resources/en/hotkey.mjs.map +1 -1
- package/es/i18n/resources/en/image.d.mts +18 -0
- package/es/i18n/resources/en/image.mjs +19 -0
- package/es/i18n/resources/en/image.mjs.map +1 -0
- package/es/i18n/resources/en/index.d.mts +4 -3
- package/es/i18n/resources/en/index.mjs +2 -0
- package/es/i18n/resources/en/index.mjs.map +1 -1
- package/es/i18n/resources/zhCn/hotkey.d.mts +1 -0
- package/es/i18n/resources/zhCn/hotkey.mjs +1 -0
- package/es/i18n/resources/zhCn/hotkey.mjs.map +1 -1
- package/es/i18n/resources/zhCn/image.d.mts +18 -0
- package/es/i18n/resources/zhCn/image.mjs +19 -0
- package/es/i18n/resources/zhCn/image.mjs.map +1 -0
- package/es/i18n/resources/zhCn/index.d.mts +4 -3
- package/es/i18n/resources/zhCn/index.mjs +2 -0
- package/es/i18n/resources/zhCn/index.mjs.map +1 -1
- package/es/i18n/types.d.mts +4 -3
- package/es/icons/lucideExtra/BotPromptIcon.d.mts +2 -2
- package/es/icons/lucideExtra/CreateBotIcon.d.mts +3 -3
- package/es/icons/lucideExtra/DiscordIcon.d.mts +3 -3
- package/es/icons/lucideExtra/GlobeOffIcon.d.mts +2 -2
- package/es/icons/lucideExtra/GroupBotIcon.d.mts +3 -3
- package/es/icons/lucideExtra/GroupBotSquareIcon.d.mts +2 -2
- package/es/icons/lucideExtra/LeftClickIcon.d.mts +3 -3
- package/es/icons/lucideExtra/LeftDoubleClickIcon.d.mts +2 -2
- package/es/icons/lucideExtra/McpIcon.d.mts +3 -3
- package/es/icons/lucideExtra/ProviderIcon.d.mts +3 -3
- package/es/icons/lucideExtra/RightClickIcon.d.mts +3 -3
- package/es/icons/lucideExtra/RightDoubleClickIcon.d.mts +3 -3
- package/es/icons/lucideExtra/ShapesUploadIcon.d.mts +3 -3
- package/es/icons/lucideExtra/SkillsIcon.d.mts +2 -2
- package/es/icons/lucideExtra/TreeDownRightIcon.d.mts +3 -3
- package/es/icons/lucideExtra/TreeUpDownRightIcon.d.mts +3 -3
- package/es/mdx/Mdx/index.d.mts +2 -2
- package/es/mobile/ChatHeader/ChatHeaderTitle.d.mts +2 -2
- package/es/mobile/ChatInputArea/components/ChatSendButton.d.mts +2 -2
- package/es/mobile/TabBar/TabBar.d.mts +2 -2
- package/es/storybook/StoryBook/index.d.mts +2 -2
- package/es/utils/blobToPng.mjs +41 -0
- package/es/utils/blobToPng.mjs.map +1 -0
- package/package.json +1 -1
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { useMarkdownContext } from "../components/MarkdownProvider.mjs";
|
|
3
4
|
import { useMarkdownComponents } from "../../hooks/useMarkdown/useMarkdownComponents.mjs";
|
|
4
5
|
import { useMarkdownContent } from "../../hooks/useMarkdown/useMarkdownContent.mjs";
|
|
5
6
|
import { useMarkdownRehypePlugins } from "../../hooks/useMarkdown/useMarkdownRehypePlugins.mjs";
|
|
6
7
|
import { useMarkdownRemarkPlugins } from "../../hooks/useMarkdown/useMarkdownRemarkPlugins.mjs";
|
|
7
8
|
import { rehypeStreamAnimated } from "../plugins/rehypeStreamAnimated.mjs";
|
|
8
9
|
import { styles } from "./style.mjs";
|
|
10
|
+
import { useSmoothStreamContent } from "./useSmoothStreamContent.mjs";
|
|
9
11
|
import { useStreamQueue } from "./useStreamQueue.mjs";
|
|
10
12
|
import { createElement, memo, useEffect, useId, useMemo, useRef } from "react";
|
|
11
13
|
import { jsx } from "react/jsx-runtime";
|
|
@@ -14,7 +16,7 @@ import { marked } from "marked";
|
|
|
14
16
|
import remend from "remend";
|
|
15
17
|
|
|
16
18
|
//#region src/Markdown/SyntaxMarkdown/StreamdownRender.tsx
|
|
17
|
-
const
|
|
19
|
+
const STREAM_FADE_DURATION = 280;
|
|
18
20
|
function countChars(text) {
|
|
19
21
|
return [...text].length;
|
|
20
22
|
}
|
|
@@ -61,14 +63,16 @@ const StreamdownBlock = memo(({ children, ...rest }) => {
|
|
|
61
63
|
}, (prevProps, nextProps) => prevProps.children === nextProps.children && prevProps.components === nextProps.components && isSamePlugins(prevProps.rehypePlugins, nextProps.rehypePlugins) && isSamePlugins(prevProps.remarkPlugins, nextProps.remarkPlugins));
|
|
62
64
|
StreamdownBlock.displayName = "StreamdownBlock";
|
|
63
65
|
const StreamdownRender = memo(({ children, ...rest }) => {
|
|
66
|
+
const { streamSmoothingPreset = "balanced" } = useMarkdownContext();
|
|
64
67
|
const escapedContent = useMarkdownContent(children || "");
|
|
65
68
|
const components = useMarkdownComponents();
|
|
66
69
|
const baseRehypePlugins = useStablePlugins(useMarkdownRehypePlugins());
|
|
67
70
|
const remarkPlugins = useStablePlugins(useMarkdownRemarkPlugins());
|
|
68
71
|
const generatedId = useId();
|
|
72
|
+
const smoothedContent = useSmoothStreamContent(typeof escapedContent === "string" ? escapedContent : "", { preset: streamSmoothingPreset });
|
|
69
73
|
const processedContent = useMemo(() => {
|
|
70
|
-
return remend(
|
|
71
|
-
}, [
|
|
74
|
+
return remend(smoothedContent);
|
|
75
|
+
}, [smoothedContent]);
|
|
72
76
|
const blocks = useMemo(() => {
|
|
73
77
|
const tokens = marked.lexer(processedContent);
|
|
74
78
|
let offset = 0;
|
|
@@ -82,41 +86,63 @@ const StreamdownRender = memo(({ children, ...rest }) => {
|
|
|
82
86
|
});
|
|
83
87
|
}, [processedContent]);
|
|
84
88
|
const { getBlockState, charDelay } = useStreamQueue(blocks);
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
89
|
+
const prevBlockCharCountRef = useRef(/* @__PURE__ */ new Map());
|
|
90
|
+
const blockTimelineRef = useRef(/* @__PURE__ */ new Map());
|
|
91
|
+
const lastRenderTsRef = useRef(null);
|
|
92
|
+
const renderTs = typeof performance === "undefined" ? Date.now() : performance.now();
|
|
93
|
+
const frameDt = lastRenderTsRef.current === null ? 0 : Math.max(0, Math.min(renderTs - lastRenderTsRef.current, 120));
|
|
94
|
+
const timelineForRender = useMemo(() => {
|
|
95
|
+
const next = /* @__PURE__ */ new Map();
|
|
96
|
+
const prevTimeline = blockTimelineRef.current;
|
|
97
|
+
const prevCharCounts = prevBlockCharCountRef.current;
|
|
98
|
+
for (const block of blocks) {
|
|
99
|
+
const blockCharCount = countChars(block.content);
|
|
100
|
+
const prevCharCount = prevCharCounts.get(block.startOffset) ?? 0;
|
|
101
|
+
const prevElapsed = prevTimeline.get(block.startOffset);
|
|
102
|
+
const latestCharStart = Math.max(0, (blockCharCount - 1) * charDelay);
|
|
103
|
+
if (prevElapsed === void 0 || blockCharCount < prevCharCount) {
|
|
104
|
+
next.set(block.startOffset, latestCharStart);
|
|
105
|
+
continue;
|
|
99
106
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
prevStreamOffsetRef.current = -1;
|
|
107
|
+
const elapsedByTime = prevElapsed + frameDt;
|
|
108
|
+
const minElapsed = Math.max(0, latestCharStart - charDelay * 2);
|
|
109
|
+
next.set(block.startOffset, Math.max(elapsedByTime, minElapsed));
|
|
104
110
|
}
|
|
105
|
-
|
|
111
|
+
return next;
|
|
112
|
+
}, [
|
|
113
|
+
blocks,
|
|
114
|
+
charDelay,
|
|
115
|
+
frameDt
|
|
116
|
+
]);
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
const nextCharCount = /* @__PURE__ */ new Map();
|
|
119
|
+
for (const block of blocks) nextCharCount.set(block.startOffset, countChars(block.content));
|
|
120
|
+
prevBlockCharCountRef.current = nextCharCount;
|
|
121
|
+
blockTimelineRef.current = timelineForRender;
|
|
122
|
+
lastRenderTsRef.current = typeof performance === "undefined" ? Date.now() : performance.now();
|
|
123
|
+
}, [blocks, timelineForRender]);
|
|
106
124
|
return /* @__PURE__ */ jsx("div", {
|
|
107
125
|
className: styles.animated,
|
|
108
126
|
children: blocks.map((block, index) => {
|
|
109
127
|
const state = getBlockState(index);
|
|
110
128
|
if (state === "queued") return null;
|
|
129
|
+
const timelineElapsedMs = timelineForRender.get(block.startOffset) ?? 0;
|
|
111
130
|
let plugins;
|
|
112
|
-
if (state === "streaming") {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
131
|
+
if (state === "streaming") plugins = [...baseRehypePlugins, [rehypeStreamAnimated, {
|
|
132
|
+
charDelay,
|
|
133
|
+
fadeDuration: STREAM_FADE_DURATION,
|
|
134
|
+
timelineElapsedMs
|
|
135
|
+
}]];
|
|
136
|
+
else if (state === "animating") plugins = [...baseRehypePlugins, [rehypeStreamAnimated, {
|
|
137
|
+
charDelay,
|
|
138
|
+
fadeDuration: STREAM_FADE_DURATION,
|
|
139
|
+
timelineElapsedMs
|
|
140
|
+
}]];
|
|
141
|
+
else plugins = [...baseRehypePlugins, [rehypeStreamAnimated, {
|
|
142
|
+
charDelay,
|
|
143
|
+
fadeDuration: STREAM_FADE_DURATION,
|
|
144
|
+
timelineElapsedMs
|
|
145
|
+
}]];
|
|
120
146
|
return /* @__PURE__ */ createElement(StreamdownBlock, {
|
|
121
147
|
...rest,
|
|
122
148
|
components,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StreamdownRender.mjs","names":["blocks: BlockInfo[]","staggerPlugins: Pluggable[]","revealedPlugins: Pluggable[]","plugins: Pluggable[]"],"sources":["../../../src/Markdown/SyntaxMarkdown/StreamdownRender.tsx"],"sourcesContent":["'use client';\n\nimport { marked } from 'marked';\nimport { memo, useEffect, useId, useMemo, useRef } 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 { rehypeStreamAnimated } from '@/Markdown/plugins/rehypeStreamAnimated';\n\nimport { styles } from './style';\nimport { type BlockInfo, useStreamQueue } from './useStreamQueue';\n\nconst STREAM_CHAR_DELAY = 15;\n\nfunction countChars(text: string): number {\n return [...text].length;\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null;\n\nconst isDeepEqualValue = (a: unknown, b: unknown): boolean => {\n if (a === b) return true;\n\n if (Array.isArray(a) || Array.isArray(b)) {\n if (!Array.isArray(a) || !Array.isArray(b)) return false;\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (!isDeepEqualValue(a[i], b[i])) return false;\n }\n return true;\n }\n\n if (!isRecord(a) || !isRecord(b)) return false;\n\n const keysA = Object.keys(a);\n const keysB = Object.keys(b);\n if (keysA.length !== keysB.length) return false;\n\n for (const key of keysA) {\n if (!isDeepEqualValue(a[key], b[key])) return false;\n }\n\n return true;\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 isDeepEqualValue(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 escapedContent = useMarkdownContent(children || '');\n const components = useMarkdownComponents();\n const baseRehypePlugins = useStablePlugins(useMarkdownRehypePlugins());\n const remarkPlugins = useStablePlugins(useMarkdownRemarkPlugins());\n const generatedId = useId();\n\n const processedContent = useMemo(() => {\n const content = typeof escapedContent === 'string' ? escapedContent : '';\n return remend(content);\n }, [escapedContent]);\n\n const blocks: BlockInfo[] = useMemo(() => {\n const tokens = marked.lexer(processedContent);\n let offset = 0;\n return tokens.map((token) => {\n const block = { content: token.raw, startOffset: offset };\n offset += token.raw.length;\n return block;\n });\n }, [processedContent]);\n\n const { getBlockState, charDelay } = useStreamQueue(blocks);\n\n const staggerPlugins: Pluggable[] = useMemo(\n () => [...baseRehypePlugins, [rehypeStreamAnimated, { baseCharCount: 0, charDelay }]],\n [baseRehypePlugins, charDelay],\n );\n\n const revealedPlugins: Pluggable[] = useMemo(\n () => [...baseRehypePlugins, [rehypeStreamAnimated, { revealed: true }]],\n [baseRehypePlugins],\n );\n\n // prevCharCount tracks the PREVIOUS render's streaming char count.\n // Updated in useEffect (after render) to avoid stale reads during\n // synchronous re-renders (e.g. useLayoutEffect auto-reveal).\n const prevCharCountRef = useRef(0);\n const prevStreamOffsetRef = useRef(-1);\n\n useEffect(() => {\n const tailIdx = blocks.length - 1;\n if (tailIdx >= 0) {\n const tail = blocks[tailIdx];\n if (tail.startOffset !== prevStreamOffsetRef.current) {\n prevStreamOffsetRef.current = tail.startOffset;\n prevCharCountRef.current = 0;\n }\n prevCharCountRef.current = countChars(tail.content);\n } else {\n prevCharCountRef.current = 0;\n prevStreamOffsetRef.current = -1;\n }\n }, [blocks]);\n\n return (\n <div className={styles.animated}>\n {blocks.map((block, index) => {\n const state = getBlockState(index);\n if (state === 'queued') return null;\n\n let plugins: Pluggable[];\n if (state === 'streaming') {\n const baseCharCount =\n block.startOffset === prevStreamOffsetRef.current ? prevCharCountRef.current : 0;\n plugins = [\n ...baseRehypePlugins,\n [rehypeStreamAnimated, { baseCharCount, charDelay: STREAM_CHAR_DELAY }],\n ];\n } else if (state === 'animating') {\n plugins = staggerPlugins;\n } else {\n plugins = revealedPlugins;\n }\n\n return (\n <StreamdownBlock\n {...rest}\n components={components}\n key={`${generatedId}-${block.startOffset}`}\n rehypePlugins={plugins}\n remarkPlugins={remarkPlugins}\n >\n {block.content}\n </StreamdownBlock>\n );\n })}\n </div>\n );\n});\n\nStreamdownRender.displayName = 'StreamdownRender';\n\nexport default StreamdownRender;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"StreamdownRender.mjs","names":["blocks: BlockInfo[]","plugins: Pluggable[]"],"sources":["../../../src/Markdown/SyntaxMarkdown/StreamdownRender.tsx"],"sourcesContent":["'use client';\n\nimport { marked } from 'marked';\nimport { memo, useEffect, useId, useMemo, useRef } 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';\n\nimport { styles } from './style';\nimport { useSmoothStreamContent } from './useSmoothStreamContent';\nimport { type BlockInfo, useStreamQueue } from './useStreamQueue';\n\nconst STREAM_FADE_DURATION = 280;\n\nfunction countChars(text: string): number {\n return [...text].length;\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null;\n\nconst isDeepEqualValue = (a: unknown, b: unknown): boolean => {\n if (a === b) return true;\n\n if (Array.isArray(a) || Array.isArray(b)) {\n if (!Array.isArray(a) || !Array.isArray(b)) return false;\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (!isDeepEqualValue(a[i], b[i])) return false;\n }\n return true;\n }\n\n if (!isRecord(a) || !isRecord(b)) return false;\n\n const keysA = Object.keys(a);\n const keysB = Object.keys(b);\n if (keysA.length !== keysB.length) return false;\n\n for (const key of keysA) {\n if (!isDeepEqualValue(a[key], b[key])) return false;\n }\n\n return true;\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 isDeepEqualValue(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 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 processedContent = useMemo(() => {\n return remend(smoothedContent);\n }, [smoothedContent]);\n\n const blocks: BlockInfo[] = useMemo(() => {\n const tokens = marked.lexer(processedContent);\n let offset = 0;\n return tokens.map((token) => {\n const block = { content: token.raw, startOffset: offset };\n offset += token.raw.length;\n return block;\n });\n }, [processedContent]);\n\n const { getBlockState, charDelay } = useStreamQueue(blocks);\n const prevBlockCharCountRef = useRef<Map<number, number>>(new Map());\n const blockTimelineRef = useRef<Map<number, number>>(new Map());\n const lastRenderTsRef = useRef<number | null>(null);\n\n const renderTs = typeof performance === 'undefined' ? Date.now() : performance.now();\n const frameDt =\n lastRenderTsRef.current === null\n ? 0\n : Math.max(0, Math.min(renderTs - lastRenderTsRef.current, 120));\n\n const timelineForRender = useMemo(() => {\n const next = new Map<number, number>();\n const prevTimeline = blockTimelineRef.current;\n const prevCharCounts = prevBlockCharCountRef.current;\n\n for (const block of blocks) {\n const blockCharCount = countChars(block.content);\n const prevCharCount = prevCharCounts.get(block.startOffset) ?? 0;\n const prevElapsed = prevTimeline.get(block.startOffset);\n const latestCharStart = Math.max(0, (blockCharCount - 1) * charDelay);\n\n if (prevElapsed === undefined || blockCharCount < prevCharCount) {\n next.set(block.startOffset, latestCharStart);\n continue;\n }\n\n const elapsedByTime = prevElapsed + frameDt;\n // Avoid huge hidden backlog when stream updates in bursts.\n const minElapsed = Math.max(0, latestCharStart - charDelay * 2);\n next.set(block.startOffset, Math.max(elapsedByTime, minElapsed));\n }\n\n return next;\n }, [blocks, charDelay, frameDt]);\n\n useEffect(() => {\n const nextCharCount = new Map<number, number>();\n for (const block of blocks) {\n nextCharCount.set(block.startOffset, countChars(block.content));\n }\n prevBlockCharCountRef.current = nextCharCount;\n blockTimelineRef.current = timelineForRender;\n lastRenderTsRef.current = typeof performance === 'undefined' ? Date.now() : performance.now();\n }, [blocks, timelineForRender]);\n\n return (\n <div className={styles.animated}>\n {blocks.map((block, index) => {\n const state = getBlockState(index);\n if (state === 'queued') return null;\n const timelineElapsedMs = timelineForRender.get(block.startOffset) ?? 0;\n\n let plugins: Pluggable[];\n if (state === 'streaming') {\n plugins = [\n ...baseRehypePlugins,\n [\n rehypeStreamAnimated,\n { charDelay, fadeDuration: STREAM_FADE_DURATION, timelineElapsedMs },\n ],\n ];\n } else if (state === 'animating') {\n // Continue from previously rendered progress instead of restarting\n // or force-switching to fully revealed.\n plugins = [\n ...baseRehypePlugins,\n [\n rehypeStreamAnimated,\n { charDelay, fadeDuration: STREAM_FADE_DURATION, timelineElapsedMs },\n ],\n ];\n } else {\n // Keep fade continuity for just-finished chars; avoid instant class\n // switch to stream-char-revealed that would cancel in-flight fades.\n plugins = [\n ...baseRehypePlugins,\n [\n rehypeStreamAnimated,\n { charDelay, fadeDuration: STREAM_FADE_DURATION, timelineElapsedMs },\n ],\n ];\n }\n\n return (\n <StreamdownBlock\n {...rest}\n components={components}\n key={`${generatedId}-${block.startOffset}`}\n rehypePlugins={plugins}\n remarkPlugins={remarkPlugins}\n >\n {block.content}\n </StreamdownBlock>\n );\n })}\n </div>\n );\n});\n\nStreamdownRender.displayName = 'StreamdownRender';\n\nexport default StreamdownRender;\n"],"mappings":"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"}
|
|
@@ -8,8 +8,8 @@ const styles = createStaticStyles(({ css: css$1 }) => {
|
|
|
8
8
|
opacity: 0;
|
|
9
9
|
|
|
10
10
|
animation-name: ${fadeIn};
|
|
11
|
-
animation-duration:
|
|
12
|
-
animation-timing-function:
|
|
11
|
+
animation-duration: 280ms;
|
|
12
|
+
animation-timing-function: cubic-bezier(0.33, 0, 0.67, 1);
|
|
13
13
|
animation-fill-mode: forwards;
|
|
14
14
|
}
|
|
15
15
|
|
|
@@ -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 styles = createStaticStyles(({ css }) => {\n return {\n animated: css`\n .stream-char {\n opacity: 0;\n\n animation-name: ${fadeIn};\n animation-duration: 280ms;\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":";;;;AAIA,MAAa,SAAS,oBAAoB,EAAE,iBAAU;AACpD,QAAO,EACL,UAAU,KAAG;;;;0BAIS,OAAO;;;;;;;;;;;;;;;OAgB9B;EACD"}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/Markdown/SyntaxMarkdown/useSmoothStreamContent.ts
|
|
4
|
+
const PRESET_CONFIG = {
|
|
5
|
+
balanced: {
|
|
6
|
+
activeInputWindowMs: 220,
|
|
7
|
+
defaultCps: 38,
|
|
8
|
+
emaAlpha: .2,
|
|
9
|
+
flushCps: 120,
|
|
10
|
+
largeAppendChars: 120,
|
|
11
|
+
maxActiveCps: 132,
|
|
12
|
+
maxCps: 72,
|
|
13
|
+
maxFlushCps: 280,
|
|
14
|
+
minCps: 18,
|
|
15
|
+
settleAfterMs: 360,
|
|
16
|
+
settleDrainMaxMs: 520,
|
|
17
|
+
settleDrainMinMs: 180,
|
|
18
|
+
targetBufferMs: 120
|
|
19
|
+
},
|
|
20
|
+
realtime: {
|
|
21
|
+
activeInputWindowMs: 140,
|
|
22
|
+
defaultCps: 50,
|
|
23
|
+
emaAlpha: .3,
|
|
24
|
+
flushCps: 170,
|
|
25
|
+
largeAppendChars: 180,
|
|
26
|
+
maxActiveCps: 180,
|
|
27
|
+
maxCps: 96,
|
|
28
|
+
maxFlushCps: 360,
|
|
29
|
+
minCps: 24,
|
|
30
|
+
settleAfterMs: 260,
|
|
31
|
+
settleDrainMaxMs: 360,
|
|
32
|
+
settleDrainMinMs: 140,
|
|
33
|
+
targetBufferMs: 40
|
|
34
|
+
},
|
|
35
|
+
silky: {
|
|
36
|
+
activeInputWindowMs: 320,
|
|
37
|
+
defaultCps: 28,
|
|
38
|
+
emaAlpha: .14,
|
|
39
|
+
flushCps: 96,
|
|
40
|
+
largeAppendChars: 100,
|
|
41
|
+
maxActiveCps: 102,
|
|
42
|
+
maxCps: 56,
|
|
43
|
+
maxFlushCps: 220,
|
|
44
|
+
minCps: 14,
|
|
45
|
+
settleAfterMs: 460,
|
|
46
|
+
settleDrainMaxMs: 680,
|
|
47
|
+
settleDrainMinMs: 240,
|
|
48
|
+
targetBufferMs: 170
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
const clamp = (value, min, max) => {
|
|
52
|
+
return Math.min(max, Math.max(min, value));
|
|
53
|
+
};
|
|
54
|
+
const countChars = (text) => {
|
|
55
|
+
return [...text].length;
|
|
56
|
+
};
|
|
57
|
+
const useSmoothStreamContent = (content, { enabled = true, preset = "balanced" } = {}) => {
|
|
58
|
+
const config = PRESET_CONFIG[preset];
|
|
59
|
+
const [displayedContent, setDisplayedContent] = useState(content);
|
|
60
|
+
const displayedContentRef = useRef(content);
|
|
61
|
+
const displayedCountRef = useRef(countChars(content));
|
|
62
|
+
const targetContentRef = useRef(content);
|
|
63
|
+
const targetCharsRef = useRef([...content]);
|
|
64
|
+
const targetCountRef = useRef(targetCharsRef.current.length);
|
|
65
|
+
const emaCpsRef = useRef(config.defaultCps);
|
|
66
|
+
const lastInputTsRef = useRef(0);
|
|
67
|
+
const lastInputCountRef = useRef(targetCountRef.current);
|
|
68
|
+
const chunkSizeEmaRef = useRef(1);
|
|
69
|
+
const arrivalCpsEmaRef = useRef(config.defaultCps);
|
|
70
|
+
const rafRef = useRef(null);
|
|
71
|
+
const lastFrameTsRef = useRef(null);
|
|
72
|
+
const stopFrameLoop = useCallback(() => {
|
|
73
|
+
if (rafRef.current !== null) {
|
|
74
|
+
cancelAnimationFrame(rafRef.current);
|
|
75
|
+
rafRef.current = null;
|
|
76
|
+
}
|
|
77
|
+
lastFrameTsRef.current = null;
|
|
78
|
+
}, []);
|
|
79
|
+
const syncImmediate = useCallback((nextContent) => {
|
|
80
|
+
stopFrameLoop();
|
|
81
|
+
const chars = [...nextContent];
|
|
82
|
+
const now = performance.now();
|
|
83
|
+
targetContentRef.current = nextContent;
|
|
84
|
+
targetCharsRef.current = chars;
|
|
85
|
+
targetCountRef.current = chars.length;
|
|
86
|
+
displayedContentRef.current = nextContent;
|
|
87
|
+
displayedCountRef.current = chars.length;
|
|
88
|
+
setDisplayedContent(nextContent);
|
|
89
|
+
emaCpsRef.current = config.defaultCps;
|
|
90
|
+
chunkSizeEmaRef.current = 1;
|
|
91
|
+
arrivalCpsEmaRef.current = config.defaultCps;
|
|
92
|
+
lastInputTsRef.current = now;
|
|
93
|
+
lastInputCountRef.current = chars.length;
|
|
94
|
+
}, [config.defaultCps, stopFrameLoop]);
|
|
95
|
+
const startFrameLoop = useCallback(() => {
|
|
96
|
+
if (rafRef.current !== null) return;
|
|
97
|
+
const tick = (ts) => {
|
|
98
|
+
if (lastFrameTsRef.current === null) {
|
|
99
|
+
lastFrameTsRef.current = ts;
|
|
100
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const dtSeconds = Math.max(.001, Math.min((ts - lastFrameTsRef.current) / 1e3, .05));
|
|
104
|
+
lastFrameTsRef.current = ts;
|
|
105
|
+
const targetCount = targetCountRef.current;
|
|
106
|
+
const displayedCount = displayedCountRef.current;
|
|
107
|
+
const backlog = targetCount - displayedCount;
|
|
108
|
+
if (backlog <= 0) {
|
|
109
|
+
stopFrameLoop();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const idleMs = performance.now() - lastInputTsRef.current;
|
|
113
|
+
const inputActive = idleMs <= config.activeInputWindowMs;
|
|
114
|
+
const settling = !inputActive && idleMs >= config.settleAfterMs;
|
|
115
|
+
const baseCps = clamp(emaCpsRef.current, config.minCps, config.maxCps);
|
|
116
|
+
const baseLagChars = Math.max(1, Math.round(baseCps * config.targetBufferMs / 1e3));
|
|
117
|
+
const lagUpperBound = Math.max(baseLagChars + 2, baseLagChars * 3);
|
|
118
|
+
const targetLagChars = inputActive ? Math.round(clamp(baseLagChars + chunkSizeEmaRef.current * .35, baseLagChars, lagUpperBound)) : 0;
|
|
119
|
+
const desiredDisplayed = Math.max(0, targetCount - targetLagChars);
|
|
120
|
+
let currentCps;
|
|
121
|
+
if (inputActive) {
|
|
122
|
+
const backlogPressure = targetLagChars > 0 ? backlog / targetLagChars : 1;
|
|
123
|
+
const chunkPressure = targetLagChars > 0 ? chunkSizeEmaRef.current / targetLagChars : 1;
|
|
124
|
+
const arrivalPressure = arrivalCpsEmaRef.current / Math.max(baseCps, 1);
|
|
125
|
+
const combinedPressure = clamp(backlogPressure * .6 + chunkPressure * .25 + arrivalPressure * .15, 1, 4.5);
|
|
126
|
+
const activeCap = clamp(config.maxActiveCps + chunkSizeEmaRef.current * 6, config.maxActiveCps, config.maxFlushCps);
|
|
127
|
+
currentCps = clamp(baseCps * combinedPressure, config.minCps, activeCap);
|
|
128
|
+
} else if (settling) {
|
|
129
|
+
const drainTargetMs = clamp(backlog * 8, config.settleDrainMinMs, config.settleDrainMaxMs);
|
|
130
|
+
currentCps = clamp(backlog * 1e3 / drainTargetMs, config.flushCps, config.maxFlushCps);
|
|
131
|
+
} else currentCps = clamp(Math.max(config.flushCps, baseCps * 1.8, arrivalCpsEmaRef.current * .8), config.flushCps, config.maxFlushCps);
|
|
132
|
+
const urgentBacklog = inputActive && targetLagChars > 0 && backlog > targetLagChars * 2.2;
|
|
133
|
+
const burstyInput = inputActive && chunkSizeEmaRef.current >= targetLagChars * .9;
|
|
134
|
+
const minRevealChars = inputActive ? urgentBacklog || burstyInput ? 2 : 1 : 2;
|
|
135
|
+
let revealChars = Math.max(minRevealChars, Math.round(currentCps * dtSeconds));
|
|
136
|
+
if (inputActive) {
|
|
137
|
+
const shortfall = desiredDisplayed - displayedCount;
|
|
138
|
+
if (shortfall <= 0) {
|
|
139
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
revealChars = Math.min(revealChars, shortfall, backlog);
|
|
143
|
+
} else revealChars = Math.min(revealChars, backlog);
|
|
144
|
+
const nextCount = displayedCount + revealChars;
|
|
145
|
+
const segment = targetCharsRef.current.slice(displayedCount, nextCount).join("");
|
|
146
|
+
if (segment) {
|
|
147
|
+
const nextDisplayed = displayedContentRef.current + segment;
|
|
148
|
+
displayedContentRef.current = nextDisplayed;
|
|
149
|
+
displayedCountRef.current = nextCount;
|
|
150
|
+
setDisplayedContent(nextDisplayed);
|
|
151
|
+
} else {
|
|
152
|
+
displayedContentRef.current = targetContentRef.current;
|
|
153
|
+
displayedCountRef.current = targetCount;
|
|
154
|
+
setDisplayedContent(targetContentRef.current);
|
|
155
|
+
}
|
|
156
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
157
|
+
};
|
|
158
|
+
rafRef.current = requestAnimationFrame(tick);
|
|
159
|
+
}, [
|
|
160
|
+
config.activeInputWindowMs,
|
|
161
|
+
config.flushCps,
|
|
162
|
+
config.maxActiveCps,
|
|
163
|
+
config.maxCps,
|
|
164
|
+
config.maxFlushCps,
|
|
165
|
+
config.minCps,
|
|
166
|
+
config.settleAfterMs,
|
|
167
|
+
config.settleDrainMaxMs,
|
|
168
|
+
config.settleDrainMinMs,
|
|
169
|
+
config.targetBufferMs,
|
|
170
|
+
stopFrameLoop
|
|
171
|
+
]);
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
if (!enabled) {
|
|
174
|
+
syncImmediate(content);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const prevTargetContent = targetContentRef.current;
|
|
178
|
+
if (content === prevTargetContent) return;
|
|
179
|
+
const now = performance.now();
|
|
180
|
+
if (!content.startsWith(prevTargetContent)) {
|
|
181
|
+
syncImmediate(content);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const appendedChars = [...content.slice(prevTargetContent.length)];
|
|
185
|
+
const appendedCount = appendedChars.length;
|
|
186
|
+
if (appendedCount > config.largeAppendChars) {
|
|
187
|
+
syncImmediate(content);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
targetContentRef.current = content;
|
|
191
|
+
targetCharsRef.current = [...targetCharsRef.current, ...appendedChars];
|
|
192
|
+
targetCountRef.current += appendedCount;
|
|
193
|
+
const deltaChars = targetCountRef.current - lastInputCountRef.current;
|
|
194
|
+
const deltaMs = Math.max(1, now - lastInputTsRef.current);
|
|
195
|
+
if (deltaChars > 0) {
|
|
196
|
+
const instantCps = deltaChars * 1e3 / deltaMs;
|
|
197
|
+
const normalizedInstantCps = clamp(instantCps, config.minCps, config.maxFlushCps * 2);
|
|
198
|
+
const chunkEmaAlpha = .35;
|
|
199
|
+
chunkSizeEmaRef.current = chunkSizeEmaRef.current * (1 - chunkEmaAlpha) + appendedCount * chunkEmaAlpha;
|
|
200
|
+
arrivalCpsEmaRef.current = arrivalCpsEmaRef.current * (1 - chunkEmaAlpha) + normalizedInstantCps * chunkEmaAlpha;
|
|
201
|
+
const clampedCps = clamp(instantCps, config.minCps, config.maxActiveCps);
|
|
202
|
+
emaCpsRef.current = emaCpsRef.current * (1 - config.emaAlpha) + clampedCps * config.emaAlpha;
|
|
203
|
+
}
|
|
204
|
+
lastInputTsRef.current = now;
|
|
205
|
+
lastInputCountRef.current = targetCountRef.current;
|
|
206
|
+
startFrameLoop();
|
|
207
|
+
}, [
|
|
208
|
+
config.emaAlpha,
|
|
209
|
+
config.largeAppendChars,
|
|
210
|
+
config.maxActiveCps,
|
|
211
|
+
config.maxCps,
|
|
212
|
+
config.maxFlushCps,
|
|
213
|
+
config.minCps,
|
|
214
|
+
content,
|
|
215
|
+
enabled,
|
|
216
|
+
startFrameLoop,
|
|
217
|
+
syncImmediate
|
|
218
|
+
]);
|
|
219
|
+
useEffect(() => {
|
|
220
|
+
return () => {
|
|
221
|
+
stopFrameLoop();
|
|
222
|
+
};
|
|
223
|
+
}, [stopFrameLoop]);
|
|
224
|
+
return displayedContent;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
//#endregion
|
|
228
|
+
export { useSmoothStreamContent };
|
|
229
|
+
//# sourceMappingURL=useSmoothStreamContent.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useSmoothStreamContent.mjs","names":["PRESET_CONFIG: Record<StreamSmoothingPreset, StreamSmoothingPresetConfig>","currentCps: number"],"sources":["../../../src/Markdown/SyntaxMarkdown/useSmoothStreamContent.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\n\nimport { type StreamSmoothingPreset } from '@/Markdown/type';\n\ninterface StreamSmoothingPresetConfig {\n activeInputWindowMs: number;\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 PRESET_CONFIG: Record<StreamSmoothingPreset, StreamSmoothingPresetConfig> = {\n balanced: {\n activeInputWindowMs: 220,\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 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 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\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 [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\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 syncImmediate = useCallback(\n (nextContent: string) => {\n stopFrameLoop();\n\n const chars = [...nextContent];\n const now = performance.now();\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, stopFrameLoop],\n );\n\n const startFrameLoop = useCallback(() => {\n if (rafRef.current !== null) return;\n\n const tick = (ts: number) => {\n if (lastFrameTsRef.current === null) {\n lastFrameTsRef.current = ts;\n rafRef.current = requestAnimationFrame(tick);\n return;\n }\n\n const dtSeconds = Math.max(0.001, Math.min((ts - lastFrameTsRef.current) / 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 = performance.now();\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 rafRef.current = requestAnimationFrame(tick);\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 rafRef.current = requestAnimationFrame(tick);\n };\n\n rafRef.current = requestAnimationFrame(tick);\n }, [\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 stopFrameLoop,\n ]);\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 = performance.now();\n const appendOnly = content.startsWith(prevTargetContent);\n\n if (!appendOnly) {\n syncImmediate(content);\n return;\n }\n\n const appended = content.slice(prevTargetContent.length);\n const appendedChars = [...appended];\n const appendedCount = appendedChars.length;\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.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 ]);\n\n useEffect(() => {\n return () => {\n stopFrameLoop();\n };\n }, [stopFrameLoop]);\n\n return displayedContent;\n};\n"],"mappings":"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"}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
2
|
|
|
3
3
|
//#region src/Markdown/SyntaxMarkdown/useStreamQueue.ts
|
|
4
|
-
const BASE_DELAY =
|
|
4
|
+
const BASE_DELAY = 18;
|
|
5
5
|
const ACCELERATION_FACTOR = .3;
|
|
6
6
|
const MAX_BLOCK_DURATION = 3e3;
|
|
7
|
-
const FADE_DURATION =
|
|
7
|
+
const FADE_DURATION = 280;
|
|
8
8
|
function countChars(text) {
|
|
9
9
|
return [...text].length;
|
|
10
10
|
}
|
|
@@ -45,15 +45,17 @@ function useStreamQueue(blocks) {
|
|
|
45
45
|
const queueLength = Math.max(0, tailIndex - effectiveRevealedCount - 1);
|
|
46
46
|
const animatingIndex = effectiveRevealedCount < tailIndex ? effectiveRevealedCount : -1;
|
|
47
47
|
const animatingCharCount = animatingIndex >= 0 ? countChars(blocks[animatingIndex]?.content ?? "") : 0;
|
|
48
|
+
const activeIndex = animatingIndex >= 0 ? animatingIndex : animatingIndex < 0 && tailIndex >= effectiveRevealedCount ? tailIndex : -1;
|
|
49
|
+
const activeCharCount = activeIndex >= 0 ? countChars(blocks[activeIndex]?.content ?? "") : 0;
|
|
48
50
|
const frozenRef = useRef({
|
|
49
51
|
delay: BASE_DELAY,
|
|
50
52
|
index: -1
|
|
51
53
|
});
|
|
52
|
-
if (
|
|
53
|
-
delay: computeCharDelay(queueLength,
|
|
54
|
-
index:
|
|
54
|
+
if (activeIndex >= 0 && activeIndex !== frozenRef.current.index) frozenRef.current = {
|
|
55
|
+
delay: computeCharDelay(queueLength, activeCharCount),
|
|
56
|
+
index: activeIndex
|
|
55
57
|
};
|
|
56
|
-
const charDelay =
|
|
58
|
+
const charDelay = activeIndex >= 0 ? frozenRef.current.delay : BASE_DELAY;
|
|
57
59
|
const onAnimationDone = useCallback(() => {
|
|
58
60
|
setRevealedCount(effectiveRevealedCount + 1);
|
|
59
61
|
}, [effectiveRevealedCount]);
|
|
@@ -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 =
|
|
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;\nconst FADE_DURATION = 280;\n\nfunction countChars(text: string): number {\n return [...text].length;\n}\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) + 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":";;;AASA,MAAM,aAAa;AACnB,MAAM,sBAAsB;AAC5B,MAAM,qBAAqB;AAC3B,MAAM,gBAAgB;AAEtB,SAAS,WAAW,MAAsB;AACxC,QAAO,CAAC,GAAG,KAAK,CAAC;;AAGnB,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,GAAG;AACtE,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,8 +1,8 @@
|
|
|
1
1
|
import { TypographyProps } from "./type.mjs";
|
|
2
|
-
import * as
|
|
2
|
+
import * as react36 from "react";
|
|
3
3
|
|
|
4
4
|
//#region src/Markdown/Typography.d.ts
|
|
5
|
-
declare const Typography:
|
|
5
|
+
declare const Typography: react36.NamedExoticComponent<TypographyProps>;
|
|
6
6
|
//#endregion
|
|
7
7
|
export { Typography };
|
|
8
8
|
//# sourceMappingURL=Typography.d.mts.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { FlexboxProps } from "../../../Flex/type.mjs";
|
|
2
2
|
import "../../../Flex/index.mjs";
|
|
3
|
-
import * as
|
|
3
|
+
import * as react76 from "react";
|
|
4
4
|
import { Ref } from "react";
|
|
5
5
|
|
|
6
6
|
//#region src/Markdown/components/SearchResultCards/index.d.ts
|
|
@@ -14,7 +14,7 @@ interface SearchResultCardsProps extends FlexboxProps {
|
|
|
14
14
|
dataSource: string[] | SearchResultItem[];
|
|
15
15
|
ref?: Ref<HTMLDivElement>;
|
|
16
16
|
}
|
|
17
|
-
declare const SearchResultCards:
|
|
17
|
+
declare const SearchResultCards: react76.NamedExoticComponent<SearchResultCardsProps>;
|
|
18
18
|
//#endregion
|
|
19
19
|
export { SearchResultCards, SearchResultCardsProps };
|
|
20
20
|
//# sourceMappingURL=index.d.mts.map
|
package/es/Markdown/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MarkdownProps, SyntaxMarkdownProps, TypographyProps } from "./type.mjs";
|
|
1
|
+
import { MarkdownProps, StreamSmoothingPreset, SyntaxMarkdownProps, TypographyProps } from "./type.mjs";
|
|
2
2
|
import { Markdown } from "./Markdown.mjs";
|
|
3
3
|
import { Typography } from "./Typography.mjs";
|
|
4
|
-
export { MarkdownProps, SyntaxMarkdownProps, Typography, TypographyProps, Markdown as default };
|
|
4
|
+
export { MarkdownProps, StreamSmoothingPreset, SyntaxMarkdownProps, Typography, TypographyProps, Markdown as default };
|
|
@@ -4,7 +4,9 @@ import { Root } from "hast";
|
|
|
4
4
|
interface StreamAnimatedOptions {
|
|
5
5
|
baseCharCount?: number;
|
|
6
6
|
charDelay?: number;
|
|
7
|
+
fadeDuration?: number;
|
|
7
8
|
revealed?: boolean;
|
|
9
|
+
timelineElapsedMs?: number;
|
|
8
10
|
}
|
|
9
11
|
declare const rehypeStreamAnimated: (options?: StreamAnimatedOptions) => (tree: Root) => void;
|
|
10
12
|
//#endregion
|
|
@@ -24,7 +24,8 @@ function hasClass(node, cls) {
|
|
|
24
24
|
return false;
|
|
25
25
|
}
|
|
26
26
|
const rehypeStreamAnimated = (options = {}) => {
|
|
27
|
-
const { charDelay = 20, baseCharCount = 0, revealed = false } = options;
|
|
27
|
+
const { charDelay = 20, fadeDuration = 150, baseCharCount = 0, revealed = false, timelineElapsedMs } = options;
|
|
28
|
+
const hasTimeline = typeof timelineElapsedMs === "number" && Number.isFinite(timelineElapsedMs);
|
|
28
29
|
return (tree) => {
|
|
29
30
|
let globalCharIndex = 0;
|
|
30
31
|
const shouldSkip = (node) => {
|
|
@@ -33,11 +34,22 @@ const rehypeStreamAnimated = (options = {}) => {
|
|
|
33
34
|
const wrapText = (node) => {
|
|
34
35
|
const newChildren = [];
|
|
35
36
|
for (const child of node.children) if (child.type === "text") for (const char of child.value) {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
const relativeIndex = globalCharIndex - baseCharCount;
|
|
38
|
+
let className = "stream-char";
|
|
39
|
+
let delay;
|
|
40
|
+
if (revealed) className = "stream-char stream-char-revealed";
|
|
41
|
+
else if (hasTimeline) {
|
|
42
|
+
const progress = timelineElapsedMs - globalCharIndex * charDelay;
|
|
43
|
+
if (progress >= fadeDuration) className = "stream-char stream-char-revealed";
|
|
44
|
+
else delay = -progress;
|
|
45
|
+
} else if (relativeIndex >= 0) delay = relativeIndex * charDelay;
|
|
46
|
+
else {
|
|
47
|
+
const elapsed = -relativeIndex * charDelay;
|
|
48
|
+
if (elapsed >= fadeDuration) className = "stream-char stream-char-revealed";
|
|
49
|
+
else delay = -elapsed;
|
|
40
50
|
}
|
|
51
|
+
const properties = { className };
|
|
52
|
+
if (delay !== void 0 && delay !== 0) properties.style = `animation-delay:${delay}ms`;
|
|
41
53
|
newChildren.push({
|
|
42
54
|
children: [{
|
|
43
55
|
type: "text",
|