Fred808 commited on
Commit
4a88e2c
·
verified ·
1 Parent(s): ade2420

Upload 3 files

Browse files
Files changed (3) hide show
  1. index.html +202 -0
  2. script.js +655 -0
  3. style.css +770 -0
index.html ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <meta name="description" content="Advanced web-based dashboard for monitoring and managing cursor tracking operations on video datasets. Real-time processing status, file management, and control interface.">
7
+ <meta name="keywords" content="cursor tracking, video processing, dashboard, monitoring, API interface">
8
+ <meta name="author" content="Cursor Tracking Dashboard">
9
+ <title>Cursor Tracking Dashboard - Real-time Video Processing Monitor</title>
10
+ <link rel="stylesheet" href="style.css">
11
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
12
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
13
+ </head>
14
+ <body>
15
+ <div class="container">
16
+ <!-- Header -->
17
+ <header class="header">
18
+ <div class="header-content">
19
+ <div class="logo">
20
+ <i class="fas fa-mouse-pointer"></i>
21
+ <h1>Cursor Tracking Dashboard</h1>
22
+ </div>
23
+ <div class="header-actions">
24
+ <button id="refreshBtn" class="btn btn-secondary">
25
+ <i class="fas fa-sync-alt"></i>
26
+ Refresh
27
+ </button>
28
+ <div class="theme-toggle">
29
+ <button id="themeToggle" class="btn btn-icon">
30
+ <i class="fas fa-moon"></i>
31
+ </button>
32
+ </div>
33
+ </div>
34
+ </div>
35
+ </header>
36
+
37
+ <!-- Main Content -->
38
+ <main class="main-content">
39
+ <!-- Status Section -->
40
+ <section class="status-section">
41
+ <div class="card">
42
+ <div class="card-header">
43
+ <h2><i class="fas fa-chart-line"></i> Processing Status</h2>
44
+ <div class="status-indicator" id="statusIndicator">
45
+ <span class="status-dot"></span>
46
+ <span class="status-text">Loading...</span>
47
+ </div>
48
+ </div>
49
+ <div class="card-content">
50
+ <div class="stats-grid">
51
+ <div class="stat-item">
52
+ <div class="stat-value" id="totalFiles">-</div>
53
+ <div class="stat-label">Total Files</div>
54
+ </div>
55
+ <div class="stat-item">
56
+ <div class="stat-value" id="processedFiles">-</div>
57
+ <div class="stat-label">Processed</div>
58
+ </div>
59
+ <div class="stat-item">
60
+ <div class="stat-value" id="extractedCourses">-</div>
61
+ <div class="stat-label">Courses</div>
62
+ </div>
63
+ <div class="stat-item">
64
+ <div class="stat-value" id="extractedVideos">-</div>
65
+ <div class="stat-label">Videos</div>
66
+ </div>
67
+ <div class="stat-item">
68
+ <div class="stat-value" id="extractedFrames">-</div>
69
+ <div class="stat-label">Frames</div>
70
+ </div>
71
+ <div class="stat-item">
72
+ <div class="stat-value" id="trackedCursors">-</div>
73
+ <div class="stat-label">Cursors Tracked</div>
74
+ </div>
75
+ </div>
76
+
77
+ <div class="progress-section">
78
+ <div class="progress-info">
79
+ <span>Current File:</span>
80
+ <span id="currentFile" class="current-file">None</span>
81
+ </div>
82
+ <div class="progress-bar">
83
+ <div class="progress-fill" id="progressFill"></div>
84
+ </div>
85
+ <div class="progress-text" id="progressText">0%</div>
86
+ </div>
87
+ </div>
88
+ </div>
89
+ </section>
90
+
91
+ <!-- Control Section -->
92
+ <section class="control-section">
93
+ <div class="card">
94
+ <div class="card-header">
95
+ <h2><i class="fas fa-cogs"></i> Processing Controls</h2>
96
+ </div>
97
+ <div class="card-content">
98
+ <div class="control-group">
99
+ <div class="input-group">
100
+ <label for="startIndex">Start Index for RAR Fetching:</label>
101
+ <input type="number" id="startIndex" min="0" value="0" class="input">
102
+ <span class="input-help">Specify which index to start processing from</span>
103
+ </div>
104
+ <div class="button-group">
105
+ <button id="startProcessing" class="btn btn-primary">
106
+ <i class="fas fa-play"></i>
107
+ Start Processing
108
+ </button>
109
+ <button id="stopProcessing" class="btn btn-danger">
110
+ <i class="fas fa-stop"></i>
111
+ Stop Processing
112
+ </button>
113
+ </div>
114
+ </div>
115
+ </div>
116
+ </div>
117
+ </section>
118
+
119
+ <!-- Cursor Data Files Section -->
120
+ <section class="files-section">
121
+ <div class="card">
122
+ <div class="card-header">
123
+ <h2><i class="fas fa-file-alt"></i> Cursor Tracking Results</h2>
124
+ <div class="file-count" id="fileCount">0 files</div>
125
+ </div>
126
+ <div class="card-content">
127
+ <div class="files-grid" id="filesGrid">
128
+ <!-- Files will be populated here -->
129
+ </div>
130
+ </div>
131
+ </div>
132
+ </section>
133
+
134
+ <!-- Logs Section -->
135
+ <section class="logs-section">
136
+ <div class="card">
137
+ <div class="card-header">
138
+ <h2><i class="fas fa-terminal"></i> Processing Logs</h2>
139
+ <div class="log-controls">
140
+ <button id="clearLogs" class="btn btn-secondary btn-sm">
141
+ <i class="fas fa-trash"></i>
142
+ Clear
143
+ </button>
144
+ <button id="autoScroll" class="btn btn-secondary btn-sm active">
145
+ <i class="fas fa-arrow-down"></i>
146
+ Auto-scroll
147
+ </button>
148
+ </div>
149
+ </div>
150
+ <div class="card-content">
151
+ <div class="logs-container" id="logsContainer">
152
+ <div class="log-entry">
153
+ <span class="log-time">[Loading...]</span>
154
+ <span class="log-message">Initializing dashboard...</span>
155
+ </div>
156
+ </div>
157
+ </div>
158
+ </div>
159
+ </section>
160
+ </main>
161
+ </div>
162
+
163
+ <!-- File Details Modal -->
164
+ <div id="fileModal" class="modal">
165
+ <div class="modal-content">
166
+ <div class="modal-header">
167
+ <h3 id="modalTitle">File Details</h3>
168
+ <button class="modal-close" id="modalClose">
169
+ <i class="fas fa-times"></i>
170
+ </button>
171
+ </div>
172
+ <div class="modal-body" id="modalBody">
173
+ <!-- File details will be populated here -->
174
+ </div>
175
+ <div class="modal-footer">
176
+ <button id="downloadFile" class="btn btn-primary">
177
+ <i class="fas fa-download"></i>
178
+ Download JSON
179
+ </button>
180
+ <button id="viewFrames" class="btn btn-secondary">
181
+ <i class="fas fa-images"></i>
182
+ View Frames
183
+ </button>
184
+ </div>
185
+ </div>
186
+ </div>
187
+
188
+ <!-- Loading Overlay -->
189
+ <div id="loadingOverlay" class="loading-overlay">
190
+ <div class="loading-spinner">
191
+ <i class="fas fa-spinner fa-spin"></i>
192
+ <p>Loading...</p>
193
+ </div>
194
+ </div>
195
+
196
+ <!-- Toast Notifications -->
197
+ <div id="toastContainer" class="toast-container"></div>
198
+
199
+ <script src="script.js"></script>
200
+ </body>
201
+ </html>
202
+
script.js ADDED
@@ -0,0 +1,655 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
style.css ADDED
@@ -0,0 +1,770 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Reset and Base Styles */
2
+ * {
3
+ margin: 0;
4
+ padding: 0;
5
+ box-sizing: border-box;
6
+ }
7
+
8
+ :root {
9
+ /* Light theme colors */
10
+ --bg-primary: #ffffff;
11
+ --bg-secondary: #f8fafc;
12
+ --bg-tertiary: #f1f5f9;
13
+ --text-primary: #1e293b;
14
+ --text-secondary: #64748b;
15
+ --text-muted: #94a3b8;
16
+ --border-color: #e2e8f0;
17
+ --accent-primary: #3b82f6;
18
+ --accent-secondary: #8b5cf6;
19
+ --success: #10b981;
20
+ --warning: #f59e0b;
21
+ --danger: #ef4444;
22
+ --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
23
+ --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
24
+ --radius: 8px;
25
+ --radius-lg: 12px;
26
+ --transition: all 0.2s ease-in-out;
27
+ }
28
+
29
+ [data-theme="dark"] {
30
+ --bg-primary: #0f172a;
31
+ --bg-secondary: #1e293b;
32
+ --bg-tertiary: #334155;
33
+ --text-primary: #f8fafc;
34
+ --text-secondary: #cbd5e1;
35
+ --text-muted: #64748b;
36
+ --border-color: #334155;
37
+ --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.3), 0 1px 2px -1px rgb(0 0 0 / 0.3);
38
+ --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.3), 0 4px 6px -4px rgb(0 0 0 / 0.3);
39
+ }
40
+
41
+ body {
42
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
43
+ background: var(--bg-secondary);
44
+ color: var(--text-primary);
45
+ line-height: 1.6;
46
+ transition: var(--transition);
47
+ }
48
+
49
+ /* Container */
50
+ .container {
51
+ max-width: 1400px;
52
+ margin: 0 auto;
53
+ padding: 0 1rem;
54
+ }
55
+
56
+ /* Header */
57
+ .header {
58
+ background: var(--bg-primary);
59
+ border-bottom: 1px solid var(--border-color);
60
+ padding: 1rem 0;
61
+ position: sticky;
62
+ top: 0;
63
+ z-index: 100;
64
+ backdrop-filter: blur(10px);
65
+ }
66
+
67
+ .header-content {
68
+ display: flex;
69
+ justify-content: space-between;
70
+ align-items: center;
71
+ }
72
+
73
+ .logo {
74
+ display: flex;
75
+ align-items: center;
76
+ gap: 0.75rem;
77
+ }
78
+
79
+ .logo i {
80
+ font-size: 1.5rem;
81
+ color: var(--accent-primary);
82
+ }
83
+
84
+ .logo h1 {
85
+ font-size: 1.5rem;
86
+ font-weight: 600;
87
+ color: var(--text-primary);
88
+ }
89
+
90
+ .header-actions {
91
+ display: flex;
92
+ align-items: center;
93
+ gap: 1rem;
94
+ }
95
+
96
+ /* Main Content */
97
+ .main-content {
98
+ padding: 2rem 0;
99
+ display: flex;
100
+ flex-direction: column;
101
+ gap: 2rem;
102
+ }
103
+
104
+ /* Cards */
105
+ .card {
106
+ background: var(--bg-primary);
107
+ border: 1px solid var(--border-color);
108
+ border-radius: var(--radius-lg);
109
+ box-shadow: var(--shadow);
110
+ overflow: hidden;
111
+ transition: var(--transition);
112
+ }
113
+
114
+ .card:hover {
115
+ box-shadow: var(--shadow-lg);
116
+ }
117
+
118
+ .card-header {
119
+ padding: 1.5rem;
120
+ border-bottom: 1px solid var(--border-color);
121
+ display: flex;
122
+ justify-content: space-between;
123
+ align-items: center;
124
+ }
125
+
126
+ .card-header h2 {
127
+ font-size: 1.25rem;
128
+ font-weight: 600;
129
+ display: flex;
130
+ align-items: center;
131
+ gap: 0.5rem;
132
+ }
133
+
134
+ .card-header i {
135
+ color: var(--accent-primary);
136
+ }
137
+
138
+ .card-content {
139
+ padding: 1.5rem;
140
+ }
141
+
142
+ /* Status Section */
143
+ .status-indicator {
144
+ display: flex;
145
+ align-items: center;
146
+ gap: 0.5rem;
147
+ font-size: 0.875rem;
148
+ font-weight: 500;
149
+ }
150
+
151
+ .status-dot {
152
+ width: 8px;
153
+ height: 8px;
154
+ border-radius: 50%;
155
+ background: var(--text-muted);
156
+ animation: pulse 2s infinite;
157
+ }
158
+
159
+ .status-dot.running {
160
+ background: var(--success);
161
+ }
162
+
163
+ .status-dot.stopped {
164
+ background: var(--danger);
165
+ }
166
+
167
+ .status-dot.idle {
168
+ background: var(--warning);
169
+ }
170
+
171
+ @keyframes pulse {
172
+ 0%, 100% { opacity: 1; }
173
+ 50% { opacity: 0.5; }
174
+ }
175
+
176
+ .stats-grid {
177
+ display: grid;
178
+ grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
179
+ gap: 1.5rem;
180
+ margin-bottom: 2rem;
181
+ }
182
+
183
+ .stat-item {
184
+ text-align: center;
185
+ padding: 1rem;
186
+ background: var(--bg-secondary);
187
+ border-radius: var(--radius);
188
+ transition: var(--transition);
189
+ }
190
+
191
+ .stat-item:hover {
192
+ transform: translateY(-2px);
193
+ box-shadow: var(--shadow);
194
+ }
195
+
196
+ .stat-value {
197
+ font-size: 2rem;
198
+ font-weight: 700;
199
+ color: var(--accent-primary);
200
+ margin-bottom: 0.25rem;
201
+ }
202
+
203
+ .stat-label {
204
+ font-size: 0.875rem;
205
+ color: var(--text-secondary);
206
+ font-weight: 500;
207
+ }
208
+
209
+ .progress-section {
210
+ background: var(--bg-secondary);
211
+ padding: 1.5rem;
212
+ border-radius: var(--radius);
213
+ }
214
+
215
+ .progress-info {
216
+ display: flex;
217
+ justify-content: space-between;
218
+ align-items: center;
219
+ margin-bottom: 1rem;
220
+ font-size: 0.875rem;
221
+ }
222
+
223
+ .current-file {
224
+ font-weight: 600;
225
+ color: var(--accent-primary);
226
+ max-width: 300px;
227
+ overflow: hidden;
228
+ text-overflow: ellipsis;
229
+ white-space: nowrap;
230
+ }
231
+
232
+ .progress-bar {
233
+ height: 8px;
234
+ background: var(--bg-tertiary);
235
+ border-radius: 4px;
236
+ overflow: hidden;
237
+ margin-bottom: 0.5rem;
238
+ }
239
+
240
+ .progress-fill {
241
+ height: 100%;
242
+ background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
243
+ border-radius: 4px;
244
+ transition: width 0.3s ease;
245
+ width: 0%;
246
+ }
247
+
248
+ .progress-text {
249
+ text-align: center;
250
+ font-size: 0.875rem;
251
+ font-weight: 600;
252
+ color: var(--text-secondary);
253
+ }
254
+
255
+ /* Control Section */
256
+ .control-group {
257
+ display: flex;
258
+ flex-direction: column;
259
+ gap: 1.5rem;
260
+ }
261
+
262
+ .input-group {
263
+ display: flex;
264
+ flex-direction: column;
265
+ gap: 0.5rem;
266
+ }
267
+
268
+ .input-group label {
269
+ font-weight: 500;
270
+ color: var(--text-primary);
271
+ }
272
+
273
+ .input {
274
+ padding: 0.75rem;
275
+ border: 1px solid var(--border-color);
276
+ border-radius: var(--radius);
277
+ background: var(--bg-primary);
278
+ color: var(--text-primary);
279
+ font-size: 0.875rem;
280
+ transition: var(--transition);
281
+ max-width: 200px;
282
+ }
283
+
284
+ .input:focus {
285
+ outline: none;
286
+ border-color: var(--accent-primary);
287
+ box-shadow: 0 0 0 3px rgb(59 130 246 / 0.1);
288
+ }
289
+
290
+ .input-help {
291
+ font-size: 0.75rem;
292
+ color: var(--text-muted);
293
+ }
294
+
295
+ .button-group {
296
+ display: flex;
297
+ gap: 1rem;
298
+ flex-wrap: wrap;
299
+ }
300
+
301
+ /* Buttons */
302
+ .btn {
303
+ display: inline-flex;
304
+ align-items: center;
305
+ gap: 0.5rem;
306
+ padding: 0.75rem 1.5rem;
307
+ border: none;
308
+ border-radius: var(--radius);
309
+ font-size: 0.875rem;
310
+ font-weight: 500;
311
+ cursor: pointer;
312
+ transition: var(--transition);
313
+ text-decoration: none;
314
+ white-space: nowrap;
315
+ }
316
+
317
+ .btn:disabled {
318
+ opacity: 0.5;
319
+ cursor: not-allowed;
320
+ }
321
+
322
+ .btn-primary {
323
+ background: var(--accent-primary);
324
+ color: white;
325
+ }
326
+
327
+ .btn-primary:hover:not(:disabled) {
328
+ background: #2563eb;
329
+ transform: translateY(-1px);
330
+ box-shadow: var(--shadow);
331
+ }
332
+
333
+ .btn-secondary {
334
+ background: var(--bg-secondary);
335
+ color: var(--text-primary);
336
+ border: 1px solid var(--border-color);
337
+ }
338
+
339
+ .btn-secondary:hover:not(:disabled) {
340
+ background: var(--bg-tertiary);
341
+ transform: translateY(-1px);
342
+ }
343
+
344
+ .btn-danger {
345
+ background: var(--danger);
346
+ color: white;
347
+ }
348
+
349
+ .btn-danger:hover:not(:disabled) {
350
+ background: #dc2626;
351
+ transform: translateY(-1px);
352
+ box-shadow: var(--shadow);
353
+ }
354
+
355
+ .btn-sm {
356
+ padding: 0.5rem 1rem;
357
+ font-size: 0.75rem;
358
+ }
359
+
360
+ .btn-icon {
361
+ padding: 0.75rem;
362
+ width: auto;
363
+ height: auto;
364
+ }
365
+
366
+ .btn.active {
367
+ background: var(--accent-primary);
368
+ color: white;
369
+ }
370
+
371
+ /* Files Section */
372
+ .file-count {
373
+ font-size: 0.875rem;
374
+ color: var(--text-secondary);
375
+ font-weight: 500;
376
+ }
377
+
378
+ .files-grid {
379
+ display: grid;
380
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
381
+ gap: 1rem;
382
+ }
383
+
384
+ .file-card {
385
+ background: var(--bg-secondary);
386
+ border: 1px solid var(--border-color);
387
+ border-radius: var(--radius);
388
+ padding: 1rem;
389
+ transition: var(--transition);
390
+ cursor: pointer;
391
+ }
392
+
393
+ .file-card:hover {
394
+ transform: translateY(-2px);
395
+ box-shadow: var(--shadow);
396
+ border-color: var(--accent-primary);
397
+ }
398
+
399
+ .file-header {
400
+ display: flex;
401
+ justify-content: space-between;
402
+ align-items: flex-start;
403
+ margin-bottom: 0.75rem;
404
+ }
405
+
406
+ .file-name {
407
+ font-weight: 600;
408
+ color: var(--text-primary);
409
+ font-size: 0.875rem;
410
+ word-break: break-word;
411
+ flex: 1;
412
+ margin-right: 0.5rem;
413
+ }
414
+
415
+ .file-size {
416
+ font-size: 0.75rem;
417
+ color: var(--text-muted);
418
+ white-space: nowrap;
419
+ }
420
+
421
+ .file-stats {
422
+ display: grid;
423
+ grid-template-columns: 1fr 1fr;
424
+ gap: 0.5rem;
425
+ margin-bottom: 0.75rem;
426
+ }
427
+
428
+ .file-stat {
429
+ display: flex;
430
+ justify-content: space-between;
431
+ font-size: 0.75rem;
432
+ }
433
+
434
+ .file-stat-label {
435
+ color: var(--text-secondary);
436
+ }
437
+
438
+ .file-stat-value {
439
+ color: var(--text-primary);
440
+ font-weight: 500;
441
+ }
442
+
443
+ .file-actions {
444
+ display: flex;
445
+ gap: 0.5rem;
446
+ }
447
+
448
+ .file-actions .btn {
449
+ padding: 0.5rem 0.75rem;
450
+ font-size: 0.75rem;
451
+ }
452
+
453
+ /* Logs Section */
454
+ .log-controls {
455
+ display: flex;
456
+ gap: 0.5rem;
457
+ }
458
+
459
+ .logs-container {
460
+ background: var(--bg-secondary);
461
+ border: 1px solid var(--border-color);
462
+ border-radius: var(--radius);
463
+ height: 300px;
464
+ overflow-y: auto;
465
+ padding: 1rem;
466
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
467
+ font-size: 0.75rem;
468
+ line-height: 1.4;
469
+ }
470
+
471
+ .log-entry {
472
+ margin-bottom: 0.5rem;
473
+ display: flex;
474
+ gap: 0.5rem;
475
+ }
476
+
477
+ .log-time {
478
+ color: var(--text-muted);
479
+ white-space: nowrap;
480
+ }
481
+
482
+ .log-message {
483
+ color: var(--text-primary);
484
+ word-break: break-word;
485
+ }
486
+
487
+ .log-entry.error .log-message {
488
+ color: var(--danger);
489
+ }
490
+
491
+ .log-entry.success .log-message {
492
+ color: var(--success);
493
+ }
494
+
495
+ .log-entry.warning .log-message {
496
+ color: var(--warning);
497
+ }
498
+
499
+ /* Modal */
500
+ .modal {
501
+ display: none;
502
+ position: fixed;
503
+ top: 0;
504
+ left: 0;
505
+ width: 100%;
506
+ height: 100%;
507
+ background: rgba(0, 0, 0, 0.5);
508
+ z-index: 1000;
509
+ backdrop-filter: blur(4px);
510
+ }
511
+
512
+ .modal.show {
513
+ display: flex;
514
+ align-items: center;
515
+ justify-content: center;
516
+ animation: fadeIn 0.2s ease;
517
+ }
518
+
519
+ .modal-content {
520
+ background: var(--bg-primary);
521
+ border-radius: var(--radius-lg);
522
+ box-shadow: var(--shadow-lg);
523
+ max-width: 600px;
524
+ width: 90%;
525
+ max-height: 80vh;
526
+ overflow: hidden;
527
+ animation: slideUp 0.2s ease;
528
+ }
529
+
530
+ .modal-header {
531
+ padding: 1.5rem;
532
+ border-bottom: 1px solid var(--border-color);
533
+ display: flex;
534
+ justify-content: space-between;
535
+ align-items: center;
536
+ }
537
+
538
+ .modal-header h3 {
539
+ font-size: 1.25rem;
540
+ font-weight: 600;
541
+ }
542
+
543
+ .modal-close {
544
+ background: none;
545
+ border: none;
546
+ font-size: 1.25rem;
547
+ cursor: pointer;
548
+ color: var(--text-muted);
549
+ padding: 0.25rem;
550
+ border-radius: var(--radius);
551
+ transition: var(--transition);
552
+ }
553
+
554
+ .modal-close:hover {
555
+ background: var(--bg-secondary);
556
+ color: var(--text-primary);
557
+ }
558
+
559
+ .modal-body {
560
+ padding: 1.5rem;
561
+ max-height: 400px;
562
+ overflow-y: auto;
563
+ }
564
+
565
+ .modal-footer {
566
+ padding: 1.5rem;
567
+ border-top: 1px solid var(--border-color);
568
+ display: flex;
569
+ gap: 1rem;
570
+ justify-content: flex-end;
571
+ }
572
+
573
+ /* Loading Overlay */
574
+ .loading-overlay {
575
+ display: none;
576
+ position: fixed;
577
+ top: 0;
578
+ left: 0;
579
+ width: 100%;
580
+ height: 100%;
581
+ background: rgba(0, 0, 0, 0.3);
582
+ z-index: 2000;
583
+ backdrop-filter: blur(2px);
584
+ }
585
+
586
+ .loading-overlay.show {
587
+ display: flex;
588
+ align-items: center;
589
+ justify-content: center;
590
+ }
591
+
592
+ .loading-spinner {
593
+ background: var(--bg-primary);
594
+ padding: 2rem;
595
+ border-radius: var(--radius-lg);
596
+ text-align: center;
597
+ box-shadow: var(--shadow-lg);
598
+ }
599
+
600
+ .loading-spinner i {
601
+ font-size: 2rem;
602
+ color: var(--accent-primary);
603
+ margin-bottom: 1rem;
604
+ }
605
+
606
+ .loading-spinner p {
607
+ color: var(--text-secondary);
608
+ font-weight: 500;
609
+ }
610
+
611
+ /* Toast Notifications */
612
+ .toast-container {
613
+ position: fixed;
614
+ top: 1rem;
615
+ right: 1rem;
616
+ z-index: 3000;
617
+ display: flex;
618
+ flex-direction: column;
619
+ gap: 0.5rem;
620
+ }
621
+
622
+ .toast {
623
+ background: var(--bg-primary);
624
+ border: 1px solid var(--border-color);
625
+ border-radius: var(--radius);
626
+ padding: 1rem;
627
+ box-shadow: var(--shadow-lg);
628
+ min-width: 300px;
629
+ animation: slideInRight 0.3s ease;
630
+ display: flex;
631
+ align-items: center;
632
+ gap: 0.75rem;
633
+ }
634
+
635
+ .toast.success {
636
+ border-left: 4px solid var(--success);
637
+ }
638
+
639
+ .toast.error {
640
+ border-left: 4px solid var(--danger);
641
+ }
642
+
643
+ .toast.warning {
644
+ border-left: 4px solid var(--warning);
645
+ }
646
+
647
+ .toast.info {
648
+ border-left: 4px solid var(--accent-primary);
649
+ }
650
+
651
+ .toast-icon {
652
+ font-size: 1.25rem;
653
+ }
654
+
655
+ .toast.success .toast-icon {
656
+ color: var(--success);
657
+ }
658
+
659
+ .toast.error .toast-icon {
660
+ color: var(--danger);
661
+ }
662
+
663
+ .toast.warning .toast-icon {
664
+ color: var(--warning);
665
+ }
666
+
667
+ .toast.info .toast-icon {
668
+ color: var(--accent-primary);
669
+ }
670
+
671
+ .toast-content {
672
+ flex: 1;
673
+ }
674
+
675
+ .toast-title {
676
+ font-weight: 600;
677
+ margin-bottom: 0.25rem;
678
+ }
679
+
680
+ .toast-message {
681
+ font-size: 0.875rem;
682
+ color: var(--text-secondary);
683
+ }
684
+
685
+ .toast-close {
686
+ background: none;
687
+ border: none;
688
+ color: var(--text-muted);
689
+ cursor: pointer;
690
+ padding: 0.25rem;
691
+ border-radius: var(--radius);
692
+ transition: var(--transition);
693
+ }
694
+
695
+ .toast-close:hover {
696
+ background: var(--bg-secondary);
697
+ color: var(--text-primary);
698
+ }
699
+
700
+ /* Animations */
701
+ @keyframes fadeIn {
702
+ from { opacity: 0; }
703
+ to { opacity: 1; }
704
+ }
705
+
706
+ @keyframes slideUp {
707
+ from { transform: translateY(20px); opacity: 0; }
708
+ to { transform: translateY(0); opacity: 1; }
709
+ }
710
+
711
+ @keyframes slideInRight {
712
+ from { transform: translateX(100%); opacity: 0; }
713
+ to { transform: translateX(0); opacity: 1; }
714
+ }
715
+
716
+ /* Responsive Design */
717
+ @media (max-width: 768px) {
718
+ .container {
719
+ padding: 0 0.5rem;
720
+ }
721
+
722
+ .header-content {
723
+ flex-direction: column;
724
+ gap: 1rem;
725
+ align-items: flex-start;
726
+ }
727
+
728
+ .stats-grid {
729
+ grid-template-columns: repeat(2, 1fr);
730
+ }
731
+
732
+ .button-group {
733
+ flex-direction: column;
734
+ }
735
+
736
+ .files-grid {
737
+ grid-template-columns: 1fr;
738
+ }
739
+
740
+ .modal-content {
741
+ width: 95%;
742
+ margin: 1rem;
743
+ }
744
+
745
+ .toast {
746
+ min-width: 280px;
747
+ }
748
+
749
+ .toast-container {
750
+ left: 1rem;
751
+ right: 1rem;
752
+ }
753
+ }
754
+
755
+ @media (max-width: 480px) {
756
+ .stats-grid {
757
+ grid-template-columns: 1fr;
758
+ }
759
+
760
+ .progress-info {
761
+ flex-direction: column;
762
+ align-items: flex-start;
763
+ gap: 0.5rem;
764
+ }
765
+
766
+ .current-file {
767
+ max-width: 100%;
768
+ }
769
+ }
770
+