AI Writing Assistant
Pro4 variants: toolbar (actions + readability stats + live editor), tone picker with preview, suggestions (accept/reject rewrite cards) and history (revision timeline with diff).
import { SvAiWritingAssistantComponent } from '@styloviz-pro/ai-writing-assistant';Demo & usage
Basic usage
ProUsage example — open the gallery for the full interactive demo.
An interactive demo for this component lives in the gallery.
Open live demoMore examples
Wire it to a real model
ProThe default already: handle actionRun, then write the answer back through text.
Inline — anchored to the selection
ProSelect text and the toolbar appears over it. Portaled to <body> and clamped on both axes.
Tone picker & suggestions
ProWhen to use
- Augmenting a writing surface with AI: rewrites, tone changes, readability stats.
- Wiring a real model needs no flag: `simulate` is off by default, so handle `actionRun` / `toneChange` and write the result back through `text` — the component renders, it does not call anything.
- Editors where the assistant should follow the selection rather than sit in a sidebar — the `inline` variant.
- Editors where users want inline suggestions they can accept or reject.
- For plain rich-text editing without AI, use the Rich Text Editor.
Async states
- loading
- error
- retry
Inputs
| Name | Type | Default | Description |
|---|---|---|---|
| loading | boolean | false | True while the data is being fetched. |
| error | SvDataError | string | null | null | Load failure. Use `toSvDataError()` from `@styloviz/core` to normalize an `HttpErrorResponse`. |
| variant | WritingAssistantVariant | 'toolbar' | View: toolbar editor, tone picker, suggestions or history. |
| text | string (two-way) | '' | The text being edited. Two-way: write a rewrite back through it. |
| actions | readonly WritingAction[] | [] | Quick actions offered in the rail and the inline toolbar. |
| tones | readonly ToneOption[] | [] | Tones offered by the tone picker. |
| suggestions | readonly WritingSuggestion[] | [] | Proposed rewrites awaiting a decision. |
| history | readonly RevisionEntry[] | [] | Past revisions. |
| busy | boolean | false | The host has a request in flight — shows the busy state without owning a timer. |
| simulate | boolean | false | Apply the bundled sample rewrites when an action runs. **Off by default.** The component emits `actionRun` / `toneChange` and waits for you to set `text` — reporting a rewrite the document never received is the worst thing this component can do, so it cannot happen by omission. Set it explicitly to `true` in a showcase. |
| protectedRanges | readonly SvProtectedRange[] | [] | Stretches of the original that must survive a rewrite untouched. Offsets are half-open `[start, end)` into the CURRENT `text()`; when the text changes, send ranges for the new text. ENFORCED WHERE THIS COMPONENT WRITES `text`, NOT BY ASKING THE MODEL NICELY. A protected range in a prompt is a request; a model asked to "make this clearer" will reword a quotation, tidy a licence clause or correct somebody's surname while doing exactly what it was told. So every write the component makes — a simulated action or tone, `applyInlineResult`, restoring a revision — is checked first and REFUSED if a protected stretch would not survive: the text stays as it was, a `role="alert"` says why, and `protectedRefused` fires. Accepting a suggestion is refused the same way when its `originalText` is found in `text()`. What the component cannot check is a rewrite the host writes into `text` directly — that write never passes through here. |
Outputs
| Event | Payload type | Description |
|---|---|---|
| retry | void | The host should re-run the failed load. |
| actionRun | string | Emitted with the id of the writing action the user runs (rewrite, summarise …). |
| toneChange | ToneType | Emitted with the tone the user applies. |
| suggestionAccepted | string | Emitted with the id of a suggestion the user accepts. |
| suggestionRejected | string | Emitted with the id of a suggestion the user rejects. |
| protectedRefused | ProtectedRefusal | Emitted when a write was refused because it would alter a protected range. |
| textChange | string | Emits the new `text` whenever it changes — the output half of `[(text)]`. |
Wire it to your API
This component is headless about transport: it renders what you give it and reports what the user asks for. Nothing below requires @styloviz/data — drive it from whatever you already use.
Template
ProBound to the names this component actually exposes.
Host class
ProDrive the three async inputs from your own fetch.
- This component does not emit a query, so it renders what you give it. Fetch on your own trigger and bind `tones`, `loading` and `error`.
- Normalize failures with `toSvDataError()` from `@styloviz/core`. It derives `retryable` from the status, so a 404 does not get a "Try again" button that can only fail again.
Full walkthrough: Connecting to a backend.
Customization & theming
- Toolbar, tone, suggestions, history and inline views.
- Every content set is an input with the bundled sample as its default — `text` (two-way), `actions`, `tones`, `suggestions` and `history` — so the component demonstrates itself out of the box and takes your data when you have it.
- `inline` anchors a floating toolbar to the current text selection: three quick actions plus a free-text prompt. It portals to `<body>` so an overflow, transform or backdrop-filter ancestor cannot clip it, prefers to sit above the selection, and flips or slides when the viewport edge is close.
- Readability stats and before/after diffs.
- Accept/reject rewrite cards.
- Emits `actionRun`, `toneChange` and `suggestionAccepted` for host integration.
- Responsive: the diff stats strip stacks two-up on phones (`grid-cols-2 sm:grid-cols-4`) so the numbers stay readable.
- `[protectedRanges]` marks stretches of the original — quotations, legal clauses, code, names — that must survive a rewrite untouched, and the component **checks the result** rather than asking the model nicely. A protected range in a prompt is a request; a model asked to 'make this clearer' will reword a quotation, tidy a licence clause or correct somebody’s surname while doing exactly what it was told, and the only enforcement a UI can actually offer is on what came back. Every write the component makes itself — a simulated action or tone, an inline rewrite, a history restore — is **refused** when it would change a protected range: the text stays as it was, a dismissible alert names the range, and `protectedRefused` fires. A suggestion whose `originalText` overlaps a range cannot be accepted (`acceptAll` skips it). Offsets are into the current `text()`; a history entry can carry its own `protectedRanges`, because one set of offsets cannot describe every revision; an entry without them is checked against `protectedRanges`, as before.
- With `simulate` off, pressing a tone or an inline action says **requested**, not applied, until the host writes the rewrite back through `text` — the component never reports a rewrite it did not see.
- Protection is about **containment, not position**: a rewrite that shortens the paragraph above a quotation has not touched the quotation, and reporting it as a violation because its offsets moved would make the check useless on exactly the rewrites it exists for. A breach renders as a `role='alert'` above the diff, naming what was changed.
- Past `SV_PROSE_DIFF_MAX_TOKENS` (2,000 words) the word-by-word comparison is abandoned, and the panel now says so in a `role='status'` notice. One enormous red block beside one enormous green block is what the reader gets when a model rewrites a document from scratch — a real and alarming outcome — and it is also what they get when the comparison simply gave up. The colours cannot tell those apart, and only one of them is a finding.
Best practices
- Do: Let users accept/reject each suggestion rather than auto-applying rewrites.
- Do: Convey diff changes with more than color so they're accessible.
- Tip: Keep a revision history so users can undo an AI edit they dislike.
Accessibility
- Suggestions are reachable and dismissible.
- The inline toolbar is `role="toolbar"` with a label naming what it acts on, and is dismissible by Escape, an outside click or a scroll — never a mouse-only affordance.
- Category/tone/history/diff toggles report aria-pressed; action feedback uses polite live regions.
- Diff changes conveyed beyond color.
Feedback on AI Writing Assistant
Spotted a gap in this page, or something that didn't work as described? Comments are GitHub Discussions in the public styloviz/community repo, so posting needs a GitHub account.