bitsnaps commited on
Commit
3c44f93
·
verified ·
1 Parent(s): ebf640e

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +300 -1158
index.html CHANGED
@@ -5,163 +5,15 @@
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>TranStudio</title>
7
  <link rel="stylesheet" href="/static/css/buefy.min.css">
8
- <link rel="stylesheet" href="/static/css/materialdesignicons.min.css">
 
 
 
9
  <script>
10
  window.process = { env: { NODE_ENV: 'production' } };
11
  </script>
12
  <script src="/static/js/vue.global.prod.js"></script>
13
  <script src="/static/js/buefy.min.js"></script>
14
- <style>
15
- .segments-container {
16
- max-height: 300px;
17
- overflow-y: auto;
18
- border: 1px solid #ddd;
19
- padding: 10px;
20
- border-radius: 4px;
21
- }
22
-
23
- .segment-box {
24
- padding: 10px;
25
- margin: 5px 0;
26
- border: 1px solid #eee;
27
- border-radius: 4px;
28
- cursor: pointer;
29
- transition: all 0.2s;
30
- }
31
-
32
- .segment-box:hover {
33
- background-color: #f5f5f5;
34
- }
35
-
36
- .segment-box.is-active {
37
- border-color: #3273dc;
38
- background-color: #e8f0fe;
39
- }
40
-
41
- .segment-time {
42
- font-size: 0.8em;
43
- color: #666;
44
- margin-bottom: 5px;
45
- }
46
-
47
- .segment-text {
48
- margin: 5px 0;
49
- }
50
-
51
- .segment-actions {
52
- text-align: right;
53
- }
54
- .audio-player {
55
- position: fixed;
56
- bottom: 0;
57
- left: 0;
58
- right: 0;
59
- background: white;
60
- padding: 1rem;
61
- box-shadow: 0 -2px 5px rgba(0,0,0,0.1);
62
- z-index: 100;
63
- margin-top: 2rem;
64
- }
65
-
66
- .waveform-container {
67
- margin: 1rem 0;
68
- padding: 0 1rem;
69
- }
70
-
71
- .waveform {
72
- position: relative;
73
- height: 10vh;
74
- background: #f0f0f0;
75
- border-radius: 4px;
76
- overflow: hidden;
77
- }
78
-
79
- .progress {
80
- position: absolute;
81
- top: 0;
82
- left: 0;
83
- height: 100%;
84
- background: rgba(50, 115, 220, 0.3);
85
- pointer-events: none;
86
- }
87
-
88
- .player-controls {
89
- display: flex;
90
- align-items: center;
91
- gap: 0.5rem;
92
- margin-bottom: 0.5rem;
93
- }
94
-
95
- .segment-marker {
96
- position: absolute;
97
- height: 100%;
98
- background: rgba(50, 115, 220, 0.2);
99
- cursor: pointer;
100
- transition: background 0.2s;
101
- border-left: 1px solid #3273dc;
102
- border-right: 1px solid #3273dc;
103
- }
104
-
105
- .segment-marker:hover {
106
- background: rgba(50, 115, 220, 0.4);
107
- }
108
-
109
- .segment-tooltip {
110
- position: absolute;
111
- bottom: 100%;
112
- background: #333;
113
- color: white;
114
- padding: 0.25rem 0.5rem;
115
- border-radius: 3px;
116
- font-size: 0.8rem;
117
- white-space: nowrap;
118
- display: none;
119
- }
120
-
121
- .segment-marker:hover .segment-tooltip {
122
- display: block;
123
- }
124
-
125
- .current-time {
126
- text-align: center;
127
- font-size: 0.9rem;
128
- color: #666;
129
- margin-top: 0.5rem;
130
- }
131
- .control-btn {
132
- /* padding: 0.5rem; */
133
- transition: all 0.2s;
134
- }
135
-
136
- .control-btn:hover {
137
- transform: scale(1.1);
138
- }
139
- .selection-info {
140
- margin: 10px 0;
141
- display: flex;
142
- align-items: center;
143
- }
144
-
145
- .slider-tick {
146
- width: 2px;
147
- height: 12px;
148
- background-color: currentColor;
149
- border-radius: 1px;
150
- }
151
-
152
- /* Customize the range slider track */
153
- .b-slider.is-info .b-slider-track {
154
- background: rgba(50, 115, 220, 0.3);
155
- }
156
-
157
- /* Style for the selected range */
158
- .b-slider.is-info .b-slider-fill {
159
- background: #3273dc;
160
- }
161
- .navbar-end {
162
- margin-top: 10vh;
163
- }
164
- </style>
165
  </head>
166
  <body>
167
  <div id="app" class="container">
@@ -181,6 +33,14 @@
181
  <b-icon icon="shield-account"></b-icon>
182
  <span class="ml-1">Admin</span>
183
  </b-navbar-item>
 
 
 
 
 
 
 
 
184
  <b-navbar-item @click="logout">Logout {{ username }}</b-navbar-item>
185
  </template>
186
  <template v-else>
@@ -258,7 +118,7 @@
258
  </b-field>
259
 
260
  <b-field label="Overlap (in seconds)">
261
- <b-slider v-model="overlap" :min="2" :max="60" :step="1"></b-slider>
262
  <span class="tag is-primary">{{ overlap }}</span>
263
  </b-field>
264
 
@@ -280,7 +140,7 @@
280
  </div>
281
 
282
  <b-field label="System Prompt (Optional)">
283
- <b-input v-model="systemPrompt" maxlength="256" type="textarea"></b-input>
284
  </b-field>
285
 
286
  <b-upload v-model="audioFile" accept="audio/*" @change="handleAudioUpload" multiple>
@@ -374,7 +234,7 @@
374
  {{ formatTime(segment.start) }} - {{ formatTime(segment.end) }}
375
  </div>
376
  </div>
377
- <div class="progress" :style="{ width: `${(currentTime / totalDuration) * 100}%` }"></div>
378
  </div>
379
  </div><!-- .waveform-container -->
380
 
@@ -385,1025 +245,307 @@
385
  </div><!-- .audio-player -->
386
  </div>
387
 
