--- name: composable-svelte-chat description: Streaming chat and collaborative features for Composable Svelte. Use when implementing LLM chat interfaces, real-time messaging, or collaborative features. Covers StreamingChat (transport-agnostic), presence tracking, typing indicators, and WebSocket integration from @composable-svelte/chat package. --- # Composable Svelte Chat Package Streaming chat with collaborative features for LLM interactions and real-time messaging. --- ## PACKAGE OVERVIEW **Package**: `@composable-svelte/chat` **Purpose**: Transport-agnostic streaming chat designed for LLM interactions with collaborative features. **Technology Stack**: - **Markdown**: Rendering with syntax highlighting (Prism.js) - **WebSocket**: Real-time communication and presence - **MediaRecorder**: Optional voice input integration - **PDF.js**: PDF attachment preview **Core Components**: - `StreamingChat` - Transport-agnostic streaming chat - `Collaborative Features` - Presence, typing, cursors - `WebSocket Manager` - Connection management **State Management**: All components follow Composable Architecture patterns with dedicated reducers and type-safe actions. --- ## STREAMING CHAT **Purpose**: Transport-agnostic streaming chat for LLM interactions (OpenAI, Anthropic, Ollama, etc.). ### Quick Start ```typescript import { createStore } from '@composable-svelte/core'; import { StandardStreamingChat, streamingChatReducer, createInitialStreamingChatState } from '@composable-svelte/chat'; // Create chat store const chatStore = createStore({ initialState: createInitialStreamingChatState(), reducer: streamingChatReducer, dependencies: { sendMessage: async function*(message, signal) { // Send to your LLM API (OpenAI, Anthropic, Ollama, etc.) const response = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message }), signal }); const reader = response.body!.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value); const lines = chunk.split('\n').filter(Boolean); for (const line of lines) { if (line.startsWith('data: ')) { const data = JSON.parse(line.slice(6)); if (data.content) { yield data.content; } } } } } } }); ``` ### Component Variants **MinimalStreamingChat**: - Message list + input - No toolbar, no status indicators - Best for embedded chat **StandardStreamingChat** (recommended): - Message list + input - Toolbar with clear history - Streaming status indicator - Best for most use cases **FullStreamingChat**: - Standard features - Message reactions - Attachments (images, PDFs, audio) - Voice input - Copy/edit/delete messages - Best for feature-rich chat apps **StreamingChat** (legacy): - Original component - Use one of the variants above instead ### Props All variants accept: - `chatStore: Store` - Chat store (required) **FullStreamingChat** additional props: - `showReactions: boolean` - Enable reactions (default: true) - `showAttachments: boolean` - Enable attachments (default: true) - `showVoiceInput: boolean` - Enable voice input (default: true) ### State Interface ```typescript interface StreamingChatState { // Messages messages: Message[]; streamingMessage: string | null; // Current streaming content isStreaming: boolean; // Input inputValue: string; inputDisabled: boolean; // Attachments attachments: MessageAttachment[]; isUploadingAttachment: boolean; // UI State isLoading: boolean; error: string | null; // Voice (if enabled) isRecordingVoice: boolean; voiceTranscript: string | null; } interface Message { id: string; role: 'user' | 'assistant' | 'system'; content: string; timestamp: number; attachments?: MessageAttachment[]; reactions?: MessageReaction[]; isEdited?: boolean; isDeleted?: boolean; } interface MessageAttachment { id: string; type: 'image' | 'pdf' | 'audio' | 'file'; url: string; name: string; size: number; metadata?: AttachmentMetadata; } ``` ### Actions ```typescript type StreamingChatAction = // Messaging | { type: 'sendMessage'; content: string; attachments?: MessageAttachment[] } | { type: 'streamingStarted' } | { type: 'streamingChunk'; chunk: string } | { type: 'streamingCompleted' } | { type: 'streamingError'; error: string } | { type: 'cancelStreaming' } // Input | { type: 'inputChanged'; value: string } | { type: 'clearInput' } // Messages | { type: 'editMessage'; messageId: string; newContent: string } | { type: 'deleteMessage'; messageId: string } | { type: 'clearHistory' } // Reactions | { type: 'addReaction'; messageId: string; emoji: string } | { type: 'removeReaction'; messageId: string; reactionId: string } // Attachments | { type: 'addAttachment'; attachment: MessageAttachment } | { type: 'removeAttachment'; attachmentId: string } | { type: 'uploadAttachment'; file: File } // Voice | { type: 'startVoiceRecording' } | { type: 'stopVoiceRecording' } | { type: 'voiceTranscriptionCompleted'; transcript: string }; ``` ### Dependencies ```typescript interface StreamingChatDependencies { // Message handler (required) sendMessage: ( content: string, attachments: MessageAttachment[], signal: AbortSignal ) => AsyncGenerator; // File upload (optional) uploadFile?: (file: File) => Promise; // Voice transcription (optional) transcribeVoice?: (audioBlob: Blob) => Promise; } ``` ### Complete Example ```typescript
{#if $chatStore.error}
{$chatStore.error}
{/if}
``` --- ## COLLABORATIVE FEATURES **Purpose**: Real-time presence tracking, typing indicators, and live cursors for multi-user chat. ### Quick Start ```typescript import { createStore } from '@composable-svelte/core'; import { StandardStreamingChat, collaborativeReducer, createInitialCollaborativeState, PresenceAvatarStack, TypingIndicator } from '@composable-svelte/chat'; // Create collaborative chat store const chatStore = createStore({ initialState: createInitialCollaborativeState(), reducer: collaborativeReducer, dependencies: { sendMessage: /* ... */, connectWebSocket: (conversationId, userId, onMessage, onConnectionChange) => { const ws = new WebSocket(`wss://api.example.com/chat/${conversationId}`); ws.onopen = () => { onConnectionChange({ status: 'connected', connectedAt: Date.now() }); ws.send(JSON.stringify({ type: 'join', userId })); }; ws.onmessage = (event) => { const message = JSON.parse(event.data); onMessage(message); }; ws.onclose = () => { onConnectionChange({ status: 'disconnected' }); }; return () => ws.close(); }, sendWebSocketMessage: async (message) => { ws.send(JSON.stringify(message)); } } }); // Connect to conversation chatStore.dispatch({ type: 'connectToConversation', conversationId: 'chat-123', userId: 'user-456' }); ``` ### Collaborative State ```typescript interface CollaborativeStreamingChatState extends StreamingChatState { // Connection connection: WebSocketConnectionState; conversationId: string | null; currentUserId: string | null; // Users users: Map; // Sync pendingActions: PendingAction[]; syncState: SyncState; } interface CollaborativeUser { id: string; name: string; avatar?: string; color: string; presence: 'active' | 'idle' | 'away' | 'offline'; typing: TypingInfo | null; cursor: CursorPosition | null; permissions: UserPermissions; lastSeen: number; lastHeartbeat: number; } ``` ### Presence Components **PresenceBadge**: ```typescript ``` **PresenceAvatarStack**: ```typescript import { PresenceAvatarStack, getActiveUsers } from '@composable-svelte/chat'; const activeUsers = $derived(getActiveUsers($chatStore.users, currentUserId)); ``` **PresenceList**: ```typescript ``` ### Typing Indicators **TypingIndicator**: ```typescript import { TypingIndicator, getTypingUsers } from '@composable-svelte/chat'; const typingUsers = $derived( getTypingUsers($chatStore.users, currentUserId, 'message') ); ``` **TypingUsersList**: ```typescript ``` ### Cursor Tracking **CursorMarker**: ```typescript ``` **CursorOverlay**: ```typescript import { CursorOverlay, getCursorPositions } from '@composable-svelte/chat'; const cursorPositions = $derived( getCursorPositions($chatStore.users, currentUserId) ); ``` ### Collaborative Actions ```typescript type CollaborativeAction = // Connection | { type: 'connectToConversation'; conversationId: string; userId: string } | { type: 'disconnectFromConversation' } | { type: 'connectionStateChanged'; state: WebSocketConnectionState } // Users | { type: 'userJoined'; user: CollaborativeUser } | { type: 'userLeft'; userId: string } | { type: 'userPresenceChanged'; userId: string; presence: UserPresence } // Typing | { type: 'userStartedTyping'; userId: string; info: TypingInfo } | { type: 'userStoppedTyping'; userId: string } // Cursors | { type: 'userCursorMoved'; userId: string; position: CursorPosition } // Sync | { type: 'syncMessage'; messageId: string; action: string } | { type: 'syncCompleted'; messageId: string }; ``` ### Complete Collaborative Example ```typescript

Team Chat

{#if typingUsers.length > 0} {/if}
{$chatStore.connection.status} {#if $chatStore.connection.status === 'connected'} {:else} {/if}
``` --- ## WEBSOCKET MANAGER **Purpose**: Low-level WebSocket management for custom implementations. ### API ```typescript import { WebSocketManager, createWebSocketManager, type WebSocketConfig } from '@composable-svelte/chat'; const config: WebSocketConfig = { url: 'wss://api.example.com/chat', reconnect: true, reconnectInterval: 1000, maxReconnectAttempts: 10, heartbeatInterval: 30000 }; const manager = createWebSocketManager(config); // Connect manager.connect(); // Send message manager.send({ type: 'message', content: 'Hello' }); // Listen for messages manager.onMessage((message) => { console.log('Received:', message); }); // Listen for connection changes manager.onConnectionChange((state) => { console.log('Connection:', state.status); }); // Disconnect manager.disconnect(); ``` --- ## MOCK UTILITIES **Purpose**: Testing and development without backend. ```typescript import { createMockStreamingChat } from '@composable-svelte/chat'; const chatStore = createStore({ initialState: createInitialStreamingChatState(), reducer: streamingChatReducer, dependencies: createMockStreamingChat({ responseDelay: 50, // Delay between chunks (ms) mockResponses: [ 'This is a mock response.', 'It simulates streaming behavior.' ] }) }); ``` --- ## COMPONENT SELECTION GUIDE **When to use each variant**: **MinimalStreamingChat**: - Embedded chat - Minimal UI needed - Custom chrome/header **StandardStreamingChat** (recommended): - Most use cases - Balanced features - Good defaults **FullStreamingChat**: - Feature-rich chat app - Need reactions - Need attachments - Need voice input **Collaborative Features**: - Multi-user chat - Team collaboration - Need presence/typing - Real-time sync required --- ## CROSS-REFERENCES **Related Skills**: - **composable-svelte-core**: Store, reducer, Effect system - **composable-svelte-media**: VoiceInput (can integrate with chat) - **composable-svelte-code**: Syntax highlighting in messages - **composable-svelte-components**: UI components **When to Use Each Package**: - **chat**: Real-time chat, streaming responses, LLM interfaces - **media**: Audio players, video embeds, standalone voice input - **code**: Code editors, syntax highlighting - **core**: Base architecture (Store, reducer, effects) --- ## TESTING PATTERNS ### StreamingChat Testing ```typescript import { TestStore } from '@composable-svelte/core'; import { streamingChatReducer, createInitialStreamingChatState, createMockStreamingChat } from '@composable-svelte/chat'; const store = new TestStore({ initialState: createInitialStreamingChatState(), reducer: streamingChatReducer, dependencies: createMockStreamingChat() }); // Test message send await store.send({ type: 'sendMessage', content: 'Hello', attachments: [] }); await store.receive({ type: 'streamingStarted' }, (state) => { expect(state.isStreaming).toBe(true); }); await store.receive({ type: 'streamingChunk', chunk: 'Hi' }, (state) => { expect(state.streamingMessage).toBe('Hi'); }); await store.receive({ type: 'streamingCompleted' }, (state) => { expect(state.isStreaming).toBe(false); expect(state.messages.length).toBe(2); // User + assistant }); ``` ### Collaborative Testing ```typescript import { TestStore } from '@composable-svelte/core'; import { collaborativeReducer, createInitialCollaborativeState } from '@composable-svelte/chat'; const store = new TestStore({ initialState: createInitialCollaborativeState(), reducer: collaborativeReducer, dependencies: { sendMessage: async function*() { yield 'Test response'; }, connectWebSocket: vi.fn(), sendWebSocketMessage: vi.fn() } }); // Test user join await store.send({ type: 'userJoined', user: { id: 'user-2', name: 'Alice', color: '#3b82f6', presence: 'active' } }, (state) => { expect(state.users.size).toBe(1); expect(state.users.get('user-2')?.name).toBe('Alice'); }); // Test typing indicator await store.send({ type: 'userStartedTyping', userId: 'user-2', info: { target: 'message', startedAt: Date.now(), lastUpdate: Date.now() } }, (state) => { expect(state.users.get('user-2')?.typing).toBeTruthy(); }); ``` --- ## TROUBLESHOOTING **Streaming not working**: - Check sendMessage generator function returns AsyncGenerator - Verify yield statements send string chunks - Ensure signal.aborted is checked for cancellation - Check network tab for response streaming **WebSocket connection failing**: - Verify WebSocket URL (wss:// for HTTPS, ws:// for HTTP) - Check CORS/authentication headers - Ensure reconnect logic is enabled - Check browser console for errors **Markdown not rendering**: - Verify message content is valid markdown - Check syntax highlighting CSS is loaded - Ensure code blocks use supported languages **Collaborative features not syncing**: - Verify WebSocket connection is active - Check message format matches expected schema - Ensure user IDs are unique and consistent - Check heartbeat/presence update intervals