Spaces:
Paused
Paused
Update static/js/app.js
Browse files- static/js/app.js +416 -17
static/js/app.js
CHANGED
|
@@ -78,6 +78,27 @@ const app = createApp({
|
|
| 78 |
loadingAudioFiles: false,
|
| 79 |
showAudioFilesModal: false,
|
| 80 |
audioFileSearchQuery: '',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
}
|
| 82 |
},
|
| 83 |
|
|
@@ -365,6 +386,34 @@ const app = createApp({
|
|
| 365 |
return `${mins}:${secs < 10 ? '0' : ''}${secs}`;
|
| 366 |
},
|
| 367 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 368 |
async processTranscription() {
|
| 369 |
const formData = new FormData();
|
| 370 |
formData.append('file', this.audioFile[0]);
|
|
@@ -431,6 +480,21 @@ const app = createApp({
|
|
| 431 |
});
|
| 432 |
}
|
| 433 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 434 |
if (this.responseFormat === 'verbose_json') {
|
| 435 |
|
| 436 |
// Validate segments
|
|
@@ -911,7 +975,6 @@ const app = createApp({
|
|
| 911 |
this.uploadPaused = false;
|
| 912 |
}, 1500);
|
| 913 |
},
|
| 914 |
-
|
| 915 |
async startChunkedUpload() {
|
| 916 |
if (!this.resumableFile || this.uploadInProgress) return;
|
| 917 |
|
|
@@ -925,7 +988,6 @@ const app = createApp({
|
|
| 925 |
|
| 926 |
await this.uploadNextChunk();
|
| 927 |
},
|
| 928 |
-
|
| 929 |
async uploadNextChunk() {
|
| 930 |
if (this.uploadPaused || !this.uploadInProgress) return;
|
| 931 |
|
|
@@ -993,7 +1055,6 @@ const app = createApp({
|
|
| 993 |
this.uploadPaused = true;
|
| 994 |
}
|
| 995 |
},
|
| 996 |
-
|
| 997 |
formatTimeRemaining(seconds) {
|
| 998 |
if (seconds < 60) {
|
| 999 |
return `${seconds} sec`;
|
|
@@ -1007,16 +1068,13 @@ const app = createApp({
|
|
| 1007 |
return `${hours} hr ${minutes} min`;
|
| 1008 |
}
|
| 1009 |
},
|
| 1010 |
-
|
| 1011 |
pauseUpload() {
|
| 1012 |
this.uploadPaused = true;
|
| 1013 |
},
|
| 1014 |
-
|
| 1015 |
async resumeUpload() {
|
| 1016 |
this.uploadPaused = false;
|
| 1017 |
await this.uploadNextChunk();
|
| 1018 |
},
|
| 1019 |
-
|
| 1020 |
async cancelUpload() {
|
| 1021 |
if (!this.uploadId) return;
|
| 1022 |
|
|
@@ -1038,7 +1096,6 @@ const app = createApp({
|
|
| 1038 |
this.uploadProgress = 0;
|
| 1039 |
this.currentChunkIndex = 0;
|
| 1040 |
},
|
| 1041 |
-
|
| 1042 |
async finalizeUpload() {
|
| 1043 |
try {
|
| 1044 |
// Check if uploadId exists
|
|
@@ -1124,17 +1181,22 @@ const app = createApp({
|
|
| 1124 |
this.showAudioFilesModal = true;
|
| 1125 |
this.loadUploadedAudioFiles();
|
| 1126 |
},
|
| 1127 |
-
|
| 1128 |
async deleteAudioFile(file) {
|
| 1129 |
try {
|
| 1130 |
this.$buefy.dialog.confirm({
|
| 1131 |
title: 'Delete Audio File',
|
| 1132 |
-
message: `Are you sure you want to delete "${file.filename}"
|
| 1133 |
confirmText: 'Delete',
|
| 1134 |
type: 'is-danger',
|
| 1135 |
hasIcon: true,
|
| 1136 |
onConfirm: async () => {
|
| 1137 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1138 |
method: 'DELETE',
|
| 1139 |
headers: {
|
| 1140 |
'Authorization': `Bearer ${this.token}`
|
|
@@ -1146,7 +1208,10 @@ const app = createApp({
|
|
| 1146 |
}
|
| 1147 |
|
| 1148 |
// Remove from local list
|
| 1149 |
-
this.uploadedAudioFiles = this.uploadedAudioFiles.filter(f =>
|
|
|
|
|
|
|
|
|
|
| 1150 |
|
| 1151 |
this.$buefy.toast.open({
|
| 1152 |
message: 'Audio file deleted successfully',
|
|
@@ -1162,11 +1227,15 @@ const app = createApp({
|
|
| 1162 |
});
|
| 1163 |
}
|
| 1164 |
},
|
| 1165 |
-
|
| 1166 |
async selectAudioFile(file) {
|
| 1167 |
try {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1168 |
// Fetch the file from the server to create a proper File object
|
| 1169 |
-
const response = await fetch(
|
| 1170 |
const blob = await response.blob();
|
| 1171 |
|
| 1172 |
// Create a File object from the blob
|
|
@@ -1176,7 +1245,7 @@ const app = createApp({
|
|
| 1176 |
|
| 1177 |
// Set the audio file and URL
|
| 1178 |
this.audioFile = [fileObj];
|
| 1179 |
-
this.audioUrl =
|
| 1180 |
|
| 1181 |
// Initialize audio player with the selected file
|
| 1182 |
this.initializeAudio();
|
|
@@ -1196,7 +1265,323 @@ const app = createApp({
|
|
| 1196 |
type: 'is-danger'
|
| 1197 |
});
|
| 1198 |
}
|
| 1199 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1200 |
}, // methods
|
| 1201 |
// Computed properties
|
| 1202 |
computed: {
|
|
@@ -1220,13 +1605,25 @@ const app = createApp({
|
|
| 1220 |
return this.uploadedAudioFiles.filter(file =>
|
| 1221 |
file.filename.toLowerCase().includes(query)
|
| 1222 |
);
|
| 1223 |
-
}
|
| 1224 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1225 |
mounted() {
|
| 1226 |
this.checkAuth();
|
| 1227 |
this.loadTranscriptions();
|
| 1228 |
if (this.isAuthenticated) {
|
| 1229 |
this.loadUploadedAudioFiles();
|
|
|
|
| 1230 |
}
|
| 1231 |
},
|
| 1232 |
watch: {
|
|
@@ -1239,11 +1636,13 @@ const app = createApp({
|
|
| 1239 |
showAdminPanel(newVal) {
|
| 1240 |
if (newVal && this.isAdmin) {
|
| 1241 |
this.loadUsers();
|
|
|
|
| 1242 |
}
|
| 1243 |
},
|
| 1244 |
isAuthenticated(newVal) {
|
| 1245 |
if (newVal) {
|
| 1246 |
this.loadUploadedAudioFiles();
|
|
|
|
| 1247 |
}
|
| 1248 |
}
|
| 1249 |
},
|
|
|
|
| 78 |
loadingAudioFiles: false,
|
| 79 |
showAudioFilesModal: false,
|
| 80 |
audioFileSearchQuery: '',
|
| 81 |
+
// Add credit system properties
|
| 82 |
+
// Change the first userCredits to currentUserCredit
|
| 83 |
+
currentUserCredit: {
|
| 84 |
+
minutes_used: 0,
|
| 85 |
+
minutes_quota: 60,
|
| 86 |
+
minutes_remaining: 60,
|
| 87 |
+
last_updated: null
|
| 88 |
+
},
|
| 89 |
+
showCreditsModal: false,
|
| 90 |
+
// Admin credit management
|
| 91 |
+
activeAdminTab: 0,
|
| 92 |
+
creditSearchQuery: '',
|
| 93 |
+
userCredits: [], // Keep this one for the admin panel
|
| 94 |
+
loadingCredits: false,
|
| 95 |
+
showCreditEditModal: false,
|
| 96 |
+
selectedUserCredit: null,
|
| 97 |
+
editCreditForm: {
|
| 98 |
+
minutes_used: 0,
|
| 99 |
+
minutes_quota: 10
|
| 100 |
+
},
|
| 101 |
+
savingCredits: false
|
| 102 |
}
|
| 103 |
},
|
| 104 |
|
|
|
|
| 386 |
return `${mins}:${secs < 10 ? '0' : ''}${secs}`;
|
| 387 |
},
|
| 388 |
|
| 389 |
+
async loadUserCredits() {
|
| 390 |
+
try {
|
| 391 |
+
const response = await fetch('/api/credits', {
|
| 392 |
+
headers: {
|
| 393 |
+
'Authorization': `Bearer ${this.token}`
|
| 394 |
+
}
|
| 395 |
+
});
|
| 396 |
+
|
| 397 |
+
if (!response.ok) {
|
| 398 |
+
throw new Error('Failed to load credit information');
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
this.currentUserCredit = await response.json();
|
| 402 |
+
|
| 403 |
+
} catch (error) {
|
| 404 |
+
console.error('Error loading credits:', error);
|
| 405 |
+
this.$buefy.toast.open({
|
| 406 |
+
message: `Error: ${error.message}`,
|
| 407 |
+
type: 'is-danger'
|
| 408 |
+
});
|
| 409 |
+
}
|
| 410 |
+
},
|
| 411 |
+
|
| 412 |
+
showCreditsInfo() {
|
| 413 |
+
this.loadUserCredits();
|
| 414 |
+
this.showCreditsModal = true;
|
| 415 |
+
},
|
| 416 |
+
|
| 417 |
async processTranscription() {
|
| 418 |
const formData = new FormData();
|
| 419 |
formData.append('file', this.audioFile[0]);
|
|
|
|
| 480 |
});
|
| 481 |
}
|
| 482 |
|
| 483 |
+
if (result.metadata?.credit_usage) {
|
| 484 |
+
this.currentUserCredit.minutes_used = result.metadata.credit_usage.total_minutes_used;
|
| 485 |
+
this.currentUserCredit.minutes_quota = result.metadata.credit_usage.minutes_quota;
|
| 486 |
+
this.currentUserCredit.minutes_remaining = result.metadata.credit_usage.minutes_remaining;
|
| 487 |
+
this.currentUserCredit.last_updated = new Date().toISOString();
|
| 488 |
+
|
| 489 |
+
// Show credit usage notification
|
| 490 |
+
this.$buefy.notification.open({
|
| 491 |
+
message: `Used ${result.metadata.credit_usage.minutes_used} minutes of credit. ${result.metadata.credit_usage.minutes_remaining} minutes remaining.`,
|
| 492 |
+
type: 'is-info',
|
| 493 |
+
position: 'is-bottom-right',
|
| 494 |
+
duration: 5000
|
| 495 |
+
});
|
| 496 |
+
}
|
| 497 |
+
|
| 498 |
if (this.responseFormat === 'verbose_json') {
|
| 499 |
|
| 500 |
// Validate segments
|
|
|
|
| 975 |
this.uploadPaused = false;
|
| 976 |
}, 1500);
|
| 977 |
},
|
|
|
|
| 978 |
async startChunkedUpload() {
|
| 979 |
if (!this.resumableFile || this.uploadInProgress) return;
|
| 980 |
|
|
|
|
| 988 |
|
| 989 |
await this.uploadNextChunk();
|
| 990 |
},
|
|
|
|
| 991 |
async uploadNextChunk() {
|
| 992 |
if (this.uploadPaused || !this.uploadInProgress) return;
|
| 993 |
|
|
|
|
| 1055 |
this.uploadPaused = true;
|
| 1056 |
}
|
| 1057 |
},
|
|
|
|
| 1058 |
formatTimeRemaining(seconds) {
|
| 1059 |
if (seconds < 60) {
|
| 1060 |
return `${seconds} sec`;
|
|
|
|
| 1068 |
return `${hours} hr ${minutes} min`;
|
| 1069 |
}
|
| 1070 |
},
|
|
|
|
| 1071 |
pauseUpload() {
|
| 1072 |
this.uploadPaused = true;
|
| 1073 |
},
|
|
|
|
| 1074 |
async resumeUpload() {
|
| 1075 |
this.uploadPaused = false;
|
| 1076 |
await this.uploadNextChunk();
|
| 1077 |
},
|
|
|
|
| 1078 |
async cancelUpload() {
|
| 1079 |
if (!this.uploadId) return;
|
| 1080 |
|
|
|
|
| 1096 |
this.uploadProgress = 0;
|
| 1097 |
this.currentChunkIndex = 0;
|
| 1098 |
},
|
|
|
|
| 1099 |
async finalizeUpload() {
|
| 1100 |
try {
|
| 1101 |
// Check if uploadId exists
|
|
|
|
| 1181 |
this.showAudioFilesModal = true;
|
| 1182 |
this.loadUploadedAudioFiles();
|
| 1183 |
},
|
|
|
|
| 1184 |
async deleteAudioFile(file) {
|
| 1185 |
try {
|
| 1186 |
this.$buefy.dialog.confirm({
|
| 1187 |
title: 'Delete Audio File',
|
| 1188 |
+
message: `Are you sure you want to delete "${file.filename}"?`,
|
| 1189 |
confirmText: 'Delete',
|
| 1190 |
type: 'is-danger',
|
| 1191 |
hasIcon: true,
|
| 1192 |
onConfirm: async () => {
|
| 1193 |
+
// Construct the API endpoint with user_id if admin is deleting another user's file
|
| 1194 |
+
let endpoint = `/api/audio-files/${file.filename}`;
|
| 1195 |
+
if (this.isAdmin && file.user_id && file.user_id !== this.currentUser.id) {
|
| 1196 |
+
endpoint += `?user_id=${file.user_id}`;
|
| 1197 |
+
}
|
| 1198 |
+
|
| 1199 |
+
const response = await fetch(endpoint, {
|
| 1200 |
method: 'DELETE',
|
| 1201 |
headers: {
|
| 1202 |
'Authorization': `Bearer ${this.token}`
|
|
|
|
| 1208 |
}
|
| 1209 |
|
| 1210 |
// Remove from local list
|
| 1211 |
+
this.uploadedAudioFiles = this.uploadedAudioFiles.filter(f =>
|
| 1212 |
+
!(f.filename === file.filename &&
|
| 1213 |
+
(!file.user_id || f.user_id === file.user_id))
|
| 1214 |
+
);
|
| 1215 |
|
| 1216 |
this.$buefy.toast.open({
|
| 1217 |
message: 'Audio file deleted successfully',
|
|
|
|
| 1227 |
});
|
| 1228 |
}
|
| 1229 |
},
|
|
|
|
| 1230 |
async selectAudioFile(file) {
|
| 1231 |
try {
|
| 1232 |
+
// Construct the correct URL based on whether the file has a user_id (admin view)
|
| 1233 |
+
const fileUrl = file.user_id
|
| 1234 |
+
? `/uploads/${file.user_id}/${file.filename}`
|
| 1235 |
+
: `/uploads/${this.currentUser.id}/${file.filename}`;
|
| 1236 |
+
|
| 1237 |
// Fetch the file from the server to create a proper File object
|
| 1238 |
+
const response = await fetch(fileUrl);
|
| 1239 |
const blob = await response.blob();
|
| 1240 |
|
| 1241 |
// Create a File object from the blob
|
|
|
|
| 1245 |
|
| 1246 |
// Set the audio file and URL
|
| 1247 |
this.audioFile = [fileObj];
|
| 1248 |
+
this.audioUrl = fileUrl;
|
| 1249 |
|
| 1250 |
// Initialize audio player with the selected file
|
| 1251 |
this.initializeAudio();
|
|
|
|
| 1265 |
type: 'is-danger'
|
| 1266 |
});
|
| 1267 |
}
|
| 1268 |
+
},
|
| 1269 |
+
async loadAllUserCredits() {
|
| 1270 |
+
try {
|
| 1271 |
+
this.loadingCredits = true;
|
| 1272 |
+
|
| 1273 |
+
const response = await fetch('/api/admin/credits', {
|
| 1274 |
+
headers: {
|
| 1275 |
+
'Authorization': `Bearer ${this.token}`
|
| 1276 |
+
}
|
| 1277 |
+
});
|
| 1278 |
+
|
| 1279 |
+
if (!response.ok) {
|
| 1280 |
+
throw new Error('Failed to load credit information');
|
| 1281 |
+
}
|
| 1282 |
+
|
| 1283 |
+
this.userCredits = await response.json();
|
| 1284 |
+
|
| 1285 |
+
} catch (error) {
|
| 1286 |
+
console.error('Error loading all credits:', error);
|
| 1287 |
+
this.$buefy.toast.open({
|
| 1288 |
+
message: `Error: ${error.message}`,
|
| 1289 |
+
type: 'is-danger'
|
| 1290 |
+
});
|
| 1291 |
+
} finally {
|
| 1292 |
+
this.loadingCredits = false;
|
| 1293 |
+
}
|
| 1294 |
+
},
|
| 1295 |
+
editUserCredits(userCredit) {
|
| 1296 |
+
this.selectedUserCredit = userCredit;
|
| 1297 |
+
this.editCreditForm.minutes_used = userCredit.minutes_used;
|
| 1298 |
+
this.editCreditForm.minutes_quota = userCredit.minutes_quota;
|
| 1299 |
+
this.showCreditEditModal = true;
|
| 1300 |
+
},
|
| 1301 |
+
async saveUserCredits() {
|
| 1302 |
+
if (!this.selectedUserCredit) return;
|
| 1303 |
+
|
| 1304 |
+
try {
|
| 1305 |
+
this.savingCredits = true;
|
| 1306 |
+
|
| 1307 |
+
const response = await fetch(`/api/admin/credits/${this.selectedUserCredit.user_id}`, {
|
| 1308 |
+
method: 'PUT',
|
| 1309 |
+
headers: {
|
| 1310 |
+
'Content-Type': 'application/json',
|
| 1311 |
+
'Authorization': `Bearer ${this.token}`
|
| 1312 |
+
},
|
| 1313 |
+
body: JSON.stringify({
|
| 1314 |
+
minutes_used: this.editCreditForm.minutes_used,
|
| 1315 |
+
minutes_quota: this.editCreditForm.minutes_quota
|
| 1316 |
+
})
|
| 1317 |
+
});
|
| 1318 |
+
|
| 1319 |
+
if (!response.ok) {
|
| 1320 |
+
throw new Error('Failed to update credit information');
|
| 1321 |
+
}
|
| 1322 |
+
|
| 1323 |
+
// Update local data
|
| 1324 |
+
const updatedCredit = await response.json();
|
| 1325 |
+
const index = this.userCredits.findIndex(c => c.user_id === updatedCredit.user_id);
|
| 1326 |
+
if (index !== -1) {
|
| 1327 |
+
this.userCredits[index] = updatedCredit;
|
| 1328 |
+
}
|
| 1329 |
+
|
| 1330 |
+
this.$buefy.toast.open({
|
| 1331 |
+
message: 'Credit information updated successfully',
|
| 1332 |
+
type: 'is-success'
|
| 1333 |
+
});
|
| 1334 |
+
|
| 1335 |
+
this.showCreditEditModal = false;
|
| 1336 |
+
} catch (error) {
|
| 1337 |
+
console.error('Error updating credits:', error);
|
| 1338 |
+
this.$buefy.toast.open({
|
| 1339 |
+
message: `Error: ${error.message}`,
|
| 1340 |
+
type: 'is-danger'
|
| 1341 |
+
});
|
| 1342 |
+
} finally {
|
| 1343 |
+
this.savingCredits = false;
|
| 1344 |
+
}
|
| 1345 |
+
},
|
| 1346 |
+
async resetUserCredits(userCredit) {
|
| 1347 |
+
try {
|
| 1348 |
+
this.$buefy.dialog.confirm({
|
| 1349 |
+
title: 'Reset Credits',
|
| 1350 |
+
message: `Are you sure you want to reset credits for user "${userCredit.username}"?`,
|
| 1351 |
+
confirmText: 'Reset',
|
| 1352 |
+
type: 'is-warning',
|
| 1353 |
+
hasIcon: true,
|
| 1354 |
+
onConfirm: async () => {
|
| 1355 |
+
const response = await fetch(`/api/admin/credits/${userCredit.user_id}/reset`, {
|
| 1356 |
+
method: 'POST',
|
| 1357 |
+
headers: {
|
| 1358 |
+
'Authorization': `Bearer ${this.token}`
|
| 1359 |
+
}
|
| 1360 |
+
});
|
| 1361 |
+
|
| 1362 |
+
if (!response.ok) {
|
| 1363 |
+
throw new Error('Failed to reset credit information');
|
| 1364 |
+
}
|
| 1365 |
+
|
| 1366 |
+
// Update local data
|
| 1367 |
+
const updatedCredit = await response.json();
|
| 1368 |
+
const index = this.userCredits.findIndex(c => c.user_id === updatedCredit.user_id);
|
| 1369 |
+
if (index !== -1) {
|
| 1370 |
+
this.userCredits[index] = updatedCredit;
|
| 1371 |
+
}
|
| 1372 |
+
|
| 1373 |
+
this.$buefy.toast.open({
|
| 1374 |
+
message: `Credits reset for user ${userCredit.username}`,
|
| 1375 |
+
type: 'is-success'
|
| 1376 |
+
});
|
| 1377 |
+
}
|
| 1378 |
+
});
|
| 1379 |
+
} catch (error) {
|
| 1380 |
+
console.error('Error resetting credits:', error);
|
| 1381 |
+
this.$buefy.toast.open({
|
| 1382 |
+
message: `Error: ${error.message}`,
|
| 1383 |
+
type: 'is-danger'
|
| 1384 |
+
});
|
| 1385 |
+
}
|
| 1386 |
+
},
|
| 1387 |
+
async resetUserQuota(userCredit) {
|
| 1388 |
+
try {
|
| 1389 |
+
this.$buefy.dialog.prompt({
|
| 1390 |
+
title: 'Reset Quota',
|
| 1391 |
+
message: `Enter new quota value for user "${userCredit.username}"`,
|
| 1392 |
+
inputAttrs: {
|
| 1393 |
+
type: 'number',
|
| 1394 |
+
min: '0',
|
| 1395 |
+
value: '60',
|
| 1396 |
+
placeholder: 'Minutes'
|
| 1397 |
+
},
|
| 1398 |
+
confirmText: 'Reset Quota',
|
| 1399 |
+
type: 'is-warning',
|
| 1400 |
+
hasIcon: true,
|
| 1401 |
+
onConfirm: async (value) => {
|
| 1402 |
+
const newQuota = parseInt(value);
|
| 1403 |
+
if (isNaN(newQuota) || newQuota < 0) {
|
| 1404 |
+
this.$buefy.toast.open({
|
| 1405 |
+
message: 'Please enter a valid non-negative number',
|
| 1406 |
+
type: 'is-danger'
|
| 1407 |
+
});
|
| 1408 |
+
return;
|
| 1409 |
+
}
|
| 1410 |
+
|
| 1411 |
+
const response = await fetch(`/api/admin/credits/${userCredit.user_id}/reset-quota`, {
|
| 1412 |
+
method: 'POST',
|
| 1413 |
+
headers: {
|
| 1414 |
+
'Content-Type': 'application/json',
|
| 1415 |
+
'Authorization': `Bearer ${this.token}`
|
| 1416 |
+
},
|
| 1417 |
+
body: JSON.stringify({
|
| 1418 |
+
new_quota: newQuota
|
| 1419 |
+
})
|
| 1420 |
+
});
|
| 1421 |
+
|
| 1422 |
+
if (!response.ok) {
|
| 1423 |
+
throw new Error('Failed to reset quota');
|
| 1424 |
+
}
|
| 1425 |
+
|
| 1426 |
+
// Update local data
|
| 1427 |
+
const updatedCredit = await response.json();
|
| 1428 |
+
const index = this.userCredits.findIndex(c => c.user_id === updatedCredit.user_id);
|
| 1429 |
+
if (index !== -1) {
|
| 1430 |
+
this.userCredits[index] = updatedCredit;
|
| 1431 |
+
}
|
| 1432 |
+
|
| 1433 |
+
this.$buefy.toast.open({
|
| 1434 |
+
message: `Quota reset for user ${userCredit.username}`,
|
| 1435 |
+
type: 'is-success'
|
| 1436 |
+
});
|
| 1437 |
+
}
|
| 1438 |
+
});
|
| 1439 |
+
} catch (error) {
|
| 1440 |
+
console.error('Error resetting quota:', error);
|
| 1441 |
+
this.$buefy.toast.open({
|
| 1442 |
+
message: `Error: ${error.message}`,
|
| 1443 |
+
type: 'is-danger'
|
| 1444 |
+
});
|
| 1445 |
+
}
|
| 1446 |
+
},
|
| 1447 |
+
refreshAdminData() {
|
| 1448 |
+
if (this.activeAdminTab === 0) {
|
| 1449 |
+
this.loadUsers();
|
| 1450 |
+
} else if (this.activeAdminTab === 1) {
|
| 1451 |
+
this.loadAllUserCredits();
|
| 1452 |
+
}
|
| 1453 |
+
},
|
| 1454 |
+
downloadTranscription(transcription) {
|
| 1455 |
+
try {
|
| 1456 |
+
// Create a JSON object with all the transcription data
|
| 1457 |
+
const jsonData = {
|
| 1458 |
+
id: transcription.id,
|
| 1459 |
+
name: transcription.name,
|
| 1460 |
+
audio_file: transcription.audio_file,
|
| 1461 |
+
text: transcription.text,
|
| 1462 |
+
segments: transcription.segments || [],
|
| 1463 |
+
created_at: transcription.created_at,
|
| 1464 |
+
model: transcription.model,
|
| 1465 |
+
language: transcription.language
|
| 1466 |
+
};
|
| 1467 |
+
|
| 1468 |
+
// Convert to a JSON string with nice formatting
|
| 1469 |
+
const jsonString = JSON.stringify(jsonData, null, 2);
|
| 1470 |
+
|
| 1471 |
+
// Create a blob with the JSON data
|
| 1472 |
+
const blob = new Blob([jsonString], { type: 'application/json' });
|
| 1473 |
+
|
| 1474 |
+
// Create a URL for the blob
|
| 1475 |
+
const url = URL.createObjectURL(blob);
|
| 1476 |
+
|
| 1477 |
+
// Create a temporary link element
|
| 1478 |
+
const link = document.createElement('a');
|
| 1479 |
+
link.href = url;
|
| 1480 |
+
|
| 1481 |
+
// Set the filename
|
| 1482 |
+
const filename = `transcription_${transcription.id}_${new Date().toISOString().slice(0, 10)}.json`;
|
| 1483 |
+
link.download = filename;
|
| 1484 |
+
|
| 1485 |
+
// Append the link to the body
|
| 1486 |
+
document.body.appendChild(link);
|
| 1487 |
+
|
| 1488 |
+
// Trigger the download
|
| 1489 |
+
link.click();
|
| 1490 |
+
|
| 1491 |
+
// Clean up
|
| 1492 |
+
document.body.removeChild(link);
|
| 1493 |
+
URL.revokeObjectURL(url);
|
| 1494 |
+
|
| 1495 |
+
this.$buefy.toast.open({
|
| 1496 |
+
message: `Transcription downloaded as ${filename}`,
|
| 1497 |
+
type: 'is-success'
|
| 1498 |
+
});
|
| 1499 |
+
} catch (error) {
|
| 1500 |
+
console.error('Error downloading transcription:', error);
|
| 1501 |
+
this.$buefy.toast.open({
|
| 1502 |
+
message: `Error downloading transcription: ${error.message}`,
|
| 1503 |
+
type: 'is-danger'
|
| 1504 |
+
});
|
| 1505 |
+
}
|
| 1506 |
+
},
|
| 1507 |
+
downloadCurrentTranscription() {
|
| 1508 |
+
if (!this.transcriptionText) {
|
| 1509 |
+
this.$buefy.toast.open({
|
| 1510 |
+
message: 'No transcription to download',
|
| 1511 |
+
type: 'is-warning'
|
| 1512 |
+
});
|
| 1513 |
+
return;
|
| 1514 |
+
}
|
| 1515 |
+
|
| 1516 |
+
try {
|
| 1517 |
+
// Create a JSON object with the current transcription data
|
| 1518 |
+
const jsonData = {
|
| 1519 |
+
text: this.transcriptionText,
|
| 1520 |
+
segments: this.segments || [],
|
| 1521 |
+
audio_file: this.audioFile && this.audioFile.length > 0 ? this.audioFile[0].name : 'unknown',
|
| 1522 |
+
created_at: new Date().toISOString(),
|
| 1523 |
+
model: this.selectedModel,
|
| 1524 |
+
language: this.selectedLanguage
|
| 1525 |
+
};
|
| 1526 |
+
|
| 1527 |
+
// Convert to a JSON string with nice formatting
|
| 1528 |
+
const jsonString = JSON.stringify(jsonData, null, 2);
|
| 1529 |
+
|
| 1530 |
+
// Create a blob with the JSON data
|
| 1531 |
+
const blob = new Blob([jsonString], { type: 'application/json' });
|
| 1532 |
+
|
| 1533 |
+
// Create a URL for the blob
|
| 1534 |
+
const url = URL.createObjectURL(blob);
|
| 1535 |
+
|
| 1536 |
+
// Create a temporary link element
|
| 1537 |
+
const link = document.createElement('a');
|
| 1538 |
+
link.href = url;
|
| 1539 |
+
|
| 1540 |
+
// Set the filename
|
| 1541 |
+
const filename = `transcription_${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.json`;
|
| 1542 |
+
link.download = filename;
|
| 1543 |
+
|
| 1544 |
+
// Append the link to the body
|
| 1545 |
+
document.body.appendChild(link);
|
| 1546 |
+
|
| 1547 |
+
// Trigger the download
|
| 1548 |
+
link.click();
|
| 1549 |
+
|
| 1550 |
+
// Clean up
|
| 1551 |
+
document.body.removeChild(link);
|
| 1552 |
+
URL.revokeObjectURL(url);
|
| 1553 |
+
|
| 1554 |
+
this.$buefy.toast.open({
|
| 1555 |
+
message: `Transcription downloaded as ${filename}`,
|
| 1556 |
+
type: 'is-success'
|
| 1557 |
+
});
|
| 1558 |
+
} catch (error) {
|
| 1559 |
+
console.error('Error downloading current transcription:', error);
|
| 1560 |
+
this.$buefy.toast.open({
|
| 1561 |
+
message: `Error downloading transcription: ${error.message}`,
|
| 1562 |
+
type: 'is-danger'
|
| 1563 |
+
});
|
| 1564 |
+
}
|
| 1565 |
+
},
|
| 1566 |
+
getQuotaTagType(credit) {
|
| 1567 |
+
const remaining = credit.minutes_remaining;
|
| 1568 |
+
const quota = credit.minutes_quota;
|
| 1569 |
+
|
| 1570 |
+
if (remaining <= 0) {
|
| 1571 |
+
return 'is-danger';
|
| 1572 |
+
} else if (remaining < quota * 0.2) {
|
| 1573 |
+
return 'is-warning';
|
| 1574 |
+
} else {
|
| 1575 |
+
return 'is-success';
|
| 1576 |
+
}
|
| 1577 |
+
},
|
| 1578 |
+
getAudioUrl(file) {
|
| 1579 |
+
if (file.user_id) {
|
| 1580 |
+
return `/uploads/${file.user_id}/${file.filename}`;
|
| 1581 |
+
} else {
|
| 1582 |
+
return `/uploads/${this.currentUser.id}/${file.filename}`;
|
| 1583 |
+
}
|
| 1584 |
+
},
|
| 1585 |
}, // methods
|
| 1586 |
// Computed properties
|
| 1587 |
computed: {
|
|
|
|
| 1605 |
return this.uploadedAudioFiles.filter(file =>
|
| 1606 |
file.filename.toLowerCase().includes(query)
|
| 1607 |
);
|
| 1608 |
+
},
|
| 1609 |
+
filteredCredits() {
|
| 1610 |
+
if (!this.creditSearchQuery) {
|
| 1611 |
+
return this.userCredits;
|
| 1612 |
+
}
|
| 1613 |
+
|
| 1614 |
+
const query = this.creditSearchQuery.toLowerCase();
|
| 1615 |
+
return this.userCredits.filter(credit =>
|
| 1616 |
+
credit.username.toLowerCase().includes(query) ||
|
| 1617 |
+
credit.user_id.toString().includes(query)
|
| 1618 |
+
);
|
| 1619 |
+
},
|
| 1620 |
+
}, // computed
|
| 1621 |
mounted() {
|
| 1622 |
this.checkAuth();
|
| 1623 |
this.loadTranscriptions();
|
| 1624 |
if (this.isAuthenticated) {
|
| 1625 |
this.loadUploadedAudioFiles();
|
| 1626 |
+
this.loadUserCredits();
|
| 1627 |
}
|
| 1628 |
},
|
| 1629 |
watch: {
|
|
|
|
| 1636 |
showAdminPanel(newVal) {
|
| 1637 |
if (newVal && this.isAdmin) {
|
| 1638 |
this.loadUsers();
|
| 1639 |
+
this.loadAllUserCredits();
|
| 1640 |
}
|
| 1641 |
},
|
| 1642 |
isAuthenticated(newVal) {
|
| 1643 |
if (newVal) {
|
| 1644 |
this.loadUploadedAudioFiles();
|
| 1645 |
+
this.loadUserCredits();
|
| 1646 |
}
|
| 1647 |
}
|
| 1648 |
},
|