Fred808 commited on
Commit
08a39ff
·
verified ·
1 Parent(s): 00f2189

Update static/script.js

Browse files
Files changed (1) hide show
  1. static/script.js +622 -655
static/script.js CHANGED
@@ -1,655 +1,622 @@
1
- // Configuration
2
- const API_BASE_URL = window.location.origin; // Use same origin as the UI
3
- const REFRESH_INTERVAL = 10000; // 10 seconds (increased for demo)
4
- const MAX_LOGS = 100;
5
-
6
- // Global state
7
- let refreshInterval;
8
- let autoScrollEnabled = true;
9
- let currentFiles = [];
10
- let selectedFile = null;
11
- let apiConnected = false;
12
-
13
- // DOM Elements
14
- const elements = {
15
- statusIndicator: document.getElementById('statusIndicator'),
16
- totalFiles: document.getElementById('totalFiles'),
17
- processedFiles: document.getElementById('processedFiles'),
18
- extractedCourses: document.getElementById('extractedCourses'),
19
- extractedVideos: document.getElementById('extractedVideos'),
20
- extractedFrames: document.getElementById('extractedFrames'),
21
- trackedCursors: document.getElementById('trackedCursors'),
22
- currentFile: document.getElementById('currentFile'),
23
- progressFill: document.getElementById('progressFill'),
24
- progressText: document.getElementById('progressText'),
25
- startIndex: document.getElementById('startIndex'),
26
- startProcessing: document.getElementById('startProcessing'),
27
- stopProcessing: document.getElementById('stopProcessing'),
28
- refreshBtn: document.getElementById('refreshBtn'),
29
- themeToggle: document.getElementById('themeToggle'),
30
- fileCount: document.getElementById('fileCount'),
31
- filesGrid: document.getElementById('filesGrid'),
32
- logsContainer: document.getElementById('logsContainer'),
33
- clearLogs: document.getElementById('clearLogs'),
34
- autoScroll: document.getElementById('autoScroll'),
35
- fileModal: document.getElementById('fileModal'),
36
- modalTitle: document.getElementById('modalTitle'),
37
- modalBody: document.getElementById('modalBody'),
38
- modalClose: document.getElementById('modalClose'),
39
- downloadFile: document.getElementById('downloadFile'),
40
- viewFrames: document.getElementById('viewFrames'),
41
- loadingOverlay: document.getElementById('loadingOverlay'),
42
- toastContainer: document.getElementById('toastContainer')
43
- };
44
-
45
- // Initialize the application
46
- document.addEventListener('DOMContentLoaded', function() {
47
- initializeTheme();
48
- setupEventListeners();
49
- startAutoRefresh();
50
- fetchInitialData();
51
- });
52
-
53
- // Theme Management
54
- function initializeTheme() {
55
- const savedTheme = localStorage.getItem('theme') || 'light';
56
- document.documentElement.setAttribute('data-theme', savedTheme);
57
- updateThemeIcon(savedTheme);
58
- }
59
-
60
- function toggleTheme() {
61
- const currentTheme = document.documentElement.getAttribute('data-theme');
62
- const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
63
- document.documentElement.setAttribute('data-theme', newTheme);
64
- localStorage.setItem('theme', newTheme);
65
- updateThemeIcon(newTheme);
66
- }
67
-
68
- function updateThemeIcon(theme) {
69
- const icon = elements.themeToggle.querySelector('i');
70
- icon.className = theme === 'dark' ? 'fas fa-sun' : 'fas fa-moon';
71
- }
72
-
73
- // Event Listeners
74
- function setupEventListeners() {
75
- elements.themeToggle.addEventListener('click', toggleTheme);
76
- elements.refreshBtn.addEventListener('click', () => {
77
- showToast('Refreshing data...', 'info');
78
- fetchAllData();
79
- });
80
-
81
- elements.startProcessing.addEventListener('click', startProcessing);
82
- elements.stopProcessing.addEventListener('click', stopProcessing);
83
-
84
- elements.clearLogs.addEventListener('click', clearLogs);
85
- elements.autoScroll.addEventListener('click', toggleAutoScroll);
86
-
87
- elements.modalClose.addEventListener('click', closeModal);
88
- elements.fileModal.addEventListener('click', (e) => {
89
- if (e.target === elements.fileModal) closeModal();
90
- });
91
-
92
- elements.downloadFile.addEventListener('click', downloadSelectedFile);
93
- elements.viewFrames.addEventListener('click', viewFrames);
94
-
95
- // Keyboard shortcuts
96
- document.addEventListener('keydown', (e) => {
97
- if (e.key === 'Escape') closeModal();
98
- if (e.key === 'F5') {
99
- e.preventDefault();
100
- fetchAllData();
101
- }
102
- });
103
- }
104
-
105
- // API Functions
106
- async function apiRequest(endpoint, options = {}) {
107
- try {
108
- showLoading();
109
- const response = await fetch(`${API_BASE_URL}${endpoint}`, {
110
- headers: {
111
- 'Content-Type': 'application/json',
112
- ...options.headers
113
- },
114
- ...options
115
- });
116
-
117
- if (!response.ok) {
118
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
119
- }
120
-
121
- apiConnected = true;
122
- return await response.json();
123
- } catch (error) {
124
- console.error('API request failed:', error);
125
- apiConnected = false;
126
-
127
- // Show different messages based on error type
128
- if (error.name === 'TypeError' && error.message.includes('fetch')) {
129
- showToast('API server not running. Please start the cursor tracking API on port 8000.', 'warning');
130
- } else {
131
- showToast(`API Error: ${error.message}`, 'error');
132
- }
133
- throw error;
134
- } finally {
135
- hideLoading();
136
- }
137
- }
138
-
139
- async function fetchStatus() {
140
- try {
141
- const data = await apiRequest('/status');
142
- updateStatusDisplay(data.processing_status);
143
- return data;
144
- } catch (error) {
145
- // Show demo data when API is not connected
146
- const demoStatus = {
147
- is_running: false,
148
- total_files: 150,
149
- processed_files: 45,
150
- extracted_courses: 12,
151
- extracted_videos: 89,
152
- extracted_frames_count: 15420,
153
- tracked_cursors_count: 8934,
154
- current_file: null,
155
- logs: [
156
- '[Demo Mode] API server not connected',
157
- '[Demo Mode] This is a demonstration of the UI',
158
- '[Demo Mode] Start the API server on port 8000 to see real data'
159
- ]
160
- };
161
- updateStatusDisplay(demoStatus);
162
- }
163
- }
164
-
165
- async function fetchCursorData() {
166
- try {
167
- const data = await apiRequest('/cursor-data');
168
- currentFiles = data.files || [];
169
- updateFilesDisplay(currentFiles);
170
- return data;
171
- } catch (error) {
172
- // Show demo files when API is not connected
173
- const demoFiles = [
174
- {
175
- filename: 'course_1_video_1_mp4_cursor_data.json',
176
- size_bytes: 45678,
177
- modified_time: 'Sun Jul 13 19:30:15 2025'
178
- },
179
- {
180
- filename: 'course_2_video_3_mp4_cursor_data.json',
181
- size_bytes: 67890,
182
- modified_time: 'Sun Jul 13 18:45:22 2025'
183
- },
184
- {
185
- filename: 'course_3_video_2_mp4_cursor_data.json',
186
- size_bytes: 34567,
187
- modified_time: 'Sun Jul 13 17:20:10 2025'
188
- }
189
- ];
190
- currentFiles = demoFiles;
191
- updateFilesDisplay(demoFiles);
192
- }
193
- }
194
-
195
- async function fetchFileDetails(filename) {
196
- try {
197
- const data = await apiRequest(`/cursor-data/${filename}/summary`);
198
- return data;
199
- } catch (error) {
200
- showToast(`Failed to fetch details for ${filename}`, 'error');
201
- return null;
202
- }
203
- }
204
-
205
- async function startProcessing() {
206
- try {
207
- const startIndex = parseInt(elements.startIndex.value) || 0;
208
- const data = await apiRequest('/start-processing', {
209
- method: 'POST',
210
- body: JSON.stringify({ start_index: startIndex })
211
- });
212
-
213
- showToast(data.message, data.status === 'started' ? 'success' : 'info');
214
-
215
- if (data.status === 'started') {
216
- elements.startProcessing.disabled = true;
217
- elements.stopProcessing.disabled = false;
218
- }
219
- } catch (error) {
220
- showToast('Failed to start processing', 'error');
221
- }
222
- }
223
-
224
- async function stopProcessing() {
225
- try {
226
- const data = await apiRequest('/stop-processing', {
227
- method: 'POST'
228
- });
229
-
230
- showToast(data.message, 'info');
231
- elements.startProcessing.disabled = false;
232
- elements.stopProcessing.disabled = true;
233
- } catch (error) {
234
- showToast('Failed to stop processing', 'error');
235
- }
236
- }
237
-
238
- // Display Update Functions
239
- function updateStatusDisplay(status) {
240
- // Update status indicator
241
- const statusDot = elements.statusIndicator.querySelector('.status-dot');
242
- const statusText = elements.statusIndicator.querySelector('.status-text');
243
-
244
- if (status.is_running) {
245
- statusDot.className = 'status-dot running';
246
- statusText.textContent = 'Processing';
247
- elements.startProcessing.disabled = true;
248
- elements.stopProcessing.disabled = false;
249
- } else {
250
- statusDot.className = 'status-dot stopped';
251
- statusText.textContent = 'Idle';
252
- elements.startProcessing.disabled = false;
253
- elements.stopProcessing.disabled = true;
254
- }
255
-
256
- // Update statistics
257
- elements.totalFiles.textContent = status.total_files || 0;
258
- elements.processedFiles.textContent = status.processed_files || 0;
259
- elements.extractedCourses.textContent = status.extracted_courses || 0;
260
- elements.extractedVideos.textContent = status.extracted_videos || 0;
261
- elements.extractedFrames.textContent = status.extracted_frames_count || 0;
262
- elements.trackedCursors.textContent = status.tracked_cursors_count || 0;
263
-
264
- // Update current file and progress
265
- const currentFile = status.current_file || 'None';
266
- elements.currentFile.textContent = currentFile;
267
-
268
- const progress = status.total_files > 0 ?
269
- Math.round((status.processed_files / status.total_files) * 100) : 0;
270
- elements.progressFill.style.width = `${progress}%`;
271
- elements.progressText.textContent = `${progress}%`;
272
-
273
- // Update logs
274
- if (status.logs && status.logs.length > 0) {
275
- updateLogs(status.logs);
276
- }
277
- }
278
-
279
- function updateFilesDisplay(files) {
280
- elements.fileCount.textContent = `${files.length} files`;
281
-
282
- if (files.length === 0) {
283
- elements.filesGrid.innerHTML = `
284
- <div class="no-files">
285
- <i class="fas fa-folder-open" style="font-size: 3rem; color: var(--text-muted); margin-bottom: 1rem;"></i>
286
- <p style="color: var(--text-muted);">No cursor tracking files found yet.</p>
287
- <p style="color: var(--text-muted); font-size: 0.875rem;">Files will appear here after processing completes.</p>
288
- </div>
289
- `;
290
- return;
291
- }
292
-
293
- elements.filesGrid.innerHTML = files.map(file => `
294
- <div class="file-card" onclick="openFileModal('${file.filename}')">
295
- <div class="file-header">
296
- <div class="file-name">${file.filename}</div>
297
- <div class="file-size">${formatFileSize(file.size_bytes)}</div>
298
- </div>
299
- <div class="file-stats">
300
- <div class="file-stat">
301
- <span class="file-stat-label">Modified:</span>
302
- <span class="file-stat-value">${formatDate(file.modified_time)}</span>
303
- </div>
304
- </div>
305
- <div class="file-actions">
306
- <button class="btn btn-primary btn-sm" onclick="event.stopPropagation(); downloadFile('${file.filename}')">
307
- <i class="fas fa-download"></i>
308
- Download
309
- </button>
310
- <button class="btn btn-secondary btn-sm" onclick="event.stopPropagation(); openFileModal('${file.filename}')">
311
- <i class="fas fa-eye"></i>
312
- Details
313
- </button>
314
- </div>
315
- </div>
316
- `).join('');
317
- }
318
-
319
- function updateLogs(logs) {
320
- const container = elements.logsContainer;
321
-
322
- // Clear existing logs if we have new ones
323
- if (logs.length > 0) {
324
- container.innerHTML = '';
325
- }
326
-
327
- logs.slice(-MAX_LOGS).forEach(log => {
328
- const logEntry = document.createElement('div');
329
- logEntry.className = 'log-entry';
330
-
331
- // Determine log type based on content
332
- let logType = '';
333
- if (log.includes('') || log.includes('ERROR') || log.includes('Failed')) {
334
- logType = 'error';
335
- } else if (log.includes('✅') || log.includes('SUCCESS') || log.includes('Successfully')) {
336
- logType = 'success';
337
- } else if (log.includes('⚠️') || log.includes('WARN')) {
338
- logType = 'warning';
339
- }
340
-
341
- if (logType) {
342
- logEntry.classList.add(logType);
343
- }
344
-
345
- // Extract timestamp and message
346
- const timestampMatch = log.match(/^\[([^\]]+)\]/);
347
- const timestamp = timestampMatch ? timestampMatch[1] : new Date().toLocaleTimeString();
348
- const message = timestampMatch ? log.substring(timestampMatch[0].length).trim() : log;
349
-
350
- logEntry.innerHTML = `
351
- <span class="log-time">[${timestamp}]</span>
352
- <span class="log-message">${escapeHtml(message)}</span>
353
- `;
354
-
355
- container.appendChild(logEntry);
356
- });
357
-
358
- // Auto-scroll to bottom if enabled
359
- if (autoScrollEnabled) {
360
- container.scrollTop = container.scrollHeight;
361
- }
362
- }
363
-
364
- // Modal Functions
365
- async function openFileModal(filename) {
366
- selectedFile = filename;
367
- elements.modalTitle.textContent = `File Details: ${filename}`;
368
-
369
- showModal();
370
-
371
- const details = await fetchFileDetails(filename);
372
- if (details) {
373
- elements.modalBody.innerHTML = `
374
- <div class="file-details">
375
- <div class="detail-section">
376
- <h4>File Information</h4>
377
- <div class="detail-grid">
378
- <div class="detail-item">
379
- <span class="detail-label">File Size:</span>
380
- <span class="detail-value">${formatFileSize(details.file_size_bytes)}</span>
381
- </div>
382
- <div class="detail-item">
383
- <span class="detail-label">Modified:</span>
384
- <span class="detail-value">${details.modified_time}</span>
385
- </div>
386
- </div>
387
- </div>
388
-
389
- <div class="detail-section">
390
- <h4>Frame Statistics</h4>
391
- <div class="detail-grid">
392
- <div class="detail-item">
393
- <span class="detail-label">Total Frames:</span>
394
- <span class="detail-value">${details.total_frames}</span>
395
- </div>
396
- <div class="detail-item">
397
- <span class="detail-label">Cursor Active:</span>
398
- <span class="detail-value">${details.cursor_active_frames}</span>
399
- </div>
400
- <div class="detail-item">
401
- <span class="detail-label">Cursor Inactive:</span>
402
- <span class="detail-value">${details.cursor_inactive_frames}</span>
403
- </div>
404
- <div class="detail-item">
405
- <span class="detail-label">Detection Rate:</span>
406
- <span class="detail-value">${(details.cursor_detection_rate * 100).toFixed(1)}%</span>
407
- </div>
408
- </div>
409
- </div>
410
-
411
- <div class="detail-section">
412
- <h4>Confidence Statistics</h4>
413
- <div class="detail-grid">
414
- <div class="detail-item">
415
- <span class="detail-label">Average:</span>
416
- <span class="detail-value">${details.confidence_stats.average.toFixed(3)}</span>
417
- </div>
418
- <div class="detail-item">
419
- <span class="detail-label">Maximum:</span>
420
- <span class="detail-value">${details.confidence_stats.maximum.toFixed(3)}</span>
421
- </div>
422
- <div class="detail-item">
423
- <span class="detail-label">Minimum:</span>
424
- <span class="detail-value">${details.confidence_stats.minimum.toFixed(3)}</span>
425
- </div>
426
- <div class="detail-item">
427
- <span class="detail-label">Measurements:</span>
428
- <span class="detail-value">${details.confidence_stats.total_measurements}</span>
429
- </div>
430
- </div>
431
- </div>
432
-
433
- <div class="detail-section">
434
- <h4>Templates Used</h4>
435
- <div class="template-list">
436
- ${details.templates_used.length > 0 ?
437
- details.templates_used.map(template => `<span class="template-tag">${template}</span>`).join('') :
438
- '<span class="no-templates">No templates detected</span>'
439
- }
440
- </div>
441
- </div>
442
- </div>
443
-
444
- <style>
445
- .file-details { font-size: 0.875rem; }
446
- .detail-section { margin-bottom: 1.5rem; }
447
- .detail-section h4 {
448
- margin-bottom: 0.75rem;
449
- color: var(--accent-primary);
450
- font-weight: 600;
451
- }
452
- .detail-grid {
453
- display: grid;
454
- grid-template-columns: 1fr 1fr;
455
- gap: 0.5rem;
456
- }
457
- .detail-item {
458
- display: flex;
459
- justify-content: space-between;
460
- padding: 0.5rem;
461
- background: var(--bg-secondary);
462
- border-radius: var(--radius);
463
- }
464
- .detail-label { color: var(--text-secondary); }
465
- .detail-value { font-weight: 500; }
466
- .template-list {
467
- display: flex;
468
- flex-wrap: wrap;
469
- gap: 0.5rem;
470
- }
471
- .template-tag {
472
- background: var(--accent-primary);
473
- color: white;
474
- padding: 0.25rem 0.5rem;
475
- border-radius: var(--radius);
476
- font-size: 0.75rem;
477
- }
478
- .no-templates {
479
- color: var(--text-muted);
480
- font-style: italic;
481
- }
482
- </style>
483
- `;
484
- } else {
485
- elements.modalBody.innerHTML = '<p>Failed to load file details.</p>';
486
- }
487
- }
488
-
489
- function showModal() {
490
- elements.fileModal.classList.add('show');
491
- document.body.style.overflow = 'hidden';
492
- }
493
-
494
- function closeModal() {
495
- elements.fileModal.classList.remove('show');
496
- document.body.style.overflow = '';
497
- selectedFile = null;
498
- }
499
-
500
- // File Operations
501
- async function downloadFile(filename) {
502
- try {
503
- const response = await fetch(`${API_BASE_URL}/cursor-data/${filename}`);
504
- if (!response.ok) throw new Error('Download failed');
505
-
506
- const blob = await response.blob();
507
- const url = window.URL.createObjectURL(blob);
508
- const a = document.createElement('a');
509
- a.href = url;
510
- a.download = filename;
511
- document.body.appendChild(a);
512
- a.click();
513
- document.body.removeChild(a);
514
- window.URL.revokeObjectURL(url);
515
-
516
- showToast(`Downloaded ${filename}`, 'success');
517
- } catch (error) {
518
- showToast(`Failed to download ${filename}`, 'error');
519
- }
520
- }
521
-
522
- function downloadSelectedFile() {
523
- if (selectedFile) {
524
- downloadFile(selectedFile);
525
- }
526
- }
527
-
528
- function viewFrames() {
529
- if (selectedFile) {
530
- showToast('Frame viewer feature coming soon!', 'info');
531
- }
532
- }
533
-
534
- // Utility Functions
535
- function formatFileSize(bytes) {
536
- if (bytes === 0) return '0 B';
537
- const k = 1024;
538
- const sizes = ['B', 'KB', 'MB', 'GB'];
539
- const i = Math.floor(Math.log(bytes) / Math.log(k));
540
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
541
- }
542
-
543
- function formatDate(dateString) {
544
- try {
545
- return new Date(dateString).toLocaleDateString();
546
- } catch {
547
- return dateString;
548
- }
549
- }
550
-
551
- function escapeHtml(text) {
552
- const div = document.createElement('div');
553
- div.textContent = text;
554
- return div.innerHTML;
555
- }
556
-
557
- // Log Management
558
- function clearLogs() {
559
- elements.logsContainer.innerHTML = '<div class="log-entry"><span class="log-time">[' +
560
- new Date().toLocaleTimeString() + ']</span><span class="log-message">Logs cleared</span></div>';
561
- showToast('Logs cleared', 'info');
562
- }
563
-
564
- function toggleAutoScroll() {
565
- autoScrollEnabled = !autoScrollEnabled;
566
- elements.autoScroll.classList.toggle('active', autoScrollEnabled);
567
-
568
- if (autoScrollEnabled) {
569
- elements.logsContainer.scrollTop = elements.logsContainer.scrollHeight;
570
- }
571
- }
572
-
573
- // Loading and Toast Functions
574
- function showLoading() {
575
- elements.loadingOverlay.classList.add('show');
576
- }
577
-
578
- function hideLoading() {
579
- elements.loadingOverlay.classList.remove('show');
580
- }
581
-
582
- function showToast(message, type = 'info', duration = 5000) {
583
- const toast = document.createElement('div');
584
- toast.className = `toast ${type}`;
585
-
586
- const icons = {
587
- success: 'fas fa-check-circle',
588
- error: 'fas fa-exclamation-circle',
589
- warning: 'fas fa-exclamation-triangle',
590
- info: 'fas fa-info-circle'
591
- };
592
-
593
- toast.innerHTML = `
594
- <i class="toast-icon ${icons[type]}"></i>
595
- <div class="toast-content">
596
- <div class="toast-message">${escapeHtml(message)}</div>
597
- </div>
598
- <button class="toast-close">
599
- <i class="fas fa-times"></i>
600
- </button>
601
- `;
602
-
603
- const closeBtn = toast.querySelector('.toast-close');
604
- closeBtn.addEventListener('click', () => removeToast(toast));
605
-
606
- elements.toastContainer.appendChild(toast);
607
-
608
- // Auto-remove after duration
609
- setTimeout(() => removeToast(toast), duration);
610
- }
611
-
612
- function removeToast(toast) {
613
- if (toast && toast.parentNode) {
614
- toast.style.animation = 'slideInRight 0.3s ease reverse';
615
- setTimeout(() => {
616
- if (toast.parentNode) {
617
- toast.parentNode.removeChild(toast);
618
- }
619
- }, 300);
620
- }
621
- }
622
-
623
- // Auto-refresh Management
624
- function startAutoRefresh() {
625
- fetchAllData(); // Initial fetch
626
- refreshInterval = setInterval(fetchAllData, REFRESH_INTERVAL);
627
- }
628
-
629
- function stopAutoRefresh() {
630
- if (refreshInterval) {
631
- clearInterval(refreshInterval);
632
- refreshInterval = null;
633
- }
634
- }
635
-
636
- async function fetchInitialData() {
637
- await fetchAllData();
638
- }
639
-
640
- async function fetchAllData() {
641
- try {
642
- await Promise.all([
643
- fetchStatus(),
644
- fetchCursorData()
645
- ]);
646
- } catch (error) {
647
- console.error('Failed to fetch data:', error);
648
- }
649
- }
650
-
651
- // Cleanup on page unload
652
- window.addEventListener('beforeunload', () => {
653
- stopAutoRefresh();
654
- });
655
-
 
