# AI streaming

Render arriving Markdown as one message and handle completion, cancellation and failure.

An AI response usually arrives in several pieces. The application receives network data and accumulates text; `QuireMarkdown` renders the Markdown received so far. It does not send model requests or determine when they finish.

## 1. Pass the message to the reader

Keep the component imports from [Quick start](/en/guide/getting-started). `message.id` is a stable identifier for one response, and `message.text` is the accumulated source:

```vue
<QuireMarkdown
  :content="message.text"
  :identity="message.id"
  :status="message.receiving ? 'receiving' : 'complete'"
  @error="reportReadingError"
/>
```

Pass the **complete received source** on each update. Prefix appends reuse a parsing session;
replacement, clearing, identity changes and replacing the options object create a fresh
session and release old resources. `status` is `receiving`, `complete`, `stopped` or `error`.
It describes transport state; stopping retains text and neither freezes blocks nor adds
synthetic closing delimiters. Late link and footnote definitions can update earlier content.
The application cancels its own network request.

## 2. Try appending and stopping

Choose “Start streaming” and watch headings, links and footnotes arrive. “Stop” retains received text. The application remains responsible for cancelling its network request.

### reading/stream.vue

```vue
<script setup lang="ts">
import { computed, onBeforeUnmount, ref } from "vue";
import { QuireMarkdown, type QuireMarkdownProps } from "@quirekit/incremark/vue";
import { QuireButton, QuireTypography } from "@quirekit/ui/vue";
import type { ExampleLocale } from "../shared/types";
import { readingSample } from "./sample";

const props = withDefaults(defineProps<{ /** Tutorial language. */ locale?: ExampleLocale }>(), { locale: "zh-CN" });
const content = ref(readingSample(props.locale)), identity = ref(0);
const status = ref<NonNullable<QuireMarkdownProps["status"]>>("complete");
const labels = computed(() => props.locale === "zh-CN"
  ? { start: "开始流式演示", stop: "停止", load: "一次性加载", reading: "流式阅读预览", receiving: "正在接收", stopped: "已停止", complete: "已完成" }
  : { start: "Start streaming", stop: "Stop", load: "Load all", reading: "Streaming reading preview", receiving: "Receiving", stopped: "Stopped", complete: "Complete" });
let timer: ReturnType<typeof setTimeout> | undefined;

/** Cancels the producer independently of QuireMarkdown's parser lifecycle. */
function cancel(): void { if (timer !== undefined) clearTimeout(timer); timer = undefined; }
/** Supplies the authoritative full text after each simulated network chunk; no parser APIs are needed. */
function start(): void {
  cancel(); identity.value++; content.value = ""; status.value = "receiving";
  const source = readingSample(props.locale);
  /** Delivers the next 20 UTF-16 code units, including arbitrary syntax boundaries. */
  function append(): void {
    content.value = source.slice(0, content.value.length + 20);
    if (content.value.length < source.length) timer = setTimeout(append, 30);
    else { timer = undefined; status.value = "complete"; }
  }
  append();
}
/** Retains the exact received prefix when the user stops delivery. */
function stop(): void { cancel(); status.value = "stopped"; }
/** Replaces the message with its complete source and a new resource identity. */
function load(): void { cancel(); identity.value++; content.value = readingSample(props.locale); status.value = "complete"; }
onBeforeUnmount(cancel);
</script>
<template>
  <div class="grid gap-4 p-qk-surface">
    <div class="flex flex-wrap items-center gap-2">
      <QuireButton size="sm" variant="outline" @click="start">{{ labels.start }}</QuireButton>
      <QuireButton size="sm" variant="ghost" :disabled="status !== 'receiving'" @click="stop">{{ labels.stop }}</QuireButton>
      <QuireButton size="sm" variant="ghost" @click="load">{{ labels.load }}</QuireButton>
      <QuireTypography as="span" variant="helper">{{ status === 'receiving' ? labels.receiving : status === 'stopped' ? labels.stopped : labels.complete }}</QuireTypography>
    </div>
    <section :aria-label="labels.reading"><QuireMarkdown :content="content" :identity="String(identity)" :status="status" :locale="locale" /></section>
  </div>
</template>

```

