aiPlugin
Add an AI assistant panel to the editor for generating and editing content with prompts. Built on the Vercel AI SDK’s useChat.
npm i @reacteditor/plugin-ai --saveimport { Editor } from "@reacteditor/core";
import { aiPlugin } from "@reacteditor/plugin-ai";
import "@reacteditor/plugin-ai/dist/index.css";
const ai = aiPlugin({
api: "/api/chat",
});
export function Editor() {
return <Editor plugins={[ai]} />;
}A server companion (@reacteditor/plugin-ai/server) provides helpers for wiring the endpoint to your model provider.
Options
Transport
Mirrors the useChat DefaultChatTransport config. Set transport to replace the entire transport and ignore the others.
| Param | Example | Type |
|---|---|---|
api | api: "/api/ai" | String |
credentials | credentials: "include" | RequestCredentials |
headers | headers: { Authorization: "" } | Record<string, string> | Headers |
body | body: { sessionId: "..." } | Object |
transport | transport: customTransport | ChatTransport<UIMessage> |
Lifecycle
| Param | Example | Type |
|---|---|---|
onFinish | onFinish: ({ message }) => {} | (event: { message: UIMessage }) => void |
onError | onError: (err) => {} | (error: Error) => void |
onToolCall | onToolCall: ({ getEditor }) => {} | (event) => unknown | Promise<unknown> | undefined |
messages | messages: [/* persisted UIMessages */] | UIMessage[] |
onToolCall is called for every model-emitted tool call before the built-in handlers run. Return a defined value (sync or async) to short-circuit and use that as the tool output; return undefined to fall through to the built-ins. getEditor() returns the live editor on each call, so reads after await always see current state.
UI
| Param | Example | Type |
|---|---|---|
attachments | attachments: true | Boolean |
renderTool | renderTool: ({ name, state }) => {} | (params: RenderToolParams) => ReactNode | undefined |
scrollToComponent | scrollToComponent: false | Boolean |
getCurrentRoute | getCurrentRoute: () => ({ path }) | () => { path?: string; title?: string } | null |
attachments
When true, a paperclip button next to the chat input opens an image picker. Selected files are sent with the next message via the AI SDK’s sendMessage({ files }) API. Defaults to false.
renderTool
Optional renderer for tool-call parts. Called for every tool-* part the model emits. Return a ReactNode to override the default rendering (shimmer "<name>..." while in flight, muted "<name>" once done) for a specific tool name + state. Return undefined to fall through to the default.
const ai = aiPlugin({
api: "/api/ai",
renderTool: ({ name, state, input, output }) => {
if (name === "addComponent" && state === "output-available") {
return <span>Added {(output as any)?.type}</span>;
}
return undefined;
},
});RenderToolParams:
| Field | Type |
|---|---|
name | string — the tool name without the tool- prefix |
state | "input-streaming" | "input-available" | "output-available" | "output-error" |
input | unknown — the model’s input arguments |
output | unknown — the tool’s output once available |
scrollToComponent
When true (the default), the chat panel scrolls newly added or modified components into view in the preview canvas via the editor’s scrollToComponent command. Set to false to disable.
getCurrentRoute
Optional callback resolving the editor’s current route. Invoked on each outgoing request and forwarded to the server as editorContext.currentRoute, which the default system prompt uses to ground responses. Return null (or omit) to send no route info.
Chat API server
The plugin’s chat panel posts to whatever api URL you configure (default /api/chat). The endpoint must accept the AI SDK’s UIMessage[] request shape and return a streamed UIMessage response. The @reacteditor/plugin-ai/server sub-export ships two helpers — reactEditorTools (the built-in tool schemas) and getEditorContext (formats the auto-injected editor context into a system prompt block) — so you can stand up the route in ~30 lines.
npm i ai @ai-sdk/anthropic zod# .env.local
ANTHROPIC_API_KEY=sk-ant-...// app/api/chat/route.ts (Next.js App Router)
import { anthropic } from "@ai-sdk/anthropic";
import {
convertToModelMessages,
stepCountIs,
streamText,
tool,
type UIMessage,
} from "ai";
import { z } from "zod";
import {
reactEditorTools,
getEditorContext,
} from "@reacteditor/plugin-ai/server";
export const runtime = "nodejs";
export const maxDuration = 60;
type Body = {
messages: UIMessage[];
editorContext?: Parameters<typeof getEditorContext>[0];
};
// Optional server-executed tool. Add as many as you like alongside the
// built-ins; client-side tools live in reactEditorTools and execute in
// the browser via the plugin's onToolCall handler.
const generateImage = tool({
description: "Generate an image from a prompt and return its URL.",
inputSchema: z.object({
prompt: z.string().describe("Description of the image to generate."),
width: z.number().int().positive().optional(),
height: z.number().int().positive().optional(),
}),
execute: async ({ width = 512, height = 512 }) => {
return {
url: `https://picsum.photos/${width}/${height}?random=${Math.floor(
Math.random() * 1_000_000
)}`,
};
},
});
export async function POST(req: Request) {
const { messages, editorContext } = (await req.json()) as Body;
const result = streamText({
model: anthropic("claude-sonnet-4-5"),
system: getEditorContext(editorContext),
messages: await convertToModelMessages(messages),
tools: { ...reactEditorTools, generateImage },
// Lets the model loop through tool calls + their results in a single
// request instead of stopping after the first tool call.
stopWhen: stepCountIs(50),
});
return result.toUIMessageStreamResponse();
}Request shape
The plugin sends { messages: UIMessage[], editorContext } on every request. editorContext is auto-injected by the plugin and contains the editor’s live state at request time:
type EditorContextPayload = {
currentRoute: { path?: string; title?: string } | null;
selectedComponentId: string | null;
componentTypes: string[];
};If you set the plugin’s body option, those keys are merged in alongside the auto-injected ones.
getEditorContext
Turns the editorContext payload into a compact system-prompt block (current route, selected component id, available component types, plus a few default instructions about calling getConfig before mutating). Use it as system: getEditorContext(editorContext) or concatenate with your own prompt for extra grounding:
const result = streamText({
model: anthropic("claude-sonnet-4-5"),
system:
getEditorContext(editorContext) +
"\n\nYou are a marketing copywriter. Prefer short, punchy headlines.",
// ...
});reactEditorTools
A registry of the built-in tool schemas (no execute — these run in the browser via the plugin’s client-side onToolCall handler). Spread them into your tools object alongside any server-executed tools.
| Name | Mutates | Purpose |
|---|---|---|
getConfig | · | Returns every component type, its prop JSON Schema, defaults, label, category. Model should call this before adding/updating. |
getSchema | · | Returns the current document { root, content, globals }. |
getComponent | · | Returns one component by id with its props. |
searchComponents | · | Lists components on the page, optionally filtered by type or substring match. |
addComponent | ✓ | Insert a component. parentId+slot targets a slot; omit to append to the page root. index defaults to end. |
updateComponent | ✓ | Merge props into the component with the given id. |
removeComponent | ✓ | Remove the component. |
moveComponent | ✓ | Move to a new position. parentId+slot targets a slot; omit to move to the page root. |
updateRootProps | ✓ | Merge props into the page’s root (e.g. title, meta). |
updateSchema | ✓ | Replace the entire document. Prefer per-component tools when possible. |
Mutating tools commit one undo step each in the editor’s reducer — the user can undo a model action atomically.
Tool-call loop
stopWhen: stepCountIs(50) caps the per-request tool-call loop server-side. The plugin pairs this with sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls on the client so tool results re-trigger the model until the loop ends. If you remove stopWhen, the model will stop after the first round of tool calls and never see the results.
Other providers
Any AI SDK provider works — swap anthropic("claude-sonnet-4-5") for openai("gpt-5"), google("gemini-3-pro"), etc. The rest of the route stays the same.
Auth
Add auth headers on the plugin side and validate them in the route. For a deployed proxy that fronts your model with an API key:
aiPlugin({
api: "https://your-app.com/api/chat",
headers: { Authorization: `Bearer ${userToken}` },
});// app/api/chat/route.ts
export async function POST(req: Request) {
const auth = req.headers.get("authorization");
if (!auth || !verifyToken(auth)) {
return new Response("Unauthorized", { status: 401 });
}
// ...rest of the handler
}If you’re shipping a client-only build (no own server), a CORS-enabled hosted endpoint works too — point api at the absolute URL and add the auth header. Note the browser will preflight the POST, so the remote endpoint must allow the Origin and the custom header.