1
+ document.addEventListener('DOMContentLoaded', function() {
2
+ // Configuration
3
+ const API_BASE_URL = window.location.origin;
4
+ const REFRESH_INTERVAL = 10000; // 10 seconds
5
+ const MAX_LOGS = 100;
6
+
7
+ // Global state
8
+ let refreshInterval;
9
+ let autoScrollEnabled = true;
10
+ let currentFiles = [];
11
+ let selectedFile = null;
12
+ let apiConnected = false;
13
+
14
+ // DOM Elements
15
+ const elements = {
16
+ statusIndicator: document.getElementById('statusIndicator'),
17
+ totalFiles: document.getElementById('totalFiles'),
18
+ processedFiles: document.getElementById('processedFiles'),
19
+ extractedCourses: document.getElementById('extractedCourses'),
20
+ extractedVideos: document.getElementById('extractedVideos'),
21
+ extractedFrames: document.getElementById('extractedFrames'),
22
+ analyzedFrames: document.getElementById('analyzedFrames'),
23
+ currentFile: document.getElementById('currentFile'),
24
+ progressFill: document.getElementById('progressFill'),
25
+ progressText: document.getElementById('progressText'),
26
+ startIndex: document.getElementById('startIndex'),
27
+ startProcessing: document.getElementById('startProcessing'),
28
+ stopProcessing: document.getElementById('stopProcessing'),
29
+ refreshBtn: document.getElementById('refreshBtn'),
30
+ themeToggle: document.getElementById('themeToggle'),
31
+ fileCount: document.getElementById('fileCount'),
32
+ filesGrid: document.getElementById('filesGrid'),
33
+ logsContainer: document.getElementById('logsContainer'),
34
+ clearLogs: document.getElementById('clearLogs'),
35
+ autoScroll: document.getElementById('autoScroll'),
36
+ fileModal: document.getElementById('fileModal'),
37
+ modalTitle: document.getElementById('modalTitle'),
38
+ modalBody: document.getElementById('modalBody'),
39
+ modalClose: document.getElementById('modalClose'),
40
+ downloadFile: document.getElementById('downloadFile'),
41
+ viewSummary: document.getElementById('viewSummary'),
42
+ loadingOverlay: document.getElementById('loadingOverlay'),
43
+ toastContainer: document.getElementById('toastContainer')
44
+ };
45
+
46
+ // Initialize the application
47
+ initializeTheme();
48
+ setupEventListeners();
49
+ startAutoRefresh();
50
+ fetchInitialData();
51
+
52
+ // Theme Management
53
+ function initializeTheme() {
54
+ const savedTheme = localStorage.getItem('theme') || 'light';
55
+ document.documentElement.setAttribute('data-theme', savedTheme);
56
+ updateThemeIcon(savedTheme);
57
+ }
58
+
59
+ function toggleTheme() {
60
+ const currentTheme = document.documentElement.getAttribute('data-theme');
61
+ const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
62
+ document.documentElement.setAttribute('data-theme', newTheme);
63
+ localStorage.setItem('theme', newTheme);
64
+ updateThemeIcon(newTheme);
65
+ }
66
+
67
+ function updateThemeIcon(theme) {
68
+ const icon = elements.themeToggle.querySelector('i');
69
+ icon.className = theme === 'dark' ? 'fas fa-sun' : 'fas fa-moon';
70
+ }
71
+
72
+ // Event Listeners
73
+ function setupEventListeners() {
74
+ elements.themeToggle.addEventListener('click', toggleTheme);
75
+ elements.refreshBtn.addEventListener('click', () => {
76
+ showToast('Refreshing data...', 'info');
77
+ fetchAllData();
78
+ });
79
+
80
+ elements.startProcessing.addEventListener('click', startProcessing);
81
+ elements.stopProcessing.addEventListener('click', stopProcessing);
82
+
83
+ elements.clearLogs.addEventListener('click', clearLogs);
84
+ elements.autoScroll.addEventListener('click', toggleAutoScroll);
85
+
86
+ elements.modalClose.addEventListener('click', closeModal);
87
+ elements.fileModal.addEventListener('click', (e) => {
88
+ if (e.target === elements.fileModal) closeModal();
89
+ });
90
+
91
+ elements.downloadFile.addEventListener('click', downloadSelectedFile);
92
+ elements.viewSummary.addEventListener('click', viewSummary);
93
+
94
+ // Keyboard shortcuts
95
+ document.addEventListener('keydown', (e) => {
96
+ if (e.key === 'Escape') closeModal();
97
+ if (e.key === 'F5') {
98
+ e.preventDefault();
99
+ fetchAllData();
100
+ }
101
+ });
102
+ }
103
+
104
+ // API Functions
105
+ async function apiRequest(endpoint, options = {}) {
106
+ try {
107
+ showLoading();
108
+ const response = await fetch(`${API_BASE_URL}${endpoint}`, {
109
+ headers: {
110
+ 'Content-Type': 'application/json',
111
+ ...options.headers
112
+ },
113
+ ...options
114
+ });
115
+
116
+ if (!response.ok) {
117
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
118
+ }
119
+
120
+ apiConnected = true;
121
+ return await response.json();
122
+ } catch (error) {
123
+ console.error('API request failed:', error);
124
+ apiConnected = false;
125
+
126
+ if (error.name === 'TypeError' && error.message.includes('fetch')) {
127
+ showToast('API server not running. Please start the video analysis API on port 8000.', 'warning');
128
+ } else {
129
+ showToast(`API Error: ${error.message}`, 'error');
130
+ }
131
+ throw error;
132
+ } finally {
133
+ hideLoading();
134
+ }
135
+ }
136
+
137
+ async function fetchStatus() {
138
+ try {
139
+ const data = await apiRequest('/status');
140
+ updateStatusDisplay(data.processing_status);
141
+ return data;
142
+ } catch (error) {
143
+ // Demo data when API is not connected
144
+ const demoStatus = {
145
+ is_running: false,
146
+ total_files: 150,
147
+ processed_files: 45,
148
+ extracted_courses: 12,
149
+ extracted_videos: 89,
150
+ extracted_frames_count: 15420,
151
+ analyzed_frames_count: 12000,
152
+ current_file: null,
153
+ logs: [
154
+ '[Demo Mode] API server not connected',
155
+ '[Demo Mode] This is a demonstration of the UI',
156
+ '[Demo Mode] Start the API server on port 8000 to see real data'
157
+ ]
158
+ };
159
+ updateStatusDisplay(demoStatus);
160
+ }
161
+ }
162
+
163
+ async function fetchAnalysisData() {
164
+ try {
165
+ const data = await apiRequest('/analysis-data');
166
+ currentFiles = data.files || [];
167
+ updateFilesDisplay(currentFiles);
168
+ return data;
169
+ } catch (error) {
170
+ // Demo files when API is not connected
171
+ const demoFiles = [
172
+ {
173
+ filename: 'course_1_video_1_mp4_analysis.json',
174
+ size_bytes: 45678,
175
+ modified_time: 'Sun Jul 13 19:30:15 2025'
176
+ },
177
+ {
178
+ filename: 'course_2_video_3_mp4_analysis.json',
179
+ size_bytes: 67890,
180
+ modified_time: 'Sun Jul 13 18:45:22 2025'
181
+ },
182
+ {
183
+ filename: 'course_3_video_2_mp4_analysis.json',
184
+ size_bytes: 34567,
185
+ modified_time: 'Sun Jul 13 17:20:10 2025'
186
+ }
187
+ ];
188
+ currentFiles = demoFiles;
189
+ updateFilesDisplay(demoFiles);
190
+ }
191
+ }
192
+
193
+ async function fetchFileDetails(filename) {
194
+ try {
195
+ const data = await apiRequest(`/analysis-data/${filename}/summary`);
196
+ return data;
197
+ } catch (error) {
198
+ showToast(`Failed to fetch details for ${filename}`, 'error');
199
+ return null;
200
+ }
201
+ }
202
+
203
+ async function startProcessing() {
204
+ try {
205
+ const startIndex = parseInt(elements.startIndex.value) || 0;
206
+ const data = await apiRequest('/start-processing', {
207
+ method: 'POST',
208
+ body: JSON.stringify({ start_index: startIndex })
209
+ });
210
+
211
+ showToast(data.message, data.status === 'started' ? 'success' : 'info');
212
+
213
+ if (data.status === 'started') {
214
+ elements.startProcessing.disabled = true;
215
+ elements.stopProcessing.disabled = false;
216
+ }
217
+ } catch (error) {
218
+ showToast('Failed to start processing', 'error');
219
+ }
220
+ }
221
+
222
+ async function stopProcessing() {
223
+ try {
224
+ const data = await apiRequest('/stop-processing', {
225
+ method: 'POST'
226
+ });
227
+
228
+ showToast(data.message, 'info');
229
+ elements.startProcessing.disabled = false;
230
+ elements.stopProcessing.disabled = true;
231
+ } catch (error) {
232
+ showToast('Failed to stop processing', 'error');
233
+ }
234
+ }
235
+
236
+ // Display Update Functions
237
+ function updateStatusDisplay(status) {
238
+ // Update status indicator
239
+ const statusDot = elements.statusIndicator.querySelector('.status-dot');
240
+ const statusText = elements.statusIndicator.querySelector('.status-text');
241
+
242
+ if (status.is_running) {
243
+ statusDot.className = 'status-dot running';
244
+ statusText.textContent = 'Processing';
245
+ elements.startProcessing.disabled = true;
246
+ elements.stopProcessing.disabled = false;
247
+ } else {
248
+ statusDot.className = 'status-dot stopped';
249
+ statusText.textContent = 'Idle';
250
+ elements.startProcessing.disabled = false;
251
+ elements.stopProcessing.disabled = true;
252
+ }
253
+
254
+ // Update statistics
255
+ elements.totalFiles.textContent = status.total_files || 0;
256
+ elements.processedFiles.textContent = status.processed_files || 0;
257
+ elements.extractedCourses.textContent = status.extracted_courses || 0;
258
+ elements.extractedVideos.textContent = status.extracted_videos || 0;
259
+ elements.extractedFrames.textContent = status.extracted_frames_count || 0;
260
+ elements.analyzedFrames.textContent = status.analyzed_frames_count || 0;
261
+
262
+ // Update current file and progress
263
+ const currentFile = status.current_file || 'None';
264
+ elements.currentFile.textContent = currentFile;
265
+
266
+ const progress = status.total_files > 0 ?
267
+ Math.round((status.processed_files / status.total_files) * 100) : 0;
268
+ elements.progressFill.style.width = `${progress}%`;
269
+ elements.progressText.textContent = `${progress}%`;
270
+
271
+ // Update logs
272
+ if (status.logs && status.logs.length > 0) {
273
+ updateLogs(status.logs);
274
+ }
275
+ }
276
+
277
+ function updateFilesDisplay(files) {
278
+ elements.fileCount.textContent = `${files.length} files`;
279
+
280
+ if (files.length === 0) {
281
+ elements.filesGrid.innerHTML = `
282
+ <div class="no-files">
283
+ <i class="fas fa-folder-open"></i>
284
+ <p>No analysis files found yet.</p>
285
+ <p>Files will appear here after processing completes.</p>
286
+ </div>
287
+ `;
288
+ return;
289
+ }
290
+
291
+ elements.filesGrid.innerHTML = files.map(file => `
292
+ <div class="file-card" onclick="openFileModal('${file.filename}')">
293
+ <div class="file-header">
294
+ <div class="file-name">${file.filename}</div>
295
+ <div class="file-size">${formatFileSize(file.size_bytes)}</div>
296
+ </div>
297
+ <div class="file-stats">
298
+ <div class="file-stat">
299
+ <span class="file-stat-label">Modified:</span>
300
+ <span class="file-stat-value">${formatDate(file.modified_time)}</span>
301
+ </div>
302
+ </div>
303
+ <div class="file-actions">
304
+ <button class="btn btn-primary btn-sm" onclick="event.stopPropagation(); downloadFile('${file.filename}')">
305
+ <i class="fas fa-download"></i>
306
+ Download
307
+ </button>
308
+ <button class="btn btn-secondary btn-sm" onclick="event.stopPropagation(); openFileModal('${file.filename}')">
309
+ <i class="fas fa-eye"></i>
310
+ Details
311
+ </button>
312
+ </div>
313
+ </div>
314
+ `).join('');
315
+ }
316
+
317
+ function updateLogs(logs) {
318
+ const container = elements.logsContainer;
319
+
320
+ if (logs.length > 0) {
321
+ container.innerHTML = '';
322
+ }
323
+
324
+ logs.slice(-MAX_LOGS).forEach(log => {
325
+ const logEntry = document.createElement('div');
326
+ logEntry.className = 'log-entry';
327
+
328
+ let logType = '';
329
+ if (log.includes('❌') || log.includes('ERROR') || log.includes('Failed')) {
330
+ logType = 'error';
331
+ } else if (log.includes('✅') || log.includes('SUCCESS') || log.includes('Successfully')) {
332
+ logType = 'success';
333
+ } else if (log.includes('⚠️') || log.includes('WARN')) {
334
+ logType = 'warning';
335
+ }
336
+
337
+ if (logType) {
338
+ logEntry.classList.add(logType);
339
+ }
340
+
341
+ const timestampMatch = log.match(/^\[([^\]]+)\]/);
342
+ const timestamp = timestampMatch ? timestampMatch[1] : new Date().toLocaleTimeString();
343
+ const message = timestampMatch ? log.substring(timestampMatch[0].length).trim() : log;
344
+
345
+ logEntry.innerHTML = `
346
+ <span class="log-time">[${timestamp}]</span>
347
+ <span class="log-message">${escapeHtml(message)}</span>
348
+ `;
349
+
350
+ container.appendChild(logEntry);
351
+ });
352
+
353
+ if (autoScrollEnabled) {
354
+ container.scrollTop = container.scrollHeight;
355
+ }
356
+ }
357
+
358
+ // Modal Functions
359
+ async function openFileModal(filename) {
360
+ selectedFile = filename;
361
+ elements.modalTitle.textContent = `Analysis Details: ${filename}`;
362
+
363
+ showModal();
364
+
365
+ const details = await fetchFileDetails(filename);
366
+ if (details) {
367
+ elements.modalBody.innerHTML = `
368
+ <div class="file-details">
369
+ <div class="detail-section">
370
+ <h4>File Information</h4>
371
+ <div class="detail-grid">
372
+ <div class="detail-item">
373
+ <span class="detail-label">File Size:</span>
374
+ <span class="detail-value">${formatFileSize(details.file_size_bytes)}</span>
375
+ </div>
376
+ <div class="detail-item">
377
+ <span class="detail-label">Modified:</span>
378
+ <span class="detail-value">${details.modified_time}</span>
379
+ </div>
380
+ </div>
381
+ </div>
382
+
383
+ <div class="detail-section">
384
+ <h4>Frame Statistics</h4>
385
+ <div class="detail-grid">
386
+ <div class="detail-item">
387
+ <span class="detail-label">Total Frames:</span>
388
+ <span class="detail-value">${details.total_frames}</span>
389
+ </div>
390
+ <div class="detail-item">
391
+ <span class="detail-label">Frames Analyzed:</span>
392
+ <span class="detail-value">${details.frames_with_descriptions}</span>
393
+ </div>
394
+ </div>
395
+ </div>
396
+
397
+ <div class="detail-section">
398
+ <h4>Analysis Summary</h4>
399
+ <div class="summary-content">
400
+ <p><strong>High Level Goal:</strong> ${details.high_level_goal || 'Not available'}</p>
401
+ <p><strong>Final Goal:</strong> ${details.final_goal || 'Not available'}</p>
402
+
403
+ <h5>Key Steps:</h5>
404
+ <ul class="steps-list">
405
+ ${details.steps.map(step => `
406
+ <li>
407
+ <strong>${step.action}:</strong> ${step.description}
408
+ </li>
409
+ `).join('')}
410
+ </ul>
411
+ </div>
412
+ </div>
413
+ </div>
414
+
415
+ <style>
416
+ .file-details { font-size: 0.875rem; }
417
+ .detail-section { margin-bottom: 1.5rem; }
418
+ .detail-section h4 {
419
+ margin-bottom: 0.75rem;
420
+ color: var(--accent-primary);
421
+ font-weight: 600;
422
+ }
423
+ .detail-grid {
424
+ display: grid;
425
+ grid-template-columns: 1fr 1fr;
426
+ gap: 0.5rem;
427
+ }
428
+ .detail-item {
429
+ display: flex;
430
+ justify-content: space-between;
431
+ padding: 0.5rem;
432
+ background: var(--bg-secondary);
433
+ border-radius: var(--radius);
434
+ }
435
+ .detail-label { color: var(--text-secondary); }
436
+ .detail-value { font-weight: 500; }
437
+ .summary-content { padding: 0.5rem; }
438
+ .steps-list {
439
+ margin-top: 0.5rem;
440
+ padding-left: 1.5rem;
441
+ }
442
+ .steps-list li {
443
+ margin-bottom: 0.5rem;
444
+ line-height: 1.4;
445
+ }
446
+ </style>
447
+ `;
448
+ } else {
449
+ elements.modalBody.innerHTML = '<p>Failed to load analysis details.</p>';
450
+ }
451
+ }
452
+
453
+ function showModal() {
454
+ elements.fileModal.classList.add('show');
455
+ document.body.style.overflow = 'hidden';
456
+ }
457
+
458
+ function closeModal() {
459
+ elements.fileModal.classList.remove('show');
460
+ document.body.style.overflow = '';
461
+ selectedFile = null;
462
+ }
463
+
464
+ // File Operations
465
+ async function downloadFile(filename) {
466
+ try {
467
+ const response = await fetch(`${API_BASE_URL}/analysis-data/${filename}`);
468
+ if (!response.ok) throw new Error('Download failed');
469
+
470
+ const blob = await response.blob();
471
+ const url = window.URL.createObjectURL(blob);
472
+ const a = document.createElement('a');
473
+ a.href = url;
474
+ a.download = filename;
475
+ document.body.appendChild(a);
476
+ a.click();
477
+ document.body.removeChild(a);
478
+ window.URL.revokeObjectURL(url);
479
+
480
+ showToast(`Downloaded ${filename}`, 'success');
481
+ } catch (error) {
482
+ showToast(`Failed to download ${filename}`, 'error');
483
+ }
484
+ }
485
+
486
+ function downloadSelectedFile() {
487
+ if (selectedFile) {
488
+ downloadFile(selectedFile);
489
+ }
490
+ }
491
+
492
+ function viewSummary() {
493
+ if (selectedFile) {
494
+ window.open(`${API_BASE_URL}/analysis-data/${selectedFile}/summary`, '_blank');
495
+ }
496
+ }
497
+
498
+ // Utility Functions
499
+ function formatFileSize(bytes) {
500
+ if (bytes === 0) return '0 B';
501
+ const k = 1024;
502
+ const sizes = ['B', 'KB', 'MB', 'GB'];
503
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
504
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
505
+ }
506
+
507
+ function formatDate(dateString) {
508
+ try {
509
+ return new Date(dateString).toLocaleDateString();
510
+ } catch {
511
+ return dateString;
512
+ }
513
+ }
514
+
515
+ function escapeHtml(text) {
516
+ const div = document.createElement('div');
517
+ div.textContent = text;
518
+ return div.innerHTML;
519
+ }
520
+
521
+ // Log Management
522
+ function clearLogs() {
523
+ elements.logsContainer.innerHTML = '<div class="log-entry"><span class="log-time">[' +
524
+ new Date().toLocaleTimeString() + ']</span><span class="log-message">Logs cleared</span></div>';
525
+ showToast('Logs cleared', 'info');
526
+ }
527
+
528
+ function toggleAutoScroll() {
529
+ autoScrollEnabled = !autoScrollEnabled;
530
+ elements.autoScroll.classList.toggle('active', autoScrollEnabled);
531
+
532
+ if (autoScrollEnabled) {
533
+ elements.logsContainer.scrollTop = elements.logsContainer.scrollHeight;
534
+ }
535
+ }
536
+
537
+ // Loading and Toast Functions
538
+ function showLoading() {
539
+ elements.loadingOverlay.classList.add('show');
540
+ }
541
+
542
+ function hideLoading() {
543
+ elements.loadingOverlay.classList.remove('show');
544
+ }
545
+
546
+ function showToast(message, type = 'info', duration = 5000) {
547
+ const toast = document.createElement('div');
548
+ toast.className = `toast ${type}`;
549
+
550
+ const icons = {
551
+ success: 'fas fa-check-circle',
552
+ error: 'fas fa-exclamation-circle',
553
+ warning: 'fas fa-exclamation-triangle',
554
+ info: 'fas fa-info-circle'
555
+ };
556
+
557
+ toast.innerHTML = `
558
+ <i class="toast-icon ${icons[type]}"></i>
559
+ <div class="toast-content">
560
+ <div class="toast-message">${escapeHtml(message)}</div>
561
+ </div>
562
+ <button class="toast-close">
563
+ <i class="fas fa-times"></i>
564
+ </button>
565
+ `;
566
+
567
+ const closeBtn = toast.querySelector('.toast-close');
568
+ closeBtn.addEventListener('click', () => removeToast(toast));
569
+
570
+ elements.toastContainer.appendChild(toast);
571
+
572
+ setTimeout(() => removeToast(toast), duration);
573
+ }
574
+
575
+ function removeToast(toast) {
576
+ if (toast && toast.parentNode) {
577
+ toast.style.animation = 'slideInRight 0.3s ease reverse';
578
+ setTimeout(() => {
579
+ if (toast.parentNode) {
580
+ toast.parentNode.removeChild(toast);
581
+ }
582
+ }, 300);
583
+ }
584
+ }
585
+
586
+ // Auto-refresh Management
587
+ function startAutoRefresh() {
588
+ fetchAllData();
589
+ refreshInterval = setInterval(fetchAllData, REFRESH_INTERVAL);
590
+ }
591
+
592
+ function stopAutoRefresh() {
593
+ if (refreshInterval) {
594
+ clearInterval(refreshInterval);
595
+ refreshInterval = null;
596
+ }
597
+ }
598
+
599
+ async function fetchInitialData() {
600
+ await fetchAllData();
601
+ }
602
+
603
+ async function fetchAllData() {
604
+ try {
605
+ await Promise.all([
606
+ fetchStatus(),
607
+ fetchAnalysisData()
608
+ ]);
609
+ } catch (error) {
610
+ console.error('Failed to fetch data:', error);
611
+ }
612
+ }
613
+
614
+ // Make functions available globally for HTML onclick handlers
615
+ window.openFileModal = openFileModal;
616
+ window.downloadFile = downloadFile;
617
+
618
+ // Cleanup on page unload
619
+ window.addEventListener('beforeunload', () => {
620
+ stopAutoRefresh();
621
+ });
622
+ });