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
This commit is contained in:
+371
-12
@@ -212,12 +212,60 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<label class="block text-sm font-medium text-gray-700">Attachments</label>
|
<label class="block text-sm font-medium text-gray-700">Attachments</label>
|
||||||
<input id="noteAttachmentsInput" type="file" multiple class="w-full text-sm" accept="image/*,video/*,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt">
|
<div class="relative">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<button id="clearAttachmentsBtn" class="px-3 py-1 rounded border border-gray-200 text-gray-700 hover:bg-gray-100 text-sm" type="button">Clear</button>
|
<button id="attachmentMenuBtn" class="px-3 py-2 rounded border border-gray-300 text-gray-700 hover:bg-gray-100 transition flex items-center gap-2" type="button" title="Add attachment">
|
||||||
<div id="noteAttachmentsSummary" class="text-xs text-gray-600">No attachments selected.</div>
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="w-5 h-5">
|
||||||
|
<path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"/>
|
||||||
|
</svg>
|
||||||
|
Attach
|
||||||
|
</button>
|
||||||
|
<button id="clearAttachmentsBtn" class="px-3 py-1 rounded border border-gray-200 text-gray-700 hover:bg-gray-100 text-sm" type="button">Clear</button>
|
||||||
|
<div id="noteAttachmentsSummary" class="text-xs text-gray-600">No attachments selected.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Attachment Menu -->
|
||||||
|
<div id="attachmentMenu" class="hidden absolute top-full left-0 mt-2 w-48 rounded border border-gray-300 bg-white shadow-lg z-10">
|
||||||
|
<button id="attachImageBtn" class="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2" type="button">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="w-4 h-4">
|
||||||
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/>
|
||||||
|
</svg>
|
||||||
|
Attach Image
|
||||||
|
</button>
|
||||||
|
<button id="attachVideoBtn" class="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2" type="button">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="w-4 h-4">
|
||||||
|
<polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>
|
||||||
|
</svg>
|
||||||
|
Attach Video
|
||||||
|
</button>
|
||||||
|
<button id="attachFileBtn" class="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2" type="button">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="w-4 h-4">
|
||||||
|
<path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><polyline points="13 2 13 9 20 9"/>
|
||||||
|
</svg>
|
||||||
|
Attach File
|
||||||
|
</button>
|
||||||
|
<button id="recordVideoBtn" class="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2" type="button">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="w-4 h-4">
|
||||||
|
<circle cx="12" cy="13" r="9"/><path d="M12 10v6"/>
|
||||||
|
</svg>
|
||||||
|
Record Video
|
||||||
|
</button>
|
||||||
|
<button id="takePictureBtn" class="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2" type="button">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="w-4 h-4">
|
||||||
|
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/>
|
||||||
|
</svg>
|
||||||
|
Take Picture
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Hidden file inputs -->
|
||||||
|
<input id="imageInput" type="file" class="hidden" accept="image/*" multiple>
|
||||||
|
<input id="videoInput" type="file" class="hidden" accept="video/*">
|
||||||
|
<input id="fileInput" type="file" class="hidden" multiple accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.zip,.rar">
|
||||||
|
<input id="cameraInput" type="file" class="hidden" accept="image/*" capture="environment">
|
||||||
|
|
||||||
<div id="noteAttachmentsList" class="space-y-1"></div>
|
<div id="noteAttachmentsList" class="space-y-1"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<button id="submitNoteBtn" class="px-4 py-2 rounded bg-secondary text-white font-semibold">Submit Note</button>
|
<button id="submitNoteBtn" class="px-4 py-2 rounded bg-secondary text-white font-semibold">Submit Note</button>
|
||||||
@@ -340,6 +388,64 @@
|
|||||||
<div id="conversationRecordStatus" class="text-xs text-gray-500"></div>
|
<div id="conversationRecordStatus" class="text-xs text-gray-500"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="block text-sm font-medium text-gray-700">Attachments</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button id="conversationAttachmentMenuBtn" class="px-3 py-2 rounded border border-gray-300 text-gray-700 hover:bg-gray-100 transition flex items-center gap-2" type="button" title="Add attachment">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="w-5 h-5">
|
||||||
|
<path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"/>
|
||||||
|
</svg>
|
||||||
|
Attach
|
||||||
|
</button>
|
||||||
|
<button id="conversationClearAttachmentsBtn" class="px-3 py-1 rounded border border-gray-200 text-gray-700 hover:bg-gray-100 text-sm" type="button">Clear</button>
|
||||||
|
<div id="conversationAttachmentsSummary" class="text-xs text-gray-600">No attachments selected.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Conversation Attachment Menu -->
|
||||||
|
<div id="conversationAttachmentMenu" class="hidden absolute top-full left-0 mt-2 w-48 rounded border border-gray-300 bg-white shadow-lg z-10">
|
||||||
|
<button id="conversationAttachImageBtn" class="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2" type="button">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="w-4 h-4">
|
||||||
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/>
|
||||||
|
</svg>
|
||||||
|
Attach Image
|
||||||
|
</button>
|
||||||
|
<button id="conversationAttachVideoBtn" class="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2" type="button">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="w-4 h-4">
|
||||||
|
<polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>
|
||||||
|
</svg>
|
||||||
|
Attach Video
|
||||||
|
</button>
|
||||||
|
<button id="conversationAttachFileBtn" class="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2" type="button">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="w-4 h-4">
|
||||||
|
<path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><polyline points="13 2 13 9 20 9"/>
|
||||||
|
</svg>
|
||||||
|
Attach File
|
||||||
|
</button>
|
||||||
|
<button id="conversationRecordVideoBtn" class="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2" type="button">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="w-4 h-4">
|
||||||
|
<circle cx="12" cy="13" r="9"/><path d="M12 10v6"/>
|
||||||
|
</svg>
|
||||||
|
Record Video
|
||||||
|
</button>
|
||||||
|
<button id="conversationTakePictureBtn" class="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2" type="button">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="w-4 h-4">
|
||||||
|
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/>
|
||||||
|
</svg>
|
||||||
|
Take Picture
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Hidden file inputs for conversation -->
|
||||||
|
<input id="conversationImageInput" type="file" class="hidden" accept="image/*" multiple>
|
||||||
|
<input id="conversationVideoInput" type="file" class="hidden" accept="video/*">
|
||||||
|
<input id="conversationFileInput" type="file" class="hidden" multiple accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.zip,.rar">
|
||||||
|
<input id="conversationCameraInput" type="file" class="hidden" accept="image/*" capture="environment">
|
||||||
|
|
||||||
|
<div id="conversationAttachmentsList" class="space-y-1"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<button id="submitConversationBtn" class="px-4 py-2 rounded bg-secondary text-white font-semibold">Submit Conversation</button>
|
<button id="submitConversationBtn" class="px-4 py-2 rounded bg-secondary text-white font-semibold">Submit Conversation</button>
|
||||||
<div id="conversationSubmitStatus" class="text-sm text-gray-600"></div>
|
<div id="conversationSubmitStatus" class="text-sm text-gray-600"></div>
|
||||||
@@ -371,6 +477,7 @@
|
|||||||
let silenceTimer = null;
|
let silenceTimer = null;
|
||||||
let searchSilenceTimer = null;
|
let searchSilenceTimer = null;
|
||||||
let pendingNoteAttachments = [];
|
let pendingNoteAttachments = [];
|
||||||
|
let pendingConversationAttachments = [];
|
||||||
|
|
||||||
// --- Voice Recognition Setup with auto-stop ---
|
// --- Voice Recognition Setup with auto-stop ---
|
||||||
function setupTitleVoiceRecognition() {
|
function setupTitleVoiceRecognition() {
|
||||||
@@ -1269,6 +1376,33 @@
|
|||||||
listEl.appendChild(frag);
|
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() {
|
function resetNoteForm() {
|
||||||
document.getElementById('noteTitle').value = '';
|
document.getElementById('noteTitle').value = '';
|
||||||
document.getElementById('noteText').value = '';
|
document.getElementById('noteText').value = '';
|
||||||
@@ -1292,6 +1426,12 @@
|
|||||||
document.getElementById('conversationJobNumberContainer').classList.add('hidden');
|
document.getElementById('conversationJobNumberContainer').classList.add('hidden');
|
||||||
document.getElementById('conversationAudioPreview').src = '';
|
document.getElementById('conversationAudioPreview').src = '';
|
||||||
document.getElementById('conversationAudioPlayer').classList.add('hidden');
|
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) {
|
function renderNotesList(items) {
|
||||||
@@ -1592,18 +1732,236 @@
|
|||||||
showNoteForm();
|
showNoteForm();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('noteAttachmentsInput').addEventListener('change', (e) => {
|
// Attachment menu handling
|
||||||
pendingNoteAttachments = Array.from(e.target.files || []);
|
const attachmentMenuBtn = document.getElementById('attachmentMenuBtn');
|
||||||
renderNoteAttachmentsList();
|
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) => {
|
document.getElementById('clearAttachmentsBtn').addEventListener('click', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
pendingNoteAttachments = [];
|
pendingNoteAttachments = [];
|
||||||
document.getElementById('noteAttachmentsInput').value = '';
|
imageInput.value = '';
|
||||||
|
videoInput.value = '';
|
||||||
|
fileInput.value = '';
|
||||||
|
cameraInput.value = '';
|
||||||
renderNoteAttachmentsList();
|
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
|
// Search input + voice
|
||||||
const notesSearchInput = document.getElementById('notesSearch');
|
const notesSearchInput = document.getElementById('notesSearch');
|
||||||
const searchVoiceBtn = document.getElementById('searchVoiceBtn');
|
const searchVoiceBtn = document.getElementById('searchVoiceBtn');
|
||||||
@@ -1758,9 +2116,7 @@
|
|||||||
const createdAtt = await pb.collection(ATTACHMENTS_COLLECTION).create(fd);
|
const createdAtt = await pb.collection(ATTACHMENTS_COLLECTION).create(fd);
|
||||||
attachmentRecords.push(createdAtt);
|
attachmentRecords.push(createdAtt);
|
||||||
}
|
}
|
||||||
const inputEl = document.getElementById('noteAttachmentsInput');
|
const filesToUpload = (pendingNoteAttachments && pendingNoteAttachments.length) ? pendingNoteAttachments : [];
|
||||||
const filesFromInput = Array.from(inputEl?.files || []);
|
|
||||||
const filesToUpload = (pendingNoteAttachments && pendingNoteAttachments.length) ? pendingNoteAttachments : filesFromInput;
|
|
||||||
if (filesToUpload && filesToUpload.length) {
|
if (filesToUpload && filesToUpload.length) {
|
||||||
const fdFiles = new FormData();
|
const fdFiles = new FormData();
|
||||||
for (let i = 0; i < filesToUpload.length; i++) {
|
for (let i = 0; i < filesToUpload.length; i++) {
|
||||||
@@ -1784,7 +2140,10 @@
|
|||||||
statusEl.textContent = `Saved note ${noteRecord.id}${attachmentMsg}`;
|
statusEl.textContent = `Saved note ${noteRecord.id}${attachmentMsg}`;
|
||||||
await clearAudio();
|
await clearAudio();
|
||||||
pendingNoteAttachments = [];
|
pendingNoteAttachments = [];
|
||||||
document.getElementById('noteAttachmentsInput').value = '';
|
document.getElementById('imageInput').value = '';
|
||||||
|
document.getElementById('videoInput').value = '';
|
||||||
|
document.getElementById('fileInput').value = '';
|
||||||
|
document.getElementById('cameraInput').value = '';
|
||||||
renderNoteAttachmentsList();
|
renderNoteAttachmentsList();
|
||||||
document.getElementById('noteText').value = '';
|
document.getElementById('noteText').value = '';
|
||||||
document.getElementById('noteTitle').value = '';
|
document.getElementById('noteTitle').value = '';
|
||||||
|
|||||||
@@ -3,6 +3,16 @@ PRISM NOTES - SESSION LOG
|
|||||||
Project history, decisions, and architectural milestones
|
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
|
[2026-01-10 21:15] MONICA SESSION: Refined Notes List Header UI
|
||||||
- Changes Applied:
|
- Changes Applied:
|
||||||
* Removed graph status badges from note/conversation form containers
|
* Removed graph status badges from note/conversation form containers
|
||||||
|
|||||||
Reference in New Issue
Block a user