AI

AI menu with commands, streaming responses in a preview or directly into the editor.

Loading...
Files
components/demo.tsx
'use client';

import React from 'react';

import { Plate } from '@udecode/plate/react';

import { editorPlugins } from '@/components/editor/plugins/editor-plugins';
import { useCreateEditor } from '@/components/editor/use-create-editor';
import { Editor, EditorContainer } from '@/components/plate-ui/editor';

import { DEMO_VALUES } from './values/demo-values';

export default function Demo({ id }: { id: string }) {
  const editor = useCreateEditor({
    plugins: [...editorPlugins],
    value: DEMO_VALUES[id],
  });

  return (
    <Plate editor={editor}>
      <EditorContainer variant="demo">
        <Editor />
      </EditorContainer>
    </Plate>
  );
}

Features

  • Combobox menu with predefined commands:
    • Generate: continue writing, add summary, explain
    • Edit: improve writing, emojify, make it longer or shorter, fix spelling & grammar, simplify language
  • Three trigger modes:
    • Cursor mode: trigger at block end
    • Selection mode: trigger with selected text
    • Block selection mode: trigger with selected blocks
  • Streaming responses in preview or direct editor insertion
  • Markdown support
  • Built-in support for Vercel AI SDK chat API

Installation

pnpm add @udecode/plate-ai @udecode/plate-selection @udecode/plate-markdown @udecode/plate-basic-marks

Usage

Plugins

import { AIChatPlugin, AIPlugin } from '@udecode/plate-ai/react';
import {
  BaseBoldPlugin,
  BaseCodePlugin,
  BaseItalicPlugin,
  BaseStrikethroughPlugin,
  BaseUnderlinePlugin,
} from '@udecode/plate-basic-marks';
import { BaseBlockquotePlugin } from '@udecode/plate-block-quote';
import {
  BaseCodeBlockPlugin,
  BaseCodeLinePlugin,
  BaseCodeSyntaxPlugin,
} from '@udecode/plate-code-block';
import { BaseParagraphPlugin, createSlateEditor } from '@udecode/plate';
import { BaseHeadingPlugin, HEADING_LEVELS } from '@udecode/plate-heading';
import { BaseHorizontalRulePlugin } from '@udecode/plate-horizontal-rule';
import { BaseIndentListPlugin } from '@udecode/plate-indent-list';
import { BaseLinkPlugin } from '@udecode/plate-link';
import { MarkdownPlugin } from '@udecode/plate-markdown';
export const createAIEditor = () => {
  const editor = createSlateEditor({
    id: 'ai',
    plugins: [
      BaseBlockquotePlugin,
      BaseBoldPlugin,
      BaseCodeBlockPlugin,
      BaseCodeLinePlugin,
      BaseCodePlugin,
      BaseCodeSyntaxPlugin,
      BaseItalicPlugin,
      BaseStrikethroughPlugin,
      BaseUnderlinePlugin,
      BaseHeadingPlugin,
      BaseHorizontalRulePlugin,
      BaseLinkPlugin,
      BaseParagraphPlugin,
      BaseIndentListPlugin.extend({
        inject: {
          targetPlugins: [
            BaseParagraphPlugin.key,
            ...HEADING_LEVELS,
            BaseBlockquotePlugin.key,
            BaseCodeBlockPlugin.key,
          ],
        },
        options: {
          listStyleTypes: {
            todo: {
              liComponent: TodoLiStatic,
              markerComponent: TodoMarkerStatic,
              type: 'todo',
            },
          },
        },
      }),
      MarkdownPlugin.configure({
        options: {
          remarkPlugins: [remarkMath, remarkGfm, remarkMdx],
        },
      }),
    ],
  });
 
 
  return editor;
};
 
