const { createApp, ref, onMounted } = Vue; const app = createApp({ data() { return { responseFormat: 'verbose_json', temperature: 0, chunkSize: 10, overlap: 1, selection: [1,100], systemPrompt: '', isAuthenticated: false, username: '', token: '', isLoading: false, audioFile: [], segments: [], transcriptionText: '', selectedLanguage: 'auto', transcriptions: [], showSidebar: true, showPlayer: false, showApiKeyModal: false, audioUrl: null, isPlaying: false, audioPlayer: null, isProcessing: false, howl: null, activeSegment: null, isPlayingSegment: false, isMuted: false, volume: 0.5, currentTime: 0, totalDuration: 0, seekInterval: null, waveformData: null, waveformCanvas: null, ctx: null, showLoginModal: false, loginForm: { username: '', password: '', }, showSignupModal: false, signupForm: { username: '', email: '', password: '', confirmPassword: '' }, // Admin panel data showAdminPanel: false, isAdmin: false, currentUser: null, users: [], loadingUsers: false, userSearchQuery: '', // processingProgress: 0, // totalChunks: 0, // showVolumeSlider: false, // Resumable upload properties showUploadModal: false, resumableFile: null, uploadId: null, chunkUploadSize: 1024 * 1024, // 1MB chunks uploadProgress: 0, uploadInProgress: false, uploadPaused: false, currentChunkIndex: 0, totalChunks: 0, // Upload statistics uploadStartTime: null, uploadSpeed: null, estimatedTimeRemaining: null, lastUploadedBytes: 0, lastUploadTime: null, // list uploaded files uploadedAudioFiles: [], loadingAudioFiles: false, showAudioFilesModal: false, audioFileSearchQuery: '', // Add credit system properties // Change the first userCredits to currentUserCredit currentUserCredit: { minutes_used: 0, minutes_quota: 60, minutes_remaining: 60, last_updated: null }, showCreditsModal: false, // Admin credit management activeAdminTab: 0, creditSearchQuery: '', userCredits: [], // Keep this one for the admin panel loadingCredits: false, showCreditEditModal: false, selectedUserCredit: null, editCreditForm: { minutes_used: 0, minutes_quota: 10 }, savingCredits: false } }, methods: { async login() { this.isAuthenticated = true; }, logout() { this.token = ''; this.username = ''; this.isAuthenticated = false; this.isAdmin = false; this.currentUser = null; this.users = []; this.transcriptions = []; localStorage.removeItem('token'); // Reset UI state this.audioUrl = null; this.audioFile = []; this.segments = []; this.transcriptionText = ''; // Show notification this.$buefy.toast.open({ message: 'Successfully logged out!', type: 'is-success' }); // Redirect to home page if not already there if (window.location.pathname !== '/') { window.location.href = '/'; } else { // window.location.reload(); } }, async handleAudioUpload(event) { if (!event.target.files || !event.target.files.length) return; const file = event.target.files[0]; const fileUrl = URL.createObjectURL(file); fileExt = file.name.split('.').splice(-1); this.howl = new Howl({ src: [fileUrl], format: this.fileExt, // This should be an array html5: true, onend: () => { this.isPlayingSegment = false; this.activeSegment = null; } }); this.audioUrl = fileUrl; this.audioFile = [file]; }, initializeAudio() { if (this.howl) { this.howl.unload(); } const fileExt = this.audioFile[0].name.split('.').splice(-1); this.howl = new Howl({ src: [this.audioUrl], format: fileExt, html5: true, volume: this.volume, sprite: this.createSprites(), onplay: () => { this.isPlaying = true; this.startSeekUpdate(); }, onpause: () => { this.isPlaying = false; this.stopSeekUpdate(); }, onstop: () => { this.isPlaying = false; this.currentTime = 0; this.stopSeekUpdate(); }, onend: () => { this.isPlaying = false; this.currentTime = 0; this.stopSeekUpdate(); }, onload: () => { this.totalDuration = this.howl.duration(); /*/ Set selection to the length of the audio file this.selection = []; for (let i = 0; i < this.totalDuration; i += this.chunkSize) { this.selection.push(i); }*/ } }); // Set initial volume this.updateVolume(); }, async initializeWaveform() { if (!this.audioUrl) return; // Load audio file and decode it const response = await fetch(this.audioUrl); const arrayBuffer = await response.arrayBuffer(); const audioContext = new AudioContext(); const audioBuffer = await audioContext.decodeAudioData(arrayBuffer); // Get waveform data const channelData = audioBuffer.getChannelData(0); this.waveformData = this.processWaveformData(channelData); // Draw waveform this.$nextTick(() => { this.drawWaveform(); }); // Set initial selection to full audio this.selection = [0, 100]; }, processWaveformData(data) { const step = Math.ceil(data.length / 1000); const waveform = []; for (let i = 0; i < data.length; i += step) { const slice = data.slice(i, i + step); // Use reducer instead of spread operator for large arrays const max = slice.reduce((a, b) => Math.max(a, b), -Infinity); const min = slice.reduce((a, b) => Math.min(a, b), Infinity); waveform.push({ max, min }); } return waveform; }, drawWaveform() { if (this.$refs.waveform){ const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); const width = this.$refs.waveform.offsetWidth; const height = 100; canvas.width = width; canvas.height = height; this.$refs.waveform.style.backgroundImage = `url(${canvas.toDataURL()})`; ctx.fillStyle = '#3273dc'; this.waveformData.forEach((point, i) => { const x = (i / this.waveformData.length) * width; const y = (1 - point.max) * height / 2; const h = (point.max - point.min) * height / 2; ctx.fillRect(x, y, 1, h); }); } }, createSprites() { const sprites = {}; this.segments.forEach((segment, index) => { sprites[`segment_${index}`] = [segment.start * 1000, (segment.end - segment.start) * 1000]; }); return sprites; }, togglePlay() { if (this.isPlaying) { this.howl.pause(); } else { this.howl.play(); } }, stopAudio() { this.howl.stop(); }, updateVolume() { if (this.volume <= 0) { this.isMuted = true; this.howl.mute(true); } else if (this.isMuted && this.volume > 0) { this.isMuted = false; this.howl.mute(false); } this.howl.volume(this.volume); }, toggleVolumeSlider() { this.showVolumeSlider = !this.showVolumeSlider; }, toggleMute() { this.isMuted = !this.isMuted; this.howl.mute(this.isMuted); if (!this.isMuted && this.volume === 0) { this.volume = 0.5; } }, playSegment(segment) { const index = this.segments.indexOf(segment); if (index !== -1) { this.howl.play(`segment_${index}`); } }, startSeekUpdate() { this.seekInterval = setInterval(() => { this.currentTime = this.howl.seek() || 0; }, 100); }, stopSeekUpdate() { clearInterval(this.seekInterval); }, formatTime(seconds) { const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins}:${secs < 10 ? '0' : ''}${secs}`; }, togglePlayPause() { if (this.audioPlayer.paused) { this.audioPlayer.play(); this.isPlaying = true; } else { this.audioPlayer.pause(); this.isPlaying = false; } }, playSegment(segment) { if (!this.howl) return; if (this.activeSegment === segment.id && this.isPlayingSegment) { this.howl.pause(); this.isPlayingSegment = false; return; } this.howl.seek(segment.start); this.howl.play(); this.activeSegment = segment.id; this.isPlayingSegment = true; this.howl.once('end', () => { this.isPlayingSegment = false; this.activeSegment = null; }); }, copySegmentText(text) { navigator.clipboard.writeText(text).then(() => { this.$buefy.toast.open({ message: 'Text copied to clipboard', type: 'is-success', position: 'is-bottom-right' }); }).catch(() => { this.$buefy.toast.open({ message: 'Failed to copy text', type: 'is-danger', position: 'is-bottom-right' }); }); }, deleteSegment(index) { this.segments.splice(index, 1); this.updateTranscriptionText(); }, updateTranscriptionText() { this.transcriptionText = this.segments .map(segment => segment.text) .join(' '); }, formatTime(seconds) { const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins}:${secs < 10 ? '0' : ''}${secs}`; }, async loadUserCredits() { try { const response = await fetch('/api/credits', { headers: { 'Authorization': `Bearer ${this.token}` } }); if (!response.ok) { throw new Error('Failed to load credit information'); } this.currentUserCredit = await response.json(); } catch (error) { console.error('Error loading credits:', error); this.$buefy.toast.open({ message: `Error: ${error.message}`, type: 'is-danger' }); } }, showCreditsInfo() { this.loadUserCredits(); this.showCreditsModal = true; }, async processTranscription() { const formData = new FormData(); formData.append('file', this.audioFile[0]); formData.append('response_format', this.responseFormat); formData.append('temperature', this.temperature.toString()); formData.append('chunk_size', parseInt(this.chunkSize * 60).toString()); formData.append('overlap', this.overlap.toString()); formData.append('prompt', this.systemPrompt.toString()); // Add selection timestamps const startTime = (this.selection[0] * this.totalDuration / 100).toFixed(2); const endTime = (this.selection[1] * this.totalDuration / 100).toFixed(2); formData.append('start_time', startTime); formData.append('end_time', endTime); if (this.selectedLanguage !== 'auto') { formData.append('language', this.selectedLanguage); } try { this.isProcessing = true; /*/ Monitor the progress of the upload for user feedback (need to be updated for reading back response) const xhr = new XMLHttpRequest(); xhr.upload.onloadstart = function (event) { console.log('Upload started'); }; xhr.upload.onprogress = function (event) { if (event.lengthComputable) { const percentComplete = (event.loaded / event.total) * 100; console.log(`Upload progress: ${percentComplete.toFixed(2)}%`); } }; xhr.upload.onload = function () { console.log('Upload complete'); }; xhr.onerror = function () { console.error('Error uploading file'); }; xhr.open('POST', '/api/upload', true); xhr.send(formData);*/ const response = await fetch('/api/upload', { method: 'POST', headers: { 'Authorization': `Bearer ${this.token}` }, body: formData }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.detail || 'Transcription failed'); } const result = await response.json(); // Check for backend validation errors if (result.metadata?.errors?.length) { console.error('Chunk processing errors:', result.metadata.errors); this.$buefy.snackbar.open({ message: `${result.metadata.errors.length} chunks failed to process`, type: 'is-warning', position: 'is-bottom-right' }); } if (result.metadata?.credit_usage) { this.currentUserCredit.minutes_used = result.metadata.credit_usage.total_minutes_used; this.currentUserCredit.minutes_quota = result.metadata.credit_usage.minutes_quota; this.currentUserCredit.minutes_remaining = result.metadata.credit_usage.minutes_remaining; this.currentUserCredit.last_updated = new Date().toISOString(); // Show credit usage notification this.$buefy.notification.open({ message: `Used ${result.metadata.credit_usage.minutes_used} minutes of credit. ${result.metadata.credit_usage.minutes_remaining} minutes remaining.`, type: 'is-info', position: 'is-bottom-right', duration: 5000 }); } if (this.responseFormat === 'verbose_json') { // Validate segments if (!result.segments) { throw new Error('Server returned invalid segments format'); } this.segments = result.segments; this.updateTranscriptionText(); } else { this.transcriptionText = result.text || result; } // this.totalChunks = result.metadata?.total_chunks || 0; // this.processingProgress = ((processedChunks / this.totalChunks) * 100).toFixed(1); } catch (error) { console.error('Transcription error:', error); this.$buefy.toast.open({ message: `Error: ${error.message}`, type: 'is-danger', position: 'is-bottom-right' }); } this.isProcessing = false; this.showPlayer = true; }, async loadTranscriptions() { try { // Check if token exists if (!this.token) { this.token = localStorage.getItem('token'); if (!this.token) { // console.log('No authentication token found'); return; } } const response = await fetch('/api/transcriptions', { headers: { 'Authorization': `Bearer ${this.token}` } }); if (response.status === 401) { // Token expired or invalid console.log('Authentication token expired or invalid'); this.logout(); this.$buefy.toast.open({ message: 'Your session has expired. Please login again.', type: 'is-warning' }); return; } if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); } this.transcriptions = await response.json(); } catch (error) { console.error('Error loading transcriptions:', error); this.$buefy.toast.open({ message: `Failed to load transcriptions: ${error.message}`, type: 'is-danger' }); } }, async loadTranscription(transcription) { try { const response = await fetch(`/api/transcriptions/${transcription.id}`, { headers: { 'Authorization': `Bearer ${this.token}` } }); const data = await response.json(); // Set the audio file for playback this.transcriptionText = data['text']; this.segments = data['segments']; // this.audioFile = data['audio_file']; if (data.audio_file) { // this.audioUrl = `/uploads/${data['audio_file']}`; // this.audioFile.push(data['audio_file']); // this.initializeAudio(); } } catch (error) { console.error('Error loading transcription:', error); } }, async saveTranscription() { if (this.audioFile.length == 0){ this.$buefy.toast.open({ message: 'Please upload an audio file first', type: 'is-warning', position: 'is-bottom-right' }); return; } try { this.isProcessing = true; const response = await fetch('/api/save-transcription', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.token}` }, body: JSON.stringify({ text: this.transcriptionText, segments: this.segments, audio_file: this.audioFile[0].name }) }); if (!response.ok) throw new Error('Failed to save transcription'); await this.loadTranscriptions(); this.$buefy.toast.open({ message: 'Transcription saved successfully', type: 'is-success', position: 'is-bottom-right' }); } catch (error) { console.error('Error saving transcription:', error); this.$buefy.toast.open({ message: `Error: ${error.message}`, type: 'is-danger', position: 'is-bottom-right' }); } this.isProcessing = false; }, toggleSidebar() { this.showSidebar = !this.showSidebar; }, togglePlayer() { this.showPlayer = !this.showPlayer; }, async deleteTranscription(id) { try { // Show confirmation dialog this.$buefy.dialog.confirm({ title: 'Delete Transcription', message: 'Are you sure you want to delete this transcription? This action cannot be undone.', confirmText: 'Delete', type: 'is-danger', hasIcon: true, onConfirm: async () => { const response = await fetch(`/api/transcriptions/${id}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${this.token}` } }); if (!response.ok) throw new Error('Failed to delete transcription'); // Remove from local list this.transcriptions = this.transcriptions.filter(t => t.id !== id); // Show success message this.$buefy.toast.open({ message: 'Transcription deleted successfully', type: 'is-success', position: 'is-bottom-right' }); } }); } catch (error) { console.error('Error deleting transcription:', error); this.$buefy.toast.open({ message: `Error: ${error.message}`, type: 'is-danger', position: 'is-bottom-right' }); } }, async handleLogin() { try { this.isLoading = true; const formData = new FormData(); formData.append('username', this.loginForm.username); // Make sure this matches the backend expectation formData.append('password', this.loginForm.password); const response = await fetch('/token', { method: 'POST', body: formData }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.detail || 'Login failed'); } const data = await response.json(); this.token = data.access_token; localStorage.setItem('token', data.access_token); // Get user info const userResponse = await fetch('/api/me', { headers: { 'Authorization': `Bearer ${this.token}` } }); if (!userResponse.ok) { throw new Error('Failed to get user information'); } const userData = await userResponse.json(); this.username = userData.username; this.isAuthenticated = true; this.showLoginModal = false; this.$buefy.toast.open({ message: 'Successfully logged in!', type: 'is-success' }); // Load user data after successful login this.loadTranscriptions(); } catch (error) { this.$buefy.toast.open({ message: `Error: ${error.message}`, type: 'is-danger' }); } finally { this.isLoading = false; } }, async handleSignup() { try { this.isLoading = true; if (this.signupForm.password !== this.signupForm.confirmPassword) { throw new Error('Passwords do not match'); } const response = await fetch('/api/signup', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: this.signupForm.username, email: this.signupForm.email, password: this.signupForm.password }) }); if (!response.ok) { const error = await response.json(); throw new Error(error.detail || 'Signup failed'); } this.showSignupModal = false; this.$buefy.toast.open({ message: 'Account created successfully! Please login.', type: 'is-success' }); // Clear form this.signupForm = { username: '', email: '', password: '', confirmPassword: '' }; // Show login modal this.showLoginModal = true; } catch (error) { this.$buefy.toast.open({ message: `Error: ${error.message}`, type: 'is-danger' }); } finally { this.isLoading = false; } }, async checkAuth() { const token = localStorage.getItem('token'); if (token) { try { this.token = token; const response = await fetch('/api/me', { headers: { 'Authorization': `Bearer ${token}` } }); if (response.ok) { const userData = await response.json(); this.username = userData.username; this.isAuthenticated = true; this.isAdmin = userData.is_admin; this.currentUser = userData; // If user is admin, load users list if (this.isAdmin) { this.loadUsers(); } } else { // Token invalid or expired this.logout(); } } catch (error) { console.error('Auth check failed:', error); this.logout(); } } }, async loadUsers() { if (!this.isAdmin) return; try { this.loadingUsers = true; const response = await fetch('/api/users', { headers: { 'Authorization': `Bearer ${this.token}` } }); if (!response.ok) { throw new Error('Failed to load users'); } this.users = await response.json(); } catch (error) { console.error('Error loading users:', error); this.$buefy.toast.open({ message: `Error: ${error.message}`, type: 'is-danger' }); } finally { this.loadingUsers = false; } }, async toggleUserStatus(user) { try { // Don't allow admins to disable themselves if (user.is_admin && user.id === this.currentUser.id && !user.disabled) { this.$buefy.toast.open({ message: 'Admins cannot disable their own accounts', type: 'is-warning' }); return; } const action = user.disabled ? 'enable' : 'disable'; const response = await fetch(`/api/users/${user.id}/${action}`, { method: 'PUT', headers: { 'Authorization': `Bearer ${this.token}`, 'Content-Type': 'application/json' } }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.detail || `Failed to ${action} user`); } // Update local user data user.disabled = !user.disabled; this.$buefy.toast.open({ message: `User ${user.username} ${action}d successfully`, type: 'is-success' }); } catch (error) { console.error(`Error ${user.disabled ? 'enabling' : 'disabling'} user:`, error); this.$buefy.toast.open({ message: `Error: ${error.message}`, type: 'is-danger' }); } }, async deleteUser(user) { try { // Don't allow admins to delete themselves if (user.id === this.currentUser.id) { this.$buefy.toast.open({ message: 'You cannot delete your own account', type: 'is-warning' }); return; } // Show confirmation dialog this.$buefy.dialog.confirm({ title: 'Delete User', message: `Are you sure you want to delete user "${user.username}"? This action cannot be undone.`, confirmText: 'Delete', type: 'is-danger', hasIcon: true, onConfirm: async () => { const response = await fetch(`/api/users/${user.id}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${this.token}` } }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.detail || 'Failed to delete user'); } // Remove from local list this.users = this.users.filter(u => u.id !== user.id); this.$buefy.toast.open({ message: `User ${user.username} deleted successfully`, type: 'is-success' }); } }); } catch (error) { console.error('Error deleting user:', error); this.$buefy.toast.open({ message: `Error: ${error.message}`, type: 'is-danger' }); } }, refreshUsers() { this.loadUsers(); }, showResumeUploadModal() { this.showUploadModal = true; this.showAudioFilesModal = false; }, closeUploadModal() { if (this.uploadInProgress && !this.uploadPaused) { this.$buefy.dialog.confirm({ title: 'Cancel Upload?', message: 'Upload is in progress. Are you sure you want to cancel?', confirmText: 'Yes, Cancel Upload', cancelText: 'No, Continue Upload', type: 'is-danger', onConfirm: () => { this.cancelUpload(); this.showUploadModal = false; } }); } else { this.showUploadModal = false; } }, generateUUID() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); }, prepareChunkedUpload() { setTimeout(() => { if (!this.resumableFile) return; this.uploadId = this.generateUUID(); this.totalChunks = Math.ceil(this.resumableFile.size / this.chunkUploadSize); this.currentChunkIndex = 0; this.uploadProgress = 0; this.uploadPaused = false; }, 1500); }, async startChunkedUpload() { if (!this.resumableFile || this.uploadInProgress) return; this.uploadInProgress = true; this.uploadPaused = false; this.uploadStartTime = Date.now(); this.lastUploadTime = Date.now(); this.lastUploadedBytes = 0; this.uploadSpeed = null; this.estimatedTimeRemaining = null; await this.uploadNextChunk(); }, async uploadNextChunk() { if (this.uploadPaused || !this.uploadInProgress) return; if (this.currentChunkIndex >= this.totalChunks) { // All chunks uploaded, finalize the upload await this.finalizeUpload(); return; } const start = this.currentChunkIndex * this.chunkUploadSize; const end = Math.min(start + this.chunkUploadSize, this.resumableFile.size); const chunk = this.resumableFile.slice(start, end); const formData = new FormData(); formData.append('file', chunk, this.resumableFile.name); formData.append('upload_id', this.uploadId); formData.append('offset', start); formData.append('total_size', this.resumableFile.size); try { const response = await fetch('/api/upload-audio-chunk', { method: 'POST', body: formData, headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } }); if (!response.ok) { throw new Error('Upload failed'); } this.currentChunkIndex++; this.uploadProgress = (this.currentChunkIndex / this.totalChunks) * 100; // Calculate upload speed and estimated time const now = Date.now(); const uploadedBytes = this.currentChunkIndex * this.chunkUploadSize; const timeDiff = (now - this.lastUploadTime) / 1000; // in seconds if (timeDiff > 0) { const bytesDiff = uploadedBytes - this.lastUploadedBytes; this.uploadSpeed = Math.round((bytesDiff / 1024) / timeDiff); // KB/s const remainingBytes = this.resumableFile.size - uploadedBytes; if (this.uploadSpeed > 0) { const secondsRemaining = Math.ceil(remainingBytes / 1024 / this.uploadSpeed); this.estimatedTimeRemaining = this.formatTimeRemaining(secondsRemaining); } this.lastUploadTime = now; this.lastUploadedBytes = uploadedBytes; } // Continue with next chunk if (!this.uploadPaused) { await this.uploadNextChunk(); } } catch (error) { console.error('Error uploading chunk:', error); this.$buefy.toast.open({ message: 'Upload error: ' + error.message, type: 'is-danger' }); this.uploadPaused = true; } }, formatTimeRemaining(seconds) { if (seconds < 60) { return `${seconds} sec`; } else if (seconds < 3600) { const minutes = Math.floor(seconds / 60); const remainingSeconds = seconds % 60; return `${minutes} min ${remainingSeconds} sec`; } else { const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); return `${hours} hr ${minutes} min`; } }, pauseUpload() { this.uploadPaused = true; }, async resumeUpload() { this.uploadPaused = false; await this.uploadNextChunk(); }, async cancelUpload() { if (!this.uploadId) return; try { await fetch('/api/cancel-audio-upload', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('token')}` }, body: JSON.stringify({ upload_id: this.uploadId }) }); } catch (error) { console.error('Error canceling upload:', error); } this.uploadInProgress = false; this.uploadPaused = false; this.uploadProgress = 0; this.currentChunkIndex = 0; }, async finalizeUpload() { try { // Check if uploadId exists if (!this.uploadId) { throw new Error('Upload ID is missing'); } const response = await fetch('/api/finalize-audio-upload', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('token')}` }, body: JSON.stringify({ upload_id: this.uploadId }) }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.detail || 'Failed to finalize upload'); } const result = await response.json(); // Set the audio file and URL // this.audioFile = [{ name: result.filename }]; // this.audioUrl = `/uploads/${result.filename}`; // Reload the audio files list await this.loadUploadedAudioFiles(); this.$buefy.toast.open({ message: 'Upload completed successfully!', type: 'is-success' }); this.uploadInProgress = false; this.showUploadModal = false; } catch (error) { console.error('Error finalizing upload:', error); this.$buefy.toast.open({ message: 'Error finalizing upload: ' + error.message, type: 'is-danger' }); this.uploadPaused = true; } }, async loadUploadedAudioFiles() { try { this.loadingAudioFiles = true; const response = await fetch('/api/audio-files', { headers: { 'Authorization': `Bearer ${this.token}` } }); if (!response.ok) { throw new Error('Failed to load audio files'); } this.uploadedAudioFiles = await response.json(); } catch (error) { console.error('Error loading audio files:', error); this.$buefy.toast.open({ message: `Error: ${error.message}`, type: 'is-danger' }); } finally { this.loadingAudioFiles = false; } }, formatFileSize(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }, showAudioFilesList() { this.showAudioFilesModal = true; this.loadUploadedAudioFiles(); }, async deleteAudioFile(file) { try { this.$buefy.dialog.confirm({ title: 'Delete Audio File', message: `Are you sure you want to delete "${file.filename}"?`, confirmText: 'Delete', type: 'is-danger', hasIcon: true, onConfirm: async () => { // Construct the API endpoint with user_id if admin is deleting another user's file let endpoint = `/api/audio-files/${file.filename}`; if (this.isAdmin && file.user_id && file.user_id !== this.currentUser.id) { endpoint += `?user_id=${file.user_id}`; } const response = await fetch(endpoint, { method: 'DELETE', headers: { 'Authorization': `Bearer ${this.token}` } }); if (!response.ok) { throw new Error('Failed to delete audio file'); } // Remove from local list this.uploadedAudioFiles = this.uploadedAudioFiles.filter(f => !(f.filename === file.filename && (!file.user_id || f.user_id === file.user_id)) ); this.$buefy.toast.open({ message: 'Audio file deleted successfully', type: 'is-success' }); } }); } catch (error) { console.error('Error deleting audio file:', error); this.$buefy.toast.open({ message: `Error: ${error.message}`, type: 'is-danger' }); } }, async selectAudioFile(file) { try { // Construct the correct URL based on whether the file has a user_id (admin view) const fileUrl = file.user_id ? `/uploads/${file.user_id}/${file.filename}` : `/uploads/${this.currentUser.id}/${file.filename}`; // Fetch the file from the server to create a proper File object const response = await fetch(fileUrl); const blob = await response.blob(); // Create a File object from the blob const fileObj = new File([blob], file.filename, { type: blob.type || 'audio/mpeg' }); // Set the audio file and URL this.audioFile = [fileObj]; this.audioUrl = fileUrl; // Initialize audio player with the selected file this.initializeAudio(); this.initializeWaveform(); // Close the audio files modal this.showAudioFilesModal = false; this.$buefy.toast.open({ message: `Selected audio file: ${file.filename}`, type: 'is-success' }); } catch (error) { console.error('Error selecting audio file:', error); this.$buefy.toast.open({ message: `Error selecting file: ${error.message}`, type: 'is-danger' }); } }, async loadAllUserCredits() { try { this.loadingCredits = true; const response = await fetch('/api/admin/credits', { headers: { 'Authorization': `Bearer ${this.token}` } }); if (!response.ok) { throw new Error('Failed to load credit information'); } this.userCredits = await response.json(); } catch (error) { console.error('Error loading all credits:', error); this.$buefy.toast.open({ message: `Error: ${error.message}`, type: 'is-danger' }); } finally { this.loadingCredits = false; } }, editUserCredits(userCredit) { this.selectedUserCredit = userCredit; this.editCreditForm.minutes_used = userCredit.minutes_used; this.editCreditForm.minutes_quota = userCredit.minutes_quota; this.showCreditEditModal = true; }, async saveUserCredits() { if (!this.selectedUserCredit) return; try { this.savingCredits = true; const response = await fetch(`/api/admin/credits/${this.selectedUserCredit.user_id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.token}` }, body: JSON.stringify({ minutes_used: this.editCreditForm.minutes_used, minutes_quota: this.editCreditForm.minutes_quota }) }); if (!response.ok) { throw new Error('Failed to update credit information'); } // Update local data const updatedCredit = await response.json(); const index = this.userCredits.findIndex(c => c.user_id === updatedCredit.user_id); if (index !== -1) { this.userCredits[index] = updatedCredit; } this.$buefy.toast.open({ message: 'Credit information updated successfully', type: 'is-success' }); this.showCreditEditModal = false; } catch (error) { console.error('Error updating credits:', error); this.$buefy.toast.open({ message: `Error: ${error.message}`, type: 'is-danger' }); } finally { this.savingCredits = false; } }, async resetUserCredits(userCredit) { try { this.$buefy.dialog.confirm({ title: 'Reset Credits', message: `Are you sure you want to reset credits for user "${userCredit.username}"?`, confirmText: 'Reset', type: 'is-warning', hasIcon: true, onConfirm: async () => { const response = await fetch(`/api/admin/credits/${userCredit.user_id}/reset`, { method: 'POST', headers: { 'Authorization': `Bearer ${this.token}` } }); if (!response.ok) { throw new Error('Failed to reset credit information'); } // Update local data const updatedCredit = await response.json(); const index = this.userCredits.findIndex(c => c.user_id === updatedCredit.user_id); if (index !== -1) { this.userCredits[index] = updatedCredit; } this.$buefy.toast.open({ message: `Credits reset for user ${userCredit.username}`, type: 'is-success' }); } }); } catch (error) { console.error('Error resetting credits:', error); this.$buefy.toast.open({ message: `Error: ${error.message}`, type: 'is-danger' }); } }, async resetUserQuota(userCredit) { try { this.$buefy.dialog.prompt({ title: 'Reset Quota', message: `Enter new quota value for user "${userCredit.username}"`, inputAttrs: { type: 'number', min: '0', value: '60', placeholder: 'Minutes' }, confirmText: 'Reset Quota', type: 'is-warning', hasIcon: true, onConfirm: async (value) => { const newQuota = parseInt(value); if (isNaN(newQuota) || newQuota < 0) { this.$buefy.toast.open({ message: 'Please enter a valid non-negative number', type: 'is-danger' }); return; } const response = await fetch(`/api/admin/credits/${userCredit.user_id}/reset-quota`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.token}` }, body: JSON.stringify({ new_quota: newQuota }) }); if (!response.ok) { throw new Error('Failed to reset quota'); } // Update local data const updatedCredit = await response.json(); const index = this.userCredits.findIndex(c => c.user_id === updatedCredit.user_id); if (index !== -1) { this.userCredits[index] = updatedCredit; } this.$buefy.toast.open({ message: `Quota reset for user ${userCredit.username}`, type: 'is-success' }); } }); } catch (error) { console.error('Error resetting quota:', error); this.$buefy.toast.open({ message: `Error: ${error.message}`, type: 'is-danger' }); } }, refreshAdminData() { if (this.activeAdminTab === 0) { this.loadUsers(); } else if (this.activeAdminTab === 1) { this.loadAllUserCredits(); } }, downloadTranscription(transcription) { try { // Create a JSON object with all the transcription data const jsonData = { id: transcription.id, name: transcription.name, audio_file: transcription.audio_file, text: transcription.text, segments: transcription.segments || [], created_at: transcription.created_at, model: transcription.model, language: transcription.language }; // Convert to a JSON string with nice formatting const jsonString = JSON.stringify(jsonData, null, 2); // Create a blob with the JSON data const blob = new Blob([jsonString], { type: 'application/json' }); // Create a URL for the blob const url = URL.createObjectURL(blob); // Create a temporary link element const link = document.createElement('a'); link.href = url; // Set the filename const filename = `transcription_${transcription.id}_${new Date().toISOString().slice(0, 10)}.json`; link.download = filename; // Append the link to the body document.body.appendChild(link); // Trigger the download link.click(); // Clean up document.body.removeChild(link); URL.revokeObjectURL(url); this.$buefy.toast.open({ message: `Transcription downloaded as ${filename}`, type: 'is-success' }); } catch (error) { console.error('Error downloading transcription:', error); this.$buefy.toast.open({ message: `Error downloading transcription: ${error.message}`, type: 'is-danger' }); } }, downloadCurrentTranscription() { if (!this.transcriptionText) { this.$buefy.toast.open({ message: 'No transcription to download', type: 'is-warning' }); return; } try { // Create a JSON object with the current transcription data const jsonData = { text: this.transcriptionText, segments: this.segments || [], audio_file: this.audioFile && this.audioFile.length > 0 ? this.audioFile[0].name : 'unknown', created_at: new Date().toISOString(), model: this.selectedModel, language: this.selectedLanguage }; // Convert to a JSON string with nice formatting const jsonString = JSON.stringify(jsonData, null, 2); // Create a blob with the JSON data const blob = new Blob([jsonString], { type: 'application/json' }); // Create a URL for the blob const url = URL.createObjectURL(blob); // Create a temporary link element const link = document.createElement('a'); link.href = url; // Set the filename const filename = `transcription_${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.json`; link.download = filename; // Append the link to the body document.body.appendChild(link); // Trigger the download link.click(); // Clean up document.body.removeChild(link); URL.revokeObjectURL(url); this.$buefy.toast.open({ message: `Transcription downloaded as ${filename}`, type: 'is-success' }); } catch (error) { console.error('Error downloading current transcription:', error); this.$buefy.toast.open({ message: `Error downloading transcription: ${error.message}`, type: 'is-danger' }); } }, getQuotaTagType(credit) { const remaining = credit.minutes_remaining; const quota = credit.minutes_quota; if (remaining <= 0) { return 'is-danger'; } else if (remaining < quota * 0.2) { return 'is-warning'; } else { return 'is-success'; } }, getAudioUrl(file) { if (file.user_id) { return `/uploads/${file.user_id}/${file.filename}`; } else { return `/uploads/${this.currentUser.id}/${file.filename}`; } }, }, // methods // Computed properties computed: { filteredUsers() { if (!this.userSearchQuery) { return this.users; } const query = this.userSearchQuery.toLowerCase(); return this.users.filter(user => user.username.toLowerCase().includes(query) || user.email.toLowerCase().includes(query) ); }, filteredAudioFiles() { if (!this.audioFileSearchQuery) { return this.uploadedAudioFiles; } const query = this.audioFileSearchQuery.toLowerCase(); return this.uploadedAudioFiles.filter(file => file.filename.toLowerCase().includes(query) ); }, filteredCredits() { if (!this.creditSearchQuery) { return this.userCredits; } const query = this.creditSearchQuery.toLowerCase(); return this.userCredits.filter(credit => credit.username.toLowerCase().includes(query) || credit.user_id.toString().includes(query) ); }, }, // computed mounted() { this.checkAuth(); this.loadTranscriptions(); if (this.isAuthenticated) { this.loadUploadedAudioFiles(); this.loadUserCredits(); } }, watch: { audioUrl() { if (this.audioUrl) { this.initializeAudio(); this.initializeWaveform(); } }, showAdminPanel(newVal) { if (newVal && this.isAdmin) { this.loadUsers(); this.loadAllUserCredits(); } }, isAuthenticated(newVal) { if (newVal) { this.loadUploadedAudioFiles(); this.loadUserCredits(); } } }, beforeUnmount() { if (this.howl) { this.howl.unload(); } this.stopSeekUpdate(); } }); app.use(Buefy.default); app.mount('#app');