Spaces:
Paused
Paused
| 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'); |