An AI-assisted rich text editor component for Retool
reading · 00%

An AI-assisted rich text editor component for Retool

Rich text editing inside a Retool app, with an AI model drafting alongside the user.

Updated
Reading5 min
CategoryRetool
Words1,052

A support agent wants the model to rewrite one paragraph of a draft reply and leave the other four alone. A text area hands the app one string. To touch a single paragraph, you would write your own selection and parsing logic, and then parse the result again before it reaches a query.

We needed two things for a client's internal support tool. AI had to act on one block of text at a time. The output had to arrive as structured JSON, ready for queries, tables, and workflows without a parsing step. The result is an open-source Retool custom component built on Editor.js. We walked through it live at Retool's Build Together session. The code is on GitHub.

If you want the block editor without the AI layer, see Editor.js for Retool.

What the component does

Editor.js stores every paragraph, heading, and list as its own JSON block. The component adds three AI actions on top:

  • Rewrite and Summarize sit in each block's settings menu (the three-dot toolbar). The response replaces that block in place through editor.blocks.update(id, data).

  • Draft reply sits in the slash menu. Typing / and picking it inserts a placeholder block, then fills it with one paragraph block per chunk of the response.

None of the three calls a model from inside the component. The component fires a Retool event, a Retool query reads the context and calls the model, and the query writes the answer back. The prompt, the model, and the context stay in the Retool app, where you can change them without touching component code. That also means any endpoint works: Retool AI, OpenAI, Anthropic, or your own.

All Retool-specific wiring lives in one file, src/components/AIEditor/index.tsx. Swap a few hooks and the same editor runs as a standalone React component.

How the event model works

Retool custom component events carry no payload. The component sets its state properties first, then fires the event. Your query reads the state.

TypeScriptsrc/components/AIEditor/index.tsx (excerpt)
import { Retool } from "@tryretool/custom-component-support";

// Output state: the component writes, the app reads
const [, setTransformBlockContent] = Retool.useStateString({
  name: "transformBlockContent",
  inspector: "hidden",
});
const [, setTransformType] = Retool.useStateString({ name: "transformType", inspector: "hidden" });
const [transformBlockId, setTransformBlockId] = Retool.useStateString({
  name: "transformBlockId",
  inspector: "hidden",
});

// Input state: the app writes, the component reads
const [transformResponse] = Retool.useStateString({
  name: "transformResponse",
  inspector: "text",
  label: "Transform response",
});

const aiTransformRequest = Retool.useEventCallback({ name: "aiTransformRequest" });

// On a block action: set state first, then fire
setTransformBlockContent(content);
setTransformType(type);
setTransformBlockId(blockId);
aiTransformRequest();

When the query returns, the component compares the new transformResponse with the last one it applied. If the model returns identical text twice in a row, the component sees no change and nothing happens. The fix is to append a zero-width space in the query, String.fromCharCode(8203), so every response registers as new.

Component properties

Output properties (the component writes, your app reads):

  • editorData (object): the full Editor.js JSON, updated on every change, debounced at 400 ms.

  • transformBlockContent (string): the text of the block being transformed, empty for a draft insert.

  • transformType (string): summarize, rewrite, or draft.

  • transformBlockId (string): the Editor.js id of the block being transformed or replaced.

  • transformScope (string): block for the settings-menu actions, insert for Draft reply.

Input properties (your app writes, the component reads):

  • transformResponse (string): the model's response. Bind it to your query result.

  • readOnly (boolean): toggles read-only mode.

Two events: aiTransformRequest fires when someone picks any AI action, and editorChange fires on content changes, debounced at 400 ms.

Install it

You need Node.js 20 or later and admin permissions on your Retool organization. You also need a Retool API access token with read and write scopes for Custom Component Libraries.

Shellterminal
git clone https://github.com/StackdropCO/retool-ai-editor.git ai-editor-retool
cd ai-editor-retool
npm install

npx retool-ccl login    # paste the API access token
npx retool-ccl init     # first time only
npx retool-ccl dev      # syncs local changes to Retool on every save

Open any Retool app and drag the AIEditor component onto the canvas. To see the output, bind a Table or JSON Viewer to {{ AIEditor1.editorData.blocks }} and watch the blocks update as you type.

When it is ready to ship:

Shellterminal
npx retool-ccl deploy

Deploy pushes an immutable version to your Retool organization. Pin each app to a specific version under Custom Component settings in the app editor. Anyone in the organization can then add the component from the component panel.

Wire up the AI query

Create a query that calls your model endpoint. Branch on {{ AIEditor1.transformScope }} first, to pick the prompt shape, then on {{ AIEditor1.transformType }} for the action. For block actions, pass {{ AIEditor1.transformBlockContent }} as the user content.

For Draft reply, the block content is empty. Ground the draft in app state instead: the selected ticket, the order record, the customer's last message. The query runs in the app's full scope, so reference any query or component directly:

Plain textprompt context
{{ ticketQuery.data }}
{{ ticketsTable.selectedRow }}
{{ toneSelect.value }}

Return paragraphs separated by blank lines. The component splits them and inserts one paragraph block per chunk.

Then add an event handler on AIEditor1:

  1. Event: aiTransformRequest.

  2. Action: trigger your transform query.

  3. On success: set AIEditor1.transformResponse to {{ aiTransformQuery.data }}.

The component tracks transformBlockId itself, so your query never needs to.

Where it fits, and where it does not

The component suits replies, drafts, and short-form content: support tools, sales operations, and content review. Three limits come from the editor and the current design:

  • It gets heavy past about 50 blocks, so it is the wrong choice for long documents.

  • Editor.js has no built-in collaborative editing, so two people cannot edit the same draft at once.

  • Responses do not stream. The editor shows a "Drafting reply..." state until the full response arrives.

Our view

We recommend routing every AI action through a Retool query, because the prompt and the context then stay where the app's owner can change them. A model called from inside the component hides both. The same choice keeps the model swappable and puts each call under the app's existing permissions on that resource. The component only edits text. For when a custom component earns its place at all, see what a custom Retool component is, and when you need one. Pull requests and new use cases are welcome on the repository.

Worth passing along? Share this note.
LinkedInX
Newsletter

Get the next one in your inbox

We write when something is worth reading. Subscribing means you are happy to hear from us when that happens. Leave any time. Privacy policy.

Founder

John Miniadis

Keep readingRetool
Related notes

Still here? Good sign

More notes from real builds, written up for the next person who needs one.

All articles
ContactLet's chat
Let's chat

Tell us what
you're building

Describe the process that is slowing your team down: the inbox, the spreadsheet, the portal nobody owns. We'll come back with a concrete approach, a rough timeline, and, where it helps, a working prototype within weeks.

What happens next
  1. We read every messageAn architect reads what you sent before anyone replies.
  2. You hear back in ≤ 2 daysA short reply with first questions and a suggested next step.
  3. We scope and prototypeA scoping call, then a working surface, not a slide deck.
Where are you in the process?