Spaces:
Running
Running
| // Multi-Modal Knowledge Distillation - JavaScript | |
| class KnowledgeDistillationApp { | |
| constructor() { | |
| this.selectedModels = []; | |
| this.selectedTeachers = []; | |
| this.selectedStudent = null; | |
| this.configuredModels = {}; | |
| this.currentStep = 1; | |
| this.trainingSession = null; | |
| this.websocket = null; | |
| // Add global error handler | |
| window.addEventListener('error', (event) => { | |
| console.error('Global error:', event.error); | |
| this.handleGlobalError(event.error); | |
| }); | |
| // Add unhandled promise rejection handler | |
| window.addEventListener('unhandledrejection', (event) => { | |
| console.error('Unhandled promise rejection:', event.reason); | |
| this.handleGlobalError(event.reason); | |
| }); | |
| this.init(); | |
| } | |
| handleGlobalError(error) { | |
| const errorMsg = error?.message || 'An unexpected error occurred'; | |
| console.error('Handling global error:', errorMsg); | |
| // Try to show error in UI, fallback to console | |
| try { | |
| if (this.showError) { | |
| this.showError(`Error: ${errorMsg}`); | |
| } | |
| } catch (e) { | |
| console.error('Could not show error in UI:', e); | |
| } | |
| } | |
| init() { | |
| this.setupEventListeners(); | |
| this.updateModelCount(); | |
| // Initialize models manager | |
| this.modelsManager = new ModelsManager(this); | |
| } | |
| setupEventListeners() { | |
| // File upload | |
| const uploadArea = document.getElementById('upload-area'); | |
| const fileInput = document.getElementById('file-input'); | |
| uploadArea.addEventListener('click', () => fileInput.click()); | |
| uploadArea.addEventListener('dragover', this.handleDragOver.bind(this)); | |
| uploadArea.addEventListener('dragleave', this.handleDragLeave.bind(this)); | |
| uploadArea.addEventListener('drop', this.handleDrop.bind(this)); | |
| fileInput.addEventListener('change', this.handleFileSelect.bind(this)); | |
| // Hugging Face models | |
| document.getElementById('add-hf-model').addEventListener('click', this.addHuggingFaceModel.bind(this)); | |
| document.getElementById('hf-repo').addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter') this.addHuggingFaceModel(); | |
| }); | |
| // URL models | |
| document.getElementById('add-url-model').addEventListener('click', this.addUrlModel.bind(this)); | |
| document.getElementById('model-url').addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter') this.addUrlModel(); | |
| }); | |
| // Navigation | |
| document.getElementById('next-step-1').addEventListener('click', () => this.goToStep(2)); | |
| document.getElementById('back-step-2').addEventListener('click', () => this.goToStep(1)); | |
| document.getElementById('back-step-3').addEventListener('click', () => this.goToStep(2)); | |
| document.getElementById('start-training').addEventListener('click', this.showConfirmModal.bind(this)); | |
| document.getElementById('start-new-training').addEventListener('click', () => this.resetAndGoToStep(1)); | |
| // Training controls | |
| document.getElementById('cancel-training').addEventListener('click', this.cancelTraining.bind(this)); | |
| document.getElementById('download-model').addEventListener('click', this.downloadModel.bind(this)); | |
| // Modals | |
| document.getElementById('confirm-start').addEventListener('click', this.startTraining.bind(this)); | |
| document.getElementById('confirm-cancel').addEventListener('click', this.hideConfirmModal.bind(this)); | |
| document.getElementById('error-ok').addEventListener('click', this.hideErrorModal.bind(this)); | |
| // Suggested models | |
| document.querySelectorAll('.suggestion-btn').forEach(btn => { | |
| btn.addEventListener('click', (e) => { | |
| const modelName = e.target.getAttribute('data-model'); | |
| const trustRequired = e.target.classList.contains('trust-required'); | |
| const gatedModel = e.target.classList.contains('gated-model'); | |
| document.getElementById('hf-repo').value = modelName; | |
| // Auto-enable trust remote code if required | |
| if (trustRequired) { | |
| document.getElementById('trust-remote-code').checked = true; | |
| this.showTokenStatus('⚠️ Trust Remote Code enabled for this model', 'warning'); | |
| } | |
| // Show warning for gated models | |
| if (gatedModel) { | |
| const tokenInput = document.getElementById('hf-token'); | |
| if (!tokenInput.value.trim()) { | |
| this.showTokenStatus('🔒 This model requires a Hugging Face token and access permission!', 'error'); | |
| tokenInput.focus(); | |
| return; | |
| } else { | |
| this.showTokenStatus('✅ Token detected for gated model', 'success'); | |
| } | |
| } | |
| this.addHuggingFaceModel(); | |
| }); | |
| }); | |
| // Test token button | |
| document.getElementById('test-token').addEventListener('click', this.testToken.bind(this)); | |
| // Test model button | |
| document.getElementById('test-model').addEventListener('click', this.testModel.bind(this)); | |
| // Download and upload buttons | |
| document.getElementById('download-model').addEventListener('click', this.downloadModel.bind(this)); | |
| document.getElementById('upload-to-hf').addEventListener('click', this.showHFUploadModal.bind(this)); | |
| document.getElementById('confirm-hf-upload').addEventListener('click', this.uploadToHuggingFace.bind(this)); | |
| document.getElementById('cancel-hf-upload').addEventListener('click', this.hideHFUploadModal.bind(this)); | |
| // Incremental training | |
| document.getElementById('enable-incremental').addEventListener('change', this.toggleIncrementalTraining.bind(this)); | |
| document.getElementById('existing-student').addEventListener('change', this.onStudentModelChange.bind(this)); | |
| document.getElementById('refresh-students').addEventListener('click', this.loadTrainedStudents.bind(this)); | |
| // Student source options | |
| document.querySelectorAll('input[name="student-source"]').forEach(radio => { | |
| radio.addEventListener('change', this.onStudentSourceChange.bind(this)); | |
| }); | |
| // HF student model | |
| document.getElementById('test-student-model').addEventListener('click', this.testStudentModel.bind(this)); | |
| document.getElementById('add-hf-student').addEventListener('click', this.addHFStudentModel.bind(this)); | |
| // HF Space student model | |
| document.getElementById('test-space-model').addEventListener('click', this.testSpaceModel.bind(this)); | |
| document.getElementById('add-space-student').addEventListener('click', this.addSpaceStudentModel.bind(this)); | |
| // File upload | |
| document.getElementById('student-file-upload').addEventListener('change', this.onStudentFilesUpload.bind(this)); | |
| // Load trained students on page load | |
| this.loadTrainedStudents(); | |
| } | |
| // File handling | |
| handleDragOver(e) { | |
| e.preventDefault(); | |
| e.currentTarget.classList.add('dragover'); | |
| } | |
| handleDragLeave(e) { | |
| e.preventDefault(); | |
| e.currentTarget.classList.remove('dragover'); | |
| } | |
| handleDrop(e) { | |
| e.preventDefault(); | |
| e.currentTarget.classList.remove('dragover'); | |
| const files = Array.from(e.dataTransfer.files); | |
| this.processFiles(files); | |
| } | |
| handleFileSelect(e) { | |
| const files = Array.from(e.target.files); | |
| this.processFiles(files); | |
| } | |
| async processFiles(files) { | |
| const validFiles = files.filter(file => this.validateFile(file)); | |
| if (validFiles.length === 0) { | |
| this.showError('No valid model files selected. Please select .pt, .pth, .bin, or .safetensors files.'); | |
| return; | |
| } | |
| this.showLoading(`Processing ${validFiles.length} file(s)...`); | |
| try { | |
| for (const file of validFiles) { | |
| await this.uploadFile(file); | |
| } | |
| } catch (error) { | |
| this.showError(`Error processing files: ${error.message}`); | |
| } finally { | |
| this.hideLoading(); | |
| } | |
| } | |
| validateFile(file) { | |
| const validExtensions = ['.pt', '.pth', '.bin', '.safetensors']; | |
| const extension = '.' + file.name.split('.').pop().toLowerCase(); | |
| const maxSize = 5 * 1024 * 1024 * 1024; // 5GB | |
| if (!validExtensions.includes(extension)) { | |
| this.showError(`Invalid file type: ${file.name}. Allowed types: ${validExtensions.join(', ')}`); | |
| return false; | |
| } | |
| if (file.size > maxSize) { | |
| this.showError(`File too large: ${file.name}. Maximum size: 5GB`); | |
| return false; | |
| } | |
| return true; | |
| } | |
| async uploadFile(file) { | |
| const formData = new FormData(); | |
| formData.append('files', file); | |
| formData.append('model_names', file.name.split('.')[0]); | |
| try { | |
| const response = await fetch('/upload', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`HTTP error! status: ${response.status}`); | |
| } | |
| const result = await response.json(); | |
| if (result.success) { | |
| result.models.forEach(model => this.addModel(model)); | |
| this.addConsoleMessage(`Successfully uploaded: ${file.name}`, 'success'); | |
| } else { | |
| throw new Error(result.message || 'Upload failed'); | |
| } | |
| } catch (error) { | |
| this.showError(`Upload failed for ${file.name}: ${error.message}`); | |
| throw error; | |
| } | |
| } | |
| async addHuggingFaceModel() { | |
| const repoInput = document.getElementById('hf-repo'); | |
| const tokenInput = document.getElementById('hf-token'); | |
| const accessTypeSelect = document.getElementById('model-access-type'); | |
| const repo = repoInput.value.trim(); | |
| const manualToken = tokenInput.value.trim(); | |
| const accessType = accessTypeSelect ? accessTypeSelect.value : 'read'; | |
| if (!repo) { | |
| this.showError('Please enter a Hugging Face repository name'); | |
| return; | |
| } | |
| if (!this.isValidHuggingFaceRepo(repo)) { | |
| this.showError('Invalid repository format. Use format: organization/model-name (e.g., google/bert_uncased_L-2_H-128_A-2)'); | |
| return; | |
| } | |
| let tokenToUse = manualToken; | |
| // If no manual token provided, get appropriate token for access type | |
| if (!manualToken) { | |
| try { | |
| const response = await fetch(`/api/tokens/for-task/${accessType}`); | |
| if (response.ok) { | |
| const data = await response.json(); | |
| if (data.success) { | |
| // We don't store the actual token, just indicate it will be used | |
| this.showSuccess(`سيتم استخدام ${data.token_info.type_name} للوصول للنموذج`); | |
| tokenToUse = 'auto'; // Indicate automatic token selection | |
| } | |
| } else { | |
| this.showWarning('لم يتم العثور على رمز مناسب، قد تحتاج لإضافة رمز يدوياً'); | |
| } | |
| } catch (error) { | |
| console.error('Error getting token for task:', error); | |
| this.showWarning('خطأ في الحصول على الرمز المناسب'); | |
| } | |
| } | |
| const model = { | |
| id: `hf_${Date.now()}`, | |
| name: repo, | |
| source: 'huggingface', | |
| path: repo, | |
| token: tokenToUse, | |
| accessType: accessType, | |
| info: { modality: 'unknown', format: 'huggingface' } | |
| }; | |
| this.addModel(model); | |
| repoInput.value = ''; | |
| // Don't clear token as user might want to use it for multiple models | |
| } | |
| async addUrlModel() { | |
| const urlInput = document.getElementById('model-url'); | |
| const url = urlInput.value.trim(); | |
| if (!url) { | |
| this.showError('Please enter a model URL'); | |
| return; | |
| } | |
| if (!this.isValidUrl(url)) { | |
| this.showError('Invalid URL format'); | |
| return; | |
| } | |
| // Validate that URL points to a model file | |
| const filename = this.extractFilenameFromUrl(url); | |
| const validExtensions = ['.pt', '.pth', '.bin', '.safetensors']; | |
| const hasValidExtension = validExtensions.some(ext => filename.toLowerCase().endsWith(ext)); | |
| if (!hasValidExtension) { | |
| this.showError(`URL must point to a model file with extension: ${validExtensions.join(', ')}`); | |
| return; | |
| } | |
| this.showLoading('Validating URL...'); | |
| try { | |
| // Test if URL is accessible | |
| const response = await fetch(url, { method: 'HEAD' }); | |
| if (!response.ok) { | |
| throw new Error(`URL not accessible: ${response.status}`); | |
| } | |
| const model = { | |
| id: `url_${Date.now()}`, | |
| name: filename, | |
| source: 'url', | |
| path: url, | |
| info: { | |
| modality: 'unknown', | |
| format: filename.split('.').pop(), | |
| size: response.headers.get('content-length') ? parseInt(response.headers.get('content-length')) : null | |
| } | |
| }; | |
| this.addModel(model); | |
| urlInput.value = ''; | |
| this.hideLoading(); | |
| } catch (error) { | |
| this.hideLoading(); | |
| this.showError(`URL validation failed: ${error.message}`); | |
| } | |
| } | |
| addModel(model) { | |
| if (this.selectedModels.length >= 10) { | |
| this.showError('Maximum 10 models allowed'); | |
| return; | |
| } | |
| // Check for duplicates | |
| if (this.selectedModels.some(m => m.path === model.path)) { | |
| this.showError('Model already added'); | |
| return; | |
| } | |
| this.selectedModels.push(model); | |
| this.updateModelsDisplay(); | |
| this.updateModelCount(); | |
| this.updateNextButton(); | |
| } | |
| removeModel(modelId) { | |
| this.selectedModels = this.selectedModels.filter(m => m.id !== modelId); | |
| this.updateModelsDisplay(); | |
| this.updateModelCount(); | |
| this.updateNextButton(); | |
| } | |
| updateModelsDisplay() { | |
| const grid = document.getElementById('models-grid'); | |
| grid.innerHTML = ''; | |
| this.selectedModels.forEach(model => { | |
| const card = this.createModelCard(model); | |
| grid.appendChild(card); | |
| }); | |
| } | |
| createModelCard(model) { | |
| const card = document.createElement('div'); | |
| card.className = 'model-card'; | |
| const modalityIcon = this.getModalityIcon(model.info.modality); | |
| const sizeText = model.size ? this.formatBytes(model.size) : 'Unknown size'; | |
| card.innerHTML = ` | |
| <button class="model-remove" onclick="app.removeModel('${model.id}')">×</button> | |
| <h4>${modalityIcon} ${model.name}</h4> | |
| <div class="model-info">Source: ${model.source}</div> | |
| <div class="model-info">Format: ${model.info.format}</div> | |
| <div class="model-info">Modality: ${model.info.modality}</div> | |
| <div class="model-info">Size: ${sizeText}</div> | |
| `; | |
| return card; | |
| } | |
| getModalityIcon(modality) { | |
| const icons = { | |
| text: '<i class="fas fa-font"></i>', | |
| vision: '<i class="fas fa-eye"></i>', | |
| multimodal: '<i class="fas fa-layer-group"></i>', | |
| audio: '<i class="fas fa-volume-up"></i>', | |
| unknown: '<i class="fas fa-question"></i>' | |
| }; | |
| return icons[modality] || icons.unknown; | |
| } | |
| updateModelCount() { | |
| document.getElementById('model-count').textContent = this.selectedModels.length; | |
| } | |
| updateNextButton() { | |
| const button = document.getElementById('next-step-1'); | |
| button.disabled = this.selectedModels.length === 0; | |
| } | |
| // Navigation | |
| goToStep(step) { | |
| // Hide all steps | |
| document.querySelectorAll('.step-section').forEach(section => { | |
| section.classList.add('hidden'); | |
| }); | |
| // Show target step | |
| document.getElementById(`step-${step}`).classList.remove('hidden'); | |
| this.currentStep = step; | |
| } | |
| resetAndGoToStep(step) { | |
| // Reset training session | |
| this.trainingSession = null; | |
| if (this.websocket) { | |
| this.websocket.close(); | |
| this.websocket = null; | |
| } | |
| // Reset UI elements | |
| document.getElementById('download-model').classList.add('hidden'); | |
| document.getElementById('start-new-training').classList.add('hidden'); | |
| document.getElementById('cancel-training').classList.remove('hidden'); | |
| // Clear console | |
| document.getElementById('training-console').innerHTML = ''; | |
| // Reset progress | |
| document.getElementById('overall-progress').style.width = '0%'; | |
| document.getElementById('progress-percentage').textContent = '0%'; | |
| // Go to step | |
| this.goToStep(step); | |
| } | |
| // Training | |
| showConfirmModal() { | |
| document.getElementById('confirm-modal').classList.remove('hidden'); | |
| } | |
| hideConfirmModal() { | |
| document.getElementById('confirm-modal').classList.add('hidden'); | |
| } | |
| async startTraining() { | |
| this.hideConfirmModal(); | |
| // Get configuration | |
| const config = this.getTrainingConfig(); | |
| // Check if any models require token and warn user | |
| const hasGatedModels = this.selectedModels.some(model => | |
| model.path.includes('gemma') || | |
| model.path.includes('llama') || | |
| model.path.includes('claude') | |
| ); | |
| if (hasGatedModels && !config.hf_token) { | |
| const proceed = confirm( | |
| 'Some selected models may require a Hugging Face token for access. ' + | |
| 'Do you want to continue without a token? (Training may fail for gated models)' | |
| ); | |
| if (!proceed) return; | |
| } | |
| try { | |
| const response = await fetch('/start-training', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(config) | |
| }); | |
| const result = await response.json(); | |
| if (result.success) { | |
| this.trainingSession = result.session_id; | |
| this.goToStep(3); | |
| this.connectWebSocket(); | |
| this.startProgressPolling(); | |
| } else { | |
| throw new Error(result.message || 'Failed to start training'); | |
| } | |
| } catch (error) { | |
| this.showError(`Failed to start training: ${error.message}`); | |
| } | |
| } | |
| getTrainingConfig() { | |
| // Get HF token from interface | |
| const hfToken = document.getElementById('hf-token').value.trim(); | |
| const trustRemoteCode = document.getElementById('trust-remote-code').checked; | |
| const incrementalTraining = document.getElementById('enable-incremental').checked; | |
| const existingStudent = document.getElementById('existing-student').value; | |
| // Get student model info based on source | |
| let studentModelPath = null; | |
| let studentSource = 'local'; | |
| if (incrementalTraining && existingStudent) { | |
| const selectedOption = document.querySelector('#existing-student option:checked'); | |
| if (selectedOption && selectedOption.dataset.source === 'huggingface') { | |
| studentSource = 'huggingface'; | |
| studentModelPath = existingStudent; // Already the repo name | |
| } else if (selectedOption && selectedOption.dataset.source === 'space') { | |
| studentSource = 'space'; | |
| studentModelPath = existingStudent.startsWith('space:') ? existingStudent.substring(6) : existingStudent; | |
| } else { | |
| studentSource = 'local'; | |
| studentModelPath = existingStudent; | |
| } | |
| } | |
| const config = { | |
| session_id: `session_${Date.now()}`, | |
| teacher_models: this.selectedModels.map(m => ({ | |
| path: m.path, | |
| token: m.token || hfToken || null, | |
| trust_remote_code: trustRemoteCode | |
| })), | |
| student_config: { | |
| hidden_size: parseInt(document.getElementById('hidden-size').value), | |
| num_layers: parseInt(document.getElementById('num-layers').value), | |
| output_size: parseInt(document.getElementById('hidden-size').value) | |
| }, | |
| training_params: { | |
| max_steps: parseInt(document.getElementById('max-steps').value), | |
| learning_rate: parseFloat(document.getElementById('learning-rate').value), | |
| temperature: parseFloat(document.getElementById('temperature').value), | |
| alpha: parseFloat(document.getElementById('alpha').value), | |
| batch_size: 8 | |
| }, | |
| distillation_strategy: document.getElementById('strategy').value, | |
| hf_token: hfToken || null, | |
| trust_remote_code: trustRemoteCode, | |
| incremental_training: incrementalTraining, | |
| existing_student_model: studentModelPath, | |
| student_source: studentSource | |
| }; | |
| return config; | |
| } | |
| connectWebSocket() { | |
| if (!this.trainingSession) return; | |
| const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; | |
| const wsUrl = `${protocol}//${window.location.host}/ws/${this.trainingSession}`; | |
| this.websocket = new WebSocket(wsUrl); | |
| this.websocket.onmessage = (event) => { | |
| const data = JSON.parse(event.data); | |
| if (data.type === 'training_update') { | |
| this.updateTrainingProgress(data.data); | |
| } | |
| }; | |
| this.websocket.onerror = (error) => { | |
| console.error('WebSocket error:', error); | |
| this.addConsoleMessage('WebSocket connection error', 'error'); | |
| }; | |
| this.websocket.onclose = () => { | |
| console.log('WebSocket connection closed'); | |
| }; | |
| } | |
| async startProgressPolling() { | |
| if (!this.trainingSession) return; | |
| this.trainingStartTime = Date.now(); // Track start time | |
| const poll = async () => { | |
| try { | |
| const response = await fetch(`/progress/${this.trainingSession}`); | |
| const progress = await response.json(); | |
| this.updateTrainingProgress(progress); | |
| // If stuck on loading for too long, show helpful message | |
| if (progress.status === 'loading_models' && progress.progress < 0.2) { | |
| const elapsed = Date.now() - this.trainingStartTime; | |
| if (elapsed > 60000) { // 1 minute | |
| const messageEl = document.getElementById('training-message'); | |
| if (messageEl && !messageEl.innerHTML.includes('Large models')) { | |
| messageEl.innerHTML = `${progress.message}<br><small style="color: #666;">Large models may take several minutes to load. Please be patient...</small>`; | |
| } | |
| } | |
| } | |
| if (progress.status === 'completed' || progress.status === 'failed') { | |
| return; // Stop polling | |
| } | |
| setTimeout(poll, 2000); // Poll every 2 seconds | |
| } catch (error) { | |
| console.error('Error polling progress:', error); | |
| setTimeout(poll, 5000); // Retry after 5 seconds | |
| } | |
| }; | |
| poll(); | |
| } | |
| updateTrainingProgress(progress) { | |
| // Update progress bar | |
| const progressFill = document.getElementById('overall-progress'); | |
| const progressText = document.getElementById('progress-percentage'); | |
| const percentage = Math.round(progress.progress * 100); | |
| progressFill.style.width = `${percentage}%`; | |
| progressText.textContent = `${percentage}%`; | |
| // Update status info | |
| document.getElementById('training-status').textContent = this.formatStatus(progress.status); | |
| document.getElementById('current-step').textContent = `${progress.current_step} / ${progress.total_steps}`; | |
| document.getElementById('eta').textContent = progress.eta || 'Calculating...'; | |
| // Update metrics | |
| if (progress.loss !== null && progress.loss !== undefined) { | |
| document.getElementById('current-loss').textContent = progress.loss.toFixed(4); | |
| } | |
| // Add console message | |
| if (progress.message) { | |
| this.addConsoleMessage(progress.message, this.getMessageType(progress.status)); | |
| } | |
| // Handle completion | |
| if (progress.status === 'completed') { | |
| document.getElementById('download-model').classList.remove('hidden'); | |
| document.getElementById('upload-to-hf').classList.remove('hidden'); | |
| document.getElementById('start-new-training').classList.remove('hidden'); | |
| document.getElementById('cancel-training').classList.add('hidden'); | |
| this.addConsoleMessage('Training completed successfully!', 'success'); | |
| } else if (progress.status === 'failed') { | |
| document.getElementById('start-new-training').classList.remove('hidden'); | |
| document.getElementById('cancel-training').classList.add('hidden'); | |
| this.addConsoleMessage(`Training failed: ${progress.message}`, 'error'); | |
| } | |
| } | |
| formatStatus(status) { | |
| const statusMap = { | |
| 'initializing': 'Initializing...', | |
| 'loading_models': 'Loading Models...', | |
| 'initializing_student': 'Initializing Student...', | |
| 'training': 'Training...', | |
| 'saving': 'Saving Model...', | |
| 'completed': 'Completed', | |
| 'failed': 'Failed' | |
| }; | |
| return statusMap[status] || status; | |
| } | |
| getMessageType(status) { | |
| if (status === 'completed') return 'success'; | |
| if (status === 'failed') return 'error'; | |
| if (status === 'loading_models' || status === 'initializing') return 'warning'; | |
| return 'info'; | |
| } | |
| addConsoleMessage(message, type = 'info') { | |
| const console = document.getElementById('training-console'); | |
| if (!console) { | |
| // Fallback to browser console if training console not found | |
| console.log(`[${type.toUpperCase()}] ${message}`); | |
| return; | |
| } | |
| try { | |
| const line = document.createElement('div'); | |
| line.className = `console-line ${type}`; | |
| line.textContent = `[${new Date().toLocaleTimeString()}] ${message}`; | |
| console.appendChild(line); | |
| console.scrollTop = console.scrollHeight; | |
| } catch (error) { | |
| console.error('Error adding console message:', error); | |
| console.log(`[${type.toUpperCase()}] ${message}`); | |
| } | |
| } | |
| async cancelTraining() { | |
| if (this.websocket) { | |
| this.websocket.close(); | |
| } | |
| this.addConsoleMessage('Training cancelled by user', 'warning'); | |
| } | |
| async downloadModel() { | |
| if (!this.trainingSession) return; | |
| try { | |
| const response = await fetch(`/download/${this.trainingSession}`); | |
| if (response.ok) { | |
| const blob = await response.blob(); | |
| const url = window.URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `distilled_model_${this.trainingSession}.safetensors`; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| window.URL.revokeObjectURL(url); | |
| } else { | |
| throw new Error('Download failed'); | |
| } | |
| } catch (error) { | |
| this.showError(`Download failed: ${error.message}`); | |
| } | |
| } | |
| // Utility functions | |
| isValidHuggingFaceRepo(repo) { | |
| return /^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/.test(repo); | |
| } | |
| isValidUrl(url) { | |
| try { | |
| new URL(url); | |
| return true; | |
| } catch { | |
| return false; | |
| } | |
| } | |
| extractFilenameFromUrl(url) { | |
| try { | |
| const pathname = new URL(url).pathname; | |
| return pathname.split('/').pop() || 'model'; | |
| } catch { | |
| return 'model'; | |
| } | |
| } | |
| formatBytes(bytes) { | |
| const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; | |
| if (bytes === 0) return '0 B'; | |
| const i = Math.floor(Math.log(bytes) / Math.log(1024)); | |
| return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`; | |
| } | |
| showError(message) { | |
| try { | |
| const errorMessage = document.getElementById('error-message'); | |
| const errorModal = document.getElementById('error-modal'); | |
| if (errorMessage && errorModal) { | |
| errorMessage.textContent = message; | |
| errorModal.classList.remove('hidden'); | |
| } else { | |
| // Fallback: use alert if modal elements not found | |
| console.error('Error modal elements not found, using alert'); | |
| alert(`Error: ${message}`); | |
| } | |
| } catch (error) { | |
| console.error('Error showing error message:', error); | |
| alert(`Error: ${message}`); | |
| } | |
| } | |
| hideErrorModal() { | |
| document.getElementById('error-modal').classList.add('hidden'); | |
| } | |
| showLoading(message) { | |
| // Create loading overlay if it doesn't exist | |
| let loadingOverlay = document.getElementById('loading-overlay'); | |
| if (!loadingOverlay) { | |
| loadingOverlay = document.createElement('div'); | |
| loadingOverlay.id = 'loading-overlay'; | |
| loadingOverlay.className = 'loading-overlay'; | |
| loadingOverlay.innerHTML = ` | |
| <div class="loading-content"> | |
| <div class="loading-spinner"></div> | |
| <div class="loading-message">${message}</div> | |
| </div> | |
| `; | |
| document.body.appendChild(loadingOverlay); | |
| } else { | |
| loadingOverlay.querySelector('.loading-message').textContent = message; | |
| loadingOverlay.classList.remove('hidden'); | |
| } | |
| } | |
| hideLoading() { | |
| const loadingOverlay = document.getElementById('loading-overlay'); | |
| if (loadingOverlay) { | |
| loadingOverlay.classList.add('hidden'); | |
| } | |
| } | |
| async testToken() { | |
| const tokenInput = document.getElementById('hf-token'); | |
| const statusDiv = document.getElementById('token-status'); | |
| const token = tokenInput.value.trim(); | |
| if (!token) { | |
| this.showTokenStatus('Please enter a token first', 'warning'); | |
| return; | |
| } | |
| this.showLoading('Testing token...'); | |
| try { | |
| const response = await fetch('/test-token'); | |
| const result = await response.json(); | |
| this.hideLoading(); | |
| if (result.token_valid) { | |
| this.showTokenStatus('✅ Token is valid and working!', 'success'); | |
| } else if (result.token_available) { | |
| this.showTokenStatus(`❌ Token validation failed: ${result.message}`, 'error'); | |
| } else { | |
| this.showTokenStatus('⚠️ No token found in environment. Using interface token.', 'warning'); | |
| } | |
| } catch (error) { | |
| this.hideLoading(); | |
| this.showTokenStatus(`❌ Error testing token: ${error.message}`, 'error'); | |
| } | |
| } | |
| showTokenStatus(message, type) { | |
| const statusDiv = document.getElementById('token-status'); | |
| if (!statusDiv) { | |
| console.warn('Token status div not found, using console message instead'); | |
| console.log(`${type.toUpperCase()}: ${message}`); | |
| return; | |
| } | |
| statusDiv.textContent = message; | |
| statusDiv.className = `token-status ${type}`; | |
| statusDiv.classList.remove('hidden'); | |
| // Hide after 5 seconds | |
| setTimeout(() => { | |
| if (statusDiv) { | |
| statusDiv.classList.add('hidden'); | |
| } | |
| }, 5000); | |
| } | |
| async testModel() { | |
| const repoInput = document.getElementById('hf-repo'); | |
| const trustRemoteCode = document.getElementById('trust-remote-code').checked; | |
| const repo = repoInput.value.trim(); | |
| if (!repo) { | |
| this.showTokenStatus('Please enter a model repository name first', 'warning'); | |
| return; | |
| } | |
| if (!this.isValidHuggingFaceRepo(repo)) { | |
| this.showTokenStatus('Invalid repository format. Use: organization/model-name', 'error'); | |
| return; | |
| } | |
| this.showLoading(`Testing model: ${repo}...`); | |
| try { | |
| const response = await fetch('/test-model', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| model_path: repo, | |
| trust_remote_code: trustRemoteCode | |
| }) | |
| }); | |
| const result = await response.json(); | |
| this.hideLoading(); | |
| if (result.success) { | |
| const info = result.model_info; | |
| let message = `✅ Model ${repo} is accessible!`; | |
| if (info.architecture) { | |
| message += ` Architecture: ${info.architecture}`; | |
| } | |
| if (info.modality) { | |
| message += `, Modality: ${info.modality}`; | |
| } | |
| this.showTokenStatus(message, 'success'); | |
| } else { | |
| let message = `❌ Model test failed: ${result.error}`; | |
| if (result.suggestions && result.suggestions.length > 0) { | |
| message += `. Suggestions: ${result.suggestions.join(', ')}`; | |
| } | |
| this.showTokenStatus(message, 'error'); | |
| } | |
| } catch (error) { | |
| this.hideLoading(); | |
| this.showTokenStatus(`❌ Error testing model: ${error.message}`, 'error'); | |
| } | |
| } | |
| downloadModel() { | |
| if (!this.trainingSession) { | |
| this.showError('No training session found'); | |
| return; | |
| } | |
| // Create download link | |
| const downloadUrl = `/download/${this.trainingSession}`; | |
| const link = document.createElement('a'); | |
| link.href = downloadUrl; | |
| link.download = `distilled_model_${this.trainingSession}`; | |
| document.body.appendChild(link); | |
| link.click(); | |
| document.body.removeChild(link); | |
| this.addConsoleMessage('Download started...', 'info'); | |
| } | |
| showHFUploadModal() { | |
| const modal = document.getElementById('hf-upload-modal'); | |
| modal.classList.remove('hidden'); | |
| // Pre-fill token if available | |
| const hfToken = document.getElementById('hf-token').value.trim(); | |
| if (hfToken) { | |
| document.getElementById('hf-upload-token').value = hfToken; | |
| // Auto-validate token and suggest username | |
| this.validateTokenAndSuggestName(hfToken); | |
| } | |
| } | |
| hideHFUploadModal() { | |
| const modal = document.getElementById('hf-upload-modal'); | |
| modal.classList.add('hidden'); | |
| } | |
| async uploadToHuggingFace() { | |
| if (!this.trainingSession) { | |
| this.showError('No training session found'); | |
| return; | |
| } | |
| const repoName = document.getElementById('hf-repo-name').value.trim(); | |
| const description = document.getElementById('hf-description').value.trim(); | |
| const token = document.getElementById('hf-upload-token').value.trim(); | |
| const isPrivate = document.getElementById('hf-private').checked; | |
| if (!repoName || !token) { | |
| this.showError('Repository name and token are required'); | |
| return; | |
| } | |
| if (!repoName.includes('/')) { | |
| this.showError('Repository name must be in format: username/model-name'); | |
| return; | |
| } | |
| this.showLoading('Uploading model to Hugging Face...'); | |
| this.hideHFUploadModal(); | |
| try { | |
| const formData = new FormData(); | |
| formData.append('repo_name', repoName); | |
| formData.append('description', description); | |
| formData.append('private', isPrivate); | |
| formData.append('hf_token', token); | |
| const response = await fetch(`/upload-to-hf/${this.trainingSession}`, { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| const result = await response.json(); | |
| this.hideLoading(); | |
| if (result.success) { | |
| this.addConsoleMessage(`✅ Model uploaded successfully to ${result.repo_url}`, 'success'); | |
| this.addConsoleMessage(`📁 Uploaded files: ${result.uploaded_files.join(', ')}`, 'info'); | |
| // Show success message with link | |
| const successMsg = document.createElement('div'); | |
| successMsg.className = 'alert alert-success'; | |
| successMsg.innerHTML = ` | |
| <strong>🎉 Upload Successful!</strong><br> | |
| Your model is now available at: <a href="${result.repo_url}" target="_blank">${result.repo_url}</a> | |
| `; | |
| // Find a safe container to insert the message | |
| let container = document.querySelector('.step-3 .step-content'); | |
| if (!container) { | |
| container = document.querySelector('.step-3'); | |
| } | |
| if (!container) { | |
| container = document.querySelector('#training-progress'); | |
| } | |
| if (!container) { | |
| container = document.body; | |
| } | |
| if (container && container.firstChild) { | |
| container.insertBefore(successMsg, container.firstChild); | |
| } else if (container) { | |
| container.appendChild(successMsg); | |
| } | |
| // Remove after 10 seconds | |
| setTimeout(() => { | |
| if (successMsg && successMsg.parentNode) { | |
| successMsg.parentNode.removeChild(successMsg); | |
| } | |
| }, 10000); | |
| } else { | |
| const errorMsg = result.detail || result.message || 'Unknown error'; | |
| this.showError(`Upload failed: ${errorMsg}`); | |
| this.addConsoleMessage(`❌ Upload failed: ${errorMsg}`, 'error'); | |
| } | |
| } catch (error) { | |
| this.hideLoading(); | |
| const errorMsg = error.message || 'Network error occurred'; | |
| this.showError(`Upload failed: ${errorMsg}`); | |
| this.addConsoleMessage(`❌ Upload error: ${errorMsg}`, 'error'); | |
| console.error('Upload error details:', error); | |
| } | |
| } | |
| async loadTrainedStudents() { | |
| try { | |
| const response = await fetch('/trained-students'); | |
| const data = await response.json(); | |
| const select = document.getElementById('existing-student'); | |
| select.innerHTML = '<option value="">Select a trained model...</option>'; | |
| if (data.trained_students && data.trained_students.length > 0) { | |
| data.trained_students.forEach(model => { | |
| const option = document.createElement('option'); | |
| option.value = model.path; | |
| option.textContent = `${model.name} (${model.architecture}, ${model.training_sessions} sessions)`; | |
| option.dataset.modelInfo = JSON.stringify(model); | |
| select.appendChild(option); | |
| }); | |
| } else { | |
| const option = document.createElement('option'); | |
| option.value = ''; | |
| option.textContent = 'No trained models found'; | |
| option.disabled = true; | |
| select.appendChild(option); | |
| } | |
| } catch (error) { | |
| console.error('Error loading trained students:', error); | |
| const select = document.getElementById('existing-student'); | |
| select.innerHTML = '<option value="">Error loading models</option>'; | |
| } | |
| } | |
| toggleIncrementalTraining() { | |
| const enabled = document.getElementById('enable-incremental').checked; | |
| const options = document.getElementById('incremental-options'); | |
| if (enabled) { | |
| options.classList.remove('hidden'); | |
| this.loadTrainedStudents(); | |
| } else { | |
| options.classList.add('hidden'); | |
| document.getElementById('student-info').classList.add('hidden'); | |
| } | |
| } | |
| onStudentModelChange() { | |
| const select = document.getElementById('existing-student'); | |
| const selectedOption = select.options[select.selectedIndex]; | |
| const studentInfo = document.getElementById('student-info'); | |
| if (selectedOption && selectedOption.dataset.modelInfo) { | |
| const modelData = JSON.parse(selectedOption.dataset.modelInfo); | |
| // Update info display | |
| document.getElementById('student-arch').textContent = modelData.architecture || 'Unknown'; | |
| document.getElementById('student-teachers').textContent = | |
| modelData.original_teachers.length > 0 ? | |
| modelData.original_teachers.join(', ') : | |
| 'None'; | |
| document.getElementById('student-sessions').textContent = modelData.training_sessions || '0'; | |
| document.getElementById('student-last').textContent = | |
| modelData.last_training !== 'unknown' ? | |
| new Date(modelData.last_training).toLocaleString() : | |
| 'Unknown'; | |
| studentInfo.classList.remove('hidden'); | |
| } else { | |
| studentInfo.classList.add('hidden'); | |
| } | |
| } | |
| onStudentSourceChange() { | |
| try { | |
| const selectedRadio = document.querySelector('input[name="student-source"]:checked'); | |
| if (!selectedRadio) { | |
| console.warn('No student source radio button selected'); | |
| return; | |
| } | |
| const selectedSource = selectedRadio.value; | |
| // Hide all options safely | |
| const optionIds = ['local-student-options', 'hf-student-options', 'space-student-options', 'upload-student-options']; | |
| optionIds.forEach(id => { | |
| const element = document.getElementById(id); | |
| if (element) { | |
| element.classList.add('hidden'); | |
| } | |
| }); | |
| // Show selected option | |
| const targetElement = document.getElementById(`${selectedSource}-student-options`); | |
| if (targetElement) { | |
| targetElement.classList.remove('hidden'); | |
| } else { | |
| console.warn(`Element ${selectedSource}-student-options not found`); | |
| } | |
| // Reset student info | |
| const studentInfo = document.getElementById('student-info'); | |
| if (studentInfo) { | |
| studentInfo.classList.add('hidden'); | |
| } | |
| } catch (error) { | |
| console.error('Error in onStudentSourceChange:', error); | |
| } | |
| } | |
| async testStudentModel() { | |
| const repoInput = document.getElementById('hf-student-repo'); | |
| const repo = repoInput.value.trim(); | |
| if (!repo) { | |
| this.showTokenStatus('Please enter a student model repository name', 'warning'); | |
| return; | |
| } | |
| if (!this.isValidHuggingFaceRepo(repo)) { | |
| this.showTokenStatus('Invalid repository format. Use: organization/model-name', 'error'); | |
| return; | |
| } | |
| this.showLoading(`Testing student model: ${repo}...`); | |
| try { | |
| const response = await fetch('/test-model', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| model_path: repo, | |
| trust_remote_code: document.getElementById('trust-remote-code').checked | |
| }) | |
| }); | |
| const result = await response.json(); | |
| this.hideLoading(); | |
| if (result.success) { | |
| this.showTokenStatus(`✅ Student model ${repo} is accessible!`, 'success'); | |
| } else { | |
| this.showTokenStatus(`❌ Student model test failed: ${result.error}`, 'error'); | |
| } | |
| } catch (error) { | |
| this.hideLoading(); | |
| this.showTokenStatus(`❌ Error testing student model: ${error.message}`, 'error'); | |
| } | |
| } | |
| addHFStudentModel() { | |
| const repo = document.getElementById('hf-student-repo').value.trim(); | |
| if (!repo) { | |
| this.showTokenStatus('Please enter a repository name first', 'warning'); | |
| return; | |
| } | |
| if (!this.isValidHuggingFaceRepo(repo)) { | |
| this.showTokenStatus('Invalid repository format. Use: organization/model-name', 'error'); | |
| return; | |
| } | |
| // Set the HF repo as the selected student model | |
| const existingStudentSelect = document.getElementById('existing-student'); | |
| // Remove any existing HF options to avoid duplicates | |
| Array.from(existingStudentSelect.options).forEach(option => { | |
| if (option.value.startsWith('hf:')) { | |
| option.remove(); | |
| } | |
| }); | |
| // Add HF repo as an option | |
| const option = document.createElement('option'); | |
| option.value = repo; // Store the repo directly, not with hf: prefix | |
| option.textContent = `${repo} (Hugging Face)`; | |
| option.selected = true; | |
| option.dataset.source = 'huggingface'; | |
| existingStudentSelect.appendChild(option); | |
| // Update student info display | |
| this.displayHFStudentInfo(repo); | |
| // Show success message | |
| this.showTokenStatus(`✅ Added Hugging Face student model: ${repo}`, 'success'); | |
| // Clear input | |
| document.getElementById('hf-student-repo').value = ''; | |
| } | |
| displayHFStudentInfo(repo) { | |
| // Show student info for HF model | |
| const studentInfo = document.getElementById('student-info'); | |
| document.getElementById('student-arch').textContent = 'Hugging Face Model'; | |
| document.getElementById('student-teachers').textContent = 'Unknown (External Model)'; | |
| document.getElementById('student-sessions').textContent = 'N/A'; | |
| document.getElementById('student-last').textContent = 'External Model'; | |
| studentInfo.classList.remove('hidden'); | |
| // Add note about HF model | |
| const noteDiv = document.createElement('div'); | |
| noteDiv.className = 'alert alert-info'; | |
| noteDiv.innerHTML = ` | |
| <i class="fas fa-info-circle"></i> | |
| <strong>Hugging Face Model:</strong> ${repo}<br> | |
| This model will be loaded from Hugging Face Hub. Make sure you have access to it. | |
| `; | |
| // Remove any existing notes | |
| const existingNotes = studentInfo.querySelectorAll('.alert-info'); | |
| existingNotes.forEach(note => note.remove()); | |
| studentInfo.appendChild(noteDiv); | |
| } | |
| async testSpaceModel() { | |
| const spaceInput = document.getElementById('hf-space-repo'); | |
| const space = spaceInput.value.trim(); | |
| if (!space) { | |
| this.showTokenStatus('Please enter a Space name first', 'warning'); | |
| return; | |
| } | |
| if (!this.isValidHuggingFaceRepo(space)) { | |
| this.showTokenStatus('Invalid Space format. Use: username/space-name', 'error'); | |
| return; | |
| } | |
| this.showLoading(`Testing Space: ${space}...`); | |
| try { | |
| // Test if the Space exists and has models | |
| const response = await fetch('/test-space', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| space_name: space, | |
| hf_token: document.getElementById('hf-token').value.trim() | |
| }) | |
| }); | |
| const result = await response.json(); | |
| this.hideLoading(); | |
| if (result.success) { | |
| const modelsCount = result.models ? result.models.length : 0; | |
| this.showTokenStatus(`✅ Space ${space} is accessible! Found ${modelsCount} trained models.`, 'success'); | |
| } else { | |
| this.showTokenStatus(`❌ Space test failed: ${result.error}`, 'error'); | |
| } | |
| } catch (error) { | |
| this.hideLoading(); | |
| this.showTokenStatus(`❌ Error testing Space: ${error.message}`, 'error'); | |
| } | |
| } | |
| addSpaceStudentModel() { | |
| const space = document.getElementById('hf-space-repo').value.trim(); | |
| if (!space) { | |
| this.showTokenStatus('Please enter a Space name first', 'warning'); | |
| return; | |
| } | |
| if (!this.isValidHuggingFaceRepo(space)) { | |
| this.showTokenStatus('Invalid Space format. Use: username/space-name', 'error'); | |
| return; | |
| } | |
| // Set the Space as the selected student model | |
| const existingStudentSelect = document.getElementById('existing-student'); | |
| // Remove any existing Space options to avoid duplicates | |
| Array.from(existingStudentSelect.options).forEach(option => { | |
| if (option.value.startsWith('space:')) { | |
| option.remove(); | |
| } | |
| }); | |
| // Add Space as an option | |
| const option = document.createElement('option'); | |
| option.value = `space:${space}`; | |
| option.textContent = `${space} (Hugging Face Space)`; | |
| option.selected = true; | |
| option.dataset.source = 'space'; | |
| existingStudentSelect.appendChild(option); | |
| // Update student info display | |
| this.displaySpaceStudentInfo(space); | |
| // Show success message | |
| this.showTokenStatus(`✅ Added Hugging Face Space: ${space}`, 'success'); | |
| // Clear input | |
| document.getElementById('hf-space-repo').value = ''; | |
| } | |
| displaySpaceStudentInfo(space) { | |
| // Show student info for Space | |
| const studentInfo = document.getElementById('student-info'); | |
| document.getElementById('student-arch').textContent = 'Hugging Face Space'; | |
| document.getElementById('student-teachers').textContent = 'Multiple Models Available'; | |
| document.getElementById('student-sessions').textContent = 'External Space'; | |
| document.getElementById('student-last').textContent = 'External Space'; | |
| studentInfo.classList.remove('hidden'); | |
| // Add note about Space | |
| const noteDiv = document.createElement('div'); | |
| noteDiv.className = 'alert alert-info'; | |
| noteDiv.innerHTML = ` | |
| <i class="fas fa-rocket"></i> | |
| <strong>Hugging Face Space:</strong> ${space}<br> | |
| This will load trained models from another Space. The Space should have completed training and saved models. | |
| `; | |
| // Remove any existing notes | |
| const existingNotes = studentInfo.querySelectorAll('.alert-info'); | |
| existingNotes.forEach(note => note.remove()); | |
| studentInfo.appendChild(noteDiv); | |
| } | |
| onStudentFilesUpload(event) { | |
| const files = event.target.files; | |
| if (files.length === 0) return; | |
| const fileNames = Array.from(files).map(f => f.name); | |
| this.showTokenStatus(`📁 Selected files: ${fileNames.join(', ')}`, 'success'); | |
| // TODO: Implement file upload functionality | |
| // For now, just show that files were selected | |
| } | |
| async validateTokenAndSuggestName(token) { | |
| if (!token) return; | |
| try { | |
| const response = await fetch('/validate-repo-name', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| repo_name: 'test/test', // Dummy name to get username | |
| hf_token: token | |
| }) | |
| }); | |
| const result = await response.json(); | |
| if (result.username) { | |
| // Auto-suggest repository name | |
| const repoInput = document.getElementById('hf-repo-name'); | |
| if (!repoInput.value.trim()) { | |
| const modelName = `distilled-model-${Date.now()}`; | |
| repoInput.value = `${result.username}/${modelName}`; | |
| repoInput.placeholder = `${result.username}/your-model-name`; | |
| } | |
| } | |
| } catch (error) { | |
| console.error('Error validating token:', error); | |
| } | |
| } | |
| async validateRepoName() { | |
| const repoName = document.getElementById('hf-repo-name').value.trim(); | |
| const token = document.getElementById('hf-upload-token').value.trim(); | |
| if (!repoName || !token) return; | |
| try { | |
| const response = await fetch('/validate-repo-name', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| repo_name: repoName, | |
| hf_token: token | |
| }) | |
| }); | |
| const result = await response.json(); | |
| const statusDiv = document.getElementById('repo-validation-status'); | |
| if (!statusDiv) { | |
| // Create status div if it doesn't exist | |
| const div = document.createElement('div'); | |
| div.id = 'repo-validation-status'; | |
| div.className = 'validation-status'; | |
| document.getElementById('hf-repo-name').parentNode.appendChild(div); | |
| } | |
| const status = document.getElementById('repo-validation-status'); | |
| if (result.valid) { | |
| status.innerHTML = `✅ Repository name is valid`; | |
| status.className = 'validation-status success'; | |
| } else { | |
| status.innerHTML = `❌ ${result.error}`; | |
| if (result.suggested_name) { | |
| status.innerHTML += `<br>💡 Suggested: <strong>${result.suggested_name}</strong>`; | |
| // Auto-fill suggested name | |
| document.getElementById('hf-repo-name').value = result.suggested_name; | |
| } | |
| status.className = 'validation-status error'; | |
| } | |
| status.classList.remove('hidden'); | |
| } catch (error) { | |
| console.error('Error validating repo name:', error); | |
| } | |
| } | |
| } | |
| // Initialize app when DOM is loaded | |
| document.addEventListener('DOMContentLoaded', () => { | |
| window.app = new KnowledgeDistillationApp(); | |
| }); | |
| // Advanced Features Functions | |
| async function showGoogleModels() { | |
| try { | |
| const response = await fetch('/api/models/google'); | |
| const data = await response.json(); | |
| if (response.ok) { | |
| const modelsHtml = data.models.map(model => ` | |
| <div class="model-card"> | |
| <h4>${model.name}</h4> | |
| <p>${model.description}</p> | |
| <div class="model-info"> | |
| <span class="badge ${model.medical_specialized ? 'bg-success' : 'bg-info'}"> | |
| ${model.medical_specialized ? 'Medical Specialized' : 'General Purpose'} | |
| </span> | |
| <span class="badge bg-secondary">${model.size_gb} GB</span> | |
| <span class="badge bg-primary">${model.modality}</span> | |
| </div> | |
| <button class="btn btn-primary mt-2" onclick="addGoogleModel('${model.name}')"> | |
| Add to Teachers | |
| </button> | |
| </div> | |
| `).join(''); | |
| showModal('Google Models', modelsHtml); | |
| } | |
| } catch (error) { | |
| console.error('Error loading Google models:', error); | |
| showError('Failed to load Google models'); | |
| } | |
| } | |
| async function showSystemInfo() { | |
| try { | |
| const response = await fetch('/api/system/performance'); | |
| const data = await response.json(); | |
| if (response.ok) { | |
| const systemInfoHtml = ` | |
| <div class="system-info"> | |
| <h5>Memory Information</h5> | |
| <div class="info-grid"> | |
| <div class="info-item"> | |
| <strong>Process Memory:</strong> ${data.memory.process_memory_mb.toFixed(1)} MB | |
| </div> | |
| <div class="info-item"> | |
| <strong>Memory Usage:</strong> ${data.memory.process_memory_percent.toFixed(1)}% | |
| </div> | |
| <div class="info-item"> | |
| <strong>Available Memory:</strong> ${data.memory.system_memory_available_gb.toFixed(1)} GB | |
| </div> | |
| <div class="info-item"> | |
| <strong>CPU Cores:</strong> ${data.cpu_cores} | |
| </div> | |
| </div> | |
| <h5 class="mt-3">Optimizations Applied</h5> | |
| <ul class="optimization-list"> | |
| ${data.optimizations_applied.map(opt => `<li>${opt}</li>`).join('')} | |
| </ul> | |
| ${data.recommendations.length > 0 ? ` | |
| <h5 class="mt-3">Recommendations</h5> | |
| <ul class="recommendation-list"> | |
| ${data.recommendations.map(rec => `<li>${rec}</li>`).join('')} | |
| </ul> | |
| ` : ''} | |
| <div class="mt-3"> | |
| <button class="btn btn-warning" onclick="forceMemoryCleanup()"> | |
| Force Memory Cleanup | |
| </button> | |
| </div> | |
| </div> | |
| `; | |
| showModal('System Information', systemInfoHtml); | |
| } | |
| } catch (error) { | |
| console.error('Error loading system info:', error); | |
| showError('Failed to load system information'); | |
| } | |
| } | |
| async function forceMemoryCleanup() { | |
| try { | |
| const response = await fetch('/api/system/cleanup', { method: 'POST' }); | |
| const data = await response.json(); | |
| if (response.ok) { | |
| showSuccess(data.message); | |
| // Refresh system info | |
| setTimeout(() => showSystemInfo(), 1000); | |
| } else { | |
| showError('Failed to cleanup memory'); | |
| } | |
| } catch (error) { | |
| console.error('Error during memory cleanup:', error); | |
| showError('Error during memory cleanup'); | |
| } | |
| } | |
| function addGoogleModel(modelName) { | |
| // Add the Google model to the HF repo input | |
| const hfRepoInput = document.getElementById('hf-repo'); | |
| if (hfRepoInput) { | |
| hfRepoInput.value = modelName; | |
| // Trigger the add model function | |
| if (window.app && window.app.addHuggingFaceModel) { | |
| window.app.addHuggingFaceModel(); | |
| } | |
| } | |
| closeModal(); | |
| } | |
| function showModal(title, content) { | |
| // Create modal if it doesn't exist | |
| let modal = document.getElementById('advanced-modal'); | |
| if (!modal) { | |
| modal = document.createElement('div'); | |
| modal.id = 'advanced-modal'; | |
| modal.className = 'modal-overlay'; | |
| modal.innerHTML = ` | |
| <div class="modal-content"> | |
| <div class="modal-header"> | |
| <h3 id="modal-title">${title}</h3> | |
| <button class="modal-close" onclick="closeModal()">×</button> | |
| </div> | |
| <div class="modal-body" id="modal-body"> | |
| ${content} | |
| </div> | |
| </div> | |
| `; | |
| document.body.appendChild(modal); | |
| } else { | |
| document.getElementById('modal-title').textContent = title; | |
| document.getElementById('modal-body').innerHTML = content; | |
| } | |
| modal.style.display = 'flex'; | |
| } | |
| function closeModal() { | |
| const modal = document.getElementById('advanced-modal'); | |
| if (modal) { | |
| modal.style.display = 'none'; | |
| } | |
| } | |
| function showSuccess(message) { | |
| showNotification(message, 'success'); | |
| } | |
| function showError(message) { | |
| showNotification(message, 'error'); | |
| } | |
| function showNotification(message, type) { | |
| const notification = document.createElement('div'); | |
| notification.className = `notification notification-${type}`; | |
| notification.textContent = message; | |
| document.body.appendChild(notification); | |
| // Auto remove after 5 seconds | |
| setTimeout(() => { | |
| if (notification.parentNode) { | |
| notification.parentNode.removeChild(notification); | |
| } | |
| }, 5000); | |
| } | |
| // Models Management Functions | |
| class ModelsManager { | |
| constructor(app) { | |
| this.app = app; | |
| this.setupEventListeners(); | |
| this.loadConfiguredModels(); | |
| } | |
| setupEventListeners() { | |
| // Refresh models | |
| const refreshButton = document.getElementById('refresh-models'); | |
| if (refreshButton) { | |
| refreshButton.addEventListener('click', () => { | |
| this.loadConfiguredModels(); | |
| }); | |
| } | |
| // Search models | |
| const searchButton = document.getElementById('search-models-btn'); | |
| if (searchButton) { | |
| searchButton.addEventListener('click', () => { | |
| this.searchModels(); | |
| }); | |
| } | |
| // Search on Enter key | |
| const searchQuery = document.getElementById('model-search-query'); | |
| if (searchQuery) { | |
| searchQuery.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter') { | |
| this.searchModels(); | |
| } | |
| }); | |
| } | |
| // Add custom model | |
| const addCustomButton = document.getElementById('add-custom-model'); | |
| if (addCustomButton) { | |
| addCustomButton.addEventListener('click', () => { | |
| this.showAddCustomModelModal(); | |
| }); | |
| } | |
| } | |
| async loadConfiguredModels() { | |
| try { | |
| const response = await fetch('/api/models/teachers'); | |
| const data = await response.json(); | |
| if (data.success) { | |
| this.app.configuredModels = data.teachers; | |
| this.app.selectedTeachers = data.selected; | |
| this.displayConfiguredModels(data.teachers, data.selected); | |
| } | |
| } catch (error) { | |
| console.error('Error loading configured models:', error); | |
| this.app.showError('خطأ في تحميل النماذج المُعدة'); | |
| } | |
| } | |
| displayConfiguredModels(models, selected) { | |
| const container = document.getElementById('configured-models-list'); | |
| if (!container) return; | |
| if (Object.keys(models).length === 0) { | |
| container.innerHTML = '<p class="text-muted">لا توجد نماذج مُعدة</p>'; | |
| return; | |
| } | |
| container.innerHTML = Object.entries(models).map(([id, model]) => ` | |
| <div class="card mb-2"> | |
| <div class="card-body"> | |
| <div class="d-flex justify-content-between align-items-start"> | |
| <div class="flex-grow-1"> | |
| <div class="form-check"> | |
| <input class="form-check-input" type="checkbox" | |
| id="model-${id}" ${selected.includes(id) ? 'checked' : ''} | |
| onchange="window.app.modelsManager.toggleModelSelection('${id}', this.checked)"> | |
| <label class="form-check-label" for="model-${id}"> | |
| <h6 class="mb-1">${model.name}</h6> | |
| </label> | |
| </div> | |
| <p class="text-muted small mb-1">${model.description || 'لا يوجد وصف'}</p> | |
| <div class="d-flex gap-2"> | |
| <span class="badge bg-primary">${model.category}</span> | |
| <span class="badge bg-secondary">${model.modality}</span> | |
| <span class="badge bg-info">${model.parameters || 'Unknown'}</span> | |
| </div> | |
| </div> | |
| <div class="d-flex gap-1"> | |
| <button class="btn btn-sm btn-outline-info" onclick="window.app.modelsManager.showModelInfo('${id}')"> | |
| <i class="fas fa-info"></i> | |
| </button> | |
| <button class="btn btn-sm btn-outline-danger" onclick="window.app.modelsManager.removeModel('${id}')"> | |
| <i class="fas fa-trash"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| `).join(''); | |
| } | |
| async searchModels() { | |
| const queryElement = document.getElementById('model-search-query'); | |
| const typeElement = document.getElementById('model-type-filter'); | |
| if (!queryElement) return; | |
| const query = queryElement.value.trim(); | |
| const modelType = typeElement ? typeElement.value : ''; | |
| if (!query) { | |
| this.app.showError('يرجى إدخال كلمة البحث'); | |
| return; | |
| } | |
| const searchButton = document.getElementById('search-models-btn'); | |
| const originalText = searchButton.innerHTML; | |
| searchButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> جاري البحث...'; | |
| searchButton.disabled = true; | |
| try { | |
| const response = await fetch('/api/models/search', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ | |
| query: query, | |
| limit: 20, | |
| model_type: modelType || null | |
| }) | |
| }); | |
| const data = await response.json(); | |
| if (data.success) { | |
| this.displaySearchResults(data.results); | |
| } else { | |
| this.app.showError('فشل في البحث عن النماذج'); | |
| } | |
| } catch (error) { | |
| console.error('Error searching models:', error); | |
| this.app.showError('خطأ في البحث عن النماذج'); | |
| } finally { | |
| searchButton.innerHTML = originalText; | |
| searchButton.disabled = false; | |
| } | |
| } | |
| displaySearchResults(results) { | |
| const resultsContainer = document.getElementById('model-search-results-list'); | |
| const searchResults = document.getElementById('model-search-results'); | |
| if (!resultsContainer || !searchResults) return; | |
| if (results.length === 0) { | |
| resultsContainer.innerHTML = '<p class="text-muted">لم يتم العثور على نتائج</p>'; | |
| } else { | |
| resultsContainer.innerHTML = results.map(result => ` | |
| <div class="card mb-2"> | |
| <div class="card-body"> | |
| <div class="d-flex justify-content-between align-items-start"> | |
| <div> | |
| <h6 class="card-title">${result.name}</h6> | |
| <p class="card-text text-muted small">${result.description || 'لا يوجد وصف'}</p> | |
| <div class="d-flex gap-2"> | |
| <span class="badge bg-primary">${result.author}</span> | |
| <span class="badge bg-secondary">${result.downloads || 0} تحميل</span> | |
| <span class="badge bg-success">${result.likes || 0} إعجاب</span> | |
| <span class="badge bg-info">${result.pipeline_tag || 'unknown'}</span> | |
| </div> | |
| </div> | |
| <button class="btn btn-sm btn-outline-primary" onclick="window.app.modelsManager.addModelFromSearch('${result.id}', '${result.name}', '${result.description || ''}', '${result.pipeline_tag || 'text'}')"> | |
| <i class="fas fa-plus"></i> إضافة | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| `).join(''); | |
| } | |
| searchResults.style.display = 'block'; | |
| } | |
| async addModelFromSearch(modelId, name, description, pipelineTag) { | |
| try { | |
| // Determine category and modality from pipeline tag | |
| let category = 'text'; | |
| let modality = 'text'; | |
| if (pipelineTag.includes('image') || pipelineTag.includes('vision')) { | |
| category = 'vision'; | |
| modality = 'vision'; | |
| } else if (pipelineTag.includes('audio') || pipelineTag.includes('speech')) { | |
| category = 'audio'; | |
| modality = 'audio'; | |
| } | |
| const modelInfo = { | |
| name: name, | |
| model_id: modelId, | |
| category: category, | |
| type: 'teacher', | |
| description: description, | |
| modality: modality, | |
| architecture: 'transformer' | |
| }; | |
| const success = await this.submitModel(modelInfo); | |
| if (success) { | |
| this.app.showSuccess(`تم إضافة النموذج: ${name}`); | |
| this.loadConfiguredModels(); | |
| } | |
| } catch (error) { | |
| console.error('Error adding model from search:', error); | |
| this.app.showError('فشل في إضافة النموذج'); | |
| } | |
| } | |
| async submitModel(modelInfo) { | |
| try { | |
| const response = await fetch('/api/models/add', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify(modelInfo) | |
| }); | |
| const data = await response.json(); | |
| return data.success; | |
| } catch (error) { | |
| console.error('Error submitting model:', error); | |
| this.app.showError('فشل في إضافة النموذج'); | |
| return false; | |
| } | |
| } | |
| async toggleModelSelection(modelId, selected) { | |
| try { | |
| if (selected) { | |
| // Add to selected teachers | |
| if (!this.app.selectedTeachers.includes(modelId)) { | |
| this.app.selectedTeachers.push(modelId); | |
| } | |
| } else { | |
| // Remove from selected teachers | |
| const index = this.app.selectedTeachers.indexOf(modelId); | |
| if (index > -1) { | |
| this.app.selectedTeachers.splice(index, 1); | |
| } | |
| } | |
| // Update server | |
| const response = await fetch('/api/models/select', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ | |
| teacher_models: this.app.selectedTeachers | |
| }) | |
| }); | |
| if (response.ok) { | |
| this.app.showSuccess(selected ? 'تم تحديد النموذج' : 'تم إلغاء تحديد النموذج'); | |
| this.updateSelectedModelsDisplay(); | |
| } | |
| } catch (error) { | |
| console.error('Error toggling model selection:', error); | |
| this.app.showError('فشل في تحديث اختيار النموذج'); | |
| } | |
| } | |
| updateSelectedModelsDisplay() { | |
| // Update the selected models count and display | |
| const countElement = document.getElementById('model-count'); | |
| if (countElement) { | |
| countElement.textContent = this.app.selectedTeachers.length; | |
| } | |
| // Update next step button | |
| const nextButton = document.getElementById('next-step-1'); | |
| if (nextButton) { | |
| nextButton.disabled = this.app.selectedTeachers.length === 0; | |
| } | |
| // Update models grid display | |
| this.displaySelectedModels(); | |
| } | |
| displaySelectedModels() { | |
| const modelsGrid = document.getElementById('models-grid'); | |
| if (!modelsGrid) return; | |
| if (this.app.selectedTeachers.length === 0) { | |
| modelsGrid.innerHTML = '<p class="text-muted">لم يتم اختيار أي نماذج بعد</p>'; | |
| return; | |
| } | |
| modelsGrid.innerHTML = this.app.selectedTeachers.map(modelId => { | |
| const model = this.app.configuredModels[modelId]; | |
| if (!model) return ''; | |
| return ` | |
| <div class="model-card"> | |
| <div class="model-info"> | |
| <h6>${model.name}</h6> | |
| <p class="text-muted small">${model.description || 'لا يوجد وصف'}</p> | |
| <div class="model-badges"> | |
| <span class="badge bg-primary">${model.category}</span> | |
| <span class="badge bg-secondary">${model.modality}</span> | |
| </div> | |
| </div> | |
| <button class="btn btn-sm btn-outline-danger" onclick="window.app.modelsManager.toggleModelSelection('${modelId}', false)"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| `; | |
| }).join(''); | |
| } | |
| async removeModel(modelId) { | |
| if (!confirm('هل أنت متأكد من حذف النموذج؟')) { | |
| return; | |
| } | |
| try { | |
| const response = await fetch(`/api/models/${encodeURIComponent(modelId)}`, { | |
| method: 'DELETE' | |
| }); | |
| const data = await response.json(); | |
| if (data.success) { | |
| this.app.showSuccess('تم حذف النموذج'); | |
| this.loadConfiguredModels(); | |
| } else { | |
| this.app.showError('فشل في حذف النموذج'); | |
| } | |
| } catch (error) { | |
| console.error('Error removing model:', error); | |
| this.app.showError('خطأ في حذف النموذج'); | |
| } | |
| } | |
| showModelInfo(modelId) { | |
| const model = this.app.configuredModels[modelId]; | |
| if (model) { | |
| this.app.showInfo(`معلومات النموذج: ${model.name}\nالوصف: ${model.description}\nالفئة: ${model.category}\nالحجم: ${model.size}`); | |
| } | |
| } | |
| showAddCustomModelModal() { | |
| // Show modal for adding custom model | |
| this.app.showInfo('سيتم إضافة نافذة إضافة نموذج مخصص قريباً'); | |
| } | |
| } | |