Assistant Message
An AI turn. Message is the human chat bubble — this is its assistant-side counterpart: roles, a streaming body, a model chip, a reasoning disclosure, and — the part almost every AI chat UI gets wrong — announcements a screen-reader user can actually follow.
Drop it inside a Conversation and stream into it.
Basic Usage
Roles
assistant and user are the two sides of the turn; system is a
configuration note — centered, muted, and no avatar, because a system turn
is not a person.
States
When a turn is a state — an error answer, a caution, a confirmation —
intent tints the bubble. danger, warning, success and info mix the
state color into the surface and border at low alpha; the body text stays
regular, so the tint reads as a marker, never as low-contrast colored text.
Streaming and announcements
This is the important part.
The obvious approach — putting aria-live on the streaming body — makes a
screen reader re-read the entire growing answer on every token. It is
unusable, and it is what most AI chat interfaces ship.
Assistant Message keeps the body silent (aria-live="off", aria-busy
while streaming) and mirrors complete sentences into a separate, always-
present live region. The reader hears the answer as prose, one finished sentence
at a time — never a stuttering re-read.
announce |
Behavior |
|---|---|
sentence (default) |
Flush each finished sentence as it lands |
all |
Flush everything once, when streaming stops (call flush()) |
off |
Never announce — you narrate it yourself |
{{-- 1. wire:stream appends tokens into the body; the announcer stays readable --}}
<x-wirekit::assistant-message :streaming="$streaming" model="atlas-2">
<span wire:stream="answer">{{ $answer }}</span>
</x-wirekit::assistant-message>
// 2. Flip $streaming around the streamed call
public bool $streaming = false;
public function ask(): void
{
$this->streaming = true;
$this->stream(to: 'answer', content: $token, replace: false);
$this->streaming = false;
}
For announce="all", call flush() on the Alpine scope when the stream ends:
{{-- 3. Announce the whole answer once, instead of sentence by sentence --}}
<x-wirekit::assistant-message announce="all" x-on:stream-finished.window="flush()">
<span wire:stream="answer">{{ $answer }}</span>
</x-wirekit::assistant-message>
Reasoning
The reasoning slot is collapsed behind a disclosure — the answer is the point,
the thinking is opt-in.
Metadata and actions
footer takes ambient chips (latency, tokens, cost); actions takes controls.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
role |
string | 'assistant' |
assistant, user, system |
name |
string|null | null |
Speaker name; defaults to the role's wording |
avatar |
string|null | null |
Avatar image for the speaker |
model |
string|null | null |
Model chip — assistant turns only |
intent |
string | 'neutral' |
State tint on the bubble: neutral, info, success, warning, danger |
streaming |
bool | false |
Marks the turn aria-busy while tokens land |
announce |
string | 'sentence' |
sentence, all, off |
scope |
string|null | null |
Scoped personalization name |
Slots
| Slot | Description |
|---|---|
| default | The answer body (rendered as prose) |
reasoning |
Collapsed disclosure above the answer |
footer |
Ambient chips — latency, tokens, cost |
actions |
Controls — copy, regenerate, rate |
Accessibility
- The body is never a live region.
aria-live="off"on the streaming body is deliberate — a live region there re-reads the whole answer per token. - A separate, always-present announcer (
role="status",aria-live="polite") receives complete sentences. It is rendered up front, because a live region created at the same moment as its text is inert to assistive technology. streamingsetsaria-busy="true"— assistive technology knows the turn is still being written.- Each turn is an
<article>named by its speaker, so a screen reader can jump turn by turn. - A
systemturn has no avatar — it is not a person. - Icon-only actions need their own
aria-label.
Keyboard Interaction
| Key | Action |
|---|---|
| Tab | Focus the reasoning disclosure, then the actions |
| Enter / Space | Toggle the reasoning disclosure |
Pitfalls
- Do not add your own
aria-liveto the body. That re-introduces exactly the stuttering re-read this component avoids. - Do not leave
streamingtrue. A turn stuckaria-busytells assistive technology the answer never finished. - Use
flush()withannounce="all". Without it, nothing is announced.
Design Tokens
| Element | Token |
|---|---|
| Assistant / system surface | --color-wk-bg-elevated, --color-wk-bg-muted |
| User surface | --color-wk-accent (10% mix over elevated) |
| Border | --color-wk-border-subtle, --border-wk-width |
| Radius | --radius-wk-lg |
| Speaker text | --text-wk-sm, --font-wk-heading-weight |
| Footer chips | --text-wk-xs, --color-wk-text-muted |