Human-in-the-Loop
By default, the AI applies changes immediately. Setapproval_mode to "ask_every_time" to review changes before they take effect.
HITL requires the async workflow (/v1/chat/async) because the job pauses to wait for your approval.
This guide covers UI-driven approval (a human reviewing in your product’s interface). If your AI agent is the one deciding whether to approve proposed changes (no human in the loop), see Agent Tool Integration → Approval modes. For batch / server-side workflows that auto-approve every change, see Server Integration and pass
approval_mode: "approve_all" instead.Recommended UX pattern
Default your UI to auto-apply; expose Review Mode as an opt-in toggle. The simplest, least-friction integration is to sendapproval_mode: "approve_all" by default and put a small in-UI toggle — one control, clearly labelled — that the user can flip on when they want to review each change before it lands. When the toggle is on, your code switches to approval_mode: "ask_every_time" and renders the proposed-change UI (chat-side card, inline editor overlay, or native track-changes — see below).
This is the pattern the SuperDocs web app at use.superdocs.app uses, and it’s what most users expect:
- Auto-apply on by default — for 90% of edits (rewording, formatting, small additions), users don’t want to approve each change. They want the AI to act, then Cmd-Z if they disagree.
- Review Mode as an explicit opt-in — when the user is working on something high-stakes (a contract clause, a regulatory filing, a legal letter) they flip the toggle ON and approve each change explicitly. Persist the toggle state in
localStorageso it survives page reload. - No hidden state — both modes should be plainly visible in the UI. Don’t bury Review Mode in a settings modal.
ask_every_time without a toggle is valid for high-stakes products (contracts, medical records, court filings) where every change must be reviewed, but expect higher friction in casual editing flows. If in doubt, start with auto-apply by default and a toggle — you can always reverse it later.
End-to-end workflow
1. Send a request with approval mode
2. Poll for approval status
status is "awaiting_approval", check metadata.pending_changes:
3. Approve or deny changes
Approve a single change:4. Continue polling
After approval, the job resumes processing. Poll untilstatus is "completed" to get the final result.
If you deny a change with feedback, the AI receives your feedback and may propose a revised change. Your polling loop should handle multiple rounds of
awaiting_approval — not just one.Understanding proposed changes
Operation types
Each proposed change has anoperation field that determines what the AI wants to do and which fields are populated:
| Operation | old_html | new_html | insert_after_chunk_id | Meaning |
|---|---|---|---|---|
edit | Current content | Proposed replacement | — | Modify an existing section |
create | null | New content to add | Section to insert after | Add a new section to the document |
delete | Content being removed | null | — | Remove a section from the document |
Building a diff view
To show users what the AI wants to change, compareold_html and new_html:
- For
edit: Use a diff library (likediff-match-patchorjsdiff) to highlight additions and removals betweenold_htmlandnew_html. Show a before/after view or inline diff. - For
create: Displaynew_htmlwith a visual indicator that this is a new section being added (e.g., a green border or “New section” label). Theinsert_after_chunk_idvalue corresponds to adata-chunk-idattribute on an element in the document HTML — find that element and insert the new content after it. - For
delete: Displayold_htmlwith a visual indicator that this section will be removed (e.g., red strikethrough or “Will be deleted” label).
ai_explanation alongside the diff — it tells the user why the AI proposed the change.
Rendering diffs inline in your editor
Three patterns for where the diff appears, in order of visual weight:Pattern 1 — Side-by-side card in your chat panel
Render a card per pending change withold_html and new_html shown as two adjacent panels (red background for old, green background for new) and Approve / Deny buttons underneath. The card lives in your chat sidebar; the editor document is unaffected until the user approves. This is the simplest pattern and works with any editor. See the JavaScript example for a working HITL flow that produces this UI.
Pattern 2 — Inline overlay in the editor (Cursor-style)
The proposed edit appears directly inside the document — the affected block gets a coloured outline, the word-level diff renders inside it (red strikethrough for removed, green highlight for added), and Approve / Deny buttons float in the top-right corner of the block. This is the most accurate visual representation of what the AI wants to change and is the pattern most developer-audience apps converge on. For ProseMirror-based editors (ProseMirror, TipTap, BlockNote, Remirror, Atlaskit), use the editor’s native decoration system. The plugin below collectsproposed_change events, builds a DecorationSet keyed by data-chunk-id, and dispatches window-level custom events when the user clicks Approve or Deny.
- Register
proposedChangeDecoration()in the plugin array when you buildEditorState. - On every
proposed_changeSSE event in the chat panel, calladdProposedChange(view, change). - Subscribe to
window.addEventListener("diff-action", ...)in the chat panel to catch Accept / Deny clicks. Post the decision to/v1/chat/{session_id}/approveand callremoveProposedChange(view, change_id). - On every
finalSSE event, callclearProposedChanges(view)before applying the new HTML — the editor is about to re-render anyway.
Pattern 3 — Native track-changes UI
If your editor already supports track-changes (CKEditor 5 with the Track Changes feature, TinyMCE Premium, etc.), map eachproposed_change to a tracked suggestion in the editor’s native model. The user then approves or rejects via the editor’s built-in UI. Consult your editor’s track-changes API docs for the specific mapping — the SuperDocs side is identical to Pattern 1 or 2.
Batch changes
When the AI proposes multiple changes at once, each change includes:batch_id— Groups related changes. All changes in a batch share the samebatch_id.batch_total— How many changes are in this batch.
batch_total events with the same batch_id before showing the review UI. You can then offer “Accept All” and “Deny All” buttons alongside individual change controls.
For polling, all changes appear together in the metadata.pending_changes array.
Alternative: SSE streaming workflow
The polling workflow above works but requires repeated API calls. For a real-time UI, use SSE to receive proposed changes as they’re generated:After calling
/approve, the AI resumes processing. If approved changes were applied, the SSE connection will eventually emit a final event with the updated document. If you denied with feedback, you may receive new proposed_change events as the AI tries again.
