Add blog posts, cleanup unused files, update components
- Add 100 blog posts covering AI, development, and tech topics - Add .env.example for environment configuration - Add accessibility and lighthouse audit scripts - Remove obsolete SEO reports and temporary files - Remove dev-dist build artifacts and backup files - Remove unused portrait images (moved/consolidated elsewhere) - Update contact form and component improvements Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,614 @@
|
||||
# Socket.IO für AI Chat Applications: Real-Time Implementation Guide
|
||||
|
||||
**Meta-Description:** Production-ready AI Chat mit Socket.IO. Rooms, Typing Indicators, Message History und LLM-Streaming für skalierbare Chat-Anwendungen.
|
||||
|
||||
**Keywords:** Socket.IO, AI Chat, Real-Time Chat, Node.js Chat, WebSocket Chat, LLM Streaming, Chat Application
|
||||
|
||||
---
|
||||
|
||||
## Einführung
|
||||
|
||||
Socket.IO ist der De-facto-Standard für Real-Time-Kommunikation in Node.js. Für AI-Chat-Anwendungen bietet es **bidirektionale Events, automatische Reconnection und Room-basierte Isolation**.
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ AI CHAT WITH SOCKET.IO │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Client Server │
|
||||
│ ┌─────────────┐ ┌─────────────────────┐ │
|
||||
│ │ React │ socket.io │ Express + │ │
|
||||
│ │ App │◄──────────────►│ Socket.IO │ │
|
||||
│ └─────────────┘ └──────────┬──────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────┐ │
|
||||
│ │ LLM Service │ │
|
||||
│ │ (Claude/GPT) │ │
|
||||
│ └─────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────┐ │
|
||||
│ │ Redis │ │
|
||||
│ │ (Sessions/Cache) │ │
|
||||
│ └─────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Server Implementation
|
||||
|
||||
### Basic Setup
|
||||
|
||||
```typescript
|
||||
// src/server.ts
|
||||
import express from 'express';
|
||||
import { createServer } from 'http';
|
||||
import { Server, Socket } from 'socket.io';
|
||||
import { Redis } from 'ioredis';
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
|
||||
const app = express();
|
||||
const server = createServer(app);
|
||||
const io = new Server(server, {
|
||||
cors: {
|
||||
origin: process.env.CLIENT_URL,
|
||||
credentials: true
|
||||
},
|
||||
pingTimeout: 60000,
|
||||
pingInterval: 25000
|
||||
});
|
||||
|
||||
const redis = new Redis(process.env.REDIS_URL!);
|
||||
const anthropic = new Anthropic();
|
||||
|
||||
// Middleware
|
||||
io.use(async (socket, next) => {
|
||||
const token = socket.handshake.auth.token;
|
||||
try {
|
||||
const user = await verifyToken(token);
|
||||
socket.data.user = user;
|
||||
next();
|
||||
} catch (error) {
|
||||
next(new Error('Authentication failed'));
|
||||
}
|
||||
});
|
||||
|
||||
// Connection Handler
|
||||
io.on('connection', (socket: Socket) => {
|
||||
const userId = socket.data.user.id;
|
||||
console.log(`User connected: ${userId}`);
|
||||
|
||||
// Persönlicher Room für User
|
||||
socket.join(`user:${userId}`);
|
||||
|
||||
setupChatHandlers(socket);
|
||||
setupTypingHandlers(socket);
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
console.log(`User disconnected: ${userId}`);
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(3000);
|
||||
```
|
||||
|
||||
### Chat Event Handlers
|
||||
|
||||
```typescript
|
||||
// src/handlers/chat.ts
|
||||
interface ChatMessage {
|
||||
id: string;
|
||||
conversationId: string;
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
function setupChatHandlers(socket: Socket) {
|
||||
const userId = socket.data.user.id;
|
||||
|
||||
// Conversation beitreten
|
||||
socket.on('join:conversation', async (conversationId: string) => {
|
||||
// Berechtigung prüfen
|
||||
const hasAccess = await checkConversationAccess(userId, conversationId);
|
||||
if (!hasAccess) {
|
||||
socket.emit('error', { message: 'Access denied' });
|
||||
return;
|
||||
}
|
||||
|
||||
socket.join(`conversation:${conversationId}`);
|
||||
|
||||
// History laden
|
||||
const history = await loadConversationHistory(conversationId);
|
||||
socket.emit('conversation:history', history);
|
||||
});
|
||||
|
||||
// Neue Nachricht senden
|
||||
socket.on('message:send', async (data: {
|
||||
conversationId: string;
|
||||
content: string;
|
||||
}) => {
|
||||
const { conversationId, content } = data;
|
||||
|
||||
// User-Nachricht speichern
|
||||
const userMessage: ChatMessage = {
|
||||
id: crypto.randomUUID(),
|
||||
conversationId,
|
||||
role: 'user',
|
||||
content,
|
||||
timestamp: new Date()
|
||||
};
|
||||
|
||||
await saveMessage(userMessage);
|
||||
|
||||
// An alle in der Conversation senden
|
||||
io.to(`conversation:${conversationId}`).emit('message:new', userMessage);
|
||||
|
||||
// AI Response generieren
|
||||
await generateAIResponse(socket, conversationId, content);
|
||||
});
|
||||
|
||||
// Conversation verlassen
|
||||
socket.on('leave:conversation', (conversationId: string) => {
|
||||
socket.leave(`conversation:${conversationId}`);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### AI Response mit Streaming
|
||||
|
||||
```typescript
|
||||
// src/handlers/ai-response.ts
|
||||
async function generateAIResponse(
|
||||
socket: Socket,
|
||||
conversationId: string,
|
||||
userMessage: string
|
||||
) {
|
||||
const responseId = crypto.randomUUID();
|
||||
|
||||
// Typing indicator starten
|
||||
io.to(`conversation:${conversationId}`).emit('ai:typing', {
|
||||
conversationId,
|
||||
isTyping: true
|
||||
});
|
||||
|
||||
try {
|
||||
// Conversation History laden
|
||||
const history = await loadConversationHistory(conversationId);
|
||||
|
||||
// Stream starten
|
||||
const stream = await anthropic.messages.stream({
|
||||
model: 'claude-3-haiku-20240307',
|
||||
max_tokens: 1000,
|
||||
system: 'Du bist ein hilfreicher Assistent.',
|
||||
messages: history.map(m => ({
|
||||
role: m.role,
|
||||
content: m.content
|
||||
}))
|
||||
});
|
||||
|
||||
let fullContent = '';
|
||||
|
||||
// Token-by-Token streamen
|
||||
for await (const event of stream) {
|
||||
if (event.type === 'content_block_delta') {
|
||||
const delta = event.delta.text;
|
||||
fullContent += delta;
|
||||
|
||||
// Delta an Client senden
|
||||
io.to(`conversation:${conversationId}`).emit('message:delta', {
|
||||
messageId: responseId,
|
||||
conversationId,
|
||||
delta,
|
||||
fullContent
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Vollständige Nachricht speichern
|
||||
const assistantMessage: ChatMessage = {
|
||||
id: responseId,
|
||||
conversationId,
|
||||
role: 'assistant',
|
||||
content: fullContent,
|
||||
timestamp: new Date()
|
||||
};
|
||||
|
||||
await saveMessage(assistantMessage);
|
||||
|
||||
// Completion Event
|
||||
io.to(`conversation:${conversationId}`).emit('message:complete', {
|
||||
messageId: responseId,
|
||||
conversationId
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
io.to(`conversation:${conversationId}`).emit('ai:error', {
|
||||
conversationId,
|
||||
error: 'AI response failed'
|
||||
});
|
||||
} finally {
|
||||
// Typing indicator stoppen
|
||||
io.to(`conversation:${conversationId}`).emit('ai:typing', {
|
||||
conversationId,
|
||||
isTyping: false
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Typing Indicators
|
||||
|
||||
```typescript
|
||||
// src/handlers/typing.ts
|
||||
function setupTypingHandlers(socket: Socket) {
|
||||
const userId = socket.data.user.id;
|
||||
const typingTimeouts = new Map<string, NodeJS.Timeout>();
|
||||
|
||||
socket.on('typing:start', (conversationId: string) => {
|
||||
// An andere User im Room senden
|
||||
socket.to(`conversation:${conversationId}`).emit('user:typing', {
|
||||
userId,
|
||||
conversationId,
|
||||
isTyping: true
|
||||
});
|
||||
|
||||
// Auto-Stop nach 3 Sekunden
|
||||
const existing = typingTimeouts.get(conversationId);
|
||||
if (existing) clearTimeout(existing);
|
||||
|
||||
typingTimeouts.set(conversationId, setTimeout(() => {
|
||||
socket.to(`conversation:${conversationId}`).emit('user:typing', {
|
||||
userId,
|
||||
conversationId,
|
||||
isTyping: false
|
||||
});
|
||||
}, 3000));
|
||||
});
|
||||
|
||||
socket.on('typing:stop', (conversationId: string) => {
|
||||
const existing = typingTimeouts.get(conversationId);
|
||||
if (existing) clearTimeout(existing);
|
||||
|
||||
socket.to(`conversation:${conversationId}`).emit('user:typing', {
|
||||
userId,
|
||||
conversationId,
|
||||
isTyping: false
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
// Cleanup
|
||||
typingTimeouts.forEach(timeout => clearTimeout(timeout));
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Client Implementation
|
||||
|
||||
### React Hook
|
||||
|
||||
```typescript
|
||||
// src/hooks/useChat.ts
|
||||
import { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import { io, Socket } from 'socket.io-client';
|
||||
|
||||
interface Message {
|
||||
id: string;
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
timestamp: Date;
|
||||
isStreaming?: boolean;
|
||||
}
|
||||
|
||||
export function useChat(conversationId: string) {
|
||||
const socketRef = useRef<Socket | null>(null);
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [isAITyping, setIsAITyping] = useState(false);
|
||||
const [streamingMessage, setStreamingMessage] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
// Socket Connection
|
||||
const socket = io(process.env.NEXT_PUBLIC_WS_URL!, {
|
||||
auth: { token: getAuthToken() },
|
||||
transports: ['websocket']
|
||||
});
|
||||
|
||||
socketRef.current = socket;
|
||||
|
||||
socket.on('connect', () => {
|
||||
setIsConnected(true);
|
||||
socket.emit('join:conversation', conversationId);
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
setIsConnected(false);
|
||||
});
|
||||
|
||||
// Event Handlers
|
||||
socket.on('conversation:history', (history: Message[]) => {
|
||||
setMessages(history);
|
||||
});
|
||||
|
||||
socket.on('message:new', (message: Message) => {
|
||||
setMessages(prev => [...prev, message]);
|
||||
});
|
||||
|
||||
socket.on('message:delta', ({ messageId, delta, fullContent }) => {
|
||||
setStreamingMessage(fullContent);
|
||||
});
|
||||
|
||||
socket.on('message:complete', ({ messageId }) => {
|
||||
setMessages(prev => [
|
||||
...prev,
|
||||
{
|
||||
id: messageId,
|
||||
role: 'assistant',
|
||||
content: streamingMessage,
|
||||
timestamp: new Date()
|
||||
}
|
||||
]);
|
||||
setStreamingMessage('');
|
||||
});
|
||||
|
||||
socket.on('ai:typing', ({ isTyping }) => {
|
||||
setIsAITyping(isTyping);
|
||||
});
|
||||
|
||||
socket.on('ai:error', ({ error }) => {
|
||||
console.error('AI Error:', error);
|
||||
setIsAITyping(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
socket.emit('leave:conversation', conversationId);
|
||||
socket.disconnect();
|
||||
};
|
||||
}, [conversationId]);
|
||||
|
||||
const sendMessage = useCallback((content: string) => {
|
||||
if (socketRef.current) {
|
||||
socketRef.current.emit('message:send', {
|
||||
conversationId,
|
||||
content
|
||||
});
|
||||
}
|
||||
}, [conversationId]);
|
||||
|
||||
const startTyping = useCallback(() => {
|
||||
socketRef.current?.emit('typing:start', conversationId);
|
||||
}, [conversationId]);
|
||||
|
||||
const stopTyping = useCallback(() => {
|
||||
socketRef.current?.emit('typing:stop', conversationId);
|
||||
}, [conversationId]);
|
||||
|
||||
return {
|
||||
messages,
|
||||
streamingMessage,
|
||||
isConnected,
|
||||
isAITyping,
|
||||
sendMessage,
|
||||
startTyping,
|
||||
stopTyping
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Chat Component
|
||||
|
||||
```tsx
|
||||
// src/components/Chat.tsx
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { useChat } from '../hooks/useChat';
|
||||
|
||||
export function Chat({ conversationId }: { conversationId: string }) {
|
||||
const {
|
||||
messages,
|
||||
streamingMessage,
|
||||
isConnected,
|
||||
isAITyping,
|
||||
sendMessage,
|
||||
startTyping,
|
||||
stopTyping
|
||||
} = useChat(conversationId);
|
||||
|
||||
const [input, setInput] = useState('');
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Auto-scroll
|
||||
useEffect(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
}, [messages, streamingMessage]);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (input.trim()) {
|
||||
sendMessage(input);
|
||||
setInput('');
|
||||
stopTyping();
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setInput(e.target.value);
|
||||
if (e.target.value) {
|
||||
startTyping();
|
||||
} else {
|
||||
stopTyping();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Connection Status */}
|
||||
<div className={`px-4 py-2 text-sm ${
|
||||
isConnected ? 'bg-green-100' : 'bg-red-100'
|
||||
}`}>
|
||||
{isConnected ? 'Connected' : 'Reconnecting...'}
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||
{messages.map(message => (
|
||||
<div
|
||||
key={message.id}
|
||||
className={`p-3 rounded-lg ${
|
||||
message.role === 'user'
|
||||
? 'bg-blue-100 ml-auto max-w-[80%]'
|
||||
: 'bg-gray-100 mr-auto max-w-[80%]'
|
||||
}`}
|
||||
>
|
||||
{message.content}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Streaming Message */}
|
||||
{streamingMessage && (
|
||||
<div className="bg-gray-100 mr-auto max-w-[80%] p-3 rounded-lg">
|
||||
{streamingMessage}
|
||||
<span className="animate-pulse">▋</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* AI Typing Indicator */}
|
||||
{isAITyping && !streamingMessage && (
|
||||
<div className="bg-gray-100 mr-auto p-3 rounded-lg">
|
||||
<span className="animate-pulse">● ● ●</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{/* Input */}
|
||||
<form onSubmit={handleSubmit} className="p-4 border-t">
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={input}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Nachricht eingeben..."
|
||||
className="flex-1 px-4 py-2 border rounded-lg"
|
||||
disabled={!isConnected}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!isConnected || !input.trim()}
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded-lg disabled:opacity-50"
|
||||
>
|
||||
Senden
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Scaling mit Redis Adapter
|
||||
|
||||
```typescript
|
||||
// src/server.ts
|
||||
import { createAdapter } from '@socket.io/redis-adapter';
|
||||
import { createClient } from 'redis';
|
||||
|
||||
const pubClient = createClient({ url: process.env.REDIS_URL });
|
||||
const subClient = pubClient.duplicate();
|
||||
|
||||
await Promise.all([pubClient.connect(), subClient.connect()]);
|
||||
|
||||
io.adapter(createAdapter(pubClient, subClient));
|
||||
|
||||
// Jetzt können mehrere Server-Instanzen kommunizieren
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Rate Limiting
|
||||
|
||||
```typescript
|
||||
import rateLimit from 'socket.io-rate-limiter';
|
||||
|
||||
io.use(rateLimit({
|
||||
windowMs: 1000, // 1 Sekunde
|
||||
max: 10 // Max 10 Events pro Sekunde
|
||||
}));
|
||||
```
|
||||
|
||||
### 2. Input Validation
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod';
|
||||
|
||||
const messageSchema = z.object({
|
||||
conversationId: z.string().uuid(),
|
||||
content: z.string().min(1).max(4000)
|
||||
});
|
||||
|
||||
socket.on('message:send', async (data) => {
|
||||
const result = messageSchema.safeParse(data);
|
||||
if (!result.success) {
|
||||
socket.emit('error', { message: 'Invalid message format' });
|
||||
return;
|
||||
}
|
||||
// Process...
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Error Handling
|
||||
|
||||
```typescript
|
||||
socket.on('error', (error) => {
|
||||
console.error('Socket error:', error);
|
||||
socket.emit('error', { message: 'An error occurred' });
|
||||
});
|
||||
|
||||
io.engine.on('connection_error', (error) => {
|
||||
console.error('Connection error:', error);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fazit
|
||||
|
||||
Socket.IO für AI Chat bietet:
|
||||
|
||||
1. **Bidirektionale Kommunikation**: Perfekt für Streaming-Responses
|
||||
2. **Rooms**: Isolation für Conversations
|
||||
3. **Auto-Reconnection**: Robuste Verbindung
|
||||
4. **Skalierbarkeit**: Redis Adapter für Multi-Server
|
||||
|
||||
Für produktionsreife AI-Chat-Anwendungen ist Socket.IO die pragmatische Wahl.
|
||||
|
||||
---
|
||||
|
||||
## Bildprompts
|
||||
|
||||
1. "Chat interface with AI assistant, real-time message bubbles appearing, modern app design"
|
||||
2. "WebSocket connection diagram between client and server, data flowing both ways"
|
||||
3. "Multiple users in chat room with AI, collaborative interface, friendly tech illustration"
|
||||
|
||||
---
|
||||
|
||||
## Quellen
|
||||
|
||||
- [Socket.IO Documentation](https://socket.io/docs/v4/)
|
||||
- [Socket.IO Best Practices](https://ably.com/topic/socketio)
|
||||
- [Building Real-Time Chat with Socket.IO](https://socket.io/get-started/chat)
|
||||
- [Socket.IO Redis Adapter](https://socket.io/docs/v4/redis-adapter/)
|
||||
Reference in New Issue
Block a user