const systemCommon = `\
You are an advanced AI-powered note-taking assistant, designed to enhance productivity and creativity in note management.
Respond directly to user prompts with clear, concise, and relevant content. Maintain a neutral, helpful tone.
 
Rules:
- <Document> is the entire note the user is working on.
- <Reminder> is a reminder of how you should reply to INSTRUCTIONS. It does not apply to questions.
- Anything else is the user prompt.
- Your response should be tailored to the user's prompt, providing precise assistance to optimize note management.
- For INSTRUCTIONS: Follow the <Reminder> exactly. Provide ONLY the content to be inserted or replaced. No explanations or comments.
- For QUESTIONS: Provide a helpful and concise answer. You may include brief explanations if necessary.
- CRITICAL: Distinguish between INSTRUCTIONS and QUESTIONS. Instructions typically ask you to modify or add content. Questions ask for information or clarification.
`;
 
const systemDefault = `\
${systemCommon}
- <Block> is the current block of text the user is working on.
- Ensure your output can seamlessly fit into the existing <Block> structure.
- CRITICAL: Provide only a single block of text. DO NOT create multiple paragraphs or separate blocks.
<Block>
{block}
</Block>
`;
 
const systemSelecting = `\
${systemCommon}
- <Block> is the block of text containing the user's selection, providing context.
- Ensure your output can seamlessly fit into the existing <Block> structure.
- <Selection> is the specific text the user has selected in the block and wants to modify or ask about.
- Consider the context provided by <Block>, but only modify <Selection>. Your response should be a direct replacement for <Selection>.
<Block>
{block}
</Block>
<Selection>
{selection}
</Selection>
`;
 
const systemBlockSelecting = `\
${systemCommon}
- <Selection> represents the full blocks of text the user has selected and wants to modify or ask about.
- Your response should be a direct replacement for the entire <Selection>.
- Maintain the overall structure and formatting of the selected blocks, unless explicitly instructed otherwise.
- CRITICAL: Provide only the content to replace <Selection>. Do not add additional blocks or change the block structure unless specifically requested.
<Selection>
{block}
</Selection>
`;
 
const userDefault = `<Reminder>
CRITICAL: DO NOT use block formatting. You can only use inline formatting.
CRITICAL: DO NOT start new lines or paragraphs.
NEVER write <Block>.
</Reminder>
{prompt}`;
 
const userSelecting = `<Reminder>
If this is a question, provide a helpful and concise answer about <Selection>.
If this is an instruction, provide ONLY the text to replace <Selection>. No explanations.
Ensure it fits seamlessly within <Block>. If <Block> is empty, write ONE random sentence.
NEVER write <Block> or <Selection>.
</Reminder>
{prompt} about <Selection>`;
 
const userBlockSelecting = `<Reminder>
If this is a question, provide a helpful and concise answer about <Selection>.
If this is an instruction, provide ONLY the content to replace the entire <Selection>. No explanations.
Maintain the overall structure unless instructed otherwise.
NEVER write <Block> or <Selection>.
</Reminder>
{prompt} about <Selection>`;
 
export const PROMPT_TEMPLATES = {
  systemBlockSelecting,
  systemDefault,
  systemSelecting,
  userBlockSelecting,
  userDefault,
  userSelecting,
};
 
const plugins = [
  // ...otherPlugins,
  MarkdownPlugin.configure({
    options: {
      remarkPlugins: [remarkMath, remarkGfm, remarkMdx],
    },
  }),
  AIPlugin,
  AIChatPlugin.configure({
    options: {
      createAIEditor,
      promptTemplate: ({ isBlockSelecting, isSelecting }) => {
        return isBlockSelecting
          ? PROMPT_TEMPLATES.userBlockSelecting
          : isSelecting
            ? PROMPT_TEMPLATES.userSelecting
            : PROMPT_TEMPLATES.userDefault;
      },
      systemTemplate: ({ isBlockSelecting, isSelecting }) => {
        return isBlockSelecting
          ? PROMPT_TEMPLATES.systemBlockSelecting
          : isSelecting
            ? PROMPT_TEMPLATES.systemSelecting
            : PROMPT_TEMPLATES.systemDefault;
      },
    },
    render: { afterEditable: () => <AIMenu /> },
  }),
];

