bitsnaps's picture
Update static/js/app.js
fae1148 verified
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');