Skip to content

WYSIWYG editor & AI assistant ​

The WYSIWYG renderer (options.wysiwyg: true) is still built on UEditor (Nuxt UI, Tiptap 3). It adds rich editing features and an optional AI writing assistant designed for an AI SDK backend owned by the host application.

{ wysiwyg: true } keeps working unchanged: AI, the formatting bubble and the / menu are off by default.

Editing features ​

OptionTypeDefaultDescription
featuresfalse | WysiwygFeaturesall onToggles each capability (Tiptap extension + toolbar entries). false removes them all.
features.taskListBooleantrueTask lists.
features.textAlignBooleantrueHeading and paragraph alignment.
features.tableBooleantrueTables, insertion and the contextual bubble (rows, columns, merge, headers).
features.textColorBooleantrueText color.
features.highlightBooleantrueMulticolor highlight.
features.linkEditorBooleantrueLink panel (rejects javascript:, data:…) instead of the native prompt().
features.findReplaceBooleantrueFind & replace (optional case sensitivity).
features.sourceModeBooleantrueRaw HTML or JSON editing; applied content goes through the Tiptap schema.
features.previewBooleantrueRead-only preview.
bubbleMenuBooleanfalseFormatting bubble on text selection.
slashCommandsBooleanfalse/ command palette.
aiWysiwygAiOptions—AI assistant (see below).

A host extension passed through options.extensions (e.g. its own Table) wins over the built-in extension of the same name. A custom toolbar is never altered; its items can use the built-in kinds: textColor, highlight, table, tableAction, findReplace, sourceMode, preview, ai.

AI assistant ​

ts
options: {
  wysiwyg: true,
  ai: {
    enabled: true,
    endpoint: '/api/ai/editor',
    actions: ['improve', 'fixGrammar', 'shorter', 'longer', 'summarize', 'translate'],
  },
}

No AI code runs unless ai.enabled === true and an endpoint / transport is provided.

Flow ​

  1. The user selects text and picks an action (bubble, toolbar or /ai).
  2. The request goes to the application backend, which calls AI SDK.
  3. The answer is streamed into a detached panel: the document is never modified while generating.
  4. The user chooses: Replace selection / Replace block, Insert, Insert below, Retry, Stop (aborts the request through AbortController) or Discard.

Without a selection, a transform action targets the current block; a generate action (continue, paragraph, ask without selection) inserts at the cursor. If the targeted text changes while generating, the replacement is refused.

ai options ​

OptionTypeDefaultDescription
enabledBoolean—Required (true).
endpointString—Backend route used by the default HTTP transport.
transportWysiwygAiTransport—Custom transport; takes precedence over endpoint.
actionsArrayallDefault ids and/or { id, label, icon?, description?, instruction?, mode?, promptForInstruction? } actions.
bubbleMenuBooleantrueAI menu in the selection bubble.
slashCommandsBooleantrueAI entries in the / palette (filter with /ai).
streamBooleantrueUses transport.stream() when available.
maxContextCharactersNumber12000Budget for the document context sent.
includeHtml / includeJsonBooleanfalseAttach the document HTML / Tiptap JSON.
format"text" | "html""text"Expected format. HTML is sanitized, then filtered by the schema.
headersObject—HTTP transport headers (never provider keys).
bodyObject—Data forwarded as-is in request.metadata.
labelsObjectFrenchMenu and panel labels.

Default actions (DEFAULT_WYSIWYG_AI_ACTIONS): improve, fixGrammar, shorter, longer, simplify, rewrite, professional, summarize, translate, continue, paragraph, ask. Their instructions are provider-agnostic.

Request payload ​

ts
type WysiwygAiRequest = {
  action: string
  instruction?: string
  mode: 'transform' | 'generate'
  selection?: { from: number; to: number }
  selectedText?: string
  currentBlock?: string
  textBeforeSelection?: string
  textAfterSelection?: string
  documentText: string // truncated to maxContextCharacters
  truncated?: boolean
  html?: string // only with includeHtml
  json?: JSONContent // only with includeJson
  format: 'text' | 'html'
  metadata?: Record<string, unknown>
  stream: boolean // added by the HTTP transport
}

