# Response Stream

Streaming AI response text with bounded arrival motion and natural line wrapping.

- Package: `@mielui/svelte`
- Component version: `2.0.0`
- Depends on Mielui components: _internal/utils
- Shared utilities: components/_internal/theme-changes, transition, utils.cn

## Install

~~~~sh
pnpm dlx @mielui/svelte add response-stream
~~~~

## API

This reference is generated at build time from the component manifest, public `index.ts`, and documentation examples below. Changes to those source files are reflected here in the published Markdown. Standard Svelte and HTML attributes accepted by the exported prop types are supported.


### ResponseStream

| Prop | Type | Default | Required | Bindable | Description |
| --- | --- | --- | --- | --- | --- |
| textStream | string \| AsyncIterable<string> | - | Yes | No | A complete response or an async source of response chunks. |
| streaming | boolean \| undefined | false | No | No | Treat string values as cumulative snapshots of one live response. |
| speed | number \| undefined | 20 | No | No | 1 is slowest and 100 is fastest: reveal pace for static strings, roll duration for arrivals. Live chunks render on arrival. |
| characterChunkSize | number \| undefined | - | No | No | Whole graphemes revealed per step; finite values are rounded down with a minimum of one. |
| onComplete | (() => void) \| undefined | - | No | No | Called when the stream ends. |
| onError | ((error: unknown) => void) \| undefined | - | No | No | Called with the error when the async source throws. |
| as | keyof SvelteHTMLElements \| undefined | 'span' | No | No | HTML element to render. |
| class | (string & (ClassValue \| null)) \| undefined | - | No | No | Classes added to the element. |
| children | Snippet<[]> \| undefined | - | No | No | Content rendered inside. |

Also accepts the native HTML attributes and events listed in the rendered API reference.

### Public types

~~~~ts
import type { DefaultProps } from '@mielui/svelte/utils';
import type { HTMLAttributes, SvelteHTMLElements } from 'svelte/elements';
import ResponseStream from './response-stream.svelte';

export type ResponseStreamProps = {
    /** A complete response or an async source of response chunks. */
    textStream: string | AsyncIterable<string>;
    /** Treat string values as cumulative snapshots of one live response. */
    streaming?: boolean;
    /** 1 is slowest and 100 is fastest: reveal pace for static strings, roll duration for arrivals. Live chunks render on arrival. */
    speed?: number;
    /** Whole graphemes revealed per step; finite values are rounded down with a minimum of one. */
    characterChunkSize?: number;
    /** Called when the stream ends. */
    onComplete?: () => void;
    /** Called with the error when the async source throws. */
    onError?: (error: unknown) => void;
    /** HTML element to render. */
    as?: keyof SvelteHTMLElements;
} & DefaultProps &
    Omit<HTMLAttributes<HTMLElement>, 'children'>;

export { ResponseStream };
~~~~

## Examples

### Async Stream

~~~~svelte
<script lang="ts">
    import { ResponseStream } from '@mielui/svelte/components/response-stream';
    import { onDestroy } from 'svelte';

    const text =
        'The release contains twelve changes. Two affect keyboard navigation, four update the documentation, and six fix existing behavior. Keep the current API unchanged, run the component checks, then review the preview before deploying. The rollback is the previous release tag.';

    let stopped = false;
    let timer: ReturnType<typeof setTimeout> | undefined;
    let settle: (() => void) | undefined;

    function wait(ms: number) {
        return new Promise<void>((resolve) => {
            settle = resolve;
            timer = setTimeout(() => {
                settle = undefined;
                resolve();
            }, ms);
        });
    }

    async function* delayedResponse() {
        await wait(1200);

        const parts = text.split(/(\s+)/);

        for (const part of parts) {
            if (stopped) {
                return;
            }
            yield part;
            await wait(40);
        }
    }

    onDestroy(() => {
        stopped = true;
        clearTimeout(timer);
        settle?.();
    });
</script>

