// Mail Fine-Tuning App - Frontend Logic const API_BASE = ''; // State let currentMails = []; let currentLabelingIndex = 0; let stats = {}; let trainingEventSource = null; // ====================== // Utility Functions // ====================== function showToast(message, type = 'info') { const container = document.getElementById('toast-container'); const toast = document.createElement('div'); toast.className = `toast ${type}`; toast.textContent = message; container.appendChild(toast); setTimeout(() => { toast.remove(); }, 4000); } async function apiCall(endpoint, options = {}) { try { const response = await fetch(API_BASE + endpoint, { ...options, headers: { 'Content-Type': 'application/json', ...options.headers } }); if (!response.ok) { const error = await response.json(); throw new Error(error.detail || 'API Error'); } return await response.json(); } catch (error) { showToast(error.message, 'error'); throw error; } } // ====================== // Navigation // ====================== function initNavigation() { const navLinks = document.querySelectorAll('.nav-link'); const views = document.querySelectorAll('.view'); navLinks.forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); const targetView = link.dataset.view; // Update active states navLinks.forEach(l => l.classList.remove('active')); link.classList.add('active'); views.forEach(v => v.classList.remove('active')); document.getElementById(`${targetView}-view`).classList.add('active'); // Load data for view if (targetView === 'labeling') { loadLabelingView(); } else if (targetView === 'export') { loadStats(); } else if (targetView === 'models') { loadModels(); } else if (targetView === 'training') { loadTrainingView(); } }); }); } // ====================== // Mail Import // ====================== function initImport() { const dropzone = document.getElementById('dropzone'); const fileInput = document.getElementById('file-input'); dropzone.addEventListener('click', () => fileInput.click()); dropzone.addEventListener('dragover', (e) => { e.preventDefault(); dropzone.classList.add('dragover'); }); dropzone.addEventListener('dragleave', () => { dropzone.classList.remove('dragover'); }); dropzone.addEventListener('drop', (e) => { e.preventDefault(); dropzone.classList.remove('dragover'); handleFiles(e.dataTransfer.files); }); fileInput.addEventListener('change', (e) => { handleFiles(e.target.files); }); document.getElementById('refresh-mails').addEventListener('click', loadMails); // Initial load loadMails(); } async function handleFiles(files) { const formData = new FormData(); for (let file of files) { formData.append('files', file); } try { const response = await fetch(API_BASE + '/api/mails/upload', { method: 'POST', body: formData }); const result = await response.json(); const successCount = result.success.reduce((sum, r) => sum + r.count, 0); showToast(`${successCount} Mails erfolgreich importiert`, 'success'); if (result.errors.length > 0) { showToast(`${result.errors.length} Fehler beim Import`, 'error'); } loadMails(); } catch (error) { showToast('Fehler beim Upload', 'error'); } } async function loadMails() { try { const data = await apiCall('/api/mails'); currentMails = data.mails; document.getElementById('mail-count').textContent = currentMails.length; renderMailList(currentMails); } catch (error) { console.error('Error loading mails:', error); } } function renderMailList(mails) { const container = document.getElementById('mail-list'); if (mails.length === 0) { container.innerHTML = '

Keine Mails vorhanden

'; return; } container.innerHTML = mails.map(mail => `
${escapeHtml(mail.subject)}
${mail.status}
Von: ${escapeHtml(mail.sender)}
${escapeHtml(mail.body)}
`).join(''); } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } async function deleteMail(id) { if (!confirm('Mail wirklich löschen?')) return; try { await apiCall(`/api/mails/${id}`, { method: 'DELETE' }); showToast('Mail gelöscht', 'success'); loadMails(); } catch (error) { console.error('Error deleting mail:', error); } } function viewMail(id) { const mail = currentMails.find(m => m.id === id); if (!mail) return; alert(`Betreff: ${mail.subject}\n\nVon: ${mail.sender}\n\n${mail.body}`); } // ====================== // Labeling // ====================== function initLabeling() { const statusFilter = document.getElementById('status-filter'); statusFilter.addEventListener('change', loadLabelingView); // Keyboard shortcuts document.addEventListener('keydown', (e) => { const activeView = document.querySelector('.view.active'); if (activeView.id !== 'labeling-view') return; if (e.key.toLowerCase() === 'n') { nextMail(); } else if (e.key.toLowerCase() === 's') { saveLabelingMail(); } else if (e.key.toLowerCase() === 'k') { skipMail(); } }); } async function loadLabelingView() { const statusFilter = document.getElementById('status-filter').value; try { const data = await apiCall(`/api/mails?status=${statusFilter || ''}`); currentMails = data.mails; currentLabelingIndex = 0; updateLabelingProgress(); renderCurrentMail(); } catch (error) { console.error('Error loading labeling view:', error); } } function updateLabelingProgress() { const labeled = currentMails.filter(m => m.status === 'labeled').length; const total = currentMails.length; const percent = total > 0 ? (labeled / total) * 100 : 0; document.getElementById('labeling-progress').style.width = `${percent}%`; document.getElementById('progress-text').textContent = `${labeled} / ${total} gelabelt`; } function renderCurrentMail() { const container = document.getElementById('labeling-container'); if (currentMails.length === 0) { container.innerHTML = '

Keine Mails zum Labeln vorhanden

'; return; } const mail = currentMails[currentLabelingIndex]; container.innerHTML = `

