docs: add comprehensive README and module documentation
This commit is contained in:
408
docs/ui.md
Normal file
408
docs/ui.md
Normal file
@@ -0,0 +1,408 @@
|
||||
# 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<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:
|
||||
|
||||
```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<void>
|
||||
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: <uuid>`
|
||||
3. Subsequent messages: include `session_id: <uuid>`
|
||||
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`
|
||||
Reference in New Issue
Block a user