AI Commands

Lightweight AI Commands toolbar — improve, simplify, translate, change tone, and more. Adds buttons to the toolbar, context bar, and addbar when ai.url is configured.

Requires a backend endpoint (for example /api/endpoint/ai-commands/). For the full AI product with grouped actions and review modes, use the AI Assistant plugin instead.

Load the bundle

<script src="/assets/redactor/redactor.js"></script>
<script src="/assets/redactor/plugins/ai/ai.js"></script>

Initialization

The plugin UI appears only when url is set.

const app = Redactor('#entry', {
    plugins: ['ai'],
    ai: {
        url: '/api/endpoint/ai-commands/',
        model: 'gpt-4o-mini'
    }
});

Options

  • url (string | false)
    • default false
    • Sync endpoint URL. The plugin UI is hidden when false.
  • model (string | false)
    • default false
    • Model id forwarded to the backend as body.model.
  • headers (object)
    • default {}
    • Extra HTTP headers attached to text requests.
  • icon (string)
    • SVG icon HTML for the AI toolbar and context buttons.
  • iconImage (string)
    • SVG icon HTML for the image generation addbar button.
  • image (object)
    • Image prompt UI settings:
      • sizes — map of size keys to labels (for example '1024x1024': 'Square').
      • defaultSize — default '1536x1024'.
  • actions (object)
    • Preset actions shown in the dropdown. Each action may define title, icon, prompt, and nested items (for translate, tone, etc.).
Redactor('#entry', {
    plugins: ['ai'],
    ai: {
        url: '/api/endpoint/ai-commands/',
        model: 'gpt-4o-mini',
        headers: {
            Authorization: 'Bearer …'
        },
        actions: {
            improve: { title: 'Improve it', prompt: 'Improve it' },
            prompt: { title: 'Ask AI…', prompt: '' }
        },
        image: {
            sizes: {
                '1536x1024': 'Landscape',
                '1024x1024': 'Square'
            },
            defaultSize: '1024x1024'
        }
    }
});

Language strings

en: {
    ai: {
        'ai': 'AI',
        'ask': 'Ask AI…',
        'no-selection': 'Select at least one block',
        'empty-response': 'AI returned empty HTML'
    }
}

Backend

The AI plugin does not call OpenAI, Claude, or any other provider directly. It sends POST requests with a JSON body to URLs you configure (url for text, editor-level ai.image.url for images). Your backend validates the session, calls the provider, and returns JSON in the shape the editor expects.

The examples below are illustrative only — simplified sketches, not production-ready endpoints. Adapt them to your CMS, framework, auth, rate limits, and logging. You can implement the same contract in any language or use official SDKs (OpenAI, Anthropic, etc.).

Text requests

Endpoint: plugin option url (for example /api/ai/text).

Method: POST
Content-Type: application/json
Extra headers: from plugin option headers (merged into the request).

Request body:

Field Type Description
prompt string Action instruction or user prompt from Ask AI…. Required.
html string HTML of the selected block(s). Empty when nothing is selected (prompt-only flow).
model string Optional. Sent when plugin option model is set.

Example payload:

{
    "prompt": "Improve it",
    "html": "<p>The product launch was really good and we had alot of people show up.</p>",
    "model": "gpt-4o-mini"
}

Success response: HTTP 200, JSON with transformed HTML:

{
    "html": "<p>The product launch went well, and many people attended.</p>"
}

The editor reads response.html (string). Empty or missing html is treated as an error (ai:action:empty-response).

Error response: non-2xx status and JSON:

{
    "error": "Prompt is empty"
}

Example (OpenAI JavaScript SDK)

Same contract using the official OpenAI Node.js library and the Responses API:

import OpenAI from 'openai';

const client = new OpenAI();

// Inside your route handler — `prompt`, `html`, and `model` come from the editor POST body.
const userContent = html
    ? `Instruction:\n${prompt}\n\nHTML:\n${html}`
    : `Instruction:\n${prompt}`;

const response = await client.responses.create({
    model: model || 'gpt-4o-mini',
    instructions: 'Return ONLY HTML. Preserve structure and attributes when possible. No markdown fences.',
    input: userContent
});

const resultHtml = (response.output_text || '').trim();

// Respond to the editor:
res.json({ html: resultHtml });

With the Anthropic JavaScript SDK, the same pattern applies: read prompt / html from the request, call messages.create, extract text from response.content, return { html }.

Example (OpenAI + cURL)