AI SDK

This plugin is depending on the ai package:

Convert the chunk into a plate nodes.

By default, AI responses are streamed to the Plate editor in a chunk-by-chunk manner.

However, this chunking approach can cause issues when handling complex nodes like tables and code blocks. If chunks are split too frequently, it can lead to performance problems as these heavy nodes are repeatedly replaced.

To solve this issue, it's recommended to use the experimental_transform parameter with the smoothStream function in the streamText function to optimize the chunk transmission.

The chunking function below implements the following default behavior: it uses line-based chunking for tables, math, links, and code blocks, while using word-based chunking for all other content.

    const result = streamText({
      experimental_transform: smoothStream({
        chunking: (buffer) => {
          // Check for code block markers
          if (buffer.includes('```')) {
            isInCodeBlock = !isInCodeBlock;
          }
 
          // test case: should not deserialize link with markdown syntax
          if (buffer.includes('http')) {
            isInLink = true;
          } else if (buffer.includes('https')) {
            isInLink = true;
          } else if (buffer.includes('\n') && isInLink) {
            isInLink = false;
          }
 
          if (buffer.includes('*') || buffer.includes('-')) {
            isInList = true;
          } else if (buffer.includes('\n') && isInList) {
            isInList = false;
          }
 
          // Simple table detection: enter on |, exit on double newline
          if (!isInTable && buffer.includes('|')) {
            isInTable = true;
          } else if (isInTable && buffer.includes('\n\n')) {
            isInTable = false;
          }
 
          // Use line chunking for code blocks and tables, word chunking otherwise
          // Choose the appropriate chunking strategy based on content type
          let match;
 
          if (isInCodeBlock || isInTable || isInLink) {
            // Use line chunking for code blocks and tables
            match = CHUNKING_REGEXPS.line.exec(buffer);
          } else if (isInList) {
            // Use list chunking for lists
            match = CHUNKING_REGEXPS.list.exec(buffer);
          } else {
            // Use word chunking for regular text
            match = CHUNKING_REGEXPS.word.exec(buffer);
          }
 
          if (!match) {
            return null;
          }
 
          return buffer.slice(0, match.index) + match?.[0];
        },
      }),
      // your other options
      
      // maxTokens: 2048,
      // messages: convertToCoreMessages(messages),
      // model: openai('gpt-4o'),
      // system: system,
    });
 

Keyboard Shortcuts

KeyDescription
Space

Open AI menu in empty block (cursor mode)

Cmd + J

Open AI menu (cursor or selection mode)

EscapeClose AI menu

Examples

Plate UI

Refer to the preview above.

Plate Plus

Combobox menu with free-form prompt input

  • Additional trigger methods:
    • Block menu button
    • Slash command menu
  • Beautifully crafted UI

Plugins

AIPlugin

Extends the editor with AI transforms.

AIChatPlugin

This plugin is experimental.

Enables chat operations and streaming text generation in the editor.

Options

Collapse all

    The Editor used to generate the AI response.

    Chat helpers returned by useChat.

    Specifies how assistant messages are handled:

    • 'chat': Shows preview with accept/reject options
    • 'insert': Directly inserts content into editor
    • Default: 'chat'

    Whether the AI chat is open.

    • Default: false

    Whether the AI response is currently streaming. Cursor mode only.

    • Default: false

    Template for generating prompts. Supports placeholders:

    • {block}: Markdown of blocks in selection
    • {editor}: Markdown of entire editor content
    • {selection}: Markdown of current selection
    • {prompt}: Actual user prompt
    • Default: '{prompt}'

    Template for system messages. Supports same placeholders as promptTemplate.

    Used internally for streamInsertChunk.

    Used internally for tracking block path.

API

api.aiChat.accept

Accepts the current AI suggestion:

  • Removes AI marks from the content
  • Hides the AI chat interface
  • Focuses the editor

api.aiChat.insertBelow

Inserts AI content below the current block.

