AI 菜单

Available in Start plan

一个功能完备、基于 AI 的 Tiptap 编辑器上下文菜单。提供智能的内容编辑、生成和转换功能,支持浮动菜单定位和可自定义的 AI 操作。

安装

通过 Tiptap CLI 添加该组件:

npx @tiptap/cli@latest add ai-menu

组件

<AiMenu />

一个全面的 AI 菜单,提供上下文的编辑和生成能力。

使用示例

import { EditorContent, EditorContext, useEditor } from '@tiptap/react'

// --- Tiptap 核心扩展 ---
import { StarterKit } from '@tiptap/starter-kit'
import { Ai } from '@tiptap-pro/extension-ai'
import { UiState } from '@/components/tiptap-extension/ui-state-extension'

import { HorizontalRule } from '@/components/tiptap-node/horizontal-rule-node/horizontal-rule-node-extension'
import { Selection } from '@tiptap/extensions'
import { AiProvider, useAi } from '@/components/contexts/ai-context'

// --- Tiptap UI ---
import { AiMenu } from '@/components/tiptap-ui/ai-menu'
import { AiAskButton } from '@/components/tiptap-ui/ai-ask-button'

// --- UI 基础组件 ---
import { ButtonGroup } from '@/components/tiptap-ui-primitive/button'

// --- 工具 ---
import { TIPTAP_AI_APP_ID } from '@/lib/tiptap-collab-utils'

// --- Tiptap 节点样式 ---
import '@/components/tiptap-node/blockquote-node/blockquote-node.scss'
import '@/components/tiptap-node/code-block-node/code-block-node.scss'
import '@/components/tiptap-node/horizontal-rule-node/horizontal-rule-node.scss'
import '@/components/tiptap-node/heading-node/heading-node.scss'
import '@/components/tiptap-node/paragraph-node/paragraph-node.scss'

export const AiMenuExample = () => {
  return (
    <AiProvider>
      <AiEditorWrapper />
    </AiProvider>
  )
}

const AiEditorWrapper = () => {
  const { aiToken } = useAi()

  if (!aiToken) {
    return <div className="tiptap-editor-wrapper">正在加载 AI...</div>
  }

  return <AiEditor aiToken={aiToken} />
}

const AiEditor = ({ aiToken }: { aiToken: string }) => {
  const editor = useEditor({
    immediatelyRender: false,
    extensions: [
      StarterKit.configure({
        horizontalRule: false,
      }),
      HorizontalRule,
      Selection,
      UiState,
      Ai.configure({
        appId: TIPTAP_AI_APP_ID,
        token: aiToken,
        autocompletion: false,
        showDecorations: true,
        hideDecorationsOnStreamEnd: false,
        onLoading: (context) => {
          context.editor.commands.aiGenerationSetIsLoading(true)
          context.editor.commands.aiGenerationHasMessage(false)
        },
        onChunk: (context) => {
          context.editor.commands.aiGenerationSetIsLoading(true)
          context.editor.commands.aiGenerationHasMessage(true)
        },
        onSuccess: (context) => {
          const hasMessage = !!context.response
          context.editor.commands.aiGenerationSetIsLoading(false)
          context.editor.commands.aiGenerationHasMessage(hasMessage)
        },
      }),
    ],
    content: `
<p>今天,我们探索 AI 如何改变创意工作流程。从写作辅助到智能摘要,我们手中的工具快速演进。但我们如何负责任地使用它们?</p>
<p>本文将展示 AI 如何增强——而非替代——人类创造力的真实案例。</p>
        `,
  })

  return (
    <EditorContext.Provider value={{ editor }}>
      <div className="controls-bar">
        <div className="control-item">
          <ButtonGroup orientation="horizontal">
            <AiAskButton />
          </ButtonGroup>
        </div>
      </div>

      <EditorContent editor={editor} role="presentation" className="control-showcase">
        <AiMenu anchorToSelection />
      </EditorContent>
    </EditorContext.Provider>
  )
}

属性

名称类型默认值描述
editorEditor | nullundefinedTiptap 编辑器实例
anchorToSelectionbooleanfalse若为 true,菜单锚点定位于当前文本选区,覆盖整个编辑器宽度,并动态响应滚动。

<AiMenuStateProvider />

管理 AI 菜单状态的状态提供器,覆盖整个应用。

使用示例

import { AiMenuStateProvider } from '@/components/tiptap-ui/ai-menu'

function App() {
  return <AiMenuStateProvider>{/* 你的编辑器组件 */}</AiMenuStateProvider>
}

<AiMenuContent />

渲染 AI 菜单界面的主内容组件。

属性

名称类型默认值描述
editorEditor | nullundefinedTiptap 编辑器实例
anchorToSelectionbooleanfalse若为 true,菜单锚点定位于当前文本选区,覆盖整个编辑器宽度,并动态响应滚动。

Hooks

useAiMenuState()