${escapeHtml(mail.subject)}

Von: ${escapeHtml(mail.sender)}

An: ${escapeHtml(mail.recipient)}


${escapeHtml(mail.body)}
${currentLabelingIndex + 1} / ${currentMails.length}
`; } async function saveLabelingMail() { const mail = currentMails[currentLabelingIndex]; const taskType = document.getElementById('task-type').value; const expectedOutput = document.getElementById('expected-output').value; if (!taskType || !expectedOutput) { showToast('Bitte alle Felder ausfüllen', 'warning'); return; } try { await apiCall(`/api/mails/${mail.id}`, { method: 'PUT', body: JSON.stringify({ task_type: taskType, expected_output: expectedOutput, status: 'labeled' }) }); showToast('Gespeichert', 'success'); mail.status = 'labeled'; updateLabelingProgress(); nextMail(); } catch (error) { console.error('Error saving mail:', error); } } async function skipMail() { const mail = currentMails[currentLabelingIndex]; try { await apiCall(`/api/mails/${mail.id}`, { method: 'PUT', body: JSON.stringify({ status: 'skip' }) }); mail.status = 'skip'; updateLabelingProgress(); nextMail(); } catch (error) { console.error('Error skipping mail:', error); } } function nextMail() { if (currentLabelingIndex < currentMails.length - 1) { currentLabelingIndex++; } else { currentLabelingIndex = 0; } renderCurrentMail(); } // ====================== // Export & Stats // ====================== function initExport() { document.getElementById('export-jsonl').addEventListener('click', exportJSONL); } async function loadStats() { try { stats = await apiCall('/api/export/stats'); renderStats(); } catch (error) { console.error('Error loading stats:', error); } } function renderStats() { const container = document.getElementById('stats-grid'); container.innerHTML = `
${stats.total || 0}
Gesamt Mails
${stats.labeled || 0}
Gelabelt
${stats.unlabeled || 0}
Unlabeled
${stats.avg_input_length || 0}
Avg Input Length
${stats.avg_output_length || 0}
Avg Output Length
${stats.sufficient_data ? '✅' : '❌'}
Genug Daten (>50)
`; } async function exportJSONL() { const trainSplit = document.getElementById('train-split').value / 100; try { const result = await apiCall('/api/export/jsonl', { method: 'POST', body: JSON.stringify({ train_split: trainSplit }) }); const resultDiv = document.getElementById('export-result'); resultDiv.innerHTML = `

✅ Export erfolgreich!

Training Samples: ${result.train_samples}

Validation Samples: ${result.val_samples}

📥 train.jsonl 📥 val.jsonl

