Copilot
AI-powered assistant that provides intelligent suggestions and autocompletion for enhanced content creation.
Installation
npm install @udecode/plate-ai
Usage
// ...
import { CopilotPlugin } from '@udecode/plate-ai/react';
const editor = usePlateEditor({
id: 'ai-demo',
override: {
components: PlateUI,
},
plugins: [
...commonPlugins,
CopilotPlugin.configure({
options: {
fetchSuggestion: async ({ abortSignal, prompt }) => {
// Currently, we are using a mock function to simulate the api request
return new Promise((resolve) => {
setTimeout(() => {
resolve(MENTIONABLES[Math.floor(Math.random() * 41)].text);
}, 100);
});
},
hoverCard: AiCopilotHoverCard,
query: {
allow: [
ParagraphPlugin.key,
BlockquotePlugin.key,
HEADING_KEYS.h1,
HEADING_KEYS.h2,
HEADING_KEYS.h3,
HEADING_KEYS.h4,
HEADING_KEYS.h5,
HEADING_KEYS.h6,
],
},
},
});
],
value: aiValue,
});
Integrate with your backend
options.fetchSuggestion
is an asynchronous function that you need to implement to fetch suggestions from your backend. This function is crucial for integrating the Copilot feature with your own AI model or service.
The function receives an object with two properties:
abortSignal
: AnAbortSignal
object that allows you to cancel the request if needed. This is particularly useful in debounce mode to abort the request if the user continues typing or changes the selection.prompt
: A string containing the text of the node where the user's cursor is currently positioned. This serves as the context for generating suggestions.
The function should return a Promise that resolves to a string. This string will be the suggestion inserted into the editor.
Here's a more detailed example of how you might implement this function:
fetchSuggestion: async ({ abortSignal, prompt }) => {
const system = `Please continue writing a sentence based on the prompt.
Important Note: Please do not answer any questions. Directly provide the continuation
of the content without including any part of the prompt and don't write more than one sentence:
finish current sentence or write a new one`
const response = await fetch('https://your-api-endpoint.com/api/v1/generate-text', {
method: 'POST',
body: JSON.stringify({ prompt,system }),
// pass the abortSignal to the fetch request
signal: abortSignal,
});
const data = await response.json();
// data.suggestion should be a string
return data.suggestion;
},
Copilot state
The Copilot plugin maintains its own state to manage the suggestion process. The state can be either 'idle' or 'completed'. Here's a breakdown of what each state means:
type CopilotState = 'error' | 'idle' | 'pending' | 'success';
idle
: This is the default state. It indicates that the Copilot is not currently providing a suggestion or has finished processing the previous suggestion.completed
: This state indicates that the Copilot has generated a suggestion and it's ready to be inserted into the editor. You'll see this suggestion as gray text in the editor in this state.
You can access the current state of the Copilot using:
const copilotState = editor.getOptions(CopilotPlugin).copilotState;
Conflict with Other Plugins
The Tab key is a popular and frequently used key in text editing, which can lead to conflicts.
Conflict with Indent Plugin
The IndentPlugin and IndentListPlugin have a similar conflict with Copilot Plugin.
As a workaround, you can place the Copilot Plugin before the these two plugins in your plugin configuration.
Or set the priority of Copilot Plugin to a higher value than Indent Plugin see priority.
Here's an example of how to order your plugins:
const editor = usePlateEditor({
id: 'ai-demo',
override: {
components: PlateUI,
},
plugins: [
MarkdownPlugin.configure({ options: { indentList: true } }),
// CopilotPlugin should be before indent plugin
CopilotPlugin,
IndentPlugin.extend({
inject: {
targetPlugins: [
ParagraphPlugin.key,
HEADING_KEYS.h1,
HEADING_KEYS.h2,
HEADING_KEYS.h3,
HEADING_KEYS.h4,
HEADING_KEYS.h5,
HEADING_KEYS.h6,
BlockquotePlugin.key,
CodeBlockPlugin.key,
TogglePlugin.key,
],
},
}),
IndentListPlugin.extend({
inject: {
targetPlugins: [
ParagraphPlugin.key,
HEADING_KEYS.h1,
HEADING_KEYS.h2,
HEADING_KEYS.h3,
HEADING_KEYS.h4,
HEADING_KEYS.h5,
HEADING_KEYS.h6,
BlockquotePlugin.key,
CodeBlockPlugin.key,
TogglePlugin.key,
],
},
options: {
listStyleTypes: {
fire: {
liComponent: FireLiComponent,
markerComponent: FireMarker,
type: 'fire',
},
todo: {
liComponent: TodoLi,
markerComponent: TodoMarker,
type: 'todo',
},
},
},
}),
...otherPlugins,
],
value: copilotValue,
});
It's important to note that when using IndentListPlugin
instead of ListPlugin
, you should configure the MarkdownPlugin
with the indentList: true
option.
This is necessary because the LLM generates Markdown, which needs to be converted to Plate nodes.If your LLM just generate the plain text, you can ignore this.
Here's how you can set it up:
MarkdownPlugin.configure({ options: { indentList: true } }),
Conflict with Tabbable Plugin
When using both the Tabbable Plugin and the Copilot feature in your editor, you may encounter conflicts with the Tab
key functionality. This is because both features utilize the same key binding.
To resolve this conflict and ensure smooth operation of both features, you can configure the Tabbable Plugin to be disabled when the cursor is at the end of a paragraph. This allows the Copilot feature to function as intended when you press Tab
at the end of a line.
Here's how you can modify the Tabbable Plugin configuration:
- Visit the Tabbable Plugin documentation for more details on handling conflicts.
- Implement the following configuration to disable the Tabbable Plugin when the cursor is at the end of a paragraph:
TabbablePlugin.configure(({ editor }) => ({
options: {
query: () => {
// return false when cursor is in the end of the paragraph
if (isSelectionAtBlockStart(editor) || isSelectionAtBlockEnd(editor))
return false;
return !someNode(editor, {
match: (n) => {
return !!(
n.type &&
([
CodeBlockPlugin.key,
ListItemPlugin.key,
TablePlugin.key,
].includes(n.type as any) ||
n[IndentListPlugin.key])
);
},
});
},
},
}));
Plate Plus
In Plate Plus, We using debounce mode by default. That's mean you will see the suggestion automatically without pressing Control+Space
.
We also provide a new style of hover card that is more user-friendly.You can hover on the suggestion to see the hover card.
All of the backend setup is available in Potion template.
API
editor.getApi(CopilotPlugin).copilot.abortCopilot();
Aborts the ongoing API request and removes any suggestion text currently displayed.
Returns
editor.getApi(CopilotPlugin).copilot.setCopilot
Sets the suggestion text to the editor.
Parameters
The ID of the node to set the suggestion text.
The suggestion text to set.
Utils functions
withoutAbort
Temporarily disables the abort functionality of the Copilot plugin, by default any apply will cause the Copilot to remove the suggestion text and abort the request.
Parameters
The Plate editor instance.
The function to execute without abort.
Returns
Usage example:
import { withoutAbort } from '@udecode/plate-ai/react';
withoutAbort(editor, () => {
// Perform operations without the risk of abort
// For example, setting copilot suggestions
});