plate
componentai-kitsource: ok · hit
Ai Kit
npx shadcn add "https://platejs.org/r/ai-kit.json"free
No selection tags yet — empty means no evidence, not lack of support.
Dependencies
Bundle weight · light@platejs/ai
Registry dependencies
https://platejs.org/r/markdown-kit.jsonhttps://platejs.org/r/cursor-overlay-kit.jsonhttps://platejs.org/r/ai-menu.jsonhttps://platejs.org/r/ai-node.jsonhttps://platejs.org/r/ai-toolbar-button.jsonhttps://platejs.org/r/ai-api.json
Files1 · source cached 10/9/2026, 3:30:58 PM
src/registry/components/editor/plugins/ai-kit.tsx
'use client';
import cloneDeep from 'lodash/cloneDeep.js';
import { BaseAIPlugin, withAIBatch } from '@platejs/ai';
import {
AIChatPlugin,
AIPlugin,
applyAISuggestions,
getInsertPreviewStart,
streamInsertChunk,
useChatChunk,
} from '@platejs/ai/react';
import { ElementApi, getPluginType, KEYS, PathApi } from 'platejs';
import { usePluginOption } from 'platejs/react';
import { AILoadingBar, AIMenu } from '@/registry/ui/ai-menu';
import { AIAnchorElement, AILeaf } from '@/registry/ui/ai-node';
import { useChat } from '../use-chat';
import { CursorOverlayKit } from './cursor-overlay-kit';
import { MarkdownKit } from './markdown-kit';
export const aiChatPlugin = AIChatPlugin.extend({
options: {
chatOptions: {
api: '/api/ai/command',
body: {},
},
},
render: {
afterContainer: AILoadingBar,
afterEditable: AIMenu,
node: AIAnchorElement,
},
shortcuts: { show: { keys: 'mod+j' } },
useHooks: ({ editor, getOption }) => {
useChat();
const mode = usePluginOption(AIChatPlugin, 'mode');
const toolName = usePluginOption(AIChatPlugin, 'toolName');
useChatChunk({
onChunk: ({ chunk, isFirst, nodes, text: content }) => {
if (isFirst && mode === 'insert') {
const { startBlock, startInEmptyParagraph } =
getInsertPreviewStart(editor);
editor.getTransforms(BaseAIPlugin).ai.beginPreview({
originalBlocks:
startInEmptyParagraph &&
startBlock &&
ElementApi.isElement(startBlock)
? [cloneDeep(startBlock)]
: [],
});
editor.tf.withoutSaving(() => {
editor.tf.insertNodes(
{
children: [{ text: '' }],
type: getPluginType(editor, KEYS.aiChat),
},
{
at: PathApi.next(editor.selection!.focus.path.slice(0, 1)),
}
);
});
editor.setOption(AIChatPlugin, 'streaming', true);
}
if (mode === 'insert' && nodes.length > 0) {
editor.tf.withoutSaving(() => {
if (!getOption('streaming')) return;
editor.tf.withScrolling(() => {
streamInsertChunk(editor, chunk, {
textProps: {
[getPluginType(editor, KEYS.ai)]: true,
},
});
});
});
}
if (toolName === 'edit' && mode === 'chat') {
withAIBatch(
editor,
() => {
applyAISuggestions(editor, content);
},
{
split: isFirst,
}
);
}
},
onFinish: () => {
editor.getApi(AIChatPlugin).aiChat.stop();
},
});
},
});
export const AIKit = [
...CursorOverlayKit,
...MarkdownKit,
AIPlugin.withComponent(AILeaf),
aiChatPlugin,
];