Compare commits

...

2 Commits

Author SHA1 Message Date
aewing 77312dfdad Add share note functionality with multiselect field support and adjust layout spacing
- Added share note input field with comma-separated name parsing
- Integrated shared_with and shared boolean fields to note payload
- Reduced form spacing (space-y-4 to space-y-2.5) to accommodate new field
- Adjusted container positioning: noteContainer translateY(-33px), notesListContainer translateY(-24px)
- Share functionality now working with PocketBase multiselect field validation
- Note cards will display green background when shared=true
2026-01-11 06:30:21 +00:00
aewing 8a0bf92da4 Add note sharing feature: user selection, shared status, green card styling, and type badges 2026-01-11 05:38:07 +00:00
2 changed files with 254 additions and 21 deletions
+239 -21
View File
@@ -51,9 +51,16 @@
window.captureSnip = async function() { window.captureSnip = async function() {
console.log('captureSnip called'); console.log('captureSnip called');
return new Promise((resolve) => { 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({ navigator.mediaDevices.getDisplayMedia({
video: { mediaSource: 'screen' } video: { mediaSource: 'screen' }
}).then(mediaStream => { }).then(mediaStream => {
// Restore visibility
appElements.forEach(el => el.style.visibility = '');
const video = document.createElement('video'); const video = document.createElement('video');
video.srcObject = mediaStream; video.srcObject = mediaStream;
@@ -225,6 +232,8 @@
}; };
video.play(); video.play();
}).catch(error => { }).catch(error => {
// Restore visibility on error
appElements.forEach(el => el.style.visibility = '');
console.error('Capture failed:', error); console.error('Capture failed:', error);
resolve(null); 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('✓ Capture tools initialized successfully');
console.log('Available: window.captureScreenshot, window.captureSnip, window.canvasToFile'); console.log('Available: window.captureScreenshot, window.captureSnip, window.canvasToFile, window.recordScreen');
</script> </script>
<script src="./tools/screen-record/index.js"></script>
</head> </head>
<body class="bg-gradient-to-br from-primary to-secondary p-4 sm:p-8 flex flex-col items-center justify-center font-sans relative h-screen overflow-hidden"> <body class="bg-gradient-to-br from-primary to-secondary p-4 sm:p-8 flex flex-col items-center justify-center font-sans relative h-screen overflow-hidden">
<!-- Top-right display name --> <!-- Top-right display name -->
@@ -281,7 +369,7 @@
</div> </div>
<!-- Notes List Container --> <!-- Notes List Container -->
<div id="notesListContainer" class="hidden flex flex-col bg-purple-100 rounded-2xl shadow-2xl border border-purple-200 max-w-5xl w-full p-6 gap-4 max-h-[calc(100vh-136px)] overflow-hidden mt-6 mb-3"> <div id="notesListContainer" class="hidden flex flex-col bg-purple-100 rounded-2xl shadow-2xl border border-purple-200 max-w-5xl w-full p-6 gap-4 max-h-[calc(100vh-136px)] overflow-hidden mb-3" style="transform: translateY(-24px);">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<div class="flex flex-row items-center justify-between gap-3"> <div class="flex flex-row items-center justify-between gap-3">
<div> <div>
@@ -351,12 +439,12 @@
</div> </div>
<!-- Note + Audio Container --> <!-- Note + Audio Container -->
<div id="noteContainer" class="hidden self-center bg-white rounded-2xl shadow-2xl max-w-xl w-full py-[8px] px-8 space-y-6 mt-4"> <div id="noteContainer" class="hidden self-center bg-white rounded-2xl shadow-2xl max-w-xl w-full py-[8px] px-8 space-y-2 mt-4" style="transform: translateY(-33px);">
<div class="flex items-center gap-2 flex-wrap"> <div class="flex items-center gap-2 flex-wrap">
<button id="backToListBtn" class="px-2 sm:px-3 py-1 sm:py-2 text-xs sm:text-sm rounded border border-gray-200 text-gray-700 hover:bg-gray-100">Back to Notes</button> <button id="backToListBtn" class="px-2 sm:px-3 py-1 sm:py-2 text-xs sm:text-sm rounded border border-gray-200 text-gray-700 hover:bg-gray-100">Back to Notes</button>
<h2 class="text-lg sm:text-2xl font-bold text-gray-800">New Note</h2> <h2 class="text-lg sm:text-2xl font-bold text-gray-800">New Note</h2>
</div> </div>
<div class="space-y-4"> <div class="space-y-2.5">
<div> <div>
<label for="noteTitle" class="block text-sm font-medium text-gray-700 mb-1">Title</label> <label for="noteTitle" class="block text-sm font-medium text-gray-700 mb-1">Title</label>
<div class="relative"> <div class="relative">
@@ -389,6 +477,13 @@
<label for="jobNumber" class="block text-sm font-medium text-gray-700 mb-1">Job Number <span class="text-red-600">*</span></label> <label for="jobNumber" class="block text-sm font-medium text-gray-700 mb-1">Job Number <span class="text-red-600">*</span></label>
<input type="text" id="jobNumber" class="w-full rounded-lg border border-gray-300 p-3 focus:outline-none focus:ring-2 focus:ring-primary" placeholder="Enter job number (required)"> <input type="text" id="jobNumber" class="w-full rounded-lg border border-gray-300 p-3 focus:outline-none focus:ring-2 focus:ring-primary" placeholder="Enter job number (required)">
</div> </div>
<div>
<label for="shareNoteInput" class="block text-sm font-medium text-gray-700 mb-2">Share Note (comma-separated names)</label>
<input type="text" id="shareNoteInput" class="w-full rounded-lg border border-gray-300 p-3 focus:outline-none focus:ring-2 focus:ring-primary" placeholder="Enter names separated by commas (e.g., John, Sarah, Mike)">
<div id="selectedUsersContainer" class="flex flex-wrap gap-2 mt-3">
<!-- Selected user capsules will appear here -->
</div>
</div>
<div> <div>
<label for="noteText" class="block text-sm font-medium text-gray-700 mb-1">Note Text</label> <label for="noteText" class="block text-sm font-medium text-gray-700 mb-1">Note Text</label>
<div class="relative"> <div class="relative">
@@ -1713,6 +1808,9 @@
document.getElementById('noteType').value = 'personal'; document.getElementById('noteType').value = 'personal';
document.getElementById('jobNumber').value = ''; document.getElementById('jobNumber').value = '';
document.getElementById('jobNumberContainer').classList.add('hidden'); document.getElementById('jobNumberContainer').classList.add('hidden');
document.getElementById('shareNoteInput').value = '';
selectedUsers = [];
renderSelectedUsers();
clearAudio(); clearAudio();
pendingNoteAttachments = []; pendingNoteAttachments = [];
const noteAttachmentsInput = document.getElementById('noteAttachmentsInput'); const noteAttachmentsInput = document.getElementById('noteAttachmentsInput');
@@ -1751,11 +1849,19 @@
card.type = 'button'; card.type = 'button';
card.dataset.noteId = note.id; card.dataset.noteId = note.id;
const type = (note.type || 'personal').toLowerCase(); const type = (note.type || 'personal').toLowerCase();
const typeClasses = {
personal: 'bg-amber-100 border border-amber-300 hover:border-amber-400', // Use green background for shared notes, otherwise use type-based colors
manager: 'bg-orange-200 border border-orange-400 hover:border-orange-500', let typeClasses;
job: 'bg-pink-200 border border-pink-400 hover:border-pink-500', if (note.shared) {
}[type] || 'bg-gray-100 border border-gray-300 hover:border-primary/60'; 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`; 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 title = note.title || 'Untitled';
const plain = note[NOTE_BODY_PLAIN_FIELD] || (note[NOTE_BODY_HTML_FIELD] ? htmlToPlainText(note[NOTE_BODY_HTML_FIELD]) : ''); const plain = note[NOTE_BODY_PLAIN_FIELD] || (note[NOTE_BODY_HTML_FIELD] ? htmlToPlainText(note[NOTE_BODY_HTML_FIELD]) : '');
@@ -1776,6 +1882,19 @@
.filter((k) => typeSet.has(k)) .filter((k) => typeSet.has(k))
.map(iconBtnHtml) .map(iconBtnHtml)
.join(''); .join('');
// Only create type indicator badge for shared notes
let typeBadge = '';
if (note.shared) {
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' };
typeBadge = `<span class="inline-block px-2 py-1 rounded-full text-xs font-semibold ${typeColor.bg} ${typeColor.border} border ${typeColor.text}">${escapeHtml(type)}</span>`;
}
card.innerHTML = ` card.innerHTML = `
<div class="flex items-center justify-between gap-2"> <div class="flex items-center justify-between gap-2">
<div class="font-semibold text-gray-900 truncate">${escapeHtml(title)}</div> <div class="font-semibold text-gray-900 truncate">${escapeHtml(title)}</div>
@@ -1784,7 +1903,10 @@
<div class="text-sm text-gray-700 line-clamp-3 flex-1">${escapeHtml(snippet || '(empty)')}</div> <div class="text-sm text-gray-700 line-clamp-3 flex-1">${escapeHtml(snippet || '(empty)')}</div>
<div class="flex items-center justify-between text-xs text-gray-600"> <div class="flex items-center justify-between text-xs text-gray-600">
<span>${escapeHtml(meta)}</span> <span>${escapeHtml(meta)}</span>
<div class="flex items-center gap-1">${attachmentIcons}</div> <div class="flex items-center gap-1">
${attachmentIcons}
${typeBadge}
</div>
</div> </div>
`; `;
frag.appendChild(card); frag.appendChild(card);
@@ -2148,11 +2270,36 @@
const recorder = await window.recordScreen(); const recorder = await window.recordScreen();
if (recorder) { if (recorder) {
// Auto-stop after 5 minutes // Create stop button overlay
setTimeout(async () => { 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(); const file = await recorder.stop();
pendingNoteAttachments.push(file); if (file) {
renderNoteAttachmentsList(); 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); }, 5 * 60 * 1000);
} }
} catch (error) { } catch (error) {
@@ -2270,11 +2417,36 @@
const recorder = await window.recordScreen(); const recorder = await window.recordScreen();
if (recorder) { if (recorder) {
// Auto-stop after 5 minutes // Create stop button overlay
setTimeout(async () => { 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(); const file = await recorder.stop();
pendingConversationAttachments.push(file); if (file) {
renderConversationAttachmentsList(); 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); }, 5 * 60 * 1000);
} }
} catch (error) { } catch (error) {
@@ -2371,6 +2543,46 @@
} }
}); });
// --- Share Note Logic ---
let selectedUsers = []; // { name }
const shareNoteInput = document.getElementById('shareNoteInput');
const selectedUsersContainer = document.getElementById('selectedUsersContainer');
shareNoteInput.addEventListener('input', (e) => {
const input = e.target.value;
const names = input.split(',')
.map(name => name.trim())
.filter(name => name.length > 0);
selectedUsers = names.map(name => ({
name: name,
firstName: name.split(' ')[0]
}));
renderSelectedUsers();
});
function renderSelectedUsers() {
selectedUsersContainer.innerHTML = '';
selectedUsers.forEach((user, index) => {
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 = `
<span>${escapeHtml(user.firstName)}</span>
<button type="button" class="text-gray-600 hover:text-gray-800 font-bold" data-remove-user="${index}">×</button>
`;
const removeBtn = capsule.querySelector('[data-remove-user]');
removeBtn.addEventListener('click', (e) => {
e.preventDefault();
selectedUsers = selectedUsers.filter((_, i) => i !== index);
const remaining = selectedUsers.map(u => u.name).join(', ');
shareNoteInput.value = remaining;
renderSelectedUsers();
});
selectedUsersContainer.appendChild(capsule);
});
}
// --- Submit note --- // --- Submit note ---
async function submitNote() { async function submitNote() {
const statusEl = document.getElementById('submitStatus'); const statusEl = document.getElementById('submitStatus');
@@ -2420,6 +2632,8 @@
job_note: jobNoteValue === 'yes', job_note: jobNoteValue === 'yes',
type: noteType, type: noteType,
userId: pb.authStore.model?.id || pb.authStore.record?.id, userId: pb.authStore.model?.id || pb.authStore.record?.id,
shared: selectedUsers.length > 0 ? true : false,
shared_with: selectedUsers.map(u => u.name),
}; };
// Only include job_number if it has a value // Only include job_number if it has a value
@@ -2466,7 +2680,7 @@
if (audioAttachmentCount) attachmentParts.push(`${audioAttachmentCount} audio`); if (audioAttachmentCount) attachmentParts.push(`${audioAttachmentCount} audio`);
if (fileAttachmentCount) attachmentParts.push(`${fileAttachmentCount} file`); if (fileAttachmentCount) attachmentParts.push(`${fileAttachmentCount} file`);
const attachmentMsg = attachmentParts.length ? ` with ${attachmentParts.join(' and ')} attachment(s)` : ''; const attachmentMsg = attachmentParts.length ? ` with ${attachmentParts.join(' and ')} attachment(s)` : '';
statusEl.textContent = `Saved note ${noteRecord.id}${attachmentMsg}`; statusEl.textContent = '';
await clearAudio(); await clearAudio();
pendingNoteAttachments = []; pendingNoteAttachments = [];
document.getElementById('imageInput').value = ''; document.getElementById('imageInput').value = '';
@@ -2484,7 +2698,11 @@
loadNotesList(); loadNotesList();
showNotesList(); showNotesList();
} catch (e) { } catch (e) {
console.error(e); console.error('Error creating note:', e);
console.error('Full error:', JSON.stringify(e, null, 2));
if (e.response) {
console.error('Response data:', e.response);
}
statusEl.textContent = `Error saving: ${e.message}`; statusEl.textContent = `Error saving: ${e.message}`;
} }
} }
+15
View File
@@ -3,6 +3,21 @@ PRISM NOTES - SESSION LOG
Project history, decisions, and architectural milestones 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 [2026-01-10 20:10] MONICA SESSION: Fixed Container Injection Bug
- Issue: Refactor commit (cee9157) had CONTAINERS + initializeContainers() code - Issue: Refactor commit (cee9157) had CONTAINERS + initializeContainers() code
but ALSO kept all old HTML in DOM, creating duplicates and preventing injection but ALSO kept all old HTML in DOM, creating duplicates and preventing injection