Files
deerflow-code/frontend-web/docs/ai-writing-右侧编辑器-滑词与段落菜单.md
T
fengxiangdebaozi ab876fc7d8 feat(ai-writing): 支持用户粘贴已有大纲并按结构生成
表单可提交 userOutline,后端解析/归一用户大纲并关联素材;补充大纲测试用例与右侧编辑器滑词/段落菜单文档。
2026-06-03 11:43:44 +08:00

11 KiB
Raw Blame History

AI 写作 · 右侧文本编辑器(滑词 / 段落菜单)

说明 AI 写作页、开放画布中文稿右侧编辑器的实现方式:编辑器本体基于 Tiptap v3;选中文本浮层(「滑词」)与段落悬停按钮为自研 React 组件,未使用 @tiptap/extension-bubble-menu 官方 BubbleMenu。


一、功能概览

交互 用户感知 组件 触发方式
滑词 拖选一段文字后,选区上方出现工具条(AI 改写、加粗、斜体、下划线、H1、复制) WritingBubbleMenu Tiptap selectionUpdate + 浏览器 window.getSelection()
段落菜单 鼠标移入某段/标题/引用块时,段落左侧出现 ✨ 按钮,点击可 AI 改写整段 WritingParagraphMenu 编辑器 DOM mousemove + ProseMirror posAtCoords
AI 改写面板 点击「AI 改写」后弹出润色/扩写等操作与预览 WritingAiMenu / WritingRewritePreview 由 TextArtifactRenderer + useWritingRewrite 串联

画布线程页(/page/canvas/:thread_id)与 AI 写作页(/page/workspace/ai-writing)右侧草稿共用同一套 TextArtifactRenderer 与菜单组件。


二、在 AI 写作中的挂载路径

flowchart TB
  AIWritingPage --> CanvasResizableLayout
  CanvasResizableLayout --> AIWritingDraftPanel
  AIWritingDraftPanel --> DraftStreamingPreview
  AIWritingDraftPanel --> TextArtifactRenderer
  DraftStreamingPreview["DraftStreamingPreview<br/>(流式只读 Markdown)"]
  TextArtifactRenderer --> WritingBubbleMenu
  TextArtifactRenderer --> WritingParagraphMenu
  TextArtifactRenderer --> TiptapEditor
  TextArtifactRenderer --> WritingAiMenu
阶段 右侧展示 滑词 / 段落菜单
草稿流式生成中(writing_draft / revising 且有 section 流) DraftStreamingPreview(ReactMarkdown 只读) 不可用
成稿可编辑(isDraftEditable === true) TextArtifactRenderer + Tiptap 可用
流式结束但尚未 editable TextArtifactRenderer 且 isStreaming={true} 不可用(editable: false)

相关状态由 CanvasUIContext.tsx 的 isDraftStreaming、isDraftEditable 控制;注释中「禁用编辑/润色/滑词」即指生成完成前不进入可编辑渲染器。

入口文件:


三、技术栈:编辑器 vs 浮层

3.1 Tiptap 编辑器(有使用)

包 用途
@tiptap/react useEditor、EditorContent
@tiptap/starter-kit 段落、标题、加粗、斜体、下划线等
@tiptap/extension-placeholder 空文档占位符
tiptap-markdown Markdown ↔ 文档互转
自研 AiHighlightExtension AI 改写前高亮选区(ProseMirror Decoration)

扩展注册见 TiptapExtensions.ts;实例创建见 useWritingEditor.ts。

3.2 浮层菜单(自研,非 Tiptap 官方插件)

包 是否用于滑词/段落菜单
@tiptap/extension-bubble-menu 未使用(package.json 中有依赖,代码未 import)
@tiptap/react 的 <BubbleMenu> 未使用

两套菜单均采用:

  1. 监听 Tiptap / DOM 事件更新位置与选区;
  2. createPortal(..., document.body) 挂到 document.body,避免被右侧滚动容器 overflow 裁切;
  3. position: fixed + getBoundingClientRect() 定位。

命名上的 WritingBubbleMenu 仅表示「气泡式选区工具条」,实现上与 Tiptap 官方 Bubble Menu 扩展无关。


四、核心文件索引

文件 职责
TiptapEditor.tsx EditorContent 渲染容器
TiptapExtensions.ts StarterKit、Placeholder、Markdown、AiHighlight
AiHighlightExtension.ts setAiHighlight / clearAiHighlight 命令
useWritingEditor.ts 创建 editor、同步外部 markdown、editable
WritingBubbleMenu.tsx 滑词 选区浮层
WritingParagraphMenu.tsx 段落 左侧 ✨ 按钮
WritingAiMenu.tsx AI 改写操作菜单
WritingRewritePreview.tsx 改写结果预览
useWritingRewrite.ts 调用润色 API、写回编辑器
apply-rewrite.ts 将改写结果应用到 ProseMirror 选区
CanvasUIContext.tsx writingSelection 全局状态
selection.ts WritingSelection 类型定义

五、滑词:WritingBubbleMenu

5.1 行为

  • 用户在编辑器内拖选非空文本时显示浮层;选区为空或位于 codeBlock 内时隐藏。
  • 浮层按钮:AI 改写、B / I / U、H1、复制。
  • 按钮 onMouseDown 使用 e.preventDefault(),避免点击时编辑器失焦导致选区丢失。