`; resultDiv.classList.add('show'); showToast('JSONL Dateien generiert', 'success'); } catch (error) { console.error('Error exporting JSONL:', error); } } // ====================== // Models // ====================== async function loadModels() { try { const data = await apiCall('/api/models'); renderModels(data.models); } catch (error) { console.error('Error loading models:', error); } } function renderModels(models) { const container = document.getElementById('models-list'); if (models.length === 0) { container.innerHTML = '

Keine Modelle vorhanden

'; return; } container.innerHTML = models.map(model => `
📦 ${model} ✓ Verfügbar
`).join(''); } // ====================== // Training // ====================== function initTraining() { const lrSlider = document.getElementById('learning-rate'); const epochsSlider = document.getElementById('epochs'); lrSlider.addEventListener('input', (e) => { const value = Math.pow(10, parseFloat(e.target.value)); document.getElementById('lr-value').textContent = value.toExponential(0); }); epochsSlider.addEventListener('input', (e) => { document.getElementById('epochs-value').textContent = e.target.value; }); document.getElementById('training-form').addEventListener('submit', startTraining); document.getElementById('stop-training').addEventListener('click', stopTraining); } async function loadTrainingView() { // Load available models try { const data = await apiCall('/api/models'); const select = document.getElementById('training-model'); select.innerHTML = '' + data.models.map(m => ``).join(''); } catch (error) { console.error('Error loading models:', error); } // Get current status updateTrainingStatus(); } async function startTraining(e) { e.preventDefault(); const modelName = document.getElementById('training-model').value; const learningRate = Math.pow(10, parseFloat(document.getElementById('learning-rate').value)); const epochs = parseInt(document.getElementById('epochs').value); const batchSize = parseInt(document.getElementById('batch-size').value); const loraRank = parseInt(document.getElementById('lora-rank').value); if (!modelName) { showToast('Bitte Modell wählen', 'warning'); return; } try { await apiCall('/api/training/start', { method: 'POST', body: JSON.stringify({ model_name: modelName, learning_rate: learningRate, epochs: epochs, batch_size: batchSize, lora_rank: loraRank }) }); showToast('Training gestartet', 'success'); document.getElementById('start-training').disabled = true; document.getElementById('stop-training').disabled = false; // Start SSE stream startTrainingStream(); } catch (error) { console.error('Error starting training:', error); } } async function stopTraining() { try { await apiCall('/api/training/stop', { method: 'POST' }); showToast('Training gestoppt', 'warning'); document.getElementById('start-training').disabled = false; document.getElementById('stop-training').disabled = true; if (trainingEventSource) { trainingEventSource.close(); } } catch (error) { console.error('Error stopping training:', error); } } function startTrainingStream() { if (trainingEventSource) { trainingEventSource.close(); } trainingEventSource = new EventSource('/api/training/stream'); trainingEventSource.onmessage = (event) => { const status = JSON.parse(event.data); updateTrainingStatusUI(status); if (!status.is_training && status.current_step > 0) { trainingEventSource.close(); document.getElementById('start-training').disabled = false; document.getElementById('stop-training').disabled = true; showToast('Training abgeschlossen', 'success'); } }; trainingEventSource.onerror = () => { trainingEventSource.close(); }; } async function updateTrainingStatus() { try { const status = await apiCall('/api/training/status'); updateTrainingStatusUI(status); if (status.is_training) { document.getElementById('start-training').disabled = true; document.getElementById('stop-training').disabled = false; startTrainingStream(); } } catch (error) { console.error('Error updating status:', error); } } function updateTrainingStatusUI(status) { const container = document.getElementById('training-status'); if (!status.is_training && status.current_step === 0) { container.innerHTML = '

Kein Training aktiv

'; return; } const eta = status.eta_seconds ? `${Math.floor(status.eta_seconds / 60)}m ${status.eta_seconds % 60}s` : 'N/A'; container.innerHTML = `
${status.is_training ? '🟢 Running' : '⏸️ Stopped'}
${status.current_step} / ${status.total_steps}
${status.current_epoch}
${status.train_loss || 'N/A'}
${status.val_loss || 'N/A'}
${eta}
${status.memory_usage_percent}%
`; // Update charts (simple implementation without chart library) updateChart('train-loss-chart', status.train_loss_history); updateChart('val-loss-chart', status.val_loss_history); } function updateChart(canvasId, data) { // Simplified chart rendering (without external library) const canvas = document.getElementById(canvasId); if (!canvas) return; const ctx = canvas.getContext('2d'); canvas.width = canvas.offsetWidth; canvas.height = 200; ctx.clearRect(0, 0, canvas.width, canvas.height); if (!data || data.length === 0) return; const padding = 20; const width = canvas.width - 2 * padding; const height = canvas.height - 2 * padding; const maxVal = Math.max(...data); const minVal = Math.min(...data); const range = maxVal - minVal || 1; ctx.strokeStyle = '#4a9eff'; ctx.lineWidth = 2; ctx.beginPath(); data.forEach((val, i) => { const x = padding + (i / (data.length - 1)) * width; const y = padding + height - ((val - minVal) / range) * height; if (i === 0) { ctx.moveTo(x, y); } else { ctx.lineTo(x, y); } }); ctx.stroke(); } // ====================== // Evaluation // ====================== function initEvaluation() { document.getElementById('load-test-prompt').addEventListener('click', loadTestPrompt); document.getElementById('run-comparison').addEventListener('click', runComparison); } async function loadTestPrompt() { const taskType = document.getElementById('eval-task-type').value; try { const prompts = await apiCall('/api/inference/test-prompts'); const prompt = prompts[taskType]; if (prompt) { // Extract mail body from prompt const parts = prompt.split('\n\n'); document.getElementById('eval-mail-text').value = parts.slice(1).join('\n\n'); showToast('Test-Beispiel geladen', 'success'); } } catch (error) { console.error('Error loading test prompt:', error); } } async function runComparison() { const taskType = document.getElementById('eval-task-type').value; const mailBody = document.getElementById('eval-mail-text').value; if (!mailBody) { showToast('Bitte Mail-Text eingeben', 'warning'); return; } document.getElementById('base-result').textContent = 'Generiere...'; document.getElementById('finetuned-result').textContent = 'Generiere...'; try { const result = await apiCall('/api/inference/compare', { method: 'POST', body: JSON.stringify({ task_type: taskType, mail_body: mailBody }) }); document.getElementById('base-result').textContent = result.base || 'Modell nicht geladen'; document.getElementById('finetuned-result').textContent = result.finetuned || 'Modell nicht geladen'; showToast('Vergleich abgeschlossen', 'success'); } catch (error) { console.error('Error running comparison:', error); document.getElementById('base-result').textContent = 'Fehler'; document.getElementById('finetuned-result').textContent = 'Fehler'; } } // ====================== // Init // ====================== document.addEventListener('DOMContentLoaded', () => { initNavigation(); initImport(); initLabeling(); initExport(); initTraining(); initEvaluation(); });