Plain PHP without SDKs — read the editor payload, call OpenAI via cURL, return { html }:

<?php
// Illustrative only — not production-ready.

header('Content-Type: application/json; charset=utf-8');

$payload = json_decode(file_get_contents('php://input'), true) ?: [];
$prompt  = trim($payload['prompt'] ?? '');
$html    = trim($payload['html'] ?? '');
$model   = $payload['model'] ?? 'gpt-4o-mini';

if ($prompt === '') {
    http_response_code(400);
    echo json_encode(['error' => 'Prompt is empty']);
    exit;
}

$input = $html === ''
    ? "Instruction:\n{$prompt}"
    : "Instruction:\n{$prompt}\n\nHTML:\n{$html}";

$ch = curl_init('https://api.openai.com/v1/responses');
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . getenv('OPENAI_API_KEY'),
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'model'        => $model,
        'instructions' => 'Return ONLY HTML. No markdown fences.',
        'input'        => $input,
    ]),
]);

$raw  = curl_exec($ch);
curl_close($ch);

$data       = json_decode($raw, true);
$resultHtml = trim($data['output_text'] ?? '');

if ($resultHtml === '') {
    http_response_code(502);
    echo json_encode(['error' => 'Model returned empty HTML']);
    exit;
}

echo json_encode(['html' => $resultHtml]);

Example (Claude + cURL)

The editor contract is the same{ prompt, html, model? } in, { html } out. Only the upstream API call differs:

OpenAI (Responses API) Claude (Messages API)
Endpoint POST /v1/responses POST /v1/messages
Auth Authorization: Bearer … x-api-key: … + anthropic-version: …
Request body model, instructions, input model, messages, max_tokens
Response field output_text content[].text (where type is "text")
<?php
// Illustrative only — not production-ready.

header('Content-Type: application/json; charset=utf-8');

$payload = json_decode(file_get_contents('php://input'), true) ?: [];
$prompt  = trim($payload['prompt'] ?? '');
$html    = trim($payload['html'] ?? '');
$model   = $payload['model'] ?? 'claude-sonnet-4-20250514';

if ($prompt === '') {
    http_response_code(400);
    echo json_encode(['error' => 'Prompt is empty']);
    exit;
}

$input = $html === ''
    ? "Instruction:\n{$prompt}"
    : "Instruction:\n{$prompt}\n\nHTML:\n{$html}";

$ch = curl_init('https://api.anthropic.com/v1/messages');
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        'x-api-key: ' . getenv('ANTHROPIC_API_KEY'),
        'anthropic-version: 2023-06-01',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'model'      => $model,
        'max_tokens' => 4096,
        'system'     => 'Return ONLY HTML. No markdown fences.',
        'messages'   => [
            ['role' => 'user', 'content' => $input],
        ],
    ]),
]);

$raw  = curl_exec($ch);
curl_close($ch);

$data = json_decode($raw, true);
$resultHtml = '';

foreach ($data['content'] ?? [] as $block) {
    if (($block['type'] ?? '') === 'text') {
        $resultHtml .= $block['text'] ?? '';
    }
}

$resultHtml = trim($resultHtml);

if ($resultHtml === '') {
    http_response_code(502);
    echo json_encode(['error' => 'Model returned empty HTML']);
    exit;
}

echo json_encode(['html' => $resultHtml]);

Image requests

Endpoint: editor option ai.image.url (separate from plugin url). The addbar image button is shown only when this URL is set.

Method: POST
Content-Type: application/json
Extra headers: from editor option ai.image.headers.

Request body:

Field Type Description
action string Always "image".
prompt string User description for the image. Required.
size string Dimensions, for example "1024x1024", "1536x1024". From the prompt UI / ai.image.size.
n number Number of images to generate (default 1).
model string Optional. Sent when editor option ai.image.model is set.

Example payload:

{
    "action": "image",
    "prompt": "A minimal flat illustration of a mountain at sunset",
    "size": "1024x1024",
    "n": 1,
    "model": "gpt-image-1"
}

Success response: HTTP 200, JSON:

{
    "images": [
        {
            "url": "https://cdn.example.com/generated/abc.png"
        }
    ],
    "meta": {
        "model": "gpt-image-1",
        "size": "1024x1024"
    }
}

Each item in images must include either:

  • url — public HTTPS URL of the image, or
  • b64_json — raw base64 payload, with optional mime (for example "image/png").

Optional fields: alt, prompt (revised prompt from the provider).

The editor normalizes this list and inserts the image block. Empty images means nothing is inserted.

