ds-markdown 0.2.3-beta.1 → 0.2.4-beta.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/dist/cjs/index.js CHANGED
@@ -4,443 +4,91 @@ Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  var jsxRuntime = require('react/jsx-runtime');
6
6
  var react = require('react');
7
- var Markdown$2 = require('react-markdown');
8
7
  var gfmPlugin = require('remark-gfm');
9
8
  var classNames = require('classnames');
9
+ var ReactMarkdown = require('react-markdown');
10
10
  var reactSyntaxHighlighter = require('react-syntax-highlighter');
11
11
 
12
- /**
13
- * 将括号格式的数学公式转换为美元符号格式
14
- * 支持以下转换:
15
- * - \(...\) → $...$ (行内公式)
16
- * - \[...\] → $$...$$ (块级公式)
17
- *
18
- * 特殊处理:
19
- * - 如果文本包含 Markdown 超链接,则跳过转换以避免误处理
20
- * - 使用占位符机制保护块级公式内的括号不被误转换
21
- *
22
- * @param value 要转换的字符串
23
- * @returns 转换后的字符串
24
- */
25
- const replaceMathBracket = (value) => {
26
- // 1. 提取所有块级公式内容,临时替换为占位符, [...]
27
- const blockMatches = [];
28
- let replaced = value.replace(/\\+\[([\s\S]+?)\\+\]/g, (_m, p1) => {
29
- blockMatches.push(p1);
30
- return `__BLOCK_MATH_${blockMatches.length - 1}__`;
31
- });
32
- // 也需要兼容 $$ xxxx $$ 这种写法
33
- replaced = replaced.replace(/\$\$([\s\S]+?)\$\$/g, (_m, p1) => {
34
- blockMatches.push(p1);
35
- return `__BLOCK_MATH_${blockMatches.length - 1}__`;
36
- });
37
- // 2. 替换块级公式外部的 ( ... ) 为 $...$
38
- replaced = replaced.replace(/\\+\(([^)]+?)\\+\)/g, (_m, p1) => {
39
- return '$' + p1 + '$';
40
- });
41
- // 3. 还原块级公式内容,保持其内部小括号原样
42
- replaced = replaced.replace(/__BLOCK_MATH_(\d+)__/g, (_m, idx) => {
43
- return '$$' + blockMatches[Number(idx)] + '$$';
44
- });
45
- return replaced;
46
- };
47
-
48
- const DEFAULT_THEME = 'light';
49
- const DEFAULT_ANSWER_TYPE = 'answer';
50
- const DEFAULT_PLUGINS = [];
51
- const MarkdownThemeContext = react.createContext({
52
- state: {
53
- theme: DEFAULT_THEME,
54
- answerType: DEFAULT_ANSWER_TYPE,
55
- },
56
- methods: {},
57
- });
58
- const MarkdownThemeProvider = ({ value = {}, children }) => {
59
- const contextValue = react.useMemo(() => ({
60
- state: {
61
- theme: DEFAULT_THEME,
62
- answerType: DEFAULT_ANSWER_TYPE,
63
- ...value,
64
- },
65
- methods: {
66
- // 这里可以添加主题相关的方法实现
67
- },
68
- }), [value]);
69
- return jsxRuntime.jsx(MarkdownThemeContext.Provider, { value: contextValue, children: children });
70
- };
71
- const useMarkdownThemeContext = () => react.useContext(MarkdownThemeContext);
72
- const useThemeState = () => {
73
- return react.useContext(MarkdownThemeContext).state;
74
- };
75
-
76
- const CodeBlockWrap = ({ children, title }) => {
77
- const { theme } = useThemeState();
78
- return (jsxRuntime.jsxs("div", { className: `md-code-block md-code-block-${theme}`, children: [jsxRuntime.jsx("div", { className: "md-code-block-banner-wrap", children: jsxRuntime.jsx("div", { className: "md-code-block-banner md-code-block-banner-lite", children: title }) }), jsxRuntime.jsx("div", { className: "md-code-block-content", children: children })] }));
79
- };
80
-
81
- const CheckMarkIcon = ({ size }) => {
82
- return (jsxRuntime.jsx("svg", { width: size, height: size, viewBox: `0 0 ${size} ${size}`, fill: "none", xmlns: "http://www.w3.org/2000/svg", children: jsxRuntime.jsx("path", { "fill-rule": "evenodd", "clip-rule": "evenodd", d: "M9.338 21.575a1.058 1.058 0 0 1-.53-.363L2.275 13.17a1.063 1.063 0 0 1 1.65-1.341l5.63 6.928L19.33 3.86a1.064 1.064 0 0 1 1.778 1.167L10.551 21.115a1.065 1.065 0 0 1-1.213.46z", fill: "currentColor" }) }));
83
- };
84
- const CopyIcon = ({ size }) => {
85
- return (jsxRuntime.jsxs("svg", { width: size, height: size, viewBox: `0 0 ${size} ${size}`, fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [jsxRuntime.jsx("path", { d: "M3.65169 12.9243C3.68173 13.1045 3.74181 13.2748 3.80189 13.445C3.87198 13.6052 3.96211 13.7654 4.06225 13.9156C4.16238 14.0658 4.27253 14.206 4.4027 14.3362C4.52286 14.4663 4.66306 14.5765 4.81326 14.6766C4.96346 14.7768 5.11366 14.8569 5.28389 14.927C5.44411 14.9971 5.61434 15.0571 5.79459 15.0872C5.97483 15.1272 6.14506 15.1373 6.3253 15.1373V16.9196C6.30739 16.9196 6.28949 16.9195 6.27159 16.9193C5.9991 16.9158 5.72659 16.8859 5.4541 16.8295C5.16371 16.7694 4.88334 16.6893 4.61298 16.5692C4.3326 16.459 4.08226 16.3188 3.83193 16.1586C3.59161 15.9884 3.3613 15.7981 3.15102 15.5878C2.94074 15.3776 2.7605 15.1473 2.59027 14.9069C2.43006 14.6566 2.28986 14.3962 2.17972 14.1259C2.06957 13.8455 1.97944 13.5651 1.91936 13.2747C1.86929 12.9843 1.83926 12.684 1.83926 12.3936V6.26532C1.83926 5.96492 1.86929 5.67456 1.91936 5.38417C1.97944 5.09378 2.06957 4.80338 2.17972 4.53302C2.28986 4.26265 2.43006 4.0023 2.59027 3.75197C2.7605 3.50163 2.94074 3.27132 3.15102 3.06104C3.3613 2.85076 3.59161 2.67052 3.83193 2.50029C4.08226 2.33006 4.3326 2.19987 4.61298 2.07971C4.88334 1.96956 5.16371 1.87943 5.4541 1.81935C5.74449 1.75927 6.03491 1.73926 6.3253 1.73926H12.3934C12.6838 1.73926 12.9842 1.75927 13.2746 1.81935C13.555 1.87943 13.8354 1.96956 14.1158 2.07971C14.3861 2.19987 14.6465 2.33006 14.8868 2.50029C15.1371 2.67052 15.3574 2.85076 15.5677 3.06104C15.778 3.27132 15.9582 3.50163 16.1284 3.75197C16.2887 4.0023 16.4288 4.26265 16.539 4.53302C16.6592 4.80338 16.7393 5.09378 16.7994 5.38417C16.8558 5.65722 16.8858 5.93024 16.8892 6.21161C16.8894 6.22948 16.8895 6.24739 16.8895 6.26532H15.1271C15.1271 6.08508 15.1071 5.90486 15.067 5.72462C15.037 5.55439 14.9869 5.38415 14.9168 5.21392C14.8467 5.04369 14.7566 4.88347 14.6665 4.73327C14.5664 4.58307 14.4462 4.45289 14.326 4.32271C14.1959 4.19254 14.0557 4.08239 13.9055 3.98226C13.7553 3.88212 13.6051 3.79202 13.4348 3.72193C13.2746 3.65184 13.1044 3.60174 12.9242 3.5717C12.7539 3.53165 12.5737 3.51163 12.3934 3.51163H6.3253C6.14506 3.51163 5.97483 3.53165 5.79459 3.5717C5.61434 3.60174 5.44411 3.65184 5.28389 3.72193C5.11366 3.79202 4.96346 3.88212 4.81326 3.98226C4.66306 4.08239 4.52286 4.19254 4.4027 4.32271C4.27253 4.45289 4.16238 4.58307 4.06225 4.73327C3.96211 4.88347 3.87198 5.04369 3.80189 5.21392C3.74181 5.38415 3.68173 5.55439 3.65169 5.72462C3.61164 5.90486 3.60164 6.08508 3.60164 6.26532V12.3936C3.60164 12.5638 3.61164 12.744 3.65169 12.9243Z", fill: "currentColor" }), jsxRuntime.jsx("path", { "fill-rule": "evenodd", "clip-rule": "evenodd", d: "M9.66972 21.6772C9.39936 21.567 9.13902 21.4268 8.8987 21.2566C8.64836 21.0964 8.42804 20.9061 8.21776 20.6959C8.00748 20.4856 7.81723 20.2553 7.65701 20.015C7.4968 19.7646 7.3566 19.5043 7.24646 19.2239C7.12629 18.9535 7.04621 18.6731 6.98613 18.3727C6.92605 18.0823 6.89601 17.792 6.89601 17.4915V11.3733C6.89601 11.0729 6.92605 10.7825 6.98613 10.4922C7.04621 10.1918 7.12629 9.91137 7.24646 9.64101C7.3566 9.36063 7.4968 9.10028 7.65701 8.85996C7.81723 8.60962 8.00748 8.37931 8.21776 8.16903C8.42804 7.95875 8.64836 7.76849 8.8987 7.60828C9.13902 7.43805 9.39936 7.29785 9.66972 7.1877C9.94009 7.07755 10.2205 6.98745 10.5108 6.92737C10.8012 6.86729 11.0916 6.83725 11.392 6.83725H17.4602C17.7506 6.83725 18.041 6.86729 18.3313 6.92737C18.6217 6.98745 18.9021 7.07755 19.1725 7.1877C19.4529 7.29785 19.7032 7.43805 19.9535 7.60828C20.1938 7.76849 20.4242 7.95875 20.6345 8.16903C20.8447 8.37931 21.025 8.60962 21.1952 8.85996C21.3554 9.10028 21.4956 9.36063 21.6058 9.64101C21.7159 9.91137 21.806 10.1918 21.8661 10.4922C21.9162 10.7825 21.9462 11.0729 21.9462 11.3733V17.4915C21.9462 17.792 21.9162 18.0823 21.8661 18.3727C21.806 18.6731 21.7159 18.9535 21.6058 19.2239C21.4956 19.5043 21.3554 19.7646 21.1952 20.015C21.025 20.2553 20.8447 20.4856 20.6345 20.6959C20.4242 20.9061 20.1938 21.0964 19.9535 21.2566C19.7032 21.4268 19.4529 21.567 19.1725 21.6772C18.9021 21.7973 18.6217 21.8774 18.3313 21.9375C18.041 21.9976 17.7506 22.0276 17.4602 22.0276H11.392C11.0916 22.0276 10.8012 21.9976 10.5108 21.9375C10.2205 21.8774 9.94009 21.7973 9.66972 21.6772ZM10.8613 8.6697C11.0316 8.63966 11.2118 8.61965 11.392 8.61965H17.4602C17.6404 8.61965 17.8107 8.63966 17.9909 8.6697C18.1611 8.70975 18.3314 8.75983 18.5016 8.82992C18.6618 8.90001 18.822 8.98012 18.9722 9.08026C19.1224 9.18039 19.2626 9.30055 19.3828 9.42071C19.513 9.55088 19.6231 9.69109 19.7232 9.84129C19.8234 9.99149 19.9035 10.1517 19.9736 10.3219C20.0437 10.4821 20.0937 10.6624 20.1338 10.8326C20.1638 11.0129 20.1838 11.1931 20.1838 11.3733V17.4915C20.1838 17.6718 20.1638 17.852 20.1338 18.0323C20.0937 18.2125 20.0437 18.3828 19.9736 18.543C19.9035 18.7132 19.8234 18.8734 19.7232 19.0236C19.6231 19.1738 19.513 19.314 19.3828 19.4342C19.2626 19.5643 19.1224 19.6845 18.9722 19.7846C18.822 19.8848 18.6618 19.9649 18.5016 20.035C18.3314 20.1051 18.1611 20.1551 17.9909 20.1952C17.8107 20.2252 17.6404 20.2452 17.4602 20.2452H11.392C11.2118 20.2452 11.0316 20.2252 10.8613 20.1952C10.6811 20.1551 10.5108 20.1051 10.3506 20.035C10.1804 19.9649 10.0202 19.8848 9.87 19.7846C9.72982 19.6845 9.58962 19.5643 9.45945 19.4342C9.33929 19.314 9.21913 19.1738 9.119 19.0236C9.01886 18.8734 8.93875 18.7132 8.86866 18.543C8.79857 18.3828 8.74847 18.2125 8.71843 18.0323C8.67838 17.852 8.65836 17.6718 8.65836 17.4915V11.3733C8.65836 11.1931 8.67838 11.0129 8.71843 10.8326C8.74847 10.6624 8.79857 10.4821 8.86866 10.3219C8.93875 10.1517 9.01886 9.99149 9.119 9.84129C9.21913 9.69109 9.33929 9.55088 9.45945 9.42071C9.58962 9.30055 9.72982 9.18039 9.87 9.08026C10.0202 8.98012 10.1804 8.90001 10.3506 8.82992C10.5108 8.75983 10.6811 8.70975 10.8613 8.6697Z", fill: "currentColor" })] }));
86
- };
87
- const DownloadIcon = ({ size }) => {
88
- return (jsxRuntime.jsxs("svg", { width: size, height: size, viewBox: `0 0 ${size} ${size}`, fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [jsxRuntime.jsx("path", { "fill-rule": "evenodd", "clip-rule": "evenodd", d: "M12 2.55a.97.97 0 0 1 .982.956v13.04a.97.97 0 0 1-.982.957.97.97 0 0 1-.982-.956V3.507A.97.97 0 0 1 12 2.55z", fill: "currentColor" }), jsxRuntime.jsx("path", { "fill-rule": "evenodd", "clip-rule": "evenodd", d: "M19.418 9.808c.382.375.37.971-.027 1.332l-6.7 6.085a1.04 1.04 0 0 1-1.41-.025.905.905 0 0 1 .027-1.332l6.7-6.085a1.04 1.04 0 0 1 1.41.025z", fill: "currentColor" }), jsxRuntime.jsx("path", { "fill-rule": "evenodd", "clip-rule": "evenodd", d: "M4.582 9.808a1.04 1.04 0 0 1 1.41-.025l6.7 6.085c.397.361.409.957.027 1.332a1.04 1.04 0 0 1-1.41.025l-6.7-6.085a.905.905 0 0 1-.027-1.332z", fill: "currentColor" }), jsxRuntime.jsx("path", { "fill-rule": "evenodd", "clip-rule": "evenodd", d: "M3.068 16.46a.97.97 0 0 1 .983.956v1.739c0 .432.36.782.803.782h14.291c.445 0 .804-.35.804-.782v-1.739a.97.97 0 0 1 .983-.956.97.97 0 0 1 .982.956v1.739c0 1.488-1.24 2.695-2.769 2.695H4.855c-1.53 0-2.77-1.207-2.77-2.695v-1.739a.97.97 0 0 1 .983-.956z", fill: "currentColor" })] }));
89
- };
90
-
91
- const zhCN = {
92
- codeBlock: {
93
- copy: '复制',
94
- copied: '已复制',
95
- download: '下载',
96
- downloaded: '已下载',
97
- },
98
- mermaid: {
99
- diagram: '图表',
100
- code: '代码',
101
- zoomOut: '缩小',
102
- zoomIn: '放大',
103
- download: '下载',
104
- fullScreen: '全屏',
105
- exitFullScreen: '退出全屏',
106
- downloadImage: '下载图片',
107
- downloadedImage: '已下载',
108
- copyImage: '复制图片',
109
- copiedImage: '已复制',
110
- fitInView: '适应页面',
111
- },
112
- };
113
-
114
- const ConfigContext = react.createContext({
115
- locale: zhCN,
116
- });
117
- const ConfigProvider = ({ locale, children, mermaidConfig, katexConfig }) => {
118
- const contextValue = react.useMemo(() => {
119
- const contextValue = {
120
- locale: locale || zhCN,
121
- };
122
- if (mermaidConfig) {
123
- contextValue.mermaidConfig = mermaidConfig;
124
- }
125
- if (katexConfig) {
126
- contextValue.katexConfig = katexConfig;
127
- }
128
- return contextValue;
129
- }, [locale, mermaidConfig, katexConfig]);
130
- return jsxRuntime.jsx(ConfigContext.Provider, { value: contextValue, children: children });
131
- };
132
- // Hook 用于在组件中使用配置
133
- const useConfig = () => {
134
- const context = react.useContext(ConfigContext);
135
- return context;
136
- };
137
- // Hook 用于获取当前语言包
138
- const useLocale = () => {
139
- const { locale } = useConfig();
140
- return locale;
141
- };
12
+ const __DEV__$1 = process.env.NODE_ENV === 'development';
13
+ const ID_PREFIX__ = '__ds-markdown__';
14
+ /** 数学公式插件id */
15
+ const katexId = `${ID_PREFIX__}katex`;
142
16
 
