AI 菜单
一个功能完备、基于 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>
)
}属性
| 名称 | 类型 | 默认值 | 描述 |
|---|---|---|---|
editor | Editor | null | undefined | Tiptap 编辑器实例 |
anchorToSelection | boolean | false | 若为 true,菜单锚点定位于当前文本选区,覆盖整个编辑器宽度,并动态响应滚动。 |
<AiMenuStateProvider />
管理 AI 菜单状态的状态提供器,覆盖整个应用。
使用示例
import { AiMenuStateProvider } from '@/components/tiptap-ui/ai-menu'
function App() {
return <AiMenuStateProvider>{/* 你的编辑器组件 */}</AiMenuStateProvider>
}<AiMenuContent />
渲染 AI 菜单界面的主内容组件。
属性
| 名称 | 类型 | 默认值 | 描述 |
|---|---|---|---|
editor | Editor | null | undefined | Tiptap 编辑器实例 |
anchorToSelection | boolean | false | 若为 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>
)
}返回值
| 名称 | 类型 | 描述 |
|---|---|---|
state | AiMenuState | 当前 AI 菜单状态 |
updateState | (updates: Partial<AiMenuState>) => void | 更新菜单状态的函数 |
setFallbackAnchor | (element: HTMLElement | null, rect?) => void | 设置备用定位锚点 |
reset | () => void | 重置菜单状态为初始值 |
useAiContentTracker()
用于追踪编辑器中 AI 生成内容变化的 Hook。支持两种定位模式:流式模式(当 anchorToSelection 为 true 并且 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>
}参数
| 名称 | 类型 | 默认值 | 描述 |
|---|---|---|---|
editor | Editor | null | — | Tiptap 编辑器实例 |
aiGenerationActive | boolean | — | AI 生成内容是否处于活跃状态 |
setAnchorElement | (element: HTMLElement) => void | — | 设置浮动菜单锚点元素的回调函数 |
anchorToSelection | boolean | false | 是否使用覆盖整个编辑器宽度的虚拟锚点,并支持滚动及流式调整 |
useTextSelectionTracker()
用于追踪文本选择变化以调整菜单定位的 Hook。当 anchorToSelection 为 true 时,基于选区的垂直位置创建覆盖编辑器宽度的虚拟锚点,替代原生选区 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>
}参数
| 名称 | 类型 | 默认值 | 描述 |
|---|---|---|---|
editor | Editor | null | — | Tiptap 编辑器实例 |
aiGenerationActive | boolean | — | AI 生成内容是否处于活跃状态 |
showMenuAtElement | (element: HTMLElement) => void | — | 显示菜单于指定元素的回调 |
setMenuVisible | (visible: boolean) => void | — | 设置菜单可见状态的回调 |
onSelectionChange | (element: HTMLElement | null, rect: DOMRect | null) => void | — | 选区变化时的可选回调 |
prevent | boolean | false | 若为 true,阻止追踪 |
anchorToSelection | boolean | false | 是否使用覆盖编辑器宽度的虚拟锚点,基于选区的垂直位置 |
子组件
<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)
计算编辑器元素的内容区矩形(排除内边距和边框),返回包含 left 和 width 属性的对象。
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>
)
}