bitsnaps commited on
Commit
fae1148
·
verified ·
1 Parent(s): aa4e39d

Update static/js/app.js

Browse files
Files changed (1) hide show
  1. 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}"? This action cannot be undone.`,
1133
  confirmText: 'Delete',
1134
  type: 'is-danger',
1135
  hasIcon: true,
1136
  onConfirm: async () => {
1137
- const response = await fetch(`/api/audio-files/${file.filename}`, {
 
 
 
 
 
 
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 => f.filename !== file.filename);
 
 
 
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(`/uploads/${file.filename}`);
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 = `/uploads/${file.filename}`;
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
  },