Claude Code Plugins

Community-maintained marketplace

Feedback

composable-svelte-chat

@jonathanbelolo/composable-svelte
0
0

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.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

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

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;
            }
          }
        }
      }
    }
  }
});

<StandardStreamingChat {chatStore} />

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<StreamingChatState, StreamingChatAction> - 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

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

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

interface StreamingChatDependencies {
  // Message handler (required)
  sendMessage: (
    content: string,
    attachments: MessageAttachment[],
    signal: AbortSignal
  ) => AsyncGenerator<string, void, unknown>;

  // File upload (optional)
  uploadFile?: (file: File) => Promise<MessageAttachment>;

  // Voice transcription (optional)
  transcribeVoice?: (audioBlob: Blob) => Promise<string>;
}

Complete Example

<script lang="ts">
import { createStore, Effect } from '@composable-svelte/core';
import {
  FullStreamingChat,
  streamingChatReducer,
  createInitialStreamingChatState,
  type MessageAttachment
} from '@composable-svelte/chat';

// Create chat store with all features
const chatStore = createStore({
  initialState: createInitialStreamingChatState(),
  reducer: streamingChatReducer,
  dependencies: {
    // Stream from OpenAI
    sendMessage: async function*(content, attachments, signal) {
      const response = await fetch('https://api.openai.com/v1/chat/completions', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${OPENAI_API_KEY}`
        },
        body: JSON.stringify({
          model: 'gpt-4',
          messages: [
            { role: 'user', content }
          ],
          stream: true
        }),
        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(line => line.trim().startsWith('data:'));

        for (const line of lines) {
          const data = line.replace('data: ', '');
          if (data === '[DONE]') break;

          try {
            const parsed = JSON.parse(data);
            const content = parsed.choices[0]?.delta?.content;
            if (content) {
              yield content;
            }
          } catch (e) {
            // Skip invalid JSON
          }
        }
      }
    },

    // Upload files to your storage
    uploadFile: async (file: File) => {
      const formData = new FormData();
      formData.append('file', file);

      const response = await fetch('/api/upload', {
        method: 'POST',
        body: formData
      });

      const { url, id } = await response.json();

      return {
        id,
        type: file.type.startsWith('image/') ? 'image' : 'file',
        url,
        name: file.name,
        size: file.size
      };
    },

    // Transcribe voice using Whisper
    transcribeVoice: async (audioBlob: Blob) => {
      const formData = new FormData();
      formData.append('file', audioBlob, 'recording.webm');
      formData.append('model', 'whisper-1');

      const response = await fetch('https://api.openai.com/v1/audio/transcriptions', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${OPENAI_API_KEY}`
        },
        body: formData
      });

      const { text } = await response.json();
      return text;
    }
  }
});
</script>

