8.1 KiB
8.1 KiB
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:
interface Message {
role: 'user' | 'assistant'
content: string
}
// State
const [messages, setMessages] = useState<Message[]>([])
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:
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:
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:
interface UseChatStreamReturn {
sendMessage: (
message: string,
onChunk: (chunk: string) => void
) => Promise<void>
sessionId: string | null
}
const { sendMessage, sessionId } = useChatStream()
Usage:
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:
: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:
@keyframes bounce {
0%, 80%, 100% { transform: scale(0.6); }
40% { transform: scale(1); }
}
Development
Setup
cd ui
npm install
Dev Server
npm run dev
# Opens http://localhost:5173
Build
npm run build
# Output: ui/dist/
Preview Production Build
npm run preview
Configuration
Vite Config
vite.config.ts:
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:
const API_BASE = '/api' // Not http://localhost:7373/api
TypeScript
Types
// 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
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:
- First message:
session_id: null→ backend creates UUID - Response header:
X-Session-ID: <uuid> - Subsequent messages: include
session_id: <uuid> - History retrieved automatically
Customization
Themes
Modify App.css and index.css:
/* Custom accent color */
--accent-primary: #ff6b6b;
--user-bg: #ff6b6b;
Fonts
Update index.css:
body {
font-family: 'Inter', -apple-system, sans-serif;
}
Message Layout
Modify MessageList.css:
.message-content {
max-width: 90%; /* Wider messages */
font-size: 16px; /* Larger text */
}
Troubleshooting
CORS errors
- Check
vite.config.tsproxy configuration - Verify backend CORS origins include
http://localhost:5173
Stream not updating
- Check browser network tab for SSE events
- Verify
EventSourceResponsefrom backend
Messages not appearing
- Check React DevTools for state updates
- Verify
messagesarray is being mutated correctly
Build fails
- Check TypeScript errors:
npx tsc --noEmit - Update dependencies:
npm update