Add audio recording workflow to notes workspace

This commit is contained in:
2026-03-28 19:59:07 +00:00
parent e9d520fd5e
commit c4c5906daf
+152
View File
@@ -176,6 +176,15 @@
<input id="attachmentsInput" class="hidden" type="file" multiple />
<div id="pendingAttachmentsList" class="mt-2 space-y-1 text-xs text-slate-300"></div>
</div>
<div class="rounded-xl border border-slate-700 bg-slate-950/60 p-3">
<div class="flex flex-wrap items-center gap-2">
<button id="startRecordBtn" class="btn btn-secondary" type="button">Start Recording</button>
<button id="stopRecordBtn" class="btn btn-secondary" type="button" disabled>Stop</button>
<button id="clearRecordBtn" class="btn btn-secondary" type="button">Clear Last Clip</button>
<span id="recordStatus" class="text-xs text-slate-400">Ready</span>
</div>
<audio id="recordPreview" class="mt-2 hidden w-full" controls preload="metadata"></audio>
</div>
</div>
<div class="mt-4 flex flex-wrap gap-3">
<button id="createBtn" class="btn btn-accent-emerald">Create Note</button>
@@ -242,11 +251,21 @@
const pendingAttachmentsList = document.getElementById('pendingAttachmentsList');
const selectedAttachmentsStatus = document.getElementById('selectedAttachmentsStatus');
const selectedAttachmentsList = document.getElementById('selectedAttachmentsList');
const recordStatus = document.getElementById('recordStatus');
const recordPreview = document.getElementById('recordPreview');
const startRecordBtn = document.getElementById('startRecordBtn');
const stopRecordBtn = document.getElementById('stopRecordBtn');
const clearRecordBtn = document.getElementById('clearRecordBtn');
const saveBtn = document.getElementById('saveBtn');
const hideBtn = document.getElementById('hideBtn');
const deleteBtn = document.getElementById('deleteBtn');
let recorderStream = null;
let mediaRecorder = null;
let recordChunks = [];
let lastRecordedFile = null;
function writeOutput(value) {
output.textContent = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
}
@@ -385,6 +404,101 @@
return created;
}
async function ensureRecorderStream() {
if (recorderStream) return recorderStream;
recorderStream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true,
},
});
return recorderStream;
}
function stopRecorderTracks() {
if (recorderStream) {
recorderStream.getTracks().forEach((t) => t.stop());
}
recorderStream = null;
}
async function startAudioRecording() {
if (mediaRecorder && mediaRecorder.state === 'recording') return;
const stream = await ensureRecorderStream();
recordChunks = [];
mediaRecorder = new MediaRecorder(stream);
mediaRecorder.ondataavailable = (event) => {
if (event.data && event.data.size > 0) {
recordChunks.push(event.data);
}
};
mediaRecorder.start();
startRecordBtn.disabled = true;
stopRecordBtn.disabled = false;
recordStatus.textContent = 'Recording…';
}
async function stopAudioRecording() {
if (!mediaRecorder || mediaRecorder.state !== 'recording') return;
await new Promise((resolve) => {
mediaRecorder.addEventListener('stop', resolve, { once: true });
mediaRecorder.stop();
});
const mimeType = mediaRecorder?.mimeType || 'audio/webm';
const blob = new Blob(recordChunks, { type: mimeType });
stopRecorderTracks();
mediaRecorder = null;
startRecordBtn.disabled = false;
stopRecordBtn.disabled = true;
if (!blob.size) {
recordStatus.textContent = 'No audio captured.';
return;
}
const ext = mimeType.includes('ogg') ? 'ogg' : mimeType.includes('mp4') ? 'm4a' : 'webm';
const fileName = `note-audio-${new Date().toISOString().replace(/[:.]/g, '-')}.${ext}`;
const file = new File([blob], fileName, { type: mimeType });
pendingAttachments.push(file);
lastRecordedFile = file;
renderPendingAttachments();
if (recordPreview.src) URL.revokeObjectURL(recordPreview.src);
recordPreview.src = URL.createObjectURL(blob);
recordPreview.classList.remove('hidden');
recordStatus.textContent = `Recorded: ${fileName}`;
}
async function clearRecordedAudio() {
if (mediaRecorder && mediaRecorder.state === 'recording') {
await stopAudioRecording();
}
if (recordPreview.src) {
URL.revokeObjectURL(recordPreview.src);
}
recordPreview.src = '';
recordPreview.classList.add('hidden');
if (lastRecordedFile) {
pendingAttachments = pendingAttachments.filter((f) => !(f.name === lastRecordedFile.name && f.size === lastRecordedFile.size));
renderPendingAttachments();
}
lastRecordedFile = null;
recordStatus.textContent = 'Ready';
stopRecorderTracks();
mediaRecorder = null;
startRecordBtn.disabled = false;
stopRecordBtn.disabled = true;
}
async function fetchAttachmentsForNote(noteId) {
try {
const atts = await pb.collection(ATTACHMENTS_COLLECTION).getFullList({
@@ -624,6 +738,13 @@
shareInput.value = '';
pendingAttachments = [];
renderPendingAttachments();
if (recordPreview.src) {
URL.revokeObjectURL(recordPreview.src);
}
recordPreview.src = '';
recordPreview.classList.add('hidden');
recordStatus.textContent = 'Ready';
lastRecordedFile = null;
selectedAttachmentsStatus.textContent = 'Select a note to view attachments.';
selectedAttachmentsList.innerHTML = '';
saveBtn.disabled = true;
@@ -641,6 +762,13 @@
shareInput.value = normalizeShares(note.shared_with).join(', ');
pendingAttachments = [];
renderPendingAttachments();
if (recordPreview.src) {
URL.revokeObjectURL(recordPreview.src);
}
recordPreview.src = '';
recordPreview.classList.add('hidden');
recordStatus.textContent = 'Ready';
lastRecordedFile = null;
saveBtn.disabled = false;
hideBtn.disabled = false;
deleteBtn.disabled = false;
@@ -841,6 +969,30 @@
renderPendingAttachments();
});
startRecordBtn.addEventListener('click', async () => {
try {
await startAudioRecording();
} catch (error) {
recordStatus.textContent = `Recorder error: ${error?.message || String(error)}`;
}
});
stopRecordBtn.addEventListener('click', async () => {
try {
await stopAudioRecording();
} catch (error) {
recordStatus.textContent = `Stop failed: ${error?.message || String(error)}`;
}
});
clearRecordBtn.addEventListener('click', async () => {
try {
await clearRecordedAudio();
} catch (error) {
recordStatus.textContent = `Clear failed: ${error?.message || String(error)}`;
}
});
document.getElementById('clearBtn').addEventListener('click', () => {
clearForm();
renderList();