ds-markdown 0.0.10-beta.2 → 0.0.10-beta.4

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.
@@ -1,11 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
2
+ import { forwardRef, useImperativeHandle, useMemo, useRef, useState } from 'react';
3
3
  import HighReactMarkdown from '../components/HighReactMarkdown/index.js';
4
4
  import classNames from 'classnames';
5
5
  import { compiler } from '../utils/compiler.js';
6
6
  import { __DEV__ } from '../constant.js';
7
7
  import deepClone from '../utils/methods/deepClone.js';
8
- const MarkdownCMD = forwardRef(({ interval = 30, isClosePrettyTyped = false, onEnd, onStart, onTypedChar }, ref) => {
8
+ import { useTypingTask } from '../hooks/useTypingTask.js';
9
+ const MarkdownCMD = forwardRef(({ interval = 30, onEnd, onStart, onTypedChar, timerType = 'requestAnimationFrame' }, ref) => {
9
10
  /** 当前需要打字的内容 */
10
11
  const charsRef = useRef([]);
11
12
  /**
@@ -13,23 +14,6 @@ const MarkdownCMD = forwardRef(({ interval = 30, isClosePrettyTyped = false, onE
13
14
  * 如果打字已经完全结束,则不会再触发打字效果
14
15
  */
15
16
  const isWholeTypedEndRef = useRef(false);
16
- /** 已经打过的字 */
17
- const typedCharsRef = useRef(undefined);
18
- /** 是否卸载 */
19
- const isUnmountRef = useRef(false);
20
- /** 是否正在打字 */
21
- const isTypedRef = useRef(false);
22
- /** 打字结束回调, */
23
- const onEndRef = useRef(onEnd);
24
- onEndRef.current = onEnd;
25
- /** 打字开始回调 */
26
- const onStartRef = useRef(onStart);
27
- onStartRef.current = onStart;
28
- /** 打字过程中回调 */
29
- const onTypedCharRef = useRef(onTypedChar);
30
- onTypedCharRef.current = onTypedChar;
31
- /** 打字定时器 */
32
- const timerRef = useRef(null);
33
17
  /**
34
18
  * 稳定段落
35
19
  * 稳定段落是已经打过字,并且不会再变化的段落
@@ -39,174 +23,22 @@ const MarkdownCMD = forwardRef(({ interval = 30, isClosePrettyTyped = false, onE
39
23
  const [currentSegment, setCurrentSegment] = useState(undefined);
40
24
  /** 当前段落引用 */
41
25
  const currentParagraphRef = useRef(undefined);
42
- currentParagraphRef.current = currentSegment;
43
- /** 清除打字定时器 */
44
- const clearTimer = () => {
45
- if (timerRef.current) {
46
- clearTimeout(timerRef.current);
47
- timerRef.current = null;
48
- }
49
- isTypedRef.current = false;
50
- };
51
- useEffect(() => {
52
- isUnmountRef.current = false;
53
- return () => {
54
- isUnmountRef.current = true;
55
- };
56
- }, []);
57
- /** 思考段落 */
58
- const thinkingParagraphs = useMemo(() => stableSegments.filter((paragraph) => paragraph.answerType === 'thinking'), [stableSegments]);
59
- /** 回答段落 */
60
- const answerParagraphs = useMemo(() => stableSegments.filter((paragraph) => paragraph.answerType === 'answer'), [stableSegments]);
61
- /**
62
- * 记录打过的字
63
- * @param char 当前字符
64
- * @returns
65
- */
66
- const recordTypedChars = (char) => {
67
- let prevStr = '';
68
- if (!typedCharsRef.current || typedCharsRef.current.answerType !== char.answerType) {
69
- typedCharsRef.current = {
70
- typedContent: char.content,
71
- answerType: char.answerType,
72
- prevStr: '',
73
- };
74
- }
75
- else {
76
- prevStr = typedCharsRef.current.typedContent;
77
- typedCharsRef.current.typedContent += char.content;
78
- typedCharsRef.current.prevStr = prevStr;
79
- }
80
- return {
81
- prevStr,
82
- nextStr: typedCharsRef.current?.typedContent || '',
83
- };
84
- };
26
+ // currentParagraphRef.current = currentSegment;
85
27
  /**
86
- * 触发打字开始回调
87
- * @param char 当前字符
28
+ * 处理字符显示逻辑
88
29
  */
89
- const triggerOnStart = (char) => {
90
- const onStartFn = onStartRef.current;
91
- if (!onStartFn) {
92
- return;
93
- }
94
- const { prevStr } = recordTypedChars(char);
95
- onStartRef.current?.({
96
- currentIndex: prevStr.length,
97
- currentChar: char.content,
98
- answerType: char.answerType,
99
- prevStr,
100
- });
101
- };
102
- /**
103
- * 触发打字结束回调
104
- */
105
- const triggerOnEnd = () => {
106
- const onEndFn = onEndRef.current;
107
- if (!onEndFn) {
108
- return;
109
- }
110
- onEndFn({
111
- str: typedCharsRef.current?.typedContent,
112
- answerType: typedCharsRef.current?.answerType,
113
- });
114
- };
115
- /**
116
- * 触发打字过程中回调
117
- * @param char 当前字符
118
- * @param isStartPoint 是否是开始打字(第一个字)
119
- */
120
- const triggerOnTypedChar = (char, isStartPoint = false) => {
121
- const onTypedCharFn = onTypedCharRef.current;
122
- if (!isStartPoint) {
123
- recordTypedChars(char);
124
- }
125
- if (!onTypedCharFn) {
126
- return;
127
- }
128
- onTypedCharFn({
129
- currentIndex: typedCharsRef.current?.prevStr.length || 0,
130
- currentChar: char.content,
131
- answerType: char.answerType,
132
- prevStr: typedCharsRef.current?.prevStr || '',
133
- });
134
- };
135
- /** 开始打字任务 */
136
- const startTypedTask = () => {
137
- if (isTypedRef.current) {
138
- return;
139
- }
140
- const chars = charsRef.current;
141
- /** 停止打字 */
142
- const stopTyped = () => {
143
- isTypedRef.current = false;
144
- if (timerRef.current) {
145
- clearTimeout(timerRef.current);
146
- timerRef.current = null;
147
- }
148
- triggerOnEnd();
149
- };
150
- /** 打下一个字 */
151
- const nextTyped = () => {
152
- if (chars.length === 0) {
153
- stopTyped();
154
- return;
155
- }
156
- timerRef.current = setTimeout(startTyped, interval);
157
- };
158
- /**
159
- * 开始打字
160
- * @param isStartPoint 是否是开始打字
161
- */
162
- function startTyped(isStartPoint = false) {
163
- if (isUnmountRef.current) {
164
- return;
165
- }
166
- isTypedRef.current = true;
167
- const char = chars.shift();
168
- if (char === undefined) {
169
- stopTyped();
170
- return;
171
- }
172
- if (isStartPoint) {
173
- triggerOnStart(char);
174
- triggerOnTypedChar(char, isStartPoint);
175
- }
176
- else {
177
- triggerOnTypedChar(char);
178
- }
179
- const currentSegment = currentParagraphRef.current;
180
- /** 如果碰到 space,和split_segment 则需要处理成两个段落 */
181
- if (char.contentType === 'space' || char.contentType === 'split_segment') {
182
- if (currentSegment) {
183
- setStableSegments((prev) => {
184
- const newParagraphs = [...prev];
185
- // 放入到稳定队列
186
- if (currentSegment) {
187
- newParagraphs.push({ ...currentSegment, isTyped: false });
188
- }
189
- if (char.contentType === 'space') {
190
- newParagraphs.push({
191
- content: '',
192
- isTyped: false,
193
- type: 'br',
194
- answerType: char.answerType,
195
- tokensReference: {
196
- [char.tokenId]: {
197
- startIndex: 0,
198
- raw: char.content,
199
- },
200
- },
201
- });
202
- }
203
- return newParagraphs;
204
- });
205
- setCurrentSegment(undefined);
206
- }
207
- else {
208
- setStableSegments((prev) => {
209
- const newParagraphs = [...prev];
30
+ const processCharDisplay = (char) => {
31
+ const currentSegment = currentParagraphRef.current;
32
+ /** 如果碰到 space,和split_segment 则需要处理成两个段落 */
33
+ if (char.contentType === 'space' || char.contentType === 'split_segment') {
34
+ if (currentSegment) {
35
+ setStableSegments((prev) => {
36
+ const newParagraphs = [...prev];
37
+ // 放入到稳定队列
38
+ if (currentSegment) {
39
+ newParagraphs.push({ ...currentSegment, isTyped: false });
40
+ }
41
+ if (char.contentType === 'space') {
210
42
  newParagraphs.push({
211
43
  content: '',
212
44
  isTyped: false,
@@ -219,176 +51,332 @@ const MarkdownCMD = forwardRef(({ interval = 30, isClosePrettyTyped = false, onE
219
51
  },
220
52
  },
221
53
  });
222
- return newParagraphs;
223
- });
224
- }
225
- nextTyped();
226
- return;
227
- }
228
- // 处理当前段落
229
- let _currentParagraph = currentSegment;
230
- const newCurrentParagraph = {
231
- content: '',
232
- isTyped: false,
233
- type: 'text',
234
- answerType: char.answerType,
235
- tokensReference: {},
236
- };
237
- if (!_currentParagraph) {
238
- // 如果当前没有段落,则直接设置为当前段落
239
- _currentParagraph = newCurrentParagraph;
54
+ }
55
+ return newParagraphs;
56
+ });
57
+ setCurrentSegment(() => undefined);
58
+ currentParagraphRef.current = undefined;
240
59
  }
241
- else if (currentSegment && currentSegment?.answerType !== char.answerType) {
242
- // 如果当前段落和当前字符的回答类型不一致,则需要处理成两个段落
60
+ else {
243
61
  setStableSegments((prev) => {
244
62
  const newParagraphs = [...prev];
245
- newParagraphs.push({ ...currentSegment, isTyped: false });
63
+ newParagraphs.push({
64
+ content: '',
65
+ isTyped: false,
66
+ type: 'br',
67
+ answerType: char.answerType,
68
+ tokensReference: {
69
+ [char.tokenId]: {
70
+ startIndex: 0,
71
+ raw: char.content,
72
+ },
73
+ },
74
+ });
246
75
  return newParagraphs;
247
76
  });
248
- _currentParagraph = newCurrentParagraph;
249
- setCurrentSegment(_currentParagraph);
250
77
  }
251
- setCurrentSegment((prev) => {
252
- const tokensReference = deepClone(_currentParagraph.tokensReference);
253
- if (tokensReference[char.tokenId]) {
254
- tokensReference[char.tokenId].raw += char.content;
255
- tokensReference[char.tokenId].startIndex = prev?.content?.length || 0;
256
- }
257
- else {
258
- tokensReference[char.tokenId] = {
259
- startIndex: prev?.content?.length || 0,
260
- raw: char.content,
261
- };
262
- }
263
- return {
264
- ..._currentParagraph,
265
- tokensReference,
266
- content: (prev?.content || '') + char.content,
267
- isTyped: true,
268
- };
78
+ return;
79
+ }
80
+ // 处理当前段落
81
+ const newCurrentParagraph = {
82
+ content: '',
83
+ isTyped: false,
84
+ type: 'text',
85
+ answerType: char.answerType,
86
+ tokensReference: {},
87
+ };
88
+ let _currentParagraph = currentSegment;
89
+ if (!_currentParagraph) {
90
+ // 如果当前没有段落,则直接设置为新当前段落
91
+ _currentParagraph = newCurrentParagraph;
92
+ }
93
+ else if (currentSegment && currentSegment?.answerType !== char.answerType) {
94
+ // 如果当前段落和当前字符的回答类型不一致,则需要处理成两个段落
95
+ setStableSegments((prev) => {
96
+ const newParagraphs = [...prev];
97
+ newParagraphs.push({ ...currentSegment, isTyped: false });
98
+ return newParagraphs;
269
99
  });
270
- nextTyped();
100
+ _currentParagraph = newCurrentParagraph;
271
101
  }
272
- startTyped(true);
102
+ const tokensReference = deepClone(_currentParagraph.tokensReference);
103
+ if (tokensReference[char.tokenId]) {
104
+ tokensReference[char.tokenId].raw += char.content;
105
+ tokensReference[char.tokenId].startIndex = currentSegment?.content?.length || 0;
106
+ }
107
+ else {
108
+ tokensReference[char.tokenId] = {
109
+ startIndex: currentSegment?.content?.length || 0,
110
+ raw: char.content,
111
+ };
112
+ }
113
+ const newCurrentSegment = {
114
+ ..._currentParagraph,
115
+ tokensReference,
116
+ content: (currentSegment?.content || '') + char.content,
117
+ isTyped: true,
118
+ };
119
+ currentParagraphRef.current = newCurrentSegment;
120
+ setCurrentSegment(() => newCurrentSegment);
273
121
  };
122
+ /** 思考段落 */
123
+ const thinkingParagraphs = useMemo(() => stableSegments.filter((paragraph) => paragraph.answerType === 'thinking'), [stableSegments]);
124
+ /** 回答段落 */
125
+ const answerParagraphs = useMemo(() => stableSegments.filter((paragraph) => paragraph.answerType === 'answer'), [stableSegments]);
126
+ // 使用新的打字任务 hook
127
+ const typingTask = useTypingTask({
128
+ timerType,
129
+ interval,
130
+ charsRef,
131
+ onEnd,
132
+ onStart,
133
+ onTypedChar,
134
+ processCharDisplay,
135
+ });
274
136
  const lastSegmentRawRef = useRef({
275
137
  thinking: '',
276
138
  answer: '',
277
139
  thinkingReference: null,
278
140
  answerReference: null,
141
+ thinkingBuffer: '',
142
+ answerBuffer: '',
279
143
  });
280
- useImperativeHandle(ref, () => ({
281
- /**
282
- * 添加内容
283
- * @param content 内容 {string}
284
- * @param answerType 回答类型 {AnswerType}
285
- */
286
- push: (content, answerType) => {
287
- const lastSegmentReference = lastSegmentRawRef.current[`${answerType}Reference`];
288
- if (isWholeTypedEndRef.current) {
289
- if (__DEV__) {
290
- console.warn('打字已经完全结束,不能再添加新的内容');
291
- }
292
- return;
144
+ /**
145
+ * 检测当前内容是否在安全的 Markdown 边界
146
+ */
147
+ const isAtSafeMarkdownBoundary = (content) => {
148
+ if (!content.trim())
149
+ return true;
150
+ const lines = content.split('\n');
151
+ const lastLine = lines[lines.length - 1];
152
+ // 如果以换行符结尾,通常是安全的
153
+ if (content.endsWith('\n'))
154
+ return true;
155
+ // 检查最后一行是否是完整的语法结构
156
+ const patterns = [
157
+ /^#+\s+.+$/, // 完整标题: "## 标题"
158
+ /^\s*(\d+\.|\*|\+|-)\s+.+$/, // 完整列表项: "1. 项目" 或 "- 项目"
159
+ /^\s*```\s*$/, // 代码块结束: "```"
160
+ /^\s*```\w*\s*$/, // 代码块开始: "```js"
161
+ /^\s*>.*$/, // 引用: "> 内容"
162
+ /^\s*\|.*\|\s*$/, // 完整表格行: "| 列1 | 列2 |"
163
+ /^.*[.!?。!?]\s*$/, // 以句号等结尾的句子
164
+ ];
165
+ return patterns.some((pattern) => pattern.test(lastLine));
166
+ };
167
+ /**
168
+ * 查找最近的安全分割点
169
+ */
170
+ const findLastSafeBoundary = (content) => {
171
+ const lines = content.split('\n');
172
+ // 从后往前找最后一个完整的行
173
+ for (let i = lines.length - 1; i >= 0; i--) {
174
+ const currentContent = lines.slice(0, i + 1).join('\n');
175
+ if (i < lines.length - 1) {
176
+ // 有后续行,说明当前行以换行结尾,通常是安全的
177
+ return currentContent.length + 1; // +1 for \n
293
178
  }
294
- let currentLastSegmentReference = null;
295
- let currentLastSegmentRaw = '';
296
- let lastSegmentRaw = '';
297
- if (lastSegmentReference) {
298
- lastSegmentRaw = lastSegmentReference.noTrimEndRaw || lastSegmentReference.raw;
299
- currentLastSegmentRaw = lastSegmentRaw + content;
179
+ const line = lines[i];
180
+ // 检查当前行是否是完整的结构
181
+ if (/^#+\s+.+$/.test(line) || // 完整标题
182
+ /^\s*(\d+\.|\*|\+|-)\s+.+$/.test(line) || // 完整列表项
183
+ /^\s*```\s*$/.test(line) || // 代码块标记
184
+ /^\s*>.*$/.test(line) || // 引用行
185
+ /^.*[.!?。!?]\s*$/.test(line)) {
186
+ // 完整句子
187
+ return currentContent.length;
300
188
  }
301
- else {
302
- currentLastSegmentRaw = content;
189
+ }
190
+ return 0; // 没找到安全点
191
+ };
192
+ /**
193
+ * 同步处理带缓冲的内容推送
194
+ */
195
+ const processBufferedPush = (content, answerType) => {
196
+ const bufferKey = `${answerType}Buffer`;
197
+ const lastSegmentRef = lastSegmentRawRef.current;
198
+ // 将内容添加到缓冲区
199
+ lastSegmentRef[bufferKey] += content;
200
+ // 检查当前是否在安全边界
201
+ if (isAtSafeMarkdownBoundary(lastSegmentRef[bufferKey])) {
202
+ // 在安全边界,直接处理所有内容
203
+ const bufferedContent = lastSegmentRef[bufferKey];
204
+ lastSegmentRef[bufferKey] = '';
205
+ processPushInternal(bufferedContent, answerType);
206
+ }
207
+ else {
208
+ // 不在安全边界,找到最后一个安全分割点
209
+ const safeBoundary = findLastSafeBoundary(lastSegmentRef[bufferKey]);
210
+ if (safeBoundary > 0) {
211
+ // 有安全分割点,处理安全部分,保留其余部分
212
+ const safeContent = lastSegmentRef[bufferKey].substring(0, safeBoundary);
213
+ const remainingContent = lastSegmentRef[bufferKey].substring(safeBoundary);
214
+ lastSegmentRef[bufferKey] = remainingContent;
215
+ processPushInternal(safeContent, answerType);
303
216
  }
304
- // debugger;
305
- const tokens = compiler(currentLastSegmentRaw);
306
- // 如果最后一个token是space,则把lastSegmentRaw设置为空
307
- if (tokens[tokens.length - 1].type === 'space') {
308
- currentLastSegmentReference = null;
217
+ // 如果没有安全分割点,继续缓冲等待更多内容
218
+ }
219
+ };
220
+ /**
221
+ * 内部推送处理逻辑
222
+ */
223
+ const processPushInternal = (content, answerType) => {
224
+ if (content.length === 0) {
225
+ return;
226
+ }
227
+ const lastSegmentReference = lastSegmentRawRef.current[`${answerType}Reference`];
228
+ if (isWholeTypedEndRef.current) {
229
+ if (__DEV__) {
230
+ console.warn('打字已经完全结束,不能再添加新的内容');
309
231
  }
310
- else {
311
- currentLastSegmentReference = tokens[tokens.length - 1];
232
+ return;
233
+ }
234
+ let currentLastSegmentReference = null;
235
+ let currentLastSegmentRaw = '';
236
+ let lastSegmentRaw = '';
237
+ if (lastSegmentReference) {
238
+ lastSegmentRaw = lastSegmentReference.noTrimEndRaw || lastSegmentReference.raw;
239
+ currentLastSegmentRaw = lastSegmentRaw + content;
240
+ }
241
+ else {
242
+ currentLastSegmentRaw = content;
243
+ }
244
+ const tokens = compiler(currentLastSegmentRaw);
245
+ // 如果最后一个token是space,则把lastSegmentRaw设置为空
246
+ if (tokens[tokens.length - 1].type === 'space') {
247
+ currentLastSegmentReference = null;
248
+ }
249
+ else {
250
+ currentLastSegmentReference = tokens[tokens.length - 1];
251
+ }
252
+ const pushAndSplitSegment = (raw, currenIndex, segmentTokenId) => {
253
+ const currentToken = tokens[currenIndex];
254
+ if (currenIndex > 0) {
255
+ const prevToken = tokens[currenIndex - 1];
256
+ if (prevToken.type !== 'space' && currentToken.type !== 'space') {
257
+ charsRef.current.push({ content: '', answerType, contentType: 'split_segment', tokenId: currentToken.id });
258
+ }
312
259
  }
313
- const pushAndSplitSegment = (raw, currenIndex) => {
314
- const currentToken = tokens[currenIndex];
315
- if (currenIndex > 0) {
316
- const prevToken = tokens[currenIndex - 1];
317
- if (prevToken.type !== 'space' && currentToken.type !== 'space') {
318
- charsRef.current.push({ content: '', answerType, contentType: 'split_segment', tokenId: currentToken.id });
319
- }
260
+ charsRef.current.push(...raw.split('').map((char) => ({ content: char, answerType, contentType: 'segment', tokenId: segmentTokenId })));
261
+ };
262
+ if (!lastSegmentReference) {
263
+ tokens.forEach((token, i) => {
264
+ if (token.type === 'space') {
265
+ charsRef.current.push({ content: token.raw, answerType, contentType: 'space', tokenId: token.id });
320
266
  }
321
- charsRef.current.push(...raw.split('').map((char) => ({ content: char, answerType, contentType: 'segment', tokenId: currentToken.id })));
322
- };
323
- if (!lastSegmentReference) {
324
- tokens.forEach((token, i) => {
325
- if (token.type === 'space') {
326
- charsRef.current.push({ content: token.raw, answerType, contentType: 'space', tokenId: token.id });
267
+ else {
268
+ pushAndSplitSegment(token.raw, i, token.id);
269
+ }
270
+ });
271
+ }
272
+ else {
273
+ let str = '';
274
+ let firstSpaceIndex = -1;
275
+ let nextTokenIndex = lastSegmentRaw.length;
276
+ for (let i = 0; i < tokens.length; i++) {
277
+ const token = tokens[i];
278
+ if (token.type === 'space') {
279
+ if (firstSpaceIndex === -1) {
280
+ firstSpaceIndex = str.length;
327
281
  }
328
- else {
329
- pushAndSplitSegment(token.raw, i);
330
- }
331
- });
332
- }
333
- else {
334
- let str = '';
335
- let firstSpaceIndex = -1;
336
- let nextTokenIndex = lastSegmentRaw.length;
337
- for (let i = 0; i < tokens.length; i++) {
338
- const token = tokens[i];
339
- if (token.type === 'space') {
340
- if (firstSpaceIndex === -1) {
341
- firstSpaceIndex = str.length;
342
- }
343
- str += token.raw;
344
- if (lastSegmentRaw.length > firstSpaceIndex) {
345
- // 如果lastSegmentRaw的长度大于firstSpaceIndex,则需要将当前设置为 segment
346
- charsRef.current.push(...token.raw.split('').map((char) => ({ content: char, answerType, contentType: 'segment', tokenId: token.id })));
347
- }
348
- else {
349
- charsRef.current.push({ content: token.raw, answerType, contentType: 'space', tokenId: token.id });
350
- }
282
+ str += token.raw;
283
+ if (lastSegmentRaw.length > firstSpaceIndex) {
284
+ // 如果lastSegmentRaw的长度大于firstSpaceIndex,则需要将当前设置为 segment
285
+ charsRef.current.push(...token.raw.split('').map((char) => ({ content: char, answerType, contentType: 'segment', tokenId: token.id })));
351
286
  }
352
287
  else {
353
- str += token.noTrimEndRaw || token.raw;
354
- const realRaw = str.slice(nextTokenIndex);
355
- if (realRaw.length > 0) {
356
- pushAndSplitSegment(realRaw, i);
357
- }
288
+ charsRef.current.push({ content: token.raw, answerType, contentType: 'space', tokenId: token.id });
358
289
  }
359
- nextTokenIndex = str.length;
360
290
  }
291
+ else {
292
+ str += token.noTrimEndRaw || token.raw;
293
+ const realRaw = str.slice(nextTokenIndex);
294
+ if (realRaw.length > 0) {
295
+ pushAndSplitSegment(realRaw, i, lastSegmentReference.id);
296
+ }
297
+ }
298
+ nextTokenIndex = str.length;
299
+ }
300
+ }
301
+ lastSegmentRawRef.current[`${answerType}Reference`] = currentLastSegmentReference;
302
+ if (!typingTask.isTyping()) {
303
+ typingTask.start();
304
+ }
305
+ };
306
+ /**
307
+ * 强制刷新缓冲区内容
308
+ */
309
+ const flushBuffer = (answerType) => {
310
+ const lastSegmentRef = lastSegmentRawRef.current;
311
+ if (answerType) {
312
+ // 刷新指定类型的缓冲区
313
+ const bufferKey = `${answerType}Buffer`;
314
+ const bufferedContent = lastSegmentRef[bufferKey];
315
+ if (bufferedContent) {
316
+ lastSegmentRef[bufferKey] = '';
317
+ processPushInternal(bufferedContent, answerType);
318
+ }
319
+ }
320
+ else {
321
+ // 刷新所有缓冲区
322
+ if (lastSegmentRef.thinkingBuffer) {
323
+ const content = lastSegmentRef.thinkingBuffer;
324
+ lastSegmentRef.thinkingBuffer = '';
325
+ processPushInternal(content, 'thinking');
361
326
  }
362
- lastSegmentRawRef.current[`${answerType}Reference`] = currentLastSegmentReference;
363
- if (!isTypedRef.current) {
364
- startTypedTask();
327
+ if (lastSegmentRef.answerBuffer) {
328
+ const content = lastSegmentRef.answerBuffer;
329
+ lastSegmentRef.answerBuffer = '';
330
+ processPushInternal(content, 'answer');
365
331
  }
332
+ }
333
+ };
334
+ useImperativeHandle(ref, () => ({
335
+ /**
336
+ * 添加内容
337
+ * @param content 内容 {string}
338
+ * @param answerType 回答类型 {AnswerType}
339
+ */
340
+ push: (content, answerType) => {
341
+ processBufferedPush(content, answerType);
366
342
  },
367
343
  /**
368
344
  * 清除打字任务
369
345
  */
370
346
  clear: () => {
371
- clearTimer();
347
+ typingTask.stop();
372
348
  charsRef.current = [];
373
349
  setStableSegments([]);
374
350
  setCurrentSegment(undefined);
375
351
  isWholeTypedEndRef.current = false;
376
- lastSegmentRawRef.current = {
377
- thinking: '',
378
- answer: '',
379
- thinkingReference: null,
380
- answerReference: null,
381
- };
352
+ currentParagraphRef.current = undefined;
353
+ typingTask.clear();
354
+ // 清理缓冲区
355
+ const lastSegmentRef = lastSegmentRawRef.current;
356
+ lastSegmentRef.thinkingBuffer = '';
357
+ lastSegmentRef.answerBuffer = '';
358
+ lastSegmentRef.thinkingReference = null;
359
+ lastSegmentRef.answerReference = null;
382
360
  },
383
361
  /**
384
362
  * 主动触发打字结束
385
363
  */
386
364
  triggerWholeEnd: () => {
365
+ // 先刷新所有缓冲区内容
366
+ flushBuffer();
387
367
  isWholeTypedEndRef.current = true;
388
- if (!isTypedRef.current) {
389
- triggerOnEnd();
368
+ if (!typingTask.isTyping()) {
369
+ // 这里需要手动触发结束回调,因为 hook 中的 triggerOnEnd 不能直接调用
370
+ onEnd?.({
371
+ str: undefined,
372
+ answerType: undefined,
373
+ });
390
374
  }
391
375
  },
376
+ /**
377
+ * 刷新缓冲区 (新增方法)
378
+ */
379
+ flushBuffer,
392
380
  }));
393
381
  const getParagraphs = (paragraphs, answerType) => {
394
382
  return (_jsxs("div", { className: `ds-markdown-paragraph ds-typed-${answerType}`, children: [paragraphs.map((paragraph, index) => {