# 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`
