docs: add ChatPanel unification design spec
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,216 @@
|
|||||||
|
# ChatPanel Unification Design
|
||||||
|
|
||||||
|
**Date:** 2026-04-03
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Replace the four divergent chat surfaces (ChatView, BriefingView, WorkspaceView, HomeView widget) with a single `ChatPanel` component that encapsulates all chat behaviour — streaming, TTS, PTT, tool calls, thinking blocks, abort — so that fixes and features automatically apply to every context.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
The app currently has four independent chat implementations that have drifted significantly:
|
||||||
|
|
||||||
|
| Surface | File | Gap |
|
||||||
|
|---|---|---|
|
||||||
|
| Main chat | `ChatView.vue` | Canonical reference |
|
||||||
|
| Briefing | `BriefingView.vue` | Had separate TTS impl (now fixed), no PTT, streaming race bug |
|
||||||
|
| Workspace | `WorkspaceView.vue` | TTS missing until recently, different input wiring |
|
||||||
|
| Dashboard widget | `HomeView.vue` + `DashboardChatInput.vue` | Separate input component, response rendered manually in parent, no TTS, no PTT |
|
||||||
|
|
||||||
|
Every fix to chat has required touching 3–4 files. This design makes chat a first-class component.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Component: `ChatPanel.vue`
|
||||||
|
|
||||||
|
A single Vue 3 component that owns the entire chat interaction loop for a given conversation context. Two variants controlled by a `variant` prop:
|
||||||
|
|
||||||
|
- **`full`** — full-height chat: message history, streaming bubble, input bar, all controls
|
||||||
|
- **`widget`** — compact embedded chat: input bar + compact response area, no history scroll
|
||||||
|
|
||||||
|
Both variants share identical internals: same composables, same store reads, same TTS/PTT/abort logic.
|
||||||
|
|
||||||
|
### Extracted Sub-components
|
||||||
|
|
||||||
|
| Component | Responsibility |
|
||||||
|
|---|---|
|
||||||
|
| `ChatInputBar.vue` | Unified input bar: textarea, note picker, PTT mic, send button, abort button |
|
||||||
|
| `ChatMessageList.vue` | Scrollable message history with auto-scroll, bulk-select (full variant only) |
|
||||||
|
| `ChatStreamingBubble.vue` | Live streaming content display + thinking block |
|
||||||
|
| `ChatToolCallList.vue` | Tool call cards, collapsed/expanded state |
|
||||||
|
|
||||||
|
### State Ownership
|
||||||
|
|
||||||
|
`ChatPanel` reads from `useChatStore` directly — it does not accept messages or streaming state as props. This mirrors how all current views work and avoids prop-drilling re-implementation.
|
||||||
|
|
||||||
|
The conversation being displayed is controlled via a `convId` prop. When `convId` is undefined, `ChatPanel` uses `chatStore.currentConversationId`. The parent view sets up the conversation (creates it if needed) and passes the ID down.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Props & Emits Interface
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ChatPanelProps {
|
||||||
|
variant: 'full' | 'widget'
|
||||||
|
convId?: number // which conversation to display; undefined = store current
|
||||||
|
projectId?: number // workspace: pins RAG scope, passed to sendMessage
|
||||||
|
briefingMode?: boolean // briefing: hides RAG scope chip, enables briefing-specific send path
|
||||||
|
placeholder?: string // input placeholder text
|
||||||
|
autoFocus?: boolean // focus input on mount
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChatPanelEmits {
|
||||||
|
// Emitted when a new conversation is started from the widget (so parent can track convId)
|
||||||
|
(e: 'conversation-started', convId: number): void
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
All other behaviour (TTS, PTT, thinking, tool calls, streaming indicator, abort) is always on — not gated by props. The intentional differences between views are expressed only through the props above.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Variant Behaviour
|
||||||
|
|
||||||
|
### `variant="full"` (ChatView, BriefingView, WorkspaceView)
|
||||||
|
|
||||||
|
Layout (top to bottom):
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────┐
|
||||||
|
│ [RAG scope chip / briefing header] │ ← shown unless briefingMode or projectId set
|
||||||
|
│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │
|
||||||
|
│ ChatMessageList │
|
||||||
|
│ user bubble │
|
||||||
|
│ assistant bubble + tool calls │
|
||||||
|
│ thinking block (always shown) │
|
||||||
|
│ ... │
|
||||||
|
│ ChatStreamingBubble (while streaming) │
|
||||||
|
│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │
|
||||||
|
│ ChatInputBar │
|
||||||
|
│ [textarea] [note-picker] [mic] [▶] │
|
||||||
|
│ [listen toggle] [abort] │
|
||||||
|
└────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### `variant="widget"` (HomeView dashboard)
|
||||||
|
|
||||||
|
Layout (top to bottom, compact):
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────┐
|
||||||
|
│ ChatInputBar (pill style) │
|
||||||
|
│ [textarea] [mic] [▶] │
|
||||||
|
├────────────────────────────────────────┤
|
||||||
|
│ [query text] (after send) │
|
||||||
|
│ [streaming / final response text] │
|
||||||
|
│ [tool call chips] │
|
||||||
|
│ [Continue in Chat →] │
|
||||||
|
└────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
The widget variant does NOT show full message history. It shows only the most recent exchange. Once a new conversation is started or the user navigates to `/chat/:id`, the full history is available.
|
||||||
|
|
||||||
|
The `.dashboard-response` section currently in `HomeView.vue` moves inside `ChatPanel` and is rendered when `variant="widget"` and a conversation exists.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TTS / PTT Wiring
|
||||||
|
|
||||||
|
`ChatPanel` instantiates `useStreamingTts` and `useListenMode` internally. These are not passed as props.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Inside ChatPanel setup()
|
||||||
|
const listenMode = useListenMode()
|
||||||
|
const voiceTtsEnabled = computed(() => /* same check as current views */)
|
||||||
|
const tts = useStreamingTts({
|
||||||
|
streamingContent: computed(() => chatStore.streamingContent),
|
||||||
|
streaming: computed(() => !!chatStore.streaming),
|
||||||
|
enabled: computed(() => listenMode.value && voiceTtsEnabled.value),
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
PTT is handled inside `ChatInputBar` via the existing `useVoiceRecorder` composable (already used in `DashboardChatInput`). On recording stop, the transcribed text is placed in the textarea and auto-submitted.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Per-View Migration
|
||||||
|
|
||||||
|
### ChatView → `<ChatPanel variant="full">`
|
||||||
|
- Remove: all TTS/PTT/streaming/abort logic, scroll management, input bar template
|
||||||
|
- Keep: route wiring, conversation list sidebar, bulk-delete UI (sidebar stays in ChatView)
|
||||||
|
- ChatPanel replaces only the right-hand panel
|
||||||
|
|
||||||
|
### BriefingView → `<ChatPanel variant="full" briefingMode />`
|
||||||
|
- Remove: streaming watch, TTS, manual scroll, input bar, response persistence workaround
|
||||||
|
- Keep: history dropdown (today / past briefings), date header
|
||||||
|
- `briefingMode` hides the RAG scope chip
|
||||||
|
|
||||||
|
### WorkspaceView → `<ChatPanel variant="full" :projectId="projectId">`
|
||||||
|
- Remove: inline chat input, streaming watch, TTS wiring
|
||||||
|
- Keep: 3-panel grid layout, task panel, note editor panel
|
||||||
|
- ChatPanel takes the centre column
|
||||||
|
|
||||||
|
### HomeView → `<ChatPanel variant="widget">`
|
||||||
|
- Remove: `DashboardChatInput` import + usage, `.dashboard-response` section, all manual store wiring (`dashboardConvId`, `dashboardQuery`, `dashboardFinalContent`, `dashboardFinalToolCalls`, `onChatSubmit`)
|
||||||
|
- Keep: dashboard layout, projects/tasks/events sections
|
||||||
|
- `DashboardChatInput.vue` deleted
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
Parent view
|
||||||
|
└─ <ChatPanel :convId="convId" variant="full|widget">
|
||||||
|
├─ reads: useChatStore (messages, streaming, streamingContent, currentConversation)
|
||||||
|
├─ ChatMessageList — renders history from store
|
||||||
|
├─ ChatStreamingBubble — renders chatStore.streamingContent while streaming
|
||||||
|
├─ ChatToolCallList — renders tool calls from streaming + finalized messages
|
||||||
|
├─ ChatInputBar
|
||||||
|
│ ├─ usePtt (mic → textarea → auto-send)
|
||||||
|
│ └─ emits: submit(content, contextNoteId)
|
||||||
|
├─ useStreamingTts (sentence-chunk TTS during streaming)
|
||||||
|
└─ useListenMode (shared global toggle)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files Created / Modified
|
||||||
|
|
||||||
|
**Created:**
|
||||||
|
- `frontend/src/components/ChatPanel.vue`
|
||||||
|
- `frontend/src/components/ChatInputBar.vue`
|
||||||
|
- `frontend/src/components/ChatMessageList.vue`
|
||||||
|
- `frontend/src/components/ChatStreamingBubble.vue`
|
||||||
|
- (no new composable needed — PTT uses existing `useVoiceRecorder.ts`)
|
||||||
|
|
||||||
|
**Modified:**
|
||||||
|
- `frontend/src/views/ChatView.vue` — use ChatPanel for the chat area
|
||||||
|
- `frontend/src/views/BriefingView.vue` — replace chat section with ChatPanel
|
||||||
|
- `frontend/src/views/WorkspaceView.vue` — replace inline chat with ChatPanel
|
||||||
|
- `frontend/src/views/HomeView.vue` — replace DashboardChatInput + response section with ChatPanel widget
|
||||||
|
|
||||||
|
**Deleted:**
|
||||||
|
- `frontend/src/components/DashboardChatInput.vue`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CSS / Styling
|
||||||
|
|
||||||
|
- `ChatPanel` carries its own scoped CSS for both variants
|
||||||
|
- `ChatInputBar` replicates the pill style currently in `DashboardChatInput` and the flat style in `ChatView` — variant is controlled by a `pill` boolean prop (default false; widget sets it true)
|
||||||
|
- All existing UI design language tokens (`--color-primary`, `--radius-lg`, Fraunces labels, gradient send button) are preserved
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What Does NOT Change
|
||||||
|
|
||||||
|
- Chat store (`useChatStore`) — unchanged
|
||||||
|
- API client (`client.ts`) — unchanged
|
||||||
|
- Backend routes — unchanged
|
||||||
|
- WorkspaceTaskPanel and WorkspaceNoteEditor — unchanged
|
||||||
|
- Briefing history dropdown and date header — unchanged
|
||||||
|
- ChatView conversation sidebar and bulk-delete — unchanged
|
||||||
|
- RAG scope chip logic — moved inside ChatPanel, behaviour identical
|
||||||
Reference in New Issue
Block a user