388
-
389
- <!-- Login Modal -->
390
- <b-modal v-model="showLoginModal" has-modal-card trap-focus>
391
- <div class="modal-card">
392
- <header class="modal-card-head">
393
- <p class="modal-card-title">Login</p>
394
- </header>
395
- <section class="modal-card-body">
396
- <b-field label="Username">
397
- <b-input type="text" v-model="loginForm.username" placeholder="Enter your username"></b-input>
398
- </b-field>
399
- <b-field label="Password">
400
- <b-input type="password" v-model="loginForm.password" password-reveal></b-input>
401
- </b-field>
402
- </section>
403
- <footer class="modal-card-foot">
404
- <b-button type="is-primary" @click="handleLogin" :loading="isLoading">Login</b-button>
405
- <b-button @click="showLoginModal = false">Cancel</b-button>
406
- </footer>
407
- </div>
408
- </b-modal>
409
-
410
- <!-- Signup Modal -->
411
- <b-modal v-model="showSignupModal" has-modal-card trap-focus>
412
- <div class="modal-card">
413
- <header class="modal-card-head">
414
- <p class="modal-card-title">Sign Up</p>
415
- </header>
416
- <section class="modal-card-body">
417
- <b-field label="Username">
418
- <b-input v-model="signupForm.username" placeholder="Enter username"></b-input>
419
- </b-field>
420
- <b-field label="Email">
421
- <b-input type="email" v-model="signupForm.email" placeholder="Enter your email"></b-input>
422
- </b-field>
423
- <b-field label="Password">
424
- <b-input type="password" v-model="signupForm.password" password-reveal></b-input>
425
- </b-field>
426
- <b-field label="Confirm Password">
427
- <b-input type="password" v-model="signupForm.confirmPassword" password-reveal></b-input>
428
- </b-field>
429
- </section>
430
- <footer class="modal-card-foot">
431
- <b-button type="is-primary" @click="handleSignup" :loading="isLoading">Sign Up</b-button>
432
- <b-button @click="showSignupModal = false">Cancel</b-button>
433
- </footer>
434
- </div>
435
- </b-modal>
436
-
437
- <!-- Admin Panel Modal -->
438
- <b-modal v-model="showAdminPanel" has-modal-card trap-focus :width="640">
439
- <div class="modal-card">
440
- <header class="modal-card-head">
441
- <p class="modal-card-title">User Management</p>
442
- </header>
443
- <section class="modal-card-body">
444
- <div class="block">
445
- <b-field grouped>
446
- <b-input placeholder="Search users..." v-model="userSearchQuery" expanded></b-input>
447
- <b-button type="is-primary" icon-left="magnify">Search</b-button>
448
- </b-field>
449
- </div>
450
-
451
- <b-table
452
- :data="filteredUsers"
453
- :loading="loadingUsers"
454
- :paginated="true"
455
- :per-page="5"
456
- :mobile-cards="true">
457
-
458
- <b-table-column field="id" label="ID" width="40" numeric v-slot="props">
459
- {{ props.row.id }}
460
- </b-table-column>
461
-
462
- <b-table-column field="username" label="Username" v-slot="props">
463
- {{ props.row.username }}
464
- </b-table-column>
465
-
466
- <b-table-column field="email" label="Email" v-slot="props">
467
- {{ props.row.email }}
468
- </b-table-column>
469
-
470
- <b-table-column field="is_admin" label="Admin" v-slot="props">
471
- <b-icon
472
- :icon="props.row.is_admin ? 'check' : 'close'"
473
- :type="props.row.is_admin ? 'is-success' : 'is-danger'">
474
- </b-icon>
475
- </b-table-column>
476
-
477
- <b-table-column field="disabled" label="Status" v-slot="props">
478
- <b-tag :type="props.row.disabled ? 'is-danger' : 'is-success'">
479
- {{ props.row.disabled ? 'Disabled' : 'Active' }}
480
- </b-tag>
481
- </b-table-column>
482
-
483
- <b-table-column label="Actions" v-slot="props">
484
- <div class="buttons">
485
- <b-button
486
- size="is-small"
487
- :type="props.row.disabled ? 'is-success' : 'is-warning'"
488
- :icon-left="props.row.disabled ? 'account-check' : 'account-cancel'"
489
- @click="toggleUserStatus(props.row)"
490
- :disabled="props.row.is_admin && props.row.id === currentUser.id">
491
- {{ props.row.disabled ? 'Enable' : 'Disable' }}
492
- </b-button>
493
- <b-button
494
- size="is-small"
495
- type="is-danger"
496
- icon-left="delete"
497
- @click="deleteUser(props.row)"
498
- :disabled="props.row.id === currentUser.id">
499
- Delete
500
- </b-button>
501
- </div>
502
- </b-table-column>
503
-
504
- <template #empty>
505
- <div class="has-text-centered">No users found</div>
506
- </template>
507
- </b-table>
508
- </section>
509
- <footer class="modal-card-foot">
510
- <b-button @click="refreshUsers" type="is-info" icon-left="refresh">Refresh</b-button>
511
- <b-button @click="showAdminPanel = false">Close</b-button>
512
- </footer>
513
- </div>
514
- </b-modal>
515
-
516
- </div><!-- #app -->
517
-
518
- <script src="/static/js/howler.min.js"></script>
519
- <script>
520
- const { createApp, ref, onMounted } = Vue;
521
- const app = createApp({
522
- data() {
523
- return {
524
- responseFormat: 'verbose_json',
525
- temperature: 0,
526
- chunkSize: 10,
527
- overlap: 5,
528
- selection: [1,100],
529
- systemPrompt: '',
530
- isAuthenticated: false,
531
- username: '',
532
- token: '',
533
- isLoading: false,
534
- audioFile: [],
535
- segments: [],
536
- transcriptionText: '',
537
- selectedLanguage: 'auto',
538
- transcriptions: [],
539
- showSidebar: true,
540
- showPlayer: false,
541
- showApiKeyModal: false,
542
- audioUrl: null,
543
- isPlaying: false,
544
- audioPlayer: null,
545
- isProcessing: false,
546
- howl: null,
547
- activeSegment: null,
548
- isPlayingSegment: false,
549
- isMuted: false,
550
- volume: 0.5,
551
- currentTime: 0,
552
- totalDuration: 0,
553
- seekInterval: null,
554
- waveformData: null,
555
- waveformCanvas: null,
556
- ctx: null,
557
- showLoginModal: false,
558
- loginForm: {
559
- username: '',
560
- password: '',
561
- },
562
- showSignupModal: false,
563
- signupForm: {
564
- username: '',
565
- email: '',
566
- password: '',
567
- confirmPassword: ''
568
- },
569
- // Admin panel data
570
- showAdminPanel: false,
571
- isAdmin: false,
572
- currentUser: null,
573
- users: [],
574
- loadingUsers: false,
575
- userSearchQuery: '',
576
- // processingProgress: 0,
577
- // totalChunks: 0,
578
- // showVolumeSlider: false
579
- }
580
- },
581
-
582
- methods: {
583
- async login() {
584
- this.isAuthenticated = true;
585
- },
586
-
587
- logout() {
588
- this.token = '';
589
- this.username = '';
590
- this.isAuthenticated = false;
591
- this.isAdmin = false;
592
- this.currentUser = null;
593
- this.users = [];
594
- this.transcriptions = [];
595
- localStorage.removeItem('token');
596
-
597
- // Reset UI state
598
- this.audioUrl = null;
599
- this.audioFile = [];
600
- this.segments = [];
601
- this.transcriptionText = '';
602
-
603
- // Show notification
604
- this.$buefy.toast.open({
605
- message: 'Successfully logged out!',
606
- type: 'is-success'
607
- });
608
-
609
- // Redirect to home page if not already there
610
- if (window.location.pathname !== '/') {
611
- window.location.href = '/';
612
- } else {
613
- // window.location.reload();
614
- }
615
- },
616
-
617
- async handleAudioUpload(event) {
618
- if (!event.target.files || !event.target.files.length) return;
619
-
620
- const file = event.target.files[0];
621
- const fileUrl = URL.createObjectURL(file);
622
- fileExt = file.name.split('.').splice(-1);
623
-
624
- this.howl = new Howl({
625
- src: [fileUrl],
626
- format: this.fileExt, // This should be an array
627
- html5: true,
628
- onend: () => {
629
- this.isPlayingSegment = false;
630
- this.activeSegment = null;
631
- }
632
- });
633
-
634
- this.audioUrl = fileUrl;
635
- this.audioFile = [file];
636
- },
637
-
638
- initializeAudio() {
639
- if (this.howl) {
640
- this.howl.unload();
641
- }
642
-
643
- const fileExt = this.audioFile[0].name.split('.').splice(-1);
644
- this.howl = new Howl({
645
- src: [this.audioUrl],
646
- format: fileExt,
647
- html5: true,
648
- volume: this.volume,
649
- sprite: this.createSprites(),
650
- onplay: () => {
651
- this.isPlaying = true;
652
- this.startSeekUpdate();
653
- },
654
- onpause: () => {
655
- this.isPlaying = false;
656
- this.stopSeekUpdate();
657
- },
658
- onstop: () => {
659
- this.isPlaying = false;
660
- this.currentTime = 0;
661
- this.stopSeekUpdate();
662
- },
663
- onend: () => {
664
- this.isPlaying = false;
665
- this.currentTime = 0;
666
- this.stopSeekUpdate();
667
- },
668
- onload: () => {
669
- this.totalDuration = this.howl.duration();
670
- /*/ Set selection to the length of the audio file
671
- this.selection = [];
672
- for (let i = 0; i < this.totalDuration; i += this.chunkSize) {
673
- this.selection.push(i);
674
- }*/
675
- }
676
- });
677
- },
678
-
679
- async initializeWaveform() {
680
- if (!this.audioUrl) return;
681
 
682
- // Load audio file and decode it
683
- const response = await fetch(this.audioUrl);
684
- const arrayBuffer = await response.arrayBuffer();
685
- const audioContext = new AudioContext();
686
- const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
687
-
688
- // Get waveform data
689
- const channelData = audioBuffer.getChannelData(0);
690
- this.waveformData = this.processWaveformData(channelData);
 
 
 
 
691
 
692
- // Draw waveform
693
- this.$nextTick(() => {
694
- this.drawWaveform();
695
- });
696
- },
697
-
698
- processWaveformData(data) {
699
- const step = Math.ceil(data.length / 1000);
700
- const waveform = [];
701
- for (let i = 0; i < data.length; i += step) {
702
- const slice = data.slice(i, i + step);
703
- // Use reducer instead of spread operator for large arrays
704
- const max = slice.reduce((a, b) => Math.max(a, b), -Infinity);
705
- const min = slice.reduce((a, b) => Math.min(a, b), Infinity);
706
- waveform.push({ max, min });
707
- }
708
- return waveform;
709
- },
710
-
711
- drawWaveform() {
712
- if (this.$refs.waveform){
713
- const canvas = document.createElement('canvas');
714
- const ctx = canvas.getContext('2d');
715
- const width = this.$refs.waveform.offsetWidth;
716
- const height = 100;
717
 
718
- canvas.width = width;
719
- canvas.height = height;
720
- this.$refs.waveform.style.backgroundImage = `url(${canvas.toDataURL()})`;
721
 
722
- ctx.fillStyle = '#3273dc';
723
- this.waveformData.forEach((point, i) => {
724
- const x = (i / this.waveformData.length) * width;
725
- const y = (1 - point.max) * height / 2;
726
- const h = (point.max - point.min) * height / 2;
727
- ctx.fillRect(x, y, 1, h);
728
- });
729
- }
730
- },
731
-
732
- createSprites() {
733
- const sprites = {};
734
- this.segments.forEach((segment, index) => {
735
- sprites[`segment_${index}`] = [segment.start * 1000, (segment.end - segment.start) * 1000];
736
- });
737
- return sprites;
738
- },
739
-
740
- togglePlay() {
741
- if (this.isPlaying) {
742
- this.howl.pause();
743
- } else {
744
- this.howl.play();
745
- }
746
- },
747
-
748
- stopAudio() {
749
- this.howl.stop();
750
- },
751
-
752
- updateVolume() {
753
- if (this.volume <= 0) {
754
- this.isMuted = true;
755
- this.howl.mute(true);
756
- } else if (this.isMuted && this.volume > 0) {
757
- this.isMuted = false;
758
- this.howl.mute(false);
759
- }
760
- this.howl.volume(this.volume);
761
- },
762
-
763
- toggleVolumeSlider() {
764
- this.showVolumeSlider = !this.showVolumeSlider;
765
- },
766
-
767
- toggleMute() {
768
- this.isMuted = !this.isMuted;
769
- this.howl.mute(this.isMuted);
770
- if (!this.isMuted && this.volume === 0) {
771
- this.volume = 0.5;
772
- }
773
- },
774
-
775
- playSegment(segment) {
776
- const index = this.segments.indexOf(segment);
777
- if (index !== -1) {
778
- this.howl.play(`segment_${index}`);
779
- }
780
- },
781
-
782
- startSeekUpdate() {
783
- this.seekInterval = setInterval(() => {
784
- this.currentTime = this.howl.seek() || 0;
785
- }, 100);
786
- },
787
-
788
- stopSeekUpdate() {
789
- clearInterval(this.seekInterval);
790
- },
791
-
792
- formatTime(seconds) {
793
- const mins = Math.floor(seconds / 60);
794
- const secs = Math.floor(seconds % 60);
795
- return `${mins}:${secs < 10 ? '0' : ''}${secs}`;
796
- },
797
-
798
- togglePlayPause() {
799
- if (this.audioPlayer.paused) {
800
- this.audioPlayer.play();
801
- this.isPlaying = true;
802
- } else {
803
- this.audioPlayer.pause();
804
- this.isPlaying = false;
805
- }
806
- },
807
-
808
- playSegment(segment) {
809
- if (!this.howl) return;
810
-
811
- if (this.activeSegment === segment.id && this.isPlayingSegment) {
812
- this.howl.pause();
813
- this.isPlayingSegment = false;
814
- return;
815
- }
816
-
817
- this.howl.seek(segment.start);
818
- this.howl.play();
819
- this.activeSegment = segment.id;
820
- this.isPlayingSegment = true;
821
-
822
- this.howl.once('end', () => {
823
- this.isPlayingSegment = false;
824
- this.activeSegment = null;
825
- });
826
- },
827
-
828
- copySegmentText(text) {
829
- navigator.clipboard.writeText(text).then(() => {
830
- this.$buefy.toast.open({
831
- message: 'Text copied to clipboard',
832
- type: 'is-success',
833
- position: 'is-bottom-right'
834
- });
835
- }).catch(() => {
836
- this.$buefy.toast.open({
837
- message: 'Failed to copy text',
838
- type: 'is-danger',
839
- position: 'is-bottom-right'
840
- });
841
- });
842
- },
843
-
844
- deleteSegment(index) {
845
- this.segments.splice(index, 1);
846
- this.updateTranscriptionText();
847
- },
848
-
849
- updateTranscriptionText() {
850
- this.transcriptionText = this.segments
851
- .map(segment => segment.text)
852
- .join(' ');
853
- },
854
-
855
- formatTime(seconds) {
856
- const mins = Math.floor(seconds / 60);
857
- const secs = Math.floor(seconds % 60);
858
- return `${mins}:${secs < 10 ? '0' : ''}${secs}`;
859
- },
860
-
861
- async processTranscription() {
862
- const formData = new FormData();
863
- formData.append('file', this.audioFile[0]);
864
- formData.append('response_format', this.responseFormat);
865
- formData.append('temperature', this.temperature.toString());
866
- formData.append('chunk_size', parseInt(this.chunkSize * 60).toString());
867
- formData.append('overlap', this.overlap.toString());
868
- formData.append('prompt', this.systemPrompt.toString());
869
-
870
- // Add selection timestamps
871
- const startTime = (this.selection[0] * this.totalDuration / 100).toFixed(2);
872
- const endTime = (this.selection[1] * this.totalDuration / 100).toFixed(2);
873
- formData.append('start_time', startTime);
874
- formData.append('end_time', endTime);
875
-
876
- if (this.selectedLanguage !== 'auto') {
877
- formData.append('language', this.selectedLanguage);
878
- }
879
- try {
880
- this.isProcessing = true;
881
-
882
- /*/ Monitor the progress of the upload for user feedback (need to be updated for reading back response)
883
- const xhr = new XMLHttpRequest();
884
- xhr.upload.onloadstart = function (event) {
885
- console.log('Upload started');
886
- };
887
-
888
- xhr.upload.onprogress = function (event) {
889
- if (event.lengthComputable) {
890
- const percentComplete = (event.loaded / event.total) * 100;
891
- console.log(`Upload progress: ${percentComplete.toFixed(2)}%`);
892
- }
893
- };
894
- xhr.upload.onload = function () {
895
- console.log('Upload complete');
896
- };
897
- xhr.onerror = function () {
898
- console.error('Error uploading file');
899
- };
900
-
901
- xhr.open('POST', '/api/upload', true);
902
- xhr.send(formData);*/
903
-
904
- const response = await fetch('/api/upload', {
905
- method: 'POST',
906
- headers: {
907
- 'Authorization': `Bearer ${this.token}`
908
- },
909
- body: formData
910
- });
911
- if (!response.ok) {
912
- const errorData = await response.json();
913
- throw new Error(errorData.detail || 'Transcription failed');
914
- }
915
- const result = await response.json();
916
 
917
- // Check for backend validation errors
918
- if (result.metadata?.errors?.length) {
919
- console.error('Chunk processing errors:', result.metadata.errors);
920
- this.$buefy.snackbar.open({
921
- message: `${result.metadata.errors.length} chunks failed to process`,
922
- type: 'is-warning',
923
- position: 'is-bottom-right'
924
- });
925
- }
926
 
927
- if (this.responseFormat === 'verbose_json') {
928
-
929
- // Validate segments
930
- if (!result.segments) {
931
- throw new Error('Server returned invalid segments format');
932
- }
933
-
934
- this.segments = result.segments;
935
- this.updateTranscriptionText();
936
- } else {
937
- this.transcriptionText = result.text || result;
938
- }
939
-
940
- // this.totalChunks = result.metadata?.total_chunks || 0;
941
- // this.processingProgress = ((processedChunks / this.totalChunks) * 100).toFixed(1);
942
- } catch (error) {
943
- console.error('Transcription error:', error);
944
- this.$buefy.toast.open({
945
- message: `Error: ${error.message}`,
946
- type: 'is-danger',
947
- position: 'is-bottom-right'
948
- });
949
- }
950
- this.isProcessing = false;
951
- this.showPlayer = true;
952
- },
953
-
954
- async loadTranscriptions() {
955
- try {
956
- // Check if token exists
957
- if (!this.token) {
958
- this.token = localStorage.getItem('token');
959
- if (!this.token) {
960
- // console.log('No authentication token found');
961
- return;
962
- }
963
- }
964
-
965
- const response = await fetch('/api/transcriptions', {
966
- headers: {
967
- 'Authorization': `Bearer ${this.token}`
968
- }
969
- });
970
 
971
- if (response.status === 401) {
972
- // Token expired or invalid
973
- console.log('Authentication token expired or invalid');
974
- this.logout();
975
- this.$buefy.toast.open({
976
- message: 'Your session has expired. Please login again.',
977
- type: 'is-warning'
978
- });
979
- return;
980
- }
981
-
982
- if (!response.ok) {
983
- throw new Error(`HTTP error! Status: ${response.status}`);
984
- }
985
-
986
- this.transcriptions = await response.json();
987
- } catch (error) {
988
- console.error('Error loading transcriptions:', error);
989
- this.$buefy.toast.open({
990
- message: `Failed to load transcriptions: ${error.message}`,
991
- type: 'is-danger'
992
- });
993
- }
994
- },
995
-
996
- async loadTranscription(transcription) {
997
- try {
998
- const response = await fetch(`/api/transcriptions/${transcription.id}`, {
999
- headers: {
1000
- 'Authorization': `Bearer ${this.token}`
1001
- }
1002
- });
1003
- const data = await response.json();
1004
- // Set the audio file for playback
1005
- this.transcriptionText = data['text'];
1006
- this.segments = data['segments'];
1007
- // this.audioFile = data['audio_file'];
1008
- if (data.audio_file) {
1009
- // this.audioUrl = `/uploads/${data['audio_file']}`;
1010
- // this.audioFile.push(data['audio_file']);
1011
- // this.initializeAudio();
1012
- }
1013
- } catch (error) {
1014
- console.error('Error loading transcription:', error);
1015
- }
1016
- },
1017
-
1018
- async saveTranscription() {
1019
- if (this.audioFile.length == 0){
1020
- this.$buefy.toast.open({
1021
- message: 'Please upload an audio file first',
1022
- type: 'is-warning',
1023
- position: 'is-bottom-right'
1024
- });
1025
- return;
1026
- }
1027
- try {
1028
- this.isProcessing = true;
1029
- const response = await fetch('/api/save-transcription', {
1030
- method: 'POST',
1031
- headers: {
1032
- 'Content-Type': 'application/json',
1033
- 'Authorization': `Bearer ${this.token}`
1034
- },
1035
- body: JSON.stringify({
1036
- text: this.transcriptionText,
1037
- segments: this.segments,
1038
- audio_file: this.audioFile[0].name
1039
- })
1040
- });
1041
 
1042
- if (!response.ok) throw new Error('Failed to save transcription');
1043
-
1044
- await this.loadTranscriptions();
1045
- this.$buefy.toast.open({
1046
- message: 'Transcription saved successfully',
1047
- type: 'is-success',
1048
- position: 'is-bottom-right'
1049
- });
1050
- } catch (error) {
1051
- console.error('Error saving transcription:', error);
1052
- this.$buefy.toast.open({
1053
- message: `Error: ${error.message}`,
1054
- type: 'is-danger',
1055
- position: 'is-bottom-right'
1056
- });
1057
- }
1058
- this.isProcessing = false;
1059
- },
1060
-
1061
- toggleSidebar() {
1062
- this.showSidebar = !this.showSidebar;
1063
- },
1064
- togglePlayer() {
1065
- this.showPlayer = !this.showPlayer;
1066
- },
1067
- async deleteTranscription(id) {
1068
- try {
1069
- // Show confirmation dialog
1070
- this.$buefy.dialog.confirm({
1071
- title: 'Delete Transcription',
1072
- message: 'Are you sure you want to delete this transcription? This action cannot be undone.',
1073
- confirmText: 'Delete',
1074
- type: 'is-danger',
1075
- hasIcon: true,
1076
- onConfirm: async () => {
1077
- const response = await fetch(`/api/transcriptions/${id}`, {
1078
- method: 'DELETE',
1079
- headers: {
1080
- 'Authorization': `Bearer ${this.token}`
1081
- }
1082
- });
1083
-
1084
- if (!response.ok) throw new Error('Failed to delete transcription');
1085
-
1086
- // Remove from local list
1087
- this.transcriptions = this.transcriptions.filter(t => t.id !== id);
1088
-
1089
- // Show success message
1090
- this.$buefy.toast.open({
1091
- message: 'Transcription deleted successfully',
1092
- type: 'is-success',
1093
- position: 'is-bottom-right'
1094
- });
1095
- }
1096
- });
1097
- } catch (error) {
1098
- console.error('Error deleting transcription:', error);
1099
- this.$buefy.toast.open({
1100
- message: `Error: ${error.message}`,
1101
- type: 'is-danger',
1102
- position: 'is-bottom-right'
1103
- });
1104
- }
1105
- },
1106
-
1107
- async handleLogin() {
1108
- try {
1109
- this.isLoading = true;
1110
- const formData = new FormData();
1111
- formData.append('username', this.loginForm.username); // Make sure this matches the backend expectation
1112
- formData.append('password', this.loginForm.password);
1113
-
1114
- const response = await fetch('/token', {
1115
- method: 'POST',
1116
- body: formData
1117
- });
1118
-
1119
- if (!response.ok) {
1120
- const errorData = await response.json();
1121
- throw new Error(errorData.detail || 'Login failed');
1122
- }
1123
-
1124
- const data = await response.json();
1125
- this.token = data.access_token;
1126
- localStorage.setItem('token', data.access_token);
1127
-
1128
- // Get user info
1129
- const userResponse = await fetch('/api/me', {
1130
- headers: {
1131
- 'Authorization': `Bearer ${this.token}`
1132
- }
1133
- });
1134
-
1135
- if (!userResponse.ok) {
1136
- throw new Error('Failed to get user information');
1137
- }
1138
-
1139
- const userData = await userResponse.json();
1140
- this.username = userData.username;
1141
- this.isAuthenticated = true;
1142
-
1143
- this.showLoginModal = false;
1144
- this.$buefy.toast.open({
1145
- message: 'Successfully logged in!',
1146
- type: 'is-success'
1147
- });
1148
-
1149
- // Load user data after successful login
1150
- this.loadTranscriptions();
1151
- } catch (error) {
1152
- this.$buefy.toast.open({
1153
- message: `Error: ${error.message}`,
1154
- type: 'is-danger'
1155
- });
1156
- } finally {
1157
- this.isLoading = false;
1158
- }
1159
- },
1160
-
1161
- async handleSignup() {
1162
- try {
1163
- this.isLoading = true;
1164
- if (this.signupForm.password !== this.signupForm.confirmPassword) {
1165
- throw new Error('Passwords do not match');
1166
- }
1167
-
1168
- const response = await fetch('/api/signup', {
1169
- method: 'POST',
1170
- headers: {
1171
- 'Content-Type': 'application/json'
1172
- },
1173
- body: JSON.stringify({
1174
- username: this.signupForm.username,
1175
- email: this.signupForm.email,
1176
- password: this.signupForm.password
1177
- })
1178
- });
1179
-
1180
- if (!response.ok) {
1181
- const error = await response.json();
1182
- throw new Error(error.detail || 'Signup failed');
1183
- }
1184
-
1185
- this.showSignupModal = false;
1186
- this.$buefy.toast.open({
1187
- message: 'Account created successfully! Please login.',
1188
- type: 'is-success'
1189
- });
1190
-
1191
- // Clear form
1192
- this.signupForm = {
1193
- username: '',
1194
- email: '',
1195
- password: '',
1196
- confirmPassword: ''
1197
- };
1198
 
1199
- // Show login modal
1200
- this.showLoginModal = true;
1201
- } catch (error) {
1202
- this.$buefy.toast.open({
1203
- message: `Error: ${error.message}`,
1204
- type: 'is-danger'
1205
- });
1206
- } finally {
1207
- this.isLoading = false;
1208
- }
1209
- },
1210
-
1211
- async checkAuth() {
1212
- const token = localStorage.getItem('token');
1213
- if (token) {
1214
- try {
1215
- this.token = token;
1216
- const response = await fetch('/api/me', {
1217
- headers: {
1218
- 'Authorization': `Bearer ${token}`
1219
- }
1220
- });
1221
-
1222
- if (response.ok) {
1223
- const userData = await response.json();
1224
- this.username = userData.username;
1225
- this.isAuthenticated = true;
1226
- this.isAdmin = userData.is_admin;
1227
- this.currentUser = userData;
1228
-
1229
- // If user is admin, load users list
1230
- if (this.isAdmin) {
1231
- this.loadUsers();
1232
- }
1233
- } else {
1234
- // Token invalid or expired
1235
- this.logout();
1236
- }
1237
- } catch (error) {
1238
- console.error('Auth check failed:', error);
1239
- this.logout();
1240
- }
1241
- }
1242
- },
1243
-
1244
- // Add these methods to the methods section
1245
- async loadUsers() {
1246
- if (!this.isAdmin) return;
1247
 
1248
- try {
1249
- this.loadingUsers = true;
1250
- const response = await fetch('/api/users', {
1251
- headers: {
1252
- 'Authorization': `Bearer ${this.token}`
1253
- }
1254
- });
1255
-
1256
- if (!response.ok) {
1257
- throw new Error('Failed to load users');
1258
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1259
 
1260
- this.users = await response.json();
1261
- } catch (error) {
1262
- console.error('Error loading users:', error);
1263
- this.$buefy.toast.open({
1264
- message: `Error: ${error.message}`,
1265
- type: 'is-danger'
1266
- });
1267
- } finally {
1268
- this.loadingUsers = false;
1269
- }
1270
- },
1271
-
1272
- async toggleUserStatus(user) {
1273
- try {
1274
- // Don't allow admins to disable themselves
1275
- if (user.is_admin && user.id === this.currentUser.id && !user.disabled) {
1276
- this.$buefy.toast.open({
1277
- message: 'Admins cannot disable their own accounts',
1278
- type: 'is-warning'
1279
- });
1280
- return;
1281
- }
1282
 
1283
- const action = user.disabled ? 'enable' : 'disable';
1284
- const response = await fetch(`/api/users/${user.id}/${action}`, {
1285
- method: 'PUT',
1286
- headers: {
1287
- 'Authorization': `Bearer ${this.token}`,
1288
- 'Content-Type': 'application/json'
1289
- }
1290
- });
1291
 
1292
- if (!response.ok) {
1293
- const errorData = await response.json();
1294
- throw new Error(errorData.detail || `Failed to ${action} user`);
1295
- }
1296
 
1297
- // Update local user data
1298
- user.disabled = !user.disabled;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1299
 
1300
- this.$buefy.toast.open({
1301
- message: `User ${user.username} ${action}d successfully`,
1302
- type: 'is-success'
1303
- });
1304
- } catch (error) {
1305
- console.error(`Error ${user.disabled ? 'enabling' : 'disabling'} user:`, error);
1306
- this.$buefy.toast.open({
1307
- message: `Error: ${error.message}`,
1308
- type: 'is-danger'
1309
- });
1310
- }
1311
- },
1312
-
1313
- async deleteUser(user) {
1314
- try {
1315
- // Don't allow admins to delete themselves
1316
- if (user.id === this.currentUser.id) {
1317
- this.$buefy.toast.open({
1318
- message: 'You cannot delete your own account',
1319
- type: 'is-warning'
1320
- });
1321
- return;
1322
- }
1323
 
1324
- // Show confirmation dialog
1325
- this.$buefy.dialog.confirm({
1326
- title: 'Delete User',
1327
- message: `Are you sure you want to delete user "${user.username}"? This action cannot be undone.`,
1328
- confirmText: 'Delete',
1329
- type: 'is-danger',
1330
- hasIcon: true,
1331
- onConfirm: async () => {
1332
- const response = await fetch(`/api/users/${user.id}`, {
1333
- method: 'DELETE',
1334
- headers: {
1335
- 'Authorization': `Bearer ${this.token}`
1336
- }
1337
- });
1338
-
1339
- if (!response.ok) {
1340
- const errorData = await response.json();
1341
- throw new Error(errorData.detail || 'Failed to delete user');
1342
- }
1343
-
1344
- // Remove from local list
1345
- this.users = this.users.filter(u => u.id !== user.id);
1346
-
1347
- this.$buefy.toast.open({
1348
- message: `User ${user.username} deleted successfully`,
1349
- type: 'is-success'
1350
- });
1351
- }
1352
- });
1353
- } catch (error) {
1354
- console.error('Error deleting user:', error);
1355
- this.$buefy.toast.open({
1356
- message: `Error: ${error.message}`,
1357
- type: 'is-danger'
1358
- });
1359
- }
1360
- },
1361
-
1362
- refreshUsers() {
1363
- this.loadUsers();
1364
- },
1365
- }, // methods
1366
- // Add this to the Vue app definition, after methods
1367
- computed: {
1368
- filteredUsers() {
1369
- if (!this.userSearchQuery) {
1370
- return this.users;
1371
- }
1372
-
1373
- const query = this.userSearchQuery.toLowerCase();
1374
- return this.users.filter(user =>
1375
- user.username.toLowerCase().includes(query) ||
1376
- user.email.toLowerCase().includes(query)
1377
- );
1378
- }
1379
- },
1380
- mounted() {
1381
- this.checkAuth();
1382
- this.loadTranscriptions();
1383
- },
1384
- watch: {
1385
- audioUrl() {
1386
- if (this.audioUrl) {
1387
- this.initializeAudio();
1388
- this.initializeWaveform();
1389
- }
1390
- },
1391
- showAdminPanel(newVal) {
1392
- if (newVal && this.isAdmin) {
1393
- this.loadUsers();
1394
- }
1395
- }
1396
- },
1397
- beforeUnmount() {
1398
- if (this.howl) {
1399
- this.howl.unload();
1400
- }
1401
- this.stopSeekUpdate();
1402
- }
1403
- });
1404
 
1405
- app.use(Buefy.default);
1406
- app.mount('#app');
1407
- </script>
1408
  </body>
1409
  </html>
 
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>TranStudio</title>
7
  <link rel="stylesheet" href="/static/css/buefy.min.css">
8
+ <!-- we'll use cdn link for materialdesignicons here -->
9
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css">
10
+ <!-- <link rel="stylesheet" href="/static/css/materialdesignicons.min.css"> -->
11
+ <link rel="stylesheet" href="/static/css/style.css">
12
  <script>
13
  window.process = { env: { NODE_ENV: 'production' } };
14
  </script>
15
  <script src="/static/js/vue.global.prod.js"></script>
16
  <script src="/static/js/buefy.min.js"></script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  </head>
18
  <body>
19
  <div id="app" class="container">
 
33
  <b-icon icon="shield-account"></b-icon>
34
  <span class="ml-1">Admin</span>
35
  </b-navbar-item>
36
+ <b-navbar-item @click="showResumeUploadModal">
37
+ <b-icon icon="upload"></b-icon>
38
+ <span class="ml-1">Upload</span>
39
+ </b-navbar-item>
40
+ <b-navbar-item @click="showAudioFilesList">
41
+ <b-icon icon="folder"></b-icon>
42
+ <span class="ml-1">My Files</span>
43
+ </b-navbar-item>
44
  <b-navbar-item @click="logout">Logout {{ username }}</b-navbar-item>
45
  </template>
46
  <template v-else>
 
118
  </b-field>
119
 
120
  <b-field label="Overlap (in seconds)">
121
+ <b-slider v-model="overlap" :min="1" :max="60" :step="1"></b-slider>
122
  <span class="tag is-primary">{{ overlap }}</span>
123
  </b-field>
124
 
 
140
  </div>
141
 
142
  <b-field label="System Prompt (Optional)">
143
+ <b-input v-model="systemPrompt" maxlength="256" type="textarea" placeholder="Provide context or specify how to spell unfamiliar words (limited to 224 tokens only in English)."></b-input>
144
  </b-field>
145
 
146
  <b-upload v-model="audioFile" accept="audio/*" @change="handleAudioUpload" multiple>
 
234
  {{ formatTime(segment.start) }} - {{ formatTime(segment.end) }}
235
  </div>
236
  </div>
237
+ <div class="progress-waveform" :style="{ width: `${(currentTime / totalDuration) * 100}%` }"></div>
238
  </div>
239
  </div><!-- .waveform-container -->
240
 
 
245
  </div><!-- .audio-player -->
246
  </div>
247
 
248
+ <!-- Login Modal -->
249
+ <b-modal v-model="showLoginModal" has-modal-card trap-focus>
250
+ <div class="modal-card">
251
+ <header class="modal-card-head">
252
+ <p class="modal-card-title">Login</p>
253
+ </header>
254
+ <section class="modal-card-body">
255
+ <b-field label="Username">
256
+ <b-input type="text" v-model="loginForm.username" placeholder="Enter your username"></b-input>
257
+ </b-field>
258
+ <b-field label="Password">
259
+ <b-input type="password" v-model="loginForm.password" password-reveal></b-input>
260
+ </b-field>
261
+ </section>
262
+ <footer class="modal-card-foot">
263
+ <b-button type="is-primary" @click="handleLogin" :loading="isLoading">Login</b-button>
264
+ <b-button @click="showLoginModal = false">Cancel</b-button>
265
+ </footer>
266
+ </div>
267
+ </b-modal>
268
+
269
+ <!-- Signup Modal -->
270
+ <b-modal v-model="showSignupModal" has-modal-card trap-focus>
271
+ <div class="modal-card">
272
+ <header class="modal-card-head">
273
+ <p class="modal-card-title">Sign Up</p>
274
+ </header>
275
+ <section class="modal-card-body">
276
+ <b-field label="Username">
277
+ <b-input v-model="signupForm.username" placeholder="Enter username"></b-input>
278
+ </b-field>
279
+ <b-field label="Email">
280
+ <b-input type="email" v-model="signupForm.email" placeholder="Enter your email"></b-input>
281
+ </b-field>
282
+ <b-field label="Password">
283
+ <b-input type="password" v-model="signupForm.password" password-reveal></b-input>
284
+ </b-field>
285
+ <b-field label="Confirm Password">
286
+ <b-input type="password" v-model="signupForm.confirmPassword" password-reveal></b-input>
287
+ </b-field>
288
+ </section>
289
+ <footer class="modal-card-foot">
290
+ <b-button type="is-primary" @click="handleSignup" :loading="isLoading">Sign Up</b-button>
291
+ <b-button @click="showSignupModal = false">Cancel</b-button>
292
+ </footer>
293
+ </div>
294
+ </b-modal>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
295
 
296
+ <!-- Admin Panel Modal -->
297
+ <b-modal v-model="showAdminPanel" has-modal-card trap-focus :width="640">
298
+ <div class="modal-card">
299
+ <header class="modal-card-head">
300
+ <p class="modal-card-title">User Management</p>
301
+ </header>
302
+ <section class="modal-card-body">
303
+ <div class="block">
304
+ <b-field grouped>
305
+ <b-input placeholder="Search users..." v-model="userSearchQuery" expanded></b-input>
306
+ <b-button type="is-primary" icon-left="magnify">Search</b-button>
307
+ </b-field>
308
+ </div>
309
 
310
+ <b-table
311
+ :data="filteredUsers"
312
+ :loading="loadingUsers"
313
+ :paginated="true"
314
+ :per-page="5"
315
+ :mobile-cards="true">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
 
317
+ <b-table-column field="id" label="ID" width="40" numeric v-slot="props">
318
+ {{ props.row.id }}
319
+ </b-table-column>
320
 
321
+ <b-table-column field="username" label="Username" v-slot="props">
322
+ {{ props.row.username }}
323
+ </b-table-column>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324
 
325
+ <b-table-column field="email" label="Email" v-slot="props">
326
+ {{ props.row.email }}
327
+ </b-table-column>
 
 
 
 
 
 
328
 
329
+ <b-table-column field="is_admin" label="Admin" v-slot="props">
330
+ <b-icon
331
+ :icon="props.row.is_admin ? 'check' : 'close'"
332
+ :type="props.row.is_admin ? 'is-success' : 'is-danger'">
333
+ </b-icon>
334
+ </b-table-column>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
335
 
336
+ <b-table-column field="disabled" label="Status" v-slot="props">
337
+ <b-tag :type="props.row.disabled ? 'is-danger' : 'is-success'">
338
+ {{ props.row.disabled ? 'Disabled' : 'Active' }}
339
+ </b-tag>
340
+ </b-table-column>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
341
 
342
+ <b-table-column label="Actions" v-slot="props">
343
+ <div class="buttons">
344
+ <b-button
345
+ size="is-small"
346
+ :type="props.row.disabled ? 'is-success' : 'is-warning'"
347
+ :icon-left="props.row.disabled ? 'account-check' : 'account-cancel'"
348
+ @click="toggleUserStatus(props.row)"
349
+ :disabled="props.row.is_admin && props.row.id === currentUser.id">
350
+ {{ props.row.disabled ? 'Enable' : 'Disable' }}
351
+ </b-button>
352
+ <b-button
353
+ size="is-small"
354
+ type="is-danger"
355
+ icon-left="delete"
356
+ @click="deleteUser(props.row)"
357
+ :disabled="props.row.id === currentUser.id">
358
+ Delete
359
+ </b-button>
360
+ </div>
361
+ </b-table-column>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
362
 
363
+ <template #empty>
364
+ <div class="has-text-centered">No users found</div>
365
+ </template>
366
+ </b-table>
367
+ </section>
368
+ <footer class="modal-card-foot">
369
+ <b-button @click="refreshUsers" type="is-info" icon-left="refresh">Refresh</b-button>
370
+ <b-button @click="showAdminPanel = false">Close</b-button>
371
+ </footer>
372
+ </div>
373
+ </b-modal>
374
+
375
+ <!-- Upload Audio Modal -->
376
+ <b-modal v-model="showUploadModal" has-modal-card trap-focus>
377
+ <div class="modal-card">
378
+ <header class="modal-card-head">
379
+ <p class="modal-card-title">Upload Audio File</p>
380
+ </header>
381
+ <section class="modal-card-body">
382
+ <b-field label="Select Audio File">
383
+ <b-upload v-model="resumableFile"
384
+ @input="prepareChunkedUpload"
385
+ accept="audio/*"
386
+ expanded
387
+ drag-drop>
388
+ <div class="content has-text-centered">
389
+ <p>
390
+ <b-icon icon="upload" size="is-large"></b-icon>
391
+ </p>
392
+ <p>Drop your audio file here or click to upload</p>
393
+ </div>
394
+ </b-upload>
395
+ </b-field>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
396
 
397
+ <div v-if="resumableFile">
398
+ <p class="has-text-weight-bold">{{ resumableFile.name }}</p>
399
+ <p>Size: {{ formatFileSize(resumableFile.size) }}</p>
400
+
401
+ <b-progress v-if="uploadInProgress"
402
+ :value="uploadProgress.toFixed(1)"
403
+ type="is-info"
404
+ show-value
405
+ size="is-medium"
406
+ format="percent"
407
+ class="mt-4 mb-4"></b-progress>
408
+ </div>
409
+
410
+ <div class="upload-stats mt-2" v-if="uploadInProgress">
411
+ <p>
412
+ <b-icon icon="file-upload-outline" size="is-small"></b-icon>
413
+ Uploaded: {{ formatFileSize(currentChunkIndex * chunkUploadSize) }} / {{ formatFileSize(resumableFile.size) }}
414
+ </p>
415
+ <p v-if="uploadSpeed">
416
+ <b-icon icon="speedometer" size="is-small"></b-icon>
417
+ Speed: {{ uploadSpeed }} KB/s
418
+ </p>
419
+ <p v-if="estimatedTimeRemaining">
420
+ <b-icon icon="clock-outline" size="is-small"></b-icon>
421
+ Time remaining: {{ estimatedTimeRemaining }}
422
+ </p>
423
+ </div>
424
+
425
+ <div v-if="uploadPaused" class="notification is-warning">
426
+ Upload paused. Click Resume to continue.
427
+ </div>
428
+ </section>
429
+ <footer class="modal-card-foot">
430
+ <b-button v-if="!uploadInProgress && resumableFile"
431
+ type="is-primary"
432
+ @click="startChunkedUpload"
433
+ :loading="!uploadId"
434
+ :disabled="!uploadId || uploadInProgress">
435
+ Start Upload
436
+ </b-button>
437
+ <b-button v-if="uploadInProgress && !uploadPaused"
438
+ type="is-warning"
439
+ @click="pauseUpload">
440
+ Pause
441
+ </b-button>
442
+ <b-button v-if="uploadPaused"
443
+ type="is-info"
444
+ @click="resumeUpload">
445
+ Resume
446
+ </b-button>
447
+ <b-button v-if="uploadInProgress"
448
+ type="is-danger"
449
+ @click="cancelUpload">
450
+ Cancel
451
+ </b-button>
452
+ <b-button @click="closeUploadModal">Close</b-button>
453
+ </footer>
454
+ </div>
455
+ </b-modal>
456
+
457
+ <!-- Uploaded files Modal -->
458
+ <b-modal v-model="showAudioFilesModal" has-modal-card trap-focus>
459
+ <div class="modal-card">
460
+ <header class="modal-card-head">
461
+ <p class="modal-card-title">Your Audio Files</p>
462
+ </header>
463
+ <section class="modal-card-body">
464
+ <b-field grouped>
465
+ <b-field expanded>
466
+ <b-input placeholder="Search files..." v-model="audioFileSearchQuery" icon="magnify"></b-input>
467
+ </b-field>
468
+ <b-field>
469
+ <b-button type="is-primary" icon-left="refresh" @click="loadUploadedAudioFiles" :loading="loadingAudioFiles">
470
+ Refresh
471
+ </b-button>
472
+ </b-field>
473
+ </b-field>
474
+
475
+ <b-table
476
+ :data="filteredAudioFiles"
477
+ :loading="loadingAudioFiles"
478
+ :paginated="uploadedAudioFiles.length > 10"
479
+ :per-page="10"
480
+ detailed
481
+ detail-key="filename"
482
+ aria-next-label="Next page"
483
+ aria-previous-label="Previous page"
484
+ aria-page-label="Page"
485
+ aria-current-label="Current page">
486
 
487
+ <b-table-column field="filename" label="Filename" v-slot="props">
488
+ {{ props.row.filename }}
489
+ </b-table-column>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
490
 
491
+ <b-table-column field="size" label="Size" v-slot="props">
492
+ {{ formatFileSize(props.row.size) }}
493
+ </b-table-column>
 
 
 
 
 
494
 
495
+ <b-table-column field="uploaded_at" label="Uploaded" v-slot="props">
496
+ {{ new Date(props.row.uploaded_at).toLocaleString() }}
497
+ </b-table-column>
 
498
 
499
+ <b-table-column label="Actions" v-slot="props">
500
+ <div class="buttons">
501
+ <b-button
502
+ size="is-small"
503
+ type="is-info"
504
+ icon-left="play"
505
+ @click="selectAudioFile(props.row)">
506
+ Select
507
+ </b-button>
508
+ <b-button
509
+ size="is-small"
510
+ type="is-danger"
511
+ icon-left="delete"
512
+ @click="deleteAudioFile(props.row)">
513
+ Delete
514
+ </b-button>
515
+ </div>
516
+ </b-table-column>
517
 
518
+ <template #empty>
519
+ <div class="has-text-centered">
520
+ <p>No audio files found</p>
521
+ <b-button
522
+ class="mt-2"
523
+ type="is-primary"
524
+ icon-left="upload"
525
+ @click="showResumeUploadModal">
526
+ Upload Audio
527
+ </b-button>
528
+ </div>
529
+ </template>
 
 
 
 
 
 
 
 
 
 
 
530
 
531
+ <template #detail="props">
532
+ <div class="content">
533
+ <audio controls :src="`/uploads/${props.row.filename}`" style="width: 100%"></audio>
534
+ </div>
535
+ </template>
536
+ </b-table>
537
+ </section>
538
+ <footer class="modal-card-foot">
539
+ <b-button @click="showResumeUploadModal" type="is-primary" icon-left="upload">
540
+ Upload New File
541
+ </b-button>
542
+ <b-button @click="showAudioFilesModal = false">Close</b-button>
543
+ </footer>
544
+ </div>
545
+ </b-modal>
546
+ </div><!-- #app -->
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
547
 
548
+ <script src="/static/js/howler.min.js"></script>
549
+ <script src="/static/js/app.js"></script>
 
550
  </body>
551
  </html>