vectorplasticity commited on
Commit
278a1e7
·
verified ·
1 Parent(s): bffe968

Add dashboard JavaScript

Browse files
Files changed (1) hide show
  1. app/static/js/dashboard.js +636 -0
app/static/js/dashboard.js ADDED
@@ -0,0 +1,636 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Universal Model Trainer - Dashboard JavaScript
3
+ */
4
+
5
+ // API Base URL
6
+ const API_BASE = '/api';
7
+
8
+ // State Management
9
+ const state = {
10
+ jobs: [],
11
+ models: [],
12
+ datasets: [],
13
+ systemJob: null,
14
+ searchResults: {
15
+ models: [],
16
+ datasets: []
17
+ }
18
+ };
19
+
20
+ // Utility Functions
21
+ const utils = {
22
+ async fetchAPI(endpoint, options = {}) {
23
+ try {
24
+ const response = await fetch(`${API_BASE}${endpoint}`, {
25
+ headers: {
26
+ 'Content-Type': 'application/json',
27
+ ...options.headers
28
+ },
29
+ ...options
30
+ });
31
+
32
+ if (!response.ok) {
33
+ const error = await response.json().catch(() => ({ detail: response.statusText }));
34
+ throw new Error(error.detail || 'Request failed');
35
+ }
36
+
37
+ return await response.json();
38
+ } catch (error) {
39
+ console.error('API Error:', error);
40
+ throw error;
41
+ }
42
+ },
43
+
44
+ formatDate(dateStr) {
45
+ if (!dateStr) return 'N/A';
46
+ return new Date(dateStr).toLocaleString();
47
+ },
48
+
49
+ formatDuration(seconds) {
50
+ if (!seconds) return 'N/A';
51
+ const h = Math.floor(seconds / 3600);
52
+ const m = Math.floor((seconds % 3600) / 60);
53
+ const s = Math.floor(seconds % 60);
54
+ return h > 0 ? `${h}h ${m}m ${s}s` : m > 0 ? `${m}m ${s}s` : `${s}s`;
55
+ },
56
+
57
+ formatNumber(num) {
58
+ if (num >= 1e9) return (num / 1e9).toFixed(2) + 'B';
59
+ if (num >= 1e6) return (num / 1e6).toFixed(2) + 'M';
60
+ if (num >= 1e3) return (num / 1e3).toFixed(2) + 'K';
61
+ return num?.toString() || '0';
62
+ },
63
+
64
+ getStatusBadgeClass(status) {
65
+ const classes = {
66
+ 'queued': 'badge-queued',
67
+ 'running': 'badge-running',
68
+ 'completed': 'badge-completed',
69
+ 'failed': 'badge-failed',
70
+ 'cancelled': 'badge-cancelled',
71
+ 'paused': 'badge-paused'
72
+ };
73
+ return classes[status?.toLowerCase()] || 'badge-queued';
74
+ },
75
+
76
+ showToast(message, type = 'info') {
77
+ const toastContainer = document.getElementById('toastContainer') || createToastContainer();
78
+ const toast = document.createElement('div');
79
+ toast.className = `alert alert-${type} alert-dismissible fade show`;
80
+ toast.innerHTML = `
81
+ ${message}
82
+ <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
83
+ `;
84
+ toastContainer.appendChild(toast);
85
+ setTimeout(() => toast.remove(), 5000);
86
+ },
87
+
88
+ debounce(func, wait) {
89
+ let timeout;
90
+ return function executedFunction(...args) {
91
+ const later = () => {
92
+ clearTimeout(timeout);
93
+ func(...args);
94
+ };
95
+ clearTimeout(timeout);
96
+ timeout = setTimeout(later, wait);
97
+ };
98
+ }
99
+ };
100
+
101
+ function createToastContainer() {
102
+ const container = document.createElement('div');
103
+ container.id = 'toastContainer';
104
+ container.style.cssText = 'position: fixed; top: 80px; right: 20px; z-index: 9999; max-width: 400px;';
105
+ document.body.appendChild(container);
106
+ return container;
107
+ }
108
+
109
+ // API Functions
110
+ const api = {
111
+ // Jobs
112
+ async getJobs(filters = {}) {
113
+ const params = new URLSearchParams(filters).toString();
114
+ return utils.fetchAPI(`/jobs?${params}`);
115
+ },
116
+
117
+ async getJob(jobId) {
118
+ return utils.fetchAPI(`/jobs/${jobId}`);
119
+ },
120
+
121
+ async getActiveJobs() {
122
+ return utils.fetchAPI('/jobs/active');
123
+ },
124
+
125
+ async getJobStatistics() {
126
+ return utils.fetchAPI('/jobs/statistics');
127
+ },
128
+
129
+ async cancelJob(jobId) {
130
+ return utils.fetchAPI(`/jobs/${jobId}/cancel`, { method: 'POST' });
131
+ },
132
+
133
+ async pauseJob(jobId) {
134
+ return utils.fetchAPI(`/jobs/${jobId}/pause`, { method: 'POST' });
135
+ },
136
+
137
+ async resumeJob(jobId) {
138
+ return utils.fetchAPI(`/jobs/${jobId}/resume`, { method: 'POST' });
139
+ },
140
+
141
+ async retryJob(jobId) {
142
+ return utils.fetchAPI(`/jobs/${jobId}/retry`, { method: 'POST' });
143
+ },
144
+
145
+ async deleteJob(jobId) {
146
+ return utils.fetchAPI(`/jobs/${jobId}`, { method: 'DELETE' });
147
+ },
148
+
149
+ async getJobLogs(jobId, limit = 100) {
150
+ return utils.fetchAPI(`/jobs/${jobId}/logs?limit=${limit}`);
151
+ },
152
+
153
+ async getJobCheckpoints(jobId) {
154
+ return utils.fetchAPI(`/jobs/${jobId}/checkpoints`);
155
+ },
156
+
157
+ // Training
158
+ async startTraining(config) {
159
+ return utils.fetchAPI('/training/start', {
160
+ method: 'POST',
161
+ body: JSON.stringify(config)
162
+ });
163
+ },
164
+
165
+ async getTrainingTemplates() {
166
+ return utils.fetchAPI('/training/templates');
167
+ },
168
+
169
+ // Models
170
+ async searchModels(query, task = null, limit = 20) {
171
+ const params = new URLSearchParams({ query, limit });
172
+ if (task) params.append('task', task);
173
+ return utils.fetchAPI(`/models/search?${params}`);
174
+ },
175
+
176
+ async getModelInfo(modelId) {
177
+ return utils.fetchAPI(`/models/${encodeURIComponent(modelId)}`);
178
+ },
179
+
180
+ async getModelRecommendations(task) {
181
+ return utils.fetchAPI(`/models/recommendations/${task}`);
182
+ },
183
+
184
+ async checkModelCompatibility(modelId, task) {
185
+ return utils.fetchAPI(`/models/${encodeURIComponent(modelId)}/compatibility?task=${task}`);
186
+ },
187
+
188
+ // Datasets
189
+ async searchDatasets(query, limit = 20) {
190
+ return utils.fetchAPI(`/datasets/search?query=${encodeURIComponent(query)}&limit=${limit}`);
191
+ },
192
+
193
+ async getDatasetInfo(datasetId) {
194
+ return utils.fetchAPI(`/datasets/${encodeURIComponent(datasetId)}`);
195
+ },
196
+
197
+ async getDatasetRecommendations(task) {
198
+ return utils.fetchAPI(`/datasets/recommendations/${task}`);
199
+ },
200
+
201
+ async getDatasetPreview(datasetId, config = null, split = null) {
202
+ let url = `/datasets/${encodeURIComponent(datasetId)}/preview`;
203
+ const params = new URLSearchParams();
204
+ if (config) params.append('config', config);
205
+ if (split) params.append('split', split);
206
+ if (params.toString()) url += `?${params}`;
207
+ return utils.fetchAPI(url);
208
+ },
209
+
210
+ // System
211
+ async getSystemInfo() {
212
+ return utils.fetchAPI('/system/info');
213
+ },
214
+
215
+ async getResourceUsage() {
216
+ return utils.fetchAPI('/system/resources');
217
+ },
218
+
219
+ async clearCache() {
220
+ return utils.fetchAPI('/system/cache/clear', { method: 'POST' });
221
+ }
222
+ };
223
+
224
+ // UI Components
225
+ const ui = {
226
+ // Dashboard Stats
227
+ async loadDashboardStats() {
228
+ try {
229
+ const [stats, system] = await Promise.all([
230
+ api.getJobStatistics(),
231
+ api.getResourceUsage()
232
+ ]);
233
+
234
+ document.getElementById('totalJobs').textContent = stats.total_jobs || 0;
235
+ document.getElementById('runningJobs').textContent = stats.running_jobs || 0;
236
+ document.getElementById('completedJobs').textContent = stats.completed_jobs || 0;
237
+ document.getElementById('failedJobs').textContent = stats.failed_jobs || 0;
238
+
239
+ // Update resource bars
240
+ if (system.cpu_percent !== undefined) {
241
+ document.getElementById('cpuUsage').style.width = `${system.cpu_percent}%`;
242
+ document.getElementById('cpuUsageText').textContent = `${system.cpu_percent.toFixed(1)}%`;
243
+ }
244
+ if (system.memory_percent !== undefined) {
245
+ document.getElementById('memoryUsage').style.width = `${system.memory_percent}%`;
246
+ document.getElementById('memoryUsageText').textContent = `${system.memory_percent.toFixed(1)}%`;
247
+ }
248
+ if (system.disk_percent !== undefined) {
249
+ document.getElementById('diskUsage').style.width = `${system.disk_percent}%`;
250
+ document.getElementById('diskUsageText').textContent = `${system.disk_percent.toFixed(1)}%`;
251
+ }
252
+ } catch (error) {
253
+ console.error('Error loading stats:', error);
254
+ }
255
+ },
256
+
257
+ // Jobs Table
258
+ async loadJobsTable() {
259
+ try {
260
+ const response = await api.getJobs();
261
+ state.jobs = response.jobs || [];
262
+ this.renderJobsTable();
263
+ } catch (error) {
264
+ utils.showToast('Failed to load jobs', 'danger');
265
+ }
266
+ },
267
+
268
+ renderJobsTable() {
269
+ const tbody = document.getElementById('jobsTableBody');
270
+ if (!tbody) return;
271
+
272
+ if (state.jobs.length === 0) {
273
+ tbody.innerHTML = `
274
+ <tr>
275
+ <td colspan="6" class="text-center text-muted py-5">
276
+ <i class="bi bi-inbox fs-1 d-block mb-2"></i>
277
+ No training jobs yet. Start a new training to see it here.
278
+ </td>
279
+ </tr>
280
+ `;
281
+ return;
282
+ }
283
+
284
+ tbody.innerHTML = state.jobs.map(job => `
285
+ <tr onclick="ui.showJobDetail('${job.id}')" style="cursor: pointer;">
286
+ <td>
287
+ <span class="fw-medium">${job.id.substring(0, 8)}...</span>
288
+ </td>
289
+ <td>
290
+ <span class="badge bg-secondary">${job.task_type}</span>
291
+ </td>
292
+ <td>
293
+ <div class="small">${job.model_name}</div>
294
+ <div class="text-muted small">${job.dataset_name}</div>
295
+ </td>
296
+ <td>
297
+ <span class="badge ${utils.getStatusBadgeClass(job.status)}">${job.status}</span>
298
+ </td>
299
+ <td>
300
+ ${job.progress !== undefined ? `
301
+ <div class="progress" style="height: 6px;">
302
+ <div class="progress-bar" style="width: ${job.progress}%"></div>
303
+ </div>
304
+ <small class="text-muted">${job.progress.toFixed(1)}%</small>
305
+ ` : '-'}
306
+ </td>
307
+ <td>${utils.formatDate(job.created_at)}</td>
308
+ <td>
309
+ <div class="btn-group btn-group-sm">
310
+ ${job.status === 'running' ? `
311
+ <button class="btn btn-outline-warning" onclick="event.stopPropagation(); ui.pauseJob('${job.id}')" title="Pause">
312
+ <i class="bi bi-pause"></i>
313
+ </button>
314
+ ` : ''}
315
+ ${job.status === 'paused' ? `
316
+ <button class="btn btn-outline-success" onclick="event.stopPropagation(); ui.resumeJob('${job.id}')" title="Resume">
317
+ <i class="bi bi-play"></i>
318
+ </button>
319
+ ` : ''}
320
+ ${job.status === 'queued' || job.status === 'running' ? `
321
+ <button class="btn btn-outline-danger" onclick="event.stopPropagation(); ui.cancelJob('${job.id}')" title="Cancel">
322
+ <i class="bi bi-x"></i>
323
+ </button>
324
+ ` : ''}
325
+ ${job.status === 'failed' ? `
326
+ <button class="btn btn-outline-primary" onclick="event.stopPropagation(); ui.retryJob('${job.id}')" title="Retry">
327
+ <i class="bi bi-arrow-clockwise"></i>
328
+ </button>
329
+ ` : ''}
330
+ </div>
331
+ </td>
332
+ </tr>
333
+ `).join('');
334
+ },
335
+
336
+ async showJobDetail(jobId) {
337
+ try {
338
+ const job = await api.getJob(jobId);
339
+ state.currentJob = job;
340
+
341
+ const modal = new bootstrap.Modal(document.getElementById('jobDetailModal'));
342
+
343
+ // Populate modal content
344
+ document.getElementById('jobDetailTitle').textContent = `Job ${job.id.substring(0, 8)}`;
345
+ document.getElementById('jobDetailStatus').innerHTML = `
346
+ <span class="badge ${utils.getStatusBadgeClass(job.status)}">${job.status}</span>
347
+ `;
348
+ document.getElementById('jobDetailModel').textContent = job.model_name;
349
+ document.getElementById('jobDetailDataset').textContent = job.dataset_name;
350
+ document.getElementById('jobDetailTask').textContent = job.task_type;
351
+ document.getElementById('jobDetailProgress').style.width = `${job.progress || 0}%`;
352
+ document.getElementById('jobDetailProgressText').textContent = `${(job.progress || 0).toFixed(1)}%`;
353
+ document.getElementById('jobDetailEpochs').textContent = `${job.current_epoch || 0}/${job.total_epochs || 0}`;
354
+ document.getElementById('jobDetailLoss').textContent = job.current_loss?.toFixed(4) || 'N/A';
355
+ document.getElementById('jobDetailLR').textContent = job.learning_rate || 'N/A';
356
+ document.getElementById('jobDetailCreated').textContent = utils.formatDate(job.created_at);
357
+ document.getElementById('jobDetailStarted').textContent = utils.formatDate(job.started_at);
358
+ document.getElementById('jobDetailFinished').textContent = utils.formatDate(job.finished_at);
359
+ document.getElementById('jobDetailDuration').textContent = utils.formatDuration(job.duration);
360
+
361
+ // Load logs
362
+ const logs = await api.getJobLogs(jobId, 50);
363
+ const logsContainer = document.getElementById('jobDetailLogs');
364
+ logsContainer.innerHTML = logs.logs?.map(log => `
365
+ <div class="log-entry log-${log.level?.toLowerCase() || 'info'}">
366
+ <span class="text-muted">[${new Date(log.timestamp).toLocaleTimeString()}]</span> ${log.message}
367
+ </div>
368
+ `).join('') || '<div class="text-muted">No logs available</div>';
369
+
370
+ modal.show();
371
+ } catch (error) {
372
+ utils.showToast('Failed to load job details', 'danger');
373
+ }
374
+ },
375
+
376
+ async cancelJob(jobId) {
377
+ if (confirm('Are you sure you want to cancel this job?')) {
378
+ try {
379
+ await api.cancelJob(jobId);
380
+ utils.showToast('Job cancelled', 'warning');
381
+ await this.loadJobsTable();
382
+ } catch (error) {
383
+ utils.showToast('Failed to cancel job', 'danger');
384
+ }
385
+ }
386
+ },
387
+
388
+ async pauseJob(jobId) {
389
+ try {
390
+ await api.pauseJob(jobId);
391
+ utils.showToast('Job paused', 'info');
392
+ await this.loadJobsTable();
393
+ } catch (error) {
394
+ utils.showToast('Failed to pause job', 'danger');
395
+ }
396
+ },
397
+
398
+ async resumeJob(jobId) {
399
+ try {
400
+ await api.resumeJob(jobId);
401
+ utils.showToast('Job resumed', 'success');
402
+ await this.loadJobsTable();
403
+ } catch (error) {
404
+ utils.showToast('Failed to resume job', 'danger');
405
+ }
406
+ },
407
+
408
+ async retryJob(jobId) {
409
+ if (confirm('Retry this job?')) {
410
+ try {
411
+ await api.retryJob(jobId);
412
+ utils.showToast('Job retry started', 'info');
413
+ await this.loadJobsTable();
414
+ } catch (error) {
415
+ utils.showToast('Failed to retry job', 'danger');
416
+ }
417
+ }
418
+ },
419
+
420
+ // Model Search
421
+ async searchModels(query) {
422
+ if (!query || query.length < 2) return;
423
+
424
+ const task = document.getElementById('taskType')?.value;
425
+ try {
426
+ const results = await api.searchModels(query, task);
427
+ state.searchResults.models = results.models || [];
428
+ this.renderModelResults();
429
+ } catch (error) {
430
+ utils.showToast('Model search failed', 'danger');
431
+ }
432
+ },
433
+
434
+ renderModelResults() {
435
+ const container = document.getElementById('modelResults');
436
+ if (!container) return;
437
+
438
+ container.innerHTML = state.searchResults.models.map(model => `
439
+ <div class="search-result" onclick="ui.selectModel('${model.id}')">
440
+ <div class="result-title">${model.id}</div>
441
+ <div class="result-meta">
442
+ <span class="me-3"><i class="bi bi-download"></i> ${utils.formatNumber(model.downloads)}</span>
443
+ <span class="me-3"><i class="bi bi-heart"></i> ${utils.formatNumber(model.likes)}</span>
444
+ <span><i class="bi bi-tag"></i> ${model.pipeline_tag || model.task}</span>
445
+ </div>
446
+ <small class="text-muted">${model.description?.substring(0, 100) || ''}...</small>
447
+ </div>
448
+ `).join('') || '<div class="text-muted text-center py-3">No models found</div>';
449
+ },
450
+
451
+ selectModel(modelId) {
452
+ document.getElementById('modelName').value = modelId;
453
+ document.getElementById('modelResults').style.display = 'none';
454
+ utils.showToast(`Selected model: ${modelId}`, 'success');
455
+ },
456
+
457
+ // Dataset Search
458
+ async searchDatasets(query) {
459
+ if (!query || query.length < 2) return;
460
+
461
+ try {
462
+ const results = await api.searchDatasets(query);
463
+ state.searchResults.datasets = results.datasets || [];
464
+ this.renderDatasetResults();
465
+ } catch (error) {
466
+ utils.showToast('Dataset search failed', 'danger');
467
+ }
468
+ },
469
+
470
+ renderDatasetResults() {
471
+ const container = document.getElementById('datasetResults');
472
+ if (!container) return;
473
+
474
+ container.innerHTML = state.searchResults.datasets.map(dataset => `
475
+ <div class="search-result" onclick="ui.selectDataset('${dataset.id}')">
476
+ <div class="result-title">${dataset.id}</div>
477
+ <div class="result-meta">
478
+ <span class="me-3"><i class="bi bi-download"></i> ${utils.formatNumber(dataset.downloads)}</span>
479
+ <span class="me-3"><i class="bi bi-heart"></i> ${utils.formatNumber(dataset.likes)}</span>
480
+ </div>
481
+ <small class="text-muted">${dataset.description?.substring(0, 100) || ''}...</small>
482
+ </div>
483
+ `).join('') || '<div class="text-muted text-center py-3">No datasets found</div>';
484
+ },
485
+
486
+ selectDataset(datasetId) {
487
+ document.getElementById('datasetName').value = datasetId;
488
+ document.getElementById('datasetResults').style.display = 'none';
489
+ utils.showToast(`Selected dataset: ${datasetId}`, 'success');
490
+ },
491
+
492
+ // Training Form
493
+ async submitTrainingForm() {
494
+ const form = document.getElementById('trainingForm');
495
+ const formData = new FormData(form);
496
+
497
+ const config = {
498
+ model_name: formData.get('modelName'),
499
+ dataset_config: {
500
+ dataset_name: formData.get('datasetName'),
501
+ split: formData.get('split') || 'train',
502
+ text_column: formData.get('textColumn'),
503
+ label_column: formData.get('labelColumn'),
504
+ },
505
+ training_args: {
506
+ task_type: formData.get('taskType'),
507
+ epochs: parseInt(formData.get('epochs')) || 3,
508
+ batch_size: parseInt(formData.get('batchSize')) || 8,
509
+ learning_rate: parseFloat(formData.get('learningRate')) || 5e-5,
510
+ max_length: parseInt(formData.get('maxLength')) || 512,
511
+ warmup_steps: parseInt(formData.get('warmupSteps')) || 0,
512
+ weight_decay: parseFloat(formData.get('weightDecay')) || 0.01,
513
+ },
514
+ peft_config: {
515
+ use_peft: formData.get('usePeft') === 'on',
516
+ lora_r: parseInt(formData.get('loraR')) || 8,
517
+ lora_alpha: parseInt(formData.get('loraAlpha')) || 16,
518
+ lora_dropout: parseFloat(formData.get('loraDropout')) || 0.05,
519
+ },
520
+ output_config: {
521
+ output_dir: formData.get('outputDir') || 'output',
522
+ push_to_hub: formData.get('pushToHub') === 'on',
523
+ hub_repo_name: formData.get('hubRepoName'),
524
+ }
525
+ };
526
+
527
+ try {
528
+ utils.showToast('Starting training...', 'info');
529
+ const result = await api.startTraining(config);
530
+ utils.showToast(`Training started: ${result.job_id}`, 'success');
531
+ form.reset();
532
+ await this.loadJobsTable();
533
+ await this.loadDashboardStats();
534
+ } catch (error) {
535
+ utils.showToast(`Failed to start training: ${error.message}`, 'danger');
536
+ }
537
+ },
538
+
539
+ // System Status
540
+ async loadSystemStatus() {
541
+ try {
542
+ const info = await api.getSystemInfo();
543
+ const container = document.getElementById('systemInfo');
544
+ if (container) {
545
+ container.innerHTML = `
546
+ <div class="row">
547
+ <div class="col-md-6">
548
+ <p><strong>Platform:</strong> ${info.platform || 'N/A'}</p>
549
+ <p><strong>Python:</strong> ${info.python_version || 'N/A'}</p>
550
+ <p><strong>CUDA Available:</strong> ${info.cuda_available ? 'Yes' : 'No'}</p>
551
+ </div>
552
+ <div class="col-md-6">
553
+ <p><strong>Transformers:</strong> ${info.transformers_version || 'N/A'}</p>
554
+ <p><strong>Torch:</strong> ${info.torch_version || 'N/A'}</p>
555
+ <p><strong>GPU:</strong> ${info.gpu_info || 'None'}</p>
556
+ </div>
557
+ </div>
558
+ `;
559
+ }
560
+ } catch (error) {
561
+ console.error('Failed to load system info:', error);
562
+ }
563
+ },
564
+
565
+ async clearCache() {
566
+ if (confirm('Clear all cached models and datasets?')) {
567
+ try {
568
+ await api.clearCache();
569
+ utils.showToast('Cache cleared', 'success');
570
+ await this.loadSystemStatus();
571
+ } catch (error) {
572
+ utils.showToast('Failed to clear cache', 'danger');
573
+ }
574
+ }
575
+ }
576
+ };
577
+
578
+ // Event Listeners
579
+ document.addEventListener('DOMContentLoaded', () => {
580
+ // Load initial data
581
+ ui.loadDashboardStats();
582
+ ui.loadJobsTable();
583
+ ui.loadSystemStatus();
584
+
585
+ // Auto-refresh
586
+ setInterval(() => {
587
+ ui.loadDashboardStats();
588
+ ui.loadJobsTable();
589
+ }, 30000);
590
+
591
+ // Model search with debounce
592
+ const modelInput = document.getElementById('modelName');
593
+ if (modelInput) {
594
+ modelInput.addEventListener('input', utils.debounce((e) => {
595
+ ui.searchModels(e.target.value);
596
+ }, 300));
597
+ modelInput.addEventListener('focus', () => {
598
+ document.getElementById('modelResults').style.display = 'block';
599
+ });
600
+ }
601
+
602
+ // Dataset search with debounce
603
+ const datasetInput = document.getElementById('datasetName');
604
+ if (datasetInput) {
605
+ datasetInput.addEventListener('input', utils.debounce((e) => {
606
+ ui.searchDatasets(e.target.value);
607
+ }, 300));
608
+ datasetInput.addEventListener('focus', () => {
609
+ document.getElementById('datasetResults').style.display = 'block';
610
+ });
611
+ }
612
+
613
+ // Training form submission
614
+ const trainingForm = document.getElementById('trainingForm');
615
+ if (trainingForm) {
616
+ trainingForm.addEventListener('submit', (e) => {
617
+ e.preventDefault();
618
+ ui.submitTrainingForm();
619
+ });
620
+ }
621
+
622
+ // Close search results on outside click
623
+ document.addEventListener('click', (e) => {
624
+ if (!e.target.closest('#modelName') && !e.target.closest('#modelResults')) {
625
+ document.getElementById('modelResults')?.style.display = 'none';
626
+ }
627
+ if (!e.target.closest('#datasetName') && !e.target.closest('#datasetResults')) {
628
+ document.getElementById('datasetResults')?.style.display = 'none';
629
+ }
630
+ });
631
+ });
632
+
633
+ // Export for global access
634
+ window.ui = ui;
635
+ window.api = api;
636
+ window.utils = utils;