用于访问和管理 AI 菜单状态的 Hook。

使用示例

import { useAiMenuState } from '@/components/tiptap-ui/ai-menu'

function MyComponent() {
  const { state, updateState, setFallbackAnchor, reset } = useAiMenuState()

  const handleOpenMenu = () => {
    updateState({ isOpen: true, shouldShowInput: true })
  }

  const handleCloseMenu = () => {
    reset()
  }

  return (
    <button onClick={state.isOpen ? handleCloseMenu : handleOpenMenu}>
      {state.isOpen ? '关闭 AI 菜单' : '打开 AI 菜单'}
    </button>
  )
}

返回值

名称类型描述
stateAiMenuState当前 AI 菜单状态
updateState(updates: Partial<AiMenuState>) => void更新菜单状态的函数
setFallbackAnchor(element: HTMLElement | null, rect?) => void设置备用定位锚点
reset() => void重置菜单状态为初始值

useAiContentTracker()

用于追踪编辑器中 AI 生成内容变化的 Hook。支持两种定位模式:流式模式(当 anchorToSelectiontrue 并且 AI 正在加载时),使用 ResizeObserver 实时调整锚点位置;静态模式则基于 AI 元素位置建立带滚动感知的虚拟锚点。

使用示例

import { useAiContentTracker } from '@/components/tiptap-ui/ai-menu'

function MyComponent() {
  const { editor } = useTiptapEditor()

  useAiContentTracker({
    editor,
    aiGenerationActive: true,
    setAnchorElement: (el) => store.setAnchorElement(el),
    anchorToSelection: true,
  })

  return <div>带 AI 内容跟踪的组件</div>
}

参数

名称类型默认值描述
editorEditor | nullTiptap 编辑器实例
aiGenerationActivebooleanAI 生成内容是否处于活跃状态
setAnchorElement(element: HTMLElement) => void设置浮动菜单锚点元素的回调函数
anchorToSelectionbooleanfalse是否使用覆盖整个编辑器宽度的虚拟锚点,并支持滚动及流式调整

useTextSelectionTracker()

用于追踪文本选择变化以调整菜单定位的 Hook。当 anchorToSelectiontrue 时,基于选区的垂直位置创建覆盖编辑器宽度的虚拟锚点,替代原生选区 DOM 元素。

使用示例

import { useTextSelectionTracker } from '@/components/tiptap-ui/ai-menu'

function MyComponent() {
  const { editor } = useTiptapEditor()

  useTextSelectionTracker({
    editor,
    aiGenerationActive: true,
    showMenuAtElement: (el) => show(el),
    setMenuVisible: (visible) => updateState({ isOpen: visible }),
    anchorToSelection: true,
  })

  return <div>带选择跟踪的组件</div>
}

参数

名称类型默认值描述
editorEditor | nullTiptap 编辑器实例
aiGenerationActivebooleanAI 生成内容是否处于活跃状态
showMenuAtElement(element: HTMLElement) => void显示菜单于指定元素的回调
setMenuVisible(visible: boolean) => void设置菜单可见状态的回调
onSelectionChange(element: HTMLElement | null, rect: DOMRect | null) => void选区变化时的可选回调
preventbooleanfalse若为 true,阻止追踪
anchorToSelectionbooleanfalse是否使用覆盖编辑器宽度的虚拟锚点,基于选区的垂直位置

子组件

<AiMenuItems />

按类别分组展示可用 AI 操作的组件。

使用示例

import { AiMenuItems } from '@/components/tiptap-ui/ai-menu'

function CustomAiMenu() {
  const { editor } = useTiptapEditor()

  return (
    <AiMenuItems
      editor={editor}
      availableActions={['improveWriting', 'aiFixSpellingAndGrammar', 'summarize']}
    />
  )
}

<AiMenuActions />

渲染 AI 菜单相关操作按钮(接受、重新生成等)的组件。

使用示例

import { AiMenuActions } from '@/components/tiptap-ui/ai-menu'

function CustomAiMenu() {
  const { editor } = useTiptapEditor()

  return (
    <AiMenuActions
      editor={editor}
      onAccept={() => console.log('AI 内容已接受')}
      onRegenerate={() => console.log('重新生成 AI 内容')}
    />
  )
}

<AiMenuInputTextarea />

用于自定义 AI 提示语的输入组件。

使用示例

import { AiMenuInputTextarea } from '@/components/tiptap-ui/ai-menu'

function CustomPromptInput() {
  const handleSubmit = (prompt: string) => {
    console.log('用户提示:', prompt)
  }

  return (
    <AiMenuInputTextarea
      onSubmit={handleSubmit}
      placeholder="请向 AI 提问以帮助完善内容..."
    />
  )
}

工具函数

getContextAndInsertAt(editor)

确定上下文及 AI 操作插入点的工具函数。

import { getContextAndInsertAt } from '@/components/tiptap-ui/ai-menu'