143
- const Button = ({ className = '', children, icon, onClick, style, disabled = false, ...restProps }) => {
144
- const handleClick = (e) => {
145
- if (disabled) {
146
- e.preventDefault();
147
- return;
148
- }
149
- onClick?.();
150
- };
151
- return (jsxRuntime.jsxs("div", { role: "button", className: classNames({
152
- 'ds-button': true,
153
- 'ds-button__disabled': disabled,
154
- }, className), onClick: handleClick, style: style, "aria-disabled": disabled, ...restProps, children: [icon && jsxRuntime.jsx("div", { className: "ds-button__icon", children: icon }), children] }));
155
- };
17
+ const __DEV__ = process.env.NODE_ENV === 'development';
156
18
 
157
- const SuccessButton = (props) => {
158
- const { onClick, icon, executeText, children, ...rest } = props;
159
- const [isLoading, setIsLoading] = react.useState(false);
160
- const [isSuccess, setIsSuccess] = react.useState(false);
161
- const isUnmounted = react.useRef(false);
162
- const handleClick = async () => {
163
- if (isLoading || isSuccess) {
164
- return;
19
+ const useTypingTask = (options) => {
20
+ const { timerType = 'setTimeout', interval, charsRef, onEnd, onStart, onBeforeTypedChar, onTypedChar, processCharDisplay, wholeContentRef, disableTyping, triggerUpdate, resetWholeContent, } = options;
21
+ /** 是否卸载 */
22
+ const isUnmountRef = react.useRef(false);
23
+ /** 是否正在打字 */
24
+ const isTypingRef = react.useRef(false);
25
+ /** 动画帧ID */
26
+ const animationFrameRef = react.useRef(null);
27
+ /** 传统定时器(兼容模式) */
28
+ const timerRef = react.useRef(null);
29
+ // 已经打过的字记录
30
+ const typedCharsRef = react.useRef(undefined);
31
+ // 是否主动调用 stop 方法
32
+ const typedIsManualStopRef = react.useRef(false);
33
+ const disableTypingRef = react.useRef(disableTyping);
34
+ disableTypingRef.current = disableTyping;
35
+ const intervalRef = react.useRef(interval);
36
+ intervalRef.current = interval;
37
+ // 记录本次打字任务的初始/最高剩余字符总量,用于计算剩余占比(流式追加时会增大)
38
+ const initialRemainTotalRef = react.useRef(0);
39
+ /**
40
+ * 根据剩余字符数与曲线配置,计算当前打字间隔(毫秒)
41
+ */
42
+ const getCurrentInterval = (remainCharsLength) => {
43
+ const cfg = intervalRef.current;
44
+ if (typeof cfg === 'number')
45
+ return cfg;
46
+ // 动态更新初始参考总量,考虑流式场景新增字符
47
+ if (remainCharsLength > initialRemainTotalRef.current) {
48
+ initialRemainTotalRef.current = remainCharsLength;
165
49
  }
166
- try {
167
- // 如果onClick不是异步函数,则直接调用
168
- const returnValue = onClick();
169
- if (returnValue instanceof Promise) {
170
- setIsLoading(true);
171
- const result = await returnValue;
172
- if (result) {
173
- setIsSuccess(true);
174
- setTimeout(() => {
175
- if (!isUnmounted.current) {
176
- setIsSuccess(false);
177
- }
178
- }, 1000);
179
- }
50
+ const baseTotal = initialRemainTotalRef.current || remainCharsLength || 1;
51
+ // r: 剩余占比 [0,1],越大表示剩余越多
52
+ const r = Math.max(0, Math.min(1, remainCharsLength / baseTotal));
53
+ // 曲线函数(优先使用自定义)
54
+ const pickCurveFn = () => {
55
+ if (typeof cfg.curveFn === 'function')
56
+ return cfg.curveFn;
57
+ switch (cfg.curve) {
58
+ case 'linear':
59
+ return (x) => x;
60
+ case 'ease-in':
61
+ return (x) => x * x; // 加速慢起
62
+ case 'ease-out':
63
+ return (x) => 1 - (1 - x) * (1 - x); // 减速快止
64
+ case 'ease-in-out':
65
+ return (x) => (x < 0.5 ? 2 * x * x : 1 - Math.pow(-2 * x + 2, 2) / 2);
66
+ case 'step-start':
67
+ return (x) => (x > 0 ? 1 : 0);
68
+ case 'step-end':
69
+ return (x) => (x < 1 ? 0 : 1);
70
+ case 'ease':
71
+ default:
72
+ // 近似通用 ease
73
+ return (x) => 1 - Math.pow(1 - x, 1.6);
180
74
  }
181
- }
182
- catch (error) {
183
- setIsSuccess(false);
184
- }
185
- finally {
186
- setIsLoading(false);
187
- }
75
+ };
76
+ const curveFn = pickCurveFn();
77
+ const y = curveFn(r); // y ∈ [0,1],随 r 增大而增大
78
+ // 设计:剩余越多 => 越快(更小的间隔)。
79
+ // interval = min + (max - min) * (1 - y)
80
+ const min = Math.max(0, cfg.min);
81
+ const max = Math.max(min, cfg.max);
82
+ const intervalMs = min + (max - min) * (1 - y);
83
+ return intervalMs;
84
+ };
85
+ const getChars = () => {
86
+ return charsRef.current;
188
87
  };
189
88
  react.useEffect(() => {
190
- isUnmounted.current = false;
89
+ isUnmountRef.current = false;
191
90
  return () => {
192
- isUnmounted.current = true;
193
- };
194
- }, []);
195
- return (jsxRuntime.jsx(Button, { ...rest, onClick: handleClick, icon: isSuccess ? jsxRuntime.jsx(CheckMarkIcon, { size: 24 }) : icon, children: isSuccess ? executeText || children : children }));
196
- };
197
-
198
- const CopyButton = ({ codeContent, style, className }) => {
199
- const { locale } = useConfig();
200
- const handleCopy = async () => {
201
- try {
202
- await navigator.clipboard.writeText(codeContent || '');
203
- return true;
204
- }
205
- catch (err) {
206
- // 降级方案:使用传统方法
207
- const textArea = document.createElement('textarea');
208
- textArea.value = codeContent || '';
209
- textArea.select();
210
- document.execCommand('copy');
211
- return true;
212
- }
213
- };
214
- return (jsxRuntime.jsx(SuccessButton, { onClick: handleCopy, icon: jsxRuntime.jsx(CopyIcon, { size: 24 }), executeText: locale.codeBlock.copied || 'copied', style: style, className: className, children: locale.codeBlock.copy || 'copy' }));
215
- };
216
-
217
- const DownloadButton = ({ codeContent, language, style, className }) => {
218
- const { locale } = useConfig();
219
- // 下载文件
220
- const handleDownload = async () => {
221
- if (!codeContent)
222
- return false;
223
- const blob = new Blob([codeContent], { type: 'text/plain;charset=utf-8' });
224
- const url = URL.createObjectURL(blob);
225
- const link = document.createElement('a');
226
- // 根据语言设置文件扩展名
227
- const getFileExtension = (lang) => {
228
- const extensions = {
229
- javascript: 'js',
230
- typescript: 'ts',
231
- jsx: 'jsx',
232
- tsx: 'tsx',
233
- python: 'py',
234
- java: 'java',
235
- cpp: 'cpp',
236
- c: 'c',
237
- csharp: 'cs',
238
- php: 'php',
239
- ruby: 'rb',
240
- go: 'go',
241
- rust: 'rs',
242
- swift: 'swift',
243
- kotlin: 'kt',
244
- scala: 'scala',
245
- shell: 'sh',
246
- bash: 'sh',
247
- powershell: 'ps1',
248
- sql: 'sql',
249
- html: 'html',
250
- css: 'css',
251
- scss: 'scss',
252
- less: 'less',
253
- json: 'json',
254
- xml: 'xml',
255
- yaml: 'yml',
256
- markdown: 'md',
257
- dockerfile: 'dockerfile',
258
- };
259
- return extensions[lang.toLowerCase()] || 'txt';
260
- };
261
- const fileName = `code.${getFileExtension(language)}`;
262
- link.href = url;
263
- link.download = fileName;
264
- document.body.appendChild(link);
265
- link.click();
266
- document.body.removeChild(link);
267
- URL.revokeObjectURL(url);
268
- return true;
269
- };
270
- return (jsxRuntime.jsx(SuccessButton, { onClick: handleDownload, icon: jsxRuntime.jsx(DownloadIcon, { size: 24 }), executeText: locale.codeBlock.downloaded || 'Downloaded', style: style, className: className, children: locale.codeBlock.download || 'Download' }));
271
- };
272
-
273
- const CodeBlockActions = ({ codeContent, language }) => {
274
- return (jsxRuntime.jsxs("div", { className: "md-code-block-header-actions", children: [jsxRuntime.jsx(CopyButton, { codeContent: codeContent, style: { fontSize: 13, padding: '0 4px' } }), jsxRuntime.jsx(DownloadButton, { codeContent: codeContent, language: language, style: { fontSize: 13, padding: '0 4px' } })] }));
275
- };
276
-
277
- const BlockWrap = ({ children, language, codeContent }) => {
278
- const { state: themeState } = useMarkdownThemeContext();
279
- // 从 context 中获取主题配置
280
- const currentCodeBlock = themeState.codeBlock;
281
- const { headerActions = true } = currentCodeBlock || {};
282
- const renderHeaderActions = () => {
283
- if (headerActions === true) {
284
- return jsxRuntime.jsx(CodeBlockActions, { codeContent: codeContent, language: language });
285
- }
286
- return headerActions;
287
- };
288
- return (jsxRuntime.jsx(CodeBlockWrap, { title: jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: "md-code-block-language", children: language }), renderHeaderActions()] }), children: children }));
289
- };
290
-
291
- const __DEV__ = process.env.NODE_ENV === 'development';
292
- const ID_PREFIX__ = '__ds-markdown__';
293
- /** 数学公式插件id */
294
- const katexId = `${ID_PREFIX__}katex`;
295
-
296
- const HighlightCode = ({ code, language }) => {
297
- return (jsxRuntime.jsx(reactSyntaxHighlighter.Prism, { useInlineStyles: false, language: language, style: {}, children: code }));
298
- };
299
-
300
- const CodeComponent = ({ className, children = '' }) => {
301
- const match = /language-(\w+)/.exec(className || '');
302
- const codeContent = String(children).replace(/\n$/, '');
303
- return match ? (jsxRuntime.jsx(BlockWrap, { language: match[1], codeContent: codeContent, children: jsxRuntime.jsx(HighlightCode, { code: codeContent, language: match[1] }) })) : (jsxRuntime.jsx("code", { className: className, children: children }));
304
- };
305
- const HighReactMarkdown = ({ children: _children, ...props }) => {
306
- const { state: themeState } = useMarkdownThemeContext();
307
- const { katexConfig } = useConfig();
308
- // 从 context 中获取主题配置
309
- const currentMath = themeState.math;
310
- const currentPlugins = themeState.plugins;
311
- const mathSplitSymbol = currentMath?.splitSymbol ?? 'dollar';
312
- const finalReplaceMathBracket = currentMath?.replaceMathBracket ?? replaceMathBracket;
313
- const { remarkPlugins, rehypePlugins, hasKatexPlugin, components } = react.useMemo(() => {
314
- let hasKatexPlugin = false;
315
- const components = {};
316
- const remarkPlugins = [gfmPlugin];
317
- const rehypePlugins = [];
318
- if (!currentPlugins) {
319
- return {
320
- remarkPlugins,
321
- rehypePlugins,
322
- };
323
- }
324
- currentPlugins.forEach((plugin) => {
325
- if (plugin.id === katexId) {
326
- hasKatexPlugin = true;
327
- remarkPlugins.push(plugin.remarkPlugin);
328
- rehypePlugins.push([plugin.rehypePlugin, katexConfig]);
329
- }
330
- else {
331
- if (plugin.rehypePlugin) {
332
- rehypePlugins.push(plugin.rehypePlugin);
333
- }
334
- if (plugin.remarkPlugin) {
335
- remarkPlugins.push(plugin.remarkPlugin);
336
- }
337
- }
338
- if (plugin.components) {
339
- Object.assign(components, plugin.components);
340
- }
341
- });
342
- return {
343
- remarkPlugins,
344
- rehypePlugins,
345
- hasKatexPlugin,
346
- components,
347
- };
348
- }, [currentPlugins]);
349
- const children = react.useMemo(() => {
350
- /** 如果存在数学公式插件,并且数学公式分隔符为括号,则替换成 $ 符号 */
351
- if (hasKatexPlugin && mathSplitSymbol === 'bracket') {
352
- return finalReplaceMathBracket(_children);
353
- }
354
- return _children;
355
- }, [hasKatexPlugin, mathSplitSymbol, finalReplaceMathBracket, _children]);
356
- return (jsxRuntime.jsx(Markdown$2, { remarkPlugins: remarkPlugins, rehypePlugins: rehypePlugins, components: {
357
- code: CodeComponent,
358
- table: ({ children, ...props }) => {
359
- return (jsxRuntime.jsx("div", { className: "markdown-table-wrapper", children: jsxRuntime.jsx("table", { className: "ds-markdown-table", children: children }) }));
360
- },
361
- ...components,
362
- }, ...props, children: children }));
363
- };
364
- var HighReactMarkdown$1 = react.memo(HighReactMarkdown);
365
-
366
- function splitGraphemes(input) {
367
- return Array.from(input);
368
- }
369
-
370
- const useTypingTask = (options) => {
371
- const { timerType = 'setTimeout', interval, charsRef, onEnd, onStart, onBeforeTypedChar, onTypedChar, processCharDisplay, wholeContentRef, disableTyping, triggerUpdate, resetWholeContent, } = options;
372
- /** 是否卸载 */
373
- const isUnmountRef = react.useRef(false);
374
- /** 是否正在打字 */
375
- const isTypingRef = react.useRef(false);
376
- /** 动画帧ID */
377
- const animationFrameRef = react.useRef(null);
378
- /** 传统定时器(兼容模式) */
379
- const timerRef = react.useRef(null);
380
- // 已经打过的字记录
381
- const typedCharsRef = react.useRef(undefined);
382
- // 是否主动调用 stop 方法
383
- const typedIsManualStopRef = react.useRef(false);
384
- const disableTypingRef = react.useRef(disableTyping);
385
- disableTypingRef.current = disableTyping;
386
- const intervalRef = react.useRef(interval);
387
- intervalRef.current = interval;
388
- // 记录本次打字任务的初始/最高剩余字符总量,用于计算剩余占比(流式追加时会增大)
389
- const initialRemainTotalRef = react.useRef(0);
390
- /**
391
- * 根据剩余字符数与曲线配置,计算当前打字间隔(毫秒)
392
- */
393
- const getCurrentInterval = (remainCharsLength) => {
394
- const cfg = intervalRef.current;
395
- if (typeof cfg === 'number')
396
- return cfg;
397
- // 动态更新初始参考总量,考虑流式场景新增字符
398
- if (remainCharsLength > initialRemainTotalRef.current) {
399
- initialRemainTotalRef.current = remainCharsLength;
400
- }
401
- const baseTotal = initialRemainTotalRef.current || remainCharsLength || 1;
402
- // r: 剩余占比 [0,1],越大表示剩余越多
403
- const r = Math.max(0, Math.min(1, remainCharsLength / baseTotal));
404
- // 曲线函数(优先使用自定义)
405
- const pickCurveFn = () => {
406
- if (typeof cfg.curveFn === 'function')
407
- return cfg.curveFn;
408
- switch (cfg.curve) {
409
- case 'linear':
410
- return (x) => x;
411
- case 'ease-in':
412
- return (x) => x * x; // 加速慢起
413
- case 'ease-out':
414
- return (x) => 1 - (1 - x) * (1 - x); // 减速快止
415
- case 'ease-in-out':
416
- return (x) => (x < 0.5 ? 2 * x * x : 1 - Math.pow(-2 * x + 2, 2) / 2);
417
- case 'step-start':
418
- return (x) => (x > 0 ? 1 : 0);
419
- case 'step-end':
420
- return (x) => (x < 1 ? 0 : 1);
421
- case 'ease':
422
- default:
423
- // 近似通用 ease
424
- return (x) => 1 - Math.pow(1 - x, 1.6);
425
- }
426
- };
427
- const curveFn = pickCurveFn();
428
- const y = curveFn(r); // y ∈ [0,1],随 r 增大而增大
429
- // 设计:剩余越多 => 越快(更小的间隔)。
430
- // interval = min + (max - min) * (1 - y)
431
- const min = Math.max(0, cfg.min);
432
- const max = Math.max(min, cfg.max);
433
- const intervalMs = min + (max - min) * (1 - y);
434
- return intervalMs;
435
- };
436
- const getChars = () => {
437
- return charsRef.current;
438
- };
439
- react.useEffect(() => {
440
- isUnmountRef.current = false;
441
- return () => {
442
- isUnmountRef.current = true;
443
- clearTimer();
91
+ isUnmountRef.current = true;
444
92
  };
445
93
  }, []);
446
94
  /**
@@ -451,11 +99,10 @@ const useTypingTask = (options) => {
451
99
  if (!onStart) {
452
100
  return;
453
101
  }
454
- const prevStr = wholeContentRef.current[char.answerType].content;
102
+ const prevStr = wholeContentRef.current.content;
455
103
  onStart({
456
104
  currentIndex: prevStr.length,
457
105
  currentChar: char.content,
458
- answerType: char.answerType,
459
106
  prevStr,
460
107
  });
461
108
  };
@@ -463,14 +110,13 @@ const useTypingTask = (options) => {
463
110
  * 触发打字结束回调
464
111
  */
465
112
  const triggerOnEnd = (data) => {
113
+ var _a;
466
114
  if (!onEnd) {
467
115
  return;
468
116
  }
469
117
  onEnd({
470
- str: wholeContentRef.current.answer.content,
471
- answerStr: wholeContentRef.current.answer.content,
472
- thinkingStr: wholeContentRef.current.thinking.content,
473
- manual: data?.manual ?? false,
118
+ str: wholeContentRef.current.content,
119
+ manual: (_a = data === null || data === void 0 ? void 0 : data.manual) !== null && _a !== void 0 ? _a : false,
474
120
  });
475
121
  };
476
122
  /**
@@ -481,15 +127,14 @@ const useTypingTask = (options) => {
481
127
  if (!onBeforeTypedChar) {
482
128
  return;
483
129
  }
484
- const { answerType, content, index } = char;
485
- const allLength = wholeContentRef.current.allLength;
130
+ const { content, index } = char;
131
+ const allLength = wholeContentRef.current.length;
486
132
  // 计算之前字符的百分比
487
133
  const percent = (char.index / allLength) * 100;
488
134
  await onBeforeTypedChar({
489
135
  currentIndex: index,
490
136
  currentChar: content,
491
- answerType: answerType,
492
- prevStr: wholeContentRef.current[answerType].content,
137
+ prevStr: wholeContentRef.current.content,
493
138
  percent,
494
139
  });
495
140
  };
@@ -498,15 +143,14 @@ const useTypingTask = (options) => {
498
143
  if (!onTypedChar) {
499
144
  return;
500
145
  }
501
- const { answerType, content, index } = char;
502
- const allLength = wholeContentRef.current.allLength;
146
+ const { content, index } = char;
147
+ const allLength = wholeContentRef.current.length;
503
148
  const percent = ((char.index + 1) / allLength) * 100;
504
149
  onTypedChar({
505
150
  currentIndex: index,
506
151
  currentChar: content,
507
- answerType: answerType,
508
- prevStr: wholeContentRef.current[answerType].content.slice(0, index),
509
- currentStr: wholeContentRef.current[answerType].content,
152
+ prevStr: wholeContentRef.current.content.slice(0, index),
153
+ currentStr: wholeContentRef.current.content,
510
154
  percent,
511
155
  });
512
156
  };
@@ -534,427 +178,811 @@ const useTypingTask = (options) => {
534
178
  if (isTypingRef.current) {
535
179
  return;
536
180
  }
537
- if (timerType === 'requestAnimationFrame') {
538
- startAnimationFrameMode();
539
- }
540
- else {
541
- startTimeoutMode();
542
- }
181
+ if (timerType === 'requestAnimationFrame') {
182
+ startAnimationFrameMode();
183
+ }
184
+ else {
185
+ startTimeoutMode();
186
+ }
187
+ };
188
+ /** 打字机打完所有字符 */
189
+ async function typingRemainAll() {
190
+ const chars = getChars();
191
+ const answerCharsStr = chars.map((char) => char.content).join('');
192
+ if (answerCharsStr) {
193
+ await (onBeforeTypedChar === null || onBeforeTypedChar === void 0 ? void 0 : onBeforeTypedChar({
194
+ currentIndex: wholeContentRef.current.length,
195
+ currentChar: answerCharsStr,
196
+ prevStr: wholeContentRef.current.content,
197
+ percent: 100,
198
+ }));
199
+ }
200
+ wholeContentRef.current.content += answerCharsStr;
201
+ wholeContentRef.current.prevLength = wholeContentRef.current.length;
202
+ wholeContentRef.current.length += answerCharsStr.length;
203
+ charsRef.current = [];
204
+ isTypingRef.current = false;
205
+ triggerOnEnd();
206
+ triggerUpdate();
207
+ }
208
+ /** requestAnimationFrame 模式 */
209
+ const startAnimationFrameMode = () => {
210
+ let lastFrameTime = performance.now();
211
+ const frameLoop = async (currentTime) => {
212
+ if (isUnmountRef.current)
213
+ return;
214
+ // 如果关闭打字机效果,则打完所有字符
215
+ if (disableTypingRef.current) {
216
+ await typingRemainAll();
217
+ return;
218
+ }
219
+ const chars = getChars();
220
+ if (chars.length === 0) {
221
+ stopAnimationFrame();
222
+ return;
223
+ }
224
+ const deltaTime = currentTime - lastFrameTime;
225
+ const currentInterval = getCurrentInterval(chars.length);
226
+ let needToTypingCharsLength = Math.max(0, Math.floor(deltaTime / currentInterval));
227
+ needToTypingCharsLength = Math.min(needToTypingCharsLength, chars.length);
228
+ if (needToTypingCharsLength > 0) {
229
+ // 处理字符
230
+ for (let i = 0; i < needToTypingCharsLength; i++) {
231
+ const char = chars.shift();
232
+ if (char === undefined)
233
+ break;
234
+ if (!isTypingRef.current) {
235
+ isTypingRef.current = true;
236
+ triggerOnStart(char);
237
+ }
238
+ /** 打字前回调 */
239
+ await triggerOnBeforeTypedChar(char);
240
+ processCharDisplay(char);
241
+ /** 打字完成回调 */
242
+ triggerOnTypedChar(char);
243
+ }
244
+ lastFrameTime = performance.now();
245
+ // 继续下一帧
246
+ if (chars.length > 0) {
247
+ animationFrameRef.current = requestAnimationFrame(frameLoop);
248
+ }
249
+ else {
250
+ isTypingRef.current = false;
251
+ triggerOnEnd();
252
+ }
253
+ }
254
+ else {
255
+ // 本次你不需要打字,继续下一帧
256
+ animationFrameRef.current = requestAnimationFrame(frameLoop);
257
+ }
258
+ };
259
+ animationFrameRef.current = requestAnimationFrame(frameLoop);
260
+ };
261
+ /** 停止动画帧模式 */
262
+ const stopAnimationFrame = (manual = false) => {
263
+ isTypingRef.current = false;
264
+ if (animationFrameRef.current) {
265
+ cancelAnimationFrame(animationFrameRef.current);
266
+ animationFrameRef.current = null;
267
+ }
268
+ if (!manual) {
269
+ triggerOnEnd({ manual });
270
+ }
271
+ };
272
+ /** setTimeout 模式 */
273
+ const startTimeoutMode = () => {
274
+ const nextTyped = () => {
275
+ const chars = getChars();
276
+ if (chars.length === 0) {
277
+ stopTimeout();
278
+ return;
279
+ }
280
+ const currentInterval = getCurrentInterval(chars.length);
281
+ timerRef.current = setTimeout(startTyped, currentInterval);
282
+ };
283
+ const startTyped = async (isStartPoint = false) => {
284
+ if (isUnmountRef.current)
285
+ return;
286
+ // 如果关闭打字机效果,则打完所有字符
287
+ if (disableTypingRef.current) {
288
+ typingRemainAll();
289
+ return;
290
+ }
291
+ const chars = getChars();
292
+ isTypingRef.current = true;
293
+ const char = chars.shift();
294
+ if (char === undefined) {
295
+ stopTimeout();
296
+ return;
297
+ }
298
+ if (isStartPoint) {
299
+ triggerOnStart(char);
300
+ }
301
+ /** 打字前回调 */
302
+ await triggerOnBeforeTypedChar(char);
303
+ processCharDisplay(char);
304
+ /** 打字完成回调 */
305
+ triggerOnTypedChar(char);
306
+ nextTyped();
307
+ };
308
+ startTyped(true);
309
+ };
310
+ /** 停止超时模式 */
311
+ const stopTimeout = () => {
312
+ isTypingRef.current = false;
313
+ if (timerRef.current) {
314
+ clearTimeout(timerRef.current);
315
+ timerRef.current = null;
316
+ }
317
+ triggerOnEnd();
318
+ };
319
+ const cancelTask = () => {
320
+ if (timerType === 'requestAnimationFrame') {
321
+ stopAnimationFrame();
322
+ }
323
+ else {
324
+ stopTimeout();
325
+ }
326
+ };
327
+ /** 暂时停止 */
328
+ const stopTask = () => {
329
+ typedIsManualStopRef.current = true;
330
+ cancelTask();
331
+ };
332
+ /** 停止打字任务 */
333
+ const endTask = () => {
334
+ cancelTask();
335
+ };
336
+ function restartTypedTask() {
337
+ endTask();
338
+ typedIsManualStopRef.current = false;
339
+ // 将wholeContentRef的内容放到charsRef中
340
+ charsRef.current.unshift(...wholeContentRef.current.content.split('').map((charUnit) => {
341
+ const char = {
342
+ content: charUnit,
343
+ tokenId: 0,
344
+ index: 0,
345
+ };
346
+ return char;
347
+ }));
348
+ charsRef.current.unshift(...wholeContentRef.current.content.split('').map((charUnit) => {
349
+ const char = {
350
+ content: charUnit,
351
+ tokenId: 0,
352
+ index: 0,
353
+ };
354
+ return char;
355
+ }));
356
+ resetWholeContent();
357
+ triggerUpdate();
358
+ startTypedTask();
359
+ }
360
+ function clear() {
361
+ clearTimer();
362
+ }
363
+ function resume() {
364
+ typedIsManualStopRef.current = false;
365
+ startTypedTask();
366
+ }
367
+ return {
368
+ start: startTypedTask,
369
+ restart: restartTypedTask,
370
+ stop: stopTask,
371
+ resume: resume,
372
+ clear: clear,
373
+ isTyping: () => isTypingRef.current,
374
+ typedIsManualStopRef,
375
+ };
376
+ };
377
+
378
+ function splitGraphemes(input) {
379
+ return Array.from(input);
380
+ }
381
+
382
+ const MarkdownTyperCMD = react.forwardRef(({ interval = 30, onEnd, onStart, onTypedChar, onBeforeTypedChar, timerType = 'setTimeout', reactMarkdownProps, disableTyping = false, autoStartTyping = true, customConvertMarkdownString }, ref) => {
383
+ /** 是否自动开启打字动画, 后面发生变化将不会生效 */
384
+ const autoStartTypingRef = react.useRef(autoStartTyping);
385
+ /** 是否打过字 */
386
+ const isStartedTypingRef = react.useRef(false);
387
+ /** 当前需要打字的内容 */
388
+ const charsRef = react.useRef([]);
389
+ /**
390
+ * 打字是否已经完全结束
391
+ * 如果打字已经完全结束,则不会再触发打字效果
392
+ */
393
+ const isWholeTypedEndRef = react.useRef(false);
394
+ const charIndexRef = react.useRef(0);
395
+ /** 整个内容引用 */
396
+ const wholeContentRef = react.useRef({
397
+ content: '',
398
+ length: 0,
399
+ prevLength: 0,
400
+ });
401
+ const [, setUpdate] = react.useState(0);
402
+ const triggerUpdate = () => {
403
+ setUpdate((prev) => prev + 1);
404
+ };
405
+ /**
406
+ * 处理字符显示逻辑
407
+ */
408
+ const processCharDisplay = (char) => {
409
+ if (!isStartedTypingRef.current) {
410
+ isStartedTypingRef.current = true;
411
+ }
412
+ wholeContentRef.current.prevLength = wholeContentRef.current.length;
413
+ wholeContentRef.current.content += char.content;
414
+ wholeContentRef.current.length += char.content.length;
415
+ triggerUpdate();
416
+ };
417
+ const resetWholeContent = () => {
418
+ wholeContentRef.current.content = '';
419
+ wholeContentRef.current.length = 0;
420
+ wholeContentRef.current.prevLength = 0;
421
+ };
422
+ // 使用新的打字任务 hook
423
+ const typingTask = useTypingTask({
424
+ timerType,
425
+ interval,
426
+ charsRef,
427
+ onEnd,
428
+ onStart,
429
+ onTypedChar,
430
+ onBeforeTypedChar,
431
+ processCharDisplay,
432
+ wholeContentRef,
433
+ disableTyping,
434
+ triggerUpdate,
435
+ resetWholeContent,
436
+ });
437
+ /**
438
+ * 内部推送处理逻辑
439
+ */
440
+ const processHasTypingPush = (content) => {
441
+ if (content.length === 0) {
442
+ return;
443
+ }
444
+ const segments = splitGraphemes(content);
445
+ charsRef.current.push(...segments.map((chatStr) => {
446
+ const index = charIndexRef.current++;
447
+ const charObj = {
448
+ content: chatStr,
449
+ tokenId: 0,
450
+ index,
451
+ };
452
+ return charObj;
453
+ }));
454
+ // 如果关闭了自动打字, 并且没有打过字, 则不开启打字动画
455
+ if (!autoStartTypingRef.current && !isStartedTypingRef.current) {
456
+ return;
457
+ }
458
+ if (!typingTask.isTyping()) {
459
+ typingTask.start();
460
+ }
461
+ };
462
+ const processNoTypingPush = (content) => {
463
+ wholeContentRef.current.content += content;
464
+ // 记录打字前的长度
465
+ wholeContentRef.current.prevLength = wholeContentRef.current.length;
466
+ wholeContentRef.current.length += content.length;
467
+ triggerUpdate();
468
+ onEnd === null || onEnd === void 0 ? void 0 : onEnd({
469
+ str: content,
470
+ manual: false,
471
+ });
472
+ };
473
+ react.useImperativeHandle(ref, () => ({
474
+ /**
475
+ * 添加内容
476
+ * @param content 内容 {string}
477
+ * @param answerType 回答类型 {AnswerType}
478
+ */
479
+ push: (content) => {
480
+ if (disableTyping) {
481
+ processNoTypingPush(content);
482
+ return;
483
+ }
484
+ processHasTypingPush(content);
485
+ },
486
+ /**
487
+ * 清除打字任务
488
+ */
489
+ clear: () => {
490
+ typingTask.stop();
491
+ typingTask.typedIsManualStopRef.current = false;
492
+ charsRef.current = [];
493
+ resetWholeContent();
494
+ isWholeTypedEndRef.current = false;
495
+ charIndexRef.current = 0;
496
+ isStartedTypingRef.current = false;
497
+ triggerUpdate();
498
+ },
499
+ /** 开启打字,只有在关闭了自动打字才生效 */
500
+ start: () => {
501
+ if (!autoStartTypingRef.current) {
502
+ typingTask.start();
503
+ }
504
+ },
505
+ /** 停止打字任务 */
506
+ stop: () => {
507
+ typingTask.stop();
508
+ },
509
+ /** 重新开始打字任务 */
510
+ resume: () => {
511
+ typingTask.resume();
512
+ },
513
+ /**
514
+ * 主动触发打字结束
515
+ */
516
+ triggerWholeEnd: () => {
517
+ isWholeTypedEndRef.current = true;
518
+ if (!typingTask.isTyping()) {
519
+ // 这里需要手动触发结束回调,因为 hook 中的 triggerOnEnd 不能直接调用
520
+ onEnd === null || onEnd === void 0 ? void 0 : onEnd({
521
+ str: wholeContentRef.current.content,
522
+ manual: true,
523
+ });
524
+ }
525
+ },
526
+ /** 重新开始打字任务 */
527
+ restart: () => {
528
+ typingTask.restart();
529
+ },
530
+ }));
531
+ const markdownString = react.useMemo(() => {
532
+ return (customConvertMarkdownString === null || customConvertMarkdownString === void 0 ? void 0 : customConvertMarkdownString(wholeContentRef.current.content)) || wholeContentRef.current.content;
533
+ }, [wholeContentRef.current.content, customConvertMarkdownString]);
534
+ return jsxRuntime.jsx(ReactMarkdown, { ...reactMarkdownProps, children: markdownString });
535
+ });
536
+ if (__DEV__) {
537
+ MarkdownTyperCMD.displayName = 'MarkdownTyperCMD';
538
+ }
539
+
540
+ const MarkdownTyperInner = ({ children: _children = '', markdownRef, ...rest }) => {
541
+ const cmdRef = react.useRef(null);
542
+ const prefixRef = react.useRef('');
543
+ const content = react.useMemo(() => {
544
+ if (typeof _children === 'string') {
545
+ return _children;
546
+ }
547
+ if (__DEV__) {
548
+ console.error('Markdown component must have a string child');
549
+ }
550
+ return '';
551
+ }, [_children]);
552
+ react.useEffect(() => {
553
+ if (prefixRef.current !== content) {
554
+ let newContent = '';
555
+ if (prefixRef.current === '') {
556
+ newContent = content;
557
+ }
558
+ else {
559
+ if (content.startsWith(prefixRef.current)) {
560
+ newContent = content.slice(prefixRef.current.length);
561
+ }
562
+ else {
563
+ newContent = content;
564
+ cmdRef.current.clear();
565
+ }
566
+ }
567
+ cmdRef.current.push(newContent);
568
+ prefixRef.current = content;
569
+ }
570
+ return () => {
571
+ console.log('unmount');
572
+ };
573
+ }, [content]);
574
+ react.useImperativeHandle(markdownRef, () => ({
575
+ stop: () => {
576
+ cmdRef.current.stop();
577
+ },
578
+ resume: () => {
579
+ cmdRef.current.resume();
580
+ },
581
+ start: () => {
582
+ cmdRef.current.start();
583
+ },
584
+ restart: () => {
585
+ cmdRef.current.restart();
586
+ },
587
+ }));
588
+ return jsxRuntime.jsx(MarkdownTyperCMD, { ref: cmdRef, ...rest });
589
+ };
590
+ const MarkdownTyper = react.forwardRef((props, ref) => {
591
+ const { children = '' } = props;
592
+ if (__DEV__) {
593
+ if (typeof children !== 'string') {
594
+ throw new Error('Markdown component must have a string child');
595
+ }
596
+ }
597
+ return jsxRuntime.jsx(MarkdownTyperInner, { ...props, markdownRef: ref });
598
+ });
599
+ react.memo(MarkdownTyper);
600
+
601
+ const DEFAULT_THEME = 'light';
602
+ const DEFAULT_ANSWER_TYPE = 'answer';
603
+ const DEFAULT_PLUGINS = [];
604
+ const MarkdownThemeContext = react.createContext({
605
+ state: {
606
+ theme: DEFAULT_THEME,
607
+ answerType: DEFAULT_ANSWER_TYPE,
608
+ },
609
+ methods: {},
610
+ });
611
+ const MarkdownThemeProvider = ({ value = {}, children }) => {
612
+ const contextValue = react.useMemo(() => ({
613
+ state: {
614
+ theme: DEFAULT_THEME,
615
+ answerType: DEFAULT_ANSWER_TYPE,
616
+ ...value,
617
+ },
618
+ methods: {
619
+ // 这里可以添加主题相关的方法实现
620
+ },
621
+ }), [value]);
622
+ return jsxRuntime.jsx(MarkdownThemeContext.Provider, { value: contextValue, children: children });
623
+ };
624
+ const useMarkdownThemeContext = () => react.useContext(MarkdownThemeContext);
625
+ const useThemeState = () => {
626
+ return react.useContext(MarkdownThemeContext).state;
627
+ };
628
+
629
+ const MarkdownContext = react.createContext({});
630
+ const MarkdownProvider = ({ value, children }) => {
631
+ const contextValue = react.useMemo(() => value, [value]);
632
+ return jsxRuntime.jsx(MarkdownContext.Provider, { value: contextValue, children: children });
633
+ };
634
+
635
+ const zhCN = {
636
+ codeBlock: {
637
+ copy: '复制',
638
+ copied: '已复制',
639
+ download: '下载',
640
+ downloaded: '已下载',
641
+ },
642
+ mermaid: {
643
+ diagram: '图表',
644
+ code: '代码',
645
+ zoomOut: '缩小',
646
+ zoomIn: '放大',
647
+ download: '下载',
648
+ fullScreen: '全屏',
649
+ exitFullScreen: '退出全屏',
650
+ downloadImage: '下载图片',
651
+ downloadedImage: '已下载',
652
+ copyImage: '复制图片',
653
+ copiedImage: '已复制',
654
+ fitInView: '适应页面',
655
+ },
656
+ };
657
+
658
+ const ConfigContext = react.createContext({
659
+ locale: zhCN,
660
+ });
661
+ const ConfigProvider = ({ locale, children, mermaidConfig, katexConfig }) => {
662
+ const contextValue = react.useMemo(() => {
663
+ const contextValue = {
664
+ locale: locale || zhCN,
665
+ };
666
+ if (mermaidConfig) {
667
+ contextValue.mermaidConfig = mermaidConfig;
668
+ }
669
+ if (katexConfig) {
670
+ contextValue.katexConfig = katexConfig;
671
+ }
672
+ return contextValue;
673
+ }, [locale, mermaidConfig, katexConfig]);
674
+ return jsxRuntime.jsx(ConfigContext.Provider, { value: contextValue, children: children });
675
+ };
676
+ // Hook 用于在组件中使用配置
677
+ const useConfig = () => {
678
+ const context = react.useContext(ConfigContext);
679
+ return context;
680
+ };
681
+ // Hook 用于获取当前语言包
682
+ const useLocale = () => {
683
+ const { locale } = useConfig();
684
+ return locale;
685
+ };
686
+
687
+ /**
688
+ * 将括号格式的数学公式转换为美元符号格式
689
+ * 支持以下转换:
690
+ * - \(...\) → $...$ (行内公式)
691
+ * - \[...\] → $$...$$ (块级公式)
692
+ *
693
+ * 特殊处理:
694
+ * - 如果文本包含 Markdown 超链接,则跳过转换以避免误处理
695
+ * - 使用占位符机制保护块级公式内的括号不被误转换
696
+ *
697
+ * @param value 要转换的字符串
698
+ * @returns 转换后的字符串
699
+ */
700
+ const replaceMathBracket = (value) => {
701
+ // 1. 提取所有块级公式内容,临时替换为占位符, [...]
702
+ const blockMatches = [];
703
+ let replaced = value.replace(/\\+\[([\s\S]+?)\\+\]/g, (_m, p1) => {
704
+ blockMatches.push(p1);
705
+ return `__BLOCK_MATH_${blockMatches.length - 1}__`;
706
+ });
707
+ // 也需要兼容 $$ xxxx $$ 这种写法
708
+ replaced = replaced.replace(/\$\$([\s\S]+?)\$\$/g, (_m, p1) => {
709
+ blockMatches.push(p1);
710
+ return `__BLOCK_MATH_${blockMatches.length - 1}__`;
711
+ });
712
+ // 2. 替换块级公式外部的 ( ... ) 为 $...$
713
+ replaced = replaced.replace(/\\+\(([^)]+?)\\+\)/g, (_m, p1) => {
714
+ return '$' + p1 + '$';
715
+ });
716
+ // 3. 还原块级公式内容,保持其内部小括号原样
717
+ replaced = replaced.replace(/__BLOCK_MATH_(\d+)__/g, (_m, idx) => {
718
+ return '$$' + blockMatches[Number(idx)] + '$$';
719
+ });
720
+ return replaced;
721
+ };
722
+
723
+ const CodeBlockWrap = ({ children, title }) => {
724
+ const { theme } = useThemeState();
725
+ return (jsxRuntime.jsxs("div", { className: `md-code-block md-code-block-${theme}`, children: [jsxRuntime.jsx("div", { className: "md-code-block-banner-wrap", children: jsxRuntime.jsx("div", { className: "md-code-block-banner md-code-block-banner-lite", children: title }) }), jsxRuntime.jsx("div", { className: "md-code-block-content", children: children })] }));
726
+ };
727
+
728
+ const CheckMarkIcon = ({ size }) => {
729
+ return (jsxRuntime.jsx("svg", { width: size, height: size, viewBox: `0 0 ${size} ${size}`, fill: "none", xmlns: "http://www.w3.org/2000/svg", children: jsxRuntime.jsx("path", { "fill-rule": "evenodd", "clip-rule": "evenodd", d: "M9.338 21.575a1.058 1.058 0 0 1-.53-.363L2.275 13.17a1.063 1.063 0 0 1 1.65-1.341l5.63 6.928L19.33 3.86a1.064 1.064 0 0 1 1.778 1.167L10.551 21.115a1.065 1.065 0 0 1-1.213.46z", fill: "currentColor" }) }));
730
+ };
731
+ const CopyIcon = ({ size }) => {
732
+ return (jsxRuntime.jsxs("svg", { width: size, height: size, viewBox: `0 0 ${size} ${size}`, fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [jsxRuntime.jsx("path", { d: "M3.65169 12.9243C3.68173 13.1045 3.74181 13.2748 3.80189 13.445C3.87198 13.6052 3.96211 13.7654 4.06225 13.9156C4.16238 14.0658 4.27253 14.206 4.4027 14.3362C4.52286 14.4663 4.66306 14.5765 4.81326 14.6766C4.96346 14.7768 5.11366 14.8569 5.28389 14.927C5.44411 14.9971 5.61434 15.0571 5.79459 15.0872C5.97483 15.1272 6.14506 15.1373 6.3253 15.1373V16.9196C6.30739 16.9196 6.28949 16.9195 6.27159 16.9193C5.9991 16.9158 5.72659 16.8859 5.4541 16.8295C5.16371 16.7694 4.88334 16.6893 4.61298 16.5692C4.3326 16.459 4.08226 16.3188 3.83193 16.1586C3.59161 15.9884 3.3613 15.7981 3.15102 15.5878C2.94074 15.3776 2.7605 15.1473 2.59027 14.9069C2.43006 14.6566 2.28986 14.3962 2.17972 14.1259C2.06957 13.8455 1.97944 13.5651 1.91936 13.2747C1.86929 12.9843 1.83926 12.684 1.83926 12.3936V6.26532C1.83926 5.96492 1.86929 5.67456 1.91936 5.38417C1.97944 5.09378 2.06957 4.80338 2.17972 4.53302C2.28986 4.26265 2.43006 4.0023 2.59027 3.75197C2.7605 3.50163 2.94074 3.27132 3.15102 3.06104C3.3613 2.85076 3.59161 2.67052 3.83193 2.50029C4.08226 2.33006 4.3326 2.19987 4.61298 2.07971C4.88334 1.96956 5.16371 1.87943 5.4541 1.81935C5.74449 1.75927 6.03491 1.73926 6.3253 1.73926H12.3934C12.6838 1.73926 12.9842 1.75927 13.2746 1.81935C13.555 1.87943 13.8354 1.96956 14.1158 2.07971C14.3861 2.19987 14.6465 2.33006 14.8868 2.50029C15.1371 2.67052 15.3574 2.85076 15.5677 3.06104C15.778 3.27132 15.9582 3.50163 16.1284 3.75197C16.2887 4.0023 16.4288 4.26265 16.539 4.53302C16.6592 4.80338 16.7393 5.09378 16.7994 5.38417C16.8558 5.65722 16.8858 5.93024 16.8892 6.21161C16.8894 6.22948 16.8895 6.24739 16.8895 6.26532H15.1271C15.1271 6.08508 15.1071 5.90486 15.067 5.72462C15.037 5.55439 14.9869 5.38415 14.9168 5.21392C14.8467 5.04369 14.7566 4.88347 14.6665 4.73327C14.5664 4.58307 14.4462 4.45289 14.326 4.32271C14.1959 4.19254 14.0557 4.08239 13.9055 3.98226C13.7553 3.88212 13.6051 3.79202 13.4348 3.72193C13.2746 3.65184 13.1044 3.60174 12.9242 3.5717C12.7539 3.53165 12.5737 3.51163 12.3934 3.51163H6.3253C6.14506 3.51163 5.97483 3.53165 5.79459 3.5717C5.61434 3.60174 5.44411 3.65184 5.28389 3.72193C5.11366 3.79202 4.96346 3.88212 4.81326 3.98226C4.66306 4.08239 4.52286 4.19254 4.4027 4.32271C4.27253 4.45289 4.16238 4.58307 4.06225 4.73327C3.96211 4.88347 3.87198 5.04369 3.80189 5.21392C3.74181 5.38415 3.68173 5.55439 3.65169 5.72462C3.61164 5.90486 3.60164 6.08508 3.60164 6.26532V12.3936C3.60164 12.5638 3.61164 12.744 3.65169 12.9243Z", fill: "currentColor" }), jsxRuntime.jsx("path", { "fill-rule": "evenodd", "clip-rule": "evenodd", d: "M9.66972 21.6772C9.39936 21.567 9.13902 21.4268 8.8987 21.2566C8.64836 21.0964 8.42804 20.9061 8.21776 20.6959C8.00748 20.4856 7.81723 20.2553 7.65701 20.015C7.4968 19.7646 7.3566 19.5043 7.24646 19.2239C7.12629 18.9535 7.04621 18.6731 6.98613 18.3727C6.92605 18.0823 6.89601 17.792 6.89601 17.4915V11.3733C6.89601 11.0729 6.92605 10.7825 6.98613 10.4922C7.04621 10.1918 7.12629 9.91137 7.24646 9.64101C7.3566 9.36063 7.4968 9.10028 7.65701 8.85996C7.81723 8.60962 8.00748 8.37931 8.21776 8.16903C8.42804 7.95875 8.64836 7.76849 8.8987 7.60828C9.13902 7.43805 9.39936 7.29785 9.66972 7.1877C9.94009 7.07755 10.2205 6.98745 10.5108 6.92737C10.8012 6.86729 11.0916 6.83725 11.392 6.83725H17.4602C17.7506 6.83725 18.041 6.86729 18.3313 6.92737C18.6217 6.98745 18.9021 7.07755 19.1725 7.1877C19.4529 7.29785 19.7032 7.43805 19.9535 7.60828C20.1938 7.76849 20.4242 7.95875 20.6345 8.16903C20.8447 8.37931 21.025 8.60962 21.1952 8.85996C21.3554 9.10028 21.4956 9.36063 21.6058 9.64101C21.7159 9.91137 21.806 10.1918 21.8661 10.4922C21.9162 10.7825 21.9462 11.0729 21.9462 11.3733V17.4915C21.9462 17.792 21.9162 18.0823 21.8661 18.3727C21.806 18.6731 21.7159 18.9535 21.6058 19.2239C21.4956 19.5043 21.3554 19.7646 21.1952 20.015C21.025 20.2553 20.8447 20.4856 20.6345 20.6959C20.4242 20.9061 20.1938 21.0964 19.9535 21.2566C19.7032 21.4268 19.4529 21.567 19.1725 21.6772C18.9021 21.7973 18.6217 21.8774 18.3313 21.9375C18.041 21.9976 17.7506 22.0276 17.4602 22.0276H11.392C11.0916 22.0276 10.8012 21.9976 10.5108 21.9375C10.2205 21.8774 9.94009 21.7973 9.66972 21.6772ZM10.8613 8.6697C11.0316 8.63966 11.2118 8.61965 11.392 8.61965H17.4602C17.6404 8.61965 17.8107 8.63966 17.9909 8.6697C18.1611 8.70975 18.3314 8.75983 18.5016 8.82992C18.6618 8.90001 18.822 8.98012 18.9722 9.08026C19.1224 9.18039 19.2626 9.30055 19.3828 9.42071C19.513 9.55088 19.6231 9.69109 19.7232 9.84129C19.8234 9.99149 19.9035 10.1517 19.9736 10.3219C20.0437 10.4821 20.0937 10.6624 20.1338 10.8326C20.1638 11.0129 20.1838 11.1931 20.1838 11.3733V17.4915C20.1838 17.6718 20.1638 17.852 20.1338 18.0323C20.0937 18.2125 20.0437 18.3828 19.9736 18.543C19.9035 18.7132 19.8234 18.8734 19.7232 19.0236C19.6231 19.1738 19.513 19.314 19.3828 19.4342C19.2626 19.5643 19.1224 19.6845 18.9722 19.7846C18.822 19.8848 18.6618 19.9649 18.5016 20.035C18.3314 20.1051 18.1611 20.1551 17.9909 20.1952C17.8107 20.2252 17.6404 20.2452 17.4602 20.2452H11.392C11.2118 20.2452 11.0316 20.2252 10.8613 20.1952C10.6811 20.1551 10.5108 20.1051 10.3506 20.035C10.1804 19.9649 10.0202 19.8848 9.87 19.7846C9.72982 19.6845 9.58962 19.5643 9.45945 19.4342C9.33929 19.314 9.21913 19.1738 9.119 19.0236C9.01886 18.8734 8.93875 18.7132 8.86866 18.543C8.79857 18.3828 8.74847 18.2125 8.71843 18.0323C8.67838 17.852 8.65836 17.6718 8.65836 17.4915V11.3733C8.65836 11.1931 8.67838 11.0129 8.71843 10.8326C8.74847 10.6624 8.79857 10.4821 8.86866 10.3219C8.93875 10.1517 9.01886 9.99149 9.119 9.84129C9.21913 9.69109 9.33929 9.55088 9.45945 9.42071C9.58962 9.30055 9.72982 9.18039 9.87 9.08026C10.0202 8.98012 10.1804 8.90001 10.3506 8.82992C10.5108 8.75983 10.6811 8.70975 10.8613 8.6697Z", fill: "currentColor" })] }));
733
+ };
734
+ const DownloadIcon = ({ size }) => {
735
+ return (jsxRuntime.jsxs("svg", { width: size, height: size, viewBox: `0 0 ${size} ${size}`, fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [jsxRuntime.jsx("path", { "fill-rule": "evenodd", "clip-rule": "evenodd", d: "M12 2.55a.97.97 0 0 1 .982.956v13.04a.97.97 0 0 1-.982.957.97.97 0 0 1-.982-.956V3.507A.97.97 0 0 1 12 2.55z", fill: "currentColor" }), jsxRuntime.jsx("path", { "fill-rule": "evenodd", "clip-rule": "evenodd", d: "M19.418 9.808c.382.375.37.971-.027 1.332l-6.7 6.085a1.04 1.04 0 0 1-1.41-.025.905.905 0 0 1 .027-1.332l6.7-6.085a1.04 1.04 0 0 1 1.41.025z", fill: "currentColor" }), jsxRuntime.jsx("path", { "fill-rule": "evenodd", "clip-rule": "evenodd", d: "M4.582 9.808a1.04 1.04 0 0 1 1.41-.025l6.7 6.085c.397.361.409.957.027 1.332a1.04 1.04 0 0 1-1.41.025l-6.7-6.085a.905.905 0 0 1-.027-1.332z", fill: "currentColor" }), jsxRuntime.jsx("path", { "fill-rule": "evenodd", "clip-rule": "evenodd", d: "M3.068 16.46a.97.97 0 0 1 .983.956v1.739c0 .432.36.782.803.782h14.291c.445 0 .804-.35.804-.782v-1.739a.97.97 0 0 1 .983-.956.97.97 0 0 1 .982.956v1.739c0 1.488-1.24 2.695-2.769 2.695H4.855c-1.53 0-2.77-1.207-2.77-2.695v-1.739a.97.97 0 0 1 .983-.956z", fill: "currentColor" })] }));
736
+ };
737
+
738
+ const Button = ({ className = '', children, icon, onClick, style, disabled = false, ...restProps }) => {
739
+ const handleClick = (e) => {
740
+ if (disabled) {
741
+ e.preventDefault();
742
+ return;
743
+ }
744
+ onClick?.();
543
745
  };
544
- /** 打字机打完所有字符 */
545
- async function typingRemainAll() {
546
- const chars = getChars();
547
- const thinkingCharsStr = chars
548
- .filter((char) => char.answerType === 'thinking')
549
- .map((char) => char.content)
550
- .join('');
551
- const answerCharsStr = chars
552
- .filter((char) => char.answerType === 'answer')
553
- .map((char) => char.content)
554
- .join('');
555
- if (thinkingCharsStr) {
556
- await onBeforeTypedChar?.({
557
- currentIndex: wholeContentRef.current.thinking.length,
558
- currentChar: thinkingCharsStr,
559
- answerType: 'thinking',
560
- prevStr: wholeContentRef.current.thinking.content,
561
- percent: 100,
562
- });
563
- }
564
- if (answerCharsStr) {
565
- await onBeforeTypedChar?.({
566
- currentIndex: wholeContentRef.current.answer.length,
567
- currentChar: answerCharsStr,
568
- answerType: 'answer',
569
- prevStr: wholeContentRef.current.answer.content,
570
- percent: 100,
571
- });
746
+ return (jsxRuntime.jsxs("div", { role: "button", className: classNames({
747
+ 'ds-button': true,
748
+ 'ds-button__disabled': disabled,
749
+ }, className), onClick: handleClick, style: style, "aria-disabled": disabled, ...restProps, children: [icon && jsxRuntime.jsx("div", { className: "ds-button__icon", children: icon }), children] }));
750
+ };
751
+
752
+ const SuccessButton = (props) => {
753
+ const { onClick, icon, executeText, children, ...rest } = props;
754
+ const [isLoading, setIsLoading] = react.useState(false);
755
+ const [isSuccess, setIsSuccess] = react.useState(false);
756
+ const isUnmounted = react.useRef(false);
757
+ const handleClick = async () => {
758
+ if (isLoading || isSuccess) {
759
+ return;
572
760
  }
573
- wholeContentRef.current.thinking.content += thinkingCharsStr;
574
- wholeContentRef.current.thinking.length += thinkingCharsStr.length;
575
- wholeContentRef.current.answer.content += answerCharsStr;
576
- wholeContentRef.current.answer.length += answerCharsStr.length;
577
- wholeContentRef.current.allLength += thinkingCharsStr.length + answerCharsStr.length;
578
- charsRef.current = [];
579
- isTypingRef.current = false;
580
- triggerOnEnd();
581
- triggerUpdate();
582
- }
583
- /** requestAnimationFrame 模式 */
584
- const startAnimationFrameMode = () => {
585
- let lastFrameTime = performance.now();
586
- const frameLoop = async (currentTime) => {
587
- // 如果关闭打字机效果,则打完所有字符
588
- if (disableTypingRef.current) {
589
- await typingRemainAll();
590
- return;
591
- }
592
- const chars = getChars();
593
- if (isUnmountRef.current)
594
- return;
595
- if (chars.length === 0) {
596
- stopAnimationFrame();
597
- return;
598
- }
599
- const deltaTime = currentTime - lastFrameTime;
600
- const currentInterval = getCurrentInterval(chars.length);
601
- let needToTypingCharsLength = Math.max(0, Math.floor(deltaTime / currentInterval));
602
- needToTypingCharsLength = Math.min(needToTypingCharsLength, chars.length);
603
- if (needToTypingCharsLength > 0) {
604
- // 处理字符
605
- for (let i = 0; i < needToTypingCharsLength; i++) {
606
- const char = chars.shift();
607
- if (char === undefined)
608
- break;
609
- if (!isTypingRef.current) {
610
- isTypingRef.current = true;
611
- triggerOnStart(char);
612
- }
613
- /** 打字前回调 */
614
- await triggerOnBeforeTypedChar(char);
615
- processCharDisplay(char);
616
- /** 打字完成回调 */
617
- triggerOnTypedChar(char);
618
- }
619
- lastFrameTime = performance.now();
620
- // 继续下一帧
621
- if (chars.length > 0) {
622
- animationFrameRef.current = requestAnimationFrame(frameLoop);
623
- }
624
- else {
625
- isTypingRef.current = false;
626
- triggerOnEnd();
761
+ try {
762
+ // 如果onClick不是异步函数,则直接调用
763
+ const returnValue = onClick();
764
+ if (returnValue instanceof Promise) {
765
+ setIsLoading(true);
766
+ const result = await returnValue;
767
+ if (result) {
768
+ setIsSuccess(true);
769
+ setTimeout(() => {
770
+ if (!isUnmounted.current) {
771
+ setIsSuccess(false);
772
+ }
773
+ }, 1000);
627
774
  }
628
775
  }
629
- else {
630
- // 本次你不需要打字,继续下一帧
631
- animationFrameRef.current = requestAnimationFrame(frameLoop);
632
- }
633
- };
634
- animationFrameRef.current = requestAnimationFrame(frameLoop);
635
- };
636
- /** 停止动画帧模式 */
637
- const stopAnimationFrame = (manual = false) => {
638
- isTypingRef.current = false;
639
- if (animationFrameRef.current) {
640
- cancelAnimationFrame(animationFrameRef.current);
641
- animationFrameRef.current = null;
642
776
  }
643
- if (!manual) {
644
- triggerOnEnd({ manual });
777
+ catch (error) {
778
+ setIsSuccess(false);
645
779
  }
646
- };
647
- /** setTimeout 模式 */
648
- const startTimeoutMode = () => {
649
- const nextTyped = () => {
650
- const chars = getChars();
651
- if (chars.length === 0) {
652
- stopTimeout();
653
- return;
654
- }
655
- const currentInterval = getCurrentInterval(chars.length);
656
- timerRef.current = setTimeout(startTyped, currentInterval);
657
- };
658
- const startTyped = async (isStartPoint = false) => {
659
- // 如果关闭打字机效果,则打完所有字符
660
- if (disableTypingRef.current) {
661
- typingRemainAll();
662
- return;
663
- }
664
- const chars = getChars();
665
- if (isUnmountRef.current)
666
- return;
667
- isTypingRef.current = true;
668
- const char = chars.shift();
669
- if (char === undefined) {
670
- stopTimeout();
671
- return;
672
- }
673
- if (isStartPoint) {
674
- triggerOnStart(char);
675
- }
676
- /** 打字前回调 */
677
- await triggerOnBeforeTypedChar(char);
678
- processCharDisplay(char);
679
- /** 打字完成回调 */
680
- triggerOnTypedChar(char);
681
- nextTyped();
682
- };
683
- startTyped(true);
684
- };
685
- /** 停止超时模式 */
686
- const stopTimeout = () => {
687
- isTypingRef.current = false;
688
- if (timerRef.current) {
689
- clearTimeout(timerRef.current);
690
- timerRef.current = null;
780
+ finally {
781
+ setIsLoading(false);
691
782
  }
692
- triggerOnEnd();
693
783
  };
694
- const cancelTask = () => {
695
- if (timerType === 'requestAnimationFrame') {
696
- stopAnimationFrame();
784
+ react.useEffect(() => {
785
+ isUnmounted.current = false;
786
+ return () => {
787
+ isUnmounted.current = true;
788
+ };
789
+ }, []);
790
+ return (jsxRuntime.jsx(Button, { ...rest, onClick: handleClick, icon: isSuccess ? jsxRuntime.jsx(CheckMarkIcon, { size: 24 }) : icon, children: isSuccess ? executeText || children : children }));
791
+ };
792
+
793
+ const CopyButton = ({ codeContent, style, className }) => {
794
+ const { locale } = useConfig();
795
+ const handleCopy = async () => {
796
+ try {
797
+ await navigator.clipboard.writeText(codeContent || '');
798
+ return true;
697
799
  }
698
- else {
699
- stopTimeout();
800
+ catch (err) {
801
+ // 降级方案:使用传统方法
802
+ const textArea = document.createElement('textarea');
803
+ textArea.value = codeContent || '';
804
+ textArea.select();
805
+ document.execCommand('copy');
806
+ return true;
700
807
  }
701
808
  };
702
- /** 暂时停止 */
703
- const stopTask = () => {
704
- typedIsManualStopRef.current = true;
705
- cancelTask();
706
- };
707
- /** 停止打字任务 */
708
- const endTask = () => {
709
- cancelTask();
710
- };
711
- function restartTypedTask() {
712
- endTask();
713
- // 重置初始参考总量
714
- initialRemainTotalRef.current = 0;
715
- // 将wholeContentRef的内容放到charsRef中
716
- charsRef.current.unshift(...splitGraphemes(wholeContentRef.current.thinking.content).map((charUnit) => {
717
- const char = {
718
- content: charUnit,
719
- answerType: 'thinking',
720
- tokenId: 0,
721
- index: 0,
722
- };
723
- return char;
724
- }));
725
- charsRef.current.unshift(...splitGraphemes(wholeContentRef.current.answer.content).map((charUnit) => {
726
- const char = {
727
- content: charUnit,
728
- answerType: 'answer',
729
- tokenId: 0,
730
- index: 0,
809
+ return (jsxRuntime.jsx(SuccessButton, { onClick: handleCopy, icon: jsxRuntime.jsx(CopyIcon, { size: 24 }), executeText: locale.codeBlock.copied || 'copied', style: style, className: className, children: locale.codeBlock.copy || 'copy' }));
810
+ };
811
+
812
+ const DownloadButton = ({ codeContent, language, style, className }) => {
813
+ const { locale } = useConfig();
814
+ // 下载文件
815
+ const handleDownload = async () => {
816
+ if (!codeContent)
817
+ return false;
818
+ const blob = new Blob([codeContent], { type: 'text/plain;charset=utf-8' });
819
+ const url = URL.createObjectURL(blob);
820
+ const link = document.createElement('a');
821
+ // 根据语言设置文件扩展名
822
+ const getFileExtension = (lang) => {
823
+ const extensions = {
824
+ javascript: 'js',
825
+ typescript: 'ts',
826
+ jsx: 'jsx',
827
+ tsx: 'tsx',
828
+ python: 'py',
829
+ java: 'java',
830
+ cpp: 'cpp',
831
+ c: 'c',
832
+ csharp: 'cs',
833
+ php: 'php',
834
+ ruby: 'rb',
835
+ go: 'go',
836
+ rust: 'rs',
837
+ swift: 'swift',
838
+ kotlin: 'kt',
839
+ scala: 'scala',
840
+ shell: 'sh',
841
+ bash: 'sh',
842
+ powershell: 'ps1',
843
+ sql: 'sql',
844
+ html: 'html',
845
+ css: 'css',
846
+ scss: 'scss',
847
+ less: 'less',
848
+ json: 'json',
849
+ xml: 'xml',
850
+ yaml: 'yml',
851
+ markdown: 'md',
852
+ dockerfile: 'dockerfile',
731
853
  };
732
- return char;
733
- }));
734
- resetWholeContent();
735
- triggerUpdate();
736
- startTypedTask();
737
- }
738
- function clear() {
739
- clearTimer();
740
- }
741
- function resume() {
742
- typedIsManualStopRef.current = false;
743
- startTypedTask();
744
- }
745
- return {
746
- start: startTypedTask,
747
- restart: restartTypedTask,
748
- stop: stopTask,
749
- resume: resume,
750
- clear: clear,
751
- isTyping: () => isTypingRef.current,
752
- typedIsManualStopRef,
854
+ return extensions[lang.toLowerCase()] || 'txt';
855
+ };
856
+ const fileName = `code.${getFileExtension(language)}`;
857
+ link.href = url;
858
+ link.download = fileName;
859
+ document.body.appendChild(link);
860
+ link.click();
861
+ document.body.removeChild(link);
862
+ URL.revokeObjectURL(url);
863
+ return true;
753
864
  };
865
+ return (jsxRuntime.jsx(SuccessButton, { onClick: handleDownload, icon: jsxRuntime.jsx(DownloadIcon, { size: 24 }), executeText: locale.codeBlock.downloaded || 'Downloaded', style: style, className: className, children: locale.codeBlock.download || 'Download' }));
754
866
  };
755
867
 
756
- const MarkdownContext = react.createContext({});
757
- const MarkdownProvider = ({ value, children }) => {
758
- const contextValue = react.useMemo(() => value, [value]);
759
- return jsxRuntime.jsx(MarkdownContext.Provider, { value: contextValue, children: children });
868
+ const CodeBlockActions = ({ codeContent, language }) => {
869
+ return (jsxRuntime.jsxs("div", { className: "md-code-block-header-actions", children: [jsxRuntime.jsx(CopyButton, { codeContent: codeContent, style: { fontSize: 13, padding: '0 4px' } }), jsxRuntime.jsx(DownloadButton, { codeContent: codeContent, language: language, style: { fontSize: 13, padding: '0 4px' } })] }));
760
870
  };
761
871
 
762
- const MarkdownCMDInner = react.forwardRef(({ interval = 30, onEnd, onStart, onTypedChar, onBeforeTypedChar, timerType = 'setTimeout', disableTyping = false, autoStartTyping = true }, ref) => {
872
+ const BlockWrap = ({ children, language, codeContent }) => {
763
873
  const { state: themeState } = useMarkdownThemeContext();
764
874
  // 从 context 中获取主题配置
765
- const currentTheme = themeState.theme;
766
- /** 是否自动开启打字动画, 后面发生变化将不会生效 */
767
- const autoStartTypingRef = react.useRef(autoStartTyping);
768
- /** 是否打过字 */
769
- const isStartedTypingRef = react.useRef(false);
770
- /** 当前需要打字的内容 */
771
- const charsRef = react.useRef([]);
772
- /**
773
- * 打字是否已经完全结束
774
- * 如果打字已经完全结束,则不会再触发打字效果
775
- */
776
- const isWholeTypedEndRef = react.useRef(false);
777
- const charIndexRef = react.useRef(0);
778
- /** 整个内容引用 */
779
- const wholeContentRef = react.useRef({
780
- thinking: {
781
- content: '',
782
- length: 0,
783
- prevLength: 0,
784
- },
785
- answer: {
786
- content: '',
787
- length: 0,
788
- prevLength: 0,
789
- },
790
- allLength: 0,
791
- });
792
- const [, setUpdate] = react.useState(0);
793
- const triggerUpdate = () => {
794
- setUpdate((prev) => prev + 1);
795
- };
796
- /**
797
- * 处理字符显示逻辑
798
- */
799
- const processCharDisplay = (char) => {
800
- if (!isStartedTypingRef.current) {
801
- isStartedTypingRef.current = true;
802
- }
803
- if (char.answerType === 'thinking') {
804
- wholeContentRef.current.thinking.content += char.content;
805
- wholeContentRef.current.thinking.length += 1;
806
- }
807
- else {
808
- wholeContentRef.current.answer.content += char.content;
809
- wholeContentRef.current.answer.length += 1;
875
+ const currentCodeBlock = themeState.codeBlock;
876
+ const { headerActions = true } = currentCodeBlock || {};
877
+ const renderHeaderActions = () => {
878
+ if (headerActions === true) {
879
+ return jsxRuntime.jsx(CodeBlockActions, { codeContent: codeContent, language: language });
810
880
  }
811
- triggerUpdate();
812
- };
813
- const resetWholeContent = () => {
814
- wholeContentRef.current.thinking.content = '';
815
- wholeContentRef.current.thinking.length = 0;
816
- wholeContentRef.current.thinking.prevLength = 0;
817
- wholeContentRef.current.answer.content = '';
818
- wholeContentRef.current.answer.length = 0;
819
- wholeContentRef.current.answer.prevLength = 0;
820
- wholeContentRef.current.allLength = 0;
881
+ return headerActions;
821
882
  };
822
- // 使用新的打字任务 hook
823
- const typingTask = useTypingTask({
824
- timerType,
825
- interval,
826
- charsRef,
827
- onEnd,
828
- onStart,
829
- onTypedChar,
830
- onBeforeTypedChar,
831
- processCharDisplay,
832
- wholeContentRef,
833
- disableTyping,
834
- triggerUpdate,
835
- resetWholeContent,
836
- });
837
- /**
838
- * 内部推送处理逻辑
839
- */
840
- const processHasTypingPush = (content, answerType) => {
841
- if (content.length === 0) {
842
- return;
843
- }
844
- const segments = splitGraphemes(content);
845
- charsRef.current.push(...segments.map((chatStr) => {
846
- const index = charIndexRef.current++;
847
- const charObj = {
848
- content: chatStr,
849
- answerType,
850
- tokenId: 0,
851
- index,
883
+ return (jsxRuntime.jsx(CodeBlockWrap, { title: jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("div", { className: "md-code-block-language", children: language }), renderHeaderActions()] }), children: children }));
884
+ };
885
+
886
+ const HighlightCode = ({ code, language }) => {
887
+ return (jsxRuntime.jsx(reactSyntaxHighlighter.Prism, { useInlineStyles: false, language: language, style: {}, children: code }));
888
+ };
889
+
890
+ const CodeComponent = ({ className, children = '' }) => {
891
+ const match = /language-(\w+)/.exec(className || '');
892
+ const codeContent = String(children).replace(/\n$/, '');
893
+ return match ? (jsxRuntime.jsx(BlockWrap, { language: match[1], codeContent: codeContent, children: jsxRuntime.jsx(HighlightCode, { code: codeContent, language: match[1] }) })) : (jsxRuntime.jsx("code", { className: className, children: children }));
894
+ };
895
+
896
+ const MarkdownCMDInner = react.forwardRef(({ answerType = 'answer', ...rest }, ref) => {
897
+ const { state: themeState } = useMarkdownThemeContext();
898
+ const cmdRef = react.useRef(null);
899
+ // 从 context 中获取主题配置
900
+ const currentTheme = themeState.theme;
901
+ react.useImperativeHandle(ref, () => ({
902
+ push: cmdRef.current.push,
903
+ clear: cmdRef.current.clear,
904
+ triggerWholeEnd: cmdRef.current.triggerWholeEnd,
905
+ stop: cmdRef.current.stop,
906
+ resume: cmdRef.current.resume,
907
+ start: cmdRef.current.start,
908
+ restart: cmdRef.current.restart,
909
+ }));
910
+ const { katexConfig } = useConfig();
911
+ // 从 context 中获取主题配置
912
+ const currentMath = themeState.math;
913
+ const currentPlugins = themeState.plugins;
914
+ const mathSplitSymbol = currentMath?.splitSymbol ?? 'dollar';
915
+ const finalReplaceMathBracket = currentMath?.replaceMathBracket ?? replaceMathBracket;
916
+ const { remarkPlugins, rehypePlugins, hasKatexPlugin, components } = react.useMemo(() => {
917
+ let hasKatexPlugin = false;
918
+ const components = {};
919
+ const remarkPlugins = [gfmPlugin];
920
+ const rehypePlugins = [];
921
+ if (!currentPlugins) {
922
+ return {
923
+ remarkPlugins,
924
+ rehypePlugins,
852
925
  };
853
- return charObj;
854
- }));
855
- wholeContentRef.current.allLength += segments.length;
856
- // 如果关闭了自动打字, 并且没有打过字, 则不开启打字动画
857
- if (!autoStartTypingRef.current && !isStartedTypingRef.current) {
858
- return;
859
- }
860
- if (!typingTask.isTyping()) {
861
- typingTask.start();
862
926
  }
863
- };
864
- const processNoTypingPush = (content, answerType) => {
865
- wholeContentRef.current[answerType].content += content;
866
- // 记录打字前的长度
867
- wholeContentRef.current[answerType].prevLength = wholeContentRef.current[answerType].length;
868
- wholeContentRef.current[answerType].length += content.length;
869
- triggerUpdate();
870
- onEnd?.({
871
- str: content,
872
- answerStr: wholeContentRef.current.answer.content,
873
- thinkingStr: wholeContentRef.current.thinking.content,
874
- manual: false,
875
- });
876
- };
877
- react.useImperativeHandle(ref, () => ({
878
- /**
879
- * 添加内容
880
- * @param content 内容 {string}
881
- * @param answerType 回答类型 {AnswerType}
882
- */
883
- push: (content, answerType = 'answer') => {
884
- if (disableTyping) {
885
- processNoTypingPush(content, answerType);
886
- return;
927
+ currentPlugins.forEach((plugin) => {
928
+ if (plugin.id === katexId) {
929
+ hasKatexPlugin = true;
930
+ remarkPlugins.push(plugin.remarkPlugin);
931
+ rehypePlugins.push([plugin.rehypePlugin, katexConfig]);
887
932
  }
888
- processHasTypingPush(content, answerType);
889
- },
890
- /**
891
- * 清除打字任务
892
- */
893
- clear: () => {
894
- typingTask.stop();
895
- typingTask.typedIsManualStopRef.current = false;
896
- charsRef.current = [];
897
- resetWholeContent();
898
- isWholeTypedEndRef.current = false;
899
- charIndexRef.current = 0;
900
- isStartedTypingRef.current = false;
901
- triggerUpdate();
902
- },
903
- /** 开启打字,只有在关闭了自动打字才生效 */
904
- start: () => {
905
- if (!autoStartTypingRef.current) {
906
- typingTask.start();
933
+ else {
934
+ if (plugin.rehypePlugin) {
935
+ rehypePlugins.push(plugin.rehypePlugin);
936
+ }
937
+ if (plugin.remarkPlugin) {
938
+ remarkPlugins.push(plugin.remarkPlugin);
939
+ }
907
940
  }
908
- },
909
- /** 停止打字任务 */
910
- stop: () => {
911
- typingTask.stop();
912
- },
913
- /** 重新开始打字任务 */
914
- resume: () => {
915
- typingTask.resume();
916
- },
917
- /**
918
- * 主动触发打字结束
919
- */
920
- triggerWholeEnd: () => {
921
- isWholeTypedEndRef.current = true;
922
- if (!typingTask.isTyping()) {
923
- // 这里需要手动触发结束回调,因为 hook 中的 triggerOnEnd 不能直接调用
924
- onEnd?.({
925
- str: wholeContentRef.current.answer.content,
926
- answerStr: wholeContentRef.current.answer.content,
927
- thinkingStr: wholeContentRef.current.thinking.content,
928
- manual: true,
929
- });
941
+ if (plugin.components) {
942
+ Object.assign(components, plugin.components);
930
943
  }
931
- },
932
- /** 重新开始打字任务 */
933
- restart: () => {
934
- typingTask.restart();
935
- },
936
- }));
937
- const getParagraphs = (answerType) => {
938
- const content = wholeContentRef.current[answerType].content || '';
939
- return (jsxRuntime.jsx("div", { className: `ds-markdown-paragraph ds-typed-${answerType}`, children: jsxRuntime.jsx(HighReactMarkdown$1, { children: content }) }));
940
- };
941
- return (jsxRuntime.jsxs("div", { className: classNames({
944
+ });
945
+ return {
946
+ remarkPlugins,
947
+ rehypePlugins,
948
+ hasKatexPlugin,
949
+ components,
950
+ };
951
+ }, [currentPlugins, katexConfig]);
952
+ const customConvertMarkdownString = react.useCallback((markdownString) => {
953
+ /** 如果存在数学公式插件,并且数学公式分隔符为括号,则替换成 $ 符号 */
954
+ if (hasKatexPlugin && mathSplitSymbol === 'bracket') {
955
+ return finalReplaceMathBracket(markdownString);
956
+ }
957
+ return markdownString;
958
+ }, [finalReplaceMathBracket, hasKatexPlugin, mathSplitSymbol]);
959
+ return (jsxRuntime.jsx("div", { className: classNames({
942
960
  'ds-markdown': true,
943
961
  apple: true,
944
962
  'ds-markdown-dark': currentTheme === 'dark',
945
- }), children: [jsxRuntime.jsx("div", { className: "ds-markdown-thinking", children: getParagraphs('thinking') }), jsxRuntime.jsx("div", { className: "ds-markdown-answer", children: getParagraphs('answer') })] }));
963
+ }), children: jsxRuntime.jsx("div", { className: `ds-markdown-${answerType}`, children: jsxRuntime.jsx(MarkdownTyperCMD, { ref: cmdRef, customConvertMarkdownString: customConvertMarkdownString, ...rest, reactMarkdownProps: {
964
+ remarkPlugins,
965
+ rehypePlugins,
966
+ components: {
967
+ code: CodeComponent,
968
+ table: ({ children, ...props }) => {
969
+ return (jsxRuntime.jsx("div", { className: "markdown-table-wrapper", children: jsxRuntime.jsx("table", { className: "ds-markdown-table", children: children }) }));
970
+ },
971
+ ...components,
972
+ },
973
+ } }) }) }));
946
974
  });
947
- if (__DEV__) {
975
+ if (__DEV__$1) {
948
976
  MarkdownCMDInner.displayName = 'MarkdownCMD';
949
977
  }
950
978
  const MarkdownCMD = react.forwardRef((props, ref) => {
951
979
  const { children = '', answerType = 'answer', isInnerRender, ...reset } = props;
952
- if (__DEV__) {
980
+ if (__DEV__$1) {
953
981
  if (!['thinking', 'answer'].includes(answerType)) {
954
- throw new Error('Markdown组件的answerType必须是thinkinganswer');
982
+ throw new Error('The answerType of MarkdownCMD component must be thinking or answer');
955
983
  }
956
984
  if (typeof children !== 'string') {
957
- throw new Error('Markdown组件的子元素必须是一个字符串');
985
+ throw new Error('The children of MarkdownCMD component must be a string');
958
986
  }
959
987
  }
960
988
  const contextValue = react.useMemo(() => ({ ...reset, answerType }), [reset, answerType]);
@@ -973,15 +1001,15 @@ const MarkdownCMD = react.forwardRef((props, ref) => {
973
1001
  return (jsxRuntime.jsx(MarkdownProvider, { value: contextValue, children: jsxRuntime.jsx(MarkdownThemeProvider, { value: themeProps, children: jsxRuntime.jsx(MarkdownCMDInner, { ...props, ref: ref }) }) }));
974
1002
  });
975
1003
 
976
- const MarkdownInner = ({ children: _children = '', answerType, markdownRef, ...rest }) => {
1004
+ const MarkdownInner = ({ children: _children = '', answerType = 'answer', markdownRef, ...rest }) => {
977
1005
  const cmdRef = react.useRef(null);
978
1006
  const prefixRef = react.useRef('');
979
1007
  const content = react.useMemo(() => {
980
1008
  if (typeof _children === 'string') {
981
1009
  return _children;
982
1010
  }
983
- if (__DEV__) {
984
- console.error('Markdown组件的子元素必须是一个字符串');
1011
+ if (__DEV__$1) {
1012
+ console.error('The children of Markdown component must be a string');
985
1013
  }
986
1014
  return '';
987
1015
  }, [_children]);
@@ -1018,18 +1046,16 @@ const MarkdownInner = ({ children: _children = '', answerType, markdownRef, ...r
1018
1046
  cmdRef.current.restart();
1019
1047
  },
1020
1048
  }));
1021
- // 只传递 MarkdownBaseProps 相关的属性
1022
- const { theme, math, plugins, codeBlock, ...baseProps } = rest;
1023
- return jsxRuntime.jsx(MarkdownCMD, { ref: cmdRef, ...baseProps, isInnerRender: true });
1049
+ return jsxRuntime.jsx(MarkdownCMD, { ref: cmdRef, ...rest, answerType: answerType, isInnerRender: true });
1024
1050
  };
1025
1051
  const Markdown = react.forwardRef((props, ref) => {
1026
1052
  const { children = '', answerType = 'answer', ...reset } = props;
1027
- if (__DEV__) {
1053
+ if (__DEV__$1) {
1028
1054
  if (!['thinking', 'answer'].includes(answerType)) {
1029
- throw new Error('Markdown组件的answerType必须是thinkinganswer');
1055
+ throw new Error('The answerType of Markdown component must be thinking or answer');
1030
1056
  }
1031
1057
  if (typeof children !== 'string') {
1032
- throw new Error('Markdown组件的子元素必须是一个字符串');
1058
+ throw new Error('The children of Markdown component must be a string');
1033
1059
  }
1034
1060
  }
1035
1061
  const contextValue = react.useMemo(() => ({ ...reset, answerType }), [reset, answerType]);