Handles both block selection and normal selection modes:

  • In block selection: Inserts after the last selected block, applying formatting from the last block
  • In normal selection: Inserts after the current block, applying formatting from the current block

Parameters

Collapse all

    Editor containing the content to insert.

    Options for insertion.

Optionsobject

Collapse all

    Format to apply:

    • 'all': Apply formatting to all blocks
    • 'none': Insert without formatting
    • 'single': Apply formatting only to first block
    • Default: 'single'

api.aiChat.replaceSelection

Replaces the current selection with AI content.

Handles different selection modes:

  • Single block selection: Replaces the selected block, applying its formatting to inserted content based on format option
  • Multiple block selection: Replaces all selected blocks
    • With format: 'none' or 'single': Preserves original formatting
    • With format: 'all': Applies first block's formatting to all content
  • Normal selection: Replaces the current selection while maintaining surrounding context

Parameters

Collapse all

    Editor containing the content to replace with.

    Options for replacement.

Optionsobject

Collapse all

    Format to apply:

    • 'all': Apply formatting to all blocks
    • 'none': Insert without formatting
    • 'single': Apply formatting only to first block
    • Default: 'single'

api.aiChat.reset

Resets the chat state:

  • Stops any ongoing generation
  • Clears chat messages
  • Removes all AI nodes from the editor

api.aiChat.node

Gets the AI chat node entry.

Parameters

Collapse all

    Options for finding the node.

    • anchor: When true, finds nodes with type matching the plugin type
    • When false (default), finds nodes with the ai property

ReturnsNodeEntry | undefined

    The found node entry or undefined if not found.

api.aiChat.reload

Reloads the current AI chat:

  • In insert mode: Undoes previous AI changes
  • Reloads the chat with the current system prompt

api.aiChat.show

Shows the AI chat interface:

  • Resets the chat state
  • Clears messages
  • Sets the open state to true

api.aiChat.hide

Hides the AI chat interface:

  • Resets the chat state
  • Sets the open state to false
  • Focuses the editor
  • Removes the AI anchor

api.aiChat.stop

Stops the current AI generation:

  • Sets streaming state to false
  • Calls the chat stop function

api.aiChat.submit

Submits a prompt to generate AI content.

OptionsSubmitOptions

Collapse all

    Mode to use. In insert mode, undoes previous AI changes before submitting.

    • Default: 'chat' for selection, 'insert' otherwise

    Custom prompt.

    • Default: Uses chat input if not provided

    Custom system message.

Transforms

tf.aiChat.removeAnchor

Removes the AI chat anchor node.

Parameters

Collapse all

    Options for finding nodes to remove.

tf.ai.insertNodes

Inserts AI-generated nodes with the AI mark.

Parameters

Collapse all

    Nodes to insert with AI mark.

    Options for inserting nodes.

OptionsInsertNodesOptions

Collapse all

    Target path.

    • Default: Current selection

tf.ai.removeMarks

Removes AI marks from nodes in the specified location.

OptionsRemoveMarksOptions

Collapse all

    Location to remove marks from.

    • Default: Entire document

tf.ai.removeNodes

Removes nodes that have the AI mark.

OptionsRemoveNodesOptions

Collapse all

    Path to remove nodes from.

    • Default: Entire document

tf.ai.undo

Special undo operation for AI changes:

  • Undoes the last operation if it was AI-generated
  • Removes the redo stack entry to prevent redoing AI operations

useAIChatEditor

A hook that registers an editor in the AI chat plugin, and deserializes markdown content with block-level memoization.

Parameters

Collapse all

    The markdown content to deserialize into the editor.

const AIChatEditor = ({ content }: { content: string }) => {
  const aiEditor = usePlateEditor({
    plugins: [
      // Your editor plugins
      MarkdownPlugin,
      // etc...
    ],
  });
 
  useAIChatEditor(aiEditor, content, {
    // Optional markdown parser options
    parser: {
      exclude: ['space'],
    },
  });
 
  return <Editor editor={aiEditor} />;
};