Chat-System

Streaming-SSE-Protokoll, React-Hooks, API-Routen, Chat-Komponenten und Quick Prompts

Kit bietet ein vollständiges Chat-System mit zwei Modi — LLM-Chat für direkte KI-Gespräche und RAG-Chat für wissensbasierte Antworten. Beide nutzen Streaming-SSE (Server-Sent Events) für die Echtzeit-Token-Übertragung, teilen eine gemeinsame Komponentenbibliothek und sind in das Credit-System integriert.
Diese Seite behandelt das Streaming-Protokoll, API-Routen, React-Hooks und Chat-Komponenten. Für die RAG-spezifische Pipeline siehe RAG-System.

Zwei Chat-Modi

AspektLLM-ChatRAG-Chat
Dashboard-Route/dashboard/chat-llm/dashboard/chat-rag
API-Route/api/ai/stream (SSE), /api/ai/chat (JSON)/api/ai/rag/ask
HookuseAIChat()Eigener RAG-Hook
KontextquelleNur Gesprächsverlaufpgvector-Suche + Gespräch
Feature-FlagNEXT_PUBLIC_AI_LLM_CHAT_ENABLEDNEXT_PUBLIC_AI_RAG_CHAT_ENABLED
Credit-Kosten20 Credits (Streaming), 30 (Bildanalyse), 40 (PDF-Analyse), 20 (Spracheingabe), 15 (synchron)15 Credits
Auth erforderlichJa (Clerk)Ja (Clerk)

Vision-Chat (Bildanalyse)

Wenn NEXT_PUBLIC_AI_VISION_ENABLED=true (Standard) und LLM-Chat aktiviert ist, können Benutzer Bilder an Nachrichten anhängen, um sie von der KI analysieren zu lassen. Vision-Chat erweitert die bestehende LLM-Chat-Oberfläche um Bild-Upload-Funktionen.
Upload-Methoden: Drag & Drop auf den Chat-Bereich, Einfügen aus der Zwischenablage (Strg+V) oder Dateiauswahl-Schaltfläche.
Einschränkungen:
EinschränkungWert
Maximale Bildgröße4,5 MB pro Bild
Maximale Bilder pro Nachricht4
Unterstützte FormatePNG, JPEG, WebP, GIF
Bilder werden als Base64-Data-URIs kodiert und als ContentPart[] im Nachrichteninhalt-Feld gesendet:
json
{
  "messages": [{
    "role": "user",
    "content": [
      { "type": "image", "image": "data:image/png;base64,..." },
      { "type": "text", "text": "Describe this image" }
    ]
  }]
}
Die Stream-Route erkennt Bildinhalte automatisch und wählt den image_analysis-Credit-Vorgang (30 Credits) statt chat_streaming (20 Credits). Der Kerntyp Message.content bleibt aus Gründen der Rückwärtskompatibilität string — multimodales ContentPart[] wird ausschließlich an der API-Grenze verarbeitet.

PDF-Chat (Dokumentenanalyse)

Wenn NEXT_PUBLIC_AI_PDF_CHAT_ENABLED=true (Standard) und LLM-Chat aktiviert ist, können Benutzer PDF-Dokumente an Nachrichten anhängen, um sie von der KI analysieren zu lassen. PDF-Chat verwendet serverseitige Textextraktion mit pdf-parse — keine Vision-API erforderlich, daher funktioniert es mit allen Providern (OpenAI, Anthropic, Google, xAI).
Upload-Methoden: Drag & Drop auf den Chat-Bereich oder Dateiauswahl-Schaltfläche (Büroklammer-Symbol).
Einschränkungen:
EinschränkungWert
Maximale Dateigröße10 MB pro PDF
Maximale PDFs pro Nachricht1
Unterstützte FormateNur PDF (.pdf)
Maximaler extrahierter Text50.000 Zeichen
PDFs werden als ArrayBuffer gelesen, als Base64 an den Server gesendet und serverseitig extrahiert. Der extrahierte Text wird der Benutzernachricht als Kontext vorangestellt:
json
{
  "messages": [{
    "role": "user",
    "content": "--- PDF Document: report.pdf ---\n[extracted text]\n--- End PDF ---\n\nSummarize this document"
  }],
  "pdfAttached": true
}
Die Stream-Route erkennt pdfAttached: true automatisch und wählt den pdf_analysis-Credit-Vorgang (40 Credits) statt chat_streaming (20 Credits).

