# React AI Chat UI: Streaming, Auto-Scroll und Mobile

> Wie eine React AI Chat UI Streaming, Auto-Scroll, IME-Eingabe und die Handytastatur sauber löst: Teil für Teil, mit Code für den Next.js App Router.

- Autor: [Serban Rusu](https://wingo-ui.com/blog/authors/serban), Founder of Wingo UI
- Veröffentlicht: 9. Okt. 2026
- Kategorie: [UI mit KI-Agenten bauen](https://wingo-ui.com/de/blog/category/ai-agents)
- Lesezeit: 10 Min.
- Kanonische URL: https://wingo-ui.com/de/blog/react-ai-chat-ui
- Englische Fassung: https://wingo-ui.com/blog/react-ai-chat-ui

## Kurz gesagt

Eine React AI Chat UI für die Produktion besteht aus fünf Teilen mit einem gemeinsamen Nachrichtentyp: Thread, Nachrichten, Composer, Reasoning-Zeile und Model Picker. Die Arbeit steckt im Verhalten: dem Stream nur folgen, solange der Leser unten ist, nur die geänderte Nachricht neu rendern, halb geschriebenes Markdown reparieren, Enter während der IME-Komposition ignorieren und den Composer über der Handytastatur halten. Streame jedes Delta in ein neues Objekt für genau diese eine Nachricht und brich mit einem AbortSignal ab, damit Stop die Teilantwort behält.

Eine React AI Chat UI sieht nach einem Nachmittag fertig aus: eine Liste mit Bubbles, eine Textarea und ein fetch-Aufruf, der Text streamt. Im echten Einsatz zeigen sich die Lücken. Der Thread zieht den Leser wieder nach unten, während er eine frühere Antwort noch einmal liest. Ein halb geschriebener Code-Fence macht den Rest der Antwort zu Code. Wer mit einer japanischen IME tippt, schickt mit Enter einen halben Satz ab, und auf dem iPhone rutscht der Composer hinter die Tastatur. Dieser Guide zeigt die fünf Teile eines produktionsreifen Chats, was jeder davon abfangen muss und mit welchem Code du sie an dein Modell anbindest. Die Beispiele nutzen die KI-Komponenten von Wingo UI, der Bibliothek, die wir bauen. Wir sind also nicht neutral. Fakten aus anderen Quellen verlinken wir.

## Aus welchen Teilen besteht eine React AI Chat UI?

Aus fünf Teilen mit einem gemeinsamen Nachrichtentyp. Jede Komponente einer React AI Chat UI hat genau eine Aufgabe, und bei jeder taucht ein typischer Fehler als Erstes auf:

| Teil | Aufgabe | Was zuerst kaputtgeht | In Wingo UI |
|---|---|---|---|
| Thread | scrollt das Gespräch, pinnt den Composer | dem Stream folgen, während der Leser nach oben scrollt | [Chat Thread](https://wingo-ui.com/components/chat-thread) |
| Nachricht | ein Beitrag: Markdown, Dateien, Quellen, Aktionen | jede Nachricht rendert bei jedem Token neu | [Chat Message](https://wingo-ui.com/components/chat-message) |
| Composer | Text, Dateien, Tools, Senden und Stop | Enter während der IME-Komposition, die Handytastatur | [Prompt Input](https://wingo-ui.com/components/prompt-input) |
| Reasoning | das Denken des Modells, eingeklappt | eine Wand aus Gedanken schiebt die Antwort aus dem Bild | [Reasoning Indicator](https://wingo-ui.com/components/reasoning-indicator) |
| Model Picker | Modelle wechseln | ein enges Popover auf dem Handy | [Model Picker](https://wingo-ui.com/components/model-picker) |

Der gemeinsame Typ ist `ChatMessageData` aus `lib/ai-chat`: eine `id`, eine `role`, `content` als Markdown und ein `status` mit `pending`, `streaming`, `complete`, `stopped` oder `error`, dazu optional `reasoning`, `toolCalls`, `attachments` und `sources`. Der Status steuert den Großteil des Screens: die Tipp-Punkte, den Textcursor, die Aktionen und das, was ein Screenreader vorliest.

```bash
npx wingo-ui@latest add chat-thread prompt-input
```

Die CLI holt Chat Message, Reasoning Indicator, Model Picker und Markdown als Abhängigkeiten mit. Coding-Agenten installieren dieselben Items über unseren MCP-Server; der [Guide zur Component Library mit MCP](https://wingo-ui.com/de/blog/component-library-mcp-guide) zeigt das Setup.

## Wie sollte ein Chat-Thread scrollen, während eine Antwort streamt?

Folge neuem Text nur, solange der Leser unten bleibt. Lass sofort los, wenn er nach oben scrollt, und komm zurück, wenn er sendet oder auf einen Sprung-Button tippt. Die [Doku zum Message Scroller von shadcn](https://ui.shadcn.com/docs/components/radix/message-scroller) formuliert die Regel so: Wer wegscrollt, wird dort in Ruhe gelassen.

Damit das im Alltag hält:

- **Erst die Absicht lesen, dann die Position.** Ganz unten hat eine Mausrad-Drehung nach oben `scrollTop` noch nicht verändert, wenn der nächste Chunk ankommt. Der Hook [useStickToBottom](https://wingo-ui.com/components/use-stick-to-bottom) des Threads lässt schon beim Wheel-Event selbst los, damit eine streamende Antwort den Leser nie zurückzieht.
- **Außerhalb von React folgen.** Das Folgen schreibt `scrollTop` in einer `requestAnimationFrame`-Schleife, die das Ziel in jedem Frame neu liest. React-State ändert sich nur, wenn „ganz unten“ oder „folgt“ umschlägt.
- **Senden führt zurück nach unten**, auch wenn der Leser vorher hochgescrollt hat.
- **Beim Nachladen der Historie bleibt das Auge, wo es ist.** Bevor ältere Nachrichten oben eingefügt werden, merkt sich der Thread die erste sichtbare Nachricht und ihren Abstand und stellt beides danach wieder her.
- **Die Sprung-Pille zählt mit.** Solange du hochgescrollt bist, zeigt eine Pille über dem Composer, wie viele Antworten angekommen sind. Ein Tipp darauf scrollt sanft nach unten und setzt den Fokus auf die neueste Nachricht.

> Live-Demo (Chat Thread): Wähl eine Folgefrage oder tipp eine Frage, dann scroll nach oben, während die Antwort streamt: Der Thread lässt sofort los und eine Pille zählt die neue Antwort. Tipp auf die Pille, um wieder nach unten zu gleiten. Probier sie unter [Chat Thread](https://wingo-ui.com/components/chat-thread) aus und installier sie mit `npx wingo-ui@latest add chat-thread`.

Der komplette Ablauf mit deiner eigenen Route:

```tsx
"use client";

import { useRef, useState } from "react";
import { ChatThread, type ChatMessageData } from "@/components/ui/chat-thread";
import { PromptInput, type PromptInputMessage } from "@/components/ui/prompt-input";
import { createChatId } from "@/lib/ai-chat";

export function Assistant() {
  const [messages, setMessages] = useState<ChatMessageData[]>([]);
  const [streaming, setStreaming] = useState(false);
  const abort = useRef<AbortController | null>(null);

  const patch = (id: string, change: Partial<ChatMessageData>) =>
    setMessages((list) => list.map((m) => (m.id === id ? { ...m, ...change } : m)));

  async function send({ text }: PromptInputMessage) {
    const question: ChatMessageData = { id: createChatId("u"), role: "user", content: text, createdAt: Date.now() };
    const history = [...messages, question].map(({ role, content }) => ({ role, content }));
    const id = createChatId("a");
    setMessages((list) => [...list, question, { id, role: "assistant", content: "", status: "pending", createdAt: Date.now() }]);
    const controller = new AbortController();
    abort.current = controller;
    setStreaming(true);
    let content = "";
    try {
      const response = await fetch("/api/chat", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ messages: history }),
        signal: controller.signal,
      });
      if (!response.ok || !response.body) throw new Error("Der Assistent ist gerade nicht erreichbar.");
      const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
      while (true) {
        const { value, done } = await reader.read();
        if (done) break;
        content += value;
        patch(id, { content, status: "streaming" });
      }
      patch(id, { status: "complete" });
    } catch {
      patch(id, { status: controller.signal.aborted ? "stopped" : "error" });
    } finally {
      setStreaming(false);
    }
  }

  return (
    <div className="h-dvh">
      <ChatThread
        messages={messages}
        composer={
          <PromptInput
            status={streaming ? "streaming" : "idle"}
            onSubmit={send}
            onStop={() => abort.current?.abort()}
            stopShortcut="escape"
          />
        }
      />
    </div>
  );
}
```

`/api/chat` ist dein Route Handler. Er muss die Antwort nur als reinen Text-Stream zurückgeben; mit dem AI SDK liefert [`streamText(...).toTextStreamResponse()`](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text) genau das. Die wartende Nachricht zeigt Tipp-Punkte bis zum ersten Chunk, und Stop behält den Text, der schon angekommen ist.

## Wie streamst du Tokens, ohne jede Nachricht neu zu rendern?

Gib nur der geänderten Nachricht ein neues Objekt, memoisiere die Nachrichten-Komponente und rendere Markdown Block für Block, damit jeder Chunk nur den letzten Block neu parst.

- **Memoisierte Nachrichten.** Chat Message steckt in `memo`, und Chat Thread bindet die Handler jeder Nachricht über eine Ref. Eine fertige Nachricht rendert also nur dann neu, wenn sich ihr eigenes Objekt ändert. Leg `messageProps` als Konstante auf Modulebene an oder pack es in `useMemo`, damit Labels und Klassennamen zwischen den Chunks ihre Identität behalten.
- **Markdown reparieren.** Der [Markdown](https://wingo-ui.com/components/markdown)-Renderer teilt die Antwort in Blöcke auf oberster Ebene. Während des Streamings repariert er das Ende vor dem Parsen: Er schließt einen offenen Code-Fence und nicht geschlossene `**`, `~~` oder Backticks, verwirft ein halb getipptes Link-Ziel und hält eine Tabelle zurück, bis ihre Trennzeile ankommt. Ohne das springt eine Antwort bei jedem Chunk zwischen Rohtext und formatiertem Text hin und her. Wenn du bei react-markdown bleibst: [Streamdown](https://github.com/vercel/streamdown) beschreibt sich als Drop-in-Ersatz für KI-Streaming, und sein [Parsing unvollständiger Blöcke](https://streamdown.ai/docs/termination) schließt offenes Fett, Inline-Code und Links. Der [Guide zu React Markdown Streaming](https://wingo-ui.com/de/blog/react-markdown-streaming) baut die Reparatur und das Memo pro Block Schritt für Schritt auf.
- **Eine Ansage pro Antwort.** Der Thread ist nie eine Live-Region, denn ein Screenreader, der jedes gestreamte Wort vorliest, ist unbrauchbar. Eine separate Status-Region im Modus polite sagt an, wann eine Antwort beginnt und wann sie fertig ist, und die streamende Nachricht trägt `aria-busy`.

## Was muss der Composer abfangen?

Auf einer Hardware-Tastatur sendet Enter und Shift+Enter fügt eine Zeile ein. Enter sendet nie, solange eine IME komponiert. Touch-Tastaturen bekommen neue Zeilen, und der Senden-Button wird zu Stop, während die Antwort streamt.

Zuerst die IME, denn hier scheitern ganze Sprachen. Japanische, chinesische und koreanische Eingabemethoden bestätigen eine Umwandlung mit Enter. Die [keydown-Referenz von MDN](https://developer.mozilla.org/en-US/docs/Web/API/Element/keydown_event) weist darauf hin, dass `isComposing` beim keydown, der die IME schließt, false sein kann, während `keyCode` noch 229 ist. Der Guard prüft deshalb beides. Das ist die erste Zeile im Key-Handler von Prompt Input:

```tsx
if (event.nativeEvent.isComposing || event.key === "Process" || event.keyCode === 229) return;
```

Der Rest:

- **Touch-Tastaturen.** Sie haben kein Shift+Enter, also fügt ein einfaches Enter auf dem Handy eine neue Zeile ein, und der runde Button sendet, ohne die Tastatur zu schließen. Der Text ist auf Handys 16px groß, damit iOS Safari nicht ins Feld zoomt.
- **Fehlgeschlagenes Senden.** Das Feld leert sich, sobald du sendest. Wenn `onSubmit` ein Promise zurückgibt, das rejected wird, kommen Text und Dateien zurück, sofern die Person nicht schon weitergetippt hat.
- **Stop.** Mit `status="streaming"` wird der Button zu Stop, und `stopShortcut="escape"` greift, solange der Fokus im Composer liegt. Ein Dialog auf der Seite behält so sein eigenes Escape.
- **Die Tastatur verdeckt das Feld.** Seit Chrome 108 verkleinert Chrome auf Android beim Öffnen der Tastatur nur noch den Visual Viewport, genau wie Safari auf iOS ([Ankündigung von Chrome](https://developer.chrome.com/blog/viewport-resize-behavior)). Ein unten fixierter Composer landet dann dahinter. Mit `position="fixed"` liest Prompt Input `visualViewport` und hebt sich über einen Motion Value um die verdeckte Höhe an, ohne neu zu rendern. Innerhalb von Chat Thread brauchst du das nicht; der Thread pinnt den Composer mit Safe-Area-Padding. Mehr dazu: [100vh auf Mobile](https://wingo-ui.com/de/blog/100vh-mobile-dvh-tailwind) und [Safe Area Insets in Tailwind](https://wingo-ui.com/de/blog/tailwind-safe-area-insets) (Englisch).

Der [Guide zum React Prompt Input](https://wingo-ui.com/de/blog/react-prompt-input) behandelt jeden dieser Punkte ausführlich, dazu Datei-Uploads mit Fortschritt und Diktat an der Cursor-Position.

## Wie sollte Reasoning in einem KI-Chat aussehen?

Eingeklappt. Während das Modell denkt, zeigst du ein schimmerndes Label, einen laufenden Timer und die letzten zwei Zeilen des Gedankens. Wenn es fertig ist, bleibt „Thought for 12 sec“ stehen und öffnet sich per Tipp. Reasoning kann länger sein als die Antwort, und niemand sollte daran vorbeiscrollen müssen, um die Antwort zu finden.

Setz `reasoning: { content, status, startedAt, endedAt }` auf die Nachricht oder nutz Reasoning Indicator allein mit `content`, `streaming` und `startedAt`. Mit `autoCollapse` (standardmäßig an) schließt sich eine Zeile, die beim Start des Denkens offen war, sobald es endet. Eine Zeile, die die Person selbst geöffnet hat, bleibt offen. Auf dem Handy endet der geöffnete Inhalt bei 40% der Bildschirmhöhe, und die Zeile behält eine Trefferfläche von 44px.

> Live-Demo (Reasoning Indicator): Sieh zu, wie das Label schimmert, während der Timer zählt und die letzten Zeilen des Gedankens nach oben gleiten. Sobald es fertig ist, tipp auf die Zeile, um das ganze Reasoning zu lesen. Probier sie unter [Reasoning Indicator](https://wingo-ui.com/components/reasoning-indicator) aus und installier sie mit `npx wingo-ui@latest add reasoning-indicator`.

Chat Message klappt Tool Calls genauso zu einer einzeiligen Zusammenfassung ein, die sich aufklappen lässt. Der [Guide zur Tool Call UI für KI-Agenten](https://wingo-ui.com/de/blog/ai-agent-tool-call-ui) zeigt die Status, Fehler und Freigabe-Zeilen, die jeder Aufruf braucht.

## Wohin gehört der Model Picker auf dem Handy?

In den Composer, und er öffnet sich als Bottom Sheet. Auf einem 390px breiten Screen lässt ein 320px breites Popover an einem kleinen Toolbar-Button wenig Platz für Namen und Beschreibungen. Unter 768px wechselt Model Picker zu einem Sheet mit 64px hohen Zeilen und zweizeiligen Beschreibungen (`responsive`, standardmäßig an). Gib `models` an Prompt Input weiter, und der Picker wandert in dessen Toolbar:

```tsx
// app/assistant/models.ts
import type { AiModel } from "@/components/ui/model-picker";

export const MODELS: AiModel[] = [
  { id: "standard", name: "Northwind Standard", description: "Ausgewogen, für die tägliche Arbeit", recommended: true },
  { id: "pro", name: "Northwind Pro", description: "Gründliches Reasoning für Berichte", disabled: true, disabledReason: "Im Business-Plan verfügbar" },
];
```

Mit `<PromptInput models={MODELS} defaultModel="standard" />` bekommt `onSubmit` die gewählte id als `message.model`. Gesperrte Modelle bleiben gedimmt in der Liste, mit `disabledReason` statt ihrer Beschreibung. So sehen Nutzer, was ein höherer Plan bringt, statt sich zu fragen, wo ein Modell geblieben ist. Bei mehr als 8 Modellen erscheint eine Suchzeile, und `modelPickerProps={{ shortcut: "mod+." }}` öffnet und schließt die Liste von überall, auch aus Textfeldern.

## Wie verbindest du eine komplette KI-Chat-Seite mit deinem Modell?

Nimm den Block [AI Chat Page](https://wingo-ui.com/components/ai-chat-page) und übergib `onSend`: einen async Generator, der Text-Deltas liefert, mit einem `signal`, das bei Stop, beim Wechsel des Gesprächs und beim Unmount abbricht. Stop behält, was angekommen ist, und markiert die Nachricht als gestoppt.

```tsx
// app/assistant/page.tsx
"use client";

import { AiChatPage, type AiChatSendHandler } from "@/components/blocks/ai-chat-page";
import { MODELS } from "./models";

const onSend: AiChatSendHandler = async function* (message, { model, messages, signal }) {
  const response = await fetch("/api/chat", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ model, messages: messages.map(({ role, content }) => ({ role, content })) }),
    signal,
  });
  if (!response.ok || !response.body) throw new Error("Der Assistent ist gerade nicht erreichbar.");
  const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
  while (true) {
    const { value, done } = await reader.read();
    if (done) return;
    yield value;
  }
};

export default function AssistantPage() {
  return (
    <AiChatPage
      onSend={onSend}
      defaultConversationId={null}
      defaultConversations={[]}
      models={MODELS}
      defaultModel="standard"
      tools={[]}
      suggestions={[]}
      usage={[]}
      plan={null}
      user={{ id: "emma", name: "Emma Carter" }}
      assistantName="Northwind Assistant"
    />
  );
}
```

Die Props nach `onSend` sind wichtig. Ohne sie zeigt der Block seine Beispielinhalte: einen Verlauf mit zehn Gesprächen, Beispielmodelle und -tools, einen Nutzungsring, einen Plan-Upsell und einen Beispielnutzer. Im Playground ist das nützlich, in der Produktion falsch. Gib also deine eigenen Daten oder leere Werte mit. `messages` im Kontext enthält die neue Frage bereits.

Wirft der Handler einen Fehler, erscheint er an der Nachricht, zusammen mit einem Retry-Button. Ohne `onSend` läuft im Block ein geskripteter Mock mit Reasoning, Tool Calls und gestreamtem Markdown. So baust du den Screen, bevor das Backend existiert. Für eine React AI Chatbot UI in einem Support-Panel nimmst du Chat Thread allein mit `variant="bubbles"` und `size="sm"`.

> Live-Demo (AI Chat Page): In einem so schmalen Rahmen bekommst du das Handy-Layout: Der Verlauf liegt hinter dem Menü-Button und der Model Picker wandert in den Composer. Sende eine Frage und drück dann Stop, während sie streamt. Probier sie unter [AI Chat Page](https://wingo-ui.com/components/ai-chat-page) aus und installier sie mit `npx wingo-ui@latest add ai-chat-page`.

## Wann sind AI Elements oder assistant-ui die bessere Wahl?

Wenn deine App schon auf dem AI SDK läuft oder wenn die Bibliothek den Chat-State verwalten soll. Beide sind kostenlos und Open Source; die KI-Komponenten von Wingo UI sind kostenpflichtig und überlassen State und Transport dir. So schneiden die drei AI Chat UI Kits im Vergleich ab, Stand Oktober 2026:

| | AI Elements | assistant-ui | KI-Komponenten von Wingo UI |
|---|---|---|---|
| Lizenz und Preis | Apache 2.0, kostenlos | MIT, kostenlos; Assistant Cloud für gehostete Threads ist optional | Pro: $8 im Monat, $80 im Jahr oder $150 Lifetime |
| Basis | shadcn/ui | Primitives, die du selbst stylst; ein CLI-Starter in Base UI oder Radix mit einem shadcn/ui-Theme | Tailwind CSS v4 und Motion, als Quellcode installiert |
| Chat-State | Das AI SDK (`useChat`) | Eigene Runtimes für das AI SDK, LangGraph und eigene Backends | Bei dir: Du übergibst `messages` und einen `status` |
| Am besten, wenn | deine App schon das AI SDK nutzt | dir die Bibliothek Threads, Streaming und Tool Calls abnehmen soll | du den Transport selbst steuerst und Scroll-Regeln, Handy-Layouts und Ansagen fertig haben willst |

Quellen: die [Doku von AI Elements](https://elements.ai-sdk.dev/components/conversation) und das [Repository](https://github.com/vercel/ai-elements), die [Primitives von assistant-ui](https://www.assistant-ui.com/docs/primitives), der [Runtime-Guide](https://www.assistant-ui.com/docs/runtimes/pick-a-runtime) und das [Repository](https://github.com/assistant-ui/assistant-ui) sowie unsere [Preisseite](https://wingo-ui.com/de/pricing).

Wenn deine App auf dem AI SDK läuft und du eine kostenlose shadcn Chat UI willst, fang mit AI Elements an. Wenn eine Bibliothek den Gesprächs-State verwalten soll, macht assistant-ui genau das. Wingo UI passt, wenn das Backend schon dir gehört und der Screen der Teil ist, den du nicht selbst bauen willst.

## Was solltest du als Nächstes ausprobieren?

Öffne die Doku zur [AI Chat Page](https://wingo-ui.com/components/ai-chat-page), probier ihre Props im Playground aus und installier sie dann mit `npx wingo-ui@latest add ai-chat-page`. Sie und die fünf Komponenten aus diesem Guide gehören zu Wingo UI Pro. Weitere Guides zum Bauen mit Coding-Agenten findest du unter [UI mit KI-Agenten bauen](https://wingo-ui.com/de/blog/category/ai-agents).

## Komponenten in diesem Artikel

- [Chat Thread](https://wingo-ui.com/components/chat-thread) (Pro): A React chat thread for AI chats: the scrolling conversation, pinned to the newest message while it streams, with day pills and follow-ups. Installation: `npx wingo-ui@latest add chat-thread`
- [Chat Message](https://wingo-ui.com/components/chat-message) (Pro): A React AI chat message: the question in a bubble, the answer with its thinking, tool calls, markdown, sources and actions. Installation: `npx wingo-ui@latest add chat-message`
- [Prompt Input](https://wingo-ui.com/components/prompt-input) (Pro): A React prompt input for AI chat: a growing text box with attachments, tool chips, a model picker, dictation and a send and stop button. Installation: `npx wingo-ui@latest add prompt-input`
- [Reasoning Indicator](https://wingo-ui.com/components/reasoning-indicator) (Pro): A React reasoning indicator for AI chat: a shimmering live timer while the model thinks, then a quiet "Thought for 12 s" row to expand. Installation: `npx wingo-ui@latest add reasoning-indicator`
- [Model Picker](https://wingo-ui.com/components/model-picker) (Pro): A React AI model picker for chat apps: a quiet trigger that opens a short grouped list of models, each with one line on what it is for. Installation: `npx wingo-ui@latest add model-picker`
- [AI Chat Page](https://wingo-ui.com/components/ai-chat-page) (Pro): A React AI chat page with conversation history, a streaming thread with reasoning and tool calls, a prompt composer and plan usage. Installation: `npx wingo-ui@latest add ai-chat-page`

## Häufige Fragen

### Wie scrolle ich einen React-Chat automatisch mit, während die KI-Antwort streamt?

Folge neuem Inhalt nur, solange der Leser nahe am Ende ist, hör beim ersten Mausrad- oder Touch-Scroll nach oben auf und scroll wieder nach unten, wenn er eine Nachricht sendet oder einen Sprung-Button drückt. Schreib scrollTop in einer requestAnimationFrame-Schleife statt in React-State, damit das Streaming die Liste nicht neu rendert.

### Warum sendet mein Chat-Eingabefeld die Nachricht, wenn ich in einer japanischen IME Enter drücke?

Das Enter, das die Umwandlung bestätigt, erreicht deinen keydown-Handler. Verlass den Handler sofort, wenn event.nativeEvent.isComposing true ist oder event.keyCode 229, denn laut MDN kann isComposing beim letzten keydown einer Komposition false sein.

### Wie rendere ich streamendes Markdown in React ohne Flackern?

Repariere das unfertige Ende vor dem Parsen: schließ einen offenen Code-Fence und offene Hervorhebungen, verwirf einen halb getippten Link und halte eine Tabelle zurück, bis ihre Trennzeile da ist. Rendere die Antwort Block für Block, damit fertige Blöcke ihr DOM behalten und nur der letzte neu geparst wird.

### Gibt es eine Chat-UI-Komponente für shadcn/ui?

shadcn/ui hat einen Message Scroller für den Verlauf, und AI Elements von Vercel ist eine kostenlose Komponentenbibliothek auf Basis von shadcn/ui für Apps mit dem AI SDK. Chat Thread, Prompt Input und die anderen KI-Komponenten von Wingo UI installierst du ebenfalls als Quelldateien, die dir gehören, und sie sind Teil von Wingo UI Pro.

### Sind die KI-Chat-Komponenten von Wingo UI kostenlos?

Nein. Chat Thread, Chat Message, Prompt Input, Reasoning Indicator, Model Picker und der Block AI Chat Page gehören zu Wingo UI Pro. Die 75 kostenlosen Items enthalten Grundbausteine, auf denen sie aufbauen, etwa Button, Textarea und Tooltip.

---

Quelle: https://wingo-ui.com/de/blog/react-ai-chat-ui
