Review and approveRewrite toolbar
Built by DesignMe, free to use under the MIT licence.
Expert design and dev support for startups and enterprises.
Book a callRewrite toolbar
Select a sentence and let the agent rewrite it in place. Free and MIT licensed, as HTML or a typed React component, in two styles with light and dark mode.
Install
Add it to your project with the CLI.
HTML, CSS and JavaScript
npx designme add rewrite-toolbarReact
npx designme add rewrite-toolbar --reactThe CLI copies the source into your project, so you own it and can change anything. The HTML version lands in ./designme with the shared core and a demo.html. The React version lands in src/components/designme and needs React 19; it works in Next.js, Vite and Astro.
About
What it is for and what it does.
Rewrite toolbar lets a person click a sentence in a document and get a small toolbar with rewrite options, such as Clearer, Shorter or Friendlier. The agent's new sentence is written in word by word next to the struck-out original, and the person accepts it or puts the old one back.
- When people edit copy with an agent and want to change one sentence without regenerating the whole text.
- When the old and new wording should be visible side by side before anything is saved.
- When you want a few one-click rewrite styles plus a way to open a fuller agent prompt.
- When a shortcut or command menu should be able to trigger the same rewrites.
- The text is passed as
sentences, one string each, so a click selects exactly one sentence. The toolbar appears above it, or below it when there is no room above. actionssets the buttons, Clearer, Shorter and Friendlier by default. Each one callsonRewritewith the sentence, the action key and its index.- While
onRewriteworks the toolbar says Rewriting. The answer is then typed in word by word, and the toolbar offers Accept and Undo. - Accept saves the sentence and calls
onSentencesChangewith the full text. Undo, or a failedonRewrite, leaves the original sentence as it was. - The wand button calls
onAskif you pass it, or runs the first action. Its tooltip showsaskLabeland theaskShortcuthint. - A ref exposes
rewrite(action),accept()andundo()so you can drive it from your own shortcuts or scripts.
- The toolbar has
role="toolbar"witharia-label="Rewrite", and it isinertwhile no sentence is selected. - The wand button has an
aria-labeltaken fromaskLabel. - With reduced motion turned on, the rewrite appears all at once instead of word by word.
Usage
Import it and drive it from your own state.
Import
import { RewriteToolbar } from '@/components/designme/rewrite-toolbar/RewriteToolbar';Example
The demo above as a React component, 34 lines you can copy.
'use client';
// The animated preview from designme.dev, rebuilt from the React component: an example of driving it from your own state.
import { useRef, useState } from 'react';
import { RewriteToolbar, type RewriteToolbarHandle } from './RewriteToolbar';
import { useStory } from '../lib/useScript';
/* ---------- Rewrite toolbar: the first sentence is picked, made clearer and kept ---------- */
const TEXT = [
'Welcome to DesignMe, a place where you and your team can work together with an agent on all of your design files in one place.',
'To get started you will need to connect Figma, which can be done from the settings page at any time.',
'After that, the agent will be able to read your frames and suggest improvements.'
];
const REWRITES: Record<string, string>[] = [
{ clear: 'DesignMe is where your team and an agent design together.', short: 'Design with your team and an agent.', warm: 'Welcome in. This is where you, your team and your agent make things together.' },
{ clear: 'Connect Figma in Settings to get started.', short: 'First, connect Figma.', warm: 'Start by connecting Figma. It takes about a minute.' },
{ clear: 'Then the agent can read your frames and suggest fixes.', short: 'Then the agent reviews your frames.', warm: 'Then your agent can look through your frames and help polish them.' }
];
export default function RewriteToolbarDemo() {
const [selected, setSelected] = useState<number | null>(null);
const toolbar = useRef<RewriteToolbarHandle>(null), ref = useRef<HTMLDivElement>(null);
// plays once on its own: picks the first sentence, makes it clearer and keeps it. Clicking, focusing or typing in it takes over
useStory(ref, async ({ wait }) => {
await wait(1300); setSelected(s => s ?? 0);
await wait(1200); toolbar.current?.rewrite('clear');
await wait(2600); toolbar.current?.accept();
});
return (
<div ref={ref} style={{ width: '100%' }}>
<RewriteToolbar ref={toolbar} title="Onboarding intro" defaultSentences={TEXT} selected={selected} onSelectedChange={setSelected}
onRewrite={(_, action, i) => new Promise<string>(res => setTimeout(() => res(REWRITES[i][action]), 350))} />
</div>
);
}
Props
Everything RewriteToolbar takes.
| Prop | Type | What it does |
|---|---|---|
titlerequired | ReactNode | The document's name |
kind | string | The file type tag in front of the name. Defaults to DOC |
sentences | string[] | The text, one sentence each, so a click picks a sentence |
defaultSentences | string[] | |
onSentencesChange | (sentences: string[]) => void | Called with every sentence once a rewrite is accepted |
selected | number | null | Index of the sentence with the toolbar on it, or null. Leave it out to let clicks pick it |
defaultSelected | number | null | |
onSelectedChange | (index: number | null) => void | |
actions | RewriteAction[] | Defaults to Clearer, Shorter and Friendlier |
onRewriterequired | (sentence: string, action: string, index: number) => Promise<string> | string | Writes the new sentence. While it works the toolbar says Rewriting; the answer is then written in word by word. If it throws, the sentence is left as it was |
onAsk | (sentence: string, index: number) => void | The wand at the end of the toolbar. Left out, it rewrites with the first action |
askLabel | string | The tip on the wand |
askShortcut | string | The shortcut shown in that tip |
ref | Ref<RewriteToolbarHandle> | |
className | string |
Code
The full source, as the CLI installs it.
RewriteToolbar.tsxReact · 173 lines
'use client';
import { useEffect, useImperativeHandle, useRef, useState, type ReactNode, type Ref } from 'react';
import { cx } from '../lib/cx';
import { useControllable, useIsoLayoutEffect, useReducedMotion } from '../lib/hooks';
import { Icon, type IconName } from '../lib/icons';
import { Swap, Think, Tip } from '../lib/motion';
import '../designme.css';
import './rewrite-toolbar.css';
/** A button in the toolbar, like Clearer or Shorter */
export interface RewriteAction {
key: string;
label: string;
icon?: IconName;
}
/** What a ref to the toolbar can do: drive it from a shortcut, a command menu or a scripted demo */
export interface RewriteToolbarHandle {
/** Rewrites the selected sentence with one of the actions, as a click on that action does */
rewrite: (action: string) => void;
/** Keeps the rewrite that is waiting for review */
accept: () => void;
/** Puts the sentence back as it was */
undo: () => void;
}
export interface RewriteToolbarProps {
/** The document's name */
title: ReactNode;
/** The file type tag in front of the name. Defaults to DOC */
kind?: string;
/** The text, one sentence each, so a click picks a sentence */
sentences?: string[];
defaultSentences?: string[];
/** Called with every sentence once a rewrite is accepted */
onSentencesChange?: (sentences: string[]) => void;
/** Index of the sentence with the toolbar on it, or null. Leave it out to let clicks pick it */
selected?: number | null;
defaultSelected?: number | null;
onSelectedChange?: (index: number | null) => void;
/** Defaults to Clearer, Shorter and Friendlier */
actions?: RewriteAction[];
/** Writes the new sentence. While it works the toolbar says Rewriting; the answer is then written in word by word.
If it throws, the sentence is left as it was */
onRewrite: (sentence: string, action: string, index: number) => Promise<string> | string;
/** The wand at the end of the toolbar. Left out, it rewrites with the first action */
onAsk?: (sentence: string, index: number) => void;
/** The tip on the wand */
askLabel?: string;
/** The shortcut shown in that tip */
askShortcut?: string;
ref?: Ref<RewriteToolbarHandle>;
className?: string;
}
const ACTIONS: RewriteAction[] = [{ key: 'clear', label: 'Clearer', icon: 'spark' }, { key: 'short', label: 'Shorter' }, { key: 'warm', label: 'Friendlier' }];
type Job = { index: number; old: string; text?: string; shown: number };
/** Click a sentence and a small toolbar offers rewrites; the new text is written in beside the old one, to accept or undo. */
export function RewriteToolbar({
title, kind = 'DOC', sentences, defaultSentences = [], onSentencesChange, selected, defaultSelected = null, onSelectedChange,
actions = ACTIONS, onRewrite, onAsk, askLabel = 'Ask the agent', askShortcut = '⌘K', ref, className
}: RewriteToolbarProps) {
const [text, setText] = useControllable(sentences, defaultSentences, onSentencesChange);
const [sel, setSel] = useControllable(selected, defaultSelected, onSelectedChange);
const [job, setJob] = useState<Job | null>(null);
const [status, setStatus] = useState('Click a sentence');
const reduce = useReducedMotion();
const root = useRef<HTMLDivElement>(null);
const bar = useRef<HTMLDivElement>(null);
const head = useRef<HTMLDivElement>(null);
const spans = useRef<(HTMLSpanElement | null)[]>([]);
const [at, setAt] = useState<{ left: number; top: number }>({ left: 0, top: 0 });
// keeping a shorter sentence keeps the doc's height, with the text centred in it, so it doesn't shrink under the reader
const busy = job !== null;
// the toolbar turns to Rewritten one beat after the last word, as the site does
const phase = !busy ? 'menu' : job.text !== undefined && job.shown > words(job.text).length ? 'review' : 'working';
// while the toolbar fades away it keeps what it last showed
const [last, setLast] = useState(phase);
if (sel !== null && phase !== last) setLast(phase);
const view = sel !== null ? phase : last;
// a new selection from outside says the same as a click does
useEffect(() => { if (sel !== null && !busy) setStatus('Pick a rewrite'); }, [sel]); // eslint-disable-line react-hooks/exhaustive-deps
// the toolbar sits above the sentence's first line, or under its last when above would cover the header. The new
// sentence takes its full room as soon as it arrives, so the toolbar is placed clear of it before the words come in
useIsoLayoutEffect(() => {
const el = sel === null ? null : spans.current[sel], tb = bar.current, box = root.current;
if (!el || !tb || !box) return;
const br = box.getBoundingClientRect(), rects = el.getClientRects(), f = rects[0]; if (!f) return;
const left = Math.max(8, Math.min(f.left - br.left, br.width - tb.offsetWidth - 8));
let top = f.top - br.top - tb.offsetHeight - 8;
if (top < (head.current ? head.current.getBoundingClientRect().bottom - br.top : 4)) top = rects[rects.length - 1].bottom - br.top + 8;
setAt({ left, top });
}, [sel, view, job?.text]);
// a click anywhere else puts the toolbar away, unless a rewrite is waiting on an answer
const latest = useRef({ busy, setSel });
useEffect(() => { latest.current = { busy, setSel }; });
useEffect(() => {
const down = (e: PointerEvent) => { const l = latest.current; if (!l.busy && !root.current?.contains(e.target as Node)) l.setSel(null); };
document.addEventListener('pointerdown', down);
return () => document.removeEventListener('pointerdown', down);
}, []);
// the answer is written in a word at a time
useEffect(() => {
if (!job?.text || job.shown > words(job.text).length) return;
const t = setTimeout(() => setJob(j => j && { ...j, shown: reduce ? words(j.text!).length + 1 : j.shown + 1 }), job.shown === 0 ? 0 : 55);
return () => clearTimeout(t);
}, [job, reduce]);
const pick = (i: number) => { if (busy) return; setSel(i); setStatus('Pick a rewrite'); };
const rewrite = async (action: string) => {
if (sel === null || busy) return;
const index = sel, old = text[index];
setJob({ index, old, shown: 0 });
let out: string;
// if the rewrite fails, the sentence goes back as it was and the toolbar offers the rewrites again
try { out = await onRewrite(old, action, index); } catch { setJob(null); return; }
setJob(j => j && j.index === index ? { ...j, text: out } : j);
};
const finish = (keep: boolean) => {
if (!job?.text) return;
if (keep) setText(text.map((s, i) => i === job.index ? job.text! : s));
setJob(null); setSel(null); setStatus(keep ? 'Saved' : 'Click a sentence');
};
useImperativeHandle(ref, () => ({ rewrite, accept: () => finish(true), undo: () => finish(false) }));
return (
<div data-dm="" ref={root} className={cx('dm-rwx dm-panel', className)}>
<div ref={head} className="dm-rwx-h"><span className="dm-ft">{kind}</span><span>{title}</span><Swap className="dm-muted dm-rwx-st">{status}</Swap></div>
<div className="dm-rwx-doc">
{text.map((s, i) => {
const mine = job?.index === i;
return (
<span key={i}>
{i > 0 && ' '}
<span ref={el => { spans.current[i] = el; }} className={cx('dm-rwx-s', sel === i && 'dm-sel')} onClick={e => { e.stopPropagation(); if (!mine) pick(i); }}>
{mine ? <><del>{job.old}</del> <ins>{job.text && words(job.text).map((w, k) => <span key={k} className={cx('dm-w', k < job.shown && 'dm-on')}>{w} </span>)}</ins></> : s}
</span>
</span>
);
})}
</div>
<div ref={bar} className={cx('dm-rwx-tb', sel !== null && 'dm-show', view !== 'menu' && 'dm-act')} role="toolbar" aria-label="Rewrite" inert={sel === null} style={at}>
{view === 'menu' ? (
<>
{actions.map(a => <button type="button" key={a.key} className="dm-rwx-b" onClick={() => rewrite(a.key)}>{a.icon && <Icon name={a.icon} size="xs" />}{a.label}</button>)}
<span className="dm-rwx-sep" />
<Tip label={askLabel} kbd={askShortcut}>
<button type="button" className="dm-rwx-b dm-ic" aria-label={askLabel} onClick={() => (onAsk && sel !== null ? onAsk(text[sel], sel) : rewrite(actions[0].key))}><Icon name="wand" size="xs" /></button>
</Tip>
</>
) : view === 'working' ? (
<span className="dm-lbl"><Think />Rewriting</span>
) : (
<>
<span className="dm-lbl"><Icon name="check" size="xs" />Rewritten</span>
<button type="button" className="dm-rwx-b dm-rwx-un" onClick={() => finish(false)}>Undo</button>
<button type="button" className="dm-pill dm-accent dm-sm dm-rwx-ok" onClick={() => finish(true)}>Accept</button>
</>
)}
</div>
</div>
);
}
const words = (s: string) => s.split(' ');
rewrite-toolbar.cssReact · 27 lines
/* Generated from designme.dev by scripts/sync.mjs. Edit the site, then sync. */
/* ---------- Rewrite toolbar ---------- */
.dm-rwx{padding:14px var(--dm-s-4) var(--dm-s-4);position:relative}
.dm-rwx-h{display:flex;align-items:center;gap:var(--dm-s-2);font-size:13px;font-weight:500;margin-bottom:10px}
.dm-rwx-st{margin-left:auto;font-weight:400;font-size:12px}
.dm-rwx-doc{font-size:14px;line-height:1.75;color:var(--dm-fg2)}
.dm-rwx-s{border-radius:5px;cursor:text;transition:background .3s,color .3s;padding:1px 0;box-decoration-break:clone;-webkit-box-decoration-break:clone}
@media (hover:hover) and (pointer:fine){.dm-rwx-s:hover{background:var(--dm-row);color:var(--dm-fg)}}
.dm-rwx-s.dm-sel{background:var(--dm-sel);color:var(--dm-fg)}
.dm-rwx-s.dm-sel del{color:var(--dm-fg2)}
.dm-rwx-s del{color:var(--dm-muted);text-decoration-color:var(--dm-bad)}
.dm-rwx-s ins{text-decoration:none;color:var(--dm-fg)}
/* the new sentence takes its full room from the start, so the toolbar can sit clear of it while the words come in */
.dm-rwx-s ins .dm-w{visibility:hidden}
.dm-rwx-s ins .dm-w.dm-on{visibility:visible;animation:dm-rww .4s var(--dm-ease) both}
@keyframes dm-rww{from{opacity:0;filter:blur(3px)}to{opacity:1;filter:none}}
.dm-rwx-tb{position:absolute;left:0;top:0;display:flex;align-items:center;gap:2px;padding:var(--dm-s-1);border-radius:var(--dm-r-pill);background:var(--dm-ink);color:var(--dm-on-ink);box-shadow:var(--dm-shadow-pop);opacity:0;pointer-events:none;transform:translateY(6px) scale(.96);filter:blur(4px);transition:opacity .3s var(--dm-ease),transform .4s var(--dm-ease),filter .3s;z-index:3;white-space:nowrap}
.dm-rwx-tb.dm-show{opacity:1;pointer-events:auto;transform:none;filter:none}
.dm-rwx-b{display:inline-flex;align-items:center;gap:6px;height:28px;padding:0 11px;border-radius:var(--dm-r-pill);font-size:12.5px;color:inherit;opacity:.85;transition:background .2s,opacity .2s}
.dm-rwx-b.dm-hl{background:color-mix(in srgb,var(--dm-on-ink) 14%,transparent);opacity:1}
@media (hover:hover) and (pointer:fine){.dm-rwx-b:hover{background:color-mix(in srgb,var(--dm-on-ink) 14%,transparent);opacity:1}}
.dm-rwx-b.dm-ic{width:28px;padding:0;justify-content:center}
.dm-rwx-sep{width:1px;height:16px;background:color-mix(in srgb,var(--dm-on-ink) 25%,transparent);margin:0 2px}
.dm-rwx-tb.dm-act{padding:var(--dm-s-1) var(--dm-s-1) var(--dm-s-1) var(--dm-s-3)}
.dm-rwx-tb .dm-lbl{font-size:12.5px;margin-right:6px;display:flex;align-items:center;gap:7px}
.dm-rwx-tb .dm-pill.dm-sm{height:28px}
rewrite-toolbar.htmlHTML · 12 lines
<div class="card" data-c="rewrite-toolbar">
<div class="stage-in">
<div class="rwx panel">
<div class="rwx-h"><span class="ft">DOC</span><span>Onboarding intro</span><span class="muted rwx-st">Click a sentence</span></div>
<div class="rwx-doc"></div>
<div class="rwx-tb" role="toolbar" aria-label="Rewrite">
<button class="rwx-b" data-k="clear"><svg class="i xs"><use href="#i-spark"/></svg>Clearer</button><button class="rwx-b" data-k="short">Shorter</button><button class="rwx-b" data-k="warm">Friendlier</button><span class="rwx-sep"></span><button class="rwx-b ic" aria-label="Ask the agent" data-tip="Ask the agent" data-tip-k="⌘K"><svg class="i xs"><use href="#i-wand"/></svg></button>
</div>
</div>
</div>
</div>
rewrite-toolbar.cssHTML · 26 lines
/* ---------- Rewrite toolbar ---------- */
.rwx{padding:14px var(--s-4) var(--s-4);position:relative}
.rwx-h{display:flex;align-items:center;gap:var(--s-2);font-size:13px;font-weight:500;margin-bottom:10px}
.rwx-st{margin-left:auto;font-weight:400;font-size:12px}
.rwx-doc{font-size:14px;line-height:1.75;color:var(--fg2)}
.rwx-s{border-radius:5px;cursor:text;transition:background .3s,color .3s;padding:1px 0;box-decoration-break:clone;-webkit-box-decoration-break:clone}
@media (hover:hover) and (pointer:fine){.rwx-s:hover{background:var(--row);color:var(--fg)}}
.rwx-s.sel{background:var(--sel);color:var(--fg)}
.rwx-s.sel del{color:var(--fg2)}
.rwx-s del{color:var(--muted);text-decoration-color:var(--bad)}
.rwx-s ins{text-decoration:none;color:var(--fg)}
/* the new sentence takes its full room from the start, so the toolbar can sit clear of it while the words come in */
.rwx-s ins .w{visibility:hidden}
.rwx-s ins .w.on{visibility:visible;animation:rww .4s var(--ease) both}
@keyframes rww{from{opacity:0;filter:blur(3px)}to{opacity:1;filter:none}}
.rwx-tb{position:absolute;left:0;top:0;display:flex;align-items:center;gap:2px;padding:var(--s-1);border-radius:var(--r-pill);background:var(--ink);color:var(--on-ink);box-shadow:var(--shadow-pop);opacity:0;pointer-events:none;transform:translateY(6px) scale(.96);filter:blur(4px);transition:opacity .3s var(--ease),transform .4s var(--ease),filter .3s;z-index:3;white-space:nowrap}
.rwx-tb.show{opacity:1;pointer-events:auto;transform:none;filter:none}
.rwx-b{display:inline-flex;align-items:center;gap:6px;height:28px;padding:0 11px;border-radius:var(--r-pill);font-size:12.5px;color:inherit;opacity:.85;transition:background .2s,opacity .2s}
.rwx-b.hl{background:color-mix(in srgb,var(--on-ink) 14%,transparent);opacity:1}
@media (hover:hover) and (pointer:fine){.rwx-b:hover{background:color-mix(in srgb,var(--on-ink) 14%,transparent);opacity:1}}
.rwx-b.ic{width:28px;padding:0;justify-content:center}
.rwx-sep{width:1px;height:16px;background:color-mix(in srgb,var(--on-ink) 25%,transparent);margin:0 2px}
.rwx-tb.act{padding:var(--s-1) var(--s-1) var(--s-1) var(--s-3)}
.rwx-tb .lbl{font-size:12.5px;margin-right:6px;display:flex;align-items:center;gap:7px}
.rwx-tb .pill.sm{height:28px}
rewrite-toolbar.jsHTML · 52 lines
DM.def('rewrite-toolbar', (r, c) => {
r.innerHTML = r.innerHTML.replace('{spark}', I('spark','xs')).replace('{at}', I('wand','xs'));
const S = [
['Welcome to DesignMe, a place where you and your team can work together with an agent on all of your design files in one place.', { clear:'DesignMe is where your team and an agent design together.', short:'Design with your team and an agent.', warm:'Welcome in. This is where you, your team and your agent make things together.' }],
['To get started you will need to connect Figma, which can be done from the settings page at any time.', { clear:'Connect Figma in Settings to get started.', short:'First, connect Figma.', warm:'Start by connecting Figma. It takes about a minute.' }],
['After that, the agent will be able to read your frames and suggest improvements.', { clear:'Then the agent can read your frames and suggest fixes.', short:'Then the agent reviews your frames.', warm:'Then your agent can look through your frames and help polish them.' }]
];
const doc = $('.rwx-doc', r), tb = $('.rwx-tb', r), st = $('.rwx-st', r), box = $('.rwx', r);
doc.innerHTML = S.map((s, i) => `<span class="rwx-s" data-i="${i}">${s[0]}</span>`).join(' ');
const tbHTML = tb.innerHTML; let sel = null, busy = false;
// shorter before a rewrite than during one, it stays centred in the room its stage keeps for it
centre(c, box, () => box.offsetHeight);
function place(el){
const br = box.getBoundingClientRect(), rects = el.getClientRects(), f = rects[0];
const w = tb.offsetWidth; let x = f.left - br.left; x = Math.max(8, Math.min(x, br.width - w - 8));
// above the sentence, unless that would cover the header: then under its last line
let y = f.top - br.top - tb.offsetHeight - 8; if (y < $('.rwx-h', r).getBoundingClientRect().bottom - br.top) y = rects[rects.length - 1].bottom - br.top + 8;
tb.style.left = x + 'px'; tb.style.top = y + 'px';
}
// hidden, the toolbar takes no focus
function hide(){ tb.classList.remove('show'); tb.inert = true; }
tb.inert = true;
function bindTb(){ $$('.rwx-b[data-k]', tb).forEach(b => b.onclick = () => rewrite(b.dataset.k)); const a = $('.rwx-b.ic', tb); if (a) a.onclick = () => rewrite('clear'); }
function select(el){
if (busy) return; $$('.rwx-s', doc).forEach(x => x.classList.toggle('sel', x === el)); sel = el;
tb.classList.remove('act'); tb.innerHTML = tbHTML; bindTb(); place(el); tb.inert = false; tb.classList.add('show'); swap(st, 'Pick a rewrite');
}
$$('.rwx-s', doc).forEach(el => el.onclick = e => { e.stopPropagation(); if (!el.querySelector('ins')) select(el); });
c.on(document, 'pointerdown', e => { if (!busy && !r.contains(e.target)) { hide(); $$('.rwx-s', doc).forEach(x => x.classList.remove('sel')); } });
function rewrite(k){
const el = sel, i = +el.dataset.i, old = S[i][0], nw = S[i][1][k]; busy = true;
tb.classList.add('act'); tb.innerHTML = `<span class="lbl"><span class="think on"><i></i><i></i><i></i><i></i></span>Rewriting</span>`; place(el);
c.run(async () => {
el.innerHTML = `<del>${old}</del> <ins>${nw.split(' ').map(w => `<span class="w">${w} </span>`).join('')}</ins>`; place(el); await c.wait(350);
for (const w of $$('ins .w', el)){ w.classList.add('on'); await c.wait(55); }
tb.innerHTML = `<span class="lbl">${I('check','xs')}Rewritten</span><button class="rwx-b rwx-un">Undo</button><button class="pill accent sm rwx-ok">Accept</button>`; place(el);
// keeping a shorter sentence lets the doc fit its text, with no empty room above or below it
$('.rwx-ok', tb).onclick = () => { el.textContent = nw; S[i][0] = nw; el.classList.remove('sel'); hide(); busy = false; swap(st, 'Saved'); };
$('.rwx-un', tb).onclick = () => { el.textContent = old; el.classList.remove('sel'); hide(); busy = false; swap(st, 'Click a sentence'); };
});
}
bindTb();
// the preview plays it once on its own after the entrance: pick a sentence, make it clearer, keep it. A click of yours takes over
c.run(async () => {
await c.step(1300); if (sel) return; select($$('.rwx-s', doc)[0]);
await c.step(1200); const b = $('.rwx-b[data-k="clear"]', tb); if (busy || !b) return;
b.classList.add('hl'); c.taken(() => b.classList.remove('hl')); await c.step(240); b.classList.remove('hl'); rewrite('clear');
await c.step(2600); const ok = $('.rwx-ok', tb); if (!ok) return;
ok.classList.add('hl'); c.taken(() => ok.classList.remove('hl')); await c.step(240); ok.click();
});
});
Questions
What people ask about it.
- How do I connect it to my model?
- Pass
onRewrite(sentence, action, index)and return the new sentence or a promise of it. The component handles the waiting state, the word-by-word reveal and the review step. - What happens if the rewrite request fails?
- If
onRewritethrows or rejects, the sentence stays as it was and the toolbar goes back to offering the rewrite actions. - Does the ⌘K shortcut work out of the box?
- No.
askShortcutis only shown in the wand's tooltip. Bind the key yourself and call the ref'srewritemethod or your ownonAsklogic. - Can I click away while the agent is still rewriting?
- A click outside closes the toolbar only when no rewrite is in progress. While a rewrite is waiting, the toolbar stays open until it is accepted or undone.
More in Review and approve
Moments where a person checks, steers or signs off on the agent's work.