Add audio recording workflow to notes workspace
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user