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