StreamElement
A whole AI answer, streamed: Markdown or structured parts revealed word by word on one clock, citation chips, code and delegated tables, math and diagrams, with the stream state for the host's own UI
Installation
pnpm add @talex-touch/tuffex
import { TxStreamElement } from '@talex-touch/tuffex/stream-element'
import '@talex-touch/tuffex/stream-element/style.css'
import '@talex-touch/tuffex/base.css' // tokens + resets, once per app
Usage
TxStreamElement streams a whole answer. Pass everything received so far as content (Markdown) and keep streaming on while the source is live: headings, paragraphs, lists, quotes and code come in word by word, strictly in order, on one steady clock; [n] markers turn into citation chips resolved against sources; tables, math, diagrams, raw HTML and images are handed to TxStreamMarkdown and come in line by line.
An AI answer
The answer streams in uneven bursts with one pause. The action row (thumbs through TxMessageActions' slot), the source stack and the follow-ups appear only once it is done, through the footer slot. Opening a chip names its source. Once it is done, Replay plays it back at the chosen pace, which is where wordMs sets the cadence; Regenerate or a follow-up streams a new one.
Markdown it hands on
A table, inline math and a task list around a code block: the native parts reveal word by word, the delegated ones line by line, all on the same clock. With complete content, replay() plays it back and reserve keeps everything below from moving.
Slots and state
parts takes structure directly, including custom parts. Every built-in piece has a slot, and the slots, the state-change event, the instance's state and the footer slot all say where the stream is.
Best Practices
- Pass the whole answer so far, not the latest delta; the element works out what is new, and a rewrite of earlier text re-reveals only what changed.
- Keep
streamingtrue until the source has really finished: while it is true a half-written construct at the end (**bold,`code) is closed for the parser, so its markers never flash. - Put everything that belongs to a finished answer (actions, sources, follow-ups) in the
footerslot behinddone, so it arrives once, after the last word. The answer's box ends at its last part, with no margin after it, so give the footer its own top spacing. - Copy and share the full answer you hold, never what is on screen: the display trails the source by up to
maxLagMs. - Give
sourcesin the order the model cites them:[n]resolves tosources[n - 1], and a marker without a source stays text. - Use
TxStreamTextfor a single run of text andTxStreamMarkdownwhen you do not need the word-by-word reveal; this element is for whole answers.
API Reference
Props
| Name | Type | Default | Description |
|---|---|---|---|
content | string | '' | Markdown: everything received so far. |
parts | StreamPart[] | — | Structured alternative to content; wins over it. |
streaming | boolean | false | The source is still producing. |
sources | AiSourceItem[] | — | Resolves [n] markers in text into citation chips (sources[n - 1]). |
wordMs | number | 24 | Release one word every wordMs. |
maxLagMs | number | 600 | Speed up so no word shows later than this after it arrived. |
drainMs | number | 320 | Once streaming turns false, release the rest within this. |
pauseMs | number | 400 | Report paused after this long without a new word. |
reveal | 'aurora' | 'hue' | 'blur' | 'languid' | 'none' | 'aurora' | How a word enters; code keeps its syntax colour. |
caret | boolean | true | Show the stream caret at the write head while live. |
reserve | boolean | false | Hold the final layout while complete content plays back: an invisible copy, and from replay() the height the answer already has, since delegated parts render a frame late. Ignored while streaming. |
renderers | Record<string, Component> | — | Components for custom parts, by name; the part-<name> slot wins over them. |
markdownProps | Partial<StreamMarkdownProps> | — | Forwarded to the TxStreamMarkdown blocks that render delegated parts. |
locale | string | 'zh' | Word segmentation locale for Intl.Segmenter. |
Events
| Name | Payload | Description |
|---|---|---|
state-change | (state: StreamState) | The stream moved to another state. |
done | — | Once per play-through, when the last word is shown and the source has finished. |
cite | (source: AiSourceItem) | A citation chip was opened. The chip never navigates by itself. |
Slots
| Name | Scope | Description |
|---|---|---|
caret | { state } | Replaces the stream caret, at the write head of text and code. |
citation | { source, label, index } | Replaces a citation chip. |
inline | { name, props } | Renders a custom inline inside text. |
code | { part, code, streaming } | Replaces a code block; code is what has been revealed so far. |
part-<name> | { part, state } | Renders { type: 'custom', name } parts. |
footer | { state, done } | Below the answer; done once the last word is shown and the source has finished. |
Exposed Methods
| Name | Type | Description |
|---|---|---|
state | StreamState | Where the stream is. |
replay | () => void | Plays the whole answer again from the first word. |
skip | () => void | Shows everything now, without entrances. |
CSS Variables
| Variable | Default | Description |
|---|---|---|
--tx-stream-reveal-1 / -2 / -3 | blue / violet / pink, per theme | The colour stops a word sweeps through (shared with TxStreamText). |
--tx-stream-reveal-duration | the preset's | Written by the element onto its root from reveal; hosts do not set it. |
Types
type StreamPart =
| { type: 'heading', depth: 1 | 2 | 3 | 4 | 5 | 6, inlines: StreamInline[] }
| { type: 'paragraph', inlines: StreamInline[], tight?: boolean }
| { type: 'list', ordered: boolean, start?: number, items: { checked?: boolean, parts: StreamPart[] }[] }
| { type: 'quote', parts: StreamPart[] }
| { type: 'rule' }
| { type: 'code', lang?: string, code: string, filename?: string }
| { type: 'markdown', raw: string }
| { type: 'custom', name: string, props?: Record<string, unknown> }
Overview
- One clock. The element owns the only pacer. Every part is laid out on it in document order — text by words (the units
TxStreamTextreleases), code by words (TxCodeStream's), delegated Markdown by lines, a rule or a custom part as one unit — and each part receives the prefix of itself the clock has reached. So parts reveal strictly in order at one steady cadence, bursts come out as a quicker flow, and the whole answer never trails the source by more thanmaxLagMs. - The write head. Only the part holding the last revealed word streams and shows the caret. The caret moves on to the next part with its first word, and the part it leaves has its caret switched off, which removes it at once, so the answer never shows two; the last caret retracts as the answer ends.
- Rendered natively: headings, paragraphs, strong, emphasis, strikethrough, inline code, links (safe URLs only), lists (nested, task), quotes, rules and fenced code (through
TxCodeStream). Delegated whole toTxStreamMarkdown, with its sanitising and remote-image policy: tables, math, mermaid, raw HTML and images; neighbouring delegated blocks form one part. - Citations.
[n]becomes a chip only in plain text, never inside code or link text, and only whensources[n - 1]exists. - A new part enters too. A part mounts with the words already due and plays their entrance, so a burst that crosses into a new paragraph never pops its first words in. Content present when the element mounts shows at once.
- Stable parts. Parts are keyed by position and type: an unchanged part keeps its element and its content's identity across new tokens, so a token re-renders only the part it lands in; a part that changes type (a paragraph becoming a setext heading) remounts once, settled.
- States.
idle→streaming→paused→draining→done, emitted, exposed and passed to thefooterslot. - Reduced motion. No entrances and no pacing: everything shows as it arrives, and the caret rests.
- Server rendering. Rendered as plain text with no animated elements and no timers.
- Accessibility. The root carries
aria-busy="true"while the stream is live; the caret and thereservecopy are hidden from assistive technology, the copy alsoinert; citation chips are links with their source's name.
Technologies
- Component source:
packages/tuffex/packages/components/src/stream-element/src/TxStreamElement.vue; Markdown to parts inparse.ts, the clock layout inplan.ts, types intypes.ts. - Built on the family:
TxStreamText(with its shared unit modelstream-text/src/model.ts),TxCodeStream(streaming mode, code units incode-stream/src/units.ts),TxStreamMarkdownfor delegated parts, and the paceruse-stream-pacer.ts. - Tested behaviour:
stream-element/__tests__/parse.test.ts(6 cases: the native subset, nested and task lists, delegation with neighbours grouped, citations only in text, closing a half-written construct while streaming, an open fence),plan.test.ts(4: one clock in document order, keys by position and type, model reuse across parses, line ends),stream-element.test.ts(14: content at mount, in-order reveal with one write head, a new part's first word, entrances in a catch-up, only the write head re-rendering as the clock ticks, a caret dropped at once as the write head moves on and the last one retracting, code and delegated Markdown, citation chips andcite, state events,replaywithreserveandskip, the answer's height held through areservereplay, custom parts throughrenderers, reduced motion, SSR) andstream-element-slots.test.ts(6: the caret slot at the write head, citation and inline slots, the code slot, custom parts and the footer, following the host when a slot comes or goes,caret: false).
Use cases
- A chat reply or an assistant panel answer, with sources, code and follow-ups.
- A generated report or summary that mixes prose, lists, tables and formulas.
- Replaying a stored answer at a readable pace in a card or a demo, with
reserve.
Related components
- StreamText streams one run of text; the element is built on it.
- CodeStream renders the element's code parts.
- StreamMarkdown renders its delegated parts.
- Sources, MessageActions and SuggestionChips finish an answer in the
footerslot.