> AI-csevegő Vue-ban vagy Nuxtban: fetch-stream, megszakítás és újrapróbálás, biztonságos markdown, aria-live bejelentések, billentyűzet és reduced motion.
>
> Web page: https://balazscsorba.com/hu/blog/vue-ai-chat-ui-streaming-accessibility · Language: Magyar · Also available in: [English](https://balazscsorba.com/blog/vue-ai-chat-ui-streaming-accessibility.md) · [Deutsch](https://balazscsorba.com/de/blog/vue-ai-chat-ui-streaming-accessibility.md)
> Author: Balázs Csorba · Published: 2026-10-17 · Keywords: vue ai chat ui, nuxt llm streaming, sse vs fetch streaming, aria-live streaming text, accessible chat interface, abort fetch stream vue, dompurify markdown llm, role log chat, reduced motion chat ui, wcag 2.2 chat, screen reader streaming

[Blog](https://balazscsorba.com/hu/blog)/Webfejlesztés

# AI-csevegő UI Vue-ban és Nuxtban: akadálymentes streaming

AI-csevegő Vue-ban vagy Nuxtban: fetch-stream, megszakítás és újrapróbálás, biztonságos markdown, aria-live bejelentések, billentyűzet és reduced motion.

[Balázs Csorba](https://balazscsorba.com/hu/about)·2026\. október 17.·11 perc olvasás

-   Vue
-   Nuxt
-   Accessibility
-   Streaming
-   AI chat

![Üzenetkártya villogó kurzorral, pontozott vonal hullámokká nyílik, mellette pipa.](https://balazscsorba.com/images/blog/vue-ai-chat-ui-streaming-accessibility/cover.webp?v=3a46026edb)

## A lényeg röviden

-   Az EventSource egyirányú értesítésekre készült; aki az üzeneteit POST-tal küldi és fejlécre van szüksége, a fetch választestét olvassa streamként.
-   A megszakítás felhasználói szándék, nem hiba: a részleges válasz megmarad, Stop után soha ne legyen piros a státusz.
-   A modell kimenete nem megbízható bemenet: alapból biztonságos markdown-parser plusz DOMPurify, és sanitizálás után senki ne nyúljon a stringhez.
-   A transzkriptum role=log régióban, az átmeneti állapotok role=status-ban; a befejezett választ jelledd, ne tokenenként.
-   A billentyűzet, a 24 képpontos célméret és a prefers-reduced-motion ugyanahhoz a késznek-minősüléshez tartozik, mint a stream-parser.

Ezen az oldalon

1.  [Válaszd ki a transzportot, mielőtt a komponenst választod](https://balazscsorba.com/#streaming-choices)
2.  [Fetch-stream kiolvasása Vue composable-ban](https://balazscsorba.com/#reading-the-stream)
3.  [A megszakítás funkció, nem hiba](https://balazscsorba.com/#abort-and-retry)
4.  [A kapcsolat négy állapotként modellje](https://balazscsorba.com/#stream-state-model)
5.  [Markdown renderelése a DOM átadása nélkül](https://balazscsorba.com/#rendering-markdown-safely)
6.  [A választ jelentsd be, ne minden tokent](https://balazscsorba.com/#announcing-streams)
7.  [Billentyűzet és fókusz: a szerkesztő az alkalmazás](https://balazscsorba.com/#keyboard-and-focus)
8.  [Tiszteld a prefers-reduced-motion-t](https://balazscsorba.com/#reduced-motion)
9.  [A csevegő UI-t felhasználóként teszteld, nem linterként](https://balazscsorba.com/#testing-the-chat-ui)
10.  [Források](https://balazscsorba.com/#sources)

Cikk meghallgatása

0:000:00

A csevegődoboz a legkönnyebben demózható LLM-funkció, és az egyik legnehezebb jól kiszállítani. A válasz tokenenként érkezik, a felhasználó a szöveg növekedését nézi, és ebben a körben három külön kötelesség ül: a streamet őszintén tartani, amikor a hálózat hibázik; a modell kimenetét távol tartani a böngésző végrehajtásától; és a felületet használhatóvá tani annak is, aki sosem látja a szöveget megjelenni. Vue- és Nuxt-alkalmazásokat építek osztrák B2B-ügyfeleknek, és minden csevegős review-nál a streaming működik, az akadálymentesség nem.

A következő építési sorrend, nem könyvtárbemutató. Választunk egy transzportot, kiolvasunk belőle egy Vue composable-ban egy readerrel és egy `AbortController`\-rel, négy állapotra modellizzük a kapcsolatot, sanitizáljuk a modell markdownját, majd jön a kimaradó rész: az új tartalom bejelentése képernyőolvasóknak, a szerkesztőbillentyűzet használata billentyűzetről, és a `prefers-reduced-motion` tiszteletben tartása. A Nuxt server route-ból történő streaming alapjai a korábbi cikkemben vannak, az [LLM-funkciók Nuxtban](https://balazscsorba.com/hu/blog/nuxt-llm-features-ai-sdk-streaming); itt a stream körüli felületre megyünk mélyebben.

## Válaszd ki a transzportot, mielőtt a komponenst választod

Két transzport uralja a csevegőfelületeket. A böngésző natív `EventSource` a server-sent eventeket beszéli: a szerver a `text/event-stream` tartalmi típussal és a `event`, `data`, `id` és `retry` mezőkből épített UTF-8 üzenetekkel válaszol, az újracsatlakozást pedig a böngésző intézi. Ennek a rugalmasságnak két éle van egy csevegésben: a kapcsolat egyirányú, tehát nem viszi az előzményeket küldő POST-kérést, az újracsatlakozás pedig automatikus. Hálózati hiba után a böngésző kérdezés nélkül újrapróbálkozik, ami egy egyszeri token-streamnél pont rossz, mert a csendes retry megismételhet egy félredarabolt választ.

Van egy infrastrukturális korlát is, érdemes tudni elköteleződés előtt. HTTP/1.1 alatt a böngészők böngészőnként és tartományonként hatra korlátozzák a nyitott kapcsolatokat, ami a Chrome-ban és a Firefoxban is won't fix státuszt kapott, míg HTTP/2 alatt a párhuzamos streamek számát kétoldalú alkudozás rögzíti, alapértelmezésben 100. Aki még HTTP/1.1-en fejezi be a kapcsolatot, néhány nyitott csevegőfüllel már a plafonon ül.

Megközelítés

Irány

Hitelesítés

Újracsatlakozás

Amit magad kezelsz

EventSource (SSE)

csak szerverből kliensbe

GET, nincs kérési törzs

automatikus a retry mezővel

a teljes küldési utat

fetch plusz ReadableStream

kérési törzs és válasz-stream

teljes fejlécek és törzs

nincs, te döntöd el

parsing, megszakítás, hibaállapotok

AI SDK useChat

POST UI-üzenet-streammel

konfigurálható fejlécek és törzs

a transzport kezeli

állapot, renderelés, akadálymentesség

Ha a csevegés üzenetlistát küld, `Authorization`\-fejlécre van szüksége, és a Stop megnyomásakor azonnal le kell állnia, a `ReadableStream`\-ként olvasott fetch választest az őszinte alapértelmezés, ez az, amit az AI SDK `useChat` hookja is csinál, a Vue-csomagjában is. A cikk többi része ezt a kört kézzel megépíti, hogy amikor a hookhoz nyúlsz, tudd, mit csinál helyetted, és hol ér véget az ő állapotmodellje, és hol kezdődik a te akadálymentességi munkád.

## Fetch-stream kiolvasása Vue composable-ban

A fetch válasz a testét `ReadableStream`ként adja ki. Lezárhatod `getReader()`\-rel és ciklusban olvashatod, vagy kihagyhatod a readert és magát a chunkokat iterálhatod: a Streams API megvalósítja az async iterable protokollt, a `for await...of` tehát a választesten működik, felkészítés nélkül. Az MDN két, gyakorlatban fontos részletre hívja fel a figyelmet: az iterálás lezárja a streamet, így más nem fogyaszthatja, a ciklus idő előtti elhagyása pedig megszakítja a streamet, hacsak nem `preventCancel` -t adsz meg. A bájtokhoz vezesd a testet egy `TextDecoderStream`\-en át, a TextDecoder streamelő változatán, amely bináris kódolást alakít át sztring-streammé.

```
<script setup lang="ts">
import { ref, onBeforeUnmount } from "vue"

type Status = "ready" | "streaming" | "error"
const reply = ref("")
const status = ref<Status>("ready")
let controller: AbortController | null = null

async function send(text: string) {
  controller?.abort()
  controller = new AbortController()
  status.value = "streaming"
  reply.value = ""
  try {
    const res = await fetch("/api/chat", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ message: text }),
      signal: controller.signal,
    })
    if (!res.ok || !res.body) throw new Error(`HTTP ${res.status}`)
    const reader = res.body
      .pipeThrough(new TextDecoderStream())
      .getReader()
    let leftover = ""
    for (;;) {
      const { done, value } = await reader.read()
      if (done) break
      leftover += value
      const events = leftover.split("\n\n")
      leftover = events.pop() ?? ""
      for (const ev of events) {
        if (ev.startsWith("data: ")) reply.value += ev.slice(6)
      }
    }
    status.value = "ready"
  } catch (err) {
    // a Stop press is not a failure
    status.value = (err as Error).name === "AbortError" ? "ready" : "error"
  }
}

onBeforeUnmount(() => controller?.abort())
</script>
```

Két sornak itt van kommentje. A maradék puffer azért kell, mert a hálózati chunkok nincsenek a protokollhoz igazítva: egyetlen `data`\-esemény két read között is kettéválhat, ezért csak a teljes, üres sorokkal elválasztott eseményeket dolgozd fel, a többit hagyd a következő körnek. A catch ág pedig az `AbortError`\-t normális befejezésként kezeli, mert a Stop felhasználói szándék, nem hiba. Minden más, beleértve a stream indulása előtt észlelt nem 2xx státuszt, error állapotba viszi az UI-t, ahol az újrapróbálás felajánlható.

## A megszakítás funkció, nem hiba

`AbortController` az erre való szabványeszköz: az `abort()` metódusa megszakítja a fetch kéréseket, a választestek és stream-ek fogyasztását. Tegyél egy controllert beszélgetésenként, az előző kérést szakítsd meg az új indítása előtt, és unmountnál is szakítsd meg, hogy egy hosszú generálás közbeni navigáció ne hagyja a szervert egy halott fülbe streamelni. Az finom rész a felület körül van: a már megérkezett részleges válasz érvényes szöveg, tartsd meg a transzkriptumban, jelöld a üzenetet megállítottnak, és soha ne legyen piros az állapot.

```
// stop is user intent: abort and keep whatever arrived
function stop() {
  controller?.abort()
  status.value = "ready"
}

// retry resends the last user message, not the last wire payload
async function retry() {
  const last = messages.value.at(-1)
  if (last?.role === "user") await send(last.text)
}
```

Az újrapróbálás normál gombként tartozik a folyamatba, amely megjelenéskor fókuszba kerül, nem rejtett kézmozdulatként. Az AI SDK pontosan ezt a párost adja: a `stop` megszakítja a fetch kérést, a `regenerate` a modellt a legutóbbi üzenet újrafeldolgozására kéri, a `status` pedig eldönti, melyik aktív. Ha valóban hiba történik, rövid általános üzenetet mutass: az AI SDK dokumentációja egy egyszerű valami-gált üzenetet ajánl a nyers hiba helyett, hogy a szerver részletei k maradjanak ki a nyilvános felületről, a valódi hiba pedig a naplódba kerül, ahol tehetsz ellene.

Két kijárat számít: a Stop-nak az állapotgépben megkülönböztethetetlennek kell lennie a sikertől, a hálózati hibának pedig újrapróbáláson kell landolnia, nem stacktrace-en.

## A kapcsolat négy állapotként modellje

Minden látható vezérlőt egyetlen státuszértékből vezess le. Az AI SDK `useChat` hookja négy értékű státuszt ad: a `submitted` azt jelenti, hogy a kérés úton van és a stream még nem indult el, a `streaming` hogy chunkok érkeznek, a `ready` hogy a válasz elkészült és új üzenet küldhető, a `error` pedig hogy a kérés meghibásodott. A küldés gomb csak ready-n kívül legyen letiltva, Stop csak submitted vagy streaming alatt, újrapróbálás csak a hibaállapotban: ez egy állapotgép, amit képernyőolvasó nélkül is le tudsz tesztelni, és ez az a gép, amit az akadálymentesítő technika a status régiódon keresztül tud olvasni.

A throttle az állapotmodellhez tartozik, nem a teljesítmény-háttérlistához. A `useChat` dokumentál Vue-hoz egy throttle opciót, amely a reaktív üzenet-frissítéseket csomagolja (a dokumentáció 50 milliszekundumot használ), miközben a stream feldolgozása és az event callbackek azonnaliak maradnak, és garantálja, hogy a legutolsó üzenetek még a terminális állapot elérése előtt kikerülnek. Nélküle egy gyors modell tucatszor mutálja a reaktív tömbödet másodpercenként, és minden fogyasztó vele együtt renderel.

A Vue a renderelési oldalon segít: reaktív állapot módosításakor a DOM-frissítések nem szinkronusan mennek végre, hanem a következő tickig vannak pufferelve, így több chunk-írás egy tickben egyetlen patchbe olvad. Ne mérd a transzkript DOM-ját abban a tickben, amelyikben hozzáfűztél egy chunkot; előbb várd meg a `nextTick()`\-et. Itt találkozik az akadálymentességi és a teljesítmény-keret is, mert kevesebb render lenyomkenti az oldal interaction next paint-jét, erről szól a [Core Web Vitals útmutató Nuxt-oldalakhoz](https://balazscsorba.com/hu/blog/nuxt-core-web-vitals-performance).

## Markdown renderelése a DOM átadása nélkül

A modell kimenete felhasználók által befolyásolt bemenet. Egy prompt megkérheti a modellt, hogy script taget, iframe-et vagy javascript linket adjon ki, aki pedig a beszélgetést irányítja, megpróbálhatja valóban: ez ugyanaz a fenyegetési modell, mint bármely más nem megbízható HTML-nél. A biztonságos kombó egy alapból biztonságos markdown-parser plusz egy sanitizer a kimenetén. Pontosan ezt hirdeti a markdown-it: CommonMark-követő és alapból biztonságos, a nyers HTML tehát escape-elve marad, hacsak nem kérsz külön. Aztán a DOMPurify eltakarítja, amit a parser legyártott.

```
import MarkdownIt from "markdown-it"
import DOMPurify from "dompurify"

// html false marad: a modell kimenetében lévő nyers HTML escape-elve lesz
const md = new MarkdownIt({ linkify: true })

export function renderAssistantMarkdown(src: string): string {
  const html = md.render(src)
  return DOMPurify.sanitize(html, {
    USE_PROFILES: { html: true }, // a válaszhoz nem kell SVG vagy MathML
    ADD_ATTR: ["target"],
  })
}
```

**Utolsóként sanitizálj, és komolyan is gondold**

A DOMPurify saját dokumentációja figyelmeztet: aki előbb sanitizál, utána pedig módosítja a markupot, könnyen semmissé teheti a sanitizálás hatását. Tehát `sanitize()` után szintaktikai kiemelő, linkáíró vagy másoló-gomb segéd ne nyúljon a stringhez; azt Vue-komponensekkel vagy data attribútumokkal tiszd a sanitizált kimenet fölött. Két további buktató ugyanabból a README-ből: az alap konfiguráció SVG-t és MathML-t is enged, amire egy csevegőválasznak sosem kell szüksége, szerveroldali sanitizálásnál pedig ne happy-dom-ot használj, amely a projekt szerint nem tekinthető biztonságosnak. A szélesebb moderációs történet a [LLM-kimeneti guardrailjeim](https://balazscsorba.com/hu/blog/llm-output-guardrails-moderation) cikkemben van; a renderelés ennek a védelemnek az utolsó vonala, nem az első.

Tartsd szűken az allowlistét és egyirányú a pipeline-t: markdown be, sanitizált HTML ki, és csak azután `v-html`\-jal renderelve. A hivatkozások a target és rel attribútumaikat a te rendereredtől kapják, sosem a modelltől, ami pedig eszközhívásnak vagy termékkártyának néz ki, azt egy általad kontrollált Vue-komponens rajzolja. A képernyőolvasó is találkozik ezzel a tartalommal, tartsd tehát szemantikusnak a generált markupot: valódi fejlécek, valódi listák, valódi code elemek, az akadálymentesítő technika ugyanis pontosan ezekkel a struktúrákkal navigál.

## A választ jelentsd be, ne minden tokent

Ez a rész, amit a legtöbb implementáció kihagy. A WCAG 2.2 2023. október 5-én jelent meg W3C-ajánlásként, és a 4.1.3 Status Messages sikerkritériuma (AA szint) megköveteli, hogy a státuszüzenetek szerepen vagy tulajdonságokon keresztül programozottan meghatározhatók legyenek, hogy az akadálymentesítő technika fókuszvétel nélkül jeleníthesse meg őket. A WAI technikái a `role=status`\-t nevezik meg eredményekhez és várakozási állapotokhoz, a `role=log`\-t folyamatos frissítésekhez, a `role=alert`\-et hibákhoz. A csevegő-transzkriptum a role=log tipikus esete: az MDN élő régióként írja le, ahová az új információ értelmes sorrendben, a végére érkezik, és egy beszélgetés is éppen így nő.

Három részlet dönti el, hogy ez hogyan érződik. A role=log régió implicit `aria-live` értéke polite, a bejelentések tehát megvárnak, amíg a felhasználó tétlen, az implicit `aria-atomic` pedig false, így csak a különbség hangzik el. A régiónak hozzáférhető név kell, tehát `aria-label` vagy `aria-labelledby` szükséges. És a transzkriptban ott a kész válasz, mert a képernyőolvasós felhasználó, aki később rátalál a logra, olvasható szövegként keresi a választ.

A puffer az akadálymentességi funkció: az olvasó egy kész választ hall, nem ötven token-frissítést.

Tartsd kicsin a bejelentési felületet: egy státuszvonal az átmeneti állapotoknak (gondolkodik, megállítva, hiba), és a log régió a tartalomnak. Mert a polite live régiók csak a változást jelentik, az assztisztens-üzenet streamzárás utáni egyszeri közzététele tiszta egyetlen bejelentést ad, míg minden token-szakasz dadogást okozna. A WAI 4.1.3-hoz szóló understanding dokumentuma a másik irányból is figyelmeztet: egy alkalmazás túl beszédes is lehet, ezért a bejelentések ritmusát valódi képernyőolvasós felhasználókkal teszteld, ne tippelj.

```
<section aria-labelledby="chat-heading">
  <h2 id="chat-heading">Support chat</h2>

  <!-- transcript: finished messages announced politely, as a delta -->
  <ol role="log" aria-live="polite" aria-label="Chat messages">
    <li v-for="m in messages" :key="m.id">
      <span class="who">{{ m.role === 'user' ? 'You' : 'Assistant' }}</span>
      <div v-html="m.html"></div>
    </li>
  </ol>

  <!-- transient state: announced once, never focused -->
  <p role="status">{{ statusText }}</p>

  <form @submit.prevent="send">
    <label for="chat-input">Message</label>
    <textarea id="chat-input" v-model="draft"
      @keydown.enter.exact.prevent="send"></textarea>
    <button type="button" v-if="status === 'streaming'" @click="stop">Stop</button>
    <button type="submit" :disabled="status === 'streaming'">Send</button>
  </form>
</section>
```

**Befejezéseket jelents, ne tokeneket**

Egy streamelő válasz minden néhány tíz milliszekundumban változik. Ha minden változás elér egy live régiót, a képernyőolvasós felhasználó folyamatos félmondat-dadogást hall, és sosem kap képet a válaszról. Puffereld a chunkokat, és az üzenetet a log régióba a stream végén vagy mondatvégeken add le; az MDN arra is felhívja a figyelmet, hogy a `aria-atomic` igazra állítása az egész régiót újraolvastatja, ami rövid státusznál hasznos, hosszú válasznál zajos. Ennek a funkciónak a rossz verziója rossabb, mint a semmi, szóval a pufferelt verziót szállítsd.

## Billentyűzet és fókusz: a szerkesztő az alkalmazás

Egy csevegésben mindennek működnie kell egyedül billentyűzetről, és a szerkesztőnél szokott ez elromlani. A küldés után maradjon a fókusz a textarea-ban, hogy aki kérdést kérdésre lő, ne ugráljon vissza Tabbal. Az Enter küld, a Shift+Enter újsor, de a billentyűt csak a textarea-n magán szabad elkapni, sosem egy burkoló diven. A Send és a Stop valódi gomb, az Enter és a Space tehát ingyen működik, a stream közepén megjelenő Stop pedig nem veheti el a fókuszt attól, aki még gépel. Semmi ne legyen elérhető csak hoverrel.

Két WCAG 2.2-es kritérium pontosan ehhez a elrendezéshez szól. A Target Size (Minimum), 2.5.8 AA szinten 24 ször 24 CSS-képpontos mutatócélokat vagy elegendő teret kér a kisebbek köré, ami elveszi a kísértést, hogy a Stopot apró inline jelként rajzold. A Focus Not Obscured (Minimum), 2.4.11 AA szinten megköveteli, hogy a billentyűzőfókuszt kapó komponens ne legyen teljesen eltakarva szerzői tartalommal, és ez a görgetett transzkript feletti ragadós szerkesztősáv pontos kockázata: adj hozzá scroll paddingot és scroll margókat, hogy a üzenetekben lévő linkek fókuszba kerülve látszódjanak.

## Tiszteld a prefers-reduced-motion-t

`prefers-reduced-motion` azt az beállítást érzékeli, amelyet az emberek a nem lényeges mozgás minimalizálására kapcsolnak be, az MDN pedig a nagy objektumok skálázását és pásztázását nevezi meg klasszikus rosszullét-kiváltóként vestibuláris mozgászavaroknál. A csevegés tele van pontosan ezekkel: pulzáló gépelőpontok, villogó kurzor, gördülő auto-scroll, amely minden chunkkal feljebb tolja a transzkriptot. A javítás olcsó, és CSS: tartsd meg az animációt azoknak, akik akarják, mindenki másnak pedig cseréld azonnali állapotváltásra.

```
.typing-dot { animation: pulse 1.1s ease-in-out infinite; }
.chat-log { scroll-behavior: smooth; }

@media (prefers-reduced-motion: reduce) {
  .typing-dot { animation: none; }
  .chat-log { scroll-behavior: auto; }
}
```

Két vélemény, amit érdemes megvédeni egy review-n. Egy: csökkentett mozgásnál az auto-scroll ugráljon, ne csússzon, és álljon meg teljesen, amint a felhasználó felfelé görget, hogy olvasson valamit, mert az olvasóval szembeni küzdelem bármelyik rosszabb animációnál. Kettő: a tevékenység egyetlen jelét soha ne rejtsd mozgás mögé; ha a gépelésjelző az egyetlen bizonyíték, hogy a modell dolgozik, a csökkentett mozgású felhasználónak egyáltalán nincs bizonyítéka. Ha a preferenciára szerveroldalon is szükséged van, van hozzá user-agent client hint: `Sec-CH-Prefers-Reduced-Motion`.

## A csevegő UI-t felhasználóként teszteld, nem linterként

Úgy tesztelj, ahogy a hiba a valós felhasználóhoz eljut. Egy kör csak billentyűzettel: Tab a szerkesztőbe, Enter a küldéshez, ellenőrizd, hogy a Stop megjelenik és elérhető a stream közben, az újrapróbálás pedig fókuszba kerül, amikor előjön. Egy kör képernyőolvasóval: VoiceOver macOS alatt és NVDA Windows alatt, az kettő, amely az osztrák és magyar ügyfeleimnél számít: a választ egyszer kellene bejelenteni, a hibát egyáltalán, és semminek nem szabad megismételnie magát tokenek közben. Futtass automatikus axe scan-t a CI-ben a hiányzó nevekért és szerepekért, de kezeld padlónak, nem bizonyítéknak.

Aztán add hozzá azt a három regressziós tesztet, amit hajnal kettőkor szeretnél. Egy: szakítsd meg a kapcsolatot a stream közepén, és ellenőrizd, hogy a UI hibaállapotba kerül újrapróbálási lehetőséggel és kezelt ígérettel. Kettő: nyomd meg a Stopot, és ellenőrizd, hogy az állapot nem error, a részleges válasz pedig túléli. Három: küldj olyan üzenetet, amitől a modell script taget ad ki, és ellenőrizd, hogy szövegként jelenik meg. Ezek a három teszt lefedik a cikk javát, és bármelyik headless böngészőben futnak. Ha az EU-ban értékesítesz, az [European Accessibility Act](https://balazscsorba.com/hu/blog/european-accessibility-act-b2b-shop) már most is üzleti követelményként kezeli a hozzáférhető felületeket, ez a munka tehát szintén nem opcionális.

## Források

1.  [MDN: Using server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events)
2.  [MDN: AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController)
3.  [MDN: ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream)
4.  [MDN: TextDecoderStream](https://developer.mozilla.org/en-US/docs/Web/API/TextDecoderStream)
5.  [MDN: ARIA log role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/log_role)
6.  [W3C: Understanding SC 4.1.3 Status Messages](https://www.w3.org/WAI/WCAG22/Understanding/status-messages.html)
7.  [W3C WAI: What's new in WCAG 2.2](https://www.w3.org/WAI/standards-guidelines/wcag/new-in-22/)
8.  [MDN: prefers-reduced-motion](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion)
9.  [DOMPurify README](https://github.com/cure53/DOMPurify)
10.  [markdown-it README](https://github.com/markdown-it/markdown-it)
11.  [AI SDK docs: useChat](https://ai-sdk.dev/docs/ai-sdk-ui/chatbot)
12.  [Vue docs: nextTick](https://vuejs.org/api/general.html#nexttick)

## Gyakori kérdések

A streamelő szöveg maga legyen aria-live régióban?

Nem. A kész üzenet kerüljön a log régióba, az átmeneti állapotok (gondolkodik, megállítva, hiba) pedig egy kis status vonalra. A token-frissítéseket kapó live régió a képernyőolvasót dadogó géppé alakítja; a közlést a befejezéskor vagy mondatvégeken add le, a transzkriptum pedig tartsa a teljes szöveget az olvasó számára.

Miért ne az EventSource-t használnám a válaszhoz?

Mert egyirányú, és az újracsatlakozás automatikus. Az EventSource csak fogad, az üzenetlistát küldő POST-hoz tehát amúgy is kell a fetch, a beépített retry pedig csendben lejátszhatja újra a félredarabolt választ. A fetch választest egy hívásban hozza a kérést és a választ, a megszakítás pedig a kezedben marad.

A markdown-it önmagában nem elég?

Alapból escape-eli a nyers HTML-t, de a sanitizer olcsó biztosítás. A linkelt URL-ek, a bővítmények kimenete és a jövőbeli opciók tágíthatják a felületet, a DOMPurify pedig a parser-mutációs, névtér- és clobbering-trükköket is kivédi, amiket a kézzel írt szűrők elnéznek. Futtasd utoljára, és utána ne módosítsd a stringet.

Hogyan maradjon reszponzív az UI tokeneken közben?

Reaktív frissítések csomagolásával. Az AI SDK Vue-hoz throttle opciót ad, Vue amúgy is a következő tickig puffereli a DOM-patcheket, elég tehát elkerülni a nehéz gyerek-komponenseket és a scroll-számítást ugyanabban a tickben. A kevesebb render a page interaction next paint-jét is védi.

Tényleg képernyőolvasóval kell tesztelnem?

Igen, és olcsóbban, mint félsz. Öt perc VoiceOver-rel macOS alatt vagy NVDA-val Windows alatt megmutatja a két klasszikus hibát: a soha be nem jelentett transzkriptet és a vizuálisan nyilvánvaló, de néma hibaállapotot. Az automatikus scan-ek megtalálják a hiányzó szerepeket; azt nem mondják meg, használhatók-e a bejelentések.

Írta: Csorba Balázs

Senior fullstack és AI mérnök Stájerországban – több mint 10 év Vue, Nuxt, Node.js és PHP, ma AI-ügynököknek építek eszközöket.

[Vue.js és Nuxt fejlesztés →](https://balazscsorba.com/hu/expertise/vue-nuxt-developer)[Rólam →](https://balazscsorba.com/hu/about)

## További cikkek

-   [Schema.org strukturált adatok B2B webshopokhoz: bejelentkezés mögötti árak, egységek és változatok](https://balazscsorba.com/hu/blog/schema-org-structured-data-b2b-shop)
-   [AI-asszisztens a B2B webshop ügyfélszolgálatához: eszközök, jogosultságok és átadás embernek](https://balazscsorba.com/hu/blog/ai-assistant-b2b-shop-customer-support)
-   [LLM-fordítási pipeline i18n-hez: glosszárium, helyőrzők és review](https://balazscsorba.com/hu/blog/llm-translation-pipeline-i18n)
-   [Többnyelvű SEO Nuxt i18n-nel: URL-stratégia, hreflang, canonical és sitemap](https://balazscsorba.com/hu/blog/nuxt-i18n-multilingual-seo-hreflang)

## Pont erre van szükséged?

Írj a projektedről vagy a pozícióról – szívesen hallok felőled.

[Lépj velem kapcsolatba](mailto:contact@balazscsorba.com) [Kapcsolódjunk LinkedInen](https://www.linkedin.com/in/balazs-csorba)
