Ctrl+Space). Press again for alternative suggestions.Cmd+→The fastest way to add Copilot functionality is with the CopilotKit, which includes pre-configured CopilotPlugin along with MarkdownKit and their Plate UI components.
'use client';
import { faker } from '@faker-js/faker';
import { CopilotPlugin } from '@platejs/ai/react';
import { stripMarkdown } from '@platejs/markdown';
import type { Element } from '@platejs/plite';
import { useEditor, useElement, usePluginStore } from 'platejs/react';
import * as React from 'react';
export function GhostText() {
const editor = useEditor();
const element = useElement();
const isSuggested = usePluginStore(
CopilotPlugin,
'isSuggested',
editor.key(element)
);
if (!isSuggested) return null;
return <GhostTextContent />;
}
function GhostTextContent() {
const suggestionText = usePluginStore(CopilotPlugin, 'suggestionText');
return (
<span
className="pointer-events-none text-muted-foreground/70 max-sm:hidden"
contentEditable={false}
>
{suggestionText}
</span>
);
}
export const CopilotKit = [
CopilotPlugin.configure(({ update }) => ({
initialState: {
completeOptions: {
api: '/api/ai/copilot',
body: {
system: `You are an advanced AI writing assistant, similar to VSCode Copilot but for general text. Your task is to predict and generate the next part of the text based on the given context.
Rules:
- Continue the text naturally up to the next punctuation mark (., ,, ;, :, ?, or !).
- Maintain style and tone. Don't repeat given text.
- For unclear context, provide the most likely continuation.
- Handle code snippets, lists, or structured text if needed.
- Don't include """ in your response.
- CRITICAL: Always end with a punctuation mark.
- CRITICAL: Avoid starting a new block. Do not use block formatting like >, #, 1., 2., -, etc. The suggestion should continue in the same block as the context.
- If no context is provided or you can't generate a continuation, return "0" without explanation.`,
},
onError: () => {
// Mock the API response. Remove it when you implement the route /api/ai/copilot
update.setBlockSuggestion({
text: stripMarkdown(faker.lorem.sentence()),
});
},
onFinish: (_, completion) => {
if (completion === '0') return;
update.setBlockSuggestion({
text: stripMarkdown(completion),
});
},
},
debounceDelay: 500,
renderGhostText: GhostText,
getPrompt: ({ editor }) => {
const contextEntry = editor.read.nodes.block();
if (!contextEntry) return '';
const prompt = editor.api.markdown.serialize({
value: { children: [contextEntry[0] as Element] },
});
return `Continue the text up to the next punctuation mark:
"""
${prompt}
"""`;
},
},
shortcuts: {
accept: {
keys: 'tab',
},
acceptNextWord: {
keys: 'mod+right',
},
reject: {
keys: 'escape',
},
triggerSuggestion: {
keys: 'ctrl+space',
},
},
})),
];'use client';
import { faker } from '@faker-js/faker';
import { CopilotPlugin } from '@platejs/ai/react';
import { stripMarkdown } from '@platejs/markdown';
import type { Element } from '@platejs/plite';
import { useEditor, useElement, usePluginStore } from 'platejs/react';
import * as React from 'react';
export function GhostText() {
const editor = useEditor();
const element = useElement();
const isSuggested = usePluginStore(
GhostText: Renders the ghost text suggestions.import { createPlateEditor } from 'platejs/react';
import { CopilotKit } from '@/components/editor/copilot';
const editor = createPlateEditor({
plugins: [
// ...otherPlugins,
...CopilotKit,
// Place tab-using plugins after CopilotKit to avoid conflicts
// IndentPlugin,
// TabbablePlugin,
],
});import { createPlateEditor } from 'platejs/react';
import
Tab Key Handling: The Copilot plugin uses the Tab key to accept suggestions. To avoid conflicts with other plugins that use Tab (like IndentPlugin or TabbablePlugin), ensure CopilotKit is placed before them in your plugin configuration.
Copilot requires a server-side API endpoint to communicate with the AI model. Add the pre-configured Copilot API route:
import { generateText } from 'ai';
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
export async function POST(req: NextRequest) {
const {
apiKey: key,
model = 'gpt-4o-mini',
prompt,
system,
} = await req.json();
const apiKey = key || process.env.AI_GATEWAY_API_KEY;
if (
Ensure your OpenAI API key is set in your environment variables:
OPENAI_API_KEY="your-api-key"OPENAI_API_KEY="your-api-key"import { CopilotPlugin } from '@platejs/ai/react';
import { MarkdownPlugin } from '@platejs/markdown';
import { createPlateEditor } from 'platejs/react';
const editor = createPlateEditor({
plugins: [
// ...otherPlugins,
MarkdownPlugin,
CopilotPlugin,
// Place tab-using plugins after CopilotPlugin to avoid conflicts
// IndentPlugin,
// TabbablePlugin,
],
});import { CopilotPlugin } from '@platejs/ai/react';
import { MarkdownPlugin } from '@platejs/markdown';
import { createPlateEditor } from 'platejs/react';
const editor = createPlateEditor({
plugins: [
// ...otherPlugins,
MarkdownPlugin,
CopilotPlugin,
// Place tab-using plugins after CopilotPlugin to avoid conflicts
// IndentPlugin,
// TabbablePlugin,
],
});MarkdownPlugin: Required for serializing editor content to send as a prompt.CopilotPlugin: Enables AI-powered text completion.Tab Key Handling: The Copilot plugin uses the Tab key to accept suggestions. To avoid conflicts with other plugins that use Tab (like IndentPlugin or TabbablePlugin), ensure CopilotPlugin is placed before them in your plugin configuration.
import { CopilotPlugin } from '@platejs/ai/react';
import {
MarkdownPlugin,
stripMarkdown,
} from '@platejs/markdown';
import { GhostText } from '@/components/editor/copilot';
const plugins = [
// ...otherPlugins,
MarkdownPlugin.configure({
initialState: {
remarkPlugins: [remarkMath, remarkGfm, remarkMdx],
},
}),
CopilotPlugin.configure(({ update }) => ({
initialState: {
completeOptions: {
api: '/api/ai/copilot',
onError: () => {
completeOptions: Configures the completion transport.
api: Required endpoint for your AI completion route.onError: A callback for handling errors (used for mocking during development).onFinish: A callback to handle the completed suggestion. Here, it sets the suggestion in the editor.debounceDelay: The delay in milliseconds for auto-triggering suggestions after the user stops typing.renderGhostText: The React component used to display the suggestion inline.shortcuts: Defines keyboard shortcuts for interacting with Copilot suggestions.Create an API route handler at app/api/ai/copilot/route.ts to process AI requests. This endpoint will receive the prompt from the editor and call the AI model.
import type { NextRequest } from 'next/server';
import { createOpenAI } from '@ai-sdk/openai';
import { generateText } from 'ai';
import { NextResponse } from 'next/server';
export async function POST(req: NextRequest) {
const {
apiKey: key,
model = 'gpt-4o-mini',
prompt,
system,
} = await req.json();
const apiKey
Then, set your OPENAI_API_KEY in .env.local.
The system prompt defines the AI's role and behavior. Modify the body.system property in completeOptions:
CopilotPlugin.configure({
initialState: {
completeOptions: {
api: '/api/ai/copilot',
body: {
system: `You are an advanced AI writing assistant, similar to VSCode Copilot but for general text. Your task is to predict and generate the next part of the text based on the given context.
Rules:
- Continue the text naturally up to the next punctuation mark (., ,, ;, :, ?, or !).
- Maintain style and tone. Don't repeat given text.
- For unclear context, provide the most likely continuation.
- Handle code snippets, lists, or structured text if needed.
- Don't include """ in your response.
- CRITICAL: Always end with a punctuation mark.
- CRITICAL: Avoid starting a new block. Do not use block formatting like >, #, 1., 2., -, etc. The suggestion should continue in the same block as the context.
- If no context is provided or you can't generate a continuation, return "0" without explanation.`,
},
// ... other options
},
// ... other plugin state
},
});The user prompt (via getPrompt) determines what context is sent to the AI. You can customize it to include more context or format it differently:
CopilotPlugin.configure({
initialState: {
getPrompt: ({ editor }) => {
const contextEntry = editor.read.nodes.block({ mode: 'highest' });
if (!contextEntry) return '';
const prompt = editor.api.markdown.serialize({
value: { children: [contextEntry[0]] },
});
return `Continue the text up to the next punctuation mark:
"""
${prompt}
"""`;
},
// ... other options
},
});Configure different AI models and providers in your API route:
import { createOpenAI } from '@ai-sdk/openai';
import { createAnthropic } from '@ai-sdk/anthropic';
export async function POST(req: NextRequest) {
const {
model = 'gpt-4o-mini',
provider = 'openai',
prompt,
system
} = await req.json();
let aiProvider;
switch (provider) {
case 'anthropic':
aiProvider = createAnthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
break;
case 'openai':
default:
aiProvider = createOpenAI({ apiKey: process.env.OPENAI_API_KEY });
break;
}
const result = await generateText({
model: aiProvider(model),
prompt,
system,
maxTokens: 50,
temperature: 0.7,
});
return NextResponse.json(result);
}import { createOpenAI } from '@ai-sdk/openai';
import { createAnthropic } from '@ai-sdk/anthropic';
export async function POST(req: NextRequest) {
const {
model = 'gpt-4o-mini',
provider = 'openai',
prompt,
system
} = await req.json();
let aiProvider;
switch (provider) {
case 'anthropic':
aiProvider = createAnthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
break
Configure the model in your CopilotPlugin:
CopilotPlugin.configure({
initialState: {
completeOptions: {
api: '/api/ai/copilot',
body: {
model: 'claude-3-haiku-20240307', // Fast model for completions
provider: 'anthropic',
system: 'Your system prompt here...',
},
},
// ... other options
},
});CopilotPlugin.configure({
initialState: {
completeOptions: {
api: '/api/ai/copilot',
body: {
model: 'claude-3-haiku-20240307', // Fast model for completions
provider: 'anthropic',
system: 'Your system prompt here...',
},
},
// ... other options
},
});For more AI providers and models, see the Vercel AI SDK documentation.
Control when suggestions are automatically triggered:
import { PLUGINS } from 'platejs';
CopilotPlugin.configure({
initialState: {
triggerQuery: ({ editor }) => {
// Only trigger in paragraph blocks
const block = editor.read.nodes.block();
const paragraph = editor.plugin(PLUGINS.paragraph);
if (!block || block[0].type !== paragraph.schema.type) return false;
// Standard checks
return (
editor.read.selection.isCollapsed() &&
editor.read.selection.isAtBlockEnd()
);
},
autoTriggerQuery: ({ editor }) => {
// Custom conditions for auto-triggering
const block = editor.read.nodes.block();
if (!block) return false;
const text = editor.read.text.string(block[1]);
// Trigger after question words
return /\b(what|how|why|when|where)\s*$/i.test(text);
},
// ... other options
},
});import { PLUGINS } from 'platejs';
CopilotPlugin.configure({
initialState: {
triggerQuery: ({ editor }) => {
// Only trigger in paragraph blocks
const block = editor.read.nodes.block();
const paragraph = editor.plugin(PLUGINS.paragraph);
if (!block || block[0].type !== paragraph.schema.type) return false;
// Standard checks
return (
editor.read.selection.isCollapsed() &&
editor.read.selection.isAtBlockEnd()
Implement security best practices for Copilot API:
export async function POST(req: NextRequest) {
const { prompt, system } = await req.json();
// Validate prompt length
if (!prompt || prompt.length > 1000) {
return NextResponse.json({ error: 'Invalid prompt' }, { status: 400 });
}
// Rate limiting (implement with your preferred solution)
// await rateLimit(req);
// Content filtering for sensitive content
if (containsSensitiveContent(prompt)) {
return NextResponse.json({ error: 'Content filtered' }, { status: 400 });
}
// Process AI request...
}export async function POST(req: NextRequest) {
const { prompt, system } = await req.json();
// Validate prompt length
if (!prompt || prompt.length > 1000) {
return NextResponse.json({ error: 'Invalid prompt' }, { status: 400 });
}
// Rate limiting (implement with your preferred solution)
// await rateLimit(req);
// Content filtering for sensitive content
if (containsSensitiveContent(prompt)) {
return NextResponse.json({ error: 'Content filtered' }, { status: 400
Security Guidelines:
Plugin for AI-powered text completion suggestions.
Additional conditions to auto trigger copilot.
AI completion transport. Configure an explicit api before triggering a completion.
Delay for debouncing auto-triggered suggestions.
0Function to extract the next word from suggestion text.
Function to generate the prompt for AI completion.
Component to render ghost text suggestions.
Conditions to trigger copilot.
Accepts the current suggestion and applies it to the editor content.
Default Shortcut: Tab
Accepts only the next word of the current suggestion, allowing for granular acceptance of suggestions.
Example Shortcut: Cmd + →
Resets the plugin state to its initial condition:
Default Shortcut: Escape
Triggers a new suggestion request. The request may be debounced based on the plugin configuration.
Example Shortcut: Ctrl + Space
Sets suggestion text for a block.
Stops ongoing suggestion requests and cleans up:
import { generateText } from 'ai';
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
export async function POST(req: NextRequest) {
const {
apiKey: key,
model = 'gpt-4o-mini',
prompt,
system,
} = await req.json();
const apiKey = key || process.env.AI_GATEWAY_API_KEY;
if (!apiKey) {
return NextResponse.json(
{ error: 'Missing ai gateway API key.' },
{ status: 401 }
);
}
try {
const result = await generateText({
abortSignal: req.signal,
maxOutputTokens: 50,
model: `openai/${model}`,
prompt,
system,
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 }
);
}
}import { CopilotPlugin } from '@platejs/ai/react';
import {
MarkdownPlugin,
stripMarkdown,
} from '@platejs/markdown';
import { GhostText } from '@/components/editor/copilot';
const plugins = [
// ...otherPlugins,
MarkdownPlugin.configure({
initialState: {
remarkPlugins: [remarkMath, remarkGfm, remarkMdx],
},
}),
CopilotPlugin.configure(({ update }) => ({
initialState: {
completeOptions: {
api: '/api/ai/copilot',
onError: () => {
// Mock the API response. Remove when you implement the route /api/ai/copilot
update.setBlockSuggestion({
text: stripMarkdown('This is a mock suggestion.'),
});
},
onFinish: (_, completion) => {
if (completion === '0') return;
update.setBlockSuggestion({
text: stripMarkdown(completion),
});
},
},
debounceDelay: 500,
renderGhostText: GhostText,
},
shortcuts: {
accept: { keys: 'tab' },
acceptNextWord: { keys: 'mod+right' },
reject: { keys: 'escape' },
triggerSuggestion: { keys: 'ctrl+space' },
},
})),
];import type { NextRequest } from 'next/server';
import { createOpenAI } from '@ai-sdk/openai';
import { generateText } from 'ai';
import { NextResponse } from 'next/server';
export async function POST(req: NextRequest) {
const {
apiKey: key,
model = 'gpt-4o-mini',
prompt,
system,
} = await req.json();
const apiKey = key || process.env.OPENAI_API_KEY;
if (!apiKey) {
return NextResponse.json(
{ error: 'Missing OpenAI API key.' },
{ status: 401 }
);
}
const openai = createOpenAI({ apiKey });
try {
const result = await generateText({
abortSignal: req.signal,
maxTokens: 50,
model: openai(model),
prompt: prompt,
system,
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 }
);
}
}CopilotPlugin.configure({
initialState: {
completeOptions: {
api: '/api/ai/copilot',
body: {
system: `You are an advanced AI writing assistant, similar to VSCode Copilot but for general text. Your task is to predict and generate the next part of the text based on the given context.
Rules:
- Continue the text naturally up to the next punctuation mark (., ,, ;, :, ?, or !).
- Maintain style and tone. Don't repeat given text.
- For unclear context, provide the most likely continuation.
- Handle code snippets, lists, or structured text if needed.
- Don't include """ in your response.
- CRITICAL: Always end with a punctuation mark.
- CRITICAL: Avoid starting a new block. Do not use block formatting like >, #, 1., 2., -, etc. The suggestion should continue in the same block as the context.
- If no context is provided or you can't generate a continuation, return "0" without explanation.`,
},
// ... other options
},
// ... other plugin state
},
});CopilotPlugin.configure({
initialState: {
getPrompt: ({ editor }) => {
const contextEntry = editor.read.nodes.block({ mode: 'highest' });
if (!contextEntry) return '';
const prompt = editor.api.markdown.serialize({
value: { children: [contextEntry[0]] },
});
return `Continue the text up to the next punctuation mark:
"""
${prompt}
"""`;
},
// ... other options
},
});