| |
| |
| |
|
|
| |
| const API_BASE = '/api'; |
|
|
| |
| const state = { |
| jobs: [], |
| models: [], |
| datasets: [], |
| systemJob: null, |
| searchResults: { |
| models: [], |
| datasets: [] |
| } |
| }; |
|
|
| |
| const utils = { |
| async fetchAPI(endpoint, options = {}) { |
| try { |
| const response = await fetch(`${API_BASE}${endpoint}`, { |
| headers: { |
| 'Content-Type': 'application/json', |
| ...options.headers |
| }, |
| ...options |
| }); |
| |
| if (!response.ok) { |
| const error = await response.json().catch(() => ({ detail: response.statusText })); |
| throw new Error(error.detail || 'Request failed'); |
| } |
| |
| return await response.json(); |
| } catch (error) { |
| console.error('API Error:', error); |
| throw error; |
| } |
| }, |
|
|
| formatDate(dateStr) { |
| if (!dateStr) return 'N/A'; |
| return new Date(dateStr).toLocaleString(); |
| }, |
|
|
| formatDuration(seconds) { |
| if (!seconds) return 'N/A'; |
| const h = Math.floor(seconds / 3600); |
| const m = Math.floor((seconds % 3600) / 60); |
| const s = Math.floor(seconds % 60); |
| return h > 0 ? `${h}h ${m}m ${s}s` : m > 0 ? `${m}m ${s}s` : `${s}s`; |
| }, |
|
|
| formatNumber(num) { |
| if (num >= 1e9) return (num / 1e9).toFixed(2) + 'B'; |
| if (num >= 1e6) return (num / 1e6).toFixed(2) + 'M'; |
| if (num >= 1e3) return (num / 1e3).toFixed(2) + 'K'; |
| return num?.toString() || '0'; |
| }, |
|
|
| getStatusBadgeClass(status) { |
| const classes = { |
| 'queued': 'badge-queued', |
| 'running': 'badge-running', |
| 'completed': 'badge-completed', |
| 'failed': 'badge-failed', |
| 'cancelled': 'badge-cancelled', |
| 'paused': 'badge-paused' |
| }; |
| return classes[status?.toLowerCase()] || 'badge-queued'; |
| }, |
|
|
| showToast(message, type = 'info') { |
| const toastContainer = document.getElementById('toastContainer') || createToastContainer(); |
| const toast = document.createElement('div'); |
| toast.className = `alert alert-${type} alert-dismissible fade show`; |
| toast.innerHTML = ` |
| ${message} |
| <button type="button" class="btn-close" data-bs-dismiss="alert"></button> |
| `; |
| toastContainer.appendChild(toast); |
| setTimeout(() => toast.remove(), 5000); |
| }, |
|
|
| debounce(func, wait) { |
| let timeout; |
| return function executedFunction(...args) { |
| const later = () => { |
| clearTimeout(timeout); |
| func(...args); |
| }; |
| clearTimeout(timeout); |
| timeout = setTimeout(later, wait); |
| }; |
| } |
| }; |
|
|
| function createToastContainer() { |
| const container = document.createElement('div'); |
| container.id = 'toastContainer'; |
| container.style.cssText = 'position: fixed; top: 80px; right: 20px; z-index: 9999; max-width: 400px;'; |
| document.body.appendChild(container); |
| return container; |
| } |
|
|
| |
| const api = { |
| |
| async getJobs(filters = {}) { |
| const params = new URLSearchParams(filters).toString(); |
| return utils.fetchAPI(`/jobs?${params}`); |
| }, |
|
|
| async getJob(jobId) { |
| return utils.fetchAPI(`/jobs/${jobId}`); |
| }, |
|
|
| async getActiveJobs() { |
| return utils.fetchAPI('/jobs/active'); |
| }, |
|
|
| async getJobStatistics() { |
| return utils.fetchAPI('/jobs/statistics'); |
| }, |
|
|
| async cancelJob(jobId) { |
| return utils.fetchAPI(`/jobs/${jobId}/cancel`, { method: 'POST' }); |
| }, |
|
|
| async pauseJob(jobId) { |
| return utils.fetchAPI(`/jobs/${jobId}/pause`, { method: 'POST' }); |
| }, |
|
|
| async resumeJob(jobId) { |
| return utils.fetchAPI(`/jobs/${jobId}/resume`, { method: 'POST' }); |
| }, |
|
|
| async retryJob(jobId) { |
| return utils.fetchAPI(`/jobs/${jobId}/retry`, { method: 'POST' }); |
| }, |
|
|
| async deleteJob(jobId) { |
| return utils.fetchAPI(`/jobs/${jobId}`, { method: 'DELETE' }); |
| }, |
|
|
| async getJobLogs(jobId, limit = 100) { |
| return utils.fetchAPI(`/jobs/${jobId}/logs?limit=${limit}`); |
| }, |
|
|
| async getJobCheckpoints(jobId) { |
| return utils.fetchAPI(`/jobs/${jobId}/checkpoints`); |
| }, |
|
|
| |
| async startTraining(config) { |
| return utils.fetchAPI('/training/start', { |
| method: 'POST', |
| body: JSON.stringify(config) |
| }); |
| }, |
|
|
| async getTrainingTemplates() { |
| return utils.fetchAPI('/training/templates'); |
| }, |
|
|
| |
| async searchModels(query, task = null, limit = 20) { |
| const params = new URLSearchParams({ query, limit }); |
| if (task) params.append('task', task); |
| return utils.fetchAPI(`/models/search?${params}`); |
| }, |
|
|
| async getModelInfo(modelId) { |
| return utils.fetchAPI(`/models/${encodeURIComponent(modelId)}`); |
| }, |
|
|
| async getModelRecommendations(task) { |
| return utils.fetchAPI(`/models/recommendations/${task}`); |
| }, |
|
|
| async checkModelCompatibility(modelId, task) { |
| return utils.fetchAPI(`/models/${encodeURIComponent(modelId)}/compatibility?task=${task}`); |
| }, |
|
|
| |
| async searchDatasets(query, limit = 20) { |
| return utils.fetchAPI(`/datasets/search?query=${encodeURIComponent(query)}&limit=${limit}`); |
| }, |
|
|
| async getDatasetInfo(datasetId) { |
| return utils.fetchAPI(`/datasets/${encodeURIComponent(datasetId)}`); |
| }, |
|
|
| async getDatasetRecommendations(task) { |
| return utils.fetchAPI(`/datasets/recommendations/${task}`); |
| }, |
|
|
| async getDatasetPreview(datasetId, config = null, split = null) { |
| let url = `/datasets/${encodeURIComponent(datasetId)}/preview`; |
| const params = new URLSearchParams(); |
| if (config) params.append('config', config); |
| if (split) params.append('split', split); |
| if (params.toString()) url += `?${params}`; |
| return utils.fetchAPI(url); |
| }, |
|
|
| |
| async getSystemInfo() { |
| return utils.fetchAPI('/system/info'); |
| }, |
|
|
| async getResourceUsage() { |
| return utils.fetchAPI('/system/resources'); |
| }, |
|
|
| async clearCache() { |
| return utils.fetchAPI('/system/cache/clear', { method: 'POST' }); |
| } |
| }; |
|
|
| |
| const ui = { |
| |
| async loadDashboardStats() { |
| try { |
| const [stats, system] = await Promise.all([ |
| api.getJobStatistics(), |
| api.getResourceUsage() |
| ]); |
|
|
| document.getElementById('totalJobs').textContent = stats.total_jobs || 0; |
| document.getElementById('runningJobs').textContent = stats.running_jobs || 0; |
| document.getElementById('completedJobs').textContent = stats.completed_jobs || 0; |
| document.getElementById('failedJobs').textContent = stats.failed_jobs || 0; |
|
|
| |
| if (system.cpu_percent !== undefined) { |
| document.getElementById('cpuUsage').style.width = `${system.cpu_percent}%`; |
| document.getElementById('cpuUsageText').textContent = `${system.cpu_percent.toFixed(1)}%`; |
| } |
| if (system.memory_percent !== undefined) { |
| document.getElementById('memoryUsage').style.width = `${system.memory_percent}%`; |
| document.getElementById('memoryUsageText').textContent = `${system.memory_percent.toFixed(1)}%`; |
| } |
| if (system.disk_percent !== undefined) { |
| document.getElementById('diskUsage').style.width = `${system.disk_percent}%`; |
| document.getElementById('diskUsageText').textContent = `${system.disk_percent.toFixed(1)}%`; |
| } |
| } catch (error) { |
| console.error('Error loading stats:', error); |
| } |
| }, |
|
|
| |
| async loadJobsTable() { |
| try { |
| const response = await api.getJobs(); |
| state.jobs = response.jobs || []; |
| this.renderJobsTable(); |
| } catch (error) { |
| utils.showToast('Failed to load jobs', 'danger'); |
| } |
| }, |
|
|
| renderJobsTable() { |
| const tbody = document.getElementById('jobsTableBody'); |
| if (!tbody) return; |
|
|
| if (state.jobs.length === 0) { |
| tbody.innerHTML = ` |
| <tr> |
| <td colspan="6" class="text-center text-muted py-5"> |
| <i class="bi bi-inbox fs-1 d-block mb-2"></i> |
| No training jobs yet. Start a new training to see it here. |
| </td> |
| </tr> |
| `; |
| return; |
| } |
|
|
| tbody.innerHTML = state.jobs.map(job => ` |
| <tr onclick="ui.showJobDetail('${job.id}')" style="cursor: pointer;"> |
| <td> |
| <span class="fw-medium">${job.id.substring(0, 8)}...</span> |
| </td> |
| <td> |
| <span class="badge bg-secondary">${job.task_type}</span> |
| </td> |
| <td> |
| <div class="small">${job.model_name}</div> |
| <div class="text-muted small">${job.dataset_name}</div> |
| </td> |
| <td> |
| <span class="badge ${utils.getStatusBadgeClass(job.status)}">${job.status}</span> |
| </td> |
| <td> |
| ${job.progress !== undefined ? ` |
| <div class="progress" style="height: 6px;"> |
| <div class="progress-bar" style="width: ${job.progress}%"></div> |
| </div> |
| <small class="text-muted">${job.progress.toFixed(1)}%</small> |
| ` : '-'} |
| </td> |
| <td>${utils.formatDate(job.created_at)}</td> |
| <td> |
| <div class="btn-group btn-group-sm"> |
| ${job.status === 'running' ? ` |
| <button class="btn btn-outline-warning" onclick="event.stopPropagation(); ui.pauseJob('${job.id}')" title="Pause"> |
| <i class="bi bi-pause"></i> |
| </button> |
| ` : ''} |
| ${job.status === 'paused' ? ` |
| <button class="btn btn-outline-success" onclick="event.stopPropagation(); ui.resumeJob('${job.id}')" title="Resume"> |
| <i class="bi bi-play"></i> |
| </button> |
| ` : ''} |
| ${job.status === 'queued' || job.status === 'running' ? ` |
| <button class="btn btn-outline-danger" onclick="event.stopPropagation(); ui.cancelJob('${job.id}')" title="Cancel"> |
| <i class="bi bi-x"></i> |
| </button> |
| ` : ''} |
| ${job.status === 'failed' ? ` |
| <button class="btn btn-outline-primary" onclick="event.stopPropagation(); ui.retryJob('${job.id}')" title="Retry"> |
| <i class="bi bi-arrow-clockwise"></i> |
| </button> |
| ` : ''} |
| </div> |
| </td> |
| </tr> |
| `).join(''); |
| }, |
|
|
| async showJobDetail(jobId) { |
| try { |
| const job = await api.getJob(jobId); |
| state.currentJob = job; |
|
|
| const modal = new bootstrap.Modal(document.getElementById('jobDetailModal')); |
| |
| |
| document.getElementById('jobDetailTitle').textContent = `Job ${job.id.substring(0, 8)}`; |
| document.getElementById('jobDetailStatus').innerHTML = ` |
| <span class="badge ${utils.getStatusBadgeClass(job.status)}">${job.status}</span> |
| `; |
| document.getElementById('jobDetailModel').textContent = job.model_name; |
| document.getElementById('jobDetailDataset').textContent = job.dataset_name; |
| document.getElementById('jobDetailTask').textContent = job.task_type; |
| document.getElementById('jobDetailProgress').style.width = `${job.progress || 0}%`; |
| document.getElementById('jobDetailProgressText').textContent = `${(job.progress || 0).toFixed(1)}%`; |
| document.getElementById('jobDetailEpochs').textContent = `${job.current_epoch || 0}/${job.total_epochs || 0}`; |
| document.getElementById('jobDetailLoss').textContent = job.current_loss?.toFixed(4) || 'N/A'; |
| document.getElementById('jobDetailLR').textContent = job.learning_rate || 'N/A'; |
| document.getElementById('jobDetailCreated').textContent = utils.formatDate(job.created_at); |
| document.getElementById('jobDetailStarted').textContent = utils.formatDate(job.started_at); |
| document.getElementById('jobDetailFinished').textContent = utils.formatDate(job.finished_at); |
| document.getElementById('jobDetailDuration').textContent = utils.formatDuration(job.duration); |
|
|
| |
| const logs = await api.getJobLogs(jobId, 50); |
| const logsContainer = document.getElementById('jobDetailLogs'); |
| logsContainer.innerHTML = logs.logs?.map(log => ` |
| <div class="log-entry log-${log.level?.toLowerCase() || 'info'}"> |
| <span class="text-muted">[${new Date(log.timestamp).toLocaleTimeString()}]</span> ${log.message} |
| </div> |
| `).join('') || '<div class="text-muted">No logs available</div>'; |
|
|
| modal.show(); |
| } catch (error) { |
| utils.showToast('Failed to load job details', 'danger'); |
| } |
| }, |
|
|
| async cancelJob(jobId) { |
| if (confirm('Are you sure you want to cancel this job?')) { |
| try { |
| await api.cancelJob(jobId); |
| utils.showToast('Job cancelled', 'warning'); |
| await this.loadJobsTable(); |
| } catch (error) { |
| utils.showToast('Failed to cancel job', 'danger'); |
| } |
| } |
| }, |
|
|
| async pauseJob(jobId) { |
| try { |
| await api.pauseJob(jobId); |
| utils.showToast('Job paused', 'info'); |
| await this.loadJobsTable(); |
| } catch (error) { |
| utils.showToast('Failed to pause job', 'danger'); |
| } |
| }, |
|
|
| async resumeJob(jobId) { |
| try { |
| await api.resumeJob(jobId); |
| utils.showToast('Job resumed', 'success'); |
| await this.loadJobsTable(); |
| } catch (error) { |
| utils.showToast('Failed to resume job', 'danger'); |
| } |
| }, |
|
|
| async retryJob(jobId) { |
| if (confirm('Retry this job?')) { |
| try { |
| await api.retryJob(jobId); |
| utils.showToast('Job retry started', 'info'); |
| await this.loadJobsTable(); |
| } catch (error) { |
| utils.showToast('Failed to retry job', 'danger'); |
| } |
| } |
| }, |
|
|
| |
| async searchModels(query) { |
| if (!query || query.length < 2) return; |
|
|
| const task = document.getElementById('taskType')?.value; |
| try { |
| const results = await api.searchModels(query, task); |
| state.searchResults.models = results.models || []; |
| this.renderModelResults(); |
| } catch (error) { |
| utils.showToast('Model search failed', 'danger'); |
| } |
| }, |
|
|
| renderModelResults() { |
| const container = document.getElementById('modelResults'); |
| if (!container) return; |
|
|
| container.innerHTML = state.searchResults.models.map(model => ` |
| <div class="search-result" onclick="ui.selectModel('${model.id}')"> |
| <div class="result-title">${model.id}</div> |
| <div class="result-meta"> |
| <span class="me-3"><i class="bi bi-download"></i> ${utils.formatNumber(model.downloads)}</span> |
| <span class="me-3"><i class="bi bi-heart"></i> ${utils.formatNumber(model.likes)}</span> |
| <span><i class="bi bi-tag"></i> ${model.pipeline_tag || model.task}</span> |
| </div> |
| <small class="text-muted">${model.description?.substring(0, 100) || ''}...</small> |
| </div> |
| `).join('') || '<div class="text-muted text-center py-3">No models found</div>'; |
| }, |
|
|
| selectModel(modelId) { |
| document.getElementById('modelName').value = modelId; |
| document.getElementById('modelResults').style.display = 'none'; |
| utils.showToast(`Selected model: ${modelId}`, 'success'); |
| }, |
|
|
| |
| async searchDatasets(query) { |
| if (!query || query.length < 2) return; |
|
|
| try { |
| const results = await api.searchDatasets(query); |
| state.searchResults.datasets = results.datasets || []; |
| this.renderDatasetResults(); |
| } catch (error) { |
| utils.showToast('Dataset search failed', 'danger'); |
| } |
| }, |
|
|
| renderDatasetResults() { |
| const container = document.getElementById('datasetResults'); |
| if (!container) return; |
|
|
| container.innerHTML = state.searchResults.datasets.map(dataset => ` |
| <div class="search-result" onclick="ui.selectDataset('${dataset.id}')"> |
| <div class="result-title">${dataset.id}</div> |
| <div class="result-meta"> |
| <span class="me-3"><i class="bi bi-download"></i> ${utils.formatNumber(dataset.downloads)}</span> |
| <span class="me-3"><i class="bi bi-heart"></i> ${utils.formatNumber(dataset.likes)}</span> |
| </div> |
| <small class="text-muted">${dataset.description?.substring(0, 100) || ''}...</small> |
| </div> |
| `).join('') || '<div class="text-muted text-center py-3">No datasets found</div>'; |
| }, |
|
|
| selectDataset(datasetId) { |
| document.getElementById('datasetName').value = datasetId; |
| document.getElementById('datasetResults').style.display = 'none'; |
| utils.showToast(`Selected dataset: ${datasetId}`, 'success'); |
| }, |
|
|
| |
| async submitTrainingForm() { |
| const form = document.getElementById('trainingForm'); |
| const formData = new FormData(form); |
|
|
| const config = { |
| model_name: formData.get('modelName'), |
| dataset_config: { |
| dataset_name: formData.get('datasetName'), |
| split: formData.get('split') || 'train', |
| text_column: formData.get('textColumn'), |
| label_column: formData.get('labelColumn'), |
| }, |
| training_args: { |
| task_type: formData.get('taskType'), |
| epochs: parseInt(formData.get('epochs')) || 3, |
| batch_size: parseInt(formData.get('batchSize')) || 8, |
| learning_rate: parseFloat(formData.get('learningRate')) || 5e-5, |
| max_length: parseInt(formData.get('maxLength')) || 512, |
| warmup_steps: parseInt(formData.get('warmupSteps')) || 0, |
| weight_decay: parseFloat(formData.get('weightDecay')) || 0.01, |
| }, |
| peft_config: { |
| use_peft: formData.get('usePeft') === 'on', |
| lora_r: parseInt(formData.get('loraR')) || 8, |
| lora_alpha: parseInt(formData.get('loraAlpha')) || 16, |
| lora_dropout: parseFloat(formData.get('loraDropout')) || 0.05, |
| }, |
| output_config: { |
| output_dir: formData.get('outputDir') || 'output', |
| push_to_hub: formData.get('pushToHub') === 'on', |
| hub_repo_name: formData.get('hubRepoName'), |
| } |
| }; |
|
|
| try { |
| utils.showToast('Starting training...', 'info'); |
| const result = await api.startTraining(config); |
| utils.showToast(`Training started: ${result.job_id}`, 'success'); |
| form.reset(); |
| await this.loadJobsTable(); |
| await this.loadDashboardStats(); |
| } catch (error) { |
| utils.showToast(`Failed to start training: ${error.message}`, 'danger'); |
| } |
| }, |
|
|
| |
| async loadSystemStatus() { |
| try { |
| const info = await api.getSystemInfo(); |
| const container = document.getElementById('systemInfo'); |
| if (container) { |
| container.innerHTML = ` |
| <div class="row"> |
| <div class="col-md-6"> |
| <p><strong>Platform:</strong> ${info.platform || 'N/A'}</p> |
| <p><strong>Python:</strong> ${info.python_version || 'N/A'}</p> |
| <p><strong>CUDA Available:</strong> ${info.cuda_available ? 'Yes' : 'No'}</p> |
| </div> |
| <div class="col-md-6"> |
| <p><strong>Transformers:</strong> ${info.transformers_version || 'N/A'}</p> |
| <p><strong>Torch:</strong> ${info.torch_version || 'N/A'}</p> |
| <p><strong>GPU:</strong> ${info.gpu_info || 'None'}</p> |
| </div> |
| </div> |
| `; |
| } |
| } catch (error) { |
| console.error('Failed to load system info:', error); |
| } |
| }, |
|
|
| async clearCache() { |
| if (confirm('Clear all cached models and datasets?')) { |
| try { |
| await api.clearCache(); |
| utils.showToast('Cache cleared', 'success'); |
| await this.loadSystemStatus(); |
| } catch (error) { |
| utils.showToast('Failed to clear cache', 'danger'); |
| } |
| } |
| } |
| }; |
|
|
| |
| document.addEventListener('DOMContentLoaded', () => { |
| |
| ui.loadDashboardStats(); |
| ui.loadJobsTable(); |
| ui.loadSystemStatus(); |
|
|
| |
| setInterval(() => { |
| ui.loadDashboardStats(); |
| ui.loadJobsTable(); |
| }, 30000); |
|
|
| |
| const modelInput = document.getElementById('modelName'); |
| if (modelInput) { |
| modelInput.addEventListener('input', utils.debounce((e) => { |
| ui.searchModels(e.target.value); |
| }, 300)); |
| modelInput.addEventListener('focus', () => { |
| document.getElementById('modelResults').style.display = 'block'; |
| }); |
| } |
|
|
| |
| const datasetInput = document.getElementById('datasetName'); |
| if (datasetInput) { |
| datasetInput.addEventListener('input', utils.debounce((e) => { |
| ui.searchDatasets(e.target.value); |
| }, 300)); |
| datasetInput.addEventListener('focus', () => { |
| document.getElementById('datasetResults').style.display = 'block'; |
| }); |
| } |
|
|
| |
| const trainingForm = document.getElementById('trainingForm'); |
| if (trainingForm) { |
| trainingForm.addEventListener('submit', (e) => { |
| e.preventDefault(); |
| ui.submitTrainingForm(); |
| }); |
| } |
|
|
| |
| document.addEventListener('click', (e) => { |
| if (!e.target.closest('#modelName') && !e.target.closest('#modelResults')) { |
| document.getElementById('modelResults')?.style.display = 'none'; |
| } |
| if (!e.target.closest('#datasetName') && !e.target.closest('#datasetResults')) { |
| document.getElementById('datasetResults')?.style.display = 'none'; |
| } |
| }); |
| }); |
|
|
| |
| window.ui = ui; |
| window.api = api; |
| window.utils = utils; |