# UI Module Documentation The UI is a React + Vite frontend for the companion chat interface. It provides real-time streaming chat with a clean, Obsidian-inspired dark theme. ## Architecture ``` HTTP/SSE ↓ ┌─────────────────┐ │ App.tsx │ - State management │ Message state │ - User/assistant messages └────────┬────────┘ ↓ ┌─────────────────┐ │ MessageList │ - Render messages │ (components/) │ - User/assistant styling └─────────────────┘ ┌─────────────────┐ │ ChatInput │ - Textarea + send │ (components/) │ - Auto-resize, hotkeys └─────────────────┘ ↓ ┌─────────────────┐ │ useChatStream │ - SSE streaming │ (hooks/) │ - Session management └─────────────────┘ ``` ## Project Structure ``` ui/ ├── src/ │ ├── main.tsx # React entry point │ ├── App.tsx # Main app component │ ├── App.css # App layout styles │ ├── index.css # Global styles │ ├── components/ │ │ ├── MessageList.tsx # Message display │ │ ├── MessageList.css # Message styling │ │ ├── ChatInput.tsx # Input textarea │ │ └── ChatInput.css # Input styling │ └── hooks/ │ └── useChatStream.ts # SSE streaming hook ├── index.html # HTML template ├── vite.config.ts # Vite configuration ├── tsconfig.json # TypeScript config └── package.json # Dependencies ``` ## Components ### App.tsx Main application state management: ```typescript interface Message { role: 'user' | 'assistant' content: string } // State const [messages, setMessages] = useState([]) const [input, setInput] = useState('') const [isLoading, setIsLoading] = useState(false) // Handlers const handleSend = async () => { /* ... */ } const handleKeyDown = (e) => { /* Enter to send, Shift+Enter newline */ } ``` **Features:** - Auto-scroll to bottom on new messages - Keyboard shortcuts (Enter to send, Shift+Enter for newline) - Loading state with animation - Message streaming in real-time ### MessageList.tsx Renders the chat history: ```typescript interface MessageListProps { messages: Message[] isLoading: boolean } ``` **Layout:** - User messages: Right-aligned, blue background - Assistant messages: Left-aligned, gray background with border - Loading indicator: Three animated dots - Empty state: Prompt text when no messages **Styling:** - Max-width 800px, centered - Smooth scroll behavior - Avatar-less design (clean, text-focused) ### ChatInput.tsx Textarea input with send button: ```typescript interface ChatInputProps { value: string onChange: (value: string) => void onSend: () => void onKeyDown: (e: KeyboardEvent) => void disabled: boolean } ``` **Features:** - Auto-resizing textarea - Send button with loading state - Placeholder text - Disabled during streaming ## Hooks ### useChatStream.ts Manages SSE streaming connection: ```typescript interface UseChatStreamReturn { sendMessage: ( message: string, onChunk: (chunk: string) => void ) => Promise sessionId: string | null } const { sendMessage, sessionId } = useChatStream() ``` **Usage:** ```typescript await sendMessage("Hello", (chunk) => { // Append chunk to current response setMessages(prev => { const last = prev[prev.length - 1] if (last?.role === 'assistant') { last.content += chunk return [...prev] } return [...prev, { role: 'assistant', content: chunk }] }) }) ``` **SSE Protocol:** The API streams events in this format: ``` data: {"type": "chunk", "content": "Hello"} data: {"type": "chunk", "content": " world"} data: {"type": "sources", "sources": [{"file": "journal.md"}]} data: {"type": "done", "session_id": "uuid"} ``` ## Styling ### Design System Based on Obsidian's dark theme: ```css :root { --bg-primary: #0d1117; /* App background */ --bg-secondary: #161b22; /* Header/footer */ --bg-tertiary: #21262d; /* Input background */ --text-primary: #c9d1d9; /* Main text */ --text-secondary: #8b949e; /* Placeholder */ --accent-primary: #58a6ff; /* Primary blue */ --accent-secondary: #79c0ff;/* Lighter blue */ --border: #30363d; /* Borders */ --user-bg: #1f6feb; /* User message */ --assistant-bg: #21262d; /* Assistant message */ } ``` ### Message Styling **User Message:** - Blue background (`--user-bg`) - White text - Border radius: 12px (12px 12px 4px 12px) - Max-width: 80% **Assistant Message:** - Gray background (`--assistant-bg`) - Light text (`--text-primary`) - Border: 1px solid `--border` - Border radius: 12px (12px 12px 12px 4px) ### Loading Animation Three bouncing dots using CSS keyframes: ```css @keyframes bounce { 0%, 80%, 100% { transform: scale(0.6); } 40% { transform: scale(1); } } ``` ## Development ### Setup ```bash cd ui npm install ``` ### Dev Server ```bash npm run dev # Opens http://localhost:5173 ``` ### Build ```bash npm run build # Output: ui/dist/ ``` ### Preview Production Build ```bash npm run preview ``` ## Configuration ### Vite Config `vite.config.ts`: ```typescript export default defineConfig({ plugins: [react()], server: { port: 5173, proxy: { '/api': { target: 'http://localhost:7373', changeOrigin: true, }, }, }, }) ``` **Proxy Setup:** - Frontend: `http://localhost:5173` - API: `http://localhost:7373` - `/api/*` → `http://localhost:7373/api/*` This allows using relative API paths in the code: ```typescript const API_BASE = '/api' // Not http://localhost:7373/api ``` ## TypeScript ### Types ```typescript // Message role type Role = 'user' | 'assistant' // Message object interface Message { role: Role content: string } // Chat request type ChatRequest = { message: string session_id?: string temperature?: number } // SSE chunk type ChunkEvent = { type: 'chunk' content: string } type SourcesEvent = { type: 'sources' sources: Array<{ file: string section?: string date?: string }> } type DoneEvent = { type: 'done' session_id: string } ``` ## API Integration ### Chat Endpoint ```typescript const response = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: userInput, session_id: sessionId, // null for new session stream: true, }), }) // Read SSE stream 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, { stream: true }) // Parse SSE lines } ``` ### Session Persistence The backend maintains conversation history via `session_id`: 1. First message: `session_id: null` → backend creates UUID 2. Response header: `X-Session-ID: ` 3. Subsequent messages: include `session_id: ` 4. History retrieved automatically ## Customization ### Themes Modify `App.css` and `index.css`: ```css /* Custom accent color */ --accent-primary: #ff6b6b; --user-bg: #ff6b6b; ``` ### Fonts Update `index.css`: ```css body { font-family: 'Inter', -apple-system, sans-serif; } ``` ### Message Layout Modify `MessageList.css`: ```css .message-content { max-width: 90%; /* Wider messages */ font-size: 16px; /* Larger text */ } ``` ## Troubleshooting **CORS errors** - Check `vite.config.ts` proxy configuration - Verify backend CORS origins include `http://localhost:5173` **Stream not updating** - Check browser network tab for SSE events - Verify `EventSourceResponse` from backend **Messages not appearing** - Check React DevTools for state updates - Verify `messages` array is being mutated correctly **Build fails** - Check TypeScript errors: `npx tsc --noEmit` - Update dependencies: `npm update`