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('سيتم إضافة نافذة إضافة نموذج مخصص قريباً'); | |
} | |
} | |