SmartRewrite
Headless rewrite primitive (render-prop). Built-in presets, streaming, accept/reject.
Render-prop-only primitive that drives rewrite state for any text. Built-in presets: Shorter / Formal / Casual / Fix grammar. Renders no DOM of its own — you choose how to surface the buttons, preview, and accept/reject UX.
Preview
import { useState } from 'react';import { SmartProvider, SmartRewrite } from '@extedcoud/smart-components';import { createMockClient } from '@extedcoud/smart-components/adapters/mock';const client = createMockClient({complete: async (req) => '— rewritten —',});export function Example() {const [value, setValue] = useState('hey, lemme circle back tmrw');return ( <SmartProvider client={client}> <SmartRewrite value={value} onChange={setValue}> {({ presets, run, status, rewrite, accept, reject }) => ( <> <textarea value={rewrite || value} readOnly={status === 'loading'} /> {presets.map((p) => ( <button key={p.label} onClick={() => run(p.instruction)}> {p.label} </button> ))} {status === 'ready' && ( <> <button onClick={accept}>Accept</button> <button onClick={reject}>Reject</button> </> )} </> )} </SmartRewrite> </SmartProvider>);}Styled example
A full editor card: header with live status, preset tone chips, a loading shimmer, and a before → after diff panel with accept/discard.
'use client';import { useState } from 'react';import { SmartProvider, SmartRewrite } from '@extedcoud/smart-components';import { makeRewriteMock } from '@/lib/mock-client';const client = makeRewriteMock();const PRESET_ICONS: Record<string, string> = { Shorter: '✂️', Formal: '🎩', Casual: '🛋️', 'Fix grammar': '📝',};export default function SmartRewriteStylized() { const [value, setValue] = useState( 'thx for getting back to me, lemme check on this and circle back tmrw', ); return ( <SmartProvider client={client}> <div className="w-full max-w-2xl"> <div className="overflow-hidden rounded-2xl border border-fd-border bg-fd-card shadow-sm"> <SmartRewrite value={value} onChange={setValue}> {({ presets, run, status, rewrite, error, accept, reject }) => { const loading = status === 'loading'; const ready = status === 'ready'; const failed = status === 'error'; return ( <> {/* Header */} <div className="flex items-center justify-between gap-3 border-b border-fd-border bg-fd-background/40 px-4 py-3"> <div className="flex items-center gap-2.5"> <span className="flex h-8 w-8 flex-none items-center justify-center rounded-lg bg-gradient-to-br from-violet-500 to-fuchsia-500 text-sm text-white shadow-sm"> ✦ </span> <div className="leading-tight"> <div className="text-sm font-semibold text-fd-foreground">Smart Rewrite</div> <div className="text-xs text-fd-muted-foreground"> Pick a tone — review before you accept </div> </div> </div> {loading && ( <span className="inline-flex items-center gap-1.5 rounded-full bg-fd-secondary/60 px-2.5 py-1 text-[11px] font-medium text-fd-muted-foreground"> <span className="h-3 w-3 animate-spin rounded-full border-2 border-fd-muted-foreground/30 border-t-fd-foreground" /> Rewriting… </span> )} {ready && ( <span className="inline-flex items-center gap-1 rounded-full bg-emerald-500/15 px-2.5 py-1 text-[11px] font-medium uppercase tracking-wider text-emerald-700 dark:text-emerald-300"> ✦ Suggestion ready </span> )} {failed && ( <span className="inline-flex items-center gap-1 rounded-full bg-rose-500/15 px-2.5 py-1 text-[11px] font-medium text-rose-700 dark:text-rose-300"> Failed </span> )} </div> {/* Body */} <div className="p-4"> {ready ? ( <div className="space-y-3"> <div className="rounded-xl border border-fd-border bg-fd-background px-4 py-3"> <div className="mb-1.5 text-[10px] font-semibold uppercase tracking-wider text-fd-muted-foreground"> Before </div> <p className="text-sm leading-relaxed text-fd-muted-foreground line-through decoration-fd-border"> {value} </p> </div> <div className="flex justify-center text-fd-muted-foreground"> <span aria-hidden className="text-lg leading-none"> ↓ </span> </div> <div className="rounded-xl border border-emerald-500/30 bg-gradient-to-br from-emerald-500/10 to-teal-500/10 px-4 py-3"> <div className="mb-1.5 text-[10px] font-semibold uppercase tracking-wider text-emerald-700 dark:text-emerald-300"> After </div> <p className="text-base leading-relaxed text-fd-foreground">{rewrite}</p> </div> </div> ) : loading ? ( <div className="space-y-2.5 px-1 py-2" aria-hidden> <div className="h-4 w-full animate-pulse rounded bg-fd-border/70" /> <div className="h-4 w-11/12 animate-pulse rounded bg-fd-border/70" /> <div className="h-4 w-3/5 animate-pulse rounded bg-fd-border/70" /> </div> ) : ( <textarea value={value} onChange={(e) => setValue(e.target.value)} rows={3} style={{ fontSize: 16 }} placeholder="Type something, then pick a rewrite tone below…" className="w-full resize-none rounded-xl border border-fd-border bg-fd-background px-4 py-3 text-base leading-relaxed text-fd-foreground outline-none transition placeholder:text-fd-muted-foreground focus:border-fd-foreground" /> )} {failed && ( <p className="mt-3 text-xs text-rose-600 dark:text-rose-400"> {error?.message ?? 'Something went wrong. Try again.'} </p> )} </div> {/* Footer toolbar */} <div className="flex flex-wrap items-center gap-2 border-t border-fd-border bg-fd-background/40 px-4 py-3"> {ready ? ( <> <button type="button" onClick={accept} className="inline-flex h-10 min-h-[44px] items-center gap-1.5 rounded-full bg-emerald-600 px-5 text-sm font-semibold text-white shadow transition hover:bg-emerald-500" style={{ touchAction: 'manipulation' }} > ✓ Accept </button> <button type="button" onClick={reject} className="inline-flex h-10 min-h-[44px] items-center gap-1.5 rounded-full border border-fd-border bg-fd-background px-5 text-sm font-medium text-fd-foreground transition hover:bg-fd-accent" style={{ touchAction: 'manipulation' }} > ✕ Discard </button> </> ) : ( <> <span className="mr-1 text-xs font-medium text-fd-muted-foreground"> Rewrite as: </span> {presets.map((p) => ( <button key={p.label} type="button" disabled={loading} onClick={() => run(p.instruction)} className="inline-flex h-9 min-h-[44px] items-center gap-1.5 rounded-full border border-fd-border bg-fd-background px-3.5 text-xs font-medium text-fd-foreground transition hover:border-fd-foreground hover:bg-fd-accent disabled:cursor-not-allowed disabled:opacity-50" style={{ touchAction: 'manipulation' }} > <span aria-hidden>{PRESET_ICONS[p.label] ?? '✦'}</span> {p.label} </button> ))} </> )} </div> </> ); }} </SmartRewrite> </div> </div> </SmartProvider> );}Custom presets
import { SmartRewrite } from '@extedcoud/smart-components';
<SmartRewrite
value={v}
onChange={setV}
presets={[
{ label: 'Pirate', instruction: 'Rewrite this like a pirate.' },
{ label: 'Haiku', instruction: 'Rewrite this as a haiku.' },
]}
>
{/* render-prop */}
</SmartRewrite>Default presets are exported as DEFAULT_REWRITE_PRESETS if you want to extend instead of replace.
Streaming
<SmartRewrite value={v} onChange={setV} stream>
{({ rewrite, status }) => (
<pre>{status === 'loading' ? rewrite + '▍' : rewrite}</pre>
)}
</SmartRewrite>Requires the active SmartClient to declare the stream capability.
Render-prop args
| Arg | Type | Description |
|---|---|---|
value | string | The current source text. |
presets | SmartRewritePreset[] | The presets passed to (or defaulting on) the component. |
rewrite | string | The current rewritten text (accumulates while streaming). |
status | 'idle' | 'loading' | 'ready' | 'error' | State machine status. |
error | Error | null | Set when status is error. |
run | (instructionOverride?: string) => void | Trigger a rewrite. |
accept | () => void | Commit the current rewrite via onChange. |
reject | () => void | Discard the rewrite, return to idle. |
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| value* | string | — | Current text. |
| onChange* | (value: string) => void | — | Called when the user accepts the rewrite. |
| instruction | string | — | Default instruction; overridable per run() call. |
| context | string | — | Optional context string included in the prompt. |
| presets | ReadonlyArray<SmartRewritePreset> | DEFAULT_REWRITE_PRESETS | Presets exposed to the render-prop. Default: Shorter / Formal / Casual / Fix grammar. |
| stream | boolean | false | Stream chunks as they arrive. |
| maxTokens | number | 512 | Max tokens for the rewrite call. |
| temperature | number | 0.4 | Sampling temperature. |
| children* | (args: SmartRewriteRenderArgs) => ReactNode | — | Render-prop. The component renders no DOM of its own. Receives { value, presets, rewrite, status, error, run, accept, reject }. |
When to reach for SmartParaphraseBox instead
SmartRewrite is the primitive. If you want an out-of-the-box input + sparkle button + auto-accept, use SmartParaphraseBox (single-line) or SmartParaphraseArea (multi-line).