Audio-Eingabe (Speech-to-Text) {#audio-input-speech-to-text}

Wenn NEXT_PUBLIC_AI_AUDIO_INPUT_ENABLED=true (Standard) und LLM-Chat aktiviert ist, erscheint eine Mikrofon-Schaltfläche im Chat-Eingabebereich. Benutzer können Sprachnachrichten aufnehmen, die über die OpenAI-Whisper-API transkribiert und in das Chat-Eingabefeld eingefügt werden.
Aufnahmeablauf: Mikrofon-Schaltfläche drücken → Berechtigung erteilen → aufnehmen (mit Live-Audiopegelanzeige und Timer) → Stopp drücken → Audio wird transkribiert → Text erscheint im Eingabefeld.
Einschränkungen:
EinschränkungWert
Maximale Aufnahmedauer120 Sekunden
AudioformatWebM (bevorzugt), WAV (Fallback)
Maximale Dateigröße25 MB
TranskriptionsmodellWhisper (whisper-1)
Das aufgenommene Audio wird als multipart/form-data an /api/ai/speech-to-text gesendet, das es an die OpenAI-Whisper-API weiterleitet. Der transkribierte Text wird mit Spracherkennung und Dauer-Metadaten zurückgegeben.
Credit-Kosten: 20 Credits pro Transkription (speech_to_text-Vorgang). Credits werden vor dem Whisper-API-Aufruf abgezogen.

Ladezustands-Muster

Die Chat-UI verwendet einen zweiphasigen Ladeindikator:
  1. Phase A — „Wird verarbeitet…"-Block: Ein separater Ladeblock erscheint unterhalb der Benutzernachricht, während auf den Beginn des Streamings gewartet wird. Dies ist KEINE Assistenten-Nachricht — sie wird entfernt, sobald das Streaming beginnt.
  2. Phase B — Streaming-Inhalt: Sobald das erste Chunk eintrifft, wird eine Assistenten-Nachricht mit isStreaming: true erstellt. Der Ladeindikator wechselt in die Nachrichtenblase, bis das Streaming abgeschlossen ist.

Streaming-Anfrageablauf

Jede Streaming-Chat-Nachricht folgt diesem Pfad vom Client über den Provider zurück:
Client (useAIChat hook)
    |--- POST /api/ai/stream
    |    Body: { messages: [...], stream: true }
    |
    v
API Route (stream/route.ts)
    |--- 1. guardLLMChat()         → 404 if disabled
    |--- 2. getAuthUserId()        → 401 if unauthenticated
    |--- 3. ensureUserExists()     → Clerk ID → DB user ID
    |--- 4. checkRateLimit()       → 429/402 if exceeded
    |--- 5. checkUsageQuota()      → 402 if monthly quota exceeded
    |--- 6. deductCredits()        → 402 if insufficient
    |--- 7. Zod validation         → 400 if invalid
    |
    v
AI Service
    |--- resolveModelAlias()
    |--- createAIProvider()
    |--- streamResponse()
    |
    v
Provider API (OpenAI/Anthropic/Google/xAI)
    |
    v
SSE Stream (text/event-stream)
    |--- data: {"choices":[{"delta":{"content":"Hello"}}]}
    |--- data: {"choices":[{"delta":{"content":" world"}}]}
    |--- data: [DONE]
    |
    v
Client parses chunks → updates message state → renders in UI

API-Routen

POST /api/ai/stream

Der primäre Endpunkt für Streaming-Chat. Gibt einen SSE-Stream mit Echtzeit-Token-Übertragung zurück.
Anfrage:
json
{
  "messages": [
    { "role": "system", "content": "You are a helpful assistant" },
    { "role": "user", "content": "Explain React hooks" }
  ],
  "model": "claude",
  "temperature": 0.7,
  "maxTokens": 1000,
  "systemPrompt": "Optional system prompt",
  "context": "Optional context string"
}
Antwort: SSE-Stream mit Content-Type: text/event-stream
src/app/api/ai/stream/route.ts — Request Schema
const StreamRequestSchema = z.object({
  messages: z.array(
    z.object({
      role: z.enum(['system', 'user', 'assistant', 'function', 'tool']),
      content: z.union([z.string(), z.array(ContentPartSchema)]),
      name: z.string().optional(),
    })
  ),
  model: z.string().optional(),
  temperature: z.number().min(0).max(2).optional(),
  maxTokens: z.number().positive().optional(),
  systemPrompt: z.string().optional(),
  context: z.string().optional(),
})

POST /api/ai/chat

Synchroner Endpunkt, der eine vollständige JSON-Antwort zurückgibt (kein Streaming). Unterstützt Einzel- und Batch-Anfragen.
Einzelanfrage:
json
{
  "messages": [{ "role": "user", "content": "What is Next.js?" }],
  "model": "gpt-5-mini",
  "temperature": 0.5
}
Batch-Anfrage:
json
{
  "requests": [
    { "messages": [{ "role": "user", "content": "Question 1" }] },
    { "messages": [{ "role": "user", "content": "Question 2" }] }
  ],
  "parallel": true
}
Batch-Anfragen sind auf 10 Elemente begrenzt. Setze parallel: true für parallele Verarbeitung oder false für sequenzielle.

React-Hooks

useAIChat

Der primäre Hook für Streaming-Chat mit vollständiger Nachrichtenverlaufs-Verwaltung. Verwendet einen eigenen SSE-Parser, der fünf Response-Formate über alle Provider verarbeitet:
src/hooks/use-ai.ts — useAIChat Hook
export function useAIChat(options: UseAIChatOptions = {}) {
  const {
    api = '/api/ai/stream',
    initialMessages = [],
    onFinish,
    onError,
  } = options
Verwendungsbeispiel:
tsx
'use client'

import { useAIChat } from '@/hooks/use-ai'

export function ChatPage() {
  const {
    messages,
    input,
    handleInputChange,
    handleSubmit,
    isLoading,
    error,
    stop,
    reload,
    clearMessages,
  } = useAIChat({
    api: '/api/ai/stream',
    onFinish: (message) => console.log('Done:', message.content),
    onError: (error) => console.error('Error:', error),
  })

  return (
    <form onSubmit={handleSubmit}>
      {messages.map((msg) => (
        <div key={msg.id}>{msg.content}</div>
      ))}
      <input value={input} onChange={handleInputChange} />
      <button type="submit" disabled={isLoading}>Senden</button>
      {isLoading && <button onClick={stop}>Stopp</button>}
    </form>
  )
}
Rückgabewerte:
EigenschaftTypBeschreibung
messagesAIChatMessage[]Vollständiger Gesprächsverlauf
inputstringAktueller Eingabefeldwert
handleInputChange(e) => voidEingabe-Change-Handler
handleSubmit(e?) => voidFormular-Submit-Handler
append(msg) => PromiseNachricht manuell anhängen
isLoadingbooleanTrue während des Streamings
errorError | nullLetzter Fehler, falls vorhanden
stop() => voidAktuellen Stream abbrechen
reload() => voidLetzte Nachricht wiederholen
setMessages(msgs) => voidNachrichtenverlauf ersetzen
clearMessages() => voidAlle Nachrichten löschen
bonusHintstring | nullHinweis, wenn Bonus-Credits verwendet wurden
clearBonusHint() => voidBonus-Hinweis löschen

Weitere Hooks

Kit bietet vier weitere Hooks für verschiedene Anwendungsfälle:
HookZweckAPI-Endpunkt
useAICompletion()Nicht-streamende Completions via Vercel AI SDK/api/ai/chat
useAIQuery()Gecachte KI-Abfragen mit TanStack Query/api/ai/chat
useAIMutation()Einmalige KI-Anfragen via useMutation/api/ai/chat
useAIStream()Low-Level-Streaming mit manueller Steuerung/api/ai/stream

Chat-Komponenten

Die Chat-UI besteht aus kombinierbaren Komponenten in apps/boilerplate/src/components/ai/:
KomponenteDateiZweck
ChatContainerchat-container.tsxHaupt-Chat-Layout mit Header, Nachrichten und Eingabe
ChatMessagechat-message.tsxEinzelne Nachrichtenblase (Benutzer/Assistent)
ChatInputchat-input.tsxTexteingabe mit Senden-Schaltfläche und Tastaturkürzeln
ChatHeaderchat-header.tsxChat-Titel, Modellinfo, Löschen-Schaltfläche
QuickPromptsquick-prompts.tsxKategoriebasierte Vorschlagsschaltflächen
SourceAttributionsource-attribution.tsxRAG-Quellenangaben mit Ähnlichkeitswerten
ChatSkeletonchat-skeleton.tsxLade-Skeleton für Chat-Nachrichten
StreamingIndicatorstreaming-indicator.tsxAnimierter Tipp-Indikator während des Streamings
ImagePreviewimage-preview.tsxMiniaturansicht-Raster über der Eingabe mit Entfernen-Schaltflächen (Vision-Chat)
ImageLightboximage-lightbox.tsxVollbild-Bildbetrachter mit Tastaturnavigation (Vision-Chat)
PdfAttachmentpdf-attachment.tsxPDF-Dateivorschau-Chip mit Name, Größe und Entfernen-Schaltfläche (PDF-Chat)
AudioRecorderaudio-recorder.tsxSprachaufnahme mit Audiopegelanzeige und STT-Transkription (Audio-Eingabe)

Quick Prompts

Beide Chat-Modi haben konfigurierbare Vorschlagsschaltflächen, nach Kategorien geordnet. Jede Kategorie hat ein Symbol und eine Reihe von Prompts:
src/lib/ai/quick-prompts.ts — Types and Configuration
/**
 * Single suggestion within a category
 */
export interface QuickPromptSuggestion {
  /** Unique identifier */
  id: string
  /** Short display label (max ~50 chars for UI) */
  label: string
  /** Full prompt text to be sent */
  prompt: string
}

/**
 * Category grouping related suggestions
 */
export interface QuickPromptCategory {
  /** Unique identifier */
  id: string
  /** Button label */
  label: string
  /** Lucide icon component */
  icon: LucideIcon
  /** List of suggestions in this category */
  suggestions: QuickPromptSuggestion[]
}

/**
 * Complete configuration for a chat type
 */
export interface QuickPromptConfig {
  chatType: 'llm' | 'rag'
  categories: QuickPromptCategory[]
LLM-Chat-Kategorien: Code, Schreiben, Debugging, Lernen, Ideen (25 Prompts gesamt)
RAG-Chat-Kategorien: Einrichtung, Auth, Zahlungen, Funktionen, Anpassung (25 Prompts gesamt)

Streaming-Protokoll

Kit verwendet Server-Sent Events (SSE) für das Streaming. Der gemeinsame SSE-Parser in src/lib/ai/sse-parser.ts verarbeitet fünf Response-Formate, um alle Provider zu unterstützen. Der Parser bietet zwei Schlüsselklassen:
  • SSEStreamError — Unterscheidet serverseitige Fehler (z.B. { "error": "Insufficient credits" }) von JSON-Parse-Fehlern. Server-Fehler werden an den Benutzer weitergegeben; fehlerhafte JSON-Chunks werden sicher ignoriert.
  • SSELineBuffer — Sammelt unvollständige Zeilen über TCP-Paketgrenzen hinweg und stellt sicher, dass vollständige SSE-Zeilen verarbeitet werden, auch wenn Daten fragmentiert ankommen.
Wenn der Stream mit null Inhalts-Chunks (leere Antwort) abschließt, entfernen die Client-Hooks die Platzhalter-„Wird verarbeitet"-Nachricht und zeigen einen benutzerfreundlichen Fehler an. Diagnosedaten (finishReason, usage, warnings) werden für Debugging-Zwecke protokolliert.
Die fünf unterstützten Response-Formate:
Format 1: OpenAI-style delta
  data: {"choices":[{"delta":{"content":"token"}}]}

Format 2: OpenAI-style text
  data: {"choices":[{"text":"token"}]}

Format 3: Direct content
  data: {"content":"token"}

Format 4: Direct text
  data: {"text":"token"}

Format 5: Anthropic-style delta
  data: {"delta":{"text":"token"}}

Termination:
  data: [DONE]
Die Stream-Antwort enthält Standard-Header:
Content-Type: text/event-stream
Cache-Control: no-cache, no-transform
Connection: keep-alive
X-Accel-Buffering: no
Der X-Accel-Buffering: no-Header deaktiviert das nginx-Proxy-Buffering, was für Echtzeit-Streaming auf Reverse-Proxy-Setups (einschließlich Vercel) entscheidend ist.

Feature-Guards

Route-Guards stellen sicher, dass deaktivierte Features korrekte 404-Antworten zurückgeben, anstatt abzustürzen. Es gibt zwei Typen — API-Guards (geben NextResponse zurück) und Seiten-Guards (geben Booleans für notFound() zurück):
src/lib/ai/route-guards.ts — API Route Guards
export function guardRAGChat(): NextResponse<FeatureDisabledError> | null {
  if (!isRAGChatEnabled()) {
    return createFeatureDisabledResponse('RAG Chat')
  }
  return null
}

/**
 * Guard for LLM Chat API routes
 *
 * Use at the start of:
 * - /api/ai/chat
 * - /api/ai/stream
 *
 * @returns NextResponse if feature disabled, null if enabled
 *
 * @example
 * export async function POST(request: Request) {
 *   const guard = guardLLMChat()
 *   if (guard) return guard
 *   // Feature is enabled, continue with handler
 * }
 */
export function guardLLMChat(): NextResponse<FeatureDisabledError> | null {
  if (!isLLMChatEnabled()) {
    return createFeatureDisabledResponse('LLM Chat')
  }
  return null
}
API-Guards (für API-Routen):
FunktionSchütztGibt zurück
guardRAGChat()/api/ai/rag/*-RoutenNextResponse (404) oder null
guardLLMChat()/api/ai/stream, /api/ai/chatNextResponse (404) oder null
guardAnyChat()/api/ai/usageNextResponse (404) oder null
guardAudioInput()/api/ai/speech-to-textNextResponse (404) oder null
guardImageGen()/api/ai/image-genNextResponse (404) oder null
Seiten-Guards (für Next.js-Seiten):
FunktionSchütztVerwendung
shouldShowRAGChat()RAG-Chat-Seiteif (!shouldShowRAGChat()) notFound()
shouldShowLLMChat()LLM-Chat-Seiteif (!shouldShowLLMChat()) notFound()
shouldShowImageGen()Bildgenerierungs-Seiteif (!shouldShowImageGen()) notFound()

Fehlerbehandlung

Das Chat-System behandelt Fehler auf mehreren Ebenen:
EbeneFehlerAntwort
Feature deaktiviertGuard gibt 404 zurück{ error: "Feature not available", code: "FEATURE_DISABLED" }
Nicht authentifiziertClerk-Prüfung schlägt fehl{ error: "Unauthorized" } (401)
Rate-Limit erreichtGlobaler Burst überschritten{ error: "Too many requests" } (429)
Unzureichende CreditsCredit-Guthaben zu niedrig{ error: "Insufficient credits" } (402)
Ungültige AnfrageZod-Validierung schlägt fehl{ error: "Validation error", details: [...] } (400)
Provider-FehlerAPI-Aufruf schlägt fehl{ error: "...", provider: "openai", retryable: true } (5xx)
Stream-FehlerFehler mitten im StreamFehler als SSE-Event gesendet, Stream schließt sich