5.2 实现要点

editor.on("selectionUpdate") 
  → 读取 editor.state.selection (from, to, empty)
  → window.getSelection() + Range.getBoundingClientRect() 算屏幕坐标
  → setWritingSelection({ from, to, text }) 写入 CanvasUIContext
  → createPortal 渲染工具条,useLayoutEffect 做贴边 / 上方空间不足时翻到选区下方

关键代码位置:WritingBubbleMenu.tsx 中 useEffect 监听 selectionUpdate 与 blur。

点击「AI 改写」时:

  1. editor.commands.setAiHighlight(from, to) 高亮选区;
  2. 调用父组件传入的 onAiRewrite(),由 TextArtifactRenderer 打开 WritingAiMenu。

六、段落菜单:WritingParagraphMenu

6.1 行为

  • 鼠标在编辑器内移动时,检测光标下的块级节点;
  • 仅对 paragraph、heading、blockquote 且文本非空时,在块左侧显示 ✨ 按钮;
  • 鼠标离开编辑器或移出可改写块后,延迟约 380ms 隐藏(HIDE_DELAY),并在按钮与正文之间留有 hover 桥接区,避免移向按钮时闪烁消失。

6.2 实现要点

editor.view.dom mousemove
  → editor.view.posAtCoords({ left, top })
  → doc.resolve(pos).before(depth) 定位块起点
  → editor.view.nodeDOM(nodeStart) 取 DOM 矩形
  → createPortal 固定定位按钮在块左侧 (BTN_SIZE=24, BTN_GAP=6)

点击 ✨ 时:

  1. setTextSelection 选中整段 ProseMirror 范围(nodeFrom ~ nodeTo);
  2. setAiHighlight + setWritingSelection;
  3. 触发与滑词相同的 onAiRewrite() 流程。

关键代码位置:WritingParagraphMenu.tsx。


七、AI 改写数据流

sequenceDiagram
  participant User
  participant Menu as BubbleMenu / ParagraphMenu
  participant Ctx as CanvasUIContext
  participant Renderer as TextArtifactRenderer
  participant API as rewrite API

  User->>Menu: 选中文本或点段落 ✨
  Menu->>Ctx: setWritingSelection
  Menu->>Renderer: onAiRewrite()
  Renderer->>Renderer: phase = menu, WritingAiMenu
  User->>Renderer: 选择润色/扩写等
  Renderer->>API: useWritingRewrite.rewrite
  API-->>Renderer: 流式结果
  Renderer->>Renderer: WritingRewritePreview
  User->>Renderer: 接受改写
  Renderer->>Renderer: applyReplace / applyInsert
  • 选区信息类型:WritingSelection(from、to、text),见 selection.ts。
  • 与画布聊天侧选区 CanvasTextSelection 分离;后者用于快捷操作栏等,不用于 Tiptap 改写。

八、与顶部工具栏、目录栏的关系

同一 TextArtifactRenderer 编辑模式下还包含:

组件 作用
WritingToolbar.tsx 顶部固定格式栏、导出等
DraftOutlineSidebar.tsx 左侧标题目录,点击滚动到对应 heading

滑词 / 段落菜单与工具栏互不替代:工具栏面向全局格式;浮层面向当前选区或当前段落的快捷操作与 AI 改写入口。


九、开发与改造建议

9.1 若要改用官方 Bubble Menu

  1. 在 TiptapExtensions.ts 或组件层引入 @tiptap/react 的 BubbleMenu(或注册 @tiptap/extension-bubble-menu);
  2. 将 WritingBubbleMenu.tsx 内手写 selectionUpdate + Portal 逻辑替换为官方组件的 shouldShow / tippyOptions(v3 API 以官方文档为准);
  3. 段落左侧按钮仍建议保留自研 WritingParagraphMenu(官方 Bubble Menu 只覆盖选区,不覆盖 hover 段落场景)。

9.2 新增浮层按钮

  • 滑词:在 WritingBubbleMenu 的 BubbleButton 列表中增加,使用 editor.chain().focus()....run()。
  • 段落:在 WritingParagraphMenu 的 handleClick 或拆出多按钮布局;注意 REWRITABLE_TYPES 与 hover 延迟逻辑。

9.3 流式阶段禁止交互

不要在 DraftStreamingPreview 中挂载 Tiptap 菜单:流式阶段 token 高频更新,挂载 useWritingEditor 易引发更新风暴(见 DraftStreamingPreview.tsx 文件头注释)。应等 isDraftEditable 后再走 TextArtifactRenderer。


十、相关文档


十一、依赖版本(摘录)

以 package.json 为准,编辑器相关主要为:

  • @tiptap/core、@tiptap/react、@tiptap/starter-kit:^3.23.4
  • @tiptap/extension-bubble-menu:已安装但未接入滑词实现
  • tiptap-markdown:Markdown 扩展

升级 Tiptap 大版本前,请对照官方迁移指南,并重点回归:WritingBubbleMenu 选区坐标、WritingParagraphMenu 的 posAtCoords、以及 AiHighlightExtension 与 StarterKit 扩展名冲突(勿重复注册 underline)。