{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ai-docs",
  "title": "AI",
  "description": "AI-powered writing assistance.",
  "files": [
    {
      "path": "../../content/docs/(plugins)/(ai)/ai.mdx",
      "content": "---\ntitle: AI\ndescription: AI-powered writing assistance.\ndocs:\n  - route: https://pro.platejs.org/docs/examples/ai\n    title: Plus\n---\n\n<ComponentPreview name=\"ai-demo\" />\n\n<PackageInfo>\n\n## Features\n\n- **Context-aware command menu** that adapts to cursor, text selection, and block selection workflows.\n- **Streaming Markdown/MDX insertion** with table, column, and code block support powered by `streamInsertChunk`.\n- **Insert and chat review modes** with localized insert previews plus undo-safe batching via `withAIBatch` and `tf.ai.undo()`.\n- **Block selection aware transforms** to replace or append entire sections using `tf.aiChat.replaceSelection` and `tf.aiChat.insertBelow`.\n- **Direct integration with `@ai-sdk/react`** so `api.aiChat.submit` can stream responses from Vercel AI SDK helpers.\n- **Suggestion and comment utilities** that diff AI edits, accept/reject changes, and map AI feedback back to document ranges.\n\n</PackageInfo>\n\n## Kit Usage\n\n<Steps>\n\n### Installation\n\nThe fastest way to add AI functionality is with the `AIKit`. It ships the configured `AIPlugin`, `AIChatPlugin`, Markdown streaming helpers, cursor overlay, and their [Plate UI](/docs/installation/plate-ui) components.\n\n<ComponentSource name=\"ai-kit\" />\n\n- [`AIMenu`](/docs/components/ai-menu): Floating command surface for prompts, tool shortcuts, and chat review.\n- [`AILoadingBar`](/docs/components/ai-loading-bar): Displays streaming status at the editor container.\n- [`AIAnchorElement`](/docs/components/ai-anchor-element): Invisible anchor node used to position the floating menu during streaming.\n- [`AILeaf`](/docs/components/ai-leaf): Renders AI-marked text with subtle styling.\n\n### Add Kit\n\n```tsx\nimport { createPlateEditor } from 'platejs/react';\nimport { AIKit } from '@/components/editor/plugins/ai-kit';\n\nconst editor = createPlateEditor({\n  plugins: [\n    // ...otherPlugins,\n    ...AIKit,\n  ],\n});\n```\n\n### Add API Route\n\nExpose a streaming command endpoint that proxies your model provider:\n\n<ComponentSource name=\"ai-api\" />\n\n### Configure Environment\n\nSet your AI Gateway key locally (replace with your provider secret if you are not using a gateway):\n\n```bash title=\".env.local\"\nAI_GATEWAY_API_KEY=\"your-api-key\"\n```\n\n</Steps>\n\n## Manual Usage\n\n<Steps>\n\n### Installation\n\n```bash\nnpm install @platejs/ai @platejs/markdown @platejs/selection @ai-sdk/react ai\n```\n\n`@platejs/suggestion` is optional but required for diff-based edit suggestions.\n\n### Add Plugins\n\n```tsx\nimport { createPlateEditor } from 'platejs/react';\nimport { AIChatPlugin, AIPlugin } from '@platejs/ai/react';\nimport { BlockSelectionPlugin } from '@platejs/selection/react';\nimport { MarkdownPlugin } from '@platejs/markdown';\n\nexport const editor = createPlateEditor({\n  plugins: [\n    BlockSelectionPlugin,\n    MarkdownPlugin,\n    AIPlugin,\n    AIChatPlugin, // extended in the next step\n  ],\n});\n```\n\n- `BlockSelectionPlugin`: Enables multi-block selections that `AIChatPlugin` relies on for insert/replace transforms.\n- `MarkdownPlugin`: Provides Markdown serialization used by streaming utilities.\n- `AIPlugin`: Adds the AI mark and transforms for undoing AI batches.\n- `AIChatPlugin`: Supplies the AI combobox, API helpers, and transforms.\n\nUse `AIPlugin.withComponent` with your own element (or [`AILeaf`](/docs/components/ai-leaf)) to highlight AI-generated text.\n\n### Configure AIChatPlugin\n\nExtend `AIChatPlugin` to hook streaming and edits. The example mirrors the core logic from `AIKit` while keeping the UI headless.\n\n```tsx\nimport cloneDeep from 'lodash/cloneDeep';\nimport { BaseAIPlugin, withAIBatch } from '@platejs/ai';\nimport {\n  AIChatPlugin,\n  applyAISuggestions,\n  getInsertPreviewStart,\n  streamInsertChunk,\n  useChatChunk,\n} from '@platejs/ai/react';\nimport { ElementApi, getPluginType, KEYS, PathApi } from 'platejs';\nimport { usePluginOption } from 'platejs/react';\n\nexport const aiChatPlugin = AIChatPlugin.extend({\n  options: {\n    chatOptions: {\n      api: '/api/ai/command',\n      body: {\n        model: 'openai/gpt-4o-mini',\n      },\n    },\n    trigger: ' ',\n    triggerPreviousCharPattern: /^\\s?$/,\n  },\n  useHooks: ({ editor, getOption }) => {\n    const mode = usePluginOption(AIChatPlugin, 'mode');\n    const toolName = usePluginOption(AIChatPlugin, 'toolName');\n\n    useChatChunk({\n      onChunk: ({ chunk, isFirst, text }) => {\n        if (isFirst && mode === 'insert') {\n          const { startBlock, startInEmptyParagraph } =\n            getInsertPreviewStart(editor);\n\n          editor.getTransforms(BaseAIPlugin).ai.beginPreview({\n            originalBlocks:\n              startInEmptyParagraph &&\n              startBlock &&\n              ElementApi.isElement(startBlock)\n                ? [cloneDeep(startBlock)]\n                : [],\n          });\n\n          editor.setOption(AIChatPlugin, 'streaming', true);\n\n          editor.tf.withoutSaving(() => {\n            editor.tf.insertNodes(\n              {\n                children: [{ text: '' }],\n                type: getPluginType(editor, KEYS.aiChat),\n              },\n              {\n                at: PathApi.next(editor.selection!.focus.path.slice(0, 1)),\n              }\n            );\n          });\n        }\n\n        if (mode === 'insert') {\n          editor.tf.withoutSaving(() => {\n            if (!getOption('streaming')) return;\n\n            editor.tf.withScrolling(() => {\n              streamInsertChunk(editor, chunk, {\n                textProps: {\n                  [getPluginType(editor, KEYS.ai)]: true,\n                },\n              });\n            });\n          });\n        }\n\n        if (toolName === 'edit' && mode === 'chat') {\n          withAIBatch(\n            editor,\n            () => {\n              applyAISuggestions(editor, text);\n            },\n            { split: isFirst }\n          );\n        }\n      },\n      onFinish: () => {\n        editor.setOption(AIChatPlugin, 'streaming', false);\n        editor.setOption(AIChatPlugin, '_blockChunks', '');\n        editor.setOption(AIChatPlugin, '_blockPath', null);\n        editor.setOption(AIChatPlugin, '_mdxName', null);\n      },\n    });\n  },\n});\n```\n\n- `useChatChunk`: Watches `UseChatHelpers` status and yields incremental chunks.\n- `tf.ai.beginPreview`: Captures the rollback slice and selection for insert-mode preview before the first streamed chunk is written.\n- `streamInsertChunk`: Streams Markdown/MDX into the document, reusing the existing block when possible.\n- `applyAISuggestions`: Converts responses into transient suggestion nodes when `toolName === 'edit'`.\n- `withAIBatch`: Marks saved AI batches so suggestion review and accepted AI changes stay undo-safe.\n\nProvide your own `render` components (toolbar button, floating menu, etc.) when you extend the plugin.\n\n### Build API Route\n\nHandle `api.aiChat.submit` requests on the server. Each request includes the chat `messages` from `@ai-sdk/react` and a `ctx` payload that contains the editor `children`, current `selection`, and last `toolName`.\n[Complete API example](https://github.com/udecode/plate-playground-template/blob/main/src/app/api/ai/command/route.ts)\n\n```ts title=\"app/api/ai/command/route.ts\"\nimport {\n  convertToModelMessages,\n  createGateway,\n  createUIMessageStreamResponse,\n  streamText,\n  toUIMessageStream,\n} from 'ai';\nimport { createSlateEditor } from 'platejs';\n\nimport { BaseEditorKit } from '@/registry/components/editor/editor-base-kit';\nimport { markdownJoinerTransform } from '@/registry/lib/markdown-joiner-transform';\n\nexport async function POST(req: Request) {\n  const { apiKey, ctx, messages, model } = await req.json();\n\n  const editor = createSlateEditor({\n    plugins: BaseEditorKit,\n    selection: ctx.selection,\n    value: ctx.children,\n  });\n\n  const gateway = createGateway({\n    apiKey: apiKey ?? process.env.AI_GATEWAY_API_KEY!,\n  });\n\n  const result = streamText({\n    experimental_transform: markdownJoinerTransform(),\n    instructions:\n      ctx.toolName === 'edit'\n        ? 'You are an editor that rewrites user text.'\n        : undefined,\n    messages: await convertToModelMessages(messages),\n    model: gateway(model ?? 'openai/gpt-4o-mini'),\n  });\n\n  return createUIMessageStreamResponse({\n    stream: toUIMessageStream({\n      originalMessages: messages,\n      stream: result.stream,\n    }),\n  });\n}\n```\n\n- `ctx.children` and `ctx.selection` are rehydrated into a Slate editor so you can build rich prompts (see [Prompt Templates](#prompt-templates)).\n- Forward provider settings (model, apiKey, temperature, gateway flags, etc.) through `chatOptions.body`; everything you add is passed verbatim in the JSON payload and can be read before calling `createGateway`.\n- Always read secrets from the server. The client should only send opaque identifiers or short-lived tokens.\n- Return a streaming response so `useChat` and `useChatChunk` can process tokens incrementally.\n\n### Connect `useChat`\n\nBridge the editor and your model endpoint with `@ai-sdk/react`. Store helpers on the plugin so transforms can reload, stop, or show chat state.\n\n```tsx\nimport { useEffect } from 'react';\n\nimport { type UIMessage, DefaultChatTransport } from 'ai';\nimport { type UseChatHelpers, useChat } from '@ai-sdk/react';\nimport { AIChatPlugin } from '@platejs/ai/react';\nimport { useEditorPlugin } from 'platejs/react';\n\ntype ChatMessage = UIMessage<{}, { toolName: 'comment' | 'edit' | 'generate'; comment?: unknown }>;\n\nexport const useEditorAIChat = () => {\n  const { editor, setOption } = useEditorPlugin(AIChatPlugin);\n\n  const chat = useChat<ChatMessage>({\n    id: 'editor',\n    api: '/api/ai/command',\n    transport: new DefaultChatTransport(),\n    onData(data) {\n      if (data.type === 'data-toolName') {\n        editor.setOption(AIChatPlugin, 'toolName', data.data);\n      }\n    },\n  });\n\n  useEffect(() => {\n    setOption('chat', chat as UseChatHelpers<ChatMessage>);\n  }, [chat, setOption]);\n\n  return chat;\n};\n```\n\nCombine the helper with `useEditorChat` to keep the floating menu anchored correctly:\n\n```tsx\nimport { useEditorChat } from '@platejs/ai/react';\n\nuseEditorChat({\n  onOpenChange: (open) => {\n    if (!open) chat.stop?.();\n  },\n});\n```\n\nNow you can submit prompts programmatically:\n\n```tsx\nimport { AIChatPlugin } from '@platejs/ai/react';\n\neditor.getApi(AIChatPlugin).aiChat.submit('', {\n  prompt: {\n    default: 'Continue the document after {block}',\n    selecting: 'Rewrite {selection} with a clearer tone',\n  },\n  toolName: 'generate',\n});\n```\n\n</Steps>\n\n## Prompt Templates\n\n### Client Prompting\n\n- `api.aiChat.submit` accepts an `EditorPrompt`. Provide a string, an object with `default`/`selecting`/`blockSelecting`, or a function that receives `{ editor, isSelecting, isBlockSelecting }`. The helper `getEditorPrompt` in the client turns that value into the final string.\n- Combine it with `replacePlaceholders(editor, template, { prompt })` to expand `{editor}`, `{block}`, `{blockSelection}`, and `{prompt}` using Markdown generated by `@platejs/ai`.\n\n```tsx\nimport { replacePlaceholders } from '@platejs/ai';\n\neditor.getApi(AIChatPlugin).aiChat.submit('Improve tone', {\n  prompt: ({ isSelecting }) =>\n    isSelecting\n      ? replacePlaceholders(editor, 'Rewrite {blockSelection} using a friendly tone.')\n      : replacePlaceholders(editor, 'Continue {block} with two more sentences.'),\n  toolName: 'generate',\n});\n```\n\n### Server Prompting\n\nThe demo backend in `apps/www/src/app/api/ai/command` reconstructs the editor from `ctx` and builds structured prompts:\n\n- `getChooseToolPrompt` decides whether the request is `generate`, `edit`, or `comment`.\n- `getGeneratePrompt`, `getEditPrompt`, and `getCommentPrompt` transform the current editor state into instructions tailored to each mode.\n- Utility helpers like `getMarkdown`, `getMarkdownWithSelection`, and `buildStructuredPrompt` (see `apps/www/src/app/api/ai/command/prompts.ts`) make it easy to embed block ids, selections, and MDX tags into the LLM request.\n\nAugment the payload you send from the client to fine-tune server prompts:\n\n```ts\neditor.setOption(aiChatPlugin, 'chatOptions', {\n  api: '/api/ai/command',\n  body: {\n    model: 'openai/gpt-4o-mini',\n    tone: 'playful',\n    temperature: 0.4,\n  },\n});\n```\n\nEverything under `chatOptions.body` arrives in the route handler, letting you swap providers, pass user-specific metadata, or branch into different prompt templates.\n\n## Keyboard Shortcuts\n\n<KeyTable>\n  <KeyTableItem hotkey=\"Space\">Open the AI menu in an empty block (cursor mode)</KeyTableItem>\n  <KeyTableItem hotkey=\"Cmd + J\">Show the AI menu (set via `shortcuts.show`)</KeyTableItem>\n  <KeyTableItem hotkey=\"Escape\">Hide the AI menu and stop streaming</KeyTableItem>\n</KeyTable>\n\n## Streaming\n\nThe streaming utilities keep complex layouts intact while responses arrive:\n\n- `streamInsertChunk(editor, chunk, options)` deserializes Markdown chunks, updates the current block in place, and appends new blocks as needed. Use `textProps`/`elementProps` to tag streamed nodes (e.g., mark AI text).\n- `streamDeserializeMd` and `streamDeserializeInlineMd` provide lower-level access if you need to control streaming for custom node types.\n- `streamSerializeMd` mirrors the editor state so you can detect drift between streamed content and the response buffer.\n\nReset the internal `_blockChunks`, `_blockPath`, and `_mdxName` options when streaming finishes to start the next response from a clean slate.\n\n## Streaming Example\n\n<ComponentPreview name=\"markdown-streaming-demo\" />\n\n## Plate Plus\n\n<ComponentPreviewPro name=\"ai-pro\" />\n\n## API Reference\n\n### `AIPlugin`\n\nAdds an `ai` mark to streamed text and exposes transforms to remove AI nodes or undo the last AI batch. Use `.withComponent` to render AI-marked text with a custom component.\n\n<API name=\"AIPlugin\">\n  <APIOptions>\n    <APIItem name=\"node.isLeaf\" type=\"true\">AI content is stored on text nodes.</APIItem>\n    <APIItem name=\"node.isDecoration\" type=\"false\">AI marks are regular text properties, not decorations.</APIItem>\n  </APIOptions>\n</API>\n\n### `AIChatPlugin`\n\nMain plugin that powers the AI menu, chat state, and transforms.\n\n<API name=\"AIChatPlugin\">\n  <APIOptions>\n    <APIItem name=\"trigger\" type=\"RegExp | string | string[]\" optional>Character(s) that open the command menu. Defaults to `' '`.</APIItem>\n    <APIItem name=\"triggerPreviousCharPattern\" type=\"RegExp\" optional>Pattern that must match the character before the trigger. Defaults to `/^\\s?$/`.</APIItem>\n    <APIItem name=\"triggerQuery\" type=\"(editor: SlateEditor) => boolean\" optional>Return `false` to cancel opening in specific contexts.</APIItem>\n    <APIItem name=\"chat\" type=\"UseChatHelpers&lt;ChatMessage&gt;\" optional>Store helpers from `useChat` so API calls can access them.</APIItem>\n    <APIItem name=\"chatNodes\" type=\"TIdElement[]\" optional>Snapshot of nodes used to diff edit suggestions (managed internally).</APIItem>\n    <APIItem name=\"chatSelection\" type=\"TRange | null\" optional>Selection captured before submitting a prompt (managed internally).</APIItem>\n    <APIItem name=\"mode\" type=\"'chat' | 'insert'\">Controls whether responses stream directly into the document or open a review panel. Defaults to `'insert'`.</APIItem>\n    <APIItem name=\"open\" type=\"boolean\" optional>Whether the AI menu is visible. Defaults to `false`.</APIItem>\n    <APIItem name=\"streaming\" type=\"boolean\" optional>True while a response is streaming. Defaults to `false`.</APIItem>\n    <APIItem name=\"toolName\" type=\"'comment' | 'edit' | 'generate' | null\" optional>Active tool used to interpret the response.</APIItem>\n  </APIOptions>\n</API>\n\n### `api.aiChat.submit(input, options?)`\n\nSubmits a prompt to your model provider. When `mode` is omitted it defaults to `'insert'` for a collapsed cursor and `'chat'` otherwise.\n\n<API name=\"submit\">\n<APIParameters>\n  <APIItem name=\"input\" type=\"string\">Raw input from the user.</APIItem>\n  <APIItem name=\"options\" type=\"object\" optional>Fine-tune submission behaviour.</APIItem>\n</APIParameters>\n<APIOptions type=\"object\">\n  <APIItem name=\"mode\" type=\"'chat' | 'insert'\" optional>Override the response mode.</APIItem>\n  <APIItem name=\"options\" type=\"ChatRequestOptions\" optional>Forwarded to `chat.sendMessage` (model, headers, etc.).</APIItem>\n  <APIItem name=\"prompt\" type=\"EditorPrompt\" optional>String, config, or function processed by `getEditorPrompt`.</APIItem>\n  <APIItem name=\"toolName\" type=\"'comment' | 'edit' | 'generate' | null\" optional>Tags the submission so hooks can react differently.</APIItem>\n</APIOptions>\n</API>\n\n### `api.aiChat.reset(options?)`\n\nClears chat state, removes AI nodes, and optionally undoes the last AI batch.\n\n<API name=\"reset\">\n<APIParameters>\n  <APIItem name=\"options\" type=\"{ undo?: boolean }\" optional>Pass `undo: false` to keep streamed content.</APIItem>\n</APIParameters>\n</API>\n\n### `api.aiChat.node(options?)`\n\nRetrieves the first AI node that matches the specified criteria.\n\n<API name=\"node\">\n<APIParameters>\n  <APIItem name=\"options\" type=\"EditorNodesOptions &amp; { anchor?: boolean; streaming?: boolean }\" optional>Set `anchor: true` to get the anchor node or `streaming: true` to retrieve the node currently being streamed into.</APIItem>\n</APIParameters>\n<APIReturns type=\"NodeEntry | undefined\">Matching node entry, if found.</APIReturns>\n</API>\n\n### `api.aiChat.reload()`\n\nReplays the last prompt using the stored `UseChatHelpers`, restoring the original selection or block selection before resubmitting.\n\n### `api.aiChat.stop()`\n\nStops streaming and calls `chat.stop`.\n\n### `api.aiChat.show()`\n\nOpens the AI menu, clears previous chat messages, and resets tool state.\n\n### `api.aiChat.hide(options?)`\n\nCloses the AI menu, optionally undoing the last AI batch and refocusing the editor.\n\n<API name=\"hide\">\n<APIParameters>\n  <APIItem name=\"options\" type=\"{ focus?: boolean; undo?: boolean }\" optional>Set `focus: false` to keep focus outside the editor or `undo: false` to preserve inserted content.</APIItem>\n</APIParameters>\n</API>\n\n### `tf.aiChat.accept()`\n\nAccepts the latest response. In insert mode it removes AI marks and places the caret at the end of the streamed content. In chat mode it applies the pending suggestions.\n\n### `tf.aiChat.insertBelow(sourceEditor, options?)`\n\nInserts the chat preview (`sourceEditor`) below the current selection or block selection.\n\n<API name=\"insertBelow\">\n<APIParameters>\n  <APIItem name=\"sourceEditor\" type=\"SlateEditor\">Editor containing the generated content.</APIItem>\n  <APIItem name=\"options\" type=\"{ format?: 'all' | 'none' | 'single' }\" optional>Copy formatting from the source selection. Defaults to `'single'`.</APIItem>\n</APIParameters>\n</API>\n\n### `tf.aiChat.replaceSelection(sourceEditor, options?)`\n\nReplaces the current selection or block selection with the chat preview.\n\n<API name=\"replaceSelection\">\n<APIParameters>\n  <APIItem name=\"sourceEditor\" type=\"SlateEditor\">Editor containing the generated content.</APIItem>\n  <APIItem name=\"options\" type=\"{ format?: 'all' | 'none' | 'single' }\" optional>Controls how much formatting from the original selection should be applied.</APIItem>\n</APIParameters>\n</API>\n\n### `tf.aiChat.removeAnchor(options?)`\n\nRemoves the temporary anchor node used to position the AI menu.\n\n<API name=\"removeAnchor\">\n<APIParameters>\n  <APIItem name=\"options\" type=\"EditorNodesOptions\" optional>Filters the nodes to remove.</APIItem>\n</APIParameters>\n</API>\n\n### `tf.ai.insertNodes(nodes, options?)`\n\nInserts nodes tagged with the AI mark at the current selection (or `options.target`).\n\n### `tf.ai.removeMarks(options?)`\n\nClears the AI mark from matching nodes.\n\n### `tf.ai.removeNodes(options?)`\n\nRemoves text nodes that are marked as AI-generated.\n\n### `tf.ai.beginPreview(options?)`\n\nCaptures the rollback slice and selection for insert-mode AI preview. Call it once before writing the first unsaved preview chunk.\n\n<API name=\"beginPreview\">\n<APIParameters>\n  <APIItem name=\"options\" type=\"{ originalBlocks?: Value }\" optional>Top-level blocks that the preview will overwrite. Use `[]` when preview inserts after existing content.</APIItem>\n</APIParameters>\n<APIReturns type=\"boolean\">Returns `true` when a new preview rollback point was stored, or `false` when preview state already exists.</APIReturns>\n</API>\n\n### `tf.ai.acceptPreview()`\n\nCommits the active preview as one fresh undoable batch, strips preview-only markers, and clears preview bookkeeping.\n\n<API name=\"acceptPreview\">\n<APIReturns type=\"boolean\">Returns `true` when an active preview was committed.</APIReturns>\n</API>\n\n### `tf.ai.cancelPreview()`\n\nRestores the rollback point for the active preview and clears preview bookkeeping.\n\n<API name=\"cancelPreview\">\n<APIReturns type=\"boolean\">Returns `true` when an active preview was restored.</APIReturns>\n</API>\n\n### `tf.ai.discardPreview()`\n\nClears preview bookkeeping without restoring content. Use it when the previewed content should stay in place.\n\n<API name=\"discardPreview\">\n<APIReturns type=\"boolean\">Returns `true` when active preview bookkeeping was cleared.</APIReturns>\n</API>\n\n### `tf.ai.hasPreview()`\n\nReports whether an insert-mode preview rollback point is currently active.\n\n<API name=\"hasPreview\">\n<APIReturns type=\"boolean\">Returns `true` when preview rollback state exists.</APIReturns>\n</API>\n\n### `tf.ai.undo()`\n\nUndoes the latest AI history entry when it was created by `withAIBatch`. If an insert-mode preview is active, it cancels that preview first instead of replaying every streamed chunk. In both cases it avoids re-applying AI output from redo.\n\n### `useAIChatEditor`\n\nRegisters an auxiliary editor for chat previews and deserializes Markdown with block-level memoization.\n\n<API name=\"useAIChatEditor\">\n<APIParameters>\n  <APIItem name=\"editor\" type=\"SlateEditor\">Editor instance dedicated to the chat preview.</APIItem>\n  <APIItem name=\"content\" type=\"string\">Markdown content returned by the model.</APIItem>\n  <APIItem name=\"options\" type=\"DeserializeMdOptions\" optional>Pass `parser` to filter tokens before deserialization.</APIItem>\n</APIParameters>\n</API>\n\n```tsx\nimport { usePlateEditor } from 'platejs/react';\nimport { MarkdownPlugin } from '@platejs/markdown';\nimport { AIChatPlugin, useAIChatEditor } from '@platejs/ai/react';\n\nconst aiPreviewEditor = usePlateEditor({\n  plugins: [MarkdownPlugin, AIChatPlugin],\n});\n\nuseAIChatEditor(aiPreviewEditor, responseMarkdown, {\n  parser: { exclude: ['space'] },\n});\n```\n\n### `useEditorChat`\n\nConnects `UseChatHelpers` to editor state so the AI menu knows whether to anchor to cursor, selection, or block selection.\n\n<API name=\"useEditorChat\">\n<APIParameters>\n  <APIItem name=\"onOpenBlockSelection\" type=\"(blocks: NodeEntry[]) => void\" optional>Called when the menu opens on block selection.</APIItem>\n  <APIItem name=\"onOpenChange\" type=\"(open: boolean) => void\" optional>Called whenever the menu opens or closes.</APIItem>\n  <APIItem name=\"onOpenCursor\" type=\"() => void\" optional>Called when the menu opens at the cursor.</APIItem>\n  <APIItem name=\"onOpenSelection\" type=\"() => void\" optional>Called when the menu opens on a text selection.</APIItem>\n</APIParameters>\n</API>\n\n### `useChatChunk`\n\nStreams chat responses chunk-by-chunk and gives you full control over insertion.\n\n<API name=\"useChatChunk\">\n<APIParameters>\n  <APIItem name=\"onChunk\" type=\"(chunk: { chunk: string; isFirst: boolean; nodes: TText[]; text: string }) => void\">Handle each streamed chunk.</APIItem>\n  <APIItem name=\"onFinish\" type=\"({ content }: { content: string }) => void\" optional>Called when streaming finishes.</APIItem>\n</APIParameters>\n</API>\n\n### `withAIBatch`\n\nGroups editor operations into a single history batch and flags it as AI-generated so `tf.ai.undo()` removes it safely.\n\n<API name=\"withAIBatch\">\n<APIParameters>\n  <APIItem name=\"editor\" type=\"SlateEditor\">Target editor.</APIItem>\n  <APIItem name=\"fn\" type=\"() => void\">Operations to run.</APIItem>\n  <APIItem name=\"options\" type=\"{ split?: boolean }\" optional>Set `split: true` to start a new history batch.</APIItem>\n</APIParameters>\n</API>\n\n### `applyAISuggestions`\n\nDiffs AI output against stored `chatNodes` and writes transient suggestion nodes. Requires `@platejs/suggestion`.\n\n<API name=\"applyAISuggestions\">\n<APIParameters>\n  <APIItem name=\"editor\" type=\"SlateEditor\">Editor to apply suggestions to.</APIItem>\n  <APIItem name=\"content\" type=\"string\">Markdown response from the model.</APIItem>\n</APIParameters>\n</API>\n\nComplementary helpers allow you to finalize or discard the diff:\n\n- `acceptAISuggestions(editor)`: Converts transient suggestion nodes into permanent suggestions.\n- `rejectAISuggestions(editor)`: Removes transient suggestion nodes and clears suggestion marks.\n\n### `aiCommentToRange`\n\nMaps streamed comment metadata back to document ranges so comments can be inserted automatically.\n\n<API name=\"aiCommentToRange\">\n<APIParameters>\n  <APIItem name=\"editor\" type=\"PlateEditor\">Editor instance.</APIItem>\n  <APIItem name=\"options\" type=\"{ blockId: string; comment: string; content: string }\">Block id and text used to locate the range.</APIItem>\n</APIParameters>\n<APIReturns type=\"{ start: BasePoint; end: BasePoint } | null\">Range matching the comment or `null` if it cannot be found.</APIReturns>\n</API>\n\n### `findTextRangeInBlock`\n\nFuzzy-search helper that uses LCS to find the closest match inside a block.\n\n<API name=\"findTextRangeInBlock\">\n<APIParameters>\n  <APIItem name=\"node\" type=\"TNode\">Block node to search.</APIItem>\n  <APIItem name=\"searchText\" type=\"string\">Text snippet to locate.</APIItem>\n</APIParameters>\n<APIReturns type=\"{ start: { path: Path; offset: number }; end: { path: Path; offset: number } } | null\">Matched range or `null`.</APIReturns>\n</API>\n\n### `getEditorPrompt`\n\nGenerates prompts that respect cursor, selection, or block selection states.\n\n<API name=\"getEditorPrompt\">\n<APIParameters>\n  <APIItem name=\"editor\" type=\"SlateEditor\">Editor providing context.</APIItem>\n  <APIItem name=\"options\" type=\"{ prompt?: EditorPrompt }\">String, config, or function describing the prompt.</APIItem>\n</APIParameters>\n<APIReturns type=\"string\">Contextualized prompt string.</APIReturns>\n</API>\n\n### `replacePlaceholders`\n\nReplaces placeholders like `{editor}`, `{blockSelection}`, and `{prompt}` with serialized Markdown.\n\n<API name=\"replacePlaceholders\">\n<APIParameters>\n  <APIItem name=\"editor\" type=\"SlateEditor\">Editor providing content.</APIItem>\n  <APIItem name=\"text\" type=\"string\">Template text.</APIItem>\n  <APIItem name=\"options\" type=\"{ prompt?: string }\" optional>Prompt value injected into `{prompt}`.</APIItem>\n</APIParameters>\n<APIReturns type=\"string\">Template with placeholders replaced by Markdown.</APIReturns>\n</API>\n\n## Customization\n\n### Adding Custom AI Commands\n\n<ComponentSource name=\"ai-menu\" />\n\nExtend the `aiChatItems` map to add new commands. Each command receives `{ aiEditor, editor, input }` and can dispatch `api.aiChat.submit` with custom prompts or transforms.\n\n#### Simple Custom Command\n\n```tsx\nsummarizeInBullets: {\n  icon: <ListIcon />,\n  label: 'Summarize in bullets',\n  value: 'summarizeInBullets',\n  onSelect: ({ editor }) => {\n    void editor.getApi(AIChatPlugin).aiChat.submit('', {\n      prompt: 'Summarize the current selection using bullet points',\n      toolName: 'generate',\n    });\n  },\n},\n```\n\n#### Command with Complex Logic\n\n```tsx\ngenerateTOC: {\n  icon: <BookIcon />,\n  label: 'Generate table of contents',\n  value: 'generateTOC',\n  onSelect: ({ editor }) => {\n    const headings = editor.api.nodes({\n      match: (n) => ['h1', 'h2', 'h3'].includes(n.type as string),\n    });\n\n    const prompt =\n      headings.length === 0\n        ? 'Create a realistic table of contents for this document'\n        : 'Generate a table of contents that reflects the existing headings';\n\n    void editor.getApi(AIChatPlugin).aiChat.submit('', {\n      mode: 'insert',\n      prompt,\n      toolName: 'generate',\n    });\n  },\n},\n```\n\nThe menu automatically switches between command and suggestion states:\n\n- `cursorCommand`: Cursor is collapsed and no response yet.\n- `selectionCommand`: Text is selected and no response yet.\n- `cursorSuggestion` / `selectionSuggestion`: A response exists, so actions like Accept, Try Again, or Insert Below are shown.\n\nUse `toolName` (`'generate' | 'edit' | 'comment'`) to control how streaming hooks process the response. For example, `'edit'` enables diff-based suggestions, and `'comment'` allows you to convert streamed comments into discussion threads with `aiCommentToRange`.\n",
      "type": "registry:file",
      "target": "content/docs/plate/(plugins)/(ai)/ai.mdx"
    }
  ],
  "type": "registry:file"
}