browser-captions/static/js/recordings.js
bunker-admin c7becf330c Initial commit: Live Captions web application
Real-time speech-to-text using OpenAI Whisper (faster-whisper).
Features browser audio capture, WebSocket streaming, and customizable display settings.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 08:53:40 -07:00

205 lines
6.4 KiB
JavaScript

/**
* Live Captions - Recordings Panel
* Handles viewing and managing saved recordings
*/
const Recordings = {
// Current state
recordings: [],
currentRecording: null,
// DOM elements
elements: {},
/**
* Initialize the recordings panel
*/
init() {
this.cacheElements();
this.bindEvents();
},
/**
* Cache DOM element references
*/
cacheElements() {
this.elements = {
btnRecordings: document.getElementById('btn-recordings'),
btnClose: document.getElementById('btn-close-recordings'),
btnBackToList: document.getElementById('btn-back-to-list'),
btnDelete: document.getElementById('btn-delete-recording'),
panel: document.getElementById('recordings-panel'),
overlay: document.getElementById('overlay'),
recordingsList: document.getElementById('recordings-list'),
recordingViewer: document.getElementById('recording-viewer'),
viewerFilename: document.getElementById('viewer-filename'),
viewerContent: document.getElementById('viewer-content'),
};
},
/**
* Bind event listeners
*/
bindEvents() {
this.elements.btnRecordings.addEventListener('click', () => this.openPanel());
this.elements.btnClose.addEventListener('click', () => this.closePanel());
this.elements.btnBackToList.addEventListener('click', () => this.showList());
this.elements.btnDelete.addEventListener('click', () => this.deleteCurrentRecording());
// Close on overlay click (but check if it's not settings panel)
this.elements.overlay.addEventListener('click', () => {
if (!this.elements.panel.classList.contains('hidden')) {
this.closePanel();
}
});
},
/**
* Open the recordings panel
*/
openPanel() {
this.elements.panel.classList.remove('hidden');
this.elements.overlay.classList.remove('hidden');
this.showList();
this.loadRecordings();
},
/**
* Close the recordings panel
*/
closePanel() {
this.elements.panel.classList.add('hidden');
this.elements.overlay.classList.add('hidden');
this.currentRecording = null;
},
/**
* Show the recordings list view
*/
showList() {
this.elements.recordingsList.classList.remove('hidden');
this.elements.recordingViewer.classList.add('hidden');
},
/**
* Show the recording viewer
*/
showViewer() {
this.elements.recordingsList.classList.add('hidden');
this.elements.recordingViewer.classList.remove('hidden');
},
/**
* Load recordings from the API
*/
async loadRecordings() {
this.elements.recordingsList.innerHTML = '<p class="recordings-empty">Loading recordings...</p>';
try {
const response = await fetch('/api/recordings');
if (!response.ok) throw new Error('Failed to load recordings');
this.recordings = await response.json();
this.renderRecordingsList();
} catch (error) {
console.error('Error loading recordings:', error);
this.elements.recordingsList.innerHTML =
'<p class="recordings-empty">Failed to load recordings</p>';
}
},
/**
* Render the recordings list
*/
renderRecordingsList() {
if (this.recordings.length === 0) {
this.elements.recordingsList.innerHTML =
'<p class="recordings-empty">No recordings yet.<br>Enable auto-save and record some captions!</p>';
return;
}
const html = this.recordings.map(recording => `
<div class="recording-item" data-filename="${recording.filename}">
<div class="recording-info">
<span class="recording-date">${recording.date}</span>
<span class="recording-meta">${this.formatFileSize(recording.size)}</span>
</div>
<span class="recording-arrow">&rsaquo;</span>
</div>
`).join('');
this.elements.recordingsList.innerHTML = html;
// Bind click events to items
this.elements.recordingsList.querySelectorAll('.recording-item').forEach(item => {
item.addEventListener('click', () => {
const filename = item.dataset.filename;
this.viewRecording(filename);
});
});
},
/**
* Format file size in human-readable format
*/
formatFileSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
},
/**
* View a specific recording
*/
async viewRecording(filename) {
try {
const response = await fetch(`/api/recordings/${encodeURIComponent(filename)}`);
if (!response.ok) throw new Error('Failed to load recording');
const data = await response.json();
this.currentRecording = filename;
this.elements.viewerFilename.textContent = filename;
this.elements.viewerContent.textContent = data.content;
this.showViewer();
} catch (error) {
console.error('Error loading recording:', error);
alert('Failed to load recording');
}
},
/**
* Delete the currently viewed recording
*/
async deleteCurrentRecording() {
if (!this.currentRecording) return;
if (!confirm('Are you sure you want to delete this recording?')) {
return;
}
try {
const response = await fetch(`/api/recordings/${encodeURIComponent(this.currentRecording)}`, {
method: 'DELETE'
});
if (!response.ok) throw new Error('Failed to delete recording');
// Remove from local list
this.recordings = this.recordings.filter(r => r.filename !== this.currentRecording);
this.currentRecording = null;
// Go back to list
this.showList();
this.renderRecordingsList();
} catch (error) {
console.error('Error deleting recording:', error);
alert('Failed to delete recording');
}
}
};
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
Recordings.init();
});