┌──────────────────────────────────────────────────────────┐ │ 📄 shadcn/directory/udecode/plate/(plugins)/(ai)/ai.cn │ └──────────────────────────────────────────────────────────┘
╔══════════════════════════════════════════════════════════════════════════════════════════════╗
║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║
title: AI description: AI 驱动的写作辅助。 docs:
streamInsertChunk 提供支持。withAIBatch 和 tf.ai.undo() 实现撤销安全的批处理。tf.aiChat.replaceSelection 和 tf.aiChat.insertBelow 替换或追加整个部分。@ai-sdk/react,使 api.aiChat.submit 可以从 Vercel AI SDK 助手流式传输响应。添加 AI 功能最快的方法是使用 AIKit。它包含配置好的 AIPlugin、AIChatPlugin、Markdown 流式传输助手、光标覆盖层及其 Plate UI 组件。
AIMenu:用于提示、工具快捷方式和对话审查的浮动命令界面。AILoadingBar:在编辑器容器中显示流式传输状态。AIAnchorElement:在流式传输期间用于定位浮动菜单的不可见锚点节点。AILeaf:使用微妙样式渲染 AI 标记的文本。import { createPlateEditor } from 'platejs/react';
import { AIKit } from '@/components/editor/plugins/ai-kit';
const editor = createPlateEditor({
plugins: [
// ...otherPlugins,
...AIKit,
],
});
公开一个流式命令端点,代理你的模型提供商:
<ComponentSource name="ai-api" />在本地设置提供商凭证:
AI_GATEWAY_API_KEY="your-api-key"
</Steps>
npm install @platejs/ai @platejs/markdown @platejs/selection @ai-sdk/react ai
@platejs/suggestion 是可选的,但对于基于差异的编辑建议是必需的。
import { createPlateEditor } from 'platejs/react';
import { AIChatPlugin, AIPlugin } from '@platejs/ai/react';
import { BlockSelectionPlugin } from '@platejs/selection/react';
import { MarkdownPlugin } from '@platejs/markdown';
export const editor = createPlateEditor({
plugins: [
BlockSelectionPlugin,
MarkdownPlugin,
AIPlugin,
AIChatPlugin, // 在下一步中扩展
],
});
BlockSelectionPlugin:启用多块选择,AIChatPlugin 依赖它来进行插入/替换变换。MarkdownPlugin:提供流式工具使用的 Markdown 序列化。AIPlugin:添加 AI 标记和用于撤销 AI 批处理的变换。AIChatPlugin:提供 AI 组合框、API 助手和变换。使用 AIPlugin.withComponent 配合你自己的元素(或 AILeaf)来高亮显示 AI 生成的文本。
扩展 AIChatPlugin 以挂接流式传输和编辑。该示例镜像了 AIKit 的核心逻辑,同时保持 UI 无头化。
import { AIChatPlugin, applyAISuggestions, streamInsertChunk, useChatChunk } from '@platejs/ai/react';
import { withAIBatch } from '@platejs/ai';
import { getPluginType, KEYS, PathApi } from 'platejs';
import { usePluginOption } from 'platejs/react';
export const aiChatPlugin = AIChatPlugin.extend({
options: {
chatOptions: {
api: '/api/ai/command',
body: {
model: 'openai/gpt-4o-mini',
},
},
trigger: ' ',
triggerPreviousCharPattern: /^\s?$/,
},
useHooks: ({ editor, getOption }) => {
const mode = usePluginOption(AIChatPlugin, 'mode');
const toolName = usePluginOption(AIChatPlugin, 'toolName');
useChatChunk({
onChunk: ({ chunk, isFirst, text }) => {
if (mode === 'insert') {
if (isFirst) {
editor.setOption(AIChatPlugin, 'streaming', true);
editor.tf.insertNodes(
{
children: [{ text: '' }],
type: getPluginType(editor, KEYS.aiChat),
},
{
at: PathApi.next(editor.selection!.focus.path.slice(0, 1)),
}
);
}
if (!getOption('streaming')) return;
withAIBatch(
editor,
() => {
streamInsertChunk(editor, chunk, {
textProps: {
[getPluginType(editor, KEYS.ai)]: true,
},
});
},
{ split: isFirst }
);
}
if (toolName === 'edit' && mode === 'chat') {
withAIBatch(
editor,
() => {
applyAISuggestions(editor, text);
},
{ split: isFirst }
);
}
},
onFinish: () => {
editor.setOption(AIChatPlugin, 'streaming', false);
editor.setOption(AIChatPlugin, '_blockChunks', '');
editor.setOption(AIChatPlugin, '_blockPath', null);
editor.setOption(AIChatPlugin, '_mdxName', null);
},
});
},
});
useChatChunk:监视 UseChatHelpers 状态并生成增量块。streamInsertChunk:将 Markdown/MDX 流式传输到文档中,尽可能重用现有块。applyAISuggestions:当 toolName === 'edit' 时,将响应转换为临时建议节点。withAIBatch:标记历史批处理,使 tf.ai.undo() 仅撤销最后一次 AI 生成的更改。当你扩展插件时,提供你自己的 render 组件(工具栏按钮、浮动菜单等)。
在服务器上处理 api.aiChat.submit 请求。每个请求都包含来自 @ai-sdk/react 的聊天 messages 和一个 ctx 负载,其中包含编辑器 children、当前 selection 和最后的 toolName。
完整 API 示例
import { createGateway } from '@ai-sdk/gateway';
import { convertToCoreMessages, streamText } from 'ai';
import { createSlateEditor } from 'platejs';
import { BaseEditorKit } from '@/registry/components/editor/editor-base-kit';
import { markdownJoinerTransform } from '@/registry/lib/markdown-joiner-transform';
export async function POST(req: Request) {
const { apiKey, ctx, messages, model } = await req.json();
const editor = createSlateEditor({
plugins: BaseEditorKit,
selection: ctx.selection,
value: ctx.children,
});
const gateway = createGateway({
apiKey: apiKey ?? process.env.AI_GATEWAY_API_KEY!,
});
const result = streamText({
experimental_transform: markdownJoinerTransform(),
messages: convertToCoreMessages(messages),
model: gateway(model ?? 'openai/gpt-4o-mini'),
system: ctx.toolName === 'edit' ? 'You are an editor that rewrites user text.' : undefined,
});
return result.toDataStreamResponse();
}
ctx.children 和 ctx.selection 被重新水合到 Slate 编辑器中,以便你可以构建丰富的提示(参见提示模板)。chatOptions.body 转发提供商设置(模型、apiKey、温度、网关标志等);你添加的所有内容都会在 JSON 负载中原样传递,后端可以在调用 createGateway 前读取。useChat 和 useChatChunk 可以逐步处理令牌。useChat使用 @ai-sdk/react 桥接编辑器和模型端点。在插件上存储助手,以便变换可以重新加载、停止或显示聊天状态。
import { useEffect } from 'react';
import { type UIMessage, DefaultChatTransport } from 'ai';
import { type UseChatHelpers, useChat } from '@ai-sdk/react';
import { AIChatPlugin } from '@platejs/ai/react';
import { useEditorPlugin } from 'platejs/react';
type ChatMessage = UIMessage<{}, { toolName: 'comment' | 'edit' | 'generate'; comment?: unknown }>;
export const useEditorAIChat = () => {
const { editor, setOption } = useEditorPlugin(AIChatPlugin);
const chat = useChat<ChatMessage>({
id: 'editor',
api: '/api/ai/command',
transport: new DefaultChatTransport(),
onData(data) {
if (data.type === 'data-toolName') {
editor.setOption(AIChatPlugin, 'toolName', data.data);
}
},
});
useEffect(() => {
setOption('chat', chat as UseChatHelpers<ChatMessage>);
}, [chat, setOption]);
return chat;
};
将助手与 useEditorChat 结合使用以保持浮动菜单正确锚定:
import { useEditorChat } from '@platejs/ai/react';
useEditorChat({
chat,
onOpenChange: (open) => {
if (!open) chat.stop?.();
},
});
现在你可以以编程方式提交提示:
import { AIChatPlugin } from '@platejs/ai/react';
editor.getApi(AIChatPlugin).aiChat.submit('', {
prompt: {
default: 'Continue the document after {block}',
selecting: 'Rewrite {selection} with a clearer tone',
},
toolName: 'generate',
});
</Steps>
api.aiChat.submit 接受一个 EditorPrompt。提供一个字符串、一个包含 default/selecting/blockSelecting 的对象,或一个接收 { editor, isSelecting, isBlockSelecting } 的函数。客户端中的 getEditorPrompt 助手将该值转换为最终字符串。replacePlaceholders(editor, template, { prompt }) 结合使用,使用 @platejs/ai 生成的 Markdown 展开 {editor}、{block}、{blockSelection} 和 {prompt}。import { replacePlaceholders } from '@platejs/ai';
editor.getApi(AIChatPlugin).aiChat.submit('Improve tone', {
prompt: ({ isSelecting }) =>
isSelecting
? replacePlaceholders(editor, 'Rewrite {blockSelection} using a friendly tone.')
: replacePlaceholders(editor, 'Continue {block} with two more sentences.'),
toolName: 'generate',
});
apps/www/src/app/api/ai/command 中的演示后端从 ctx 重构编辑器并构建结构化提示:
getChooseToolPrompt 决定请求是 generate、edit 还是 comment。getGeneratePrompt、getEditPrompt 和 getCommentPrompt 将当前编辑器状态转换为针对每种模式量身定制的指令。getMarkdown、getMarkdownWithSelection 和 buildStructuredPrompt 这样的实用助手(参见 apps/www/src/app/api/ai/command/prompts.ts)使得将块 ID、选择和 MDX 标签嵌入到 LLM 请求中变得容易。增强从客户端发送的负载以微调服务器提示:
editor.setOption(aiChatPlugin, 'chatOptions', {
api: '/api/ai/command',
body: {
model: 'openai/gpt-4o-mini',
tone: 'playful',
temperature: 0.4,
},
});
chatOptions.body 下的所有内容都会到达路由处理程序,让你可以交换提供商、传递用户特定的元数据或分支到不同的提示模板。
流式传输工具在响应到达时保持复杂布局完整:
streamInsertChunk(editor, chunk, options) 反序列化 Markdown 块,就地更新当前块,并根据需要追加新块。使用 textProps/elementProps 标记流式节点(例如,标记 AI 文本)。streamDeserializeMd 和 streamDeserializeInlineMd 提供较低级别的访问,如果你需要控制自定义节点类型的流式传输。streamSerializeMd 镜像编辑器状态,以便你可以检测流式内容和响应缓冲区之间的偏差。当流式传输完成时,重置内部的 _blockChunks、_blockPath 和 _mdxName 选项,以便从干净的状态开始下一个响应。
useAIChatEditor为聊天预览注册一个辅助编辑器,并使用块级记忆化反序列化 Markdown。
<API name="useAIChatEditor"> <APIParameters> <APIItem name="editor" type="SlateEditor">专用于聊天预览的编辑器实例。</APIItem> <APIItem name="content" type="string">模型返回的 Markdown 内容。</APIItem> <APIItem name="options" type="DeserializeMdOptions" optional>传递 `parser` 以在反序列化之前过滤标记。</APIItem> </APIParameters> </API>import { usePlateEditor } from 'platejs/react';
import { MarkdownPlugin } from '@platejs/markdown';
import { AIChatPlugin, useAIChatEditor } from '@platejs/ai/react';
const aiPreviewEditor = usePlateEditor({
plugins: [MarkdownPlugin, AIChatPlugin],
});
useAIChatEditor(aiPreviewEditor, responseMarkdown, {
parser: { exclude: ['space'] },
});
useEditorChat将 UseChatHelpers 连接到编辑器状态,以便 AI 菜单知道是锚定到光标、选择还是块选择。
useChatChunk逐块流式传输聊天响应,并让你完全控制插入。
<API name="useChatChunk"> <APIParameters> <APIItem name="onChunk" type="(chunk: { chunk: string; isFirst: boolean; nodes: TText[]; text: string }) => void">处理每个流式块。</APIItem> <APIItem name="onFinish" type="({ content }: { content: string }) => void" optional>当流式传输完成时调用。</APIItem> </APIParameters> </API>withAIBatch将编辑器操作分组到单个历史批处理中,并将其标记为 AI 生成,以便 tf.ai.undo() 可以安全地删除它。
applyAISuggestions将 AI 输出与存储的 chatNodes 进行差异比较,并写入临时建议节点。需要 @platejs/suggestion。
补充助手允许你完成或丢弃差异:
acceptAISuggestions(editor):将临时建议节点转换为永久建议。rejectAISuggestions(editor):删除临时建议节点并清除建议标记。aiCommentToRange将流式评论元数据映射回文档范围,以便可以自动插入评论。
<API name="aiCommentToRange"> <APIParameters> <APIItem name="editor" type="PlateEditor">编辑器实例。</APIItem> <APIItem name="options" type="{ blockId: string; comment: string; content: string }">用于定位范围的块 ID 和文本。</APIItem> </APIParameters> <APIReturns type="{ start: BasePoint; end: BasePoint } | null">与评论匹配的范围,如果找不到则为 `null`。</APIReturns> </API>findTextRangeInBlock使用 LCS 在块内查找最接近匹配的模糊搜索助手。
<API name="findTextRangeInBlock"> <APIParameters> <APIItem name="node" type="TNode">要搜索的块节点。</APIItem> <APIItem name="searchText" type="string">要定位的文本片段。</APIItem> </APIParameters> <APIReturns type="{ start: { path: Path; offset: number }; end: { path: Path; offset: number } } | null">匹配的范围或 `null`。</APIReturns> </API>getEditorPrompt生成尊重光标、选择或块选择状态的提示。
<API name="getEditorPrompt"> <APIParameters> <APIItem name="editor" type="SlateEditor">提供上下文的编辑器。</APIItem> <APIItem name="options" type="{ prompt?: EditorPrompt }">描述提示的字符串、配置或函数。</APIItem> </APIParameters> <APIReturns type="string">上下文化的提示字符串。</APIReturns> </API>replacePlaceholders用序列化的 Markdown 替换像 {editor}、{blockSelection} 和 {prompt} 这样的占位符。
AIPlugin向流式文本添加 ai 标记,并公开用于删除 AI 节点或撤销最后一个 AI 批处理的变换。使用 .withComponent 来使用自定义组件渲染 AI 标记的文本。
AIChatPlugin支持 AI 菜单、聊天状态和变换的主要插件。
<API name="AIChatPlugin"> <APIOptions> <APIItem name="trigger" type="RegExp | string | string[]" optional>打开命令菜单的字符。默认为 `' '`。</APIItem> <APIItem name="triggerPreviousCharPattern" type="RegExp" optional>必须与触发器之前的字符匹配的模式。默认为 `/^\s?$/`。</APIItem> <APIItem name="triggerQuery" type="(editor: SlateEditor) => boolean" optional>返回 `false` 以取消在特定上下文中打开。</APIItem> <APIItem name="chat" type="UseChatHelpers<ChatMessage>" optional>存储来自 `useChat` 的助手,以便 API 调用可以访问它们。</APIItem> <APIItem name="chatNodes" type="TIdElement[]" optional>用于差异编辑建议的节点快照(内部管理)。</APIItem> <APIItem name="chatSelection" type="TRange | null" optional>提交提示之前捕获的选择(内部管理)。</APIItem> <APIItem name="mode" type="'chat' | 'insert'">控制响应是直接流式传输到文档中还是打开审查面板。默认为 `'insert'`。</APIItem> <APIItem name="open" type="boolean" optional>AI 菜单是否可见。默认为 `false`。</APIItem> <APIItem name="streaming" type="boolean" optional>响应流式传输时为 true。默认为 `false`。</APIItem> <APIItem name="toolName" type="'comment' | 'edit' | 'generate' | null" optional>用于解释响应的活动工具。</APIItem> </APIOptions> </API>api.aiChat.submit(input, options?)向模型提供商提交提示。当省略 mode 时,对于折叠的光标默认为 'insert',否则为 'chat'。
api.aiChat.reset(options?)清除聊天状态,删除 AI 节点,并可选地撤销最后一个 AI 批处理。
<API name="reset"> <APIParameters> <APIItem name="options" type="{ undo?: boolean }" optional>传递 `undo: false` 以保留流式内容。</APIItem> </APIParameters> </API>api.aiChat.node(options?)检索与指定条件匹配的第一个 AI 节点。
<API name="node"> <APIParameters> <APIItem name="options" type="EditorNodesOptions & { anchor?: boolean; streaming?: boolean }" optional>设置 `anchor: true` 以获取锚点节点,或 `streaming: true` 以检索当前正在流式传输到的节点。</APIItem> </APIParameters> <APIReturns type="NodeEntry | undefined">匹配的节点条目(如果找到)。</APIReturns> </API>api.aiChat.reload()使用存储的 UseChatHelpers 重放最后一个提示,在重新提交之前恢复原始选择或块选择。
api.aiChat.stop()停止流式传输并调用 chat.stop。
api.aiChat.show()打开 AI 菜单,清除先前的聊天消息,并重置工具状态。
api.aiChat.hide(options?)关闭 AI 菜单,可选地撤销最后一个 AI 批处理并重新聚焦编辑器。
<API name="hide"> <APIParameters> <APIItem name="options" type="{ focus?: boolean; undo?: boolean }" optional>设置 `focus: false` 以将焦点保持在编辑器外部,或 `undo: false` 以保留插入的内容。</APIItem> </APIParameters> </API>tf.aiChat.accept()接受最新响应。在插入模式下,它删除 AI 标记并将插入符号放置在流式内容的末尾。在聊天模式下,它应用待处理的建议。
tf.aiChat.insertBelow(sourceEditor, options?)在当前选择或块选择下方插入聊天预览(sourceEditor)。
tf.aiChat.replaceSelection(sourceEditor, options?)用聊天预览替换当前选择或块选择。
<API name="replaceSelection"> <APIParameters> <APIItem name="sourceEditor" type="SlateEditor">包含生成内容的编辑器。</APIItem> <APIItem name="options" type="{ format?: 'all' | 'none' | 'single' }" optional>控制应应用原始选择的多少格式。</APIItem> </APIParameters> </API>tf.aiChat.removeAnchor(options?)删除用于定位 AI 菜单的临时锚点节点。
<API name="removeAnchor"> <APIParameters> <APIItem name="options" type="EditorNodesOptions" optional>过滤要删除的节点。</APIItem> </APIParameters> </API>tf.ai.insertNodes(nodes, options?)在当前选择(或 options.target)处插入标记有 AI 标记的节点。
tf.ai.removeMarks(options?)从匹配节点中清除 AI 标记。
tf.ai.removeNodes(options?)删除标记为 AI 生成的文本节点。
tf.ai.undo()如果最新的历史条目是由 withAIBatch 创建的并包含 AI 内容,则撤销它。清除配对的重做条目以避免重新应用 AI 输出。
扩展 aiChatItems 映射以添加新命令。每个命令都接收 { aiEditor, editor, input },并可以使用自定义提示或变换调度 api.aiChat.submit。
summarizeInBullets: {
icon: <ListIcon />,
label: 'Summarize in bullets',
value: 'summarizeInBullets',
onSelect: ({ editor }) => {
void editor.getApi(AIChatPlugin).aiChat.submit('', {
prompt: 'Summarize the current selection using bullet points',
toolName: 'generate',
});
},
},
generateTOC: {
icon: <BookIcon />,
label: 'Generate table of contents',
value: 'generateTOC',
onSelect: ({ editor }) => {
const headings = editor.api.nodes({
match: (n) => ['h1', 'h2', 'h3'].includes(n.type as string),
});
const prompt =
headings.length === 0
? 'Create a realistic table of contents for this document'
: 'Generate a table of contents that reflects the existing headings';
void editor.getApi(AIChatPlugin).aiChat.submit('', {
mode: 'insert',
prompt,
toolName: 'generate',
});
},
},
菜单会自动在命令和建议状态之间切换:
cursorCommand:光标已折叠且尚无响应。selectionCommand:已选择文本且尚无响应。cursorSuggestion / selectionSuggestion:存在响应,因此会显示接受、重试或在下方插入等操作。使用 toolName('generate' | 'edit' | 'comment')来控制流式钩子如何处理响应。例如,'edit' 启用基于差异的建议,而 'comment' 允许你使用 aiCommentToRange 将流式评论转换为讨论线程。
║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║
╚══════════════════════════════════════════════════════════════════════════════════════════════╝