Error response: same as text — { "error": "…" } with a non-2xx status.

Example (OpenAI JavaScript SDK)

import OpenAI from 'openai';

const client = new OpenAI();

// `prompt`, `size`, `n`, and `model` come from the editor POST body.
const response = await client.images.generate({
    model: model || 'gpt-image-1',
    prompt,
    size: size || '1024x1024',
    n: n || 1
});

const images = (response.data ?? []).flatMap((item) => {
    if (item.url) return [{ url: item.url }];
    if (item.b64_json) return [{ b64_json: item.b64_json, mime: 'image/png' }];
    return [];
});

// Respond to the editor:
res.json({ images, meta: { model: model || 'gpt-image-1', size } });

The editor contract is the same for any image provider — { images } out, with each item as url or b64_json. Only the upstream API call changes.

Example (PHP + cURL)

Plain PHP without SDKs — read the editor payload, call OpenAI Images via cURL, return { images }:

<?php
// Illustrative only — not production-ready.

header('Content-Type: application/json; charset=utf-8');

$payload = json_decode(file_get_contents('php://input'), true) ?: [];
$prompt  = trim($payload['prompt'] ?? '');
$size    = $payload['size'] ?? '1024x1024';
$n       = max(1, (int) ($payload['n'] ?? 1));
$model   = $payload['model'] ?? 'gpt-image-1';

if ($prompt === '') {
    http_response_code(400);
    echo json_encode(['error' => 'Image prompt is empty']);
    exit;
}

$ch = curl_init('https://api.openai.com/v1/images/generations');
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . getenv('OPENAI_API_KEY'),
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'model'  => $model,
        'prompt' => $prompt,
        'size'   => $size,
        'n'      => $n,
    ]),
]);

$raw  = curl_exec($ch);
curl_close($ch);

$data   = json_decode($raw, true);
$images = [];

foreach ($data['data'] ?? [] as $item) {
    if (!empty($item['url'])) {
        $images[] = ['url' => $item['url']];
    } elseif (!empty($item['b64_json'])) {
        $images[] = [
            'b64_json' => $item['b64_json'],
            'mime'     => 'image/' . ($item['output_format'] ?? 'png'),
        ];
    }
}

echo json_encode([
    'images' => $images,
    'meta'   => ['model' => $model, 'size' => $size],
]);

Hook into the flow with ai:image:before if you need to adjust URL, headers, or body before the request leaves the browser.

Events

ai:create

Hook when an AI request is prepared. Return modified fields or call stop() to cancel.

Payload: action, actionKey, configPath, prompt, html, requestHtml, blocks, selected.

app.on('ai:create', ({ action, prompt, html, stop }) => {
    if (shouldCancel()) {
        stop();
        return;
    }
    return { action, prompt, html };
});

ai:before:send

Hook before the HTTP request. Return modified url, init, or body, or call stop() to cancel.

Payload: action, configPath, prompt, html, url, init, body.

app.on('ai:before:send', ({ action, prompt, url, body, stop }) => {
    if (shouldCancel()) {
        stop();
        return;
    }
    return { url, body: { ...body, prompt } };
});

ai:before:insert

Hook before generated HTML is inserted. Call stop() to skip insertion.

Payload: action, actionKey, configPath, prompt, html, insertHtml, blocks, selected.

app.on('ai:before:insert', ({ action, html, insertHtml, stop }) => {
    if (shouldCancel()) {
        stop();
        return;
    }
    return { html, insertHtml };
});

ai:complete

Fired when a request completes successfully.

Payload: action, actionKey, configPath, prompt, html, transformed.

app.on('ai:complete', ({ action, html, transformed }) => {
    console.log(action, transformed.length);
});

ai:insert

Fired when AI content is inserted into the document.

Payload: blocks, html, finalHtml, action, prompt, newBlocks, insert.

app.on('ai:insert', ({ action, html }) => {
    console.log(action, html.length);
});

ai:error

Fired on request or processing errors.

Payload: error, action.

app.on('ai:error', ({ error, action }) => {
    console.error('ai:error', action, error);
});

ai:prompt:open

Fired when the free-form prompt panel opens.

Payload: blocks, targetBlocks, action, currentAction, mode, currentMode.

app.on('ai:prompt:open', ({ action, blocks }) => {
    console.log('prompt open', action, blocks.length);
});

ai:prompt:close

Fired when the free-form prompt panel closes.

app.on('ai:prompt:close', () => {
    console.log('prompt closed');
});