react-virtual in messages list
This commit is contained in:
+882
@@ -0,0 +1,882 @@
|
||||
# Chat & ChatHistory Apps Architecture
|
||||
|
||||
**Date**: February 20, 2026
|
||||
**Status**: Complete & Running
|
||||
**Scope**: Unified Pi harness integration for chat functionality
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The Chat and ChatHistory applications work together to provide a complete conversational interface with session management, grouping, and model selection. They consume the new Pi harness WebSocket API (`/api/pi/chat/ws`) and REST endpoints (`/api/pi/*`).
|
||||
|
||||
---
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── apps/officer-web/
|
||||
│ ├── Screens/Dashboard/
|
||||
│ │ ├── Chat/ # Chat input/output UI components
|
||||
│ │ │ ├── index.tsx (empty export)
|
||||
│ │ │ ├── ChatPanel.tsx (main chat container)
|
||||
│ │ │ ├── EmbeddableChat.tsx (reusable chat widget)
|
||||
│ │ │ ├── InputArea.tsx (message input with attachments & voice)
|
||||
│ │ │ ├── Settings.tsx (model/provider selector)
|
||||
│ │ │ ├── ChatList/index.tsx (placeholder)
|
||||
│ │ │ ├── OpenCodeModelPicker.tsx (legacy model picker, keep for now)
|
||||
│ │ │ └── usePi.ts (WebSocket hook for Pi harness)
|
||||
│ │ │
|
||||
│ │ └── ChatHistory/ # Session & group management UI
|
||||
│ │ ├── index.tsx (SessionListPage - main router)
|
||||
│ │ ├── Screen.tsx (SessionList - tree view)
|
||||
│ │ ├── ChatDetailPanel.tsx (session or new chat detail panel)
|
||||
│ │ ├── Widget.tsx (sidebar widget showing recent sessions)
|
||||
│ │ ├── CreateGroupDialog.tsx (new group form)
|
||||
│ │ ├── GroupContextMenu.tsx (group actions: rename, delete)
|
||||
│ │ └── SessionContextMenu.tsx (session actions: move, rename, delete)
|
||||
│ │
|
||||
│ └── state/ # State management hooks
|
||||
│ ├── useChatSessions.ts (REST endpoints: list, get, update, delete, search)
|
||||
│ ├── useChatGroups.ts (REST endpoints: create, list, update, delete, move)
|
||||
│ ├── useModels.ts (unified Pi models API)
|
||||
│ └── useRecentModels.ts (recently used models tracking)
|
||||
│
|
||||
└── workspaces/apps/ # Shared Chat workspace components
|
||||
├── Chat/
|
||||
│ ├── index.ts (exports: MessageList, MessageBubble, etc.)
|
||||
│ ├── types.ts (type definitions for all chat types)
|
||||
│ ├── MessageList.tsx (message history display)
|
||||
│ ├── MessageBubble.tsx (individual message component)
|
||||
│ ├── ToolActivity.tsx (tool execution display)
|
||||
│ ├── QuestionActivity.tsx (question suggestions)
|
||||
│ └── [other message components]
|
||||
│
|
||||
└── ChatHistory/
|
||||
├── index.ts (exports: SessionListPage, SessionList)
|
||||
└── SessionBar.tsx (session header bar with nav)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Component Hierarchy
|
||||
|
||||
### Chat Flow
|
||||
|
||||
```
|
||||
ChatPanel (main container)
|
||||
├── SessionBar (header with back, delete, fullscreen)
|
||||
└── EmbeddableChat (reusable chat interface)
|
||||
├── MessageList (scrollable message history)
|
||||
│ ├── MessageBubble[] (user/assistant messages)
|
||||
│ ├── ToolActivity (tool execution results)
|
||||
│ └── StreamingBubble (real-time assistant text)
|
||||
│
|
||||
└── InputArea (message input)
|
||||
├── Attachment controls (image, webpage, file)
|
||||
├── Textarea (message input with auto-resize)
|
||||
├── Model picker (Settings component)
|
||||
├── Send/Stop button
|
||||
└── Voice recording (Mic button)
|
||||
```
|
||||
|
||||
### Session Management Flow
|
||||
|
||||
```
|
||||
SessionListPage (routing)
|
||||
└── WorkspaceLayout (split pane layout)
|
||||
├── SessionList (left panel: session tree)
|
||||
│ ├── Ungrouped sessions
|
||||
│ └── Grouped sections
|
||||
│ ├── GroupContextMenu (rename, delete group)
|
||||
│ └── SessionContextMenu (move, rename, delete session)
|
||||
│
|
||||
└── ChatDetailPanel (right panel: chat view)
|
||||
├── SessionChat (existing session)
|
||||
│ ├── DetailBar (session info, delete button)
|
||||
│ └── EmbeddableChat (chat interface)
|
||||
│
|
||||
└── NewChat (new conversation)
|
||||
├── DetailBar
|
||||
└── EmbeddableChat
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Core Components
|
||||
|
||||
### 1. ChatPanel
|
||||
|
||||
**File**: `Chat/ChatPanel.tsx`
|
||||
|
||||
**Purpose**: Main chat container for the `/chat/:sessionId` route.
|
||||
|
||||
**Key Features**:
|
||||
- Displays session title in header
|
||||
- Handles location.state for pre-filled messages and initial sends
|
||||
- Manages fullscreen toggle
|
||||
- Passes chat state to EmbeddableChat
|
||||
|
||||
**Props**:
|
||||
```typescript
|
||||
type ChatPanelProps = {
|
||||
chat: ReturnType<typeof usePi>;
|
||||
availableModels?: ModelOption[];
|
||||
};
|
||||
```
|
||||
|
||||
**Dependencies**:
|
||||
- `usePi` hook (WebSocket connection)
|
||||
- `useChatSessions` hook (session data)
|
||||
- `useSlashCommands` hook (command handling)
|
||||
|
||||
---
|
||||
|
||||
### 2. EmbeddableChat
|
||||
|
||||
**File**: `Chat/EmbeddableChat.tsx`
|
||||
|
||||
**Purpose**: Reusable, embeddable chat widget used in both ChatPanel and ChatDetailPanel.
|
||||
|
||||
**Key Features**:
|
||||
- Message display with streaming text
|
||||
- Input area with attachment handling
|
||||
- Jump-to-bottom button when user scrolls up
|
||||
- Auto-scroll to latest message (unless user scrolled)
|
||||
- Textarea auto-resize
|
||||
- Webpage scraping and image upload
|
||||
- Before-send hook for slash commands
|
||||
|
||||
**Key Refs**:
|
||||
- `scrollViewportRef`: Track scroll position
|
||||
- `bottomRef`: Auto-scroll target
|
||||
- `userScrolledRef`: Track if user manually scrolled
|
||||
- `textareaRef`: Direct textarea access
|
||||
|
||||
**Props**:
|
||||
```typescript
|
||||
type EmbeddableChatProps = {
|
||||
chat: ReturnType<typeof usePi>;
|
||||
availableModels?: ModelOption[];
|
||||
onBeforeSend?: (text: string) => boolean | Promise<boolean>;
|
||||
commandFeedback?: string | null;
|
||||
defaultInput?: string;
|
||||
promptPrefix?: string;
|
||||
className?: string;
|
||||
cwd?: { root?: string; path: string };
|
||||
autoSend?: boolean;
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. InputArea
|
||||
|
||||
**File**: `Chat/InputArea.tsx`
|
||||
|
||||
**Purpose**: Message input with advanced features (attachments, voice, model selection).
|
||||
|
||||
**Key Features**:
|
||||
- Textarea with dynamic height
|
||||
- Attachment dropdown (image, file, URL, PDF)
|
||||
- Voice recording to WAV format
|
||||
- Whisper transcription (hardcoded to `http://macmini:8178/inference`)
|
||||
- Paste-to-attach images
|
||||
- Model picker with provider selection
|
||||
- Command feedback display
|
||||
- Send/Stop button
|
||||
|
||||
**Voice Recording Pipeline**:
|
||||
1. Start MediaRecorder
|
||||
2. On stop: Convert to WAV format
|
||||
3. POST to Whisper: `http://macmini:8178/inference`
|
||||
4. Append transcribed text to input
|
||||
|
||||
**Dependencies**:
|
||||
- `sonner` toast notifications
|
||||
- `lucide-react` icons
|
||||
- Settings component (model picker)
|
||||
|
||||
---
|
||||
|
||||
### 4. usePi Hook
|
||||
|
||||
**File**: `Chat/usePi.ts`
|
||||
|
||||
**Purpose**: Central WebSocket connection to Pi harness with message handling.
|
||||
|
||||
**Key Features**:
|
||||
- Establishes WebSocket connection to `/api/pi/chat/ws`
|
||||
- Manages message streaming with requestAnimationFrame batching
|
||||
- Handles all ServerMessage types
|
||||
- Session initialization and resume
|
||||
- Debounced message saving (1s)
|
||||
- Model and CWD tracking
|
||||
|
||||
**Message Type Handling**:
|
||||
|
||||
| Server Message | Handler | Action |
|
||||
|----------------|---------|--------|
|
||||
| `session:init` | Sets sessionId, model, cwd; updates URL |
|
||||
| `assistant:delta` | Accumulates in streaming buffer |
|
||||
| `assistant:text` | Commits streaming or adds as-is |
|
||||
| `tool:start` | Creates tool execution message |
|
||||
| `tool:result` | Updates tool message with output |
|
||||
| `result` | Commits streaming, adds cost metadata, sets isGenerating=false |
|
||||
| `sync:messages` | Restores full message history (resume) |
|
||||
| `error` | Adds error message, sets isGenerating=false |
|
||||
| `stopped` | Commits streaming, sets isGenerating=false |
|
||||
|
||||
**Streaming Optimization**:
|
||||
- Uses refs (`streamingRef`) to avoid state thrashing
|
||||
- RequestAnimationFrame batches updates
|
||||
- Commits on message boundaries or generation end
|
||||
|
||||
**Options**:
|
||||
```typescript
|
||||
type UsePiOptions = {
|
||||
replaceUrl?: boolean; // auto-update URL on session init
|
||||
storage?: ResourceChatStorage; // custom storage provider
|
||||
resourceChatDir?: string; // for resource/task chats
|
||||
taskInfo?: TaskInfo; // task metadata
|
||||
};
|
||||
```
|
||||
|
||||
**Return Type**:
|
||||
```typescript
|
||||
{
|
||||
messages: ChatMessage[];
|
||||
streamingText: string;
|
||||
isConnected: boolean;
|
||||
isGenerating: boolean;
|
||||
sessionId: string | null;
|
||||
model: string | null;
|
||||
selectedModel: string | null;
|
||||
cwd: string | null;
|
||||
setSelectedModel: (modelId: string) => void;
|
||||
sendPrompt: (text, attachmentIds?, images?, cwd?, groupSlug?) => void;
|
||||
stopGeneration: () => void;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. Settings Component
|
||||
|
||||
**File**: `Chat/Settings.tsx`
|
||||
|
||||
**Purpose**: Model selection UI with provider filtering.
|
||||
|
||||
**Key Features**:
|
||||
- Shows provider buttons before first message
|
||||
- Switches to dropdown display after first message
|
||||
- Groups models by provider
|
||||
- Display names for well-known providers
|
||||
|
||||
**Display Names**:
|
||||
```
|
||||
anthropic → "Anthropic"
|
||||
openai → "OpenAI"
|
||||
opencode → "OpenCode Zen"
|
||||
google → "Google"
|
||||
groq → "Groq"
|
||||
... (14 total providers defined)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. SessionList (Screen.tsx)
|
||||
|
||||
**File**: `ChatHistory/Screen.tsx`
|
||||
|
||||
**Purpose**: Tree view showing all sessions organized by groups.
|
||||
|
||||
**Key Features**:
|
||||
- Ungrouped sessions at top
|
||||
- Collapsible group sections
|
||||
- Session count badges
|
||||
- Jump to selected session on render
|
||||
- Create group and new chat buttons
|
||||
|
||||
**Structure**:
|
||||
```
|
||||
Sessions (Header)
|
||||
├─ New Chat (button)
|
||||
└─ New Group (button)
|
||||
|
||||
Ungrouped Sessions
|
||||
├─ Session 1
|
||||
│ ├─ Title + ID (truncated)
|
||||
│ ├─ Created date/time
|
||||
│ ├─ Model name
|
||||
│ └─ Context menu
|
||||
|
||||
Groups (sorted by updatedAt desc)
|
||||
├─ 📁 Group Name (5 sessions)
|
||||
│ ├─ Collapse/expand chevron
|
||||
│ ├─ Group context menu
|
||||
│ └─ Sessions (nested, indented)
|
||||
│ ├─ Session A
|
||||
│ ├─ Session B
|
||||
│ └─ Context menu per session
|
||||
```
|
||||
|
||||
**Styling**:
|
||||
- Selected session: `border-duck-teal/30 bg-duck-teal/5`
|
||||
- Unselected: `border-duck-dark/10 bg-background/80`
|
||||
|
||||
---
|
||||
|
||||
### 7. ChatDetailPanel
|
||||
|
||||
**File**: `ChatHistory/ChatDetailPanel.tsx`
|
||||
|
||||
**Purpose**: Right panel showing either existing session or new chat.
|
||||
|
||||
**Key Features**:
|
||||
- Session detail bar (title, delete, status indicators)
|
||||
- Switches between SessionChat and NewChat components
|
||||
- Handles location.state for initial message/prefill
|
||||
- Syncs selected session via panel channel
|
||||
|
||||
**Panel States**:
|
||||
1. No session selected: Empty state message
|
||||
2. Selected existing session: Load and display conversation
|
||||
3. Selected 'new': New chat form with optional pre-fill
|
||||
|
||||
**Status Indicators**:
|
||||
- Red dot: Disconnected
|
||||
- Orange pulsing dot: Generating
|
||||
- Green dot: Ready
|
||||
|
||||
---
|
||||
|
||||
### 8. Group & Session Context Menus
|
||||
|
||||
**GroupContextMenu** (`ChatHistory/GroupContextMenu.tsx`):
|
||||
- Rename group (inline edit)
|
||||
- Delete group (confirm dialog)
|
||||
- Shows sessions will be ungrouped on delete
|
||||
|
||||
**SessionContextMenu** (`ChatHistory/SessionContextMenu.tsx`):
|
||||
- Move to group (submenu)
|
||||
- Rename session (inline edit)
|
||||
- Delete session (direct action)
|
||||
- Move submenu shows ungrouped + all groups
|
||||
|
||||
---
|
||||
|
||||
### 9. CreateGroupDialog
|
||||
|
||||
**File**: `ChatHistory/CreateGroupDialog.tsx`
|
||||
|
||||
**Purpose**: Modal for creating new session groups.
|
||||
|
||||
**Features**:
|
||||
- Name input (required)
|
||||
- Description input (optional)
|
||||
- Auto-generated slug (lowercase, kebab-case)
|
||||
- Form validation
|
||||
- Error display
|
||||
- Loading state
|
||||
|
||||
**Slug Generation**:
|
||||
```javascript
|
||||
name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-') // Replace non-alphanumeric with hyphens
|
||||
.replace(/^-|-$/g, '') // Remove leading/trailing hyphens
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## State Management
|
||||
|
||||
### useChatSessions Hook
|
||||
|
||||
**File**: `state/useChatSessions.ts`
|
||||
|
||||
**REST Endpoints**:
|
||||
- `POST /pi/sessions` - List all sessions for user
|
||||
- `GET /pi/sessions/{sessionId}` - Get session with messages
|
||||
- `PUT /pi/sessions/{sessionId}/messages` - Save messages (debounced)
|
||||
- `PATCH /pi/sessions/{sessionId}` - Rename session
|
||||
- `DELETE /pi/sessions/{sessionId}` - Delete session
|
||||
- `GET /pi/sessions/search?q={query}` - Search sessions
|
||||
|
||||
**Query Keys**:
|
||||
- `['PI_SESSIONS']` - Invalidated on any session change
|
||||
|
||||
---
|
||||
|
||||
### useChatGroups Hook
|
||||
|
||||
**File**: `state/useChatGroups.ts`
|
||||
|
||||
**REST Endpoints**:
|
||||
- `GET /pi/groups` - List all groups for user
|
||||
- `POST /pi/groups` - Create new group
|
||||
- `PATCH /pi/groups/{slug}` - Update group (name, description)
|
||||
- `DELETE /pi/groups/{slug}` - Delete group (moves sessions to root)
|
||||
- `POST /pi/sessions/{sessionId}/move` - Move session to/from group
|
||||
|
||||
**Query Keys**:
|
||||
- `['PI_GROUPS']` - Invalidated on group changes
|
||||
- Invalidates `['PI_SESSIONS']` on move/create/delete
|
||||
|
||||
---
|
||||
|
||||
### useModels Hook
|
||||
|
||||
**File**: `state/useModels.ts`
|
||||
|
||||
**Functions**:
|
||||
- `usePiModels()` - Get all available models
|
||||
- `useVisiblePiModels()` - Filter by enabled models in settings
|
||||
|
||||
**REST Endpoint**:
|
||||
- `GET /pi/models` - List all models
|
||||
|
||||
**Query Key**: `['PI_MODELS']` (5-minute stale time)
|
||||
|
||||
---
|
||||
|
||||
## Type System
|
||||
|
||||
### Core Types
|
||||
|
||||
**SessionEntry**:
|
||||
```typescript
|
||||
{
|
||||
id: string;
|
||||
title: string;
|
||||
model: string;
|
||||
cwd: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
messageCount: number;
|
||||
cost: MessageCost;
|
||||
groupSlug?: string | null;
|
||||
}
|
||||
```
|
||||
|
||||
**GroupEntry**:
|
||||
```typescript
|
||||
{
|
||||
name: string;
|
||||
slug: string;
|
||||
description?: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
sessionCount: number;
|
||||
}
|
||||
```
|
||||
|
||||
**ChatMessage** (frontend message):
|
||||
```typescript
|
||||
| { role: 'user'; text: string; images?: [...] }
|
||||
| { role: 'assistant'; text: string }
|
||||
| { role: 'system'; text: string }
|
||||
| {
|
||||
role: 'tool';
|
||||
toolName: string;
|
||||
toolInput: Record<string, unknown>;
|
||||
toolCallId: string;
|
||||
output?: string;
|
||||
isError?: boolean;
|
||||
}
|
||||
| { role: 'result'; cost: MessageCost }
|
||||
| { role: 'error'; text: string }
|
||||
```
|
||||
|
||||
**ServerMessage** (WebSocket protocol):
|
||||
```typescript
|
||||
| { type: 'session:init'; sessionId: string; model: string; cwd: string }
|
||||
| { type: 'assistant:text'; text: string }
|
||||
| { type: 'assistant:delta'; text: string }
|
||||
| { type: 'tool:start'; toolCallId: string; toolName: string; toolInput: ... }
|
||||
| { type: 'tool:result'; toolCallId: string; output: string; isError: boolean }
|
||||
| { type: 'result'; sessionId: string; cost: MessageCost }
|
||||
| { type: 'sync:messages'; sessionId: string; messages: Message[]; isGenerating: boolean; streamingText: string }
|
||||
| { type: 'error'; message: string; errorCode?: string }
|
||||
| { type: 'stopped' }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Flow
|
||||
|
||||
### New Chat
|
||||
|
||||
```
|
||||
User clicks "New Chat"
|
||||
↓
|
||||
SessionListPage sets selected.id = 'new'
|
||||
↓
|
||||
ChatDetailPanel renders NewChat component
|
||||
↓
|
||||
NewChat creates usePi() hook (no sessionId)
|
||||
↓
|
||||
usePi connects to WebSocket, sends first prompt
|
||||
↓
|
||||
Backend spawns Pi process, sends 'session:init'
|
||||
↓
|
||||
usePi updates sessionId, URL changes to /chat/{sessionId}
|
||||
↓
|
||||
SessionListPage re-renders, session appears in list
|
||||
↓
|
||||
Chat continues normally
|
||||
```
|
||||
|
||||
### Resume Existing Session
|
||||
|
||||
```
|
||||
User clicks session in SessionList
|
||||
↓
|
||||
SessionListPage.useEffect sets selected.id = sessionId
|
||||
↓
|
||||
ChatDetailPanel renders SessionChat with sessionId
|
||||
↓
|
||||
SessionChat calls usePi(sessionId) hook
|
||||
↓
|
||||
usePi.useEffect calls getSession(sessionId)
|
||||
↓
|
||||
Loads messages from server, populates local state
|
||||
↓
|
||||
User sends first new message
|
||||
↓
|
||||
usePi sends prompt with sessionId to WebSocket
|
||||
↓
|
||||
Backend loads session from disk, injects history in system prompt
|
||||
↓
|
||||
Pi process continues with context
|
||||
```
|
||||
|
||||
### Model Switching
|
||||
|
||||
```
|
||||
User selects different model in Settings
|
||||
↓
|
||||
onChange handler calls setSelectedModel(modelId)
|
||||
↓
|
||||
Next sendPrompt() includes { model: selectedModel }
|
||||
↓
|
||||
WebSocket sends to backend with model ID
|
||||
↓
|
||||
New Pi process spawned with specified model
|
||||
↓
|
||||
session:init message returns new model
|
||||
```
|
||||
|
||||
### Attachment Upload
|
||||
|
||||
```
|
||||
User clicks image/URL in InputArea
|
||||
↓
|
||||
EmbeddableChat.handleAttach{Image|Webpage}()
|
||||
↓
|
||||
POST /scrape or /upload with sessionId/provider
|
||||
↓
|
||||
Attachment added to state with loading=true
|
||||
↓
|
||||
Response includes attachmentId
|
||||
↓
|
||||
Attachment loading=false, displays thumbnail
|
||||
↓
|
||||
User sends message
|
||||
↓
|
||||
Attachment prepended to prompt text
|
||||
↓
|
||||
Server relocates file to session directory
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Patterns
|
||||
|
||||
### 1. Streaming Text Optimization
|
||||
|
||||
```typescript
|
||||
const streamingRef = useRef('');
|
||||
const rafRef = useRef<number | null>(null);
|
||||
|
||||
function flushStreaming() {
|
||||
if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
|
||||
rafRef.current = requestAnimationFrame(() => {
|
||||
setStreamingText(streamingRef.current);
|
||||
rafRef.current = null;
|
||||
});
|
||||
}
|
||||
|
||||
// On delta: accumulate + flush
|
||||
streamingRef.current += msg.text;
|
||||
flushStreaming();
|
||||
|
||||
// On message end: commit to messages
|
||||
function commitStreaming() {
|
||||
if (!streamingRef.current) return;
|
||||
setMessages((prev) => [...prev, { role: 'assistant', text: streamingRef.current }]);
|
||||
streamingRef.current = '';
|
||||
setStreamingText('');
|
||||
}
|
||||
```
|
||||
|
||||
**Benefit**: Avoids rendering on every character; batches updates with RAF
|
||||
|
||||
### 2. Auto-Scroll Detection
|
||||
|
||||
```typescript
|
||||
const userScrolledRef = useRef(false);
|
||||
|
||||
const handleScroll = () => {
|
||||
const { scrollTop, scrollHeight, clientHeight } = viewport;
|
||||
const atBottom = scrollHeight - scrollTop - clientHeight < 60;
|
||||
userScrolledRef.current = !atBottom;
|
||||
setShowJumpToBottom(!atBottom);
|
||||
};
|
||||
|
||||
// Auto-scroll only if user hasn't scrolled up
|
||||
useEffect(() => {
|
||||
if (!userScrolledRef.current) {
|
||||
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}, [messages, streamingText]);
|
||||
```
|
||||
|
||||
**Benefit**: Respects user's scroll position; auto-scrolls only when at bottom
|
||||
|
||||
### 3. Textarea Auto-Resize
|
||||
|
||||
```typescript
|
||||
useEffect(() => {
|
||||
const textarea = textareaRef.current;
|
||||
if (!textarea) return;
|
||||
textarea.style.height = 'auto';
|
||||
textarea.style.height = Math.min(textarea.scrollHeight, 200) + 'px';
|
||||
}, [input]);
|
||||
```
|
||||
|
||||
**Behavior**: Grows up to 200px, then scrolls internally
|
||||
|
||||
### 4. Debounced Save
|
||||
|
||||
```typescript
|
||||
useEffect(() => {
|
||||
if (!sessionIdRef.current || messages.length === 0) return;
|
||||
|
||||
if (saveTimerRef.current !== null) clearTimeout(saveTimerRef.current);
|
||||
|
||||
const sid = sessionIdRef.current;
|
||||
const snapshot = messages;
|
||||
saveTimerRef.current = window.setTimeout(() => {
|
||||
saveMessages(sid, snapshot);
|
||||
saveTimerRef.current = null;
|
||||
}, SAVE_DEBOUNCE_MS);
|
||||
|
||||
return () => {
|
||||
if (saveTimerRef.current !== null) clearTimeout(saveTimerRef.current);
|
||||
};
|
||||
}, [messages]);
|
||||
```
|
||||
|
||||
**Benefit**: Saves after 1s of no message changes; avoids spam
|
||||
|
||||
---
|
||||
|
||||
## Hardcoded Configuration
|
||||
|
||||
### Whisper Transcription
|
||||
|
||||
**Location**: `InputArea.tsx:blobToWav()` and `handleMicClick()`
|
||||
|
||||
**Endpoint**: `http://macmini:8178/inference`
|
||||
|
||||
**Status**: ⚠️ Hardcoded - needs parameterization
|
||||
|
||||
### Session Idle Timeout
|
||||
|
||||
**Location**: Backend (see PI_HARNESS_REBUILD.md)
|
||||
|
||||
**Value**: 1 hour
|
||||
|
||||
---
|
||||
|
||||
## Known Issues & TODOs
|
||||
|
||||
1. **OpenCodeModelPicker**: Still imported in InputArea but Settings component handles model selection. Can remove if consolidation complete.
|
||||
|
||||
2. **Whisper endpoint**: Hardcoded to `http://macmini:8178`. Should be configurable.
|
||||
|
||||
3. **ChatList placeholder**: `Chat/ChatList/index.tsx` is just a placeholder, contains no actual implementation.
|
||||
|
||||
4. **Legacy type aliases**: In `types.ts` - marked for removal after Phase 9 cleanup.
|
||||
|
||||
5. **Panel channel**: Uses string key `'chat:selected-session'` for panel synchronization. Could be more type-safe.
|
||||
|
||||
---
|
||||
|
||||
## Integration Points
|
||||
|
||||
### WebSocket (`usePi.ts`)
|
||||
|
||||
**URL**: `/api/pi/chat/ws?token={token}`
|
||||
|
||||
**Protocol**: See `ServerMessage` types above
|
||||
|
||||
**Lifecycle**:
|
||||
1. Open: empty
|
||||
2. Message: handle ServerMessage
|
||||
3. Close: session stays in memory with idle timeout
|
||||
4. Error: display error message
|
||||
|
||||
### REST API
|
||||
|
||||
**Base**: `/api/pi/`
|
||||
|
||||
**Endpoints**:
|
||||
- Sessions: `/sessions` (list), `/sessions/{id}` (get/update/delete), `/sessions/search` (search), `/sessions/{id}/move` (move)
|
||||
- Groups: `/groups` (list/create), `/groups/{slug}` (update/delete)
|
||||
- Models: `/models` (list)
|
||||
|
||||
**Auth**: Bearer token in Authorization header
|
||||
|
||||
### Storage
|
||||
|
||||
**Cache Keys**:
|
||||
- `PI_SESSIONS`: SessionEntry[] array
|
||||
- `PI_GROUPS`: GroupEntry[] array
|
||||
- `PI_MODELS`: ModelOption[] array
|
||||
|
||||
**Persistence**: TanStack Query with localStorage (default)
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] New chat creates session on backend
|
||||
- [ ] Resume chat loads messages from server
|
||||
- [ ] Model switching sends to backend
|
||||
- [ ] Streaming text appears correctly
|
||||
- [ ] Tool execution tracked properly
|
||||
- [ ] Session rename works
|
||||
- [ ] Session delete removes from list
|
||||
- [ ] Create group works
|
||||
- [ ] Move session to group updates counts
|
||||
- [ ] Delete group moves sessions to root
|
||||
- [ ] Collapse/expand groups works
|
||||
- [ ] Fullscreen toggle works
|
||||
- [ ] Voice recording transcribes to input
|
||||
- [ ] Image paste/upload works
|
||||
- [ ] Webpage scrape/attach works
|
||||
- [ ] Jump-to-bottom button appears when scrolled
|
||||
- [ ] Auto-scroll respects user scroll
|
||||
- [ ] Messages debounce save correctly
|
||||
- [ ] Panel sync between components works
|
||||
- [ ] URL updates on session init
|
||||
|
||||
---
|
||||
|
||||
## Future Refactoring Opportunities
|
||||
|
||||
1. **Extract message handlers**: Move ServerMessage handlers to separate utility file
|
||||
2. **Consolidate model selection**: Remove OpenCodeModelPicker, use only Settings
|
||||
3. **Streaming abstraction**: Create reusable streaming hook for other features
|
||||
4. **Panel channel typing**: Create typed version of usePanelChannel
|
||||
5. **Attachment architecture**: Unified attachment service (currently inline)
|
||||
6. **Voice transcription**: Move Whisper into dedicated service with configurable endpoint
|
||||
7. **Message components**: Extract common patterns from MessageBubble, ToolActivity, QuestionActivity
|
||||
8. **Group UI refinement**: Drag-and-drop reordering, nested group support
|
||||
9. **Search refinement**: Full-text search, filter by date/model/group
|
||||
10. **Cost tracking**: Display cumulative cost per session/group
|
||||
|
||||
---
|
||||
|
||||
## Integration Status
|
||||
|
||||
### ✅ Filled Instances
|
||||
|
||||
All Chat components have been properly integrated throughout the officer-web application:
|
||||
|
||||
1. **Chat/index.tsx** - Now exports:
|
||||
- `ChatPanel` (main chat container)
|
||||
- `EmbeddableChat` (reusable chat widget)
|
||||
- `Attachment` type
|
||||
- `InputArea` (message input)
|
||||
- `Settings` (model selector)
|
||||
- `usePi` (WebSocket hook)
|
||||
- `ChatList` (session list)
|
||||
|
||||
2. **ChatList Component** - Fully implemented:
|
||||
- Displays all sessions
|
||||
- Shows creation date/time
|
||||
- Shows model name
|
||||
- Links to individual sessions
|
||||
|
||||
3. **ChatHistory/index.tsx** - Exports:
|
||||
- `SessionListPage` (main page component)
|
||||
- `SessionList` (session tree view)
|
||||
- `ChatHistory` (sidebar widget)
|
||||
|
||||
4. **App Registry Integration** - Fully configured:
|
||||
- ✅ `chat` - ChatPanel widget
|
||||
- ✅ `chat-history` - SessionHistory navigation
|
||||
- ✅ `chat-launcher` - Quick chat launcher
|
||||
- All registered in Workspaces/app-registry.tsx
|
||||
|
||||
5. **Dashboard Routes** - Complete:
|
||||
- ✅ `/chat` - SessionListPage root
|
||||
- ✅ `/chat/new` - New chat
|
||||
- ✅ `/chat/:sessionId` - Resume session
|
||||
- All wired to SessionListPage
|
||||
|
||||
6. **Integration Points**:
|
||||
- ✅ ChatPanel (ChatHistory/ChatDetailPanel.tsx)
|
||||
- ✅ EmbeddableChat (Files/TaskRunnerModal.tsx)
|
||||
- ✅ usePi hook (CapabilityPage.tsx)
|
||||
- ✅ Model selection (Settings.tsx)
|
||||
- ✅ Task runner modal (Files/Screen/TaskRunnerModal.tsx)
|
||||
- ✅ Automation edit chat (Automation/AutomationEditChat.tsx)
|
||||
|
||||
### Type Exports
|
||||
|
||||
All Chat-related types properly exported from `workspaces/apps/Chat/types.ts`:
|
||||
- ✅ `ChatMessage` (union type for all message variants)
|
||||
- ✅ `ServerMessage` (WebSocket protocol types)
|
||||
- ✅ `Message` (backend message format)
|
||||
- ✅ `SessionEntry` (session metadata)
|
||||
- ✅ `GroupEntry` (group metadata)
|
||||
- ✅ `MessageCost` (cost tracking)
|
||||
- ✅ `ModelOption` (model definition)
|
||||
- ✅ `TaskInfo` (task metadata)
|
||||
|
||||
### State Management
|
||||
|
||||
All state hooks properly integrated:
|
||||
- ✅ `useChatSessions` - REST endpoints for sessions
|
||||
- ✅ `useChatGroups` - REST endpoints for groups
|
||||
- ✅ `usePiModels` - Model listing
|
||||
- ✅ `useVisiblePiModels` - Filtered models by settings
|
||||
|
||||
### Compilation Status
|
||||
|
||||
✅ No Chat-related TypeScript errors
|
||||
✅ All imports resolve correctly
|
||||
✅ Type definitions align with WebSocket protocol
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
The Chat & ChatHistory apps form a cohesive system for conversational AI interaction:
|
||||
|
||||
- **Chat** components handle real-time messaging and user input
|
||||
- **ChatHistory** components manage session persistence and organization
|
||||
- **usePi** hook bridges frontend and Pi harness backend
|
||||
- **State hooks** provide clean REST API abstraction
|
||||
- **Type system** ensures message protocol consistency
|
||||
- **Streaming optimization** prevents UI thrashing
|
||||
- **Grouping feature** organizes conversations logically
|
||||
- **All instances filled** and properly integrated across the dashboard
|
||||
|
||||
The architecture is clean, modular, and fully integrated throughout the application. Ready for the major refactor.
|
||||
Reference in New Issue
Block a user