bitsnaps commited on
Commit
ff852a8
·
verified ·
1 Parent(s): a13449b

Upload app.js

Browse files
Files changed (1) hide show
  1. static/js/app.js +1259 -0
static/js/app.js ADDED
@@ -0,0 +1,1259 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const { createApp, ref, onMounted } = Vue;
2
+ const app = createApp({
3
+ data() {
4
+ return {
5
+ responseFormat: 'verbose_json',
6
+ temperature: 0,
7
+ chunkSize: 10,
8
+ overlap: 1,
9
+ selection: [1,100],
10
+ systemPrompt: '',
11
+ isAuthenticated: false,
12
+ username: '',
13
+ token: '',
14
+ isLoading: false,
15
+ audioFile: [],
16
+ segments: [],
17
+ transcriptionText: '',
18
+ selectedLanguage: 'auto',
19
+ transcriptions: [],
20
+ showSidebar: true,
21
+ showPlayer: false,
22
+ showApiKeyModal: false,
23
+ audioUrl: null,
24
+ isPlaying: false,
25
+ audioPlayer: null,
26
+ isProcessing: false,
27
+ howl: null,
28
+ activeSegment: null,
29
+ isPlayingSegment: false,
30
+ isMuted: false,
31
+ volume: 0.5,
32
+ currentTime: 0,
33
+ totalDuration: 0,
34
+ seekInterval: null,
35
+ waveformData: null,
36
+ waveformCanvas: null,
37
+ ctx: null,
38
+ showLoginModal: false,
39
+ loginForm: {
40
+ username: '',
41
+ password: '',
42
+ },
43
+ showSignupModal: false,
44
+ signupForm: {
45
+ username: '',
46
+ email: '',
47
+ password: '',
48
+ confirmPassword: ''
49
+ },
50
+ // Admin panel data
51
+ showAdminPanel: false,
52
+ isAdmin: false,
53
+ currentUser: null,
54
+ users: [],
55
+ loadingUsers: false,
56
+ userSearchQuery: '',
57
+ // processingProgress: 0,
58
+ // totalChunks: 0,
59
+ // showVolumeSlider: false,
60
+ // Resumable upload properties
61
+ showUploadModal: false,
62
+ resumableFile: null,
63
+ uploadId: null,
64
+ chunkUploadSize: 1024 * 1024, // 1MB chunks
65
+ uploadProgress: 0,
66
+ uploadInProgress: false,
67
+ uploadPaused: false,
68
+ currentChunkIndex: 0,
69
+ totalChunks: 0,
70
+ // Upload statistics
71
+ uploadStartTime: null,
72
+ uploadSpeed: null,
73
+ estimatedTimeRemaining: null,
74
+ lastUploadedBytes: 0,
75
+ lastUploadTime: null,
76
+ // list uploaded files
77
+ uploadedAudioFiles: [],
78
+ loadingAudioFiles: false,
79
+ showAudioFilesModal: false,
80
+ audioFileSearchQuery: '',
81
+ }
82
+ },
83
+
84
+ methods: {
85
+ async login() {
86
+ this.isAuthenticated = true;
87
+ },
88
+
89
+ logout() {
90
+ this.token = '';
91
+ this.username = '';
92
+ this.isAuthenticated = false;
93
+ this.isAdmin = false;
94
+ this.currentUser = null;
95
+ this.users = [];
96
+ this.transcriptions = [];
97
+ localStorage.removeItem('token');
98
+
99
+ // Reset UI state
100
+ this.audioUrl = null;
101
+ this.audioFile = [];
102
+ this.segments = [];
103
+ this.transcriptionText = '';
104
+
105
+ // Show notification
106
+ this.$buefy.toast.open({
107
+ message: 'Successfully logged out!',
108
+ type: 'is-success'
109
+ });
110
+
111
+ // Redirect to home page if not already there
112
+ if (window.location.pathname !== '/') {
113
+ window.location.href = '/';
114
+ } else {
115
+ // window.location.reload();
116
+ }
117
+ },
118
+
119
+ async handleAudioUpload(event) {
120
+ if (!event.target.files || !event.target.files.length) return;
121
+
122
+ const file = event.target.files[0];
123
+ const fileUrl = URL.createObjectURL(file);
124
+ fileExt = file.name.split('.').splice(-1);
125
+
126
+ this.howl = new Howl({
127
+ src: [fileUrl],
128
+ format: this.fileExt, // This should be an array
129
+ html5: true,
130
+ onend: () => {
131
+ this.isPlayingSegment = false;
132
+ this.activeSegment = null;
133
+ }
134
+ });
135
+
136
+ this.audioUrl = fileUrl;
137
+ this.audioFile = [file];
138
+ },
139
+
140
+ initializeAudio() {
141
+ if (this.howl) {
142
+ this.howl.unload();
143
+ }
144
+
145
+ const fileExt = this.audioFile[0].name.split('.').splice(-1);
146
+ this.howl = new Howl({
147
+ src: [this.audioUrl],
148
+ format: fileExt,
149
+ html5: true,
150
+ volume: this.volume,
151
+ sprite: this.createSprites(),
152
+ onplay: () => {
153
+ this.isPlaying = true;
154
+ this.startSeekUpdate();
155
+ },
156
+ onpause: () => {
157
+ this.isPlaying = false;
158
+ this.stopSeekUpdate();
159
+ },
160
+ onstop: () => {
161
+ this.isPlaying = false;
162
+ this.currentTime = 0;
163
+ this.stopSeekUpdate();
164
+ },
165
+ onend: () => {
166
+ this.isPlaying = false;
167
+ this.currentTime = 0;
168
+ this.stopSeekUpdate();
169
+ },
170
+ onload: () => {
171
+ this.totalDuration = this.howl.duration();
172
+ /*/ Set selection to the length of the audio file
173
+ this.selection = [];
174
+ for (let i = 0; i < this.totalDuration; i += this.chunkSize) {
175
+ this.selection.push(i);
176
+ }*/
177
+ }
178
+ });
179
+ // Set initial volume
180
+ this.updateVolume();
181
+ },
182
+
183
+ async initializeWaveform() {
184
+ if (!this.audioUrl) return;
185
+
186
+ // Load audio file and decode it
187
+ const response = await fetch(this.audioUrl);
188
+ const arrayBuffer = await response.arrayBuffer();
189
+ const audioContext = new AudioContext();
190
+ const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
191
+
192
+ // Get waveform data
193
+ const channelData = audioBuffer.getChannelData(0);
194
+ this.waveformData = this.processWaveformData(channelData);
195
+
196
+ // Draw waveform
197
+ this.$nextTick(() => {
198
+ this.drawWaveform();
199
+ });
200
+
201
+ // Set initial selection to full audio
202
+ this.selection = [0, 100];
203
+ },
204
+
205
+ processWaveformData(data) {
206
+ const step = Math.ceil(data.length / 1000);
207
+ const waveform = [];
208
+ for (let i = 0; i < data.length; i += step) {
209
+ const slice = data.slice(i, i + step);
210
+ // Use reducer instead of spread operator for large arrays
211
+ const max = slice.reduce((a, b) => Math.max(a, b), -Infinity);
212
+ const min = slice.reduce((a, b) => Math.min(a, b), Infinity);
213
+ waveform.push({ max, min });
214
+ }
215
+ return waveform;
216
+ },
217
+
218
+ drawWaveform() {
219
+ if (this.$refs.waveform){
220
+ const canvas = document.createElement('canvas');
221
+ const ctx = canvas.getContext('2d');
222
+ const width = this.$refs.waveform.offsetWidth;
223
+ const height = 100;
224
+
225
+ canvas.width = width;
226
+ canvas.height = height;
227
+ this.$refs.waveform.style.backgroundImage = `url(${canvas.toDataURL()})`;
228
+
229
+ ctx.fillStyle = '#3273dc';
230
+ this.waveformData.forEach((point, i) => {
231
+ const x = (i / this.waveformData.length) * width;
232
+ const y = (1 - point.max) * height / 2;
233
+ const h = (point.max - point.min) * height / 2;
234
+ ctx.fillRect(x, y, 1, h);
235
+ });
236
+ }
237
+ },
238
+
239
+ createSprites() {
240
+ const sprites = {};
241
+ this.segments.forEach((segment, index) => {
242
+ sprites[`segment_${index}`] = [segment.start * 1000, (segment.end - segment.start) * 1000];
243
+ });
244
+ return sprites;
245
+ },
246
+
247
+ togglePlay() {
248
+ if (this.isPlaying) {
249
+ this.howl.pause();
250
+ } else {
251
+ this.howl.play();
252
+ }
253
+ },
254
+
255
+ stopAudio() {
256
+ this.howl.stop();
257
+ },
258
+
259
+ updateVolume() {
260
+ if (this.volume <= 0) {
261
+ this.isMuted = true;
262
+ this.howl.mute(true);
263
+ } else if (this.isMuted && this.volume > 0) {
264
+ this.isMuted = false;
265
+ this.howl.mute(false);
266
+ }
267
+ this.howl.volume(this.volume);
268
+ },
269
+
270
+ toggleVolumeSlider() {
271
+ this.showVolumeSlider = !this.showVolumeSlider;
272
+ },
273
+
274
+ toggleMute() {
275
+ this.isMuted = !this.isMuted;
276
+ this.howl.mute(this.isMuted);
277
+ if (!this.isMuted && this.volume === 0) {
278
+ this.volume = 0.5;
279
+ }
280
+ },
281
+
282
+ playSegment(segment) {
283
+ const index = this.segments.indexOf(segment);
284
+ if (index !== -1) {
285
+ this.howl.play(`segment_${index}`);
286
+ }
287
+ },
288
+
289
+ startSeekUpdate() {
290
+ this.seekInterval = setInterval(() => {
291
+ this.currentTime = this.howl.seek() || 0;
292
+ }, 100);
293
+ },
294
+
295
+ stopSeekUpdate() {
296
+ clearInterval(this.seekInterval);
297
+ },
298
+
299
+ formatTime(seconds) {
300
+ const mins = Math.floor(seconds / 60);
301
+ const secs = Math.floor(seconds % 60);
302
+ return `${mins}:${secs < 10 ? '0' : ''}${secs}`;
303
+ },
304
+
305
+ togglePlayPause() {
306
+ if (this.audioPlayer.paused) {
307
+ this.audioPlayer.play();
308
+ this.isPlaying = true;
309
+ } else {
310
+ this.audioPlayer.pause();
311
+ this.isPlaying = false;
312
+ }
313
+ },
314
+
315
+ playSegment(segment) {
316
+ if (!this.howl) return;
317
+
318
+ if (this.activeSegment === segment.id && this.isPlayingSegment) {
319
+ this.howl.pause();
320
+ this.isPlayingSegment = false;
321
+ return;
322
+ }
323
+
324
+ this.howl.seek(segment.start);
325
+ this.howl.play();
326
+ this.activeSegment = segment.id;
327
+ this.isPlayingSegment = true;
328
+
329
+ this.howl.once('end', () => {
330
+ this.isPlayingSegment = false;
331
+ this.activeSegment = null;
332
+ });
333
+ },
334
+
335
+ copySegmentText(text) {
336
+ navigator.clipboard.writeText(text).then(() => {
337
+ this.$buefy.toast.open({
338
+ message: 'Text copied to clipboard',
339
+ type: 'is-success',
340
+ position: 'is-bottom-right'
341
+ });
342
+ }).catch(() => {
343
+ this.$buefy.toast.open({
344
+ message: 'Failed to copy text',
345
+ type: 'is-danger',
346
+ position: 'is-bottom-right'
347
+ });
348
+ });
349
+ },
350
+
351
+ deleteSegment(index) {
352
+ this.segments.splice(index, 1);
353
+ this.updateTranscriptionText();
354
+ },
355
+
356
+ updateTranscriptionText() {
357
+ this.transcriptionText = this.segments
358
+ .map(segment => segment.text)
359
+ .join(' ');
360
+ },
361
+
362
+ formatTime(seconds) {
363
+ const mins = Math.floor(seconds / 60);
364
+ const secs = Math.floor(seconds % 60);
365
+ return `${mins}:${secs < 10 ? '0' : ''}${secs}`;
366
+ },
367
+
368
+ async processTranscription() {
369
+ const formData = new FormData();
370
+ formData.append('file', this.audioFile[0]);
371
+ formData.append('response_format', this.responseFormat);
372
+ formData.append('temperature', this.temperature.toString());
373
+ formData.append('chunk_size', parseInt(this.chunkSize * 60).toString());
374
+ formData.append('overlap', this.overlap.toString());
375
+ formData.append('prompt', this.systemPrompt.toString());
376
+
377
+ // Add selection timestamps
378
+ const startTime = (this.selection[0] * this.totalDuration / 100).toFixed(2);
379
+ const endTime = (this.selection[1] * this.totalDuration / 100).toFixed(2);
380
+ formData.append('start_time', startTime);
381
+ formData.append('end_time', endTime);
382
+
383
+ if (this.selectedLanguage !== 'auto') {
384
+ formData.append('language', this.selectedLanguage);
385
+ }
386
+ try {
387
+ this.isProcessing = true;
388
+
389
+ /*/ Monitor the progress of the upload for user feedback (need to be updated for reading back response)
390
+ const xhr = new XMLHttpRequest();
391
+ xhr.upload.onloadstart = function (event) {
392
+ console.log('Upload started');
393
+ };
394
+
395
+ xhr.upload.onprogress = function (event) {
396
+ if (event.lengthComputable) {
397
+ const percentComplete = (event.loaded / event.total) * 100;
398
+ console.log(`Upload progress: ${percentComplete.toFixed(2)}%`);
399
+ }
400
+ };
401
+ xhr.upload.onload = function () {
402
+ console.log('Upload complete');
403
+ };
404
+ xhr.onerror = function () {
405
+ console.error('Error uploading file');
406
+ };
407
+
408
+ xhr.open('POST', '/api/upload', true);
409
+ xhr.send(formData);*/
410
+
411
+ const response = await fetch('/api/upload', {
412
+ method: 'POST',
413
+ headers: {
414
+ 'Authorization': `Bearer ${this.token}`
415
+ },
416
+ body: formData
417
+ });
418
+ if (!response.ok) {
419
+ const errorData = await response.json();
420
+ throw new Error(errorData.detail || 'Transcription failed');
421
+ }
422
+ const result = await response.json();
423
+
424
+ // Check for backend validation errors
425
+ if (result.metadata?.errors?.length) {
426
+ console.error('Chunk processing errors:', result.metadata.errors);
427
+ this.$buefy.snackbar.open({
428
+ message: `${result.metadata.errors.length} chunks failed to process`,
429
+ type: 'is-warning',
430
+ position: 'is-bottom-right'
431
+ });
432
+ }
433
+
434
+ if (this.responseFormat === 'verbose_json') {
435
+
436
+ // Validate segments
437
+ if (!result.segments) {
438
+ throw new Error('Server returned invalid segments format');
439
+ }
440
+
441
+ this.segments = result.segments;
442
+ this.updateTranscriptionText();
443
+ } else {
444
+ this.transcriptionText = result.text || result;
445
+ }
446
+
447
+ // this.totalChunks = result.metadata?.total_chunks || 0;
448
+ // this.processingProgress = ((processedChunks / this.totalChunks) * 100).toFixed(1);
449
+ } catch (error) {
450
+ console.error('Transcription error:', error);
451
+ this.$buefy.toast.open({
452
+ message: `Error: ${error.message}`,
453
+ type: 'is-danger',
454
+ position: 'is-bottom-right'
455
+ });
456
+ }
457
+ this.isProcessing = false;
458
+ this.showPlayer = true;
459
+ },
460
+
461
+ async loadTranscriptions() {
462
+ try {
463
+ // Check if token exists
464
+ if (!this.token) {
465
+ this.token = localStorage.getItem('token');
466
+ if (!this.token) {
467
+ // console.log('No authentication token found');
468
+ return;
469
+ }
470
+ }
471
+
472
+ const response = await fetch('/api/transcriptions', {
473
+ headers: {
474
+ 'Authorization': `Bearer ${this.token}`
475
+ }
476
+ });
477
+
478
+ if (response.status === 401) {
479
+ // Token expired or invalid
480
+ console.log('Authentication token expired or invalid');
481
+ this.logout();
482
+ this.$buefy.toast.open({
483
+ message: 'Your session has expired. Please login again.',
484
+ type: 'is-warning'
485
+ });
486
+ return;
487
+ }
488
+
489
+ if (!response.ok) {
490
+ throw new Error(`HTTP error! Status: ${response.status}`);
491
+ }
492
+
493
+ this.transcriptions = await response.json();
494
+ } catch (error) {
495
+ console.error('Error loading transcriptions:', error);
496
+ this.$buefy.toast.open({
497
+ message: `Failed to load transcriptions: ${error.message}`,
498
+ type: 'is-danger'
499
+ });
500
+ }
501
+ },
502
+
503
+ async loadTranscription(transcription) {
504
+ try {
505
+ const response = await fetch(`/api/transcriptions/${transcription.id}`, {
506
+ headers: {
507
+ 'Authorization': `Bearer ${this.token}`
508
+ }
509
+ });
510
+ const data = await response.json();
511
+ // Set the audio file for playback
512
+ this.transcriptionText = data['text'];
513
+ this.segments = data['segments'];
514
+ // this.audioFile = data['audio_file'];
515
+ if (data.audio_file) {
516
+ // this.audioUrl = `/uploads/${data['audio_file']}`;
517
+ // this.audioFile.push(data['audio_file']);
518
+ // this.initializeAudio();
519
+ }
520
+ } catch (error) {
521
+ console.error('Error loading transcription:', error);
522
+ }
523
+ },
524
+
525
+ async saveTranscription() {
526
+ if (this.audioFile.length == 0){
527
+ this.$buefy.toast.open({
528
+ message: 'Please upload an audio file first',
529
+ type: 'is-warning',
530
+ position: 'is-bottom-right'
531
+ });
532
+ return;
533
+ }
534
+ try {
535
+ this.isProcessing = true;
536
+ const response = await fetch('/api/save-transcription', {
537
+ method: 'POST',
538
+ headers: {
539
+ 'Content-Type': 'application/json',
540
+ 'Authorization': `Bearer ${this.token}`
541
+ },
542
+ body: JSON.stringify({
543
+ text: this.transcriptionText,
544
+ segments: this.segments,
545
+ audio_file: this.audioFile[0].name
546
+ })
547
+ });
548
+
549
+ if (!response.ok) throw new Error('Failed to save transcription');
550
+
551
+ await this.loadTranscriptions();
552
+ this.$buefy.toast.open({
553
+ message: 'Transcription saved successfully',
554
+ type: 'is-success',
555
+ position: 'is-bottom-right'
556
+ });
557
+ } catch (error) {
558
+ console.error('Error saving transcription:', error);
559
+ this.$buefy.toast.open({
560
+ message: `Error: ${error.message}`,
561
+ type: 'is-danger',
562
+ position: 'is-bottom-right'
563
+ });
564
+ }
565
+ this.isProcessing = false;
566
+ },
567
+
568
+ toggleSidebar() {
569
+ this.showSidebar = !this.showSidebar;
570
+ },
571
+
572
+ togglePlayer() {
573
+ this.showPlayer = !this.showPlayer;
574
+ },
575
+
576
+ async deleteTranscription(id) {
577
+ try {
578
+ // Show confirmation dialog
579
+ this.$buefy.dialog.confirm({
580
+ title: 'Delete Transcription',
581
+ message: 'Are you sure you want to delete this transcription? This action cannot be undone.',
582
+ confirmText: 'Delete',
583
+ type: 'is-danger',
584
+ hasIcon: true,
585
+ onConfirm: async () => {
586
+ const response = await fetch(`/api/transcriptions/${id}`, {
587
+ method: 'DELETE',
588
+ headers: {
589
+ 'Authorization': `Bearer ${this.token}`
590
+ }
591
+ });
592
+
593
+ if (!response.ok) throw new Error('Failed to delete transcription');
594
+
595
+ // Remove from local list
596
+ this.transcriptions = this.transcriptions.filter(t => t.id !== id);
597
+
598
+ // Show success message
599
+ this.$buefy.toast.open({
600
+ message: 'Transcription deleted successfully',
601
+ type: 'is-success',
602
+ position: 'is-bottom-right'
603
+ });
604
+ }
605
+ });
606
+ } catch (error) {
607
+ console.error('Error deleting transcription:', error);
608
+ this.$buefy.toast.open({
609
+ message: `Error: ${error.message}`,
610
+ type: 'is-danger',
611
+ position: 'is-bottom-right'
612
+ });
613
+ }
614
+ },
615
+
616
+ async handleLogin() {
617
+ try {
618
+ this.isLoading = true;
619
+ const formData = new FormData();
620
+ formData.append('username', this.loginForm.username); // Make sure this matches the backend expectation
621
+ formData.append('password', this.loginForm.password);
622
+
623
+ const response = await fetch('/token', {
624
+ method: 'POST',
625
+ body: formData
626
+ });
627
+
628
+ if (!response.ok) {
629
+ const errorData = await response.json();
630
+ throw new Error(errorData.detail || 'Login failed');
631
+ }
632
+
633
+ const data = await response.json();
634
+ this.token = data.access_token;
635
+ localStorage.setItem('token', data.access_token);
636
+
637
+ // Get user info
638
+ const userResponse = await fetch('/api/me', {
639
+ headers: {
640
+ 'Authorization': `Bearer ${this.token}`
641
+ }
642
+ });
643
+
644
+ if (!userResponse.ok) {
645
+ throw new Error('Failed to get user information');
646
+ }
647
+
648
+ const userData = await userResponse.json();
649
+ this.username = userData.username;
650
+ this.isAuthenticated = true;
651
+
652
+ this.showLoginModal = false;
653
+ this.$buefy.toast.open({
654
+ message: 'Successfully logged in!',
655
+ type: 'is-success'
656
+ });
657
+
658
+ // Load user data after successful login
659
+ this.loadTranscriptions();
660
+ } catch (error) {
661
+ this.$buefy.toast.open({
662
+ message: `Error: ${error.message}`,
663
+ type: 'is-danger'
664
+ });
665
+ } finally {
666
+ this.isLoading = false;
667
+ }
668
+ },
669
+
670
+ async handleSignup() {
671
+ try {
672
+ this.isLoading = true;
673
+ if (this.signupForm.password !== this.signupForm.confirmPassword) {
674
+ throw new Error('Passwords do not match');
675
+ }
676
+
677
+ const response = await fetch('/api/signup', {
678
+ method: 'POST',
679
+ headers: {
680
+ 'Content-Type': 'application/json'
681
+ },
682
+ body: JSON.stringify({
683
+ username: this.signupForm.username,
684
+ email: this.signupForm.email,
685
+ password: this.signupForm.password
686
+ })
687
+ });
688
+
689
+ if (!response.ok) {
690
+ const error = await response.json();
691
+ throw new Error(error.detail || 'Signup failed');
692
+ }
693
+
694
+ this.showSignupModal = false;
695
+ this.$buefy.toast.open({
696
+ message: 'Account created successfully! Please login.',
697
+ type: 'is-success'
698
+ });
699
+
700
+ // Clear form
701
+ this.signupForm = {
702
+ username: '',
703
+ email: '',
704
+ password: '',
705
+ confirmPassword: ''
706
+ };
707
+
708
+ // Show login modal
709
+ this.showLoginModal = true;
710
+ } catch (error) {
711
+ this.$buefy.toast.open({
712
+ message: `Error: ${error.message}`,
713
+ type: 'is-danger'
714
+ });
715
+ } finally {
716
+ this.isLoading = false;
717
+ }
718
+ },
719
+
720
+ async checkAuth() {
721
+ const token = localStorage.getItem('token');
722
+ if (token) {
723
+ try {
724
+ this.token = token;
725
+ const response = await fetch('/api/me', {
726
+ headers: {
727
+ 'Authorization': `Bearer ${token}`
728
+ }
729
+ });
730
+
731
+ if (response.ok) {
732
+ const userData = await response.json();
733
+ this.username = userData.username;
734
+ this.isAuthenticated = true;
735
+ this.isAdmin = userData.is_admin;
736
+ this.currentUser = userData;
737
+
738
+ // If user is admin, load users list
739
+ if (this.isAdmin) {
740
+ this.loadUsers();
741
+ }
742
+ } else {
743
+ // Token invalid or expired
744
+ this.logout();
745
+ }
746
+ } catch (error) {
747
+ console.error('Auth check failed:', error);
748
+ this.logout();
749
+ }
750
+ }
751
+ },
752
+
753
+ async loadUsers() {
754
+ if (!this.isAdmin) return;
755
+
756
+ try {
757
+ this.loadingUsers = true;
758
+ const response = await fetch('/api/users', {
759
+ headers: {
760
+ 'Authorization': `Bearer ${this.token}`
761
+ }
762
+ });
763
+
764
+ if (!response.ok) {
765
+ throw new Error('Failed to load users');
766
+ }
767
+
768
+ this.users = await response.json();
769
+ } catch (error) {
770
+ console.error('Error loading users:', error);
771
+ this.$buefy.toast.open({
772
+ message: `Error: ${error.message}`,
773
+ type: 'is-danger'
774
+ });
775
+ } finally {
776
+ this.loadingUsers = false;
777
+ }
778
+ },
779
+
780
+ async toggleUserStatus(user) {
781
+ try {
782
+ // Don't allow admins to disable themselves
783
+ if (user.is_admin && user.id === this.currentUser.id && !user.disabled) {
784
+ this.$buefy.toast.open({
785
+ message: 'Admins cannot disable their own accounts',
786
+ type: 'is-warning'
787
+ });
788
+ return;
789
+ }
790
+
791
+ const action = user.disabled ? 'enable' : 'disable';
792
+ const response = await fetch(`/api/users/${user.id}/${action}`, {
793
+ method: 'PUT',
794
+ headers: {
795
+ 'Authorization': `Bearer ${this.token}`,
796
+ 'Content-Type': 'application/json'
797
+ }
798
+ });
799
+
800
+ if (!response.ok) {
801
+ const errorData = await response.json();
802
+ throw new Error(errorData.detail || `Failed to ${action} user`);
803
+ }
804
+
805
+ // Update local user data
806
+ user.disabled = !user.disabled;
807
+
808
+ this.$buefy.toast.open({
809
+ message: `User ${user.username} ${action}d successfully`,
810
+ type: 'is-success'
811
+ });
812
+ } catch (error) {
813
+ console.error(`Error ${user.disabled ? 'enabling' : 'disabling'} user:`, error);
814
+ this.$buefy.toast.open({
815
+ message: `Error: ${error.message}`,
816
+ type: 'is-danger'
817
+ });
818
+ }
819
+ },
820
+
821
+ async deleteUser(user) {
822
+ try {
823
+ // Don't allow admins to delete themselves
824
+ if (user.id === this.currentUser.id) {
825
+ this.$buefy.toast.open({
826
+ message: 'You cannot delete your own account',
827
+ type: 'is-warning'
828
+ });
829
+ return;
830
+ }
831
+
832
+ // Show confirmation dialog
833
+ this.$buefy.dialog.confirm({
834
+ title: 'Delete User',
835
+ message: `Are you sure you want to delete user "${user.username}"? This action cannot be undone.`,
836
+ confirmText: 'Delete',
837
+ type: 'is-danger',
838
+ hasIcon: true,
839
+ onConfirm: async () => {
840
+ const response = await fetch(`/api/users/${user.id}`, {
841
+ method: 'DELETE',
842
+ headers: {
843
+ 'Authorization': `Bearer ${this.token}`
844
+ }
845
+ });
846
+
847
+ if (!response.ok) {
848
+ const errorData = await response.json();
849
+ throw new Error(errorData.detail || 'Failed to delete user');
850
+ }
851
+
852
+ // Remove from local list
853
+ this.users = this.users.filter(u => u.id !== user.id);
854
+
855
+ this.$buefy.toast.open({
856
+ message: `User ${user.username} deleted successfully`,
857
+ type: 'is-success'
858
+ });
859
+ }
860
+ });
861
+ } catch (error) {
862
+ console.error('Error deleting user:', error);
863
+ this.$buefy.toast.open({
864
+ message: `Error: ${error.message}`,
865
+ type: 'is-danger'
866
+ });
867
+ }
868
+ },
869
+
870
+ refreshUsers() {
871
+ this.loadUsers();
872
+ },
873
+
874
+ showResumeUploadModal() {
875
+ this.showUploadModal = true;
876
+ this.showAudioFilesModal = false;
877
+ },
878
+
879
+ closeUploadModal() {
880
+ if (this.uploadInProgress && !this.uploadPaused) {
881
+ this.$buefy.dialog.confirm({
882
+ title: 'Cancel Upload?',
883
+ message: 'Upload is in progress. Are you sure you want to cancel?',
884
+ confirmText: 'Yes, Cancel Upload',
885
+ cancelText: 'No, Continue Upload',
886
+ type: 'is-danger',
887
+ onConfirm: () => {
888
+ this.cancelUpload();
889
+ this.showUploadModal = false;
890
+ }
891
+ });
892
+ } else {
893
+ this.showUploadModal = false;
894
+ }
895
+ },
896
+
897
+ generateUUID() {
898
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
899
+ var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
900
+ return v.toString(16);
901
+ });
902
+ },
903
+
904
+ prepareChunkedUpload() {
905
+ setTimeout(() => {
906
+ if (!this.resumableFile) return;
907
+ this.uploadId = this.generateUUID();
908
+ this.totalChunks = Math.ceil(this.resumableFile.size / this.chunkUploadSize);
909
+ this.currentChunkIndex = 0;
910
+ this.uploadProgress = 0;
911
+ this.uploadPaused = false;
912
+ }, 1500);
913
+ },
914
+
915
+ async startChunkedUpload() {
916
+ if (!this.resumableFile || this.uploadInProgress) return;
917
+
918
+ this.uploadInProgress = true;
919
+ this.uploadPaused = false;
920
+ this.uploadStartTime = Date.now();
921
+ this.lastUploadTime = Date.now();
922
+ this.lastUploadedBytes = 0;
923
+ this.uploadSpeed = null;
924
+ this.estimatedTimeRemaining = null;
925
+
926
+ await this.uploadNextChunk();
927
+ },
928
+
929
+ async uploadNextChunk() {
930
+ if (this.uploadPaused || !this.uploadInProgress) return;
931
+
932
+ if (this.currentChunkIndex >= this.totalChunks) {
933
+ // All chunks uploaded, finalize the upload
934
+ await this.finalizeUpload();
935
+ return;
936
+ }
937
+
938
+ const start = this.currentChunkIndex * this.chunkUploadSize;
939
+ const end = Math.min(start + this.chunkUploadSize, this.resumableFile.size);
940
+ const chunk = this.resumableFile.slice(start, end);
941
+
942
+ const formData = new FormData();
943
+ formData.append('file', chunk, this.resumableFile.name);
944
+ formData.append('upload_id', this.uploadId);
945
+ formData.append('offset', start);
946
+ formData.append('total_size', this.resumableFile.size);
947
+
948
+ try {
949
+ const response = await fetch('/api/upload-audio-chunk', {
950
+ method: 'POST',
951
+ body: formData,
952
+ headers: {
953
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
954
+ }
955
+ });
956
+
957
+ if (!response.ok) {
958
+ throw new Error('Upload failed');
959
+ }
960
+
961
+ this.currentChunkIndex++;
962
+ this.uploadProgress = (this.currentChunkIndex / this.totalChunks) * 100;
963
+
964
+ // Calculate upload speed and estimated time
965
+ const now = Date.now();
966
+ const uploadedBytes = this.currentChunkIndex * this.chunkUploadSize;
967
+ const timeDiff = (now - this.lastUploadTime) / 1000; // in seconds
968
+
969
+ if (timeDiff > 0) {
970
+ const bytesDiff = uploadedBytes - this.lastUploadedBytes;
971
+ this.uploadSpeed = Math.round((bytesDiff / 1024) / timeDiff); // KB/s
972
+
973
+ const remainingBytes = this.resumableFile.size - uploadedBytes;
974
+ if (this.uploadSpeed > 0) {
975
+ const secondsRemaining = Math.ceil(remainingBytes / 1024 / this.uploadSpeed);
976
+ this.estimatedTimeRemaining = this.formatTimeRemaining(secondsRemaining);
977
+ }
978
+
979
+ this.lastUploadTime = now;
980
+ this.lastUploadedBytes = uploadedBytes;
981
+ }
982
+
983
+ // Continue with next chunk
984
+ if (!this.uploadPaused) {
985
+ await this.uploadNextChunk();
986
+ }
987
+ } catch (error) {
988
+ console.error('Error uploading chunk:', error);
989
+ this.$buefy.toast.open({
990
+ message: 'Upload error: ' + error.message,
991
+ type: 'is-danger'
992
+ });
993
+ this.uploadPaused = true;
994
+ }
995
+ },
996
+
997
+ formatTimeRemaining(seconds) {
998
+ if (seconds < 60) {
999
+ return `${seconds} sec`;
1000
+ } else if (seconds < 3600) {
1001
+ const minutes = Math.floor(seconds / 60);
1002
+ const remainingSeconds = seconds % 60;
1003
+ return `${minutes} min ${remainingSeconds} sec`;
1004
+ } else {
1005
+ const hours = Math.floor(seconds / 3600);
1006
+ const minutes = Math.floor((seconds % 3600) / 60);
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
+
1023
+ try {
1024
+ await fetch('/api/cancel-audio-upload', {
1025
+ method: 'POST',
1026
+ headers: {
1027
+ 'Content-Type': 'application/json',
1028
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
1029
+ },
1030
+ body: JSON.stringify({ upload_id: this.uploadId })
1031
+ });
1032
+ } catch (error) {
1033
+ console.error('Error canceling upload:', error);
1034
+ }
1035
+
1036
+ this.uploadInProgress = false;
1037
+ this.uploadPaused = false;
1038
+ this.uploadProgress = 0;
1039
+ this.currentChunkIndex = 0;
1040
+ },
1041
+
1042
+ async finalizeUpload() {
1043
+ try {
1044
+ // Check if uploadId exists
1045
+ if (!this.uploadId) {
1046
+ throw new Error('Upload ID is missing');
1047
+ }
1048
+
1049
+ const response = await fetch('/api/finalize-audio-upload', {
1050
+ method: 'POST',
1051
+ headers: {
1052
+ 'Content-Type': 'application/json',
1053
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
1054
+ },
1055
+ body: JSON.stringify({
1056
+ upload_id: this.uploadId
1057
+ })
1058
+ });
1059
+
1060
+ if (!response.ok) {
1061
+ const errorData = await response.json();
1062
+ throw new Error(errorData.detail || 'Failed to finalize upload');
1063
+ }
1064
+
1065
+ const result = await response.json();
1066
+
1067
+ // Set the audio file and URL
1068
+ // this.audioFile = [{ name: result.filename }];
1069
+ // this.audioUrl = `/uploads/${result.filename}`;
1070
+
1071
+ // Reload the audio files list
1072
+ await this.loadUploadedAudioFiles();
1073
+
1074
+ this.$buefy.toast.open({
1075
+ message: 'Upload completed successfully!',
1076
+ type: 'is-success'
1077
+ });
1078
+
1079
+ this.uploadInProgress = false;
1080
+ this.showUploadModal = false;
1081
+ } catch (error) {
1082
+ console.error('Error finalizing upload:', error);
1083
+ this.$buefy.toast.open({
1084
+ message: 'Error finalizing upload: ' + error.message,
1085
+ type: 'is-danger'
1086
+ });
1087
+ this.uploadPaused = true;
1088
+ }
1089
+ },
1090
+ async loadUploadedAudioFiles() {
1091
+ try {
1092
+ this.loadingAudioFiles = true;
1093
+ const response = await fetch('/api/audio-files', {
1094
+ headers: {
1095
+ 'Authorization': `Bearer ${this.token}`
1096
+ }
1097
+ });
1098
+
1099
+ if (!response.ok) {
1100
+ throw new Error('Failed to load audio files');
1101
+ }
1102
+
1103
+ this.uploadedAudioFiles = await response.json();
1104
+ } catch (error) {
1105
+ console.error('Error loading audio files:', error);
1106
+ this.$buefy.toast.open({
1107
+ message: `Error: ${error.message}`,
1108
+ type: 'is-danger'
1109
+ });
1110
+ } finally {
1111
+ this.loadingAudioFiles = false;
1112
+ }
1113
+ },
1114
+ formatFileSize(bytes) {
1115
+ if (bytes === 0) return '0 Bytes';
1116
+
1117
+ const k = 1024;
1118
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
1119
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
1120
+
1121
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
1122
+ },
1123
+ showAudioFilesList() {
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}`
1141
+ }
1142
+ });
1143
+
1144
+ if (!response.ok) {
1145
+ throw new Error('Failed to delete audio file');
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',
1153
+ type: 'is-success'
1154
+ });
1155
+ }
1156
+ });
1157
+ } catch (error) {
1158
+ console.error('Error deleting audio file:', error);
1159
+ this.$buefy.toast.open({
1160
+ message: `Error: ${error.message}`,
1161
+ type: 'is-danger'
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
1173
+ const fileObj = new File([blob], file.filename, {
1174
+ type: blob.type || 'audio/mpeg'
1175
+ });
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();
1183
+ this.initializeWaveform();
1184
+
1185
+ // Close the audio files modal
1186
+ this.showAudioFilesModal = false;
1187
+
1188
+ this.$buefy.toast.open({
1189
+ message: `Selected audio file: ${file.filename}`,
1190
+ type: 'is-success'
1191
+ });
1192
+ } catch (error) {
1193
+ console.error('Error selecting audio file:', error);
1194
+ this.$buefy.toast.open({
1195
+ message: `Error selecting file: ${error.message}`,
1196
+ type: 'is-danger'
1197
+ });
1198
+ }
1199
+ }
1200
+ }, // methods
1201
+ // Computed properties
1202
+ computed: {
1203
+ filteredUsers() {
1204
+ if (!this.userSearchQuery) {
1205
+ return this.users;
1206
+ }
1207
+
1208
+ const query = this.userSearchQuery.toLowerCase();
1209
+ return this.users.filter(user =>
1210
+ user.username.toLowerCase().includes(query) ||
1211
+ user.email.toLowerCase().includes(query)
1212
+ );
1213
+ },
1214
+ filteredAudioFiles() {
1215
+ if (!this.audioFileSearchQuery) {
1216
+ return this.uploadedAudioFiles;
1217
+ }
1218
+
1219
+ const query = this.audioFileSearchQuery.toLowerCase();
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: {
1233
+ audioUrl() {
1234
+ if (this.audioUrl) {
1235
+ this.initializeAudio();
1236
+ this.initializeWaveform();
1237
+ }
1238
+ },
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
+ },
1250
+ beforeUnmount() {
1251
+ if (this.howl) {
1252
+ this.howl.unload();
1253
+ }
1254
+ this.stopSeekUpdate();
1255
+ }
1256
+ });
1257
+
1258
+ app.use(Buefy.default);
1259
+ app.mount('#app');