<div class="chat-container">
  <FullStreamingChat
    {chatStore}
    showReactions={true}
    showAttachments={true}
    showVoiceInput={true}
  />

  <!-- Error display -->
  {#if $chatStore.error}
    <div class="error-toast">{$chatStore.error}</div>
  {/if}
</div>

COLLABORATIVE FEATURES

Purpose: Real-time presence tracking, typing indicators, and live cursors for multi-user chat.

Quick Start

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

interface CollaborativeStreamingChatState extends StreamingChatState {
  // Connection
  connection: WebSocketConnectionState;
  conversationId: string | null;
  currentUserId: string | null;

  // Users
  users: Map<string, CollaborativeUser>;

  // 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:

<PresenceBadge presence="active" showText={true} />

PresenceAvatarStack:

import { PresenceAvatarStack, getActiveUsers } from '@composable-svelte/chat';

const activeUsers = $derived(getActiveUsers($chatStore.users, currentUserId));

<PresenceAvatarStack users={activeUsers} maxVisible={5} />

PresenceList:

<PresenceList users={activeUsers} groupByPresence={true} />

Typing Indicators

TypingIndicator:

import { TypingIndicator, getTypingUsers } from '@composable-svelte/chat';

const typingUsers = $derived(
  getTypingUsers($chatStore.users, currentUserId, 'message')
);

<TypingIndicator users={typingUsers} />

TypingUsersList:

<TypingUsersList users={typingUsers} />

Cursor Tracking

CursorMarker:

<CursorMarker
  user={user}
  position={user.cursor}
/>

CursorOverlay:

import { CursorOverlay, getCursorPositions } from '@composable-svelte/chat';

const cursorPositions = $derived(
  getCursorPositions($chatStore.users, currentUserId)
);

<CursorOverlay cursors={cursorPositions} />

Collaborative Actions

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

<script lang="ts">
import { createStore } from '@composable-svelte/core';
import {
  StandardStreamingChat,
  collaborativeReducer,
  createInitialCollaborativeState,
  PresenceAvatarStack,
  TypingIndicator,
  getActiveUsers,
  getTypingUsers,
  type CollaborativeUser
} from '@composable-svelte/chat';

const currentUserId = 'user-123';

// Create WebSocket connection
let ws: WebSocket;

const chatStore = createStore({
  initialState: createInitialCollaborativeState(),
  reducer: collaborativeReducer,
  dependencies: {
    sendMessage: /* ... */,

    connectWebSocket: (conversationId, userId, onMessage, onConnectionChange) => {
      ws = new WebSocket(`wss://api.example.com/chat/${conversationId}`);

      ws.onopen = () => {
        onConnectionChange({ status: 'connected', connectedAt: Date.now() });
        ws.send(JSON.stringify({
          type: 'join',
          userId,
          user: {
            id: userId,
            name: 'Current User',
            color: '#3b82f6'
          }
        }));
      };

      ws.onmessage = (event) => {
        const message = JSON.parse(event.data);
        onMessage(message);
      };

      ws.onerror = () => {
        onConnectionChange({ status: 'error', error: 'Connection failed' });
      };

      ws.onclose = () => {
        onConnectionChange({ status: 'disconnected' });
      };

      return () => {
        ws.close();
      };
    },

    sendWebSocketMessage: async (message) => {
      if (ws?.readyState === WebSocket.OPEN) {
        ws.send(JSON.stringify(message));
      }
    }
  }
});

// Connect on mount
$effect(() => {
  chatStore.dispatch({
    type: 'connectToConversation',
    conversationId: 'chat-room-123',
    userId: currentUserId
  });

  return () => {
    chatStore.dispatch({ type: 'disconnectFromConversation' });
  };
});

// Emit typing events
let typingTimeout: ReturnType<typeof setTimeout>;
function handleInputChange(value: string) {
  chatStore.dispatch({ type: 'inputChanged', value });

  // Emit typing started
  chatStore.dispatch({
    type: 'userStartedTyping',
    userId: currentUserId,
    info: {
      target: 'message',
      startedAt: Date.now(),
      lastUpdate: Date.now()
    }
  });

  // Clear previous timeout
  clearTimeout(typingTimeout);

  // Stop typing after 2 seconds of inactivity
  typingTimeout = setTimeout(() => {
    chatStore.dispatch({ type: 'userStoppedTyping', userId: currentUserId });
  }, 2000);
}

// Derived state
const activeUsers = $derived(getActiveUsers($chatStore.users, currentUserId));
const typingUsers = $derived(getTypingUsers($chatStore.users, currentUserId, 'message'));
</script>

<div class="collaborative-chat">
  <!-- Presence indicators -->
  <div class="chat-header">
    <h2>Team Chat</h2>
    <PresenceAvatarStack users={activeUsers} maxVisible={5} />
  </div>

  <!-- Chat interface -->
  <StandardStreamingChat {chatStore} />

  <!-- Typing indicator -->
  {#if typingUsers.length > 0}
    <TypingIndicator users={typingUsers} />
  {/if}

  <!-- Connection status -->
  <div class="connection-status">
    {$chatStore.connection.status}
    {#if $chatStore.connection.status === 'connected'}
      <span class="indicator active"></span>
    {:else}
      <span class="indicator inactive"></span>
    {/if}
  </div>
</div>

WEBSOCKET MANAGER

Purpose: Low-level WebSocket management for custom implementations.

API

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.

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

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

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