docs(14): create phase plan for Chat Integration & Display

Phase 14: Chat Integration & Display
- 2 plan(s) in 1 wave(s)
- 2 parallel (wave 1), 0 sequential
- Extends JKChatMessage for attachment display
- Adds clickable links with signed URL fetching
- Human verification checkpoint in plan 02
- Ready for execution
This commit is contained in:
Nuwan 2026-02-05 19:16:42 +05:30
parent 45e284096a
commit 68229dd003
3 changed files with 563 additions and 5 deletions

View File

@ -55,8 +55,8 @@ Decimal phases appear between their surrounding integers in numeric order.
**Milestone Goal:** Add file attachment capability to music sessions, allowing users to upload files from their computer and view them in the chat window with real-time synchronization across all session participants.
- [ ] **Phase 12: Attachment Research & Backend Validation** - Explore legacy attachment implementation, validate existing backend infrastructure
- [ ] **Phase 13: File Upload Infrastructure** - Attach button, file dialog, validation, S3 upload with progress
- [x] **Phase 12: Attachment Research & Backend Validation** - Explore legacy attachment implementation, validate existing backend infrastructure
- [x] **Phase 13: File Upload Infrastructure** - Attach button, file dialog, validation, S3 upload with progress
- [ ] **Phase 14: Chat Integration & Display** - Display attachments as messages in chat window
- [ ] **Phase 15: Real-time Synchronization** - WebSocket broadcast and attachment history
- [ ] **Phase 16: Attachment Finalization** - Error handling, edge cases, UAT
@ -259,8 +259,8 @@ Plans:
9. Attachment messages display correctly at different window sizes (responsive layout, long filename truncation)
Plans:
- [ ] 14-01: Attachment message display with metadata and styling
- [ ] 14-02: Clickable links, chat history integration, responsive layout
- [ ] 14-01-PLAN.md — Attachment message display with metadata and styling
- [ ] 14-02-PLAN.md — Clickable links, chat history integration, responsive layout
#### Phase 15: Real-time Synchronization
**Goal**: New attachments broadcast to all session participants in real-time via WebSocket
@ -320,6 +320,6 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 →
| 11. Chat Finalization | v1.1 | 2/2 | Complete | 2026-01-31 |
| 12. Attachment Research & Backend Validation | v1.2 | 2/2 | Complete | 2026-02-02 |
| 13. File Upload Infrastructure | v1.2 | 3/3 | Complete | 2026-02-05 |
| 14. Chat Integration & Display | v1.2 | 0/2 | Not started | - |
| 14. Chat Integration & Display | v1.2 | 0/2 | Planned | - |
| 15. Real-time Synchronization | v1.2 | 0/1 | Not started | - |
| 16. Attachment Finalization | v1.2 | 0/2 | Not started | - |

View File