AI SDK backend (Nuxt / Nitro example) ​

The library depends neither on ai nor on any @ai-sdk/* package, and never calls a provider from the browser. The model and API key stay on the server.

ts
// server/api/ai/editor.post.ts
import { streamText } from 'ai'
import { openai } from '@ai-sdk/openai'

const SYSTEM = `
You are an AI writing assistant embedded inside a rich-text editor.
Follow the requested editing action precisely.
Answer in the language of the text unless asked otherwise.
Return only the proposed text, without quotes or commentary.
`.trim()

const buildPrompt = (r: Record<string, any>) =>
  [
    `Action: ${r.action}`,
    r.instruction && `Instruction: ${r.instruction}`,
    r.selectedText ? `Text:\n${r.selectedText}` : `Text before cursor:\n${r.textBeforeSelection}`,
    r.textAfterSelection && `Text after:\n${r.textAfterSelection}`,
  ]
    .filter(Boolean)
    .join('\n\n')

export default defineEventHandler(async (event) => {
  const request = await readBody(event)
  // Authenticate the user and rate-limit here.
  const result = streamText({
    model: openai(process.env.AI_MODEL ?? 'gpt-5-mini'),
    system: SYSTEM,
    prompt: buildPrompt(request),
  })
  return result.toUIMessageStreamResponse() // or result.toTextStreamResponse()
})

The provider is swappable without touching the editor: anthropic(...), google(...), mistral(...), xai(...), OpenRouter, any OpenAI-compatible provider, or a local model exposed through an AI SDK provider.

The default HTTP transport understands:

  • toUIMessageStreamResponse(): text-delta, error and data-* parts carrying { operations };
  • toTextStreamResponse(): streamed plain text;
  • a JSON { text }, { operation, content, explanation } or { operations } body, e.g. from generateText({ output: Output.object(...) }) or generateObject().

Custom transport ​

ts
import { createWysiwygAiHttpTransport, type WysiwygAiTransport } from '@ficsysfr/jsonforms_builder'

const transport = createWysiwygAiHttpTransport({
  endpoint: '/api/ai/editor',
  headers: () => ({ 'x-csrf-token': csrfToken() }),
})

// Or fully custom:
const custom: WysiwygAiTransport = {
  async generate(request, { signal } = {}) {
    return { text: await myClient.rewrite(request, signal) }
  },
  async *stream(request, { signal } = {}) {
    for await (const delta of myClient.stream(request, signal)) yield delta
  },
}

Pass the transport in a UI schema built in code (ai: { enabled: true, transport }); a serialized JSON UI schema uses endpoint.

Structured operations and agent tools ​

A response can propose operations (replaceSelection, insertBelow, insertAtCursor, replaceText, insertContent). They are validated, then applied only after clicking Apply.

createWysiwygEditorTools(editor) exposes tools shaped like AI SDK tool() (description, JSON Schema inputSchema, execute) for a future agent mode:

  • read-only, safe to run automatically: getSelection, getDocumentText, getCurrentBlock, getHeadings, findText;
  • proposal, never mutating: proposeReplaceSelection, proposeInsertContent, proposeReplaceText return a WysiwygAiOperation that needs approval (applyWysiwygAiOperation).

Security ​

  • No provider key in the browser: authentication, quotas and model choice belong to the backend.
  • Generated text is inserted as Tiptap text nodes, never interpreted as HTML.
  • With format: 'html', output goes through an allowlist (tags; href limited to http(s), mailto, tel and relative) then through the Tiptap schema; the preview never uses v-html.
  • A read-only, disabled or previewing editor neither runs nor applies AI actions.

Playground ​

Released under the BSD-3-Clause License.