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.
- package/README.md +305 -67
- package/dist/cjs/MarkdownCMD/index.d.ts +1 -0
- package/dist/cjs/MarkdownCMD/index.js +297 -309
- package/dist/cjs/MarkdownCMD/index.js.map +1 -1
- package/dist/cjs/defined.d.ts +14 -0
- package/dist/cjs/hooks/useTypingTask.d.ts +31 -0
- package/dist/cjs/hooks/useTypingTask.js +226 -0
- package/dist/cjs/hooks/useTypingTask.js.map +1 -0
- package/dist/cjs/utils/rule.js +0 -1
- package/dist/cjs/utils/rule.js.map +1 -1
- package/dist/esm/MarkdownCMD/index.d.ts +1 -0
- package/dist/esm/MarkdownCMD/index.js +298 -310
- package/dist/esm/MarkdownCMD/index.js.map +1 -1
- package/dist/esm/defined.d.ts +14 -0
- package/dist/esm/hooks/useTypingTask.d.ts +31 -0
- package/dist/esm/hooks/useTypingTask.js +222 -0
- package/dist/esm/hooks/useTypingTask.js.map +1 -0
- package/dist/esm/utils/rule.js +0 -1
- package/dist/esm/utils/rule.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { forwardRef,
|
|
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
|
-
|
|
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
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
|
|
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
|
|
242
|
-
// 如果当前段落和当前字符的回答类型不一致,则需要处理成两个段落
|
|
60
|
+
else {
|
|
243
61
|
setStableSegments((prev) => {
|
|
244
62
|
const newParagraphs = [...prev];
|
|
245
|
-
newParagraphs.push({
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
100
|
+
_currentParagraph = newCurrentParagraph;
|
|
271
101
|
}
|
|
272
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
302
|
-
|
|
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
|
-
//
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
311
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
329
|
-
|
|
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
|
-
|
|
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
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
|
|
347
|
+
typingTask.stop();
|
|
372
348
|
charsRef.current = [];
|
|
373
349
|
setStableSegments([]);
|
|
374
350
|
setCurrentSegment(undefined);
|
|
375
351
|
isWholeTypedEndRef.current = false;
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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 (!
|
|
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) => {
|