Block Editor重构审阅模式下diff功能的实现
This commit is contained in:
parent
e86af3110a
commit
c88f94726e
@ -388,6 +388,7 @@ const _handleAIAction = async (e: Event) => {
|
||||
insertTo: range.to,
|
||||
modifiedText: range.modifiedText,
|
||||
originalContent: range.originalContent,
|
||||
modifiedContent: range.modifiedContent,
|
||||
})
|
||||
},
|
||||
}, true) // reviewMode is now always true
|
||||
|
||||
@ -24,7 +24,7 @@ export interface AIStreamCallbacks {
|
||||
/** 流式完成 */
|
||||
onDone: (fullContent: string) => void
|
||||
/** 审阅模式:AI 输出完成并插入后的位置范围和对比文本 */
|
||||
onReviewReady?: (range: { from: number; to: number; originalText: string; modifiedText: string; originalContent?: any[] }) => void
|
||||
onReviewReady?: (range: { from: number; to: number; originalText: string; modifiedText: string; originalContent?: any[]; modifiedContent?: any[] }) => void
|
||||
/** 出错 */
|
||||
onError: (error: string) => void
|
||||
}
|
||||
@ -484,6 +484,10 @@ async function callInlineAI(options: InlineAICallOptions) {
|
||||
if (options.reviewMode && originalInsertAt !== null) {
|
||||
const reviewTo = editor.state.selection.to
|
||||
const modifiedText = editor.state.doc.textBetween(originalInsertAt, reviewTo, '\n', '\n')
|
||||
// 捕获修改后内容的 PM 结构化节点,用于块级 diff
|
||||
const modifiedContent = editor.state.doc
|
||||
.slice(originalInsertAt, reviewTo)
|
||||
.toJSON()?.content
|
||||
options.callbacks?.onReviewReady?.(
|
||||
{
|
||||
from: originalInsertAt,
|
||||
@ -491,6 +495,7 @@ async function callInlineAI(options: InlineAICallOptions) {
|
||||
originalText: options.context.selectedText,
|
||||
modifiedText,
|
||||
originalContent: (options as any)._originalContent,
|
||||
modifiedContent,
|
||||
},
|
||||
)
|
||||
} else {
|
||||
|
||||
@ -17,6 +17,7 @@ import { ref, type Ref } from 'vue'
|
||||
import type { Editor } from '@tiptap/core'
|
||||
import { Decoration, DecorationSet } from '@tiptap/pm/view'
|
||||
import { computeDiff } from '../plugins/ai-review/diff'
|
||||
import { computeStructuredDiff } from '../plugins/ai-review/structuredDiff'
|
||||
import { buildDecorationSet, createDeleteWidgetDom } from '../plugins/ai-review/decorations'
|
||||
import {
|
||||
reviewState,
|
||||
@ -36,6 +37,8 @@ export interface ReviewStartOptions {
|
||||
modifiedText: string
|
||||
/** 原始文档结构(ProseMirror JSON,用于 rejectAll 恢复段落结构) */
|
||||
originalContent?: any[]
|
||||
/** 修改后文档结构(ProseMirror JSON,用于块级 diff) */
|
||||
modifiedContent?: any[]
|
||||
}
|
||||
|
||||
export function useAIReview(editorRef: Ref<Editor | null>) {
|
||||
@ -96,8 +99,10 @@ export function useAIReview(editorRef: Ref<Editor | null>) {
|
||||
|
||||
const { originalText, insertFrom, modifiedText, insertTo } = opts
|
||||
|
||||
// 1. 计算 diff
|
||||
const diffSegments = computeDiff(originalText, modifiedText)
|
||||
// 1. 计算 diff(优先使用块级结构化 diff)
|
||||
const diffSegments = (opts.originalContent && opts.modifiedContent)
|
||||
? computeStructuredDiff(opts.originalContent, opts.modifiedContent)
|
||||
: computeDiff(originalText, modifiedText)
|
||||
const nonEqualSegs = diffSegments.filter(s => s.type !== 'equal')
|
||||
|
||||
// 2. 构建 segment 信息列表 + DecorationSet(含位置)
|
||||
|
||||
@ -0,0 +1,97 @@
|
||||
/**
|
||||
* plugins/ai-review/structuredDiff.ts — 块级结构化差异算法
|
||||
*
|
||||
* 将 ProseMirror JSON 节点数组按块对齐,跳过未变更的整块段落,
|
||||
* 仅在变更块内部执行字符级 LCS diff。
|
||||
*
|
||||
* 相比纯字符级 diff 的优势:
|
||||
* - 未变更的整块段落不产生任何 decoration(用户不会看到"无关文本被灰色覆盖")
|
||||
* - diff 不会跨块边界,避免块间误匹配
|
||||
* - 与 ProseMirror 文档模型天然对齐
|
||||
*
|
||||
* 用法:当 originalContent(原始 PM 节点数组)和 modifiedContent(修改后 PM 节点数组)
|
||||
* 同时存在时,此函数替代 computeDiff。
|
||||
*/
|
||||
|
||||
import { computeDiff, type DiffSegment } from './diff'
|
||||
|
||||
/** ProseMirror JSON 节点(简化接口) */
|
||||
interface PMNode {
|
||||
type: string
|
||||
content?: PMNode[]
|
||||
text?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取 PM 节点的纯文本内容(递归遍历 content 数组)。
|
||||
* 与 ProseMirror 的 textBetween('', '') 行为一致。
|
||||
*/
|
||||
function extractNodeText(node: PMNode): string {
|
||||
if (node.type === 'text') return node.text || ''
|
||||
if (!node.content || node.content.length === 0) return ''
|
||||
return node.content.map(extractNodeText).join('')
|
||||
}
|
||||
|
||||
/**
|
||||
* 块级结构化 diff。
|
||||
*
|
||||
* @param originalContent 原始内容的 PM JSON 节点数组(doc.slice().toJSON().content)
|
||||
* @param modifiedContent 修改后内容的 PM JSON 节点数组
|
||||
* @returns DiffSegment[] 可直接输入 buildDecorationSet
|
||||
*
|
||||
* 算法:
|
||||
* 1. 按索引对齐原始与修改的块
|
||||
* 2. 文本完全相同的块 → 单个 equal 段(跳过 decoration)
|
||||
* 3. 文本不同的块 → 在块范围内执行字符级 LCS diff
|
||||
* 4. 仅出现在原始中的块 → delete 段
|
||||
* 5. 仅出现在修改中的块 → insert 段
|
||||
* 6. 块间 \n 作为独立 equal 段插入,维持 charOffset 与 textBetween 一致
|
||||
*/
|
||||
export function computeStructuredDiff(
|
||||
originalContent: PMNode[],
|
||||
modifiedContent: PMNode[],
|
||||
): DiffSegment[] {
|
||||
if (!originalContent || originalContent.length === 0) {
|
||||
return [{ type: 'insert', original: '', modified: modifiedContent.map(extractNodeText).join('\n') }]
|
||||
}
|
||||
if (!modifiedContent || modifiedContent.length === 0) {
|
||||
return [{ type: 'delete', original: originalContent.map(extractNodeText).join('\n'), modified: '' }]
|
||||
}
|
||||
|
||||
const origTexts = originalContent.map(extractNodeText)
|
||||
const modTexts = modifiedContent.map(extractNodeText)
|
||||
const maxLen = Math.max(origTexts.length, modTexts.length)
|
||||
|
||||
const segments: DiffSegment[] = []
|
||||
|
||||
for (let i = 0; i < maxLen; i++) {
|
||||
// 块间分隔符:\n 作为独立 equal 段
|
||||
if (i > 0) {
|
||||
segments.push({ type: 'equal', original: '\n', modified: '\n' })
|
||||
}
|
||||
|
||||
const orig = i < origTexts.length ? origTexts[i] : ''
|
||||
const mod = i < modTexts.length ? modTexts[i] : ''
|
||||
|
||||
if (orig === mod) {
|
||||
// ── 未变更块:单段 equal,不产生 decoration ──
|
||||
if (orig !== '') {
|
||||
segments.push({ type: 'equal', original: orig, modified: mod })
|
||||
}
|
||||
// 空块跳过(不输出,保留位置计数但装饰系统会忽略)
|
||||
} else if (orig === '') {
|
||||
// ── 新增块 ──
|
||||
segments.push({ type: 'insert', original: '', modified: mod })
|
||||
} else if (mod === '') {
|
||||
// ── 删除块 ──
|
||||
segments.push({ type: 'delete', original: orig, modified: '' })
|
||||
} else {
|
||||
// ── 变更块:块内字符级 LCS diff ──
|
||||
const innerSegs = computeDiff(orig, mod)
|
||||
segments.push(...innerSegs)
|
||||
}
|
||||
}
|
||||
|
||||
return segments
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user