[{"data":1,"prerenderedAt":772},["ShallowReactive",2],{"blog-vue-ai-chat-ui-streaming-accessibility-hu":3},{"slug":4,"published":5,"minutes":6,"category":7,"tags":8,"keywords":14,"about":26,"sources":36,"cover":73,"og":74,"expertise":75,"locales":76,"lang":79,"title":80,"description":81,"coverAlt":82,"metaTitle":83,"takeaways":84,"faq":90,"toc":106,"blocks":137,"others":526},"vue-ai-chat-ui-streaming-accessibility","2026-10-17",11,"web",[9,10,11,12,13],"Vue","Nuxt","Accessibility","Streaming","AI chat",[15,16,17,18,19,20,21,22,23,24,25],"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",[27,30,33],{"name":28,"url":29},"Web accessibility","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FWeb_accessibility",{"name":31,"url":32},"Server-sent events","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FServer-sent_events",{"name":34,"url":35},"Vue.js","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FVue.js",[37,40,43,46,49,52,55,58,61,64,67,70],{"title":38,"url":39},"MDN: Using server-sent events","https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FServer-sent_events\u002FUsing_server-sent_events",{"title":41,"url":42},"MDN: AbortController","https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FAbortController",{"title":44,"url":45},"MDN: ReadableStream","https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FReadableStream",{"title":47,"url":48},"MDN: TextDecoderStream","https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FTextDecoderStream",{"title":50,"url":51},"MDN: ARIA log role","https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAccessibility\u002FARIA\u002FReference\u002FRoles\u002Flog_role",{"title":53,"url":54},"W3C: Understanding SC 4.1.3 Status Messages","https:\u002F\u002Fwww.w3.org\u002FWAI\u002FWCAG22\u002FUnderstanding\u002Fstatus-messages.html",{"title":56,"url":57},"W3C WAI: What's new in WCAG 2.2","https:\u002F\u002Fwww.w3.org\u002FWAI\u002Fstandards-guidelines\u002Fwcag\u002Fnew-in-22\u002F",{"title":59,"url":60},"MDN: prefers-reduced-motion","https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FCSS\u002F@media\u002Fprefers-reduced-motion",{"title":62,"url":63},"DOMPurify README","https:\u002F\u002Fgithub.com\u002Fcure53\u002FDOMPurify",{"title":65,"url":66},"markdown-it README","https:\u002F\u002Fgithub.com\u002Fmarkdown-it\u002Fmarkdown-it",{"title":68,"url":69},"AI SDK docs: useChat","https:\u002F\u002Fai-sdk.dev\u002Fdocs\u002Fai-sdk-ui\u002Fchatbot",{"title":71,"url":72},"Vue docs: nextTick","https:\u002F\u002Fvuejs.org\u002Fapi\u002Fgeneral.html#nexttick","\u002Fimages\u002Fblog\u002Fvue-ai-chat-ui-streaming-accessibility\u002Fcover.webp","\u002Fimages\u002Fblog\u002Fvue-ai-chat-ui-streaming-accessibility\u002Fog.jpg","vue-nuxt-developer",[77,78,79],"en","de","hu","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.","Üzenetkártya villogó kurzorral, pontozott vonal hullámokká nyílik, mellette pipa.","AI-csevegő UI Vue-ban, akadálymentes · Balázs Csorba",[85,86,87,88,89],"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.",[91,94,97,100,103],{"q":92,"a":93},"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.",{"q":95,"a":96},"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.",{"q":98,"a":99},"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.",{"q":101,"a":102},"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.",{"q":104,"a":105},"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.",[107,110,113,116,119,122,125,128,131,134],{"id":108,"title":109},"streaming-choices","Válaszd ki a transzportot, mielőtt a komponenst választod",{"id":111,"title":112},"reading-the-stream","Fetch-stream kiolvasása Vue composable-ban",{"id":114,"title":115},"abort-and-retry","A megszakítás funkció, nem hiba",{"id":117,"title":118},"stream-state-model","A kapcsolat négy állapotként modellje",{"id":120,"title":121},"rendering-markdown-safely","Markdown renderelése a DOM átadása nélkül",{"id":123,"title":124},"announcing-streams","A választ jelentsd be, ne minden tokent",{"id":126,"title":127},"keyboard-and-focus","Billentyűzet és fókusz: a szerkesztő az alkalmazás",{"id":129,"title":130},"reduced-motion","Tiszteld a prefers-reduced-motion-t",{"id":132,"title":133},"testing-the-chat-ui","A csevegő UI-t felhasználóként teszteld, nem linterként",{"id":135,"title":136},"sources","Források",[138,142,160,163,189,192,239,254,255,277,279,289,290,299,301,316,325,326,348,354,366,367,370,372,388,395,396,411,430,437,440,442,451,452,455,458,459,464,466,472,473,476,484,485],{"type":139,"content":140},"paragraph",[141],"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.",{"type":139,"content":143},[144,145,149,150,153,154,159],"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 ",{"tag":146,"children":147},"code",[148],"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 ",{"tag":146,"children":151},[152],"prefers-reduced-motion"," tiszteletben tartása. A Nuxt server route-ból történő streaming alapjai a korábbi cikkemben vannak, az ",{"tag":155,"to":156,"children":157},"link","\u002Fblog\u002Fnuxt-llm-features-ai-sdk-streaming",[158],"LLM-funkciók Nuxtban","; itt a stream körüli felületre megyünk mélyebben.",{"type":161,"level":162,"id":108,"text":109},"heading",2,{"type":139,"content":164},[165,166,169,170,173,174,177,178,177,181,184,185,188],"Két transzport uralja a csevegőfelületeket. A böngésző natív ",{"tag":146,"children":167},[168],"EventSource"," a server-sent eventeket beszéli: a szerver a ",{"tag":146,"children":171},[172],"text\u002Fevent-stream"," tartalmi típussal és a ",{"tag":146,"children":175},[176],"event",", ",{"tag":146,"children":179},[180],"data",{"tag":146,"children":182},[183],"id"," és ",{"tag":146,"children":186},[187],"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.",{"type":139,"content":190},[191],"Van egy infrastrukturális korlát is, érdemes tudni elköteleződés előtt. HTTP\u002F1.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\u002F2 alatt a párhuzamos streamek számát kétoldalú alkudozás rögzíti, alapértelmezésben 100. Aki még HTTP\u002F1.1-en fejezi be a kapcsolatot, néhány nyitott csevegőfüllel már a plafonon ül.",{"type":193,"head":194,"rows":205},"table",[195,197,199,201,203],[196],"Megközelítés",[198],"Irány",[200],"Hitelesítés",[202],"Újracsatlakozás",[204],"Amit magad kezelsz",[206,217,228],[207,209,211,213,215],[208],"EventSource (SSE)",[210],"csak szerverből kliensbe",[212],"GET, nincs kérési törzs",[214],"automatikus a retry mezővel",[216],"a teljes küldési utat",[218,220,222,224,226],[219],"fetch plusz ReadableStream",[221],"kérési törzs és válasz-stream",[223],"teljes fejlécek és törzs",[225],"nincs, te döntöd el",[227],"parsing, megszakítás, hibaállapotok",[229,231,233,235,237],[230],"AI SDK useChat",[232],"POST UI-üzenet-streammel",[234],"konfigurálható fejlécek és törzs",[236],"a transzport kezeli",[238],"állapot, renderelés, akadálymentesség",{"type":139,"content":240},[241,242,245,246,249,250,253],"Ha a csevegés üzenetlistát küld, ",{"tag":146,"children":243},[244],"Authorization","-fejlécre van szüksége, és a Stop megnyomásakor azonnal le kell állnia, a ",{"tag":146,"children":247},[248],"ReadableStream","-ként olvasott fetch választest az őszinte alapértelmezés, ez az, amit az AI SDK ",{"tag":146,"children":251},[252],"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.",{"type":161,"level":162,"id":111,"text":112},{"type":139,"content":256},[257,258,260,261,264,265,268,269,272,273,276],"A fetch válasz a testét ",{"tag":146,"children":259},[248],"ként adja ki. Lezárhatod ",{"tag":146,"children":262},[263],"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 ",{"tag":146,"children":266},[267],"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 ",{"tag":146,"children":270},[271],"preventCancel"," -t adsz meg. A bájtokhoz vezesd a testet egy ",{"tag":146,"children":274},[275],"TextDecoderStream","-en át, a TextDecoder streamelő változatán, amely bináris kódolást alakít át sztring-streammé.",{"type":146,"code":278},"\u003Cscript setup lang=\"ts\">\nimport { ref, onBeforeUnmount } from \"vue\"\n\ntype Status = \"ready\" | \"streaming\" | \"error\"\nconst reply = ref(\"\")\nconst status = ref\u003CStatus>(\"ready\")\nlet controller: AbortController | null = null\n\nasync function send(text: string) {\n  controller?.abort()\n  controller = new AbortController()\n  status.value = \"streaming\"\n  reply.value = \"\"\n  try {\n    const res = await fetch(\"\u002Fapi\u002Fchat\", {\n      method: \"POST\",\n      headers: { \"Content-Type\": \"application\u002Fjson\" },\n      body: JSON.stringify({ message: text }),\n      signal: controller.signal,\n    })\n    if (!res.ok || !res.body) throw new Error(`HTTP ${res.status}`)\n    const reader = res.body\n      .pipeThrough(new TextDecoderStream())\n      .getReader()\n    let leftover = \"\"\n    for (;;) {\n      const { done, value } = await reader.read()\n      if (done) break\n      leftover += value\n      const events = leftover.split(\"\\n\\n\")\n      leftover = events.pop() ?? \"\"\n      for (const ev of events) {\n        if (ev.startsWith(\"data: \")) reply.value += ev.slice(6)\n      }\n    }\n    status.value = \"ready\"\n  } catch (err) {\n    \u002F\u002F a Stop press is not a failure\n    status.value = (err as Error).name === \"AbortError\" ? \"ready\" : \"error\"\n  }\n}\n\nonBeforeUnmount(() => controller?.abort())\n\u003C\u002Fscript>",{"type":139,"content":280},[281,282,284,285,288],"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 ",{"tag":146,"children":283},[180],"-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 ",{"tag":146,"children":286},[287],"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ó.",{"type":161,"level":162,"id":114,"text":115},{"type":139,"content":291},[292,294,295,298],{"tag":146,"children":293},[148]," az erre való szabványeszköz: az ",{"tag":146,"children":296},[297],"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.",{"type":146,"code":300},"\u002F\u002F stop is user intent: abort and keep whatever arrived\nfunction stop() {\n  controller?.abort()\n  status.value = \"ready\"\n}\n\n\u002F\u002F retry resends the last user message, not the last wire payload\nasync function retry() {\n  const last = messages.value.at(-1)\n  if (last?.role === \"user\") await send(last.text)\n}",{"type":139,"content":302},[303,304,307,308,311,312,315],"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 ",{"tag":146,"children":305},[306],"stop"," megszakítja a fetch kérést, a ",{"tag":146,"children":309},[310],"regenerate"," a modellt a legutóbbi üzenet újrafeldolgozására kéri, a ",{"tag":146,"children":313},[314],"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.",{"type":317,"attrs":318,"inner":322,"caption":323},"diagram",{"viewBox":319,"role":320,"aria-labelledby":321},"0 0 720 200","img","vaic1-t vaic1-d","\u003Ctitle id=\"vaic1-t\">Csevegő-stream életciklusa\u003C\u002Ftitle>\u003Cdesc id=\"vaic1-d\">A szerkesztő POST kérést küld, a szerver token-chunkokat streamel vissza, a kliens hozzáfűzi és sanitizálja őket, a stream a kész állapotban ér véget; az Stop út megszakítja a fetchet, az hiba út újrapróbálást kínál.\u003C\u002Fdesc>\u003Cdefs>\u003Cmarker id=\"vaic1-ah\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\">\u003Cpath d=\"M0 0L10 5L0 10z\" class=\"d-head\" \u002F>\u003C\u002Fmarker>\u003C\u002Fdefs>\u003Crect x=\"20\" y=\"30\" width=\"118\" height=\"76\" rx=\"10\" class=\"d-box\" \u002F>\u003Ctext x=\"79\" y=\"64\" text-anchor=\"middle\" class=\"d-text\">Composer\u003C\u002Ftext>\u003Ctext x=\"79\" y=\"86\" text-anchor=\"middle\" class=\"d-small\">küldés\u003C\u002Ftext>\u003Cpath d=\"M138 68H158\" class=\"d-line\" marker-end=\"url(#vaic1-ah)\" \u002F>\u003Crect x=\"160\" y=\"30\" width=\"118\" height=\"76\" rx=\"10\" class=\"d-sky\" \u002F>\u003Ctext x=\"219\" y=\"64\" text-anchor=\"middle\" class=\"d-text\">POST\u003C\u002Ftext>\u003Ctext x=\"219\" y=\"86\" text-anchor=\"middle\" class=\"d-small\">\u002Fapi\u002Fchat\u003C\u002Ftext>\u003Cpath d=\"M278 68H298\" class=\"d-line\" marker-end=\"url(#vaic1-ah)\" \u002F>\u003Crect x=\"300\" y=\"30\" width=\"118\" height=\"76\" rx=\"10\" class=\"d-gold\" \u002F>\u003Ctext x=\"359\" y=\"64\" text-anchor=\"middle\" class=\"d-text\">Stream\u003C\u002Ftext>\u003Ctext x=\"359\" y=\"86\" text-anchor=\"middle\" class=\"d-small\">token-chunkok\u003C\u002Ftext>\u003Cpath d=\"M418 68H438\" class=\"d-line\" marker-end=\"url(#vaic1-ah)\" \u002F>\u003Crect x=\"440\" y=\"30\" width=\"118\" height=\"76\" rx=\"10\" class=\"d-mint\" \u002F>\u003Ctext x=\"499\" y=\"64\" text-anchor=\"middle\" class=\"d-text\">Render\u003C\u002Ftext>\u003Ctext x=\"499\" y=\"86\" text-anchor=\"middle\" class=\"d-small\">sanitizálás\u003C\u002Ftext>\u003Cpath d=\"M558 68H578\" class=\"d-line\" marker-end=\"url(#vaic1-ah)\" \u002F>\u003Crect x=\"580\" y=\"30\" width=\"118\" height=\"76\" rx=\"10\" class=\"d-box\" \u002F>\u003Ctext x=\"639\" y=\"64\" text-anchor=\"middle\" class=\"d-text\">Kész\u003C\u002Ftext>\u003Ctext x=\"639\" y=\"86\" text-anchor=\"middle\" class=\"d-small\">ready állapot\u003C\u002Ftext>\u003Cpath d=\"M359 106V140\" class=\"d-dash\" marker-end=\"url(#vaic1-ah)\" \u002F>\u003Crect x=\"300\" y=\"142\" width=\"118\" height=\"52\" rx=\"10\" class=\"d-fill-muted\" \u002F>\u003Ctext x=\"359\" y=\"166\" text-anchor=\"middle\" class=\"d-text\">Stop\u003C\u002Ftext>\u003Ctext x=\"359\" y=\"184\" text-anchor=\"middle\" class=\"d-small\">abort()\u003C\u002Ftext>\u003Cpath d=\"M499 106V140\" class=\"d-dash\" marker-end=\"url(#vaic1-ah)\" \u002F>\u003Crect x=\"440\" y=\"142\" width=\"118\" height=\"52\" rx=\"10\" class=\"d-box\" \u002F>\u003Ctext x=\"499\" y=\"166\" text-anchor=\"middle\" class=\"d-text\">Hiba\u003C\u002Ftext>\u003Ctext x=\"499\" y=\"184\" text-anchor=\"middle\" class=\"d-small\">retry\u003C\u002Ftext>\u003C\u002Fsvg>",[324],"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.",{"type":161,"level":162,"id":117,"text":118},{"type":139,"content":327},[328,329,331,332,335,336,339,340,343,344,347],"Minden látható vezérlőt egyetlen státuszértékből vezess le. Az AI SDK ",{"tag":146,"children":330},[252]," hookja négy értékű státuszt ad: a ",{"tag":146,"children":333},[334],"submitted"," azt jelenti, hogy a kérés úton van és a stream még nem indult el, a ",{"tag":146,"children":337},[338],"streaming"," hogy chunkok érkeznek, a ",{"tag":146,"children":341},[342],"ready"," hogy a válasz elkészült és új üzenet küldhető, a ",{"tag":146,"children":345},[346],"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.",{"type":139,"content":349},[350,351,353],"A throttle az állapotmodellhez tartozik, nem a teljesítmény-háttérlistához. A ",{"tag":146,"children":352},[252]," 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.",{"type":139,"content":355},[356,357,360,361,365],"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 ",{"tag":146,"children":358},[359],"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 ",{"tag":155,"to":362,"children":363},"\u002Fblog\u002Fnuxt-core-web-vitals-performance",[364],"Core Web Vitals útmutató Nuxt-oldalakhoz",".",{"type":161,"level":162,"id":120,"text":121},{"type":139,"content":368},[369],"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.",{"type":146,"code":371},"import MarkdownIt from \"markdown-it\"\nimport DOMPurify from \"dompurify\"\n\n\u002F\u002F html false marad: a modell kimenetében lévő nyers HTML escape-elve lesz\nconst md = new MarkdownIt({ linkify: true })\n\nexport function renderAssistantMarkdown(src: string): string {\n  const html = md.render(src)\n  return DOMPurify.sanitize(html, {\n    USE_PROFILES: { html: true }, \u002F\u002F a válaszhoz nem kell SVG vagy MathML\n    ADD_ATTR: [\"target\"],\n  })\n}",{"type":373,"variant":374,"title":375,"body":376},"callout","warn","Utolsóként sanitizálj, és komolyan is gondold",[377],[378,379,382,383,387],"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 ",{"tag":146,"children":380},[381],"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 ",{"tag":155,"to":384,"children":385},"\u002Fblog\u002Fllm-output-guardrails-moderation",[386],"LLM-kimeneti guardrailjeim"," cikkemben van; a renderelés ennek a védelemnek az utolsó vonala, nem az első.",{"type":139,"content":389},[390,391,394],"Tartsd szűken az allowlistét és egyirányú a pipeline-t: markdown be, sanitizált HTML ki, és csak azután ",{"tag":146,"children":392},[393],"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.",{"type":161,"level":162,"id":123,"text":124},{"type":139,"content":397},[398,399,402,403,406,407,410],"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 ",{"tag":146,"children":400},[401],"role=status","-t nevezik meg eredményekhez és várakozási állapotokhoz, a ",{"tag":146,"children":404},[405],"role=log","-t folyamatos frissítésekhez, a ",{"tag":146,"children":408},[409],"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ő.",{"type":139,"content":412},[413,414,417,418,421,422,425,426,429],"Három részlet dönti el, hogy ez hogyan érződik. A role=log régió implicit ",{"tag":146,"children":415},[416],"aria-live"," értéke polite, a bejelentések tehát megvárnak, amíg a felhasználó tétlen, az implicit ",{"tag":146,"children":419},[420],"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 ",{"tag":146,"children":423},[424],"aria-label"," vagy ",{"tag":146,"children":427},[428],"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.",{"type":317,"attrs":431,"inner":434,"caption":435},{"viewBox":432,"role":320,"aria-labelledby":433},"0 0 720 170","vaic2-t vaic2-d","\u003Ctitle id=\"vaic2-t\">Bejelentési pipeline\u003C\u002Ftitle>\u003Cdesc id=\"vaic2-d\">A nyers tokenek mondatnyi darabokba bufferelődnek, egyszer kerülnek egy polite log régióba, és a képernyőolvasó akkor mondja el őket, amikor a felhasználó tétlen.\u003C\u002Fdesc>\u003Cdefs>\u003Cmarker id=\"vaic2-ah\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\">\u003Cpath d=\"M0 0L10 5L0 10z\" class=\"d-head\" \u002F>\u003C\u002Fmarker>\u003C\u002Fdefs>\u003Crect x=\"20\" y=\"47\" width=\"118\" height=\"76\" rx=\"10\" class=\"d-box\" \u002F>\u003Ctext x=\"79\" y=\"81\" text-anchor=\"middle\" class=\"d-text\">Tokenek\u003C\u002Ftext>\u003Ctext x=\"79\" y=\"103\" text-anchor=\"middle\" class=\"d-small\">nyers stream\u003C\u002Ftext>\u003Cpath d=\"M138 85H158\" class=\"d-line\" marker-end=\"url(#vaic2-ah)\" \u002F>\u003Crect x=\"160\" y=\"47\" width=\"118\" height=\"76\" rx=\"10\" class=\"d-sky\" \u002F>\u003Ctext x=\"219\" y=\"81\" text-anchor=\"middle\" class=\"d-text\">Puffer\u003C\u002Ftext>\u003Ctext x=\"219\" y=\"103\" text-anchor=\"middle\" class=\"d-small\">mondatnyi\u003C\u002Ftext>\u003Cpath d=\"M278 85H298\" class=\"d-line\" marker-end=\"url(#vaic2-ah)\" \u002F>\u003Crect x=\"300\" y=\"47\" width=\"118\" height=\"76\" rx=\"10\" class=\"d-gold\" \u002F>\u003Ctext x=\"359\" y=\"81\" text-anchor=\"middle\" class=\"d-text\">Log régió\u003C\u002Ftext>\u003Ctext x=\"359\" y=\"103\" text-anchor=\"middle\" class=\"d-small\">polite\u003C\u002Ftext>\u003Cpath d=\"M418 85H438\" class=\"d-line\" marker-end=\"url(#vaic2-ah)\" \u002F>\u003Crect x=\"440\" y=\"47\" width=\"118\" height=\"76\" rx=\"10\" class=\"d-mint\" \u002F>\u003Ctext x=\"499\" y=\"81\" text-anchor=\"middle\" class=\"d-text\">Delta\u003C\u002Ftext>\u003Ctext x=\"499\" y=\"103\" text-anchor=\"middle\" class=\"d-small\">egyszer jelentve\u003C\u002Ftext>\u003Cpath d=\"M558 85H578\" class=\"d-line\" marker-end=\"url(#vaic2-ah)\" \u002F>\u003Crect x=\"580\" y=\"47\" width=\"118\" height=\"76\" rx=\"10\" class=\"d-box\" \u002F>\u003Ctext x=\"639\" y=\"81\" text-anchor=\"middle\" class=\"d-text\">Olvasó\u003C\u002Ftext>\u003Ctext x=\"639\" y=\"103\" text-anchor=\"middle\" class=\"d-small\">beszéd tétlenben\u003C\u002Ftext>\u003C\u002Fsvg>",[436],"A puffer az akadálymentességi funkció: az olvasó egy kész választ hall, nem ötven token-frissítést.",{"type":139,"content":438},[439],"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.",{"type":146,"code":441},"\u003Csection aria-labelledby=\"chat-heading\">\n  \u003Ch2 id=\"chat-heading\">Support chat\u003C\u002Fh2>\n\n  \u003C!-- transcript: finished messages announced politely, as a delta -->\n  \u003Col role=\"log\" aria-live=\"polite\" aria-label=\"Chat messages\">\n    \u003Cli v-for=\"m in messages\" :key=\"m.id\">\n      \u003Cspan class=\"who\">{{ m.role === 'user' ? 'You' : 'Assistant' }}\u003C\u002Fspan>\n      \u003Cdiv v-html=\"m.html\">\u003C\u002Fdiv>\n    \u003C\u002Fli>\n  \u003C\u002Fol>\n\n  \u003C!-- transient state: announced once, never focused -->\n  \u003Cp role=\"status\">{{ statusText }}\u003C\u002Fp>\n\n  \u003Cform @submit.prevent=\"send\">\n    \u003Clabel for=\"chat-input\">Message\u003C\u002Flabel>\n    \u003Ctextarea id=\"chat-input\" v-model=\"draft\"\n      @keydown.enter.exact.prevent=\"send\">\u003C\u002Ftextarea>\n    \u003Cbutton type=\"button\" v-if=\"status === 'streaming'\" @click=\"stop\">Stop\u003C\u002Fbutton>\n    \u003Cbutton type=\"submit\" :disabled=\"status === 'streaming'\">Send\u003C\u002Fbutton>\n  \u003C\u002Fform>\n\u003C\u002Fsection>",{"type":373,"variant":443,"title":444,"body":445},"note","Befejezéseket jelents, ne tokeneket",[446],[447,448,450],"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 ",{"tag":146,"children":449},[420]," 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.",{"type":161,"level":162,"id":126,"text":127},{"type":139,"content":453},[454],"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.",{"type":139,"content":456},[457],"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.",{"type":161,"level":162,"id":129,"text":130},{"type":139,"content":460},[461,463],{"tag":146,"children":462},[152]," 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.",{"type":146,"code":465},".typing-dot { animation: pulse 1.1s ease-in-out infinite; }\n.chat-log { scroll-behavior: smooth; }\n\n@media (prefers-reduced-motion: reduce) {\n  .typing-dot { animation: none; }\n  .chat-log { scroll-behavior: auto; }\n}",{"type":139,"content":467},[468,469,365],"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: ",{"tag":146,"children":470},[471],"Sec-CH-Prefers-Reduced-Motion",{"type":161,"level":162,"id":132,"text":133},{"type":139,"content":474},[475],"Ú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.",{"type":139,"content":477},[478,479,483],"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 ",{"tag":155,"to":480,"children":481},"\u002Fblog\u002Feuropean-accessibility-act-b2b-shop",[482],"European Accessibility Act"," 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.",{"type":161,"level":162,"id":135,"text":136},{"type":486,"ordered":487,"items":488},"list",true,[489,493,496,499,502,505,508,511,514,517,520,523],[490],{"tag":491,"href":39,"children":492},"a",[38],[494],{"tag":491,"href":42,"children":495},[41],[497],{"tag":491,"href":45,"children":498},[44],[500],{"tag":491,"href":48,"children":501},[47],[503],{"tag":491,"href":51,"children":504},[50],[506],{"tag":491,"href":54,"children":507},[53],[509],{"tag":491,"href":57,"children":510},[56],[512],{"tag":491,"href":60,"children":513},[59],[515],{"tag":491,"href":63,"children":516},[62],[518],{"tag":491,"href":66,"children":519},[65],[521],{"tag":491,"href":69,"children":522},[68],[524],{"tag":491,"href":72,"children":525},[71],[527,600,656,724],{"slug":528,"published":5,"minutes":529,"category":7,"tags":530,"keywords":535,"about":547,"sources":556,"cover":593,"og":594,"expertise":595,"locales":596,"lang":79,"title":597,"description":598,"coverAlt":599},"schema-org-structured-data-b2b-shop",10,[531,532,533,534,10],"Schema.org","Structured data","B2B e-commerce","Technical SEO",[536,537,538,539,540,541,542,543,544,545,546],"schema.org b2b shop","product structured data b2b","merchant listing structured data","product snippet vs merchant listing","unitpricespecification referencequantity","productgroup variants structured data","organization schema vatid","b2b prices behind login seo","json-ld nuxt schema org","merchant return policy structured data","mpn gtin structured data",[548,550,553],{"name":531,"url":549},"https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FSchema.org",{"name":551,"url":552},"JSON-LD","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FJSON-LD",{"name":554,"url":555},"Business-to-business","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FBusiness-to-business",[557,560,563,566,569,572,575,578,581,584,587,590],{"title":558,"url":559},"Google Search Central: Introduction to Product structured data","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Fproduct",{"title":561,"url":562},"Google Search Central: Merchant listing (Product, Offer) structured data","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Fmerchant-listing",{"title":564,"url":565},"Google Search Central: Product snippet structured data","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Fproduct-snippet",{"title":567,"url":568},"Google Search Central: Product variant structured data","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Fproduct-variants",{"title":570,"url":571},"Google Search Central: Organization structured data","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Forganization",{"title":573,"url":574},"Google Search Central: Merchant return policy structured data","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Freturn-policy",{"title":576,"url":577},"Google Search Central: General structured data guidelines","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Fsd-policies",{"title":579,"url":580},"Google Search Central: Generate structured data with JavaScript","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fappearance\u002Fstructured-data\u002Fgenerate-structured-data-with-javascript",{"title":582,"url":583},"Schema.org: Offer","https:\u002F\u002Fschema.org\u002FOffer",{"title":585,"url":586},"Schema.org: UnitPriceSpecification","https:\u002F\u002Fschema.org\u002FUnitPriceSpecification",{"title":588,"url":589},"Unhead: Product schema (defineProduct)","https:\u002F\u002Funhead.unjs.io\u002Fdocs\u002Fschema-org\u002Fapi\u002Fschema\u002Fproduct",{"title":591,"url":592},"Nuxt SEO: Schema.org identity setup","https:\u002F\u002Fnuxtseo.com\u002Fdocs\u002Fschema-org\u002Fguides\u002Fsetup-identity","\u002Fimages\u002Fblog\u002Fschema-org-structured-data-b2b-shop\u002Fcover.webp","\u002Fimages\u002Fblog\u002Fschema-org-structured-data-b2b-shop\u002Fog.jpg","b2b-ecommerce-developer",[77,78,79],"Schema.org strukturált adatok B2B webshopokhoz: bejelentkezés mögötti árak, egységek és változatok","Termék-markup B2B shopokhoz: snippet vagy kereskedői lista, melyik ár, cikkszámok, egységárak, változatok, Organization és SSR Nuxtban.","Terméktéglalap vékony kerettel, alsó kis mezőjét alacsony fal takarja el.",{"slug":601,"published":5,"minutes":602,"category":7,"tags":603,"keywords":608,"about":620,"sources":628,"cover":650,"og":651,"expertise":595,"locales":652,"lang":79,"title":653,"description":654,"coverAlt":655},"ai-assistant-b2b-shop-customer-support",9,[533,604,605,606,607],"AI assistant","Customer support","LLM agents","EU AI Act",[609,610,611,612,613,614,615,616,617,618,619],"ai assistant b2b shop","ai customer support b2b ecommerce","llm chatbot order status","ai agent customer service tools","chatbot liability air canada","klarna ai assistant results","human handoff chatbot design","eu ai act chatbot disclosure","owasp excessive agency","ai support resolution rate","b2b customer portal chatbot",[621,624,627],{"name":622,"url":623},"Customer service","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FCustomer_service",{"name":625,"url":626},"Chatbot","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FChatbot",{"name":554,"url":555},[629,632,635,638,641,644,647],{"title":630,"url":631},"Klarna: AI assistant handles two-thirds of customer service chats in its first month (press release, 2024)","https:\u002F\u002Fwww.klarna.com\u002Finternational\u002Fpress\u002Fklarna-ai-assistant-handles-two-thirds-of-customer-service-chats-in-its-first-month\u002F",{"title":633,"url":634},"Entrepreneur: Klarna is hiring customer service agents again, according to its CEO (May 2025)","https:\u002F\u002Fwww.entrepreneur.com\u002Fbusiness-news\u002Fklarna-ceo-reverses-course-by-hiring-more-humans-not-ai\u002F491396",{"title":636,"url":637},"ABA Business Law Today: BC tribunal confirms companies remain liable for information provided by AI chatbot","https:\u002F\u002Fwww.americanbar.org\u002Fgroups\u002Fbusiness_law\u002Fresources\u002Fbusiness-law-today\u002F2024-february\u002Fbc-tribunal-confirms-companies-remain-liable-information-provided-ai-chatbot\u002F",{"title":639,"url":640},"OWASP GenAI Security Project: LLM06:2025 Excessive Agency","https:\u002F\u002Fgenai.owasp.org\u002Fllmrisk\u002Fllm062025-excessive-agency\u002F",{"title":642,"url":643},"OWASP GenAI Security Project: LLM05:2025 Improper Output Handling","https:\u002F\u002Fgenai.owasp.org\u002Fllmrisk\u002Fllm052025-improper-output-handling\u002F",{"title":645,"url":646},"EU AI Act, Article 50: Transparency obligations","https:\u002F\u002Fartificialintelligenceact.eu\u002Farticle\u002F50\u002F",{"title":648,"url":649},"Intercom: Fin pricing and billable outcomes","https:\u002F\u002Ffin.ai\u002Fpricing","\u002Fimages\u002Fblog\u002Fai-assistant-b2b-shop-customer-support\u002Fcover.webp","\u002Fimages\u002Fblog\u002Fai-assistant-b2b-shop-customer-support\u002Fog.jpg",[77,78,79],"AI-asszisztens a B2B webshop ügyfélszolgálatához: eszközök, jogosultságok és átadás embernek","Hogyan építs AI ügyfélszolgálati asszisztenst B2B shophoz: mit automatizálj, eszközök ügyfélkontextusban, átadás embernek, AI Act és mérőszámok.","Magas fal keskeny réssel: nyíl halad át rajta a három négyzetből álló bekerített mezőbe, egy másik lefelé, egy tálca felé mutat.",{"slug":657,"published":5,"minutes":602,"category":7,"tags":658,"keywords":663,"about":675,"sources":685,"cover":718,"og":719,"expertise":75,"locales":720,"lang":79,"title":721,"description":722,"coverAlt":723},"llm-translation-pipeline-i18n",[659,660,661,9,662],"Translation","i18n","LLM","Localization",[664,665,666,667,668,669,670,671,672,673,674],"llm translation pipeline","icu messageformat placeholders","vue i18n llm translation","translation memory llm","glossary enforcement translation","german informal sie du","hungarian translation quality llm","structured output json translation","machine translation post-editing","locale file validation","cldr plural categories",[676,679,682],{"name":677,"url":678},"Internationalization and localization","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FInternationalization_and_localization",{"name":680,"url":681},"ICU MessageFormat","https:\u002F\u002Funicode-org.github.io\u002Ficu\u002Fuserguide\u002Fformat_parse\u002Fmessages\u002F",{"name":683,"url":684},"Vue I18n","https:\u002F\u002Fvue-i18n.intlify.dev\u002F",[686,688,691,694,697,700,703,706,709,712,715],{"title":687,"url":681},"Unicode ICU User Guide: Formatting Messages",{"title":689,"url":690},"Vue I18n: Message Format Syntax","https:\u002F\u002Fvue-i18n.intlify.dev\u002Fguide\u002Fessentials\u002Fsyntax",{"title":692,"url":693},"Nuxt i18n: Getting started","https:\u002F\u002Fi18n.nuxtjs.org\u002Fdocs\u002Fgetting-started",{"title":695,"url":696},"Unicode CLDR: Plural rules","https:\u002F\u002Fcldr.unicode.org\u002Findex\u002Fcldr-spec\u002Fplural-rules",{"title":698,"url":699},"OpenAI: Structured outputs","https:\u002F\u002Fplatform.openai.com\u002Fdocs\u002Fguides\u002Fstructured-outputs",{"title":701,"url":702},"DeepL API: Glossaries","https:\u002F\u002Fdevelopers.deepl.com\u002Fdocs\u002Fapi-reference\u002Fglossaries",{"title":704,"url":705},"W3C: Localization vs. internationalization","https:\u002F\u002Fwww.w3.org\u002FInternational\u002Fquestions\u002Fqa-i18n",{"title":707,"url":708},"GNU gettext manual","https:\u002F\u002Fwww.gnu.org\u002Fsoftware\u002Fgettext\u002Fmanual\u002Fgettext.html",{"title":710,"url":711},"RFC 5646: Tags for Identifying Languages","https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc5646",{"title":713,"url":714},"Rat für deutsche Rechtschreibung","https:\u002F\u002Fwww.rechtschreibrat.com\u002F",{"title":716,"url":717},"Microsoft Writing Style Guide","https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fstyle-guide\u002Fwelcome\u002F","\u002Fimages\u002Fblog\u002Fllm-translation-pipeline-i18n\u002Fcover.webp","\u002Fimages\u002Fblog\u002Fllm-translation-pipeline-i18n\u002Fog.jpg",[77,78,79],"LLM-fordítási pipeline i18n-hez: glosszárium, helyőrzők és review","Termék- és weboldalszövegek fordítása LLM-mel úgy, hogy a helyőrzők, a többes szám és a hangnem épen maradjon: glosszárium, ICU üzenetek, QA-ellenőrzések.","Három forráskártya, egy lefordított kártya glosszárium-szalaggal, majd ellenőrzött tábla.",{"slug":725,"published":726,"minutes":727,"category":7,"tags":728,"keywords":732,"about":743,"sources":750,"cover":766,"og":767,"expertise":75,"locales":768,"lang":79,"title":769,"description":770,"coverAlt":771},"nuxt-i18n-multilingual-seo-hreflang","2026-10-16",8,[10,660,729,730,731],"SEO","hreflang","Multilingual",[733,734,735,736,737,738,739,740,741,742],"nuxt i18n seo","nuxt hreflang","useLocaleHead","nuxt i18n canonical","hreflang x-default","prefix_except_default","multilingual seo nuxt","nuxt i18n sitemap hreflang","hreflang self-referencing","nuxt i18n baseUrl",[744,746,749],{"name":10,"url":745},"https:\u002F\u002Fnuxt.com\u002F",{"name":747,"url":748},"Hreflang","https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FHreflang",{"name":677,"url":678},[751,754,757,760,763],{"title":752,"url":753},"Nuxt i18n docs: SEO (useLocaleHead)","https:\u002F\u002Fi18n.nuxtjs.org\u002Fdocs\u002Fguide\u002Fseo",{"title":755,"url":756},"Nuxt i18n docs: Routing strategies","https:\u002F\u002Fi18n.nuxtjs.org\u002Fdocs\u002Fguide",{"title":758,"url":759},"Google Search Central: Tell Google about localized versions of your page","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fspecialty\u002Finternational\u002Flocalized-versions",{"title":761,"url":762},"Google Search Central: Managing multi-regional and multilingual sites","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fspecialty\u002Finternational\u002Fmanaging-multi-regional-sites",{"title":764,"url":765},"Google Search Central: How to specify a canonical URL","https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fcrawling-indexing\u002Fconsolidate-duplicate-urls","\u002Fimages\u002Fblog\u002Fnuxt-i18n-multilingual-seo-hreflang\u002Fcover.webp","\u002Fimages\u002Fblog\u002Fnuxt-i18n-multilingual-seo-hreflang\u002Fog.jpg",[77,78,79],"Többnyelvű SEO Nuxt i18n-nel: URL-stratégia, hreflang, canonical és sitemap","Így állítsd be a Nuxt i18n modult, hogy a keresők minden nyelvet indexeljenek: útvonal-stratégia, useLocaleHead, hreflang, részleges fordítás, sitemap.","Nagy lapot három vízszintes kapcsolat köt össze három kisebb lap oszlopával.",1792245343978]