feat(06-02): design session chat component architecture

Create comprehensive component design for session chat following JamTrack
and Backing Track patterns:

- 8 components: JKSessionChatButton, JKSessionChatWindow, 6 sub-components
- WindowPortal integration for modeless dialog (reuse existing)
- Props vs Redux decision matrix (heavy Redux, minimal props)
- Auto-scroll logic for message list
- Enter-to-send keyboard handling for composer
- Integration plan with JKSessionScreen
- Component lifecycle patterns documented
- Testing strategy defined (unit, integration, E2E)

Component hierarchy:
- JKSessionChatButton (top nav with unread badge)
- JKSessionChatWindow (WindowPortal wrapper)
  - JKChatHeader
  - JKChatMessageList (with auto-scroll)
    - JKChatMessage (avatar, name, text, timestamp)
    - JKChatLoadingSpinner
    - JKChatEmptyState
  - JKChatComposer (textarea + send button)

File structure: jam-ui/src/components/client/chat/

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Nuwan 2026-01-26 15:46:15 +05:30
parent d839be3ca4
commit 192703f592
1 changed files with 954 additions and 0 deletions

View File

@ -0,0 +1,954 @@
# Session Chat Component Architecture Design
**Purpose:** Design React component hierarchy for session chat, following established jam-ui patterns from Backing Track and JamTrack.
**Date:** 2026-01-26
**Phase:** 06-session-chat-research-design
**Plan:** 02
---
## Overview
This document designs the component architecture for session chat in jam-ui, reusing existing patterns from Phase 3 (Backing Track) and Phase 5 (JamTrack), with WindowPortal for modeless dialog support.
**Key Design Principles:**
- Follow JamTrack player pattern: Main component with specialized sub-components
- Use WindowPortal for modeless chat window (already proven for Metronome, JamTrack)
- Heavy Redux integration for state management (minimal props)
- Clear separation: UI components vs data/behavior (Redux)
---
## Component Hierarchy
```
JKSessionChatButton (in session screen top nav)
├── Badge (unread count indicator)
└── onClick → dispatch(openChatWindow)
JKSessionChatWindow (WindowPortal wrapper)
├── WindowPortal (from jam-ui/src/components/common/WindowPortal.js)
│ └── Chat content (rendered inside popup window)
│ ├── JKChatHeader
│ │ ├── Title ("Session Chat")
│ │ └── Close button
│ ├── JKChatMessageList (scrollable container)
│ │ ├── JKChatLoadingSpinner (conditional: fetchStatus === 'loading')
│ │ ├── JKChatEmptyState (conditional: messages.length === 0)
│ │ └── JKChatMessage[] (repeated for each message)
│ │ ├── Avatar (user photo)
│ │ ├── Sender name
│ │ ├── Message text
│ │ └── Timestamp
│ └── JKChatComposer
│ ├── Textarea (message input)
│ ├── Character count (255 max)
│ └── Send button
```
---
## Component Specifications
### 1. JKSessionChatButton
**File:** `jam-ui/src/components/client/JKSessionChatButton.js`
**Purpose:** Chat button in session screen top nav, shows unread badge.
**Props:**
```javascript
{
// No props - all state from Redux
}
```
**Redux Selectors:**
```javascript
const unreadCount = useSelector(selectUnreadCount(sessionId));
const isWindowOpen = useSelector(state => state.sessionChat.isWindowOpen);
```
**Redux Actions:**
```javascript
dispatch(openChatWindow());
dispatch(closeChatWindow());
```
**Render:**
```jsx
<button
className="chat-button"
onClick={handleToggleChatWindow}
data-testid="chat-button"
>
<span className="chat-icon">💬</span>
<span className="chat-label">Chat</span>
{unreadCount > 0 && (
<span className="badge" data-testid="chat-badge">
{unreadCount}
</span>
)}
</button>
```
**State Ownership:**
- ✅ Redux: unreadCount, isWindowOpen
- ❌ Props: None
- ✅ Local: None (purely presentational)
**Integration Point:**
- Add to `JKSessionScreen` top navigation area (near mixer, metronome buttons)
- Position: Absolute or flex layout (TBD in implementation)
---
### 2. JKSessionChatWindow
**File:** `jam-ui/src/components/client/JKSessionChatWindow.js`
**Purpose:** Main chat window component, wraps WindowPortal for modeless dialog.
**Props:**
```javascript
{
// No props - all state from Redux
}
```
**Redux Selectors:**
```javascript
const isWindowOpen = useSelector(state => state.sessionChat.isWindowOpen);
const sessionId = useSelector(state => state.activeSession.sessionId);
const messages = useSelector(selectChatMessages(sessionId));
const fetchStatus = useSelector(state => state.sessionChat.fetchStatus[sessionId]);
const currentUser = useSelector(state => state.auth.user);
```
**Redux Actions:**
```javascript
dispatch(closeChatWindow());
dispatch(fetchChatHistory({ channel: sessionId, channelType: 'session', sessionId }));
dispatch(markAsRead({ channel: sessionId }));
```
**Lifecycle:**
```javascript
useEffect(() => {
if (isWindowOpen && sessionId) {
// Fetch message history on window open
dispatch(fetchChatHistory({ channel: sessionId, channelType: 'session', sessionId }));
// Mark messages as read
dispatch(markAsRead({ channel: sessionId }));
}
}, [isWindowOpen, sessionId, dispatch]);
```
**Render:**
```jsx
{isWindowOpen && (
<WindowPortal
windowFeatures="width=400,height=600,left=200,top=200,menubar=no,toolbar=no"
title="Session Chat"
onClose={handleClose}
windowId="session-chat"
>
<div className="chat-window-content">
<JKChatHeader />
<JKChatMessageList />
<JKChatComposer />
</div>
</WindowPortal>
)}
```
**State Ownership:**
- ✅ Redux: isWindowOpen, messages, fetchStatus
- ❌ Props: None
- ✅ Local: None (delegates to sub-components)
**Window Management:**
- Uses WindowPortal (no modifications needed)
- Dimensions: 400x600px (similar to Metronome popup)
- Position: User-configurable (WindowPortal handles)
- Auto-cleanup: WindowPortal handles on close
---
### 3. JKChatHeader
**File:** `jam-ui/src/components/client/chat/JKChatHeader.js`
**Purpose:** Chat window title bar with close button.
**Props:**
```javascript
{
onClose: () => void // Callback to close window
}
```
**Redux Selectors:**
```javascript
// None - purely presentational
```
**Render:**
```jsx
<div className="chat-header">
<h3 className="chat-title">Session Chat</h3>
<button
className="chat-close-button"
onClick={onClose}
aria-label="Close chat"
data-testid="chat-close"
>
×
</button>
</div>
```
**State Ownership:**
- ❌ Redux: None
- ✅ Props: onClose callback
- ✅ Local: None
**Styling:**
- Fixed height (e.g., 48px)
- Flex layout: title left, close button right
- Border-bottom separator
---
### 4. JKChatMessageList
**File:** `jam-ui/src/components/client/chat/JKChatMessageList.js`
**Purpose:** Scrollable message container with auto-scroll to bottom.
**Props:**
```javascript
{
// No props - all state from Redux
}
```
**Redux Selectors:**
```javascript
const sessionId = useSelector(state => state.activeSession.sessionId);
const messages = useSelector(selectChatMessages(sessionId)); // Sorted by created_at ASC
const fetchStatus = useSelector(state => state.sessionChat.fetchStatus[sessionId]);
const currentUser = useSelector(state => state.auth.user);
```
**Local State:**
```javascript
const [isUserScrolling, setIsUserScrolling] = useState(false);
const scrollContainerRef = useRef(null);
```
**Auto-Scroll Logic:**
```javascript
useEffect(() => {
if (!isUserScrolling && scrollContainerRef.current) {
// Auto-scroll to bottom on new messages
scrollContainerRef.current.scrollTo({
top: scrollContainerRef.current.scrollHeight,
behavior: 'smooth'
});
}
}, [messages.length, isUserScrolling]);
const handleScroll = (e) => {
const { scrollTop, scrollHeight, clientHeight } = e.target;
const isAtBottom = scrollHeight - scrollTop - clientHeight < 50;
setIsUserScrolling(!isAtBottom);
};
```
**Render:**
```jsx
<div
className="chat-message-list"
ref={scrollContainerRef}
onScroll={handleScroll}
data-testid="chat-message-list"
>
{fetchStatus === 'loading' && <JKChatLoadingSpinner />}
{fetchStatus === 'succeeded' && messages.length === 0 && (
<JKChatEmptyState />
)}
{messages.map(message => (
<JKChatMessage
key={message.id}
message={message}
isCurrentUser={message.senderId === currentUser.id}
/>
))}
</div>
```
**State Ownership:**
- ✅ Redux: messages, fetchStatus, currentUser
- ❌ Props: None
- ✅ Local: isUserScrolling, scrollContainerRef
**Styling:**
- Flex-grow: 1 (fills space between header and composer)
- Overflow-y: auto (scrollable)
- Padding: 16px
- Background: Light gray (#f5f5f5)
---
### 5. JKChatMessage
**File:** `jam-ui/src/components/client/chat/JKChatMessage.js`
**Purpose:** Single message item (avatar, name, text, timestamp).
**Props:**
```javascript
{
message: {
id: string,
senderId: string,
senderName: string,
message: string,
createdAt: string, // ISO 8601 timestamp
channel: string
},
isCurrentUser: boolean // True if message from current user
}
```
**Redux Selectors:**
```javascript
// None - all data passed via props
```
**Helper Functions:**
```javascript
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
dayjs.extend(relativeTime);
const formatTimestamp = (isoTimestamp) => {
return dayjs(isoTimestamp).fromNow(); // "5 minutes ago"
};
```
**Render:**
```jsx
<div
className={`chat-message ${isCurrentUser ? 'current-user' : 'other-user'}`}
data-testid="chat-message"
>
{!isCurrentUser && (
<div className="message-avatar">
{/* Placeholder: Use first letter of name */}
<span className="avatar-initial">
{message.senderName.charAt(0).toUpperCase()}
</span>
</div>
)}
<div className="message-content">
<div className="message-header">
<span className="sender-name">
{isCurrentUser ? 'You' : message.senderName}
</span>
<span className="message-timestamp">
{formatTimestamp(message.createdAt)}
</span>
</div>
<div className="message-text">
{message.message}
</div>
</div>
{isCurrentUser && (
<div className="message-avatar message-avatar-right">
<span className="avatar-initial">
{message.senderName.charAt(0).toUpperCase()}
</span>
</div>
)}
</div>
```
**State Ownership:**
- ❌ Redux: None
- ✅ Props: message, isCurrentUser
- ✅ Local: None (purely presentational)
**Styling:**
- Layout: Flex row
- Current user messages: Avatar on right, text bubble right-aligned (blue)
- Other user messages: Avatar on left, text bubble left-aligned (gray)
- Text bubble: Rounded corners, padding, max-width 70%
- Timestamp: Small, gray, relative time format
**Future Enhancement (Phase 11):**
- Replace avatar placeholder with actual user photo from API
- Add attachment support (music_notation, claimed_recording)
---
### 6. JKChatComposer
**File:** `jam-ui/src/components/client/chat/JKChatComposer.js`
**Purpose:** Message composition area (textarea + send button).
**Props:**
```javascript
{
// No props - all state from Redux
}
```
**Redux Selectors:**
```javascript
const sessionId = useSelector(state => state.activeSession.sessionId);
const sendStatus = useSelector(state => state.sessionChat.sendStatus);
const sendError = useSelector(state => state.sessionChat.sendError);
const clientId = useSelector(state => state.auth.clientId); // For WebSocket routing
const isConnected = useSelector(state => state.jamServer.isConnected);
```
**Redux Actions:**
```javascript
dispatch(sendMessage({
message,
channel: sessionId,
channelType: 'session',
sessionId,
clientId
}));
```
**Local State:**
```javascript
const [message, setMessage] = useState('');
const textareaRef = useRef(null);
```
**Keyboard Handling:**
```javascript
const handleKeyDown = (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault(); // Prevent newline
handleSend();
}
// Shift+Enter allows newline
};
```
**Send Handler:**
```javascript
const handleSend = () => {
const trimmed = message.trim();
// Validation
if (!trimmed) return;
if (trimmed.length > 255) {
alert('Message too long (max 255 characters)');
return;
}
if (!isConnected) {
alert('Cannot send message: disconnected from server');
return;
}
// Send message
dispatch(sendMessage({
message: trimmed,
channel: sessionId,
channelType: 'session',
sessionId,
clientId
}));
// Clear input
setMessage('');
textareaRef.current?.focus();
};
```
**Render:**
```jsx
<div className="chat-composer">
{sendError && (
<div className="chat-error" data-testid="chat-error">
Error: {sendError}
</div>
)}
<div className="composer-input-area">
<textarea
ref={textareaRef}
className="chat-input"
placeholder="enter message"
value={message}
onChange={(e) => setMessage(e.target.value)}
onKeyDown={handleKeyDown}
maxLength={255}
disabled={sendStatus === 'loading' || !isConnected}
data-testid="chat-input"
/>
<div className="composer-footer">
<span className="character-count">
{message.length}/255
</span>
<button
className="button-orange chat-send-button"
onClick={handleSend}
disabled={!message.trim() || sendStatus === 'loading' || !isConnected}
data-testid="chat-send"
>
{sendStatus === 'loading' ? 'SENDING...' : 'SEND'}
</button>
</div>
</div>
</div>
```
**State Ownership:**
- ✅ Redux: sendStatus, sendError, sessionId, clientId, isConnected
- ❌ Props: None
- ✅ Local: message, textareaRef
**Styling:**
- Fixed height textarea (e.g., 80px)
- Auto-resize (optional, Phase 11)
- Send button: Orange (primary action)
- Character count: Right-aligned, small font
- Error message: Red background, above input
---
### 7. JKChatLoadingSpinner
**File:** `jam-ui/src/components/client/chat/JKChatLoadingSpinner.js`
**Purpose:** Loading indicator while fetching message history.
**Props:**
```javascript
{
// No props
}
```
**Render:**
```jsx
<div className="chat-loading-spinner" data-testid="chat-loading">
<div className="spinner"></div>
<p>Loading messages...</p>
</div>
```
**State Ownership:**
- ❌ Redux: None
- ❌ Props: None
- ✅ Local: None (purely presentational)
**Styling:**
- Centered in message list
- CSS spinner animation (existing jam-ui spinner class?)
- Gray text
---
### 8. JKChatEmptyState
**File:** `jam-ui/src/components/client/chat/JKChatEmptyState.js`
**Purpose:** Empty state when no messages in session.
**Props:**
```javascript
{
// No props
}
```
**Render:**
```jsx
<div className="chat-empty-state" data-testid="chat-empty">
<div className="empty-icon">💬</div>
<p className="empty-title">No messages yet</p>
<p className="empty-subtitle">
Start the conversation by sending a message below
</p>
</div>
```
**State Ownership:**
- ❌ Redux: None
- ❌ Props: None
- ✅ Local: None (purely presentational)
**Styling:**
- Centered in message list
- Large icon
- Gray text
- Friendly copy
---
## Props vs Redux Decision Matrix
| Data/State | Props | Redux | Rationale |
|------------|-------|-------|-----------|
| **Messages** | ❌ | ✅ | Shared across WebSocket handler, needs persistence |
| **Unread count** | ❌ | ✅ | Displayed in button, updated by WebSocket |
| **Window open/closed** | ❌ | ✅ | Persisted across re-renders |
| **Fetch status** | ❌ | ✅ | Shared loading state |
| **Send status** | ❌ | ✅ | Shared send state |
| **Current user** | ❌ | ✅ | Available from auth slice |
| **Session ID** | ❌ | ✅ | Available from activeSession slice |
| **Client ID** | ❌ | ✅ | Available from auth slice |
| **Connection status** | ❌ | ✅ | Available from jamServer context |
| **Message input text** | ❌ | ❌ | ✅ Local (ephemeral, no need to persist) |
| **Scroll position** | ❌ | ❌ | ✅ Local (managed by ref) |
| **User scrolling flag** | ❌ | ❌ | ✅ Local (auto-scroll logic) |
| **onClose callback** | ✅ | ❌ | Passed to sub-components |
**Summary:**
- **Heavy Redux usage** for data and state management
- **Minimal props** (only callbacks like onClose)
- **Local state** only for ephemeral UI state (input text, scroll position)
---
## Reusable Components
### Available from jam-ui
**1. WindowPortal**
- ✅ **File:** `jam-ui/src/components/common/WindowPortal.js`
- ✅ **Usage:** Wrap chat content for modeless dialog
- ✅ **No modifications needed**
**2. Button Classes**
- ✅ **CSS Classes:** `.button-orange` (primary), `.button-grey` (secondary)
- ✅ **Usage:** Apply to `<button>` elements
- ✅ **Already styled** in jam-ui
**3. Input/Textarea Patterns**
- ✅ **Pattern:** Standard HTML `<textarea>` with controlled value
- ✅ **Usage:** Follow existing input patterns in jam-ui
### Not Available (Need to Create)
**1. JKAvatar Component**
- ❌ **Not found** in jam-ui codebase
- **For Phase 8:** Use placeholder (first letter of name)
- **For Phase 11:** Create reusable JKAvatar component with photo support
**2. JKTimeDisplay Component**
- ❌ **Not found** in jam-ui codebase
- **For Phase 8:** Use inline `dayjs.fromNow()` formatting
- **For Phase 11:** Create reusable JKTimeDisplay component (if needed elsewhere)
**3. JKBadge Component**
- ❌ **Not found** in jam-ui codebase
- **For Phase 8:** Use inline `<span className="badge">`
- **For Phase 11:** Create reusable JKBadge component (if needed elsewhere)
---
## File Structure
```
jam-ui/src/components/client/
├── JKSessionScreen.js (existing - add chat button)
├── JKSessionChatButton.js (new)
├── JKSessionChatWindow.js (new)
└── chat/ (new directory)
├── JKChatHeader.js
├── JKChatMessageList.js
├── JKChatMessage.js
├── JKChatComposer.js
├── JKChatLoadingSpinner.js
└── JKChatEmptyState.js
```
**Rationale:**
- Main components at `client/` level (JKSessionChatButton, JKSessionChatWindow)
- Sub-components in dedicated `chat/` directory (cleaner organization)
- Matches existing jam-ui structure (e.g., `media/` directory for media components)
---
## Integration with Session Screen
### Current JKSessionScreen Structure (Reference)
```jsx
export const JKSessionScreen = () => {
const { sessionId } = useParams();
const dispatch = useDispatch();
useEffect(() => {
if (sessionId) {
dispatch(loadSession(sessionId));
}
}, [sessionId, dispatch]);
useSessionWebSocket(sessionId);
return (
<div className="session-screen">
{/* Existing components */}
<JKSessionMixer />
<JKSessionTracks />
<JKSessionJamTrackPlayer />
<JKSessionBackingTrackPlayer />
<JKSessionMetronomePlayer />
{/* NEW: Chat components */}
<JKSessionChatButton />
<JKSessionChatWindow />
</div>
);
};
```
### Chat Button Placement
**Option A: Top Navigation Bar (Recommended)**
- Position: Absolute or flex layout in top-right area
- Near: Metronome button, settings button
- Visibility: Always visible during session
**Option B: Sidebar Panel**
- Position: Left sidebar (like legacy chatPanel.js)
- Collapsible panel
- Less intrusive but harder to discover
**Recommendation:** Option A - Top navigation bar (better discoverability)
### Chat Window Rendering
**Conditional Rendering:**
- Only renders when `isWindowOpen === true`
- WindowPortal handles actual popup creation
- No DOM footprint when closed (cleanup automatic)
---
## Component Lifecycle Patterns
### JKSessionChatWindow Lifecycle
**Mount:**
1. Component mounts (window opens)
2. `useEffect` triggers
3. Fetch message history: `dispatch(fetchChatHistory(...))`
4. Mark messages as read: `dispatch(markAsRead(...))`
5. Subscribe to WebSocket (via useSessionWebSocket - already active)
**Update:**
1. New message received via WebSocket
2. Redux state updated: `addMessageFromWebSocket()`
3. Component re-renders (messages array changes)
4. Auto-scroll to bottom (if not manually scrolling)
**Unmount:**
1. User closes window
2. WindowPortal triggers `onClose` callback
3. Dispatch `closeChatWindow()` action
4. Redux state updated: `isWindowOpen = false`
5. Component unmounts (cleanup automatic)
**No explicit cleanup needed:**
- WebSocket subscription lives at session level (not per-component)
- No manual event listeners (all via React)
- WindowPortal handles popup cleanup
---
### JKChatMessageList Auto-Scroll Pattern
**Problem:** Auto-scroll on new messages, but respect user manual scrolling.
**Solution:**
1. Track `isUserScrolling` state (true if user scrolled up)
2. On scroll event: Check if at bottom (`scrollHeight - scrollTop - clientHeight < 50`)
3. If at bottom: `isUserScrolling = false` (enable auto-scroll)
4. If not at bottom: `isUserScrolling = true` (disable auto-scroll)
5. On new message: If `!isUserScrolling`, scroll to bottom
**Reference:** Legacy pattern from `ChatWindow.componentDidUpdate()`
---
## Comparison to Backing Track / JamTrack Players
### Similarities
**1. Component Structure:**
- Main component (JKSessionChatWindow) with sub-components
- Uses WindowPortal for popup mode
- Heavy Redux integration
**2. Redux Patterns:**
- State in Redux slices
- Async thunks for API calls
- Selectors for derived data
**3. Lifecycle:**
- Mount → fetch data
- Update → handle WebSocket messages
- Unmount → cleanup (automatic)
### Differences
**1. No jamClient Integration:**
- Chat is purely web-based (no native client calls)
- Backing Track / JamTrack use jamClient.* methods
- Chat uses REST API + WebSocket only
**2. Simpler State Machine:**
- Chat: 3 states (idle, loading, succeeded)
- JamTrack: 6-state download/sync machine
- Chat has no download/packaging complexity
**3. Real-Time Emphasis:**
- Chat: WebSocket-first (messages arrive instantly)
- Backing Track: Polling for playback position
- JamTrack: Hybrid (polling + WebSocket)
**4. User Input:**
- Chat: Text input (textarea with validation)
- Players: Buttons/sliders (no text input)
---
## Accessibility Considerations
**Phase 11 (Polish):**
- Add `aria-label` to buttons
- Add `role="log"` to message list (for screen readers)
- Add `aria-live="polite"` for new message announcements
- Keyboard navigation (Tab, Enter, Esc)
- Focus management (auto-focus input on window open)
**Not Phase 8 Priority:**
- Basic functionality first, accessibility in polish phase
---
## Performance Considerations
**Phase 11 (Optimization):**
- Virtualize message list (react-window) if >100 messages
- Debounce scroll event handler
- Memoize message components (React.memo)
- Optimize selector performance (Reselect)
**Not Phase 8 Priority:**
- Build basic functionality first, optimize later if needed
---
## Testing Strategy
### Unit Tests (Jest)
**JKChatMessage:**
- Renders sender name correctly
- Formats timestamp using dayjs.fromNow()
- Applies 'current-user' class when isCurrentUser=true
- Shows avatar on correct side
**JKChatComposer:**
- Sends message on Enter key
- Allows newline on Shift+Enter
- Validates message length (max 255)
- Disables send button when disconnected
- Clears input after send
**JKChatMessageList:**
- Auto-scrolls to bottom on new message
- Stops auto-scroll when user scrolls up
- Resumes auto-scroll when user scrolls to bottom
- Shows loading spinner when fetchStatus='loading'
- Shows empty state when messages.length=0
### Integration Tests (Playwright)
**Send Message Flow:**
1. Open chat window
2. Type message
3. Click SEND (or press Enter)
4. Verify API call (POST /api/chat)
5. Verify message appears in UI
6. Verify input cleared
**Receive Message Flow:**
1. Open chat window
2. Simulate WebSocket message (mock)
3. Verify message appears in UI
4. Verify auto-scroll to bottom
**Unread Badge Flow:**
1. Close chat window
2. Simulate incoming WebSocket message
3. Verify badge appears with count=1
4. Open chat window
5. Verify badge disappears (count=0)
### E2E Tests (Playwright)
**Complete Chat Workflow:**
1. Login → Create session → Join session
2. Open chat window
3. Send message
4. Verify message sent
5. Close chat window
6. Simulate reply from other user
7. Verify unread badge
8. Open chat window
9. Verify badge cleared
10. Verify reply visible
---
## Summary
**Component Architecture Designed:**
- ✅ 8 components specified (JKSessionChatButton, JKSessionChatWindow, 6 sub-components)
- ✅ Props vs Redux decision matrix defined
- ✅ Integration with session screen planned
- ✅ Reusable components identified (WindowPortal, button classes)
- ✅ Component hierarchy clear and modular
- ✅ File structure follows jam-ui conventions
**Key Patterns Applied:**
- Heavy Redux integration (minimal props)
- WindowPortal for modeless dialog (proven pattern)
- Sub-component modularity (like JamTrack player)
- Auto-scroll logic (from legacy ChatWindow)
- Enter-to-send keyboard handling (legacy pattern)
**Ready for Phase 7-8 Implementation:**
- Component specs ready for TDD implementation
- Clear integration points identified
- Testing strategy defined
- Follows established jam-ui patterns
---
**Next Document:** CHAT_REDUX_DESIGN.md (Redux state structure and WebSocket integration)