Plate 如何用 Copilot 添加打字时的幽灵文本 AI 补全?
【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate
如果你的 Plate 编辑器里还缺少"边打字边预测下一段文字"的能力,Copilot 插件就是为此设计的:它在光标所在块末尾之后渲染一段灰色"幽灵文本"(ghost text),你可以用 Tab 整段接受、用Cmd+→逐词接受,或用 Escape 拒绝。功能来自@platejs/ai包里的CopilotPlugin,配合@platejs/markdown把编辑器内容序列化成提示词,再经由你自己的后端 API 路由调用 Vercel AI SDK 的补全接口。
下面的路径基于 Next.js 项目(API 路由示例使用app/api/...目录约定),完成后的效果是:在段落末尾敲空格后自动出现补全建议,或按Ctrl+Space手动触发。
两种接法:Kit 与手动配置
Plate 提供两条安装路径,任选其一。
快速路径(CopilotKit):Kit 自带预配置的CopilotPlugin、MarkdownKit和 Plate UI 组件(包括渲染幽灵文本的GhostText组件):
import { createPlateEditor } from 'platejs/react'; import { CopilotKit } from '@/components/editor/plugins/copilot-kit'; const editor = createPlateEditor({ plugins: [ // ...otherPlugins, ...CopilotKit, // 使用 Tab 的插件要放在 CopilotKit 之后,避免冲突 // IndentPlugin, // TabbablePlugin, ], });手动路径:单独安装依赖并逐个挂载插件:
npm install @platejs/ai @platejs/markdownimport { CopilotPlugin } from '@platejs/ai/react'; import { MarkdownPlugin } from '@platejs/markdown'; import { createPlateEditor } from 'platejs/react'; const editor = createPlateEditor({ plugins: [ // ...otherPlugins, MarkdownPlugin, CopilotPlugin, // 使用 Tab 的插件要放在 CopilotPlugin 之后,避免冲突 // IndentPlugin, // TabbablePlugin, ], });两个插件的分工是:MarkdownPlugin负责把编辑器内容序列化为 Markdown 作为提示词发送,CopilotPlugin负责补全逻辑本身。注意插件顺序是硬性要求——CopilotPlugin用 Tab 接受建议,如果你同时挂载了IndentPlugin或TabbablePlugin,必须把 Copilot 放在它们之前,否则 Tab 行为会互相抢占。
Kit 路径下,补全接口走预配置的 API 路由(文档中的copilot-api组件模板);手动路径则需要自己创建路由并配置插件选项,见下一节。
配置 CopilotPlugin:接口、快捷键与幽灵文本组件
手动接法的核心是CopilotPlugin.configure,它决定了调哪个接口、多久自动触发一次、建议用哪个组件渲染、以及哪些快捷键生效:
import { CopilotPlugin } from '@platejs/ai/react'; import { serializeMd, stripMarkdown } from '@platejs/markdown'; import { GhostText } from '@/components/ui/ghost-text'; const plugins = [ // ...otherPlugins, MarkdownPlugin.configure({ options: { remarkPlugins: [remarkMath, remarkGfm, remarkMdx], }, }), CopilotPlugin.configure(({ api }) => ({ options: { completeOptions: { api: '/api/ai/copilot', onError: () => { // Mock the API response. Remove when you implement the route /api/ai/copilot api.copilot.setBlockSuggestion({ text: stripMarkdown('This is a mock suggestion.'), }); }, onFinish: (_, completion) => { if (completion === '0') return; api.copilot.setBlockSuggestion({ text: stripMarkdown(completion), }); }, }, debounceDelay: 500, renderGhostText: GhostText, }, shortcuts: { accept: { keys: 'tab' }, acceptNextWord: { keys: 'mod+right' }, reject: { keys: 'escape' }, triggerSuggestion: { keys: 'ctrl+space' }, }, })), ];各选项的用途:
completeOptions:对应 Vercel AI SDKuseCompletionhook 的配置。api指定补全接口地址;onError在请求失败时回调,文档用它在开发阶段 mock 一条建议(接口实现后应移除该 mock);onFinish拿到补全文本后调用api.copilot.setBlockSuggestion把它设为当前块的幽灵文本,completion === '0'表示模型认为无法续写,直接忽略。debounceDelay:自动触发前的防抖毫秒数,默认0(不防抖)。renderGhostText:渲染幽灵文本的 React 组件。仓库中的 ghost-text 组件实现 读取CopilotPlugin的isSuggested/suggestionText插件状态,无建议时返回null,有建议时输出一段pointer-events-none、contentEditable={false}的灰色 span,保证建议文本不可被光标选中或编辑。shortcuts:tab整段接受(对应 transformtf.copilot.accept())、mod+right逐词接受(tf.copilot.acceptNextWord())、escape拒绝并重置插件状态(api.copilot.reject())、ctrl+space手动触发一次建议请求(api.copilot.triggerSuggestion())。
自动触发(debatnce 模式)在段落末尾输入空格后生效;默认triggerQuery只检查两点:选区未展开、选区位于块末尾。autoTriggerQuery的默认条件是上一块非空、上一块以空格结尾、且当前没有已存在的建议。
添加服务端补全路由
Copilot 需要你自己的 API 路由中转模型请求。在app/api/ai/copilot/route.ts创建 POST 处理器:
import type { NextRequest } from 'next/server'; import { createGateway, generateText } from 'ai'; import { NextResponse } from 'next/server'; export async function POST(req: NextRequest) { const { apiKey: key, instructions, model = 'gpt-4o-mini', prompt, } = await req.json(); const apiKey = typeof key === 'string' ? key.trim() : ''; if (!apiKey) { return NextResponse.json( { error: 'Missing AI Gateway API key.' }, { status: 401 } ); } const gateway = createGateway({ apiKey }); try { const result = await generateText({ abortSignal: req.signal, instructions, maxOutputTokens: 50, model: gateway(`openai/${model}`), prompt, temperature: 0.7, }); return NextResponse.json(result); } catch (error) { if (error instanceof Error && error.name === 'AbortError') { return NextResponse.json(null, { status: 408 }); } return NextResponse.json( { error: 'Failed to process AI request' }, { status: 500 } ); } }这个路由从请求体里解出apiKey、instructions、model(默认gpt-4o-mini)和prompt,通过createGateway({ apiKey })创建 AI Gateway 客户端后调用generateText,maxOutputTokens限制为 50,temperature为 0.7。缺少 key 返回 401,客户端中断返回 408,其他错误返回 500。
密钥从哪来:BYOK(自带密钥)场景下,用户在编辑器设置里填入 AI Gateway key,浏览器把它作为completeOptions.body中的apiKey发给路由,仅本次请求使用。如果改用共享的应用级凭据,则应在服务端加载 key、对每个请求做认证鉴权和按用户的用量限制,并且绝不能把共享 key 写进客户端代码或请求体。
验证补全链路
文档给出的验证方式是分层的:
- 接口未实现时:配置中的
onError回调会 mock 一条This is a mock suggestion.(示例结果)。此时如果你敲空格或按Ctrl+Space后光标后面出现这段灰色文本,说明幽灵文本渲染链路(setBlockSuggestion→renderGhostText)已经通了。 - 接口实现后:移除
onError里的 mock。请求成功时onFinish收到补全文本,setBlockSuggestion将其(经stripMarkdown处理后)设为建议;模型返回"0"表示无续写,界面不出现幽灵文本。 - 交互验证:建议出现后按 Tab 应整段接受进正文;按
Cmd+→应只接受下一个词;按 Escape 应清除建议。
自定义:换模型、改触发条件
切换模型:模型在 API 路由侧配置,也可以让客户端通过body.model指定:
CopilotPlugin.configure(({ api }) => ({ options: { completeOptions: { api: '/api/ai/copilot', body: { instructions: 'Continue the current paragraph in the same tone.', model: 'anthropic/claude-3-haiku-20240307', }, }, // ... other options }, })),路由侧则把gateway('openai/' + model)改成gateway(model),让模型标识(含 provider 前缀)由请求体决定。更多 provider 与模型以 Vercel AI SDK 的文档为准。
改触发条件:用triggerQuery/autoTriggerQuery两个函数控制"何时允许触发"和"何时自动触发"。文档示例只在段落块(type !== 'p'时直接false)里触发,且自动触发要求选区在块尾、无展开选区:
triggerQuery: ({ editor }) => { // Only trigger in paragraph blocks const block = editor.api.block(); if (!block || block[0].type !== 'p') return false; return editor.selection && !editor.api.isExpanded() && editor.api.isAtEnd(); }, autoTriggerQuery: ({ editor }) => { const block = editor.api.block(); if (!block) return false; const text = editor.api.string(block[0]); // Trigger after question words return /\b(what|how|why|when|where)\s*$/i.test(text); },另外两个可调项:body.instructions定义 AI 的角色与行为(文档示例要求"续写到下一个标点为止、保持语气、不新起块、无法续写时返回0");getPrompt决定发送哪些上下文(默认取祖先节点的 Markdown 序列化,可用serializeMd自定义,例如只取最高层块并包在Continue the text up to the next punctuation mark:模板里)。
边界与限制
- Tab 冲突是最常见的坑:Copilot 用 Tab 接受建议,任何其他占用 Tab 的插件(
IndentPlugin、TabbablePlugin等)必须排在 CopilotKit/CopilotPlugin 之后。 maxOutputTokens: 50与示例路由中"续写到下一个标点"的 instructions 是配套的:补全被刻意限制得短小,而不是生成整段。- 文档建议对路由做输入校验(如 prompt 长度上限)、限流和内容过滤,但示例中的
rateLimit(req)与containsSensitiveContent(prompt)只是占位注释,需要你自行实现。 - 运行中如需中断,
api.copilot.stop()会取消防抖触发、abort 当前请求并重置 abort controller。
完整选项与 transform 定义见 Copilot 文档/(ai)/copilot.mdx),包括CopilotPlugin各选项(getPrompt默认值、triggerQuery默认值等)和setBlockSuggestion的参数说明(text必填,id可选,默认作用于当前块)。
【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考