smart-components
Components

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.

Smart Rewrite
Pick a tone — review before you accept
Rewrite as:
'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

ArgTypeDescription
valuestringThe current source text.
presetsSmartRewritePreset[]The presets passed to (or defaulting on) the component.
rewritestringThe current rewritten text (accumulates while streaming).
status'idle' | 'loading' | 'ready' | 'error'State machine status.
errorError | nullSet when status is error.
run(instructionOverride?: string) => voidTrigger a rewrite.
accept() => voidCommit the current rewrite via onChange.
reject() => voidDiscard the rewrite, return to idle.

Props

PropTypeDefaultDescription
value*stringCurrent text.
onChange*(value: string) => voidCalled when the user accepts the rewrite.
instructionstringDefault instruction; overridable per run() call.
contextstringOptional context string included in the prompt.
presetsReadonlyArray<SmartRewritePreset>DEFAULT_REWRITE_PRESETSPresets exposed to the render-prop. Default: Shorter / Formal / Casual / Fix grammar.
streambooleanfalseStream chunks as they arrive.
maxTokensnumber512Max tokens for the rewrite call.
temperaturenumber0.4Sampling temperature.
children*(args: SmartRewriteRenderArgs) => ReactNodeRender-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).