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
| Aspekt | LLM-Chat | RAG-Chat |
|---|---|---|
| Dashboard-Route | /dashboard/chat-llm | /dashboard/chat-rag |
| API-Route | /api/ai/stream (SSE), /api/ai/chat (JSON) | /api/ai/rag/ask |
| Hook | useAIChat() | Eigener RAG-Hook |
| Kontextquelle | Nur Gesprächsverlauf | pgvector-Suche + Gespräch |
| Feature-Flag | NEXT_PUBLIC_AI_LLM_CHAT_ENABLED | NEXT_PUBLIC_AI_RAG_CHAT_ENABLED |
| Credit-Kosten | 20 Credits (Streaming), 30 (Bildanalyse), 40 (PDF-Analyse), 20 (Spracheingabe), 15 (synchron) | 15 Credits |
| Auth erforderlich | Ja (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änkung | Wert |
|---|---|
| Maximale Bildgröße | 4,5 MB pro Bild |
| Maximale Bilder pro Nachricht | 4 |
| Unterstützte Formate | PNG, 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.Vision-Chat erfordert einen aktivierten LLM-Chat. Sowohl
NEXT_PUBLIC_AI_LLM_CHAT_ENABLED als auch NEXT_PUBLIC_AI_VISION_ENABLED müssen true sein, damit Bild-Upload-Funktionen erscheinen. Setze NEXT_PUBLIC_AI_VISION_ENABLED=false, um den Bild-Upload auszublenden, während LLM-Chat aktiv bleibt.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änkung | Wert |
|---|---|
| Maximale Dateigröße | 10 MB pro PDF |
| Maximale PDFs pro Nachricht | 1 |
| Unterstützte Formate | Nur PDF (.pdf) |
| Maximaler extrahierter Text | 50.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).PDF-Chat erfordert einen aktivierten LLM-Chat. Sowohl
NEXT_PUBLIC_AI_LLM_CHAT_ENABLED als auch NEXT_PUBLIC_AI_PDF_CHAT_ENABLED müssen true sein, damit PDF-Upload-Funktionen erscheinen. Setze NEXT_PUBLIC_AI_PDF_CHAT_ENABLED=false, um den PDF-Upload auszublenden, während LLM-Chat aktiv bleibt.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änkung | Wert |
|---|---|
| Maximale Aufnahmedauer | 120 Sekunden |
| Audioformat | WebM (bevorzugt), WAV (Fallback) |
| Maximale Dateigröße | 25 MB |
| Transkriptionsmodell | Whisper (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.Audio-Eingabe erfordert einen aktivierten LLM-Chat. Sowohl
NEXT_PUBLIC_AI_LLM_CHAT_ENABLED als auch NEXT_PUBLIC_AI_AUDIO_INPUT_ENABLED müssen true sein, damit die Mikrofon-Schaltfläche erscheint. Setze NEXT_PUBLIC_AI_AUDIO_INPUT_ENABLED=false, um die Spracheingabe auszublenden, während LLM-Chat aktiv bleibt.Ladezustands-Muster
Die Chat-UI verwendet einen zweiphasigen Ladeindikator:
- 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.
- Phase B — Streaming-Inhalt: Sobald das erste Chunk eintrifft, wird eine Assistenten-Nachricht mit
isStreaming: trueerstellt. 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-streamsrc/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(),
})
Sowohl
/api/ai/stream als auch /api/ai/chat erfordern Clerk-Authentifizierung. Die Route konvertiert die Clerk-ID über ensureUserExists() in eine Datenbank-Benutzer-ID — dies ist erforderlich, damit das Credit-System den richtigen Benutzer identifizieren kann.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
useChat-Hook des Vercel AI SDKs verwendet ein proprietäres Data-Stream-Protokoll, das mit dem Standard-SSE-Streaming von Kit inkompatibel ist. Wenn du useChat aus ai/react importierst, schlägt das Streaming lautlos fehl — kein Fehler, keine Tokens, nur eine leere Antwort.typescript
// FALSCH — verwendet Vercels proprietäres Protokoll, inkompatibel mit Kits SSE
import { useChat } from 'ai/react'
const { messages } = useChat({ api: '/api/ai/stream' }) // Stiller Fehler!
// RICHTIG — Kits eigener Hook mit Multi-Provider-SSE-Parser
import { useAIChat } from '@/hooks/use-ai'
const { messages } = useAIChat({ api: '/api/ai/stream' }) // Funktioniert!
Dies ist der häufigste Fehler bei Entwicklern, die von anderen Vercel-AI-SDK-Projekten kommen. Kit verwendet Standard-OpenAI-kompatibles SSE, da mehrere Provider (OpenAI, Anthropic, Google, xAI) unterstützt werden müssen — Vercels Data-Stream-Protokoll funktioniert nur mit deren spezifischem Backend-Format.
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:
| Eigenschaft | Typ | Beschreibung |
|---|---|---|
messages | AIChatMessage[] | Vollständiger Gesprächsverlauf |
input | string | Aktueller Eingabefeldwert |
handleInputChange | (e) => void | Eingabe-Change-Handler |
handleSubmit | (e?) => void | Formular-Submit-Handler |
append | (msg) => Promise | Nachricht manuell anhängen |
isLoading | boolean | True während des Streamings |
error | Error | null | Letzter Fehler, falls vorhanden |
stop | () => void | Aktuellen Stream abbrechen |
reload | () => void | Letzte Nachricht wiederholen |
setMessages | (msgs) => void | Nachrichtenverlauf ersetzen |
clearMessages | () => void | Alle Nachrichten löschen |
bonusHint | string | null | Hinweis, wenn Bonus-Credits verwendet wurden |
clearBonusHint | () => void | Bonus-Hinweis löschen |
Weitere Hooks
Kit bietet vier weitere Hooks für verschiedene Anwendungsfälle:
| Hook | Zweck | API-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/:| Komponente | Datei | Zweck |
|---|---|---|
ChatContainer | chat-container.tsx | Haupt-Chat-Layout mit Header, Nachrichten und Eingabe |
ChatMessage | chat-message.tsx | Einzelne Nachrichtenblase (Benutzer/Assistent) |
ChatInput | chat-input.tsx | Texteingabe mit Senden-Schaltfläche und Tastaturkürzeln |
ChatHeader | chat-header.tsx | Chat-Titel, Modellinfo, Löschen-Schaltfläche |
QuickPrompts | quick-prompts.tsx | Kategoriebasierte Vorschlagsschaltflächen |
SourceAttribution | source-attribution.tsx | RAG-Quellenangaben mit Ähnlichkeitswerten |
ChatSkeleton | chat-skeleton.tsx | Lade-Skeleton für Chat-Nachrichten |
StreamingIndicator | streaming-indicator.tsx | Animierter Tipp-Indikator während des Streamings |
ImagePreview | image-preview.tsx | Miniaturansicht-Raster über der Eingabe mit Entfernen-Schaltflächen (Vision-Chat) |
ImageLightbox | image-lightbox.tsx | Vollbild-Bildbetrachter mit Tastaturnavigation (Vision-Chat) |
PdfAttachment | pdf-attachment.tsx | PDF-Dateivorschau-Chip mit Name, Größe und Entfernen-Schaltfläche (PDF-Chat) |
AudioRecorder | audio-recorder.tsx | Sprachaufnahme 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)
Bearbeite
apps/boilerplate/src/lib/ai/quick-prompts.ts, um die Vorschlagsschaltflächen für deine Anwendung anzupassen. Jede Kategorie benötigt eine id, ein label, ein icon (Lucide-Icon-Komponente) und ein Array von suggestions mit den Feldern id, label und prompt.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.Credits werden vor Beginn der Streaming-Antwort abgezogen — nicht danach. Das ist beabsichtigt: Es verhindert, dass Benutzer eine vollständige KI-Antwort erhalten und der Credit-Abzug anschließend fehlschlägt. Wenn der Stream mitten in der Antwort abbricht, wird der Credit nicht automatisch erstattet.
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):
| Funktion | Schützt | Gibt zurück |
|---|---|---|
guardRAGChat() | /api/ai/rag/*-Routen | NextResponse (404) oder null |
guardLLMChat() | /api/ai/stream, /api/ai/chat | NextResponse (404) oder null |
guardAnyChat() | /api/ai/usage | NextResponse (404) oder null |
guardAudioInput() | /api/ai/speech-to-text | NextResponse (404) oder null |
guardImageGen() | /api/ai/image-gen | NextResponse (404) oder null |
Seiten-Guards (für Next.js-Seiten):
| Funktion | Schützt | Verwendung |
|---|---|---|
shouldShowRAGChat() | RAG-Chat-Seite | if (!shouldShowRAGChat()) notFound() |
shouldShowLLMChat() | LLM-Chat-Seite | if (!shouldShowLLMChat()) notFound() |
shouldShowImageGen() | Bildgenerierungs-Seite | if (!shouldShowImageGen()) notFound() |
Fehlerbehandlung
Das Chat-System behandelt Fehler auf mehreren Ebenen:
| Ebene | Fehler | Antwort |
|---|---|---|
| Feature deaktiviert | Guard gibt 404 zurück | { error: "Feature not available", code: "FEATURE_DISABLED" } |
| Nicht authentifiziert | Clerk-Prüfung schlägt fehl | { error: "Unauthorized" } (401) |
| Rate-Limit erreicht | Globaler Burst überschritten | { error: "Too many requests" } (429) |
| Unzureichende Credits | Credit-Guthaben zu niedrig | { error: "Insufficient credits" } (402) |
| Ungültige Anfrage | Zod-Validierung schlägt fehl | { error: "Validation error", details: [...] } (400) |
| Provider-Fehler | API-Aufruf schlägt fehl | { error: "...", provider: "openai", retryable: true } (5xx) |
| Stream-Fehler | Fehler mitten im Stream | Fehler als SSE-Event gesendet, Stream schließt sich |