### shared/types.ts

```ts
/** Example host language; independent of the documentation router. */
export type ExampleLocale = "zh-CN" | "en";

```

### reading/sample.ts

````ts
import type { ExampleLocale } from "../shared/types";

/** @param locale - Tutorial language. @returns One source with late definitions, repeated footnotes and an incrementally displayed code block. */
export function readingSample(locale: ExampleLocale): string {
  const chinese = locale === "zh-CN";
  return [chinese ? "## 流式 Markdown" : "## Streaming Markdown",
    chinese ? "先收到正文和[文档][api]引用，定义会在末尾到达。这里重复引用同一脚注[^note]、[^note]。"
      : "The [documentation][api] reference arrives before its definition. This footnote is used twice[^note], [^note].",
    ...Array.from({ length: 6 }, (_, index) => chinese
      ? `第 ${index + 1} 段保留 **强调**、中文与 emoji 🙂。已收到的正文继续可读，后置定义到达时前面的链接会更新。`
      : `Paragraph ${index + 1} keeps **emphasis** and emoji 🙂. Existing text stays readable while late definitions update earlier links.`),
    "```ts\nconst message = 'hello'\nconsole.log(message)\n```",
    chinese ? "[^note]: 脚注正文也使用[文档][api]链接。" : "[^note]: The footnote also links to the [documentation][api].",
    "[api]: https://example.com/docs", "[api]: https://example.com/ignored",
  ].join("\n\n");
}

````

## Server-rendered first content

SSR produces semantic headings, lists and links by default, with the same parsing strategy
on the first client render. Resource loading starts only after client mount. To defer initial
parsing, use `ssr="source"`: the server and initial client frame show escaped source, then
parse after mounting. An existing snapshot may be passed as `state`, mutually exclusive
with `content`.


## 3. Receive a real text response

This reusable function handles HTTP bodies containing **raw Markdown text**.
Pass the application's AbortSignal to both fetch and this function; update message.text in onText.
Set receiving on start, complete on success, stopped on user cancellation and error for other failures.
Guard callbacks with the request identity so an old request cannot overwrite a different message. The host aborts on unmount.

`recipes/response.ts` only reads and decodes the response body. `readMarkdownResponse(response, signal, onText)` supplies accumulated source to `onText` and returns the complete text when finished. The application owns its message list, request state and error feedback.

### recipes/response.ts

```ts
/**
 * Consume an HTTP response whose body is raw Markdown text, not SSE or provider JSON.
 * @param response - Response returned by the application's fetch call.
 * @param signal - The same AbortSignal passed to fetch by the host.
 * @param onText - Receives the entire accepted Markdown prefix after each decoded chunk.
 * @returns Complete source, preserving UTF-8 characters split across transport chunks.
 */
export async function readMarkdownResponse(response: Response, signal: AbortSignal, onText: (text: string) => void): Promise<string> {
  signal.throwIfAborted();
  if (!response.ok || !response.body) throw new Error(`markdown-response:${response.status}`);
  const reader = response.body.getReader(), decoder = new TextDecoder();
  let content = "";
  /** Cancel producer reads; the component retains whatever the host has already accepted. */
  const cancel = (): void => { void reader.cancel().catch(() => {}); };
  signal.addEventListener("abort", cancel, { once: true });
  try {
    while (true) {
      const chunk = await reader.read();
      signal.throwIfAborted();
      if (chunk.done) break;
      content += decoder.decode(chunk.value, { stream: true });
      onText(content);
    }
    const tail = decoder.decode();
    if (tail) { content += tail; onText(content); }
    return content;
  } finally {
    signal.removeEventListener("abort", cancel);
    await reader.cancel().catch(() => {});
    reader.releaseLock();
  }
}

```

For SSE or JSON events, decode the protocol in the host first and append only text deltas.
Do not feed `data:`, event JSON or binary chunk boundaries directly to Markdown. TextDecoder must preserve decoding state between chunks.
