# Chat Module — Architecture & Developer Documentation > **Location:** [`src/modules/ai/`](../src/modules/ai/) > **Page:** [`src/pages/PlaygroundChat.tsx`](../src/pages/PlaygroundChat.tsx) --- ## Table of Contents - [Overview](#overview) - [Architecture](#architecture) - [Component Tree](#component-tree) - [ChatPanel — Reusable Component](#chatpanel--reusable-component) - [Message Flow](#message-flow) - [Tool-Calling Flow](#tool-calling-flow) - [Session Management](#session-management) - [Features](#features) - [File Browser Integration](#file-browser-integration) - [External Context & Tools Injection](#external-context--tools-injection) - [Tool System](#tool-system) - [Storage](#storage) - [File Reference](#file-reference) --- ## Overview The chat module is a client-side AI playground built on the **OpenAI SDK** (used for both OpenAI and OpenRouter providers). It supports multi-turn conversations, image attachments, file context attachments, streaming responses, tool calling (search, page creation, image generation, file system), session persistence, and full export capabilities. The core is a **reusable `ChatPanel` component** ([`ChatPanel.tsx`](../src/modules/ai/ChatPanel.tsx)) with preset modes (`simple`, `standard`, `developer`), embeddable anywhere. All state and logic lives in [`useChatEngine`](../src/modules/ai/useChatEngine.ts). The playground page ([`PlaygroundChat.tsx`](../src/pages/PlaygroundChat.tsx)) is a thin wrapper using the `developer` preset. --- ## Architecture ```mermaid graph TB subgraph ChatPanelComp["ChatPanel.tsx (reusable)"] Header["ChatHeader"] RPG["ResizablePanelGroup"] subgraph Sidebar["ChatSidebar (ResizablePanel)"] Sessions["Sessions"] Provider["Provider & Model"] SysPrompt["System Prompt"] Tools["Tools Toggles"] FileBrowser["Files (FileBrowserPanel)"] Stats["Stats"] Payload["Prompt Payload (CompactTreeView)"] Logs["Chat Logs (ChatLogBrowser)"] end subgraph Main["ChatMessages (ResizablePanel)"] Messages["MessageBubble × N"] Composer["ChatComposer + FileContext chips"] end end subgraph Engine["useChatEngine Hook"] State["State Management"] API["API Client (OpenAI SDK)"] ToolPreset["Tool Presets"] ExtTools["extraToolsRef (external)"] CtxProvider["contextProviderRef (external)"] FileCtx["FileContext State"] SessionMgr["Session Storage"] Export["Export Handlers"] end ChatPanelComp --> Engine API --> OpenAI["OpenAI API"] API --> OpenRouter["OpenRouter API"] ToolPreset --> SearchTools["searchTools.ts"] ToolPreset --> ImageTools["imageTools.ts"] ToolPreset --> VfsTools["vfsTools.ts"] ToolPreset --> PageTools["pageTools.ts"] SessionMgr --> LocalStorage["localStorage"] ``` --- ## Component Tree | Component | File | Role | |-----------|------|------| | **PlaygroundChat** | [`PlaygroundChat.tsx`](../src/pages/PlaygroundChat.tsx) | Thin page wrapper: `` | | **ChatPanel** | [`ChatPanel.tsx`](../src/modules/ai/ChatPanel.tsx) | Reusable layout: header + sidebar + messages, configurable via presets and props | | ↳ **ChatHeader** | [`components/ChatHeader.tsx`](../src/modules/ai/components/ChatHeader.tsx) | Top bar: provider badge, New/JSON/MD/Clear/Settings buttons | | ↳ **ChatSidebar** | [`components/ChatSidebar.tsx`](../src/modules/ai/components/ChatSidebar.tsx) | Collapsible settings: sessions, provider, system prompt, tools, **file browser**, stats, payload inspector, logs | | ↳ **FileBrowserPanel** | [`FileBrowserPanel.tsx`](../src/apps/filebrowser/FileBrowserPanel.tsx) | VFS file browser (home mount, list view) with file selection → attach as context | | ↳ **ChatMessages** | (in ChatPanel.tsx) | Messages area + composer wrapper | | ↳ **MessageBubble** | [`components/MessageBubble.tsx`](../src/modules/ai/components/MessageBubble.tsx) | Single message: avatar, copy-to-clipboard, markdown rendering, streaming indicator | | ↳ **ChatComposer** | [`components/ChatComposer.tsx`](../src/modules/ai/components/ChatComposer.tsx) | Textarea input, attachments, drag-drop, image picker, **file context chips**, prompt history, send/cancel | | ↳ **CompactTreeView** | [`ChatLogBrowser.tsx`](../src/components/ChatLogBrowser.tsx) | Keyboard-navigable JSON tree browser (shared by Payload and Logs) | | ↳ **ChatLogBrowser** | [`ChatLogBrowser.tsx`](../src/components/ChatLogBrowser.tsx) | Log viewer with level filtering and drill-in on data objects | --- ## ChatPanel — Reusable Component [`ChatPanel.tsx`](../src/modules/ai/ChatPanel.tsx) is the primary entry point for embedding chat anywhere. ### Presets | Preset | Header | Sidebar | Sidebar open by default | |-----------|--------|---------|-------------------------| | `simple` | ❌ | ❌ | — | | `standard` | ✅ | ✅ | closed | | `developer` | ✅ | ✅ | open | ### Props | Prop | Type | Description | |------|------|-------------| | `preset` | `'simple' \| 'standard' \| 'developer'` | Layout preset (default: `'developer'`) | | `showHeader` | `boolean?` | Override header visibility | | `showSidebar` | `boolean?` | Override sidebar availability | | `sidebarOpen` | `boolean?` | Override sidebar initial state | | `className` | `string?` | CSS class on outer container | | `layoutId` | `string?` | Persistence ID for panel sizes (default: `'chat-layout'`) | | `getContext` | `() => string \| null` | Dynamic context injected into system prompt per-send | | `extraTools` | `() => any[]` | Extra tools added to tool-calling payload per-send | ### Usage ```tsx import ChatPanel from '@/modules/ai/ChatPanel'; // Full developer experience (default) // Embeddable minimal chat // With external context and tools `Active page: ${slug}\nSelection: ${ids.join(', ')}`} extraTools={() => [myCustomTool]} /> ``` ### Exports | Export | Description | |--------|-------------| | `ChatPanel` (default) | Full layout with header + sidebar + messages | | `ChatMessages` | Just the messages + composer (for custom layouts) | | `ChatPreset` | Type: `'simple' \| 'standard' \| 'developer'` | | `ChatPanelProps` | Props interface | --- ## Message Flow ```mermaid sequenceDiagram participant U as User participant C as ChatComposer participant E as useChatEngine participant A as OpenAI API U->>C: Types message + optional images U->>C: Press Enter C->>E: sendMessage() E->>E: Create userMsg + empty assistantMsg E->>E: setMessages([...prev, user, assistant]) E->>E: Build apiMessages[] from systemPrompt + history E->>A: chat.completions.create (stream: true) loop Streaming A-->>E: delta chunks E->>E: Append to assistantMsg.content E->>E: setMessages() (triggers re-render) end A-->>E: Stream complete E->>E: Mark assistantMsg.isStreaming = false E->>E: Auto-save session to localStorage ``` ### Cancellation The user can cancel an in-progress response via the **Stop** button in the composer or the streaming indicator in the message bubble. This triggers `handleCancel()`, which calls `abortRef.current?.abort()` to abort the fetch and marks the current assistant message as complete. --- ## Tool-Calling Flow When **tools are enabled**, the engine uses `runTools()` from the OpenAI SDK instead of `create()`: ```mermaid sequenceDiagram participant E as useChatEngine participant A as OpenAI API participant T as Tool Functions E->>A: runTools(apiMessages, tools[]) loop Tool Calls A-->>E: tool_call request (e.g. search_content) E->>T: Execute tool function T-->>E: JSON result E->>E: Append tool message to context E->>E: addChatLog() for verbose logging E->>A: Continue with tool result end A-->>E: Final assistant response (streamed) E->>E: Merge toolContext into assistant message E->>E: Stream content to UI ``` ### Tool Context Persistence Tool call results are summarized and attached to the assistant message as `toolContext`. This context is included in subsequent API calls, preventing the AI from re-searching the same data in follow-up questions. --- ## Session Management ```mermaid sequenceDiagram participant E as useChatEngine participant S as chatSessions.ts participant LS as localStorage Note over E: On every message change E->>S: saveSession({ id, title, messages }) S->>LS: setItem("chat-session-{id}", full data) S->>LS: setItem("chat-sessions-index", metadata[]) S->>S: Trim to MAX_SESSIONS (50) Note over E: Load session E->>S: loadSession(id) S->>LS: getItem("chat-session-{id}") S-->>E: ChatSession (sanitized) E->>E: setMessages(clean), setSessionId(id) ``` --- ## Features ### Multi-Provider Support - **OpenAI** — via proxy or direct API key - **OpenRouter** — custom `baseURL` + API key - Provider/model selection persisted in localStorage - API keys stored in Supabase user secrets ### Image Attachments - **Drag & drop** files onto the composer - **Paste** images from clipboard - **File picker** (file input dialog) - **Gallery picker** — browse platform images via `ImagePickerDialog` - Remote images proxied via server render API for resizing (`getResizedImageUrl`) ### Streaming Responses - Uses `stream: true` with the OpenAI SDK - Real-time content rendering via `isStreaming` flag on assistant messages - Auto-scroll to bottom during streaming ### Prompt History - All sent prompts saved to localStorage via `usePromptHistory` - Navigate with **Ctrl+↑ / Ctrl+↓** in the composer - History persisted under key `promptHistoryChat` ### Export | Format | Action | Method | |--------|--------|--------| | **JSON** | Download `.json` file + copy to clipboard | [`exportChatAsJson`](../src/modules/ai/chatExport.ts) | | **Markdown** | Copy to clipboard | [`exportChatAsMarkdown`](../src/modules/ai/chatExport.ts) | | **Payload JSON** | Copy button in sidebar | `CompactTreeView` headerContent | | **Logs JSON** | Copy button in sidebar | `ChatSidebar` headerContent | ### Sidebar Inspector - **Prompt Payload** — live `useMemo` of the API messages array, browseable via `CompactTreeView` with keyboard navigation (↑↓←→, search, breadcrumbs) - **Chat Logs** — verbose timestamped log of all engine events (tool calls, results, errors) - Both sections support **copy-to-clipboard** via header buttons ### Desktop/Mobile Layout - **Desktop** — `ResizablePanelGroup` (sidebar 25% default, 15–45% range; `autoSaveId="chat-layout"` for persistence) - **Mobile** — sidebar as overlay (85vw, max 360px) with backdrop; full-width chat panel --- ## File Browser Integration The sidebar includes a **Files** collapsible section embedding a [`FileBrowserPanel`](../src/apps/filebrowser/FileBrowserPanel.tsx) configured for the user's `home` VFS mount. ### File Context Workflow 1. **Browse** — navigate your home drive in the sidebar file browser 2. **Preview** — double-click / Enter / Space opens the lightbox (image, video, or text) 3. **Attach** — click a file, then click the **Attach** button to add it as context 4. **View** — attached files appear as **teal chips** above the composer and listed in the Files section footer 5. **Inject** — file path + content is injected into the system prompt under `--- Attached Files (editable via fs_write) ---` 6. **Write back** — the LLM can modify attached files via the existing `fs_write` tool 7. **Remove** — click the × on any chip or in the sidebar list to detach ### FileContext Type ```typescript interface FileContext { path: string; // VFS path (e.g. "notes/readme.md") mount: string; // VFS mount (e.g. "home") name: string; // filename only content: string; // file text content } ``` --- ## External Context & Tools Injection Consumers can inject dynamic context and custom tools into the chat engine via `ChatPanel` props, enabling domain-specific integrations without modifying the core chat module. ### `getContext` — Dynamic System Prompt Injection Called synchronously in `sendMessage()` just before building the API payload. The returned string is appended to the system prompt for that specific request. ```tsx { const sel = getSelectedItems(); return sel.length ? `Selected items:\n${sel.map(s => `- ${s.name}`).join('\n')}` : null; }} /> ``` ### `extraTools` — Custom Tool Injection Called when assembling the tools array for each send. Return OpenAI-compatible `RunnableToolFunctionWithParse` definitions. ```tsx [ { type: 'function', function: { name: 'update_product', parse: JSON.parse, description: 'Update a product field', parameters: { type: 'object', properties: { id: { type: 'string' } } }, function: async (args) => { /* ... */ }, }, }, ]} /> ``` ### Implementation Both use **ref-based injection** in `useChatEngine`: - `contextProviderRef` — `React.MutableRefObject<(() => string | null) | null>` - `extraToolsRef` — `React.MutableRefObject<(() => any[]) | null>` `ChatPanel` wires its props into these refs via `useEffect`, ensuring the engine always has the latest provider functions without prop-drilling into the hook. ## Tool System All tools follow the OpenAI SDK `RunnableToolFunctionWithParse` interface (Zod parse + async function + JSON schema). ### Search Tools — [`searchTools.ts`](../src/modules/ai/searchTools.ts) | Tool | Description | |------|-------------| | `search_content` | Full-text search across pages, posts, pictures (type filter, limit) | | `find_pages` | Search pages only | | `find_pictures` | Search pictures only | | `get_page_content` | Fetch full page content by user_id + slug | | `list_categories` | List all category trees | | `find_by_category` | Find items by category slug | Bundled via `createSearchToolPreset()`. ### Image Tools — [`imageTools.ts`](../src/modules/ai/imageTools.ts) | Tool | Description | |------|-------------| | `generate_image` | Generate image from prompt → upload to Supabase `temp-images` bucket → return markdown embed | ### VFS Tools — [`vfsTools.ts`](../src/modules/ai/vfsTools.ts) | Tool | Description | |------|-------------| | `vfs_ls` | List directory contents (optional glob filter) | | `vfs_read` | Read file content | | `vfs_write` | Write/create file | | `vfs_mkdir` | Create directory | | `vfs_delete` | Delete file or directory | Operates on the user's home drive via authenticated API calls. ### Page Tools — [`lib/pageTools.ts`](../src/lib/pageTools.ts) | Tool | Description | |------|-------------| | `create_page` | Create a new page on the user's account | ### Tool Toggle Controls Each tool category has an independent toggle in the sidebar: | Toggle | localStorage Key | Default | |--------|-----------------|---------| | Search Tools | `chat-settings-tools` | `true` | | Page Tools | `chat-settings-page-tools` | `true` | | Image Tools | `chat-settings-image-tools` | `false` | | File Tools | `chat-settings-vfs-tools` | `false` | --- ## Storage All persistence is **client-side via `localStorage`**. No server database is used for chat data. ### localStorage Keys | Key | Type | Description | |-----|------|-------------| | `chat-sessions-index` | `Omit[]` | Session metadata index (max 50) | | `chat-session-{uuid}` | `ChatSession` | Full session data (messages + metadata) | | `chat-settings-provider` | `string` | Selected AI provider | | `chat-settings-model` | `string` | Selected model | | `chat-settings-system-prompt` | `string` | System prompt text | | `chat-settings-tools` | `boolean` | Search tools toggle | | `chat-settings-page-tools` | `boolean` | Page tools toggle | | `chat-settings-image-tools` | `boolean` | Image tools toggle | | `chat-settings-vfs-tools` | `boolean` | VFS tools toggle | | `chat-settings-show` | `boolean` | Sidebar visibility | | `chat-settings-sidebar-width` | `number` | (legacy — now managed by ResizablePanelGroup) | | `chat-layout` | (react-resizable-panels) | Panel sizes (auto-managed) | | `promptHistoryChat` | `string[]` | Prompt history ring buffer | | `chat-section-*` | `boolean` | CollapsibleSection open/closed states | ### Session Lifecycle 1. **Auto-save** — sessions save on every message change via `useEffect` 2. **Title generation** — first 60 chars of the first user message 3. **Max sessions** — oldest sessions pruned when exceeding 50 4. **Sanitization** — streaming flags stripped, empty orphan messages filtered on save and load --- ## File Reference | File | Purpose | |------|---------| | [`ChatPanel.tsx`](../src/modules/ai/ChatPanel.tsx) | **Reusable component**: presets, layout, external context/tools injection | | [`useChatEngine.ts`](../src/modules/ai/useChatEngine.ts) | Central hook: all state, API calls, streaming, tool orchestration, file contexts | | [`types.ts`](../src/modules/ai/types.ts) | `ChatMessage`, `ImageAttachment`, `FileContext`, helpers | | [`chatSessions.ts`](../src/modules/ai/chatSessions.ts) | Session persistence (localStorage) | | [`chatExport.ts`](../src/modules/ai/chatExport.ts) | JSON/Markdown export (download + clipboard) | | [`searchTools.ts`](../src/modules/ai/searchTools.ts) | 6 search/content tools + `createSearchToolPreset` | | [`imageTools.ts`](../src/modules/ai/imageTools.ts) | `generate_image` tool | | [`vfsTools.ts`](../src/modules/ai/vfsTools.ts) | 6 VFS tools (ls/read/write/write_many/mkdir/delete) | | [`ChatHeader.tsx`](../src/modules/ai/components/ChatHeader.tsx) | Top bar with provider badge and action buttons | | [`ChatSidebar.tsx`](../src/modules/ai/components/ChatSidebar.tsx) | Settings panel: sessions, provider, prompt, tools, **file browser**, stats, payload, logs | | [`ChatComposer.tsx`](../src/modules/ai/components/ChatComposer.tsx) | Input area: textarea, attachments, **file context chips**, drag-drop, image picker | | [`MessageBubble.tsx`](../src/modules/ai/components/MessageBubble.tsx) | Message rendering: avatar, copy, markdown, streaming | | [`PlaygroundChat.tsx`](../src/pages/PlaygroundChat.tsx) | Page wrapper: `` | | [`ChatLogBrowser.tsx`](../src/components/ChatLogBrowser.tsx) | Log viewer + CompactTreeView (keyboard-nav JSON browser) |