@incremark/react 0.0.4 → 0.0.5
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 +188 -1
- package/dist/index.d.ts +115 -3
- package/dist/index.js +240 -8
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@ Incremark 的 React 18+ 集成库。
|
|
|
7
7
|
## 特性
|
|
8
8
|
|
|
9
9
|
- 📦 **开箱即用** - 提供 `useIncremark` hook 和 `<Incremark>` 组件
|
|
10
|
+
- ⌨️ **打字机效果** - 内置 `useBlockTransformer` 实现逐字符显示
|
|
10
11
|
- 🎨 **可定制** - 支持自定义渲染组件
|
|
11
12
|
- ⚡ **高性能** - 利用 React 的 reconciliation 机制
|
|
12
13
|
- 🔧 **DevTools** - 内置开发者工具
|
|
@@ -71,11 +72,96 @@ function App() {
|
|
|
71
72
|
| `blocks` | `Block[]` | 所有块 |
|
|
72
73
|
| `completedBlocks` | `Block[]` | 已完成块 |
|
|
73
74
|
| `pendingBlocks` | `Block[]` | 待处理块 |
|
|
75
|
+
| `isLoading` | `boolean` | 是否正在加载 |
|
|
74
76
|
| `append` | `Function` | 追加内容 |
|
|
75
77
|
| `finalize` | `Function` | 完成解析 |
|
|
76
78
|
| `reset` | `Function` | 重置状态 |
|
|
77
79
|
| `render` | `Function` | 一次性渲染(reset + append + finalize) |
|
|
78
80
|
|
|
81
|
+
### useBlockTransformer(sourceBlocks, options)
|
|
82
|
+
|
|
83
|
+
打字机效果 hook。作为解析器和渲染器之间的中间层,控制内容的逐步显示。
|
|
84
|
+
|
|
85
|
+
**参数:**
|
|
86
|
+
|
|
87
|
+
| 参数 | 类型 | 说明 |
|
|
88
|
+
|------|------|------|
|
|
89
|
+
| `sourceBlocks` | `SourceBlock[]` | 源 blocks(通常来自 `completedBlocks`) |
|
|
90
|
+
| `options.charsPerTick` | `number` | 每次显示的字符数(默认:2) |
|
|
91
|
+
| `options.tickInterval` | `number` | 每次显示的间隔时间 ms(默认:50) |
|
|
92
|
+
| `options.plugins` | `TransformerPlugin[]` | 插件列表(用于特殊块的处理) |
|
|
93
|
+
|
|
94
|
+
**返回值:**
|
|
95
|
+
|
|
96
|
+
| 属性 | 类型 | 说明 |
|
|
97
|
+
|------|------|------|
|
|
98
|
+
| `displayBlocks` | `DisplayBlock[]` | 用于渲染的 blocks |
|
|
99
|
+
| `isProcessing` | `boolean` | 是否正在处理中 |
|
|
100
|
+
| `skip` | `Function` | 跳过动画,立即显示全部内容 |
|
|
101
|
+
| `reset` | `Function` | 重置状态 |
|
|
102
|
+
| `setOptions` | `Function` | 动态更新配置 |
|
|
103
|
+
|
|
104
|
+
**使用示例:**
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
import { useMemo, useState, useEffect } from 'react'
|
|
108
|
+
import { useIncremark, useBlockTransformer, Incremark, defaultPlugins } from '@incremark/react'
|
|
109
|
+
|
|
110
|
+
function App() {
|
|
111
|
+
const { completedBlocks, append, finalize, reset: resetParser } = useIncremark()
|
|
112
|
+
|
|
113
|
+
// 配置打字机速度
|
|
114
|
+
const [typewriterSpeed, setTypewriterSpeed] = useState(2)
|
|
115
|
+
const [typewriterInterval, setTypewriterInterval] = useState(50)
|
|
116
|
+
|
|
117
|
+
// 转换为 SourceBlock 格式
|
|
118
|
+
const sourceBlocks = useMemo(() =>
|
|
119
|
+
completedBlocks.map(block => ({
|
|
120
|
+
id: block.id,
|
|
121
|
+
node: block.node,
|
|
122
|
+
status: block.status
|
|
123
|
+
})),
|
|
124
|
+
[completedBlocks]
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
// 使用 BlockTransformer
|
|
128
|
+
const {
|
|
129
|
+
displayBlocks,
|
|
130
|
+
isProcessing,
|
|
131
|
+
skip,
|
|
132
|
+
reset: resetTransformer,
|
|
133
|
+
setOptions
|
|
134
|
+
} = useBlockTransformer(sourceBlocks, {
|
|
135
|
+
charsPerTick: typewriterSpeed,
|
|
136
|
+
tickInterval: typewriterInterval,
|
|
137
|
+
plugins: defaultPlugins
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
// 监听配置变化
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
setOptions({ charsPerTick: typewriterSpeed, tickInterval: typewriterInterval })
|
|
143
|
+
}, [typewriterSpeed, typewriterInterval, setOptions])
|
|
144
|
+
|
|
145
|
+
// 转换为渲染格式
|
|
146
|
+
const renderBlocks = useMemo(() =>
|
|
147
|
+
displayBlocks.map(db => ({
|
|
148
|
+
...db,
|
|
149
|
+
stableId: db.id,
|
|
150
|
+
node: db.displayNode,
|
|
151
|
+
status: db.isDisplayComplete ? 'completed' : 'pending'
|
|
152
|
+
})),
|
|
153
|
+
[displayBlocks]
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<div>
|
|
158
|
+
<Incremark blocks={renderBlocks} />
|
|
159
|
+
{isProcessing && <button onClick={skip}>跳过</button>}
|
|
160
|
+
</div>
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
79
165
|
### useDevTools(incremark)
|
|
80
166
|
|
|
81
167
|
启用 DevTools。
|
|
@@ -93,9 +179,18 @@ useDevTools(incremark)
|
|
|
93
179
|
<Incremark
|
|
94
180
|
blocks={blocks}
|
|
95
181
|
components={{ heading: MyHeading }}
|
|
182
|
+
showBlockStatus={true}
|
|
96
183
|
/>
|
|
97
184
|
```
|
|
98
185
|
|
|
186
|
+
**Props:**
|
|
187
|
+
|
|
188
|
+
| Prop | 类型 | 说明 |
|
|
189
|
+
|------|------|------|
|
|
190
|
+
| `blocks` | `Block[]` | 要渲染的 blocks |
|
|
191
|
+
| `components` | `object` | 自定义组件映射 |
|
|
192
|
+
| `showBlockStatus` | `boolean` | 是否显示块状态(pending/completed) |
|
|
193
|
+
|
|
99
194
|
## 自定义组件
|
|
100
195
|
|
|
101
196
|
```tsx
|
|
@@ -114,6 +209,44 @@ function App() {
|
|
|
114
209
|
}
|
|
115
210
|
```
|
|
116
211
|
|
|
212
|
+
## 插件系统
|
|
213
|
+
|
|
214
|
+
BlockTransformer 支持插件来处理特殊类型的块:
|
|
215
|
+
|
|
216
|
+
```tsx
|
|
217
|
+
import {
|
|
218
|
+
defaultPlugins,
|
|
219
|
+
codeBlockPlugin,
|
|
220
|
+
imagePlugin,
|
|
221
|
+
mermaidPlugin,
|
|
222
|
+
mathPlugin,
|
|
223
|
+
thematicBreakPlugin,
|
|
224
|
+
createPlugin
|
|
225
|
+
} from '@incremark/react'
|
|
226
|
+
|
|
227
|
+
// 使用默认插件集
|
|
228
|
+
const { displayBlocks } = useBlockTransformer(sourceBlocks, {
|
|
229
|
+
plugins: defaultPlugins
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
// 或自定义插件
|
|
233
|
+
const myPlugin = createPlugin({
|
|
234
|
+
name: 'my-plugin',
|
|
235
|
+
match: (node) => node.type === 'myType',
|
|
236
|
+
transform: (node) => ({ displayNode: node, isComplete: true })
|
|
237
|
+
})
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
**内置插件:**
|
|
241
|
+
|
|
242
|
+
| 插件 | 说明 |
|
|
243
|
+
|------|------|
|
|
244
|
+
| `codeBlockPlugin` | 代码块整体显示 |
|
|
245
|
+
| `imagePlugin` | 图片整体显示 |
|
|
246
|
+
| `mermaidPlugin` | Mermaid 图表整体显示 |
|
|
247
|
+
| `mathPlugin` | 数学公式整体显示 |
|
|
248
|
+
| `thematicBreakPlugin` | 分隔线整体显示 |
|
|
249
|
+
|
|
117
250
|
## 与 React Query 集成
|
|
118
251
|
|
|
119
252
|
```tsx
|
|
@@ -143,7 +276,61 @@ function StreamingContent() {
|
|
|
143
276
|
}
|
|
144
277
|
```
|
|
145
278
|
|
|
279
|
+
## AutoScrollContainer
|
|
280
|
+
|
|
281
|
+
自动滚动容器组件,适用于流式内容场景:
|
|
282
|
+
|
|
283
|
+
```tsx
|
|
284
|
+
import { useRef, useState } from 'react'
|
|
285
|
+
import { AutoScrollContainer, Incremark, type AutoScrollContainerRef } from '@incremark/react'
|
|
286
|
+
|
|
287
|
+
function App() {
|
|
288
|
+
const scrollRef = useRef<AutoScrollContainerRef>(null)
|
|
289
|
+
const [autoScrollEnabled, setAutoScrollEnabled] = useState(true)
|
|
290
|
+
|
|
291
|
+
return (
|
|
292
|
+
<div>
|
|
293
|
+
<AutoScrollContainer
|
|
294
|
+
ref={scrollRef}
|
|
295
|
+
enabled={autoScrollEnabled}
|
|
296
|
+
threshold={50}
|
|
297
|
+
behavior="smooth"
|
|
298
|
+
>
|
|
299
|
+
<Incremark blocks={blocks} />
|
|
300
|
+
</AutoScrollContainer>
|
|
301
|
+
|
|
302
|
+
{/* 显示滚动状态 */}
|
|
303
|
+
{scrollRef.current?.isUserScrolledUp() && (
|
|
304
|
+
<span>用户已暂停自动滚动</span>
|
|
305
|
+
)}
|
|
306
|
+
|
|
307
|
+
{/* 强制滚动到底部 */}
|
|
308
|
+
<button onClick={() => scrollRef.current?.scrollToBottom()}>
|
|
309
|
+
滚动到底部
|
|
310
|
+
</button>
|
|
311
|
+
</div>
|
|
312
|
+
)
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
**Props:**
|
|
317
|
+
|
|
318
|
+
| Prop | 类型 | 默认值 | 说明 |
|
|
319
|
+
|------|------|--------|------|
|
|
320
|
+
| `enabled` | `boolean` | `true` | 是否启用自动滚动 |
|
|
321
|
+
| `threshold` | `number` | `50` | 触发自动滚动的底部阈值(像素) |
|
|
322
|
+
| `behavior` | `ScrollBehavior` | `'instant'` | 滚动行为 |
|
|
323
|
+
| `className` | `string` | - | 容器类名 |
|
|
324
|
+
| `style` | `CSSProperties` | - | 容器样式 |
|
|
325
|
+
|
|
326
|
+
**Ref 方法(通过 useRef):**
|
|
327
|
+
|
|
328
|
+
| 方法 | 说明 |
|
|
329
|
+
|------|------|
|
|
330
|
+
| `scrollToBottom()` | 强制滚动到底部 |
|
|
331
|
+
| `isUserScrolledUp()` | 返回用户是否手动向上滚动了 |
|
|
332
|
+
| `container` | 容器 DOM 元素引用 |
|
|
333
|
+
|
|
146
334
|
## License
|
|
147
335
|
|
|
148
336
|
MIT
|
|
149
|
-
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ParserOptions, ParsedBlock, Root, IncrementalUpdate, IncremarkParser, RootContent } from '@incremark/core';
|
|
2
|
-
export { IncrementalUpdate, ParsedBlock, ParserOptions, Root, RootContent } from '@incremark/core';
|
|
1
|
+
import { ParserOptions, ParsedBlock, Root, IncrementalUpdate, IncremarkParser, SourceBlock, TransformerOptions, DisplayBlock, AnimationEffect, BlockTransformer, RootContent } from '@incremark/core';
|
|
2
|
+
export { AnimationEffect, BlockTransformer, DisplayBlock, IncrementalUpdate, ParsedBlock, ParserOptions, Root, RootContent, SourceBlock, TransformerOptions, TransformerPlugin, TransformerState, allPlugins, cloneNode, codeBlockPlugin, countChars, createBlockTransformer, createPlugin, defaultPlugins, imagePlugin, mathPlugin, mermaidPlugin, sliceAst, thematicBreakPlugin } from '@incremark/core';
|
|
3
3
|
import * as _incremark_devtools from '@incremark/devtools';
|
|
4
4
|
import { DevToolsOptions } from '@incremark/devtools';
|
|
5
5
|
import React from 'react';
|
|
@@ -77,8 +77,78 @@ interface UseDevToolsOptions extends DevToolsOptions {
|
|
|
77
77
|
*/
|
|
78
78
|
declare function useDevTools(incremark: UseIncremarkReturn, options?: UseDevToolsOptions): _incremark_devtools.IncremarkDevTools | null;
|
|
79
79
|
|
|
80
|
+
interface UseBlockTransformerOptions extends Omit<TransformerOptions, 'onChange'> {
|
|
81
|
+
}
|
|
82
|
+
interface UseBlockTransformerReturn<T = unknown> {
|
|
83
|
+
/** 用于渲染的 display blocks */
|
|
84
|
+
displayBlocks: DisplayBlock<T>[];
|
|
85
|
+
/** 是否正在处理中 */
|
|
86
|
+
isProcessing: boolean;
|
|
87
|
+
/** 是否已暂停 */
|
|
88
|
+
isPaused: boolean;
|
|
89
|
+
/** 当前动画效果 */
|
|
90
|
+
effect: AnimationEffect;
|
|
91
|
+
/** 跳过所有动画 */
|
|
92
|
+
skip: () => void;
|
|
93
|
+
/** 重置状态 */
|
|
94
|
+
reset: () => void;
|
|
95
|
+
/** 暂停动画 */
|
|
96
|
+
pause: () => void;
|
|
97
|
+
/** 恢复动画 */
|
|
98
|
+
resume: () => void;
|
|
99
|
+
/** 动态更新配置 */
|
|
100
|
+
setOptions: (options: Partial<Pick<TransformerOptions, 'charsPerTick' | 'tickInterval' | 'effect' | 'pauseOnHidden'>>) => void;
|
|
101
|
+
/** transformer 实例(用于高级用法) */
|
|
102
|
+
transformer: BlockTransformer<T>;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* React Hook: Block Transformer
|
|
106
|
+
*
|
|
107
|
+
* 用于控制 blocks 的逐步显示(打字机效果)
|
|
108
|
+
* 作为解析器和渲染器之间的中间层
|
|
109
|
+
*
|
|
110
|
+
* 特性:
|
|
111
|
+
* - 使用 requestAnimationFrame 实现流畅动画
|
|
112
|
+
* - 支持随机步长 `charsPerTick: [1, 3]`
|
|
113
|
+
* - 支持动画效果 `effect: 'typing'`
|
|
114
|
+
* - 页面不可见时自动暂停
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* ```tsx
|
|
118
|
+
* import { useIncremark, useBlockTransformer, defaultPlugins } from '@incremark/react'
|
|
119
|
+
*
|
|
120
|
+
* function App() {
|
|
121
|
+
* const { completedBlocks, append, finalize } = useIncremark()
|
|
122
|
+
*
|
|
123
|
+
* // 转换为 SourceBlock 格式
|
|
124
|
+
* const sourceBlocks = useMemo(() => completedBlocks.map(block => ({
|
|
125
|
+
* id: block.id,
|
|
126
|
+
* node: block.node,
|
|
127
|
+
* status: block.status
|
|
128
|
+
* })), [completedBlocks])
|
|
129
|
+
*
|
|
130
|
+
* // 添加打字机效果
|
|
131
|
+
* const { displayBlocks, isProcessing, skip, effect } = useBlockTransformer(sourceBlocks, {
|
|
132
|
+
* charsPerTick: [1, 3], // 随机步长
|
|
133
|
+
* tickInterval: 30,
|
|
134
|
+
* effect: 'typing', // 光标效果
|
|
135
|
+
* plugins: defaultPlugins
|
|
136
|
+
* })
|
|
137
|
+
*
|
|
138
|
+
* return (
|
|
139
|
+
* <div className={effect === 'typing' ? 'typing' : ''}>
|
|
140
|
+
* <Incremark blocks={displayBlocks} />
|
|
141
|
+
* {isProcessing && <button onClick={skip}>跳过</button>}
|
|
142
|
+
* </div>
|
|
143
|
+
* )
|
|
144
|
+
* }
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
declare function useBlockTransformer<T = unknown>(sourceBlocks: SourceBlock<T>[], options?: UseBlockTransformerOptions): UseBlockTransformerReturn<T>;
|
|
148
|
+
|
|
80
149
|
interface BlockWithStableId extends ParsedBlock {
|
|
81
150
|
stableId: string;
|
|
151
|
+
isLastPending?: boolean;
|
|
82
152
|
}
|
|
83
153
|
interface IncremarkProps {
|
|
84
154
|
/** 要渲染的块列表 */
|
|
@@ -118,4 +188,46 @@ interface IncremarkRendererProps {
|
|
|
118
188
|
*/
|
|
119
189
|
declare const IncremarkRenderer: React.FC<IncremarkRendererProps>;
|
|
120
190
|
|
|
121
|
-
|
|
191
|
+
interface AutoScrollContainerProps {
|
|
192
|
+
/** 子元素 */
|
|
193
|
+
children: React.ReactNode;
|
|
194
|
+
/** 是否启用自动滚动 */
|
|
195
|
+
enabled?: boolean;
|
|
196
|
+
/** 触发自动滚动的底部阈值(像素) */
|
|
197
|
+
threshold?: number;
|
|
198
|
+
/** 滚动行为 */
|
|
199
|
+
behavior?: ScrollBehavior;
|
|
200
|
+
/** 容器样式 */
|
|
201
|
+
style?: React.CSSProperties;
|
|
202
|
+
/** 容器类名 */
|
|
203
|
+
className?: string;
|
|
204
|
+
}
|
|
205
|
+
interface AutoScrollContainerRef {
|
|
206
|
+
/** 强制滚动到底部 */
|
|
207
|
+
scrollToBottom: () => void;
|
|
208
|
+
/** 是否用户手动向上滚动了 */
|
|
209
|
+
isUserScrolledUp: () => boolean;
|
|
210
|
+
/** 容器元素引用 */
|
|
211
|
+
container: HTMLDivElement | null;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* 自动滚动容器
|
|
215
|
+
*
|
|
216
|
+
* 当内容更新时自动滚动到底部。
|
|
217
|
+
* 如果用户手动向上滚动,则暂停自动滚动,直到用户再次滚动到底部。
|
|
218
|
+
*
|
|
219
|
+
* @example
|
|
220
|
+
* ```tsx
|
|
221
|
+
* const scrollRef = useRef<AutoScrollContainerRef>(null)
|
|
222
|
+
*
|
|
223
|
+
* <AutoScrollContainer ref={scrollRef} enabled={true}>
|
|
224
|
+
* <Incremark blocks={blocks} />
|
|
225
|
+
* </AutoScrollContainer>
|
|
226
|
+
*
|
|
227
|
+
* // 强制滚动到底部
|
|
228
|
+
* scrollRef.current?.scrollToBottom()
|
|
229
|
+
* ```
|
|
230
|
+
*/
|
|
231
|
+
declare const AutoScrollContainer: React.ForwardRefExoticComponent<AutoScrollContainerProps & React.RefAttributes<AutoScrollContainerRef>>;
|
|
232
|
+
|
|
233
|
+
export { AutoScrollContainer, type AutoScrollContainerProps, type AutoScrollContainerRef, Incremark, type IncremarkProps, IncremarkRenderer, type IncremarkRendererProps, type UseBlockTransformerOptions, type UseBlockTransformerReturn, type UseDevToolsOptions, type UseIncremarkOptions, type UseIncremarkReturn, useBlockTransformer, useDevTools, useIncremark };
|
package/dist/index.js
CHANGED
|
@@ -137,6 +137,80 @@ function useDevTools(incremark, options = {}) {
|
|
|
137
137
|
return devtoolsRef.current;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
// src/hooks/useBlockTransformer.ts
|
|
141
|
+
import { useState as useState2, useCallback as useCallback2, useRef as useRef3, useEffect as useEffect2 } from "react";
|
|
142
|
+
import {
|
|
143
|
+
createBlockTransformer
|
|
144
|
+
} from "@incremark/core";
|
|
145
|
+
function useBlockTransformer(sourceBlocks, options = {}) {
|
|
146
|
+
const [displayBlocks, setDisplayBlocks] = useState2([]);
|
|
147
|
+
const [isProcessing, setIsProcessing] = useState2(false);
|
|
148
|
+
const [isPaused, setIsPaused] = useState2(false);
|
|
149
|
+
const [effect, setEffect] = useState2(options.effect ?? "none");
|
|
150
|
+
const transformerRef = useRef3(null);
|
|
151
|
+
if (!transformerRef.current) {
|
|
152
|
+
transformerRef.current = createBlockTransformer({
|
|
153
|
+
...options,
|
|
154
|
+
onChange: (blocks) => {
|
|
155
|
+
setDisplayBlocks(blocks);
|
|
156
|
+
setIsProcessing(transformerRef.current?.isProcessing() ?? false);
|
|
157
|
+
setIsPaused(transformerRef.current?.isPausedState() ?? false);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
const transformer = transformerRef.current;
|
|
162
|
+
useEffect2(() => {
|
|
163
|
+
transformer.push(sourceBlocks);
|
|
164
|
+
const currentDisplaying = displayBlocks.find((b) => !b.isDisplayComplete);
|
|
165
|
+
if (currentDisplaying) {
|
|
166
|
+
const updated = sourceBlocks.find((b) => b.id === currentDisplaying.id);
|
|
167
|
+
if (updated) {
|
|
168
|
+
transformer.update(updated);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}, [sourceBlocks, transformer]);
|
|
172
|
+
useEffect2(() => {
|
|
173
|
+
return () => {
|
|
174
|
+
transformer.destroy();
|
|
175
|
+
};
|
|
176
|
+
}, [transformer]);
|
|
177
|
+
const skip = useCallback2(() => {
|
|
178
|
+
transformer.skip();
|
|
179
|
+
}, [transformer]);
|
|
180
|
+
const reset = useCallback2(() => {
|
|
181
|
+
transformer.reset();
|
|
182
|
+
}, [transformer]);
|
|
183
|
+
const pause = useCallback2(() => {
|
|
184
|
+
transformer.pause();
|
|
185
|
+
setIsPaused(true);
|
|
186
|
+
}, [transformer]);
|
|
187
|
+
const resume = useCallback2(() => {
|
|
188
|
+
transformer.resume();
|
|
189
|
+
setIsPaused(false);
|
|
190
|
+
}, [transformer]);
|
|
191
|
+
const setOptionsCallback = useCallback2(
|
|
192
|
+
(opts) => {
|
|
193
|
+
transformer.setOptions(opts);
|
|
194
|
+
if (opts.effect !== void 0) {
|
|
195
|
+
setEffect(opts.effect);
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
[transformer]
|
|
199
|
+
);
|
|
200
|
+
return {
|
|
201
|
+
displayBlocks,
|
|
202
|
+
isProcessing,
|
|
203
|
+
isPaused,
|
|
204
|
+
effect,
|
|
205
|
+
skip,
|
|
206
|
+
reset,
|
|
207
|
+
pause,
|
|
208
|
+
resume,
|
|
209
|
+
setOptions: setOptionsCallback,
|
|
210
|
+
transformer
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
140
214
|
// src/components/IncremarkRenderer.tsx
|
|
141
215
|
import React from "react";
|
|
142
216
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -210,18 +284,176 @@ var Incremark = ({
|
|
|
210
284
|
showBlockStatus = true,
|
|
211
285
|
className = ""
|
|
212
286
|
}) => {
|
|
213
|
-
return /* @__PURE__ */ jsx2("div", { className: `incremark ${className}`, children: blocks.map((block) =>
|
|
214
|
-
"
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
287
|
+
return /* @__PURE__ */ jsx2("div", { className: `incremark ${className}`, children: blocks.map((block) => {
|
|
288
|
+
const isPending = block.status === "pending";
|
|
289
|
+
const classes = [
|
|
290
|
+
"incremark-block",
|
|
291
|
+
isPending ? "incremark-pending" : "incremark-completed",
|
|
292
|
+
block.isLastPending ? "incremark-last-pending" : ""
|
|
293
|
+
].filter(Boolean).join(" ");
|
|
294
|
+
return /* @__PURE__ */ jsx2("div", { className: classes, children: /* @__PURE__ */ jsx2(IncremarkRenderer, { node: block.node, components }) }, block.stableId);
|
|
295
|
+
}) });
|
|
221
296
|
};
|
|
297
|
+
|
|
298
|
+
// src/components/AutoScrollContainer.tsx
|
|
299
|
+
import {
|
|
300
|
+
useRef as useRef4,
|
|
301
|
+
useEffect as useEffect3,
|
|
302
|
+
useCallback as useCallback3,
|
|
303
|
+
useState as useState3,
|
|
304
|
+
forwardRef,
|
|
305
|
+
useImperativeHandle
|
|
306
|
+
} from "react";
|
|
307
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
308
|
+
var AutoScrollContainer = forwardRef(
|
|
309
|
+
({
|
|
310
|
+
children,
|
|
311
|
+
enabled = true,
|
|
312
|
+
threshold = 50,
|
|
313
|
+
behavior = "instant",
|
|
314
|
+
style,
|
|
315
|
+
className
|
|
316
|
+
}, ref) => {
|
|
317
|
+
const containerRef = useRef4(null);
|
|
318
|
+
const [isUserScrolledUp, setIsUserScrolledUp] = useState3(false);
|
|
319
|
+
const lastScrollTopRef = useRef4(0);
|
|
320
|
+
const lastScrollHeightRef = useRef4(0);
|
|
321
|
+
const isNearBottom = useCallback3(() => {
|
|
322
|
+
const container = containerRef.current;
|
|
323
|
+
if (!container) return true;
|
|
324
|
+
const { scrollTop, scrollHeight, clientHeight } = container;
|
|
325
|
+
return scrollHeight - scrollTop - clientHeight <= threshold;
|
|
326
|
+
}, [threshold]);
|
|
327
|
+
const scrollToBottom = useCallback3(
|
|
328
|
+
(force = false) => {
|
|
329
|
+
const container = containerRef.current;
|
|
330
|
+
if (!container) return;
|
|
331
|
+
if (isUserScrolledUp && !force) return;
|
|
332
|
+
container.scrollTo({
|
|
333
|
+
top: container.scrollHeight,
|
|
334
|
+
behavior
|
|
335
|
+
});
|
|
336
|
+
},
|
|
337
|
+
[isUserScrolledUp, behavior]
|
|
338
|
+
);
|
|
339
|
+
const hasScrollbar = useCallback3(() => {
|
|
340
|
+
const container = containerRef.current;
|
|
341
|
+
if (!container) return false;
|
|
342
|
+
return container.scrollHeight > container.clientHeight;
|
|
343
|
+
}, []);
|
|
344
|
+
const handleScroll = useCallback3(() => {
|
|
345
|
+
const container = containerRef.current;
|
|
346
|
+
if (!container) return;
|
|
347
|
+
const { scrollTop, scrollHeight, clientHeight } = container;
|
|
348
|
+
if (scrollHeight <= clientHeight) {
|
|
349
|
+
setIsUserScrolledUp(false);
|
|
350
|
+
lastScrollTopRef.current = 0;
|
|
351
|
+
lastScrollHeightRef.current = scrollHeight;
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (isNearBottom()) {
|
|
355
|
+
setIsUserScrolledUp(false);
|
|
356
|
+
} else {
|
|
357
|
+
const isScrollingUp = scrollTop < lastScrollTopRef.current;
|
|
358
|
+
const isContentUnchanged = scrollHeight === lastScrollHeightRef.current;
|
|
359
|
+
if (isScrollingUp && isContentUnchanged) {
|
|
360
|
+
setIsUserScrolledUp(true);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
lastScrollTopRef.current = scrollTop;
|
|
364
|
+
lastScrollHeightRef.current = scrollHeight;
|
|
365
|
+
}, [isNearBottom]);
|
|
366
|
+
useEffect3(() => {
|
|
367
|
+
const container = containerRef.current;
|
|
368
|
+
if (container) {
|
|
369
|
+
lastScrollTopRef.current = container.scrollTop;
|
|
370
|
+
lastScrollHeightRef.current = container.scrollHeight;
|
|
371
|
+
}
|
|
372
|
+
}, []);
|
|
373
|
+
useEffect3(() => {
|
|
374
|
+
const container = containerRef.current;
|
|
375
|
+
if (!container || !enabled) return;
|
|
376
|
+
const observer = new MutationObserver(() => {
|
|
377
|
+
requestAnimationFrame(() => {
|
|
378
|
+
if (!containerRef.current) return;
|
|
379
|
+
const { scrollHeight, clientHeight } = containerRef.current;
|
|
380
|
+
if (scrollHeight <= clientHeight) {
|
|
381
|
+
setIsUserScrolledUp(false);
|
|
382
|
+
}
|
|
383
|
+
lastScrollHeightRef.current = scrollHeight;
|
|
384
|
+
if (!isUserScrolledUp) {
|
|
385
|
+
scrollToBottom();
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
observer.observe(container, {
|
|
390
|
+
childList: true,
|
|
391
|
+
subtree: true,
|
|
392
|
+
characterData: true
|
|
393
|
+
});
|
|
394
|
+
return () => observer.disconnect();
|
|
395
|
+
}, [enabled, isUserScrolledUp, scrollToBottom]);
|
|
396
|
+
useImperativeHandle(
|
|
397
|
+
ref,
|
|
398
|
+
() => ({
|
|
399
|
+
scrollToBottom: () => scrollToBottom(true),
|
|
400
|
+
isUserScrolledUp: () => isUserScrolledUp,
|
|
401
|
+
container: containerRef.current
|
|
402
|
+
}),
|
|
403
|
+
[scrollToBottom, isUserScrolledUp]
|
|
404
|
+
);
|
|
405
|
+
return /* @__PURE__ */ jsx3(
|
|
406
|
+
"div",
|
|
407
|
+
{
|
|
408
|
+
ref: containerRef,
|
|
409
|
+
className,
|
|
410
|
+
style: {
|
|
411
|
+
overflowY: "auto",
|
|
412
|
+
height: "100%",
|
|
413
|
+
...style
|
|
414
|
+
},
|
|
415
|
+
onScroll: handleScroll,
|
|
416
|
+
children
|
|
417
|
+
}
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
);
|
|
421
|
+
AutoScrollContainer.displayName = "AutoScrollContainer";
|
|
422
|
+
|
|
423
|
+
// src/index.ts
|
|
424
|
+
import {
|
|
425
|
+
BlockTransformer as BlockTransformer2,
|
|
426
|
+
createBlockTransformer as createBlockTransformer2,
|
|
427
|
+
countChars,
|
|
428
|
+
sliceAst,
|
|
429
|
+
cloneNode,
|
|
430
|
+
codeBlockPlugin,
|
|
431
|
+
mermaidPlugin,
|
|
432
|
+
imagePlugin,
|
|
433
|
+
mathPlugin,
|
|
434
|
+
thematicBreakPlugin,
|
|
435
|
+
defaultPlugins,
|
|
436
|
+
allPlugins,
|
|
437
|
+
createPlugin
|
|
438
|
+
} from "@incremark/core";
|
|
222
439
|
export {
|
|
440
|
+
AutoScrollContainer,
|
|
441
|
+
BlockTransformer2 as BlockTransformer,
|
|
223
442
|
Incremark,
|
|
224
443
|
IncremarkRenderer,
|
|
444
|
+
allPlugins,
|
|
445
|
+
cloneNode,
|
|
446
|
+
codeBlockPlugin,
|
|
447
|
+
countChars,
|
|
448
|
+
createBlockTransformer2 as createBlockTransformer,
|
|
449
|
+
createPlugin,
|
|
450
|
+
defaultPlugins,
|
|
451
|
+
imagePlugin,
|
|
452
|
+
mathPlugin,
|
|
453
|
+
mermaidPlugin,
|
|
454
|
+
sliceAst,
|
|
455
|
+
thematicBreakPlugin,
|
|
456
|
+
useBlockTransformer,
|
|
225
457
|
useDevTools,
|
|
226
458
|
useIncremark
|
|
227
459
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@incremark/react",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Incremark React integration - Incremental Markdown parser for AI streaming",
|
|
6
6
|
"type": "module",
|
|
@@ -19,10 +19,10 @@
|
|
|
19
19
|
],
|
|
20
20
|
"peerDependencies": {
|
|
21
21
|
"react": ">=18.0.0",
|
|
22
|
-
"@incremark/core": "0.0.
|
|
22
|
+
"@incremark/core": "0.0.5"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@incremark/devtools": "0.0.
|
|
25
|
+
"@incremark/devtools": "0.0.5"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/react": "^18.2.0",
|