@incremark/vue 0.0.4 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,14 +1,62 @@
1
- import { ref, shallowRef, computed, markRaw } from 'vue'
1
+ import { ref, shallowRef, computed, markRaw, watch, onUnmounted, type Ref, type ComputedRef } from 'vue'
2
2
  import {
3
3
  IncremarkParser,
4
4
  createIncremarkParser,
5
+ createBlockTransformer,
6
+ defaultPlugins,
5
7
  type ParserOptions,
6
8
  type ParsedBlock,
7
9
  type IncrementalUpdate,
8
- type Root
10
+ type Root,
11
+ type RootContent,
12
+ type DisplayBlock,
13
+ type TransformerPlugin,
14
+ type AnimationEffect,
15
+ type BlockTransformer
9
16
  } from '@incremark/core'
10
17
 
11
- export interface UseIncremarkOptions extends ParserOptions {}
18
+ /** 打字机效果配置 */
19
+ export interface TypewriterOptions {
20
+ /** 是否启用打字机效果(可响应式切换) */
21
+ enabled?: boolean
22
+ /** 每次显示的字符数,可以是固定值或范围 [min, max] */
23
+ charsPerTick?: number | [number, number]
24
+ /** 更新间隔 (ms) */
25
+ tickInterval?: number
26
+ /** 动画效果: 'none' | 'fade-in' | 'typing' */
27
+ effect?: AnimationEffect
28
+ /** 光标字符(仅 typing 效果使用) */
29
+ cursor?: string
30
+ /** 页面不可见时暂停 */
31
+ pauseOnHidden?: boolean
32
+ /** 自定义插件 */
33
+ plugins?: TransformerPlugin[]
34
+ }
35
+
36
+ export interface UseIncremarkOptions extends ParserOptions {
37
+ /** 打字机配置,传入即创建 transformer(可通过 enabled 控制是否启用) */
38
+ typewriter?: TypewriterOptions
39
+ }
40
+
41
+ /** 打字机控制对象 */
42
+ export interface TypewriterControls {
43
+ /** 是否启用 */
44
+ enabled: Ref<boolean>
45
+ /** 是否正在处理中 */
46
+ isProcessing: ComputedRef<boolean>
47
+ /** 是否已暂停 */
48
+ isPaused: ComputedRef<boolean>
49
+ /** 当前动画效果 */
50
+ effect: ComputedRef<AnimationEffect>
51
+ /** 跳过动画,直接显示全部 */
52
+ skip: () => void
53
+ /** 暂停动画 */
54
+ pause: () => void
55
+ /** 恢复动画 */
56
+ resume: () => void
57
+ /** 动态更新配置 */
58
+ setOptions: (options: Partial<TypewriterOptions>) => void
59
+ }
12
60
 
13
61
  /** useIncremark 的返回类型 */
14
62
  export type UseIncremarkReturn = ReturnType<typeof useIncremark>
@@ -19,32 +67,68 @@ export type UseIncremarkReturn = ReturnType<typeof useIncremark>
19
67
  * @example
20
68
  * ```vue
21
69
  * <script setup>
22
- * import { useIncremark } from '@incremark/vue'
70
+ * import { useIncremark, Incremark } from '@incremark/vue'
23
71
  *
24
- * const { markdown, blocks, append, finalize } = useIncremark()
72
+ * // 基础用法
73
+ * const { blocks, append, finalize } = useIncremark()
25
74
  *
26
- * // 处理 AI 流式输出
27
- * async function handleStream(stream) {
28
- * for await (const chunk of stream) {
29
- * append(chunk)
75
+ * // 启用打字机效果
76
+ * const { blocks, append, finalize, typewriter } = useIncremark({
77
+ * typewriter: {
78
+ * enabled: true, // 可响应式切换
79
+ * charsPerTick: [1, 3],
80
+ * tickInterval: 30,
81
+ * effect: 'typing',
82
+ * cursor: '|'
30
83
  * }
31
- * finalize()
32
- * }
84
+ * })
85
+ *
86
+ * // 动态切换打字机效果
87
+ * typewriter.enabled.value = false
33
88
  * </script>
34
89
  *
35
90
  * <template>
36
- * <div>已接收: {{ markdown.length }} 字符</div>
91
+ * <Incremark :blocks="blocks" />
92
+ * <button v-if="typewriter.isProcessing.value" @click="typewriter.skip">跳过</button>
37
93
  * </template>
38
94
  * ```
