âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ â ð shadcn/directory/udecode/plate/(plugins)/(ai)/copilot.cn â âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â
title: Copilot description: AI 驱åšçææ¬è¡¥å šå»ºè®®ã docs:
Ctrl+SpaceïŒã忬¡æäžå¯è·åæ¿ä»£å»ºè®®ãCmd+â éè¯æ¥åæ·»å Copilot åèœæå¿«çæ¹åŒæ¯äœ¿çš CopilotKitïŒå®å
å«é¢é
眮ç CopilotPlugin 以å MarkdownKit åå®ä»¬ç Plate UI ç»ä»¶ã
GhostText: æž²æå¹œçµææ¬å»ºè®®ãimport { createPlateEditor } from 'platejs/react';
import { CopilotKit } from '@/components/editor/plugins/copilot-kit';
const editor = createPlateEditor({
plugins: [
// ...å
¶ä»æä»¶,
...CopilotKit,
// å°äœ¿çš Tab é®çæä»¶æŸåš CopilotKit ä¹å以é¿å
å²çª
// IndentPlugin,
// TabbablePlugin,
],
});
Tab é®å€ç: Copilot æä»¶äœ¿çš Tab 鮿¥æ¥å建议ã䞺é¿å
äžå
¶ä»äœ¿çš Tab çæä»¶ïŒåŠ IndentPlugin æ TabbablePluginïŒå²çªïŒè¯·ç¡®ä¿ CopilotKit åšæä»¶é
眮äžäœäºå®ä»¬ä¹åã
Copilot éèŠäžäžªæå¡åšç«¯ API ç«¯ç¹æ¥äž AI æš¡åéä¿¡ãæ·»å é¢é 眮ç Copilot API è·¯ç±ïŒ
<ComponentSource name="copilot-api" />ç¡®ä¿æšç OpenAI API å¯é¥å·²è®Ÿçœ®åšç¯å¢åéäžïŒ
OPENAI_API_KEY="æšç-api-å¯é¥"
</Steps>
npm install @platejs/ai @platejs/markdown
import { CopilotPlugin } from '@platejs/ai/react';
import { MarkdownPlugin } from '@platejs/markdown';
import { createPlateEditor } from 'platejs/react';
const editor = createPlateEditor({
plugins: [
// ...å
¶ä»æä»¶,
MarkdownPlugin,
CopilotPlugin,
// å°äœ¿çš Tab é®çæä»¶æŸåš CopilotPlugin ä¹å以é¿å
å²çª
// IndentPlugin,
// TabbablePlugin,
],
});
MarkdownPlugin: çšäºå°çŒèŸåšå
容åºåå䞺æç€ºè¯åéãCopilotPlugin: å¯çš AI 驱åšçææ¬è¡¥å
šãTab é®å€ç: Copilot æä»¶äœ¿çš Tab 鮿¥æ¥å建议ã䞺é¿å
äžå
¶ä»äœ¿çš Tab çæä»¶ïŒåŠ IndentPlugin æ TabbablePluginïŒå²çªïŒè¯·ç¡®ä¿ CopilotPlugin åšæä»¶é
眮äžäœäºå®ä»¬ä¹åã
import { CopilotPlugin } from '@platejs/ai/react';
import { serializeMd, stripMarkdown } from '@platejs/markdown';
import { GhostText } from '@/components/ui/ghost-text';
const plugins = [
// ...å
¶ä»æä»¶,
MarkdownPlugin.configure({
options: {
remarkPlugins: [remarkMath, remarkGfm, remarkMdx],
},
}),
CopilotPlugin.configure(({ api }) => ({
options: {
completeOptions: {
api: '/api/ai/copilot',
onError: () => {
// æš¡æ API ååºãåšå®ç°è·¯ç± /api/ai/copilot åç§»é€
api.copilot.setBlockSuggestion({
text: stripMarkdown('è¿æ¯äžäžªæš¡æå»ºè®®ã'),
});
},
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 SDK useCompletion é©åã
api: AI è¡¥å
šè·¯ç±ç端ç¹ãonError: å€çé误çåè°ïŒçšäºåŒåæéŽçæš¡æïŒãonFinish: å€çå®æå»ºè®®çåè°ãæ€å€å°å»ºè®®è®Ÿçœ®å°çŒèŸåšäžãdebounceDelay: çšæ·åæ¢èŸå
¥åèªåšè§Šå建议çå»¶è¿æ¶éŽïŒæ¯«ç§ïŒãrenderGhostText: çšäºå
èæŸç€ºå»ºè®®ç React ç»ä»¶ãshortcuts: å®ä¹äž Copilot 建议亀äºçé®çå¿«æ·é®ãåš app/api/ai/copilot/route.ts å建 API è·¯ç±å€ççšåºæ¥å€ç AI 请æ±ãæ€ç«¯ç¹å°æ¥æ¶æ¥èªçŒèŸåšçæç€ºè¯å¹¶è°çš AI æš¡åã
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: 'çŒºå° OpenAI API å¯é¥ã' },
{ 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: 'å€ç AI 请æ±å€±èŽ¥' },
{ status: 500 }
);
}
}
ç¶åïŒåš .env.local äžè®Ÿçœ®æšç OPENAI_API_KEYã
ç³»ç»æç€ºè¯å®ä¹äº AI çè§è²åè¡äžºãä¿®æ¹ completeOptions äžç body.system 屿§ïŒ
CopilotPlugin.configure(({ api }) => ({
options: {
completeOptions: {
api: '/api/ai/copilot',
body: {
system: {
system: `æšæ¯äžäžªé«çº§ AI åäœå©æïŒç±»äŒŒäº VSCode CopilotïŒäœéçšäºéçšææ¬ãæšç任塿¯æ ¹æ®ç»å®äžäžæé¢æµå¹¶çæææ¬çäžäžéšåã
è§åïŒ
- èªç¶å°ç»§ç»ææ¬çŽå°äžäžäžªæ ç¹ç¬Šå·ïŒ., ,, ;, :, ? æ !ïŒã
- ä¿æé£æ Œåè¯æ°ãäžèŠéå€ç»å®ææ¬ã
- 对äºäžæç¡®çäžäžæïŒæäŸæå¯èœçå»¶ç»ã
- åŠæéèŠïŒå€ç代ç çæ®µãå衚æç»æåææ¬ã
- äžèŠåšååºäžå
å« """ã
- å
³é®ïŒå§ç»ä»¥æ ç¹ç¬Šå·ç»å°Ÿã
- å
³é®ïŒé¿å
åŒå§æ°åãäžèŠäœ¿çšåæ ŒåŒååŠ >, #, 1., 2., - çã建议åºç»§ç»åšäžäžäžæçžåçåäžã
- åŠææªæäŸäžäžæææ æ³çæå»¶ç»ïŒè¿å "0" èäžè§£éã`,
},
},
// ... å
¶ä»é项
},
// ... å
¶ä»æä»¶é项
},
})),
çšæ·æç€ºè¯ïŒéè¿ getPromptïŒå³å®åéç» AI çäžäžæå
å®¹ãæšå¯ä»¥èªå®ä¹å®ä»¥å
嫿Žå€äžäžææä»¥äžåæ¹åŒæ ŒåŒåïŒ
CopilotPlugin.configure(({ api }) => ({
options: {
getPrompt: ({ editor }) => {
const contextEntry = editor.api.block({ highest: true });
if (!contextEntry) return '';
const prompt = serializeMd(editor, {
value: [contextEntry[0] as TElement],
});
return `ç»§ç»ææ¬çŽå°äžäžäžªæ ç¹ç¬Šå·ïŒ
"""
${prompt}
"""`;
},
// ... å
¶ä»é项
},
})),
</Steps>
åš API è·¯ç±äžé 眮äžåç AI æš¡åå providerïŒ
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);
}
åš CopilotPlugin äžé
眮暡åïŒ
CopilotPlugin.configure(({ api }) => ({
options: {
completeOptions: {
api: '/api/ai/copilot',
body: {
model: 'claude-3-haiku-20240307', // çšäºè¡¥å
šçå¿«éæš¡å
provider: 'anthropic',
system: 'æšçç³»ç»æç€ºè¯...',
},
},
// ... å
¶ä»é项
},
})),
æŽå€ AI provider åæš¡åïŒè¯·åé Vercel AI SDK ææ¡£ã
æ§å¶äœæ¶èªåšè§Šå建议ïŒ
CopilotPlugin.configure(({ api }) => ({
options: {
triggerQuery: ({ editor }) => {
// ä»
åšæ®µèœåäžè§Šå
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]);
// åšçé®è¯åè§Šå
return /\b(what|how|why|when|where)\s*$/i.test(text);
},
// ... å
¶ä»é项
},
})),
䞺 Copilot API 宿œå®å šæäœ³å®è·µïŒ
export async function POST(req: NextRequest) {
const { prompt, system } = await req.json();
// éªè¯æç€ºè¯é¿åºŠ
if (!prompt || prompt.length > 1000) {
return NextResponse.json({ error: 'æ ææç€ºè¯' }, { status: 400 });
}
// éçéå¶ïŒäœ¿çšæšå奜çè§£å³æ¹æ¡å®ç°ïŒ
// await rateLimit(req);
// ææå
å®¹è¿æ»€
if (containsSensitiveContent(prompt)) {
return NextResponse.json({ error: 'å
å®¹è¢«è¿æ»€' }, { status: 400 });
}
// å€ç AI 请æ±...
}
å®å šæå:
CopilotPluginçšäº AI 驱åšçææ¬è¡¥å šå»ºè®®çæä»¶ã
<API name="CopilotPlugin"> <APIOptions> <APIItem name="autoTriggerQuery" type="(options: { editor: PlateEditor }) => boolean" optional> èªåšè§Šå copilot çéå æ¡ä»¶ã - **é»è®€:** æ£æ¥ïŒ - äžæ¹åäžäžºç©º - äžæ¹åä»¥ç©ºæ Œç»å°Ÿ - æ ç°æå»ºè®® </APIItem> <APIItem name="completeOptions" type="Partial<CompleteOptions>"> AI è¡¥å šé 眮é项ãåè§ [AI SDK useCompletion åæ°](https://sdk.vercel.ai/docs/reference/ai-sdk-ui/use-completion#parameters)ã </APIItem> <APIItem name="debounceDelay" type="number" optional> èªåšè§Šå建议ç鲿延è¿ã - **é»è®€:** `0` </APIItem> <APIItem name="getNextWord" type="(options: { text: string }) => { firstWord: string; remainingText: string }" optional> ä»å»ºè®®ææ¬äžæåäžäžäžªåè¯çåœæ°ã </APIItem> <APIItem name="getPrompt" type="(options: { editor: PlateEditor }) => string" optional> çæ AI è¡¥å šæç€ºè¯çåœæ°ã - **é»è®€:** 䜿çšç¥å èç¹ç markdown åºåå </APIItem> <APIItem name="renderGhostText" type="(() => React.ReactNode) | null" optional> æž²æå¹œçµææ¬å»ºè®®çç»ä»¶ã </APIItem> <APIItem name="triggerQuery" type="(options: { editor: PlateEditor }) => boolean" optional> è§Šå copilot çæ¡ä»¶ã - **é»è®€:** æ£æ¥ïŒ - éæ©æªå±åŒ - éæ©åšåæ«å°Ÿ </APIItem> </APIOptions> </API>tf.copilot.accept()æ¥ååœå建议并å°å ¶åºçšå°çŒèŸåšå 容äžã
é»è®€å¿«æ·é®: Tab
tf.copilot.acceptNextWord()ä» æ¥ååœå建议çäžäžäžªåè¯ïŒå è®žéæ¥æ¥å建议ã
瀺äŸå¿«æ·é®: Cmd + â
api.copilot.reject()å°æä»¶ç¶æé眮䞺åå§æ¡ä»¶ïŒ
é»è®€å¿«æ·é®: Escape
api.copilot.triggerSuggestion()è§Šåæ°ç建议请æ±ã请æ±å¯èœäŒæ ¹æ®æä»¶é 眮è¿è¡é²æã
瀺äŸå¿«æ·é®: Ctrl + Space
api.copilot.setBlockSuggestion()䞺åè®Ÿçœ®å»ºè®®ææ¬ã
<API name="setBlockSuggestion"> <APIParameters> <APIItem name="options" type="SetBlockSuggestionOptions"> 讟眮å建议çé项ã </APIItem> </APIParameters> <APIOptions type="SetBlockSuggestionOptions"> <APIItem name="text" type="string"> èŠè®Ÿçœ®çå»ºè®®ææ¬ã </APIItem> <APIItem name="id" type="string" optional> ç®æ å IDã - **é»è®€:** åœåå </APIItem> </APIOptions> </API>api.copilot.stop()忢æ£åšè¿è¡ç建议请æ±å¹¶æž çïŒ
â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â â
ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