<div class="w-full max-w-xl">
    <ResponseStream textStream={delayedResponse()} speed={10} />
</div>
~~~~

### Complete

~~~~svelte
<script lang="ts">
    import { ResponseStream } from '@mielui/svelte/components/response-stream';
</script>

<div class="w-full max-w-md text-sm leading-relaxed">
    <ResponseStream
        textStream="Your draft is ready. Review the changes, then share it with your team."
        speed={65}
    />
</div>
~~~~

### Events

~~~~svelte
<script lang="ts">
    import { Button } from '@mielui/svelte/components/button';
    import { ResponseStream } from '@mielui/svelte/components/response-stream';

    let run = $state(0);
    let done = $state(false);
</script>

<div class="flex w-full max-w-md flex-col gap-3 text-sm leading-relaxed">
    {#key run}
        <ResponseStream
            textStream="The export finished. 4,218 rows were written, and two rows were skipped because their dates were empty."
            speed={40}
            characterChunkSize={3}
            onComplete={() => {
                done = true;
            }}
        />
    {/key}
    <div class="flex items-center gap-3">
        <Button
            variant="outline"
            disabled={!done}
            onclick={() => {
                done = false;
                run += 1;
            }}
        >
            Replay
        </Button>
        <span class="text-foreground-muted">{done ? 'Finished' : 'Streaming'}</span>
    </div>
</div>
~~~~

### Lifecycle

~~~~svelte
<script lang="ts">
    import { Button } from '@mielui/svelte/components/button';
    import { ResponseStream } from '@mielui/svelte/components/response-stream';
    import { Switch } from '@mielui/svelte/components/switch';
    import { onDestroy } from 'svelte';

    type Delivery = 'idle' | 'streaming' | 'complete' | 'stopped' | 'error';

    const answer =
        'The release is ready for review. The migration keeps the existing API stable, adds keyboard checks, and documents the rollback steps. Start with the canary region, then compare error rates before widening the rollout.';
    const messages: Record<Delivery, string> = {
        idle: 'Start a simulated response. No request leaves your browser.',
        streaming: 'Receiving response…',
        complete: 'Response complete.',
        stopped: 'Stopped. Partial text stays visible.',
        error: 'Connection lost. Turn off simulated failure and retry.'
    };

    let content = $state('');
    let delivery = $state<Delivery>('idle');
    let fail = $state(false);
    let timer: ReturnType<typeof setInterval> | undefined;

    function stop() {
        clearInterval(timer);
        timer = undefined;
        delivery = 'stopped';
    }

    function start() {
        clearInterval(timer);
        content = '';
        delivery = 'streaming';
        const shouldFail = fail;
        timer = setInterval(() => {
            if (shouldFail && content.length >= 80) {
                clearInterval(timer);
                timer = undefined;
                delivery = 'error';
                return;
            }
            content = answer.slice(0, content.length + 5);
            if (content === answer) {
                clearInterval(timer);
                timer = undefined;
                delivery = 'complete';
            }
        }, 65);
    }

    onDestroy(() => {
        clearInterval(timer);
    });
</script>

<div class="w-full max-w-xl space-y-4">
    <div class="flex flex-wrap items-center gap-3">
        <Button onclick={start} disabled={delivery === 'streaming'}>
            {delivery === 'error' ? 'Retry response' : 'Start response'}
        </Button>
        <Button variant="secondary" onclick={stop} disabled={delivery !== 'streaming'}>Stop</Button>
        <Switch bind:checked={fail} label="Simulate interrupted connection" />
    </div>
    <div class="min-h-24">
        {#if delivery !== 'idle'}
            <ResponseStream textStream={content} streaming={delivery === 'streaming'} />
        {/if}
    </div>
    <p role="status" class="text-sm text-foreground-muted">{messages[delivery]}</p>
</div>
~~~~

For the rendered reference, visit [/docs/components/response-stream](/docs/components/response-stream).