39
95
  */
40
96
  export function useIncremark(options: UseIncremarkOptions = {}) {
97
+ // 解析器
41
98
  const parser = createIncremarkParser(options)
42
99
  const completedBlocks = shallowRef<ParsedBlock[]>([])
43
100
  const pendingBlocks = shallowRef<ParsedBlock[]>([])
44
101
  const isLoading = ref(false)
45
- // 使用 ref 存储 markdown,确保响应式
46
102
  const markdown = ref('')
47
103
 
104
+ // 打字机相关状态
105
+ const typewriterEnabled = ref(options.typewriter?.enabled ?? !!options.typewriter)
106
+ const displayBlocksRef = shallowRef<DisplayBlock<RootContent>[]>([])
107
+ const isTypewriterProcessing = ref(false)
108
+ const isTypewriterPaused = ref(false)
109
+ const typewriterEffect = ref<AnimationEffect>(options.typewriter?.effect ?? 'none')
110
+ const typewriterCursor = ref(options.typewriter?.cursor ?? '|')
111
+
112
+ // 创建 transformer(如果有 typewriter 配置)
113
+ let transformer: BlockTransformer<RootContent> | null = null
114
+
115
+ if (options.typewriter) {
116
+ const twOptions = options.typewriter
117
+ transformer = createBlockTransformer<RootContent>({
118
+ charsPerTick: twOptions.charsPerTick ?? [1, 3],
119
+ tickInterval: twOptions.tickInterval ?? 30,
120
+ effect: twOptions.effect ?? 'none',
121
+ pauseOnHidden: twOptions.pauseOnHidden ?? true,
122
+ plugins: twOptions.plugins ?? defaultPlugins,
123
+ onChange: (blocks) => {
124
+ displayBlocksRef.value = blocks
125
+ isTypewriterProcessing.value = transformer?.isProcessing() ?? false
126
+ isTypewriterPaused.value = transformer?.isPausedState() ?? false
127
+ }
128
+ })
129
+ }
130
+
131
+ // AST
48
132
  const ast = computed<Root>(() => ({
49
133
  type: 'root',
50
134
  children: [
@@ -53,8 +137,68 @@ export function useIncremark(options: UseIncremarkOptions = {}) {
53
137
  ]
54
138
  }))
55
139
 
56
- // 所有块,带稳定 ID(已完成块用真实 ID,待处理块用索引)
57
- const blocks = computed(() => {
140
+ // completedBlocks 转换为 SourceBlock 格式
141
+ const sourceBlocks = computed(() => {
142
+ return completedBlocks.value.map(block => ({
143
+ id: block.id,
144
+ node: block.node,
145
+ status: block.status as 'pending' | 'stable' | 'completed'
146
+ }))
147
+ })
148
+
149
+ // 监听 sourceBlocks 变化,推送给 transformer
150
+ if (transformer) {
151
+ watch(
152
+ sourceBlocks,
153
+ (blocks) => {
154
+ transformer!.push(blocks)
155
+
156
+ // 更新正在显示的 block
157
+ const currentDisplaying = displayBlocksRef.value.find((b) => !b.isDisplayComplete)
158
+ if (currentDisplaying) {
159
+ const updated = blocks.find((b) => b.id === currentDisplaying.id)
160
+ if (updated) {
161
+ transformer!.update(updated)
162
+ }
163
+ }
164
+ },
165
+ { immediate: true, deep: true }
166
+ )
167
+ }
168
+
169
+ /**
170
+ * 在节点末尾添加光标
171
+ */
172
+ function addCursorToNode(node: RootContent, cursor: string): RootContent {
173
+ const cloned = JSON.parse(JSON.stringify(node))
174
+
175
+ function addToLast(n: { children?: unknown[]; type?: string; value?: string }): boolean {
176
+ if (n.children && n.children.length > 0) {
177
+ for (let i = n.children.length - 1; i >= 0; i--) {
178
+ if (addToLast(n.children[i] as { children?: unknown[]; type?: string; value?: string })) {
179
+ return true
180
+ }
181
+ }
182
+ n.children.push({ type: 'text', value: cursor })
183
+ return true
184
+ }
185
+ if (n.type === 'text' && typeof n.value === 'string') {
186
+ n.value += cursor
187
+ return true
188
+ }
189
+ if (typeof n.value === 'string') {
190
+ n.value += cursor
191
+ return true
192
+ }
193
+ return false
194
+ }
195
+
196
+ addToLast(cloned)
197
+ return cloned
198
+ }
199
+
200
+ // 原始 blocks(不经过打字机)
201
+ const rawBlocks = computed(() => {
58
202
  const result: Array<ParsedBlock & { stableId: string }> = []
59
203
 
60
204
  for (const block of completedBlocks.value) {
@@ -71,14 +215,43 @@ export function useIncremark(options: UseIncremarkOptions = {}) {
71
215
  return result
72
216
  })
73
217
 
218
+ // 最终用于渲染的 blocks
219
+ const blocks = computed(() => {
220
+ // 未启用打字机或没有 transformer:返回原始 blocks
221
+ if (!typewriterEnabled.value || !transformer) {
222
+ return rawBlocks.value
223
+ }
224
+
225
+ // 启用打字机:使用 displayBlocks
226
+ return displayBlocksRef.value.map((db, index) => {
227
+ const isPending = !db.isDisplayComplete
228
+ const isLastPending = isPending && index === displayBlocksRef.value.length - 1
229
+
230
+ // typing 效果时添加光标
231
+ let node = db.displayNode
232
+ if (typewriterEffect.value === 'typing' && isLastPending) {
233
+ node = addCursorToNode(db.displayNode, typewriterCursor.value)
234
+ }
235
+
236
+ return {
237
+ id: db.id,
238
+ stableId: db.id,
239
+ status: (db.isDisplayComplete ? 'completed' : 'pending') as 'pending' | 'stable' | 'completed',
240
+ isLastPending,
241
+ node,
242
+ startOffset: 0,
243
+ endOffset: 0,
244
+ rawText: ''
245
+ }
246
+ })
247
+ })
248
+
74
249
  function append(chunk: string): IncrementalUpdate {
75
250
  isLoading.value = true
76
251
  const update = parser.append(chunk)
77
252
 
78
- // 更新 markdown ref
79
253
  markdown.value = parser.getBuffer()
80
254
 
81
- // 使用 markRaw 避免深层响应式
82
255
  if (update.completed.length > 0) {
83
256
  completedBlocks.value = [
84
257
  ...completedBlocks.value,
@@ -93,7 +266,6 @@ export function useIncremark(options: UseIncremarkOptions = {}) {
93
266
  function finalize(): IncrementalUpdate {
94
267
  const update = parser.finalize()
95
268
 
96
- // 更新 markdown ref
97
269
  markdown.value = parser.getBuffer()
98
270
 
99
271
  if (update.completed.length > 0) {
@@ -118,17 +290,14 @@ export function useIncremark(options: UseIncremarkOptions = {}) {
118
290
  pendingBlocks.value = []
119
291
  markdown.value = ''
120
292
  isLoading.value = false
293
+
294
+ // 重置 transformer
295
+ transformer?.reset()
121
296
  }
122
297
 
123
- /**
124
- * 一次性渲染完整 Markdown(reset + append + finalize)
125
- * @param content 完整的 Markdown 内容
126
- */
127
298
  function render(content: string): IncrementalUpdate {
128
- // 调用 parser 的 render 方法
129
299
  const update = parser.render(content)
130
300
 
131
- // 同步状态
132
301
  markdown.value = parser.getBuffer()
133
302
  completedBlocks.value = parser.getCompletedBlocks().map(b => markRaw(b))
134
303
  pendingBlocks.value = []
@@ -137,6 +306,47 @@ export function useIncremark(options: UseIncremarkOptions = {}) {
137
306
  return update
138
307
  }
139
308
 
309
+ // 打字机控制对象
310
+ const typewriter: TypewriterControls = {
311
+ enabled: typewriterEnabled,
312
+ isProcessing: computed(() => isTypewriterProcessing.value),
313
+ isPaused: computed(() => isTypewriterPaused.value),
314
+ effect: computed(() => typewriterEffect.value),
315
+ skip: () => transformer?.skip(),
316
+ pause: () => {
317
+ transformer?.pause()
318
+ isTypewriterPaused.value = true
319
+ },
320
+ resume: () => {
321
+ transformer?.resume()
322
+ isTypewriterPaused.value = false
323
+ },
324
+ setOptions: (opts) => {
325
+ if (opts.enabled !== undefined) {
326
+ typewriterEnabled.value = opts.enabled
327
+ }
328
+ if (opts.charsPerTick !== undefined || opts.tickInterval !== undefined || opts.effect !== undefined || opts.pauseOnHidden !== undefined) {
329
+ transformer?.setOptions({
330
+ charsPerTick: opts.charsPerTick,
331
+ tickInterval: opts.tickInterval,
332
+ effect: opts.effect,
333
+ pauseOnHidden: opts.pauseOnHidden
334
+ })
335
+ }
336
+ if (opts.effect !== undefined) {
337
+ typewriterEffect.value = opts.effect
338
+ }
339
+ if (opts.cursor !== undefined) {
340
+ typewriterCursor.value = opts.cursor
341
+ }
342
+ }
343
+ }
344
+
345
+ // 清理
346
+ onUnmounted(() => {
347
+ transformer?.destroy()
348
+ })
349
+
140
350
  return {
141
351
  /** 已收集的完整 Markdown 字符串 */
142
352
  markdown,
@@ -146,7 +356,7 @@ export function useIncremark(options: UseIncremarkOptions = {}) {
146
356
  pendingBlocks,
147
357
  /** 当前完整的 AST */
148
358
  ast,
149
- /** 所有块(完成 + 待处理),带稳定 ID */
359
+ /** 用于渲染的 blocks(根据打字机设置自动处理) */
150
360
  blocks,
151
361
  /** 是否正在加载 */
152
362
  isLoading,
@@ -156,11 +366,13 @@ export function useIncremark(options: UseIncremarkOptions = {}) {
156
366
  finalize,
157
367
  /** 强制中断 */
158
368
  abort,
159
- /** 重置解析器 */
369
+ /** 重置解析器和打字机 */
160
370
  reset,
161
371
  /** 一次性渲染(reset + append + finalize) */
162
372
  render,
163
373
  /** 解析器实例 */
164
- parser
374
+ parser,
375
+ /** 打字机控制 */
376
+ typewriter
165
377
  }
166
378
  }
package/src/index.ts CHANGED
@@ -1,6 +1,14 @@
1
1
  // Composables
2
- export { useIncremark, useStreamRenderer, useDevTools } from './composables'
3
- export type { UseIncremarkOptions, UseStreamRendererOptions, UseDevToolsOptions } from './composables'
2
+ export { useIncremark, useStreamRenderer, useDevTools, useBlockTransformer } from './composables'
3
+ export type {
4
+ UseIncremarkOptions,
5
+ TypewriterOptions,
6
+ TypewriterControls,
7
+ UseStreamRendererOptions,
8
+ UseDevToolsOptions,
9
+ UseBlockTransformerOptions,
10
+ UseBlockTransformerReturn
11
+ } from './composables'
4
12
 
5
13
  // Components
6
14
  export {
@@ -15,7 +23,8 @@ export {
15
23
  IncremarkThematicBreak,
16
24
  IncremarkInline,
17
25
  IncremarkMath,
18
- IncremarkDefault
26
+ IncremarkDefault,
27
+ AutoScrollContainer
19
28
  } from './components'
20
29
  export type { ComponentMap, BlockWithStableId } from './components'
21
30
 
@@ -26,5 +35,29 @@ export type {
26
35
  ParserOptions,
27
36
  BlockStatus,
28
37
  Root,
29
- RootContent
38
+ RootContent,
39
+ // Transformer types
40
+ SourceBlock,
41
+ DisplayBlock,
42
+ TransformerPlugin,
43
+ TransformerOptions,
44
+ TransformerState,
45
+ AnimationEffect
46
+ } from '@incremark/core'
47
+
48
+ // Re-export transformer utilities and plugins
49
+ export {
50
+ BlockTransformer,
51
+ createBlockTransformer,
52
+ countChars,
53
+ sliceAst,
54
+ cloneNode,
55
+ codeBlockPlugin,
56
+ mermaidPlugin,
57
+ imagePlugin,
58
+ mathPlugin,
59
+ thematicBreakPlugin,
60
+ defaultPlugins,
61
+ allPlugins,
62
+ createPlugin
30
63
  } from '@incremark/core'