From 8a0bf92da43a456fbc7d721b2e2ec92fb8cd9b85 Mon Sep 17 00:00:00 2001 From: aewing Date: Sun, 11 Jan 2026 05:38:07 +0000 Subject: [PATCH] Add note sharing feature: user selection, shared status, green card styling, and type badges --- index.html | 301 ++++++++++++++++++++++++++++++++++++++++--- logs/SESSION_LOG.txt | 15 +++ 2 files changed, 297 insertions(+), 19 deletions(-) diff --git a/index.html b/index.html index e40f5bb..b17cb8e 100644 --- a/index.html +++ b/index.html @@ -51,9 +51,16 @@ window.captureSnip = async function() { console.log('captureSnip called'); return new Promise((resolve) => { + // Hide main app elements before showing dialog + const appElements = document.querySelectorAll('body > div:not([style*="z-index"])'); + appElements.forEach(el => el.style.visibility = 'hidden'); + navigator.mediaDevices.getDisplayMedia({ video: { mediaSource: 'screen' } }).then(mediaStream => { + // Restore visibility + appElements.forEach(el => el.style.visibility = ''); + const video = document.createElement('video'); video.srcObject = mediaStream; @@ -225,6 +232,8 @@ }; video.play(); }).catch(error => { + // Restore visibility on error + appElements.forEach(el => el.style.visibility = ''); console.error('Capture failed:', error); resolve(null); }); @@ -241,11 +250,90 @@ }); }); }; + + window.recordScreen = async function(options = {}) { + const { + maxDuration = 5 * 60 * 1000, + mimeType = 'video/webm', + onStart = null, + onStop = null + } = options; + + try { + const mediaStream = await navigator.mediaDevices.getDisplayMedia({ + video: { mediaSource: 'screen' }, + audio: false + }); + + const mediaRecorder = new MediaRecorder(mediaStream, { mimeType }); + const chunks = []; + let isRecording = true; + let timeoutId = null; + + mediaRecorder.ondataavailable = (event) => { + if (event.data.size > 0) { + chunks.push(event.data); + } + }; + + mediaRecorder.onstop = () => { + isRecording = false; + clearTimeout(timeoutId); + mediaStream.getTracks().forEach(track => track.stop()); + if (onStop) onStop(); + }; + + mediaRecorder.start(); + if (onStart) onStart(); + + timeoutId = setTimeout(() => { + if (isRecording) { + mediaRecorder.stop(); + } + }, maxDuration); + + mediaStream.getVideoTracks()[0].onended = () => { + if (isRecording) { + mediaRecorder.stop(); + } + }; + + return { + stop: async () => { + return new Promise((resolve) => { + if (!isRecording) { + resolve(null); + return; + } + + const stopHandler = () => { + mediaRecorder.removeEventListener('stop', stopHandler); + const blob = new Blob(chunks, { type: mimeType }); + const file = new File( + [blob], + `screen-recording-${Date.now()}.webm`, + { type: mimeType } + ); + resolve(file); + }; + + mediaRecorder.addEventListener('stop', stopHandler); + mediaRecorder.stop(); + }); + }, + isRecording: () => isRecording, + getDuration: () => mediaRecorder.state === 'recording' ? mediaRecorder.state : 0, + mediaStream + }; + } catch (error) { + console.error('Screen recording failed:', error); + return null; + } + }; console.log('✓ Capture tools initialized successfully'); - console.log('Available: window.captureScreenshot, window.captureSnip, window.canvasToFile'); + console.log('Available: window.captureScreenshot, window.captureSnip, window.canvasToFile, window.recordScreen'); - @@ -389,6 +477,16 @@ +
+ +
+ + +
+
+ +
+
@@ -1713,6 +1811,9 @@ document.getElementById('noteType').value = 'personal'; document.getElementById('jobNumber').value = ''; document.getElementById('jobNumberContainer').classList.add('hidden'); + document.getElementById('userSearchInput').value = ''; + selectedUsers = []; + renderSelectedUsers(); clearAudio(); pendingNoteAttachments = []; const noteAttachmentsInput = document.getElementById('noteAttachmentsInput'); @@ -1751,11 +1852,19 @@ card.type = 'button'; card.dataset.noteId = note.id; const type = (note.type || 'personal').toLowerCase(); - const typeClasses = { - personal: 'bg-amber-100 border border-amber-300 hover:border-amber-400', - manager: 'bg-orange-200 border border-orange-400 hover:border-orange-500', - job: 'bg-pink-200 border border-pink-400 hover:border-pink-500', - }[type] || 'bg-gray-100 border border-gray-300 hover:border-primary/60'; + + // Use green background for shared notes, otherwise use type-based colors + let typeClasses; + if (note.shared) { + typeClasses = 'bg-green-100 border border-green-300 hover:border-green-400'; + } else { + typeClasses = { + personal: 'bg-amber-100 border border-amber-300 hover:border-amber-400', + manager: 'bg-orange-200 border border-orange-400 hover:border-orange-500', + job: 'bg-pink-200 border border-pink-400 hover:border-pink-500', + }[type] || 'bg-gray-100 border border-gray-300 hover:border-primary/60'; + } + card.className = `text-left ${typeClasses} hover:shadow-md transition rounded-xl px-2 py-0.5 space-y-0 h-full flex flex-col`; const title = note.title || 'Untitled'; const plain = note[NOTE_BODY_PLAIN_FIELD] || (note[NOTE_BODY_HTML_FIELD] ? htmlToPlainText(note[NOTE_BODY_HTML_FIELD]) : ''); @@ -1776,15 +1885,25 @@ .filter((k) => typeSet.has(k)) .map(iconBtnHtml) .join(''); + + // Create type indicator badge with matching card background colors + const typeColorMap = { + personal: { bg: 'bg-amber-200', border: 'border-amber-400', text: 'text-amber-900' }, + manager: { bg: 'bg-orange-300', border: 'border-orange-500', text: 'text-orange-900' }, + job: { bg: 'bg-pink-300', border: 'border-pink-500', text: 'text-pink-900' }, + }; + const typeColor = typeColorMap[type] || { bg: 'bg-gray-200', border: 'border-gray-400', text: 'text-gray-900' }; + const typeBadge = `${escapeHtml(type)}`; + card.innerHTML = `
${escapeHtml(title)}
${formatDateShort(note.created)}
-
${escapeHtml(snippet || '(empty)')}
-
- ${escapeHtml(meta)} +
${escapeHtml(snippet || '(empty)')}
+
${attachmentIcons}
+ ${typeBadge}
`; frag.appendChild(card); @@ -2148,11 +2267,36 @@ const recorder = await window.recordScreen(); if (recorder) { - // Auto-stop after 5 minutes - setTimeout(async () => { + // Create stop button overlay + const stopOverlay = document.createElement('div'); + stopOverlay.style.cssText = `position:fixed;top:20px;right:20px;z-index:50000;background:white;padding:15px 20px;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.15);`; + + const stopBtn = document.createElement('button'); + stopBtn.textContent = '⏹ Stop Recording'; + stopBtn.style.cssText = `padding:10px 20px;background:#EF4444;color:white;border:none;border-radius:6px;font-size:14px;font-weight:bold;cursor:pointer;`; + + stopBtn.addEventListener('click', async () => { const file = await recorder.stop(); - pendingNoteAttachments.push(file); - renderNoteAttachmentsList(); + if (file) { + pendingNoteAttachments.push(file); + renderNoteAttachmentsList(); + } + stopOverlay.remove(); + }); + + stopOverlay.appendChild(stopBtn); + document.body.appendChild(stopOverlay); + + // Auto-stop after 5 minutes as fallback + setTimeout(async () => { + if (recorder.isRecording()) { + const file = await recorder.stop(); + if (file) { + pendingNoteAttachments.push(file); + renderNoteAttachmentsList(); + } + stopOverlay.remove(); + } }, 5 * 60 * 1000); } } catch (error) { @@ -2270,11 +2414,36 @@ const recorder = await window.recordScreen(); if (recorder) { - // Auto-stop after 5 minutes - setTimeout(async () => { + // Create stop button overlay + const stopOverlay = document.createElement('div'); + stopOverlay.style.cssText = `position:fixed;top:20px;right:20px;z-index:50000;background:white;padding:15px 20px;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.15);`; + + const stopBtn = document.createElement('button'); + stopBtn.textContent = '⏹ Stop Recording'; + stopBtn.style.cssText = `padding:10px 20px;background:#EF4444;color:white;border:none;border-radius:6px;font-size:14px;font-weight:bold;cursor:pointer;`; + + stopBtn.addEventListener('click', async () => { const file = await recorder.stop(); - pendingConversationAttachments.push(file); - renderConversationAttachmentsList(); + if (file) { + pendingConversationAttachments.push(file); + renderConversationAttachmentsList(); + } + stopOverlay.remove(); + }); + + stopOverlay.appendChild(stopBtn); + document.body.appendChild(stopOverlay); + + // Auto-stop after 5 minutes as fallback + setTimeout(async () => { + if (recorder.isRecording()) { + const file = await recorder.stop(); + if (file) { + pendingConversationAttachments.push(file); + renderConversationAttachmentsList(); + } + stopOverlay.remove(); + } }, 5 * 60 * 1000); } } catch (error) { @@ -2371,6 +2540,98 @@ } }); + // --- Share Note Logic --- + let selectedUsers = []; // { id, displayName, firstName } + const userSearchInput = document.getElementById('userSearchInput'); + const userSuggestions = document.getElementById('userSuggestions'); + const selectedUsersContainer = document.getElementById('selectedUsersContainer'); + + userSearchInput.addEventListener('input', async (e) => { + const query = e.target.value.trim().toLowerCase(); + userSuggestions.innerHTML = ''; + + if (!query) { + userSuggestions.classList.add('hidden'); + return; + } + + try { + const allUsers = await pb.collection('users').getFullList({ + sort: 'displayName' + }); + + const matching = allUsers.filter(u => + u.displayName?.toLowerCase().includes(query) || + u.firstName?.toLowerCase().includes(query) || + u.email?.toLowerCase().includes(query) + ).filter(u => !selectedUsers.find(su => su.id === u.id)); + + if (matching.length === 0) { + userSuggestions.innerHTML = '
No users found
'; + } else { + matching.forEach(user => { + const option = document.createElement('button'); + option.type = 'button'; + option.className = 'w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition'; + option.innerHTML = `
${escapeHtml(user.displayName || user.email)}
${escapeHtml(user.email)}
`; + option.addEventListener('click', (ev) => { + ev.preventDefault(); + selectUser(user); + userSearchInput.value = ''; + userSuggestions.classList.add('hidden'); + }); + userSuggestions.appendChild(option); + }); + } + userSuggestions.classList.remove('hidden'); + } catch (err) { + console.error('Failed to load users:', err); + userSuggestions.innerHTML = '
Failed to load users
'; + } + }); + + function selectUser(user) { + if (selectedUsers.find(u => u.id === user.id)) return; + + selectedUsers.push({ + id: user.id, + displayName: user.displayName || user.email, + firstName: user.firstName || (user.displayName?.split(' ')[0] || 'User') + }); + + renderSelectedUsers(); + } + + function removeUser(userId) { + selectedUsers = selectedUsers.filter(u => u.id !== userId); + renderSelectedUsers(); + } + + function renderSelectedUsers() { + selectedUsersContainer.innerHTML = ''; + selectedUsers.forEach(user => { + const capsule = document.createElement('div'); + capsule.className = 'inline-flex items-center gap-2 px-3 py-1 rounded-full bg-green-100 border border-green-300 text-sm text-gray-800'; + capsule.innerHTML = ` + ${escapeHtml(user.firstName)} + + `; + const removeBtn = capsule.querySelector('[data-remove-user]'); + removeBtn.addEventListener('click', (e) => { + e.preventDefault(); + removeUser(user.id); + }); + selectedUsersContainer.appendChild(capsule); + }); + } + + // Close suggestions when clicking outside + document.addEventListener('click', (e) => { + if (!e.target.closest('#userSearchInput') && !e.target.closest('#userSuggestions')) { + userSuggestions.classList.add('hidden'); + } + }); + // --- Submit note --- async function submitNote() { const statusEl = document.getElementById('submitStatus'); @@ -2420,6 +2681,8 @@ job_note: jobNoteValue === 'yes', type: noteType, userId: pb.authStore.model?.id || pb.authStore.record?.id, + shared: selectedUsers.length > 0, + shared_with: selectedUsers.map(u => u.id), }; // Only include job_number if it has a value @@ -2466,7 +2729,7 @@ if (audioAttachmentCount) attachmentParts.push(`${audioAttachmentCount} audio`); if (fileAttachmentCount) attachmentParts.push(`${fileAttachmentCount} file`); const attachmentMsg = attachmentParts.length ? ` with ${attachmentParts.join(' and ')} attachment(s)` : ''; - statusEl.textContent = `Saved note ${noteRecord.id}${attachmentMsg}`; + statusEl.textContent = ''; await clearAudio(); pendingNoteAttachments = []; document.getElementById('imageInput').value = ''; diff --git a/logs/SESSION_LOG.txt b/logs/SESSION_LOG.txt index 2f59380..88bbb36 100644 --- a/logs/SESSION_LOG.txt +++ b/logs/SESSION_LOG.txt @@ -3,6 +3,21 @@ PRISM NOTES - SESSION LOG Project history, decisions, and architectural milestones ================================================================================ +[2026-01-11 TODO] Screen Capture Window Visibility Issue - INVESTIGATE LATER +- Issue: Prism Notes window still appears in getDisplayMedia() dialog options + even though visibility is hidden before dialog opens +- Attempted Fix: Used visibility:hidden on app elements before getDisplayMedia() +- Result: Not working - window still shows in screen selector +- Root Cause: Unclear - may be timing issue or browser caching of window list +- Workaround: User can manually select other screen/window instead +- Investigation Needed: + * Try alternative hiding methods (display:none, opacity:0, positioning off-screen) + * Check if timing delay needed before getDisplayMedia() + * Verify with different browsers (Chrome vs Edge vs Firefox) + * Consider if OS-level window management interferes +- Impact: Minor UX issue - doesn't prevent functionality, just less elegant +- Priority: LOW - defer until screen capture feature stabilized otherwise + [2026-01-10 20:10] MONICA SESSION: Fixed Container Injection Bug - Issue: Refactor commit (cee9157) had CONTAINERS + initializeContainers() code but ALSO kept all old HTML in DOM, creating duplicates and preventing injection