From b889e4832533764c83122c0cfdf01c17efbb7337 Mon Sep 17 00:00:00 2001 From: aewing Date: Sat, 10 Jan 2026 22:54:33 +0000 Subject: [PATCH] Add menu-driven attachment system with video recording - Replace single file input with paper clip icon button and dropdown menu - Add 5 attachment options: image, video, file, video recording, picture capture - Implement video recording with MediaRecorder API (5 min auto-stop) - Support multiple file input interactions accumulating in pending array - Add same menu system to conversation container (separate attachment array) - Add render functions for displaying selected attachment summaries - Fix CSS positioning for menu with relative parent and top-full positioning - Menu closes on outside click and when file is selected --- index.html | 383 +++++++++++++++++++++++++++++++++++++++++-- logs/SESSION_LOG.txt | 10 ++ 2 files changed, 381 insertions(+), 12 deletions(-) diff --git a/index.html b/index.html index 5341a5e..705ce2a 100644 --- a/index.html +++ b/index.html @@ -212,12 +212,60 @@
- -
- -
No attachments selected.
+
+
+ + +
No attachments selected.
+
+ + + + + + + + + +
+
@@ -340,6 +388,64 @@
+
+ +
+
+ + +
No attachments selected.
+
+ + + + + + + + + + +
+
+
+
@@ -371,6 +477,7 @@ let silenceTimer = null; let searchSilenceTimer = null; let pendingNoteAttachments = []; + let pendingConversationAttachments = []; // --- Voice Recognition Setup with auto-stop --- function setupTitleVoiceRecognition() { @@ -1269,6 +1376,33 @@ listEl.appendChild(frag); } + function renderConversationAttachmentsList() { + const listEl = document.getElementById('conversationAttachmentsList'); + const summaryEl = document.getElementById('conversationAttachmentsSummary'); + listEl.innerHTML = ''; + if (!pendingConversationAttachments.length) { + summaryEl.textContent = 'No attachments selected.'; + return; + } + summaryEl.textContent = `${pendingConversationAttachments.length} file(s) selected`; + const frag = document.createDocumentFragment(); + pendingConversationAttachments.forEach((file) => { + const row = document.createElement('div'); + row.className = 'flex items-center gap-2 text-sm text-gray-800'; + const kind = attachmentKindFromName(file.name); + const badge = document.createElement('span'); + badge.className = 'px-2 py-0.5 rounded-full text-xs border border-gray-200 text-gray-700 bg-gray-50'; + badge.textContent = kind; + const label = document.createElement('span'); + const size = formatBytes(file.size); + label.textContent = size ? `${file.name} (${size})` : file.name; + row.appendChild(badge); + row.appendChild(label); + frag.appendChild(row); + }); + listEl.appendChild(frag); + } + function resetNoteForm() { document.getElementById('noteTitle').value = ''; document.getElementById('noteText').value = ''; @@ -1292,6 +1426,12 @@ document.getElementById('conversationJobNumberContainer').classList.add('hidden'); document.getElementById('conversationAudioPreview').src = ''; document.getElementById('conversationAudioPlayer').classList.add('hidden'); + pendingConversationAttachments = []; + document.getElementById('conversationImageInput').value = ''; + document.getElementById('conversationVideoInput').value = ''; + document.getElementById('conversationFileInput').value = ''; + document.getElementById('conversationCameraInput').value = ''; + renderConversationAttachmentsList(); } function renderNotesList(items) { @@ -1592,18 +1732,236 @@ showNoteForm(); }); - document.getElementById('noteAttachmentsInput').addEventListener('change', (e) => { - pendingNoteAttachments = Array.from(e.target.files || []); - renderNoteAttachmentsList(); + // Attachment menu handling + const attachmentMenuBtn = document.getElementById('attachmentMenuBtn'); + const attachmentMenu = document.getElementById('attachmentMenu'); + const imageInput = document.getElementById('imageInput'); + const videoInput = document.getElementById('videoInput'); + const fileInput = document.getElementById('fileInput'); + const cameraInput = document.getElementById('cameraInput'); + + attachmentMenuBtn.addEventListener('click', (e) => { + e.preventDefault(); + attachmentMenu.classList.toggle('hidden'); + }); + + document.getElementById('attachImageBtn').addEventListener('click', () => { + attachmentMenu.classList.add('hidden'); + imageInput.click(); + }); + + document.getElementById('attachVideoBtn').addEventListener('click', () => { + attachmentMenu.classList.add('hidden'); + videoInput.click(); + }); + + document.getElementById('attachFileBtn').addEventListener('click', () => { + attachmentMenu.classList.add('hidden'); + fileInput.click(); + }); + + document.getElementById('takePictureBtn').addEventListener('click', () => { + attachmentMenu.classList.add('hidden'); + cameraInput.click(); + }); + + document.getElementById('recordVideoBtn').addEventListener('click', async () => { + attachmentMenu.classList.add('hidden'); + try { + const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); + const videoRecorder = new MediaRecorder(stream); + const chunks = []; + + videoRecorder.ondataavailable = (e) => { + if (e.data.size > 0) chunks.push(e.data); + }; + + videoRecorder.onstop = () => { + const blob = new Blob(chunks, { type: 'video/webm' }); + const file = new File([blob], `video-${Date.now()}.webm`, { type: 'video/webm' }); + pendingNoteAttachments.push(file); + renderNoteAttachmentsList(); + stream.getTracks().forEach(track => track.stop()); + }; + + videoRecorder.start(); + + // Show recording UI + const recordStatus = document.getElementById('recordStatus'); + const originalStatus = recordStatus.textContent; + recordStatus.textContent = 'Recording video... (Click to stop)'; + recordStatus.style.cursor = 'pointer'; + recordStatus.style.color = '#dc2626'; + + const stopHandler = () => { + recordStatus.removeEventListener('click', stopHandler); + videoRecorder.stop(); + recordStatus.textContent = originalStatus; + recordStatus.style.cursor = 'auto'; + recordStatus.style.color = ''; + }; + + recordStatus.addEventListener('click', stopHandler); + + // Auto-stop after 5 minutes + setTimeout(() => { + if (videoRecorder.state === 'recording') { + recordStatus.removeEventListener('click', stopHandler); + videoRecorder.stop(); + recordStatus.textContent = 'Video recording stopped (5 minute limit)'; + recordStatus.style.cursor = 'auto'; + setTimeout(() => { + recordStatus.textContent = originalStatus; + recordStatus.style.color = ''; + }, 3000); + } + }, 5 * 60 * 1000); + } catch (err) { + console.error('Failed to access camera/microphone:', err); + alert('Unable to access camera or microphone. Check permissions.'); + } + }); + + // File input change handlers + [imageInput, videoInput, fileInput, cameraInput].forEach(input => { + input.addEventListener('change', (e) => { + pendingNoteAttachments.push(...Array.from(e.target.files || [])); + renderNoteAttachmentsList(); + }); }); document.getElementById('clearAttachmentsBtn').addEventListener('click', (e) => { e.preventDefault(); pendingNoteAttachments = []; - document.getElementById('noteAttachmentsInput').value = ''; + imageInput.value = ''; + videoInput.value = ''; + fileInput.value = ''; + cameraInput.value = ''; renderNoteAttachmentsList(); }); + // Close menu on outside click + document.addEventListener('click', (e) => { + if (!attachmentMenuBtn.contains(e.target) && !attachmentMenu.contains(e.target)) { + attachmentMenu.classList.add('hidden'); + } + }); + + // Conversation attachment menu setup + const conversationAttachmentMenuBtn = document.getElementById('conversationAttachmentMenuBtn'); + const conversationAttachmentMenu = document.getElementById('conversationAttachmentMenu'); + const conversationImageInput = document.getElementById('conversationImageInput'); + const conversationVideoInput = document.getElementById('conversationVideoInput'); + const conversationFileInput = document.getElementById('conversationFileInput'); + const conversationCameraInput = document.getElementById('conversationCameraInput'); + + conversationAttachmentMenuBtn.addEventListener('click', (e) => { + e.preventDefault(); + conversationAttachmentMenu.classList.toggle('hidden'); + }); + + document.getElementById('conversationAttachImageBtn').addEventListener('click', () => { + conversationAttachmentMenu.classList.add('hidden'); + conversationImageInput.click(); + }); + + document.getElementById('conversationAttachVideoBtn').addEventListener('click', () => { + conversationAttachmentMenu.classList.add('hidden'); + conversationVideoInput.click(); + }); + + document.getElementById('conversationAttachFileBtn').addEventListener('click', () => { + conversationAttachmentMenu.classList.add('hidden'); + conversationFileInput.click(); + }); + + document.getElementById('conversationTakePictureBtn').addEventListener('click', () => { + conversationAttachmentMenu.classList.add('hidden'); + conversationCameraInput.click(); + }); + + document.getElementById('conversationRecordVideoBtn').addEventListener('click', async () => { + conversationAttachmentMenu.classList.add('hidden'); + try { + const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); + const videoRecorder = new MediaRecorder(stream); + const chunks = []; + + videoRecorder.ondataavailable = (e) => { + if (e.data.size > 0) chunks.push(e.data); + }; + + videoRecorder.onstop = () => { + const blob = new Blob(chunks, { type: 'video/webm' }); + const file = new File([blob], `video-${Date.now()}.webm`, { type: 'video/webm' }); + pendingConversationAttachments.push(file); + renderConversationAttachmentsList(); + stream.getTracks().forEach(track => track.stop()); + }; + + videoRecorder.start(); + + // Show recording UI + const recordStatus = document.getElementById('conversationRecordStatus'); + const originalStatus = recordStatus.textContent; + recordStatus.textContent = 'Recording video... (Click to stop)'; + recordStatus.style.cursor = 'pointer'; + recordStatus.style.color = '#dc2626'; + + const stopHandler = () => { + recordStatus.removeEventListener('click', stopHandler); + videoRecorder.stop(); + recordStatus.textContent = originalStatus; + recordStatus.style.cursor = 'auto'; + recordStatus.style.color = ''; + }; + + recordStatus.addEventListener('click', stopHandler); + + // Auto-stop after 5 minutes + setTimeout(() => { + if (videoRecorder.state === 'recording') { + recordStatus.removeEventListener('click', stopHandler); + videoRecorder.stop(); + recordStatus.textContent = 'Video recording stopped (5 minute limit)'; + recordStatus.style.cursor = 'auto'; + setTimeout(() => { + recordStatus.textContent = originalStatus; + recordStatus.style.color = ''; + }, 3000); + } + }, 5 * 60 * 1000); + } catch (err) { + console.error('Failed to access camera/microphone:', err); + alert('Unable to access camera or microphone. Check permissions.'); + } + }); + + // File input change handlers for conversation + [conversationImageInput, conversationVideoInput, conversationFileInput, conversationCameraInput].forEach(input => { + input.addEventListener('change', (e) => { + pendingConversationAttachments.push(...Array.from(e.target.files || [])); + renderConversationAttachmentsList(); + }); + }); + + document.getElementById('conversationClearAttachmentsBtn').addEventListener('click', (e) => { + e.preventDefault(); + pendingConversationAttachments = []; + conversationImageInput.value = ''; + conversationVideoInput.value = ''; + conversationFileInput.value = ''; + conversationCameraInput.value = ''; + renderConversationAttachmentsList(); + }); + + // Close conversation menu on outside click + document.addEventListener('click', (e) => { + if (!conversationAttachmentMenuBtn.contains(e.target) && !conversationAttachmentMenu.contains(e.target)) { + conversationAttachmentMenu.classList.add('hidden'); + } + }); + // Search input + voice const notesSearchInput = document.getElementById('notesSearch'); const searchVoiceBtn = document.getElementById('searchVoiceBtn'); @@ -1758,9 +2116,7 @@ const createdAtt = await pb.collection(ATTACHMENTS_COLLECTION).create(fd); attachmentRecords.push(createdAtt); } - const inputEl = document.getElementById('noteAttachmentsInput'); - const filesFromInput = Array.from(inputEl?.files || []); - const filesToUpload = (pendingNoteAttachments && pendingNoteAttachments.length) ? pendingNoteAttachments : filesFromInput; + const filesToUpload = (pendingNoteAttachments && pendingNoteAttachments.length) ? pendingNoteAttachments : []; if (filesToUpload && filesToUpload.length) { const fdFiles = new FormData(); for (let i = 0; i < filesToUpload.length; i++) { @@ -1784,7 +2140,10 @@ statusEl.textContent = `Saved note ${noteRecord.id}${attachmentMsg}`; await clearAudio(); pendingNoteAttachments = []; - document.getElementById('noteAttachmentsInput').value = ''; + document.getElementById('imageInput').value = ''; + document.getElementById('videoInput').value = ''; + document.getElementById('fileInput').value = ''; + document.getElementById('cameraInput').value = ''; renderNoteAttachmentsList(); document.getElementById('noteText').value = ''; document.getElementById('noteTitle').value = ''; diff --git a/logs/SESSION_LOG.txt b/logs/SESSION_LOG.txt index 530087c..54b0000 100644 --- a/logs/SESSION_LOG.txt +++ b/logs/SESSION_LOG.txt @@ -3,6 +3,16 @@ PRISM NOTES - SESSION LOG Project history, decisions, and architectural milestones ================================================================================ +[2026-01-10 21:45] MONICA SESSION: Mobile Layout & Scrolling Fixes +- Changes Applied: + * Kept button group beside Notes heading on all screen sizes (flex-row always) + * Removed graph status badges from form containers + * Added overflow-x-hidden to Notes and Conversations card containers + * Prevents horizontal scrolling/swiping that made cards feel "floating" + * Allows vertical scroll to remain responsive +- Result: Cards now stay stationary horizontally, only scroll up/down normally +- Status: Fixed and committed (4bb4e54 and 0dc0ffb) + [2026-01-10 21:15] MONICA SESSION: Refined Notes List Header UI - Changes Applied: * Removed graph status badges from note/conversation form containers