const { context, insertAt, isSelection } = getContextAndInsertAt(editor)

if (isSelection) {
  // 处理基于选择的 AI 操作
  editor.chain().aiEdit({ prompt: '改进这段文字', insertAt }).run()
} else {
  // 处理基于插入点的 AI 操作
  editor.chain().aiGenerate({ prompt: '写一段关于 AI 的内容', insertAt }).run()
}

findPrioritizedAIElement(editor)

查找最合适用于 AI 菜单定位的 DOM 元素。

import { findPrioritizedAIElement } from '@/components/tiptap-ui/ai-menu'

const targetElement = findPrioritizedAIElement(editor)
if (targetElement) {
  // 将菜单相对于此元素定位
}

getSelectionRangeRect(editor)

使用浏览器的 Selection API 获取当前文本选区的边界矩形。若选区为空,则返回 null。当原生方法失效时,使用 ProseMirror 坐标做备选。

import { getSelectionRangeRect } from '@/components/tiptap-ui/ai-menu'

const rect = getSelectionRangeRect(editor)
if (rect) {
  // 使用 rect 进行定位
}

createEditorWidthAnchorRect(editorDom, sourceRect)

基于给定的源矩形,在编辑器内容区(排除内边距和边框)创建一条覆盖编辑器宽度、位置在源矩形垂直位置的锚点矩形。

import { createEditorWidthAnchorRect } from '@/components/tiptap-ui/ai-menu'

const selectionRect = getSelectionRangeRect(editor)
if (selectionRect) {
  const anchorRect = createEditorWidthAnchorRect(editor.view.dom, selectionRect)
  // anchorRect 覆盖编辑器内容区宽度,垂直对应选区位置
}

createVirtualAnchor(rect, referenceElement?)

创建一个虚拟锚点元素,其 getBoundingClientRect() 方法返回传入的矩形。当提供了 referenceElement 时,锚点会存储相对于该元素的偏移,并在每次调用时动态计算视口坐标,从而实现支持滚动感知的定位。

import { createVirtualAnchor, createEditorWidthAnchorRect } from '@/components/tiptap-ui/ai-menu'

const anchorRect = createEditorWidthAnchorRect(editor.view.dom, selectionRect)
const anchor = createVirtualAnchor(anchorRect, editor.view.dom)

// 锚点的 getBoundingClientRect() 会随着编辑器滚动实时更新
store.setAnchorElement(anchor)

getEditorContentRect(editorDom)

计算编辑器元素的内容区矩形(排除内边距和边框),返回包含 leftwidth 属性的对象。

import { getEditorContentRect } from '@/components/tiptap-ui/ai-menu'

const { left, width } = getEditorContentRect(editor.view.dom)

AI 操作

AI 菜单提供若干内置操作,按类别组织:

编辑类操作

  • 调整语气:改变所选文字的语气
  • 拼写与语法修正:纠正拼写和语法错误
  • 扩展:扩展所选内容
  • 缩短:使内容更简洁
  • 简化语言:使用更简单、更清晰的语言
  • 改进写作:增强整体写作质量
  • 表情符号化:为内容添加相关表情符号

写作类操作

  • 继续写作:生成内容的延续部分
  • 摘要:对所选文本进行总结
  • 翻译到:将内容翻译成不同语言

状态管理

AiMenuState 接口

interface AiMenuState {
  isOpen: boolean
  tone?: string
  language: string
  shouldShowInput: boolean
  inputIsFocused: boolean
  fallbackAnchor: {
    element: HTMLElement | null
    rect: DOMRect | null
  }
}

状态更新

可以使用 updateState 函数更新 AI 菜单状态:

const { updateState } = useAiMenuState()

// 打开菜单并聚焦输入框
updateState({
  isOpen: true,
  shouldShowInput: true,
  inputIsFocused: true,
})

// 设置翻译语言
updateState({ language: 'es' })

// 设置内容调整语气
updateState({ tone: 'professional' })

依赖与要求

依赖包

  • @tiptap/react - Tiptap React 核心集成
  • @tiptap-pro/extension-ai - 内容生成的 AI 扩展
  • @tiptap/starter-kit - 基础 Tiptap 扩展
  • react-hotkeys-hook - 键盘快捷键管理

扩展

  • ui-state-extension - 管理 AI 操作的 UI 状态
  • selection-extension - 增强文本选择处理

相关组件

  • use-tiptap-editor(Hook)
  • use-ui-editor-state(Hook)
  • menu(基础组件)
  • button, button-group(基础组件)
  • card(基础组件)
  • combobox(基础组件)
  • tiptap-utils(工具库)
  • sparkles-icon, stop-circle-2-icon(图标)

配置

AI 提供者配置示例

import { AiProvider } from '@/contexts/ai-context'

function App() {
  return (
    <AiProvider appId="your-app-id" token="your-ai-token">
      <YourEditor />
    </AiProvider>
  )
}