@ -0,0 +1,237 @@
---
phase: 14-chat-integration-and-display
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- jam-ui/src/components/client/chat/JKChatMessage.js
- jam-ui/src/components/client/JKSessionScreen.js
autonomous: true
must_haves:
truths:
- "Attachment messages display with '[UserName] attached [FileName]' format"
- "Attachment messages include file size (KB/MB), uploader name, and timestamp"
- "Attachment messages have visual distinction from regular text messages (paperclip icon, different styling)"
- "Attachment messages sort chronologically with regular chat messages"
artifacts:
- path: "jam-ui/src/components/client/chat/JKChatMessage.js"
provides: "Attachment message rendering with metadata display"
contains: "attachmentId"
- path: "jam-ui/src/components/client/JKSessionScreen.js"
provides: "WebSocket message transformation including attachment fields"
contains: "attachment_id"
key_links:
- from: "JKSessionScreen.js"
to: "sessionChatSlice"
via: "addMessageFromWebSocket with attachment fields"
pattern: "attachmentId.*attachmentName.*attachmentSize"
- from: "JKChatMessage.js"
to: "formatFileSize"
via: "import from attachmentValidation"
pattern: "formatFileSize.*attachmentSize"
---
<objective>
Extend JKChatMessage component to detect and display attachment messages with metadata and visual distinction from regular text messages.
Purpose: Users need to distinguish attachment messages from regular chat messages and see file metadata (name, size, uploader, timestamp) at a glance.
Output: JKChatMessage renders attachment messages with paperclip icon, styled differently from text messages, showing all required metadata.
</objective>
<execution_context>
@/Users/nuwan/.claude/get-shit-done/workflows/execute-plan.md
@/Users/nuwan/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/13-file-upload-infrastructure/13-03-SUMMARY.md
@jam-ui/src/components/client/chat/JKChatMessage.js
@jam-ui/src/components/client/JKSessionScreen.js
@jam-ui/src/services/attachmentValidation.js
</context>
<tasks>
<task type="auto">
<name>Task 1: Extend WebSocket message transformation to include attachment fields</name>
<files>jam-ui/src/components/client/JKSessionScreen.js</files>
<action>
Find the CHAT_MESSAGE WebSocket handler in handleWebSocketMessage function. The handler transforms Protocol Buffer payload to Redux format. Extend the message transformation to include attachment fields from the WebSocket payload:
```javascript
// In CHAT_MESSAGE handler, extend the message object construction:
const message = {
id: payload.msg_id,
senderId: payload.user_id,
senderName: payload.user_name,
message: payload.message,
createdAt: payload.created_at,
channel: payload.channel,
sessionId: payload.session_id,
// NEW: Attachment fields (null/undefined if not an attachment)
purpose: payload.purpose, // 'Notation File', 'Audio File', or undefined
attachmentId: payload.attachment_id, // MusicNotation UUID
attachmentType: payload.attachment_type, // 'notation' or 'audio'
attachmentName: payload.attachment_name, // filename
attachmentSize: payload.attachment_size // file size in bytes (may be null)
};
```
The backend ChatMessage.send_chat_msg includes these fields. The payload structure matches Protocol Buffer definitions in pb/src/client_container.proto.
Note: attachmentSize may not be present in current WebSocket payload. If backend doesn't send it, we'll handle gracefully (show filename without size). Check if size is available; if not, we can fetch it later or display without size.
</action>
<verify>
Review the code to confirm attachment fields are mapped correctly. The message object passed to addMessageFromWebSocket should include attachmentId, attachmentName, attachmentType, purpose, and attachmentSize (if available).
</verify>
<done>
WebSocket CHAT_MESSAGE handler transforms attachment fields into Redux message format. Messages with attachments have attachmentId, attachmentName, attachmentType, and purpose set; regular messages have these as undefined.
</done>
</task>
<task type="auto">
<name>Task 2: Extend JKChatMessage to render attachment messages with metadata</name>
<files>jam-ui/src/components/client/chat/JKChatMessage.js</files>
<action>
Modify JKChatMessage to detect and render attachment messages differently from regular text messages.
1. Import formatFileSize from attachmentValidation service:
```javascript
import { formatFileSize } from '../../../services/attachmentValidation';
```
2. Add helper function to determine if message is an attachment:
```javascript
const isAttachmentMessage = (message) => {
return message.attachmentId && message.attachmentName;
};
```
3. Create attachment-specific render section inside the component. When message has attachmentId, render:
- Paperclip icon (use Unicode character or simple text indicator: "[File]" or "📎")
- Format: "[senderName] attached [attachmentName]"
- File size in parentheses if available: "(2.5 MB)"
- Timestamp (using existing formatTimestamp)
- Visual styling: Different background color (light blue #e3f2fd) to distinguish from regular messages
4. Keep existing rendering for regular text messages unchanged.
5. Update PropTypes to include new optional fields:
```javascript
JKChatMessage.propTypes = {
message: PropTypes.shape({
id: PropTypes.string.isRequired,
senderId: PropTypes.string.isRequired,
senderName: PropTypes.string,
message: PropTypes.string, // Optional for attachment-only messages
createdAt: PropTypes.string.isRequired,
// Attachment fields (optional)
attachmentId: PropTypes.string,
attachmentName: PropTypes.string,
attachmentType: PropTypes.oneOf(['notation', 'audio']),
purpose: PropTypes.string,
attachmentSize: PropTypes.number
}).isRequired
};
```
6. Styling for attachment message (inline styles, consistent with existing pattern):
```javascript
const attachmentStyle = {
display: 'flex',
gap: '12px',
marginBottom: '16px',
padding: '8px',
borderRadius: '4px',
backgroundColor: '#e3f2fd', // Light blue to distinguish from regular messages (#f8f9fa)
border: '1px solid #bbdefb'
};
const fileIconStyle = {
fontSize: '16px',
marginRight: '4px'
};
```
7. Conditional rendering structure:
```javascript
if (isAttachmentMessage(message)) {
return (
<div style={attachmentStyle}>
{/* Avatar */}
<div style={avatarStyle}>
{getInitials(senderName)}
</div>
{/* Attachment content */}
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: '8px', marginBottom: '4px' }}>
<span style={{ fontWeight: 600, fontSize: '14px', color: '#212529' }}>
{senderName || 'Unknown'}
</span>
<span style={{ fontSize: '12px', color: '#6c757d' }}>
{formatTimestamp(createdAt)}
</span>
</div>
<div style={{ fontSize: '14px', color: '#212529', display: 'flex', alignItems: 'center' }}>
<span style={fileIconStyle}>📎</span>
<span>
attached <strong>{attachmentName}</strong>
{attachmentSize && <span style={{ color: '#6c757d', marginLeft: '4px' }}>({formatFileSize(attachmentSize)})</span>}
</span>
</div>
</div>
</div>
);
}
// ... existing regular message rendering
```
React.memo should remain in place for performance optimization.
</action>
<verify>
1. Code compiles without errors
2. Regular text messages render unchanged
3. Attachment messages render with paperclip icon, "[name] attached [filename]" format
4. File size displays when available (formatted as KB/MB)
5. Visual distinction: attachment messages have light blue background
</verify>
<done>
JKChatMessage detects attachment messages and renders them with distinct styling, paperclip icon, sender name, filename, size (if available), and timestamp. Regular messages render unchanged.
</done>
</task>
</tasks>
<verification>
1. Start dev server: `cd jam-ui && npm start`
2. Join a music session
3. Upload a file using Attach button (from Phase 13)
4. Verify attachment message appears in chat with:
- Light blue background (distinct from regular messages)
- Paperclip icon
- "[YourName] attached [filename]" format
- Timestamp
5. Send regular text message
6. Verify text message renders normally (gray background, no icon)
7. Verify both messages appear in chronological order
</verification>
<success_criteria>
- Attachment messages display with "[UserName] attached [FileName]" format
- Attachment messages have visual distinction (paperclip icon, light blue background)
- File size displays if available (formatted as KB/MB)
- Timestamp displays using existing formatTimestamp utility
- Regular text messages render unchanged
- Messages sort chronologically (existing behavior preserved)
</success_criteria>
<output>
After completion, create `.planning/phases/14-chat-integration-and-display/14-01-SUMMARY.md`
</output>

View File

@ -0,0 +1,321 @@
---
phase: 14-chat-integration-and-display
plan: 02
type: execute
wave: 1
depends_on: []
files_modified:
- jam-ui/src/components/client/chat/JKChatMessage.js
- jam-ui/src/helpers/rest.js
autonomous: false
must_haves:
truths:
- "Filename is clickable link that opens file in new browser tab"
- "Browser handles view/download based on file type (PDF viewer, image display, audio player)"
- "Chat history includes attachments (visible when joining session, persists across refresh)"
- "Long filenames truncate with ellipsis at appropriate window sizes"
- "Attachment messages display correctly at different window sizes (responsive)"
artifacts:
- path: "jam-ui/src/components/client/chat/JKChatMessage.js"
provides: "Clickable attachment links with signed URL fetching"
contains: "handleAttachmentClick"
- path: "jam-ui/src/helpers/rest.js"
provides: "getMusicNotationUrl helper for signed S3 URLs"
contains: "getMusicNotationUrl"
key_links:
- from: "JKChatMessage.js"
to: "getMusicNotationUrl"
via: "import and fetch on click"
pattern: "getMusicNotationUrl.*attachmentId"
- from: "JKChatMessage.js"
to: "window.open"
via: "open signed URL in new tab"
pattern: "window\\.open.*_blank"
---
<objective>
Add clickable attachment links with signed URL fetching and responsive layout for attachment messages.
Purpose: Users need to click attachment filenames to view/download files, and the UI must work well at different window sizes with proper filename truncation.
Output: Attachment filenames are clickable links that fetch signed S3 URLs and open files in new browser tabs. Long filenames truncate gracefully, and layout is responsive.
</objective>
<execution_context>
@/Users/nuwan/.claude/get-shit-done/workflows/execute-plan.md
@/Users/nuwan/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/14-chat-integration-and-display/14-01-PLAN.md
@jam-ui/src/components/client/chat/JKChatMessage.js
@jam-ui/src/helpers/rest.js
@.planning/phases/12-attachment-research-&-backend-validation/12-RESEARCH.md
</context>
<tasks>
<task type="auto">
<name>Task 1: Add click handler to fetch signed URL and open file</name>
<files>jam-ui/src/components/client/chat/JKChatMessage.js</files>
<action>
Add click handler for attachment filename that:
1. Fetches signed S3 URL via getMusicNotationUrl REST helper
2. Opens URL in new browser tab (target="_blank")
Import the REST helper:
```javascript
import { getMusicNotationUrl } from '../../../helpers/rest';
```
Add click handler inside the component (wrap in useCallback for memoization):
```javascript
import React, { useCallback, useState } from 'react';
// Inside component:
const [isLoadingUrl, setIsLoadingUrl] = useState(false);
const handleAttachmentClick = useCallback(async (attachmentId) => {
if (isLoadingUrl) return; // Prevent rapid clicks
try {
setIsLoadingUrl(true);
const response = await getMusicNotationUrl(attachmentId);
// Response is { url: "https://s3.amazonaws.com/...?signature=..." }
if (response && response.url) {
window.open(response.url, '_blank');
} else {
console.error('No URL in response:', response);
}
} catch (error) {
console.error('Failed to fetch attachment URL:', error);
// Could show toast error here, but keep it simple for now
} finally {
setIsLoadingUrl(false);
}
}, [isLoadingUrl]);
```
Update the attachment name rendering to be a clickable link:
```javascript
<span
onClick={() => handleAttachmentClick(message.attachmentId)}
style={{
cursor: 'pointer',
color: '#1976d2',
textDecoration: 'underline',
// Truncation for long filenames
maxWidth: '200px',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
display: 'inline-block',
verticalAlign: 'bottom'
}}
title={message.attachmentName} // Full filename on hover
>
{message.attachmentName}
</span>
```
Note: Browser will handle file display based on Content-Type returned by S3:
- PDF: Opens in browser's PDF viewer
- Images (PNG, JPG, GIF): Displays in browser
- Audio (MP3, WAV): Opens browser's audio player
- Other types (XML, TXT): Typically downloads
The backend sets Content-Disposition based on file type.
</action>
<verify>
1. Code compiles without errors
2. Clicking attachment filename triggers handleAttachmentClick
3. isLoadingUrl state prevents rapid multiple clicks
4. Error handling logs to console (no crash on failure)
</verify>
<done>
Attachment filenames are clickable links that fetch signed S3 URLs and open in new browser tabs. Loading state prevents rapid clicks. Errors logged to console without crashing.
</done>
</task>
<task type="auto">
<name>Task 2: Add responsive styling with filename truncation</name>
<files>jam-ui/src/components/client/chat/JKChatMessage.js</files>
<action>
Enhance attachment message styling for responsive behavior:
1. Update attachment content container for better responsiveness:
```javascript
const attachmentContentStyle = {
flex: 1,
minWidth: 0, // Required for text-overflow to work in flex containers
overflow: 'hidden'
};
const attachmentTextStyle = {
fontSize: '14px',
color: '#212529',
display: 'flex',
alignItems: 'center',
flexWrap: 'wrap', // Allow wrapping on very narrow windows
gap: '4px'
};
const filenameLinkStyle = {
cursor: isLoadingUrl ? 'wait' : 'pointer',
color: '#1976d2',
textDecoration: 'underline',
fontWeight: 600,
// Truncation
maxWidth: '100%', // Take available width
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
display: 'inline-block',
verticalAlign: 'bottom'
};
const fileSizeStyle = {
color: '#6c757d',
fontSize: '13px',
flexShrink: 0 // Prevent size from shrinking
};
```
2. Update the attachment render:
```javascript
if (isAttachmentMessage(message)) {
const { senderName, createdAt, attachmentId, attachmentName, attachmentSize } = message;
return (
<div style={{
display: 'flex',
gap: '12px',
marginBottom: '16px',
padding: '8px',
borderRadius: '4px',
backgroundColor: '#e3f2fd',
border: '1px solid #bbdefb'
}}>
{/* Avatar */}
<div style={{
width: '36px',
height: '36px',
borderRadius: '50%',
backgroundColor: '#007bff',
color: 'white',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 'bold',
fontSize: '14px',
flexShrink: 0
}}>
{getInitials(senderName)}
</div>
{/* Content */}
<div style={attachmentContentStyle}>
{/* Header: Name + Timestamp */}
<div style={{ display: 'flex', alignItems: 'baseline', gap: '8px', marginBottom: '4px', flexWrap: 'wrap' }}>
<span style={{ fontWeight: 600, fontSize: '14px', color: '#212529' }}>
{senderName || 'Unknown'}
</span>
<span style={{ fontSize: '12px', color: '#6c757d' }}>
{formatTimestamp(createdAt)}
</span>
</div>
{/* Attachment info */}
<div style={attachmentTextStyle}>
<span style={{ fontSize: '16px', flexShrink: 0 }}>📎</span>
<span>attached</span>
<span
onClick={() => handleAttachmentClick(attachmentId)}
style={filenameLinkStyle}
title={attachmentName}
>
{attachmentName}
</span>
{attachmentSize && (
<span style={fileSizeStyle}>({formatFileSize(attachmentSize)})</span>
)}
</div>
</div>
</div>
);
}
```
3. Ensure minWidth: 0 is set on all flex children that need truncation (this is critical for text-overflow: ellipsis to work).
</action>
<verify>
1. Long filenames truncate with ellipsis ("verylongfilename..." instead of overflow)
2. Full filename visible on hover (title attribute)
3. File size stays visible and doesn't truncate
4. Layout works at different window widths (resize chat window)
5. Content doesn't overflow container bounds
</verify>
<done>
Attachment messages display correctly at different window sizes. Long filenames truncate with ellipsis, full name shown on hover. File size and timestamp always visible.
</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<what-built>
Complete attachment message display with clickable links, signed URL fetching, and responsive layout
</what-built>
<how-to-verify>
1. Start the app: `cd jam-ui && npm start`
2. Start backend: `./runweb` (or appropriate Rails command)
3. Login and join a music session
4. Upload a file using the Attach button (from Phase 13)
5. Verify attachment message appears with:
- Light blue background (distinct from text messages)
- Paperclip icon
- "[YourName] attached [filename]" format
- Clickable filename (underlined, blue color)
6. Click the filename:
- Should open new browser tab
- PDF/images should display in browser
- Other files should download
7. Test long filename: Upload a file with a very long name
- Should truncate with ellipsis
- Hover should show full name
8. Resize chat window:
- Content should remain readable
- No overflow issues
9. Refresh page:
- Attachment should still be visible in chat history
10. Test regular text message still works normally
</how-to-verify>
<resume-signal>Type "approved" if all checks pass, or describe any issues found</resume-signal>
</task>
</tasks>
<verification>
1. Attachment filename is clickable link
2. Clicking fetches signed S3 URL and opens in new tab
3. Browser displays/downloads file based on type
4. Long filenames truncate with ellipsis, full name on hover
5. Layout responsive at different window sizes
6. Chat history includes attachments after refresh
7. Regular text messages still render correctly
</verification>
<success_criteria>
- Filename is clickable link opening file in new browser tab
- Browser handles view/download based on file type (PDF views, images display, others download)
- Long filenames truncate with ellipsis, full name shown on hover
- Responsive layout at different window sizes
- Chat history includes attachments (persists across refresh)
- Auto-scroll works when new attachment appears (existing behavior)
</success_criteria>
<output>
After completion, create `.planning/phases/14-chat-integration-and-display/14-02-SUMMARY.md`
</output>