@lobehub/ui 5.32.5 → 5.32.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/es/Markdown/SyntaxMarkdown/StreamdownRender.mjs +15 -294
- package/es/Markdown/SyntaxMarkdown/StreamdownRender.mjs.map +1 -1
- package/es/Markdown/type.d.mts +2 -3
- package/es/base-ui/ContextMenu/renderItems.mjs +24 -1
- package/es/base-ui/ContextMenu/renderItems.mjs.map +1 -1
- package/es/base-ui/Segmented/Segmented.mjs +4 -0
- package/es/base-ui/Segmented/Segmented.mjs.map +1 -1
- package/es/base-ui/Segmented/style.mjs +4 -0
- package/es/base-ui/Segmented/style.mjs.map +1 -1
- package/es/base-ui/Tabs/style.mjs +12 -0
- package/es/base-ui/Tabs/style.mjs.map +1 -1
- package/es/hooks/useMarkdown/useMarkdownContent.mjs +4 -16
- package/es/hooks/useMarkdown/useMarkdownContent.mjs.map +1 -1
- package/es/hooks/useMarkdown/utils.mjs +1 -1
- package/es/hooks/useMarkdown/utils.mjs.map +1 -1
- package/es/index.d.mts +1 -1
- package/es/index.mjs +1 -1
- package/package.json +2 -88
- package/es/Markdown/SyntaxMarkdown/CachedMarkdown.mjs +0 -83
- package/es/Markdown/SyntaxMarkdown/CachedMarkdown.mjs.map +0 -1
- package/es/Markdown/SyntaxMarkdown/fenceState.mjs +0 -42
- package/es/Markdown/SyntaxMarkdown/fenceState.mjs.map +0 -1
- package/es/Markdown/SyntaxMarkdown/streamAnimationMeta.mjs +0 -14
- package/es/Markdown/SyntaxMarkdown/streamAnimationMeta.mjs.map +0 -1
- package/es/Markdown/SyntaxMarkdown/style.mjs +0 -28
- package/es/Markdown/SyntaxMarkdown/style.mjs.map +0 -1
- package/es/Markdown/SyntaxMarkdown/useSmoothStreamContent.mjs +0 -314
- package/es/Markdown/SyntaxMarkdown/useSmoothStreamContent.mjs.map +0 -1
- package/es/Markdown/SyntaxMarkdown/useStreamQueue.mjs +0 -88
- package/es/Markdown/SyntaxMarkdown/useStreamQueue.mjs.map +0 -1
- package/es/Markdown/plugins/rehypeStreamAnimated.d.mts +0 -33
- package/es/Markdown/plugins/rehypeStreamAnimated.mjs +0 -115
- package/es/Markdown/plugins/rehypeStreamAnimated.mjs.map +0 -1
- package/es/Markdown/streamProfiler/StreamdownProfilerProvider.mjs +0 -19
- package/es/Markdown/streamProfiler/StreamdownProfilerProvider.mjs.map +0 -1
- package/es/hooks/useMarkdown/latex.mjs +0 -294
- package/es/hooks/useMarkdown/latex.mjs.map +0 -1
- package/es/node_modules/.pnpm/@types_hast@3.0.5/node_modules/@types/hast/index.d.mts +0 -869
- package/es/utils/getNow.mjs +0 -8
- package/es/utils/getNow.mjs.map +0 -1
|
@@ -1,314 +0,0 @@
|
|
|
1
|
-
import { getNow } from "../../utils/getNow.mjs";
|
|
2
|
-
import { useStreamdownProfiler } from "../streamProfiler/StreamdownProfilerProvider.mjs";
|
|
3
|
-
import { findOpenFenceLanguage } from "./fenceState.mjs";
|
|
4
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
5
|
-
//#region src/Markdown/SyntaxMarkdown/useSmoothStreamContent.ts
|
|
6
|
-
const DEFAULT_BYPASS_LANGUAGES = ["html"];
|
|
7
|
-
const PRESET_CONFIG = {
|
|
8
|
-
balanced: {
|
|
9
|
-
activeInputWindowMs: 220,
|
|
10
|
-
bypassFencedLanguages: DEFAULT_BYPASS_LANGUAGES,
|
|
11
|
-
defaultCps: 38,
|
|
12
|
-
emaAlpha: .2,
|
|
13
|
-
flushCps: 120,
|
|
14
|
-
largeAppendChars: 120,
|
|
15
|
-
maxActiveCps: 132,
|
|
16
|
-
maxCps: 72,
|
|
17
|
-
maxFlushCps: 280,
|
|
18
|
-
minCommitIntervalMs: 48,
|
|
19
|
-
minCps: 18,
|
|
20
|
-
settleAfterMs: 360,
|
|
21
|
-
settleDrainMaxMs: 520,
|
|
22
|
-
settleDrainMinMs: 180,
|
|
23
|
-
targetBufferMs: 120
|
|
24
|
-
},
|
|
25
|
-
realtime: {
|
|
26
|
-
activeInputWindowMs: 140,
|
|
27
|
-
bypassFencedLanguages: DEFAULT_BYPASS_LANGUAGES,
|
|
28
|
-
defaultCps: 50,
|
|
29
|
-
emaAlpha: .3,
|
|
30
|
-
flushCps: 170,
|
|
31
|
-
largeAppendChars: 180,
|
|
32
|
-
maxActiveCps: 180,
|
|
33
|
-
maxCps: 96,
|
|
34
|
-
maxFlushCps: 360,
|
|
35
|
-
minCommitIntervalMs: 32,
|
|
36
|
-
minCps: 24,
|
|
37
|
-
settleAfterMs: 260,
|
|
38
|
-
settleDrainMaxMs: 360,
|
|
39
|
-
settleDrainMinMs: 140,
|
|
40
|
-
targetBufferMs: 40
|
|
41
|
-
},
|
|
42
|
-
silky: {
|
|
43
|
-
activeInputWindowMs: 320,
|
|
44
|
-
bypassFencedLanguages: DEFAULT_BYPASS_LANGUAGES,
|
|
45
|
-
defaultCps: 28,
|
|
46
|
-
emaAlpha: .14,
|
|
47
|
-
flushCps: 96,
|
|
48
|
-
largeAppendChars: 100,
|
|
49
|
-
maxActiveCps: 102,
|
|
50
|
-
maxCps: 56,
|
|
51
|
-
maxFlushCps: 220,
|
|
52
|
-
minCommitIntervalMs: 56,
|
|
53
|
-
minCps: 14,
|
|
54
|
-
settleAfterMs: 460,
|
|
55
|
-
settleDrainMaxMs: 680,
|
|
56
|
-
settleDrainMinMs: 240,
|
|
57
|
-
targetBufferMs: 170
|
|
58
|
-
}
|
|
59
|
-
};
|
|
60
|
-
const clamp = (value, min, max) => {
|
|
61
|
-
return Math.min(max, Math.max(min, value));
|
|
62
|
-
};
|
|
63
|
-
const MAX_COMMIT_INTERVAL_MS = 96;
|
|
64
|
-
const COMMIT_INTERVAL_TAIL_SCALE_UNITS = 256;
|
|
65
|
-
const findTailUnits = (content) => {
|
|
66
|
-
const boundary = content.lastIndexOf("\n\n");
|
|
67
|
-
return boundary === -1 ? content.length : content.length - boundary - 2;
|
|
68
|
-
};
|
|
69
|
-
const countChars = (text) => {
|
|
70
|
-
let count = 0;
|
|
71
|
-
for (const _char of text) count += 1;
|
|
72
|
-
return count;
|
|
73
|
-
};
|
|
74
|
-
const useSmoothStreamContent = (content, { enabled = true, preset = "balanced" } = {}) => {
|
|
75
|
-
const config = PRESET_CONFIG[preset];
|
|
76
|
-
const profiler = useStreamdownProfiler();
|
|
77
|
-
const [displayedContent, setDisplayedContent] = useState(content);
|
|
78
|
-
const displayedContentRef = useRef(content);
|
|
79
|
-
const displayedCountRef = useRef(countChars(content));
|
|
80
|
-
const targetContentRef = useRef(content);
|
|
81
|
-
const targetCharsRef = useRef([...content]);
|
|
82
|
-
const targetCountRef = useRef(targetCharsRef.current.length);
|
|
83
|
-
const emaCpsRef = useRef(config.defaultCps);
|
|
84
|
-
const tailUnitsRef = useRef(findTailUnits(content));
|
|
85
|
-
const lastInputTsRef = useRef(0);
|
|
86
|
-
const lastInputCountRef = useRef(targetCountRef.current);
|
|
87
|
-
const chunkSizeEmaRef = useRef(1);
|
|
88
|
-
const arrivalCpsEmaRef = useRef(config.defaultCps);
|
|
89
|
-
const rafRef = useRef(null);
|
|
90
|
-
const lastFrameTsRef = useRef(null);
|
|
91
|
-
const wakeTimerRef = useRef(null);
|
|
92
|
-
const clearWakeTimer = useCallback(() => {
|
|
93
|
-
if (wakeTimerRef.current !== null) {
|
|
94
|
-
clearTimeout(wakeTimerRef.current);
|
|
95
|
-
wakeTimerRef.current = null;
|
|
96
|
-
}
|
|
97
|
-
}, []);
|
|
98
|
-
const stopFrameLoop = useCallback(() => {
|
|
99
|
-
if (rafRef.current !== null) {
|
|
100
|
-
cancelAnimationFrame(rafRef.current);
|
|
101
|
-
rafRef.current = null;
|
|
102
|
-
}
|
|
103
|
-
lastFrameTsRef.current = null;
|
|
104
|
-
}, []);
|
|
105
|
-
const stopScheduling = useCallback(() => {
|
|
106
|
-
stopFrameLoop();
|
|
107
|
-
clearWakeTimer();
|
|
108
|
-
}, [clearWakeTimer, stopFrameLoop]);
|
|
109
|
-
const startFrameLoopRef = useRef(() => {});
|
|
110
|
-
const scheduleFrameWake = useCallback((delayMs) => {
|
|
111
|
-
clearWakeTimer();
|
|
112
|
-
wakeTimerRef.current = setTimeout(() => {
|
|
113
|
-
wakeTimerRef.current = null;
|
|
114
|
-
startFrameLoopRef.current();
|
|
115
|
-
}, Math.max(1, Math.ceil(delayMs)));
|
|
116
|
-
}, [clearWakeTimer]);
|
|
117
|
-
const syncImmediate = useCallback((nextContent) => {
|
|
118
|
-
stopScheduling();
|
|
119
|
-
const chars = [...nextContent];
|
|
120
|
-
const now = getNow();
|
|
121
|
-
targetContentRef.current = nextContent;
|
|
122
|
-
targetCharsRef.current = chars;
|
|
123
|
-
targetCountRef.current = chars.length;
|
|
124
|
-
displayedContentRef.current = nextContent;
|
|
125
|
-
displayedCountRef.current = chars.length;
|
|
126
|
-
setDisplayedContent(nextContent);
|
|
127
|
-
emaCpsRef.current = config.defaultCps;
|
|
128
|
-
chunkSizeEmaRef.current = 1;
|
|
129
|
-
arrivalCpsEmaRef.current = config.defaultCps;
|
|
130
|
-
tailUnitsRef.current = findTailUnits(nextContent);
|
|
131
|
-
lastInputTsRef.current = now;
|
|
132
|
-
lastInputCountRef.current = chars.length;
|
|
133
|
-
}, [config.defaultCps, stopScheduling]);
|
|
134
|
-
const startFrameLoop = useCallback(() => {
|
|
135
|
-
clearWakeTimer();
|
|
136
|
-
if (rafRef.current !== null) return;
|
|
137
|
-
const tick = (ts) => {
|
|
138
|
-
const targetCount = targetCountRef.current;
|
|
139
|
-
const displayedCount = displayedCountRef.current;
|
|
140
|
-
const backlog = targetCount - displayedCount;
|
|
141
|
-
if (backlog <= 0) {
|
|
142
|
-
stopFrameLoop();
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
if (lastFrameTsRef.current === null) {
|
|
146
|
-
lastFrameTsRef.current = ts;
|
|
147
|
-
rafRef.current = requestAnimationFrame(tick);
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
const commitIntervalMs = Math.min(MAX_COMMIT_INTERVAL_MS, config.minCommitIntervalMs * (1 + tailUnitsRef.current / COMMIT_INTERVAL_TAIL_SCALE_UNITS));
|
|
151
|
-
const frameIntervalMs = Math.max(0, ts - lastFrameTsRef.current);
|
|
152
|
-
if (frameIntervalMs < commitIntervalMs) {
|
|
153
|
-
rafRef.current = requestAnimationFrame(tick);
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
const frameStart = getNow();
|
|
157
|
-
const dtSeconds = Math.max(.001, Math.min(frameIntervalMs / 1e3, .12));
|
|
158
|
-
lastFrameTsRef.current = ts;
|
|
159
|
-
const idleMs = frameStart - lastInputTsRef.current;
|
|
160
|
-
const inputActive = idleMs <= config.activeInputWindowMs;
|
|
161
|
-
const settling = !inputActive && idleMs >= config.settleAfterMs;
|
|
162
|
-
const baseCps = clamp(emaCpsRef.current, config.minCps, config.maxCps);
|
|
163
|
-
const baseLagChars = Math.max(1, Math.round(baseCps * config.targetBufferMs / 1e3));
|
|
164
|
-
const lagUpperBound = Math.max(baseLagChars + 2, baseLagChars * 3);
|
|
165
|
-
const targetLagChars = inputActive ? Math.round(clamp(baseLagChars + chunkSizeEmaRef.current * .35, baseLagChars, lagUpperBound)) : 0;
|
|
166
|
-
const desiredDisplayed = Math.max(0, targetCount - targetLagChars);
|
|
167
|
-
let currentCps;
|
|
168
|
-
if (inputActive) {
|
|
169
|
-
const backlogPressure = targetLagChars > 0 ? backlog / targetLagChars : 1;
|
|
170
|
-
const chunkPressure = targetLagChars > 0 ? chunkSizeEmaRef.current / targetLagChars : 1;
|
|
171
|
-
const arrivalPressure = arrivalCpsEmaRef.current / Math.max(baseCps, 1);
|
|
172
|
-
const combinedPressure = clamp(backlogPressure * .6 + chunkPressure * .25 + arrivalPressure * .15, 1, 4.5);
|
|
173
|
-
const activeCap = clamp(config.maxActiveCps + chunkSizeEmaRef.current * 6, config.maxActiveCps, config.maxFlushCps);
|
|
174
|
-
currentCps = clamp(baseCps * combinedPressure, config.minCps, activeCap);
|
|
175
|
-
} else if (settling) {
|
|
176
|
-
const drainTargetMs = clamp(backlog * 8, config.settleDrainMinMs, config.settleDrainMaxMs);
|
|
177
|
-
const settleCps = backlog * 1e3 / drainTargetMs;
|
|
178
|
-
currentCps = clamp(settleCps, config.flushCps, config.maxFlushCps);
|
|
179
|
-
} else {
|
|
180
|
-
const idleFlushCps = Math.max(config.flushCps, baseCps * 1.8, arrivalCpsEmaRef.current * .8);
|
|
181
|
-
currentCps = clamp(idleFlushCps, config.flushCps, config.maxFlushCps);
|
|
182
|
-
}
|
|
183
|
-
const urgentBacklog = inputActive && targetLagChars > 0 && backlog > targetLagChars * 2.2;
|
|
184
|
-
const burstyInput = inputActive && chunkSizeEmaRef.current >= targetLagChars * .9;
|
|
185
|
-
let revealChars = Math.max(inputActive ? urgentBacklog || burstyInput ? 2 : 1 : 2, Math.round(currentCps * dtSeconds));
|
|
186
|
-
if (inputActive) {
|
|
187
|
-
const shortfall = desiredDisplayed - displayedCount;
|
|
188
|
-
if (shortfall <= 0) {
|
|
189
|
-
stopFrameLoop();
|
|
190
|
-
scheduleFrameWake(config.activeInputWindowMs - idleMs);
|
|
191
|
-
profiler?.recordAnimationFrame({
|
|
192
|
-
backlog,
|
|
193
|
-
durationMs: getNow() - frameStart,
|
|
194
|
-
frameIntervalMs,
|
|
195
|
-
inputActive,
|
|
196
|
-
revealChars: 0,
|
|
197
|
-
settling
|
|
198
|
-
});
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
revealChars = Math.min(revealChars, shortfall, backlog);
|
|
202
|
-
} else revealChars = Math.min(revealChars, backlog);
|
|
203
|
-
const nextCount = displayedCount + revealChars;
|
|
204
|
-
const segment = targetCharsRef.current.slice(displayedCount, nextCount).join("");
|
|
205
|
-
if (segment) {
|
|
206
|
-
const nextDisplayed = displayedContentRef.current + segment;
|
|
207
|
-
displayedContentRef.current = nextDisplayed;
|
|
208
|
-
displayedCountRef.current = nextCount;
|
|
209
|
-
setDisplayedContent(nextDisplayed);
|
|
210
|
-
} else {
|
|
211
|
-
displayedContentRef.current = targetContentRef.current;
|
|
212
|
-
displayedCountRef.current = targetCount;
|
|
213
|
-
setDisplayedContent(targetContentRef.current);
|
|
214
|
-
}
|
|
215
|
-
profiler?.recordAnimationFrame({
|
|
216
|
-
backlog,
|
|
217
|
-
durationMs: getNow() - frameStart,
|
|
218
|
-
frameIntervalMs,
|
|
219
|
-
inputActive,
|
|
220
|
-
revealChars: segment ? revealChars : backlog,
|
|
221
|
-
settling
|
|
222
|
-
});
|
|
223
|
-
rafRef.current = requestAnimationFrame(tick);
|
|
224
|
-
};
|
|
225
|
-
rafRef.current = requestAnimationFrame(tick);
|
|
226
|
-
}, [
|
|
227
|
-
clearWakeTimer,
|
|
228
|
-
config.activeInputWindowMs,
|
|
229
|
-
config.flushCps,
|
|
230
|
-
config.maxActiveCps,
|
|
231
|
-
config.maxCps,
|
|
232
|
-
config.maxFlushCps,
|
|
233
|
-
config.minCommitIntervalMs,
|
|
234
|
-
config.minCps,
|
|
235
|
-
config.settleAfterMs,
|
|
236
|
-
config.settleDrainMaxMs,
|
|
237
|
-
config.settleDrainMinMs,
|
|
238
|
-
config.targetBufferMs,
|
|
239
|
-
scheduleFrameWake,
|
|
240
|
-
stopFrameLoop
|
|
241
|
-
]);
|
|
242
|
-
startFrameLoopRef.current = startFrameLoop;
|
|
243
|
-
useEffect(() => {
|
|
244
|
-
if (!enabled) {
|
|
245
|
-
syncImmediate(content);
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
const prevTargetContent = targetContentRef.current;
|
|
249
|
-
if (content === prevTargetContent) return;
|
|
250
|
-
const now = getNow();
|
|
251
|
-
if (!content.startsWith(prevTargetContent)) {
|
|
252
|
-
syncImmediate(content);
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
if (config.bypassFencedLanguages.length > 0) {
|
|
256
|
-
const openLang = findOpenFenceLanguage(content);
|
|
257
|
-
if (openLang !== null && config.bypassFencedLanguages.includes(openLang)) {
|
|
258
|
-
syncImmediate(content);
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
const appendedChars = [...content.slice(prevTargetContent.length)];
|
|
263
|
-
const appendedCount = appendedChars.length;
|
|
264
|
-
profiler?.recordInputAppend({
|
|
265
|
-
appendedChars: appendedCount,
|
|
266
|
-
contentLength: countChars(content)
|
|
267
|
-
});
|
|
268
|
-
if (appendedCount > config.largeAppendChars) {
|
|
269
|
-
syncImmediate(content);
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
272
|
-
targetContentRef.current = content;
|
|
273
|
-
targetCharsRef.current.push(...appendedChars);
|
|
274
|
-
targetCountRef.current += appendedCount;
|
|
275
|
-
tailUnitsRef.current = findTailUnits(content);
|
|
276
|
-
const deltaChars = targetCountRef.current - lastInputCountRef.current;
|
|
277
|
-
const deltaMs = Math.max(1, now - lastInputTsRef.current);
|
|
278
|
-
if (deltaChars > 0) {
|
|
279
|
-
const instantCps = deltaChars * 1e3 / deltaMs;
|
|
280
|
-
const normalizedInstantCps = clamp(instantCps, config.minCps, config.maxFlushCps * 2);
|
|
281
|
-
const chunkEmaAlpha = .35;
|
|
282
|
-
chunkSizeEmaRef.current = chunkSizeEmaRef.current * .65 + appendedCount * chunkEmaAlpha;
|
|
283
|
-
arrivalCpsEmaRef.current = arrivalCpsEmaRef.current * .65 + normalizedInstantCps * chunkEmaAlpha;
|
|
284
|
-
const clampedCps = clamp(instantCps, config.minCps, config.maxActiveCps);
|
|
285
|
-
emaCpsRef.current = emaCpsRef.current * (1 - config.emaAlpha) + clampedCps * config.emaAlpha;
|
|
286
|
-
}
|
|
287
|
-
lastInputTsRef.current = now;
|
|
288
|
-
lastInputCountRef.current = targetCountRef.current;
|
|
289
|
-
startFrameLoop();
|
|
290
|
-
}, [
|
|
291
|
-
config.bypassFencedLanguages,
|
|
292
|
-
config.emaAlpha,
|
|
293
|
-
config.largeAppendChars,
|
|
294
|
-
config.maxActiveCps,
|
|
295
|
-
config.maxCps,
|
|
296
|
-
config.maxFlushCps,
|
|
297
|
-
config.minCps,
|
|
298
|
-
content,
|
|
299
|
-
enabled,
|
|
300
|
-
startFrameLoop,
|
|
301
|
-
syncImmediate,
|
|
302
|
-
profiler
|
|
303
|
-
]);
|
|
304
|
-
useEffect(() => {
|
|
305
|
-
return () => {
|
|
306
|
-
stopScheduling();
|
|
307
|
-
};
|
|
308
|
-
}, [stopScheduling]);
|
|
309
|
-
return displayedContent;
|
|
310
|
-
};
|
|
311
|
-
//#endregion
|
|
312
|
-
export { countChars, useSmoothStreamContent };
|
|
313
|
-
|
|
314
|
-
//# sourceMappingURL=useSmoothStreamContent.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"useSmoothStreamContent.mjs","names":["now"],"sources":["../../../src/Markdown/SyntaxMarkdown/useSmoothStreamContent.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\n\nimport { useStreamdownProfiler } from '@/Markdown/streamProfiler';\nimport { type StreamSmoothingPreset } from '@/Markdown/type';\nimport { getNow } from '@/utils/getNow';\n\nimport { findOpenFenceLanguage } from './fenceState';\n\ninterface StreamSmoothingPresetConfig {\n activeInputWindowMs: number;\n /**\n * Code-fence languages whose contents bypass smoothing entirely. While\n * the input ends inside an open fence in this set, every chunk is\n * `syncImmediate`-d to the display so downstream consumers (the\n * HtmlPreview iframe in particular) see partial HTML at the demo's\n * actual production rate instead of waiting on the smoother's\n * ~maxFlushCps budget. As soon as the fence closes — or another\n * non-bypass fence opens — smoothing resumes for the rest of the\n * stream. Default `['html']` covers the artifact case that motivated\n * this; tune via preset if you need to bypass `mermaid`, `svg`, etc.\n */\n bypassFencedLanguages: readonly string[];\n defaultCps: number;\n emaAlpha: number;\n flushCps: number;\n largeAppendChars: number;\n maxActiveCps: number;\n maxCps: number;\n maxFlushCps: number;\n minCommitIntervalMs: number;\n minCps: number;\n settleAfterMs: number;\n settleDrainMaxMs: number;\n settleDrainMinMs: number;\n targetBufferMs: number;\n}\n\nconst DEFAULT_BYPASS_LANGUAGES = ['html'] as const;\n\nconst PRESET_CONFIG: Record<StreamSmoothingPreset, StreamSmoothingPresetConfig> = {\n balanced: {\n activeInputWindowMs: 220,\n bypassFencedLanguages: DEFAULT_BYPASS_LANGUAGES,\n defaultCps: 38,\n emaAlpha: 0.2,\n flushCps: 120,\n largeAppendChars: 120,\n maxActiveCps: 132,\n maxCps: 72,\n maxFlushCps: 280,\n minCommitIntervalMs: 48,\n minCps: 18,\n settleAfterMs: 360,\n settleDrainMaxMs: 520,\n settleDrainMinMs: 180,\n targetBufferMs: 120,\n },\n realtime: {\n activeInputWindowMs: 140,\n bypassFencedLanguages: DEFAULT_BYPASS_LANGUAGES,\n defaultCps: 50,\n emaAlpha: 0.3,\n flushCps: 170,\n largeAppendChars: 180,\n maxActiveCps: 180,\n maxCps: 96,\n maxFlushCps: 360,\n minCommitIntervalMs: 32,\n minCps: 24,\n settleAfterMs: 260,\n settleDrainMaxMs: 360,\n settleDrainMinMs: 140,\n targetBufferMs: 40,\n },\n silky: {\n activeInputWindowMs: 320,\n bypassFencedLanguages: DEFAULT_BYPASS_LANGUAGES,\n defaultCps: 28,\n emaAlpha: 0.14,\n flushCps: 96,\n largeAppendChars: 100,\n maxActiveCps: 102,\n maxCps: 56,\n maxFlushCps: 220,\n minCommitIntervalMs: 56,\n minCps: 14,\n settleAfterMs: 460,\n settleDrainMaxMs: 680,\n settleDrainMinMs: 240,\n targetBufferMs: 170,\n },\n};\n\nconst clamp = (value: number, min: number, max: number): number => {\n return Math.min(max, Math.max(min, value));\n};\n\n// Every reveal commit re-parses and re-wraps the entire trailing block, so\n// the per-commit cost grows linearly with how far the content is from the\n// last block boundary. Widen the commit interval as that distance grows —\n// long paragraphs/code fences keep total work bounded while short blocks\n// stay at the preset's snappy interval. Per-char animation-delay stagger\n// hides the lower commit rate.\nconst MAX_COMMIT_INTERVAL_MS = 96;\nconst COMMIT_INTERVAL_TAIL_SCALE_UNITS = 256;\n\nconst findTailUnits = (content: string): number => {\n const boundary = content.lastIndexOf('\\n\\n');\n return boundary === -1 ? content.length : content.length - boundary - 2;\n};\n\nexport const countChars = (text: string): number => {\n let count = 0;\n for (const _char of text) count += 1;\n return count;\n};\n\ninterface UseSmoothStreamContentOptions {\n enabled?: boolean;\n preset?: StreamSmoothingPreset;\n}\n\nexport const useSmoothStreamContent = (\n content: string,\n { enabled = true, preset = 'balanced' }: UseSmoothStreamContentOptions = {},\n): string => {\n const config = PRESET_CONFIG[preset];\n const profiler = useStreamdownProfiler();\n const [displayedContent, setDisplayedContent] = useState(content);\n\n const displayedContentRef = useRef(content);\n const displayedCountRef = useRef(countChars(content));\n\n const targetContentRef = useRef(content);\n const targetCharsRef = useRef([...content]);\n const targetCountRef = useRef(targetCharsRef.current.length);\n\n const emaCpsRef = useRef(config.defaultCps);\n const tailUnitsRef = useRef(findTailUnits(content));\n const lastInputTsRef = useRef(0);\n const lastInputCountRef = useRef(targetCountRef.current);\n const chunkSizeEmaRef = useRef(1);\n const arrivalCpsEmaRef = useRef(config.defaultCps);\n\n const rafRef = useRef<number | null>(null);\n const lastFrameTsRef = useRef<number | null>(null);\n const wakeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const clearWakeTimer = useCallback(() => {\n if (wakeTimerRef.current !== null) {\n clearTimeout(wakeTimerRef.current);\n wakeTimerRef.current = null;\n }\n }, []);\n\n const stopFrameLoop = useCallback(() => {\n if (rafRef.current !== null) {\n cancelAnimationFrame(rafRef.current);\n rafRef.current = null;\n }\n lastFrameTsRef.current = null;\n }, []);\n\n const stopScheduling = useCallback(() => {\n stopFrameLoop();\n clearWakeTimer();\n }, [clearWakeTimer, stopFrameLoop]);\n\n const startFrameLoopRef = useRef<() => void>(() => {});\n\n const scheduleFrameWake = useCallback(\n (delayMs: number) => {\n clearWakeTimer();\n\n wakeTimerRef.current = setTimeout(\n () => {\n wakeTimerRef.current = null;\n startFrameLoopRef.current();\n },\n Math.max(1, Math.ceil(delayMs)),\n );\n },\n [clearWakeTimer],\n );\n\n const syncImmediate = useCallback(\n (nextContent: string) => {\n stopScheduling();\n\n const chars = [...nextContent];\n const now = getNow();\n\n targetContentRef.current = nextContent;\n targetCharsRef.current = chars;\n targetCountRef.current = chars.length;\n\n displayedContentRef.current = nextContent;\n displayedCountRef.current = chars.length;\n setDisplayedContent(nextContent);\n\n emaCpsRef.current = config.defaultCps;\n chunkSizeEmaRef.current = 1;\n arrivalCpsEmaRef.current = config.defaultCps;\n tailUnitsRef.current = findTailUnits(nextContent);\n lastInputTsRef.current = now;\n lastInputCountRef.current = chars.length;\n },\n [config.defaultCps, stopScheduling],\n );\n\n const startFrameLoop = useCallback(() => {\n clearWakeTimer();\n if (rafRef.current !== null) return;\n\n const tick = (ts: number) => {\n const targetCount = targetCountRef.current;\n const displayedCount = displayedCountRef.current;\n const backlog = targetCount - displayedCount;\n\n if (backlog <= 0) {\n stopFrameLoop();\n return;\n }\n\n if (lastFrameTsRef.current === null) {\n lastFrameTsRef.current = ts;\n rafRef.current = requestAnimationFrame(tick);\n return;\n }\n\n // Reveal commits are throttled below the display refresh rate: each\n // commit re-renders the tail block and re-runs remend + lexing, so\n // committing at 60-120fps burns CPU without visible benefit — the\n // per-char stagger inside a commit is carried by animation-delay.\n const commitIntervalMs = Math.min(\n MAX_COMMIT_INTERVAL_MS,\n config.minCommitIntervalMs * (1 + tailUnitsRef.current / COMMIT_INTERVAL_TAIL_SCALE_UNITS),\n );\n const frameIntervalMs = Math.max(0, ts - lastFrameTsRef.current);\n if (frameIntervalMs < commitIntervalMs) {\n rafRef.current = requestAnimationFrame(tick);\n return;\n }\n\n const frameStart = getNow();\n const dtSeconds = Math.max(0.001, Math.min(frameIntervalMs / 1000, 0.12));\n lastFrameTsRef.current = ts;\n\n const now = frameStart;\n const idleMs = now - lastInputTsRef.current;\n const inputActive = idleMs <= config.activeInputWindowMs;\n const settling = !inputActive && idleMs >= config.settleAfterMs;\n\n const baseCps = clamp(emaCpsRef.current, config.minCps, config.maxCps);\n const baseLagChars = Math.max(1, Math.round((baseCps * config.targetBufferMs) / 1000));\n const lagUpperBound = Math.max(baseLagChars + 2, baseLagChars * 3);\n const targetLagChars = inputActive\n ? Math.round(\n clamp(baseLagChars + chunkSizeEmaRef.current * 0.35, baseLagChars, lagUpperBound),\n )\n : 0;\n const desiredDisplayed = Math.max(0, targetCount - targetLagChars);\n\n let currentCps: number;\n if (inputActive) {\n const backlogPressure = targetLagChars > 0 ? backlog / targetLagChars : 1;\n const chunkPressure = targetLagChars > 0 ? chunkSizeEmaRef.current / targetLagChars : 1;\n const arrivalPressure = arrivalCpsEmaRef.current / Math.max(baseCps, 1);\n const combinedPressure = clamp(\n backlogPressure * 0.6 + chunkPressure * 0.25 + arrivalPressure * 0.15,\n 1,\n 4.5,\n );\n const activeCap = clamp(\n config.maxActiveCps + chunkSizeEmaRef.current * 6,\n config.maxActiveCps,\n config.maxFlushCps,\n );\n currentCps = clamp(baseCps * combinedPressure, config.minCps, activeCap);\n } else if (settling) {\n // If upstream likely ended, cap the remaining tail duration so\n // we do not keep replaying old backlog for seconds.\n const drainTargetMs = clamp(backlog * 8, config.settleDrainMinMs, config.settleDrainMaxMs);\n const settleCps = (backlog * 1000) / drainTargetMs;\n currentCps = clamp(settleCps, config.flushCps, config.maxFlushCps);\n } else {\n const idleFlushCps = Math.max(\n config.flushCps,\n baseCps * 1.8,\n arrivalCpsEmaRef.current * 0.8,\n );\n currentCps = clamp(idleFlushCps, config.flushCps, config.maxFlushCps);\n }\n\n const urgentBacklog = inputActive && targetLagChars > 0 && backlog > targetLagChars * 2.2;\n const burstyInput = inputActive && chunkSizeEmaRef.current >= targetLagChars * 0.9;\n const minRevealChars = inputActive ? (urgentBacklog || burstyInput ? 2 : 1) : 2;\n let revealChars = Math.max(minRevealChars, Math.round(currentCps * dtSeconds));\n\n if (inputActive) {\n const shortfall = desiredDisplayed - displayedCount;\n if (shortfall <= 0) {\n stopFrameLoop();\n scheduleFrameWake(config.activeInputWindowMs - idleMs);\n\n profiler?.recordAnimationFrame({\n backlog,\n durationMs: getNow() - frameStart,\n frameIntervalMs,\n inputActive,\n revealChars: 0,\n settling,\n });\n return;\n }\n revealChars = Math.min(revealChars, shortfall, backlog);\n } else {\n revealChars = Math.min(revealChars, backlog);\n }\n\n const nextCount = displayedCount + revealChars;\n const segment = targetCharsRef.current.slice(displayedCount, nextCount).join('');\n\n if (segment) {\n const nextDisplayed = displayedContentRef.current + segment;\n displayedContentRef.current = nextDisplayed;\n displayedCountRef.current = nextCount;\n setDisplayedContent(nextDisplayed);\n } else {\n displayedContentRef.current = targetContentRef.current;\n displayedCountRef.current = targetCount;\n setDisplayedContent(targetContentRef.current);\n }\n\n profiler?.recordAnimationFrame({\n backlog,\n durationMs: getNow() - frameStart,\n frameIntervalMs,\n inputActive,\n revealChars: segment ? revealChars : backlog,\n settling,\n });\n\n rafRef.current = requestAnimationFrame(tick);\n };\n\n rafRef.current = requestAnimationFrame(tick);\n }, [\n clearWakeTimer,\n config.activeInputWindowMs,\n config.flushCps,\n config.maxActiveCps,\n config.maxCps,\n config.maxFlushCps,\n config.minCommitIntervalMs,\n config.minCps,\n config.settleAfterMs,\n config.settleDrainMaxMs,\n config.settleDrainMinMs,\n config.targetBufferMs,\n scheduleFrameWake,\n stopFrameLoop,\n ]);\n startFrameLoopRef.current = startFrameLoop;\n\n useEffect(() => {\n if (!enabled) {\n syncImmediate(content);\n return;\n }\n\n const prevTargetContent = targetContentRef.current;\n if (content === prevTargetContent) return;\n\n const now = getNow();\n const appendOnly = content.startsWith(prevTargetContent);\n\n if (!appendOnly) {\n syncImmediate(content);\n return;\n }\n\n // Bypass smoothing entirely while the input ends inside an open\n // fence whose language is opted out — see the preset config.\n // Without this, a 5 KB inline `<style>` block can keep `</style>`\n // (and therefore the HtmlPreview head-close trigger) trapped in the\n // smoother's buffer for tens of seconds, leaving the iframe pinned\n // on the loading placeholder while the artifact \"looks stuck\".\n if (config.bypassFencedLanguages.length > 0) {\n const openLang = findOpenFenceLanguage(content);\n if (openLang !== null && config.bypassFencedLanguages.includes(openLang)) {\n syncImmediate(content);\n return;\n }\n }\n\n const appended = content.slice(prevTargetContent.length);\n const appendedChars = [...appended];\n const appendedCount = appendedChars.length;\n\n profiler?.recordInputAppend({\n appendedChars: appendedCount,\n contentLength: countChars(content),\n });\n\n if (appendedCount > config.largeAppendChars) {\n syncImmediate(content);\n return;\n }\n\n targetContentRef.current = content;\n targetCharsRef.current.push(...appendedChars);\n targetCountRef.current += appendedCount;\n\n tailUnitsRef.current = findTailUnits(content);\n\n const deltaChars = targetCountRef.current - lastInputCountRef.current;\n const deltaMs = Math.max(1, now - lastInputTsRef.current);\n\n if (deltaChars > 0) {\n const instantCps = (deltaChars * 1000) / deltaMs;\n const normalizedInstantCps = clamp(instantCps, config.minCps, config.maxFlushCps * 2);\n const chunkEmaAlpha = 0.35;\n chunkSizeEmaRef.current =\n chunkSizeEmaRef.current * (1 - chunkEmaAlpha) + appendedCount * chunkEmaAlpha;\n arrivalCpsEmaRef.current =\n arrivalCpsEmaRef.current * (1 - chunkEmaAlpha) + normalizedInstantCps * chunkEmaAlpha;\n\n const clampedCps = clamp(instantCps, config.minCps, config.maxActiveCps);\n emaCpsRef.current = emaCpsRef.current * (1 - config.emaAlpha) + clampedCps * config.emaAlpha;\n }\n\n lastInputTsRef.current = now;\n lastInputCountRef.current = targetCountRef.current;\n\n startFrameLoop();\n }, [\n config.bypassFencedLanguages,\n config.emaAlpha,\n config.largeAppendChars,\n config.maxActiveCps,\n config.maxCps,\n config.maxFlushCps,\n config.minCps,\n content,\n enabled,\n startFrameLoop,\n syncImmediate,\n profiler,\n ]);\n\n useEffect(() => {\n return () => {\n stopScheduling();\n };\n }, [stopScheduling]);\n\n return displayedContent;\n};\n"],"mappings":"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"}
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import { countChars } from "./useSmoothStreamContent.mjs";
|
|
2
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
-
//#region src/Markdown/SyntaxMarkdown/useStreamQueue.ts
|
|
4
|
-
const BASE_DELAY = 18;
|
|
5
|
-
const ACCELERATION_FACTOR = .3;
|
|
6
|
-
const MAX_BLOCK_DURATION = 3e3;
|
|
7
|
-
function computeCharDelay(queueLength, charCount) {
|
|
8
|
-
const acceleration = 1 + queueLength * ACCELERATION_FACTOR;
|
|
9
|
-
let delay = BASE_DELAY / acceleration;
|
|
10
|
-
delay = Math.min(delay, MAX_BLOCK_DURATION / Math.max(charCount, 1));
|
|
11
|
-
return delay;
|
|
12
|
-
}
|
|
13
|
-
function useStreamQueue(blocks) {
|
|
14
|
-
const [revealedCount, setRevealedCount] = useState(0);
|
|
15
|
-
const timerRef = useRef(null);
|
|
16
|
-
const prevBlocksLenRef = useRef(0);
|
|
17
|
-
const minRevealedRef = useRef(0);
|
|
18
|
-
if (blocks.length === 0 && prevBlocksLenRef.current !== 0) minRevealedRef.current = 0;
|
|
19
|
-
if (blocks.length > prevBlocksLenRef.current && prevBlocksLenRef.current > 0) {
|
|
20
|
-
const prevTail = prevBlocksLenRef.current - 1;
|
|
21
|
-
minRevealedRef.current = Math.max(minRevealedRef.current, prevTail + 1);
|
|
22
|
-
}
|
|
23
|
-
prevBlocksLenRef.current = blocks.length;
|
|
24
|
-
useEffect(() => {
|
|
25
|
-
if (blocks.length === 0) {
|
|
26
|
-
setRevealedCount(0);
|
|
27
|
-
minRevealedRef.current = 0;
|
|
28
|
-
if (timerRef.current) {
|
|
29
|
-
clearTimeout(timerRef.current);
|
|
30
|
-
timerRef.current = null;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}, [blocks.length]);
|
|
34
|
-
const effectiveRevealedCount = Math.max(revealedCount, minRevealedRef.current);
|
|
35
|
-
const tailIndex = blocks.length - 1;
|
|
36
|
-
const getBlockState = useCallback((index) => {
|
|
37
|
-
if (index < effectiveRevealedCount) return "revealed";
|
|
38
|
-
if (index === effectiveRevealedCount && index < tailIndex) return "animating";
|
|
39
|
-
if (index === effectiveRevealedCount && index === tailIndex) return "streaming";
|
|
40
|
-
return "queued";
|
|
41
|
-
}, [effectiveRevealedCount, tailIndex]);
|
|
42
|
-
const queueLength = Math.max(0, tailIndex - effectiveRevealedCount - 1);
|
|
43
|
-
const animatingIndex = effectiveRevealedCount < tailIndex ? effectiveRevealedCount : -1;
|
|
44
|
-
const animatingCharCount = animatingIndex >= 0 ? countChars(blocks[animatingIndex]?.content ?? "") : 0;
|
|
45
|
-
const activeIndex = animatingIndex >= 0 ? animatingIndex : animatingIndex < 0 && tailIndex >= effectiveRevealedCount ? tailIndex : -1;
|
|
46
|
-
const activeCharCount = activeIndex >= 0 ? countChars(blocks[activeIndex]?.content ?? "") : 0;
|
|
47
|
-
const frozenRef = useRef({
|
|
48
|
-
delay: BASE_DELAY,
|
|
49
|
-
index: -1
|
|
50
|
-
});
|
|
51
|
-
if (activeIndex >= 0 && activeIndex !== frozenRef.current.index) frozenRef.current = {
|
|
52
|
-
delay: computeCharDelay(queueLength, activeCharCount),
|
|
53
|
-
index: activeIndex
|
|
54
|
-
};
|
|
55
|
-
const charDelay = activeIndex >= 0 ? frozenRef.current.delay : BASE_DELAY;
|
|
56
|
-
const onAnimationDone = useCallback(() => {
|
|
57
|
-
setRevealedCount(effectiveRevealedCount + 1);
|
|
58
|
-
}, [effectiveRevealedCount]);
|
|
59
|
-
useEffect(() => {
|
|
60
|
-
if (timerRef.current) {
|
|
61
|
-
clearTimeout(timerRef.current);
|
|
62
|
-
timerRef.current = null;
|
|
63
|
-
}
|
|
64
|
-
if (animatingIndex < 0) return;
|
|
65
|
-
const totalTime = Math.max(0, (animatingCharCount - 1) * charDelay) + 180;
|
|
66
|
-
timerRef.current = setTimeout(onAnimationDone, totalTime);
|
|
67
|
-
return () => {
|
|
68
|
-
if (timerRef.current) {
|
|
69
|
-
clearTimeout(timerRef.current);
|
|
70
|
-
timerRef.current = null;
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
}, [
|
|
74
|
-
animatingIndex,
|
|
75
|
-
animatingCharCount,
|
|
76
|
-
charDelay,
|
|
77
|
-
onAnimationDone
|
|
78
|
-
]);
|
|
79
|
-
return {
|
|
80
|
-
charDelay,
|
|
81
|
-
getBlockState,
|
|
82
|
-
queueLength
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
//#endregion
|
|
86
|
-
export { useStreamQueue };
|
|
87
|
-
|
|
88
|
-
//# sourceMappingURL=useStreamQueue.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"useStreamQueue.mjs","names":[],"sources":["../../../src/Markdown/SyntaxMarkdown/useStreamQueue.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\n\nimport { STREAM_FADE_DURATION } from './style';\nimport { countChars } from './useSmoothStreamContent';\n\nexport interface BlockInfo {\n content: string;\n startOffset: number;\n}\n\nexport type BlockState = 'revealed' | 'animating' | 'streaming' | 'queued';\n\nconst BASE_DELAY = 18;\nconst ACCELERATION_FACTOR = 0.3;\nconst MAX_BLOCK_DURATION = 3000;\n\nfunction computeCharDelay(queueLength: number, charCount: number): number {\n const acceleration = 1 + queueLength * ACCELERATION_FACTOR;\n let delay = BASE_DELAY / acceleration;\n delay = Math.min(delay, MAX_BLOCK_DURATION / Math.max(charCount, 1));\n return delay;\n}\n\nexport interface UseStreamQueueReturn {\n charDelay: number;\n getBlockState: (index: number) => BlockState;\n queueLength: number;\n}\n\nexport function useStreamQueue(blocks: BlockInfo[]): UseStreamQueueReturn {\n const [revealedCount, setRevealedCount] = useState(0);\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const prevBlocksLenRef = useRef(0);\n const minRevealedRef = useRef(0);\n\n // Synchronous auto-reveal during render.\n // When blocks grow, the previous tail (streaming block) is instantly\n // promoted to revealed — its chars are already visible via stream-mode\n // animation. This runs during render (not in effect) so there is NO\n // intermediate frame where the old streaming block enters 'animating'\n // state and gets stagger plugins that would restart its animations.\n if (blocks.length === 0 && prevBlocksLenRef.current !== 0) {\n minRevealedRef.current = 0;\n }\n if (blocks.length > prevBlocksLenRef.current && prevBlocksLenRef.current > 0) {\n const prevTail = prevBlocksLenRef.current - 1;\n minRevealedRef.current = Math.max(minRevealedRef.current, prevTail + 1);\n }\n prevBlocksLenRef.current = blocks.length;\n\n // State reset when stream restarts (blocks empty)\n useEffect(() => {\n if (blocks.length === 0) {\n setRevealedCount(0);\n minRevealedRef.current = 0;\n if (timerRef.current) {\n clearTimeout(timerRef.current);\n timerRef.current = null;\n }\n }\n }, [blocks.length]);\n\n const effectiveRevealedCount = Math.max(revealedCount, minRevealedRef.current);\n const tailIndex = blocks.length - 1;\n\n const getBlockState = useCallback(\n (index: number): BlockState => {\n if (index < effectiveRevealedCount) return 'revealed';\n if (index === effectiveRevealedCount && index < tailIndex) return 'animating';\n if (index === effectiveRevealedCount && index === tailIndex) return 'streaming';\n return 'queued';\n },\n [effectiveRevealedCount, tailIndex],\n );\n\n const queueLength = Math.max(0, tailIndex - effectiveRevealedCount - 1);\n\n const animatingIndex = effectiveRevealedCount < tailIndex ? effectiveRevealedCount : -1;\n const animatingCharCount =\n animatingIndex >= 0 ? countChars(blocks[animatingIndex]?.content ?? '') : 0;\n\n const streamingIndex = animatingIndex < 0 && tailIndex >= effectiveRevealedCount ? tailIndex : -1;\n const activeIndex = animatingIndex >= 0 ? animatingIndex : streamingIndex;\n const activeCharCount = activeIndex >= 0 ? countChars(blocks[activeIndex]?.content ?? '') : 0;\n\n // Freeze charDelay when entering a new active block (animating or streaming)\n const frozenRef = useRef({ delay: BASE_DELAY, index: -1 });\n if (activeIndex >= 0 && activeIndex !== frozenRef.current.index) {\n frozenRef.current = {\n delay: computeCharDelay(queueLength, activeCharCount),\n index: activeIndex,\n };\n }\n const charDelay = activeIndex >= 0 ? frozenRef.current.delay : BASE_DELAY;\n\n const onAnimationDone = useCallback(() => {\n setRevealedCount(effectiveRevealedCount + 1);\n }, [effectiveRevealedCount]);\n\n useEffect(() => {\n if (timerRef.current) {\n clearTimeout(timerRef.current);\n timerRef.current = null;\n }\n\n if (animatingIndex < 0) return;\n\n const totalTime = Math.max(0, (animatingCharCount - 1) * charDelay) + STREAM_FADE_DURATION;\n timerRef.current = setTimeout(onAnimationDone, totalTime);\n\n return () => {\n if (timerRef.current) {\n clearTimeout(timerRef.current);\n timerRef.current = null;\n }\n };\n }, [animatingIndex, animatingCharCount, charDelay, onAnimationDone]);\n\n return { charDelay, getBlockState, queueLength };\n}\n"],"mappings":";;;AAYA,MAAM,aAAa;AACnB,MAAM,sBAAsB;AAC5B,MAAM,qBAAqB;AAE3B,SAAS,iBAAiB,aAAqB,WAA2B;CACxE,MAAM,eAAe,IAAI,cAAc;CACvC,IAAI,QAAQ,aAAa;CACzB,QAAQ,KAAK,IAAI,OAAO,qBAAqB,KAAK,IAAI,WAAW,CAAC,CAAC;CACnE,OAAO;AACT;AAQA,SAAgB,eAAe,QAA2C;CACxE,MAAM,CAAC,eAAe,oBAAoB,SAAS,CAAC;CACpD,MAAM,WAAW,OAA6C,IAAI;CAClE,MAAM,mBAAmB,OAAO,CAAC;CACjC,MAAM,iBAAiB,OAAO,CAAC;CAQ/B,IAAI,OAAO,WAAW,KAAK,iBAAiB,YAAY,GACtD,eAAe,UAAU;CAE3B,IAAI,OAAO,SAAS,iBAAiB,WAAW,iBAAiB,UAAU,GAAG;EAC5E,MAAM,WAAW,iBAAiB,UAAU;EAC5C,eAAe,UAAU,KAAK,IAAI,eAAe,SAAS,WAAW,CAAC;CACxE;CACA,iBAAiB,UAAU,OAAO;CAGlC,gBAAgB;EACd,IAAI,OAAO,WAAW,GAAG;GACvB,iBAAiB,CAAC;GAClB,eAAe,UAAU;GACzB,IAAI,SAAS,SAAS;IACpB,aAAa,SAAS,OAAO;IAC7B,SAAS,UAAU;GACrB;EACF;CACF,GAAG,CAAC,OAAO,MAAM,CAAC;CAElB,MAAM,yBAAyB,KAAK,IAAI,eAAe,eAAe,OAAO;CAC7E,MAAM,YAAY,OAAO,SAAS;CAElC,MAAM,gBAAgB,aACnB,UAA8B;EAC7B,IAAI,QAAQ,wBAAwB,OAAO;EAC3C,IAAI,UAAU,0BAA0B,QAAQ,WAAW,OAAO;EAClE,IAAI,UAAU,0BAA0B,UAAU,WAAW,OAAO;EACpE,OAAO;CACT,GACA,CAAC,wBAAwB,SAAS,CACpC;CAEA,MAAM,cAAc,KAAK,IAAI,GAAG,YAAY,yBAAyB,CAAC;CAEtE,MAAM,iBAAiB,yBAAyB,YAAY,yBAAyB;CACrF,MAAM,qBACJ,kBAAkB,IAAI,WAAW,OAAO,eAAe,EAAE,WAAW,EAAE,IAAI;CAG5E,MAAM,cAAc,kBAAkB,IAAI,iBADnB,iBAAiB,KAAK,aAAa,yBAAyB,YAAY;CAE/F,MAAM,kBAAkB,eAAe,IAAI,WAAW,OAAO,YAAY,EAAE,WAAW,EAAE,IAAI;CAG5F,MAAM,YAAY,OAAO;EAAE,OAAO;EAAY,OAAO;CAAG,CAAC;CACzD,IAAI,eAAe,KAAK,gBAAgB,UAAU,QAAQ,OACxD,UAAU,UAAU;EAClB,OAAO,iBAAiB,aAAa,eAAe;EACpD,OAAO;CACT;CAEF,MAAM,YAAY,eAAe,IAAI,UAAU,QAAQ,QAAQ;CAE/D,MAAM,kBAAkB,kBAAkB;EACxC,iBAAiB,yBAAyB,CAAC;CAC7C,GAAG,CAAC,sBAAsB,CAAC;CAE3B,gBAAgB;EACd,IAAI,SAAS,SAAS;GACpB,aAAa,SAAS,OAAO;GAC7B,SAAS,UAAU;EACrB;EAEA,IAAI,iBAAiB,GAAG;EAExB,MAAM,YAAY,KAAK,IAAI,IAAI,qBAAqB,KAAK,SAAS,IAAA;EAClE,SAAS,UAAU,WAAW,iBAAiB,SAAS;EAExD,aAAa;GACX,IAAI,SAAS,SAAS;IACpB,aAAa,SAAS,OAAO;IAC7B,SAAS,UAAU;GACrB;EACF;CACF,GAAG;EAAC;EAAgB;EAAoB;EAAW;CAAe,CAAC;CAEnE,OAAO;EAAE;EAAW;EAAe;CAAY;AACjD"}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { Root } from "../../node_modules/.pnpm/@types_hast@3.0.5/node_modules/@types/hast/index.mjs";
|
|
2
|
-
//#region src/Markdown/plugins/rehypeStreamAnimated.d.ts
|
|
3
|
-
interface StreamAnimatedRuntime {
|
|
4
|
-
births: number[];
|
|
5
|
-
/**
|
|
6
|
-
* Write-once per-char render cache, indexed like `births`:
|
|
7
|
-
* `undefined` = char not rendered yet, `null` = born fully revealed,
|
|
8
|
-
* string = inline style frozen at first render.
|
|
9
|
-
* Freezing the style keeps span props referentially stable across the
|
|
10
|
-
* tail block's re-renders, so React never rewrites `animation-delay`
|
|
11
|
-
* on an in-flight fade (a rewrite restarts the CSS animation).
|
|
12
|
-
*/
|
|
13
|
-
styles: (string | null | undefined)[];
|
|
14
|
-
}
|
|
15
|
-
interface StreamAnimatedOptions {
|
|
16
|
-
births?: number[];
|
|
17
|
-
fadeDuration?: number;
|
|
18
|
-
/**
|
|
19
|
-
* `'word'` wraps whitespace-delimited runs in one span instead of one
|
|
20
|
-
* span per char. Every concurrent CSS animation keeps the compositor
|
|
21
|
-
* producing frames and fires animationstart/end through React's root
|
|
22
|
-
* event delegation, so animating ~5x fewer nodes is the main CPU lever —
|
|
23
|
-
* char-level remains available for the finer-grained look.
|
|
24
|
-
*/
|
|
25
|
-
granularity?: 'char' | 'word';
|
|
26
|
-
nowMs?: number;
|
|
27
|
-
revealed?: boolean;
|
|
28
|
-
runtime?: StreamAnimatedRuntime;
|
|
29
|
-
}
|
|
30
|
-
declare const rehypeStreamAnimated: (options?: StreamAnimatedOptions) => (tree: Root) => void;
|
|
31
|
-
//#endregion
|
|
32
|
-
export { StreamAnimatedOptions, StreamAnimatedRuntime, rehypeStreamAnimated };
|
|
33
|
-
//# sourceMappingURL=rehypeStreamAnimated.d.mts.map
|
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import { visit } from "../../node_modules/.pnpm/unist-util-visit@5.1.0/node_modules/unist-util-visit/lib/index.mjs";
|
|
2
|
-
import { getNow } from "../../utils/getNow.mjs";
|
|
3
|
-
//#region src/Markdown/plugins/rehypeStreamAnimated.ts
|
|
4
|
-
const WORD_SEGMENT_RE = /\s+|\S+/g;
|
|
5
|
-
const wordSegmenter = typeof Intl !== "undefined" && "Segmenter" in Intl ? new Intl.Segmenter(void 0, { granularity: "word" }) : null;
|
|
6
|
-
const segmentWords = (value) => {
|
|
7
|
-
if (!wordSegmenter) return value.match(WORD_SEGMENT_RE) ?? [];
|
|
8
|
-
const segments = [];
|
|
9
|
-
for (const item of wordSegmenter.segment(value)) segments.push(item.segment);
|
|
10
|
-
return segments;
|
|
11
|
-
};
|
|
12
|
-
const BLOCK_TAGS = /* @__PURE__ */ new Set([
|
|
13
|
-
"p",
|
|
14
|
-
"h1",
|
|
15
|
-
"h2",
|
|
16
|
-
"h3",
|
|
17
|
-
"h4",
|
|
18
|
-
"h5",
|
|
19
|
-
"h6",
|
|
20
|
-
"li"
|
|
21
|
-
]);
|
|
22
|
-
const SKIP_TAGS = /* @__PURE__ */ new Set([
|
|
23
|
-
"pre",
|
|
24
|
-
"code",
|
|
25
|
-
"table",
|
|
26
|
-
"svg"
|
|
27
|
-
]);
|
|
28
|
-
function hasClass(node, cls) {
|
|
29
|
-
const cn = node.properties?.className;
|
|
30
|
-
if (Array.isArray(cn)) return cn.some((c) => String(c).includes(cls));
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
33
|
-
const rehypeStreamAnimated = (options = {}) => {
|
|
34
|
-
const { births, fadeDuration = 150, granularity = "char", nowMs, revealed = false, runtime } = options;
|
|
35
|
-
const resolvedRuntime = revealed ? void 0 : runtime ?? (Array.isArray(births) && typeof nowMs === "number" ? {
|
|
36
|
-
births,
|
|
37
|
-
styles: []
|
|
38
|
-
} : void 0);
|
|
39
|
-
const nowOverride = runtime ? void 0 : nowMs;
|
|
40
|
-
return (tree) => {
|
|
41
|
-
let globalCharIndex = 0;
|
|
42
|
-
const now = nowOverride ?? (resolvedRuntime ? getNow() : 0);
|
|
43
|
-
const shouldSkip = (node) => {
|
|
44
|
-
return SKIP_TAGS.has(node.tagName) || hasClass(node, "katex");
|
|
45
|
-
};
|
|
46
|
-
const resolveStyle = (index) => {
|
|
47
|
-
const styles = resolvedRuntime.styles;
|
|
48
|
-
const cached = styles[index];
|
|
49
|
-
if (cached !== void 0) return cached;
|
|
50
|
-
const birthTs = resolvedRuntime.births[index];
|
|
51
|
-
let resolved;
|
|
52
|
-
if (birthTs === void 0) resolved = null;
|
|
53
|
-
else {
|
|
54
|
-
const elapsed = now - birthTs;
|
|
55
|
-
resolved = elapsed >= fadeDuration ? null : `animation-delay:${-elapsed}ms`;
|
|
56
|
-
}
|
|
57
|
-
styles[index] = resolved;
|
|
58
|
-
return resolved;
|
|
59
|
-
};
|
|
60
|
-
const buildSpan = (value, startIndex) => {
|
|
61
|
-
let className = "stream-char";
|
|
62
|
-
let style;
|
|
63
|
-
if (revealed) className = "stream-char stream-char-revealed";
|
|
64
|
-
else if (resolvedRuntime) {
|
|
65
|
-
const resolved = resolveStyle(startIndex);
|
|
66
|
-
if (resolved === null) className = "stream-char stream-char-revealed";
|
|
67
|
-
else style = resolved;
|
|
68
|
-
}
|
|
69
|
-
const properties = { className };
|
|
70
|
-
if (style !== void 0) properties.style = style;
|
|
71
|
-
return {
|
|
72
|
-
children: [{
|
|
73
|
-
type: "text",
|
|
74
|
-
value
|
|
75
|
-
}],
|
|
76
|
-
properties,
|
|
77
|
-
tagName: "span",
|
|
78
|
-
type: "element"
|
|
79
|
-
};
|
|
80
|
-
};
|
|
81
|
-
const wrapText = (node) => {
|
|
82
|
-
const newChildren = [];
|
|
83
|
-
for (const child of node.children) if (child.type === "text") {
|
|
84
|
-
if (granularity === "word") for (const segment of segmentWords(child.value)) {
|
|
85
|
-
const startIndex = globalCharIndex;
|
|
86
|
-
for (const _char of segment) globalCharIndex++;
|
|
87
|
-
if (segment.trim() === "") newChildren.push({
|
|
88
|
-
type: "text",
|
|
89
|
-
value: segment
|
|
90
|
-
});
|
|
91
|
-
else newChildren.push(buildSpan(segment, startIndex));
|
|
92
|
-
}
|
|
93
|
-
else for (const char of child.value) {
|
|
94
|
-
newChildren.push(buildSpan(char, globalCharIndex));
|
|
95
|
-
globalCharIndex++;
|
|
96
|
-
}
|
|
97
|
-
} else if (child.type === "element") {
|
|
98
|
-
if (!shouldSkip(child)) wrapText(child);
|
|
99
|
-
newChildren.push(child);
|
|
100
|
-
} else newChildren.push(child);
|
|
101
|
-
node.children = newChildren;
|
|
102
|
-
};
|
|
103
|
-
visit(tree, "element", ((node) => {
|
|
104
|
-
if (shouldSkip(node)) return "skip";
|
|
105
|
-
if (BLOCK_TAGS.has(node.tagName)) {
|
|
106
|
-
wrapText(node);
|
|
107
|
-
return "skip";
|
|
108
|
-
}
|
|
109
|
-
}));
|
|
110
|
-
};
|
|
111
|
-
};
|
|
112
|
-
//#endregion
|
|
113
|
-
export { rehypeStreamAnimated };
|
|
114
|
-
|
|
115
|
-
//# sourceMappingURL=rehypeStreamAnimated.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"rehypeStreamAnimated.mjs","names":[],"sources":["../../../src/Markdown/plugins/rehypeStreamAnimated.ts"],"sourcesContent":["import { type Element, type ElementContent, type Root } from 'hast';\nimport { type BuildVisitor } from 'unist-util-visit';\nimport { visit } from 'unist-util-visit';\n\nimport { getNow } from '@/utils/getNow';\n\nexport interface StreamAnimatedRuntime {\n births: number[];\n /**\n * Write-once per-char render cache, indexed like `births`:\n * `undefined` = char not rendered yet, `null` = born fully revealed,\n * string = inline style frozen at first render.\n * Freezing the style keeps span props referentially stable across the\n * tail block's re-renders, so React never rewrites `animation-delay`\n * on an in-flight fade (a rewrite restarts the CSS animation).\n */\n styles: (string | null | undefined)[];\n}\n\nexport interface StreamAnimatedOptions {\n births?: number[];\n fadeDuration?: number;\n /**\n * `'word'` wraps whitespace-delimited runs in one span instead of one\n * span per char. Every concurrent CSS animation keeps the compositor\n * producing frames and fires animationstart/end through React's root\n * event delegation, so animating ~5x fewer nodes is the main CPU lever —\n * char-level remains available for the finer-grained look.\n */\n granularity?: 'char' | 'word';\n nowMs?: number;\n revealed?: boolean;\n runtime?: StreamAnimatedRuntime;\n}\n\n// Intl.Segmenter splits CJK runs into words too — the whitespace regex\n// fallback would otherwise fade an entire unspaced CJK paragraph as one\n// unit.\nconst WORD_SEGMENT_RE = /\\s+|\\S+/g;\n\nconst wordSegmenter =\n typeof Intl !== 'undefined' && 'Segmenter' in Intl\n ? new Intl.Segmenter(undefined, { granularity: 'word' })\n : null;\n\nconst segmentWords = (value: string): string[] => {\n if (!wordSegmenter) return value.match(WORD_SEGMENT_RE) ?? [];\n\n const segments: string[] = [];\n for (const item of wordSegmenter.segment(value)) {\n segments.push(item.segment);\n }\n return segments;\n};\n\nconst BLOCK_TAGS = new Set(['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li']);\nconst SKIP_TAGS = new Set(['pre', 'code', 'table', 'svg']);\n\nfunction hasClass(node: Element, cls: string): boolean {\n const cn: unknown = node.properties?.className;\n if (Array.isArray(cn)) return cn.some((c) => String(c).includes(cls));\n return false;\n}\n\nexport const rehypeStreamAnimated = (options: StreamAnimatedOptions = {}) => {\n const {\n births,\n fadeDuration = 150,\n granularity = 'char',\n nowMs,\n revealed = false,\n runtime,\n } = options;\n // Legacy births/nowMs callers share the runtime path through a throwaway\n // cache: the plugin factory runs once per render, so their styles are\n // recomputed against the caller's nowMs each run, exactly as before.\n const resolvedRuntime = revealed\n ? undefined\n : (runtime ??\n (Array.isArray(births) && typeof nowMs === 'number' ? { births, styles: [] } : undefined));\n const nowOverride = runtime ? undefined : nowMs;\n\n return (tree: Root) => {\n let globalCharIndex = 0;\n const now = nowOverride ?? (resolvedRuntime ? getNow() : 0);\n\n const shouldSkip = (node: Element): boolean => {\n return SKIP_TAGS.has(node.tagName) || hasClass(node, 'katex');\n };\n\n const resolveStyle = (index: number): string | null => {\n const styles = resolvedRuntime!.styles;\n const cached = styles[index];\n if (cached !== undefined) return cached;\n\n const birthTs = resolvedRuntime!.births[index];\n let resolved: string | null;\n if (birthTs === undefined) {\n resolved = null;\n } else {\n const elapsed = now - birthTs;\n // Negative delay = already elapsed ms into the fade. Positive\n // delay = not started yet (char born in the future, i.e.\n // staggered within the same commit).\n resolved = elapsed >= fadeDuration ? null : `animation-delay:${-elapsed}ms`;\n }\n styles[index] = resolved;\n return resolved;\n };\n\n const buildSpan = (value: string, startIndex: number): ElementContent => {\n let className = 'stream-char';\n let style: string | undefined;\n\n if (revealed) {\n className = 'stream-char stream-char-revealed';\n } else if (resolvedRuntime) {\n const resolved = resolveStyle(startIndex);\n if (resolved === null) {\n className = 'stream-char stream-char-revealed';\n } else {\n style = resolved;\n }\n }\n\n const properties: Record<string, any> = { className };\n if (style !== undefined) {\n properties.style = style;\n }\n return {\n children: [{ type: 'text', value }],\n properties,\n tagName: 'span',\n type: 'element',\n };\n };\n\n const wrapText = (node: Element) => {\n const newChildren: ElementContent[] = [];\n for (const child of node.children) {\n if (child.type === 'text') {\n if (granularity === 'word') {\n for (const segment of segmentWords(child.value)) {\n const startIndex = globalCharIndex;\n for (const _char of segment) globalCharIndex++;\n\n if (segment.trim() === '') {\n newChildren.push({ type: 'text', value: segment });\n } else {\n newChildren.push(buildSpan(segment, startIndex));\n }\n }\n } else {\n for (const char of child.value) {\n newChildren.push(buildSpan(char, globalCharIndex));\n globalCharIndex++;\n }\n }\n } else if (child.type === 'element') {\n if (!shouldSkip(child)) {\n wrapText(child);\n }\n newChildren.push(child);\n } else {\n newChildren.push(child);\n }\n }\n node.children = newChildren;\n };\n\n visit(tree, 'element', ((node: Element) => {\n if (shouldSkip(node)) return 'skip';\n if (BLOCK_TAGS.has(node.tagName)) {\n wrapText(node);\n return 'skip';\n }\n }) as BuildVisitor<Root, 'element'>);\n };\n};\n"],"mappings":"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"}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { createContext, memo, use } from "react";
|
|
3
|
-
import { jsx } from "react/jsx-runtime";
|
|
4
|
-
//#region src/Markdown/streamProfiler/StreamdownProfilerProvider.tsx
|
|
5
|
-
const StreamdownProfilerContext = createContext(null);
|
|
6
|
-
const StreamdownProfilerProvider = memo(({ children, profiler = null }) => {
|
|
7
|
-
return /* @__PURE__ */ jsx(StreamdownProfilerContext, {
|
|
8
|
-
value: profiler,
|
|
9
|
-
children
|
|
10
|
-
});
|
|
11
|
-
});
|
|
12
|
-
StreamdownProfilerProvider.displayName = "StreamdownProfilerProvider";
|
|
13
|
-
const useStreamdownProfiler = () => {
|
|
14
|
-
return use(StreamdownProfilerContext);
|
|
15
|
-
};
|
|
16
|
-
//#endregion
|
|
17
|
-
export { StreamdownProfilerProvider, useStreamdownProfiler };
|
|
18
|
-
|
|
19
|
-
//# sourceMappingURL=StreamdownProfilerProvider.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"StreamdownProfilerProvider.mjs","names":[],"sources":["../../../src/Markdown/streamProfiler/StreamdownProfilerProvider.tsx"],"sourcesContent":["'use client';\n\nimport { createContext, memo, type PropsWithChildren, use } from 'react';\n\nimport { type StreamdownProfiler } from './profiler';\n\nconst StreamdownProfilerContext = createContext<StreamdownProfiler | null>(null);\n\nexport interface StreamdownProfilerProviderProps {\n profiler?: StreamdownProfiler | null;\n}\n\nexport const StreamdownProfilerProvider = memo<PropsWithChildren<StreamdownProfilerProviderProps>>(\n ({ children, profiler = null }) => {\n return <StreamdownProfilerContext value={profiler}>{children}</StreamdownProfilerContext>;\n },\n);\n\nStreamdownProfilerProvider.displayName = 'StreamdownProfilerProvider';\n\nexport const useStreamdownProfiler = () => {\n return use(StreamdownProfilerContext);\n};\n"],"mappings":";;;;AAMA,MAAM,4BAA4B,cAAyC,IAAI;AAM/E,MAAa,6BAA6B,MACvC,EAAE,UAAU,WAAW,WAAW;CACjC,OAAO,oBAAC,2BAAD;EAA2B,OAAO;EAAW;CAAoC,CAAA;AAC1F,CACF;AAEA,2BAA2B,cAAc;AAEzC,MAAa,8BAA8B;CACzC,OAAO,IAAI,yBAAyB;AACtC"}
|