Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>NeuraPrompt AI - Admin Panel</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet"> | |
| <script type="module" src="https://unpkg.com/ionicons@5.5.2/dist/ionicons/ionicons.esm.js"></script> | |
| <script nomodule src="https://unpkg.com/ionicons@5.5.2/dist/ionicons/ionicons.js"></script> | |
| <style> | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| background-color: #111827; /* bg-gray-900 */ | |
| color: #d1d5db; /* text-gray-300 */ | |
| } | |
| .sidebar { background-color: #1f2937; /* bg-gray-800 */ } | |
| .sidebar-link { @apply flex items-center p-3 my-1 rounded-lg text-gray-300 hover:bg-gray-700 transition-colors; } | |
| .sidebar-link.active { @apply bg-blue-600 text-white; } | |
| .card { @apply bg-gray-800 p-6 rounded-lg shadow-lg border border-gray-700; } | |
| .input-field { @apply w-full bg-gray-700 border border-gray-600 text-white rounded-lg p-2.5 focus:ring-blue-500 focus:border-blue-500; } | |
| .btn { @apply px-4 py-2 rounded-lg font-semibold transition-colors; } | |
| .btn-primary { @apply bg-blue-600 text-white hover:bg-blue-700; } | |
| .btn-secondary { @apply bg-gray-600 text-white hover:bg-gray-700; } | |
| .btn-danger { @apply bg-red-600 text-white hover:bg-red-700; } | |
| .btn-success { @apply bg-green-600 text-white hover:bg-green-700; } | |
| .toast { | |
| animation: toast-in 0.5s, toast-out 0.5s 4.5s; | |
| @apply fixed bottom-5 right-5 p-4 rounded-lg shadow-lg text-white; | |
| } | |
| .toast.success { @apply bg-green-600; } | |
| .toast.error { @apply bg-red-600; } | |
| @keyframes toast-in { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } | |
| @keyframes toast-out { from { transform: translateX(0); opacity: 1; } to { transform: translateX(100%); opacity: 0; } } | |
| </style> | |
| </head> | |
| <body class="flex h-screen"> | |
| <!-- Sidebar Navigation --> | |
| <aside class="sidebar w-64 flex-shrink-0 p-4"> | |
| <div class="flex items-center gap-3 mb-8"> | |
| <ion-icon name="hardware-chip-outline" class="text-3xl text-blue-400"></ion-icon> | |
| <h1 class="text-xl font-bold text-white">NeuraPrompt AI</h1> | |
| </div> | |
| <nav> | |
| <a href="#dashboard" class="sidebar-link active" data-target="dashboard-section"> | |
| <ion-icon name="grid-outline" class="mr-3"></ion-icon> Dashboard | |
| </a> | |
| <a href="#image-verification" class="sidebar-link" data-target="image-verification-section"> | |
| <ion-icon name="checkbox-outline" class="mr-3"></ion-icon> Image Verification | |
| <span id="pending-count-badge" class="ml-auto bg-yellow-500 text-black text-xs font-bold px-2 py-0.5 rounded-full hidden">0</span> | |
| </a> | |
| <a href="#model-training" class="sidebar-link" data-target="model-training-section"> | |
| <ion-icon name="school-outline" class="mr-3"></ion-icon> Model Training | |
| </a> | |
| <a href="#system-management" class="sidebar-link" data-target="system-management-section"> | |
| <ion-icon name="server-outline" class="mr-3"></ion-icon> System Management | |
| </a> | |
| </nav> | |
| </aside> | |
| <!-- Main Content --> | |
| <main class="flex-1 p-8 overflow-y-auto"> | |
| <!-- Dashboard Section --> | |
| <section id="dashboard-section" class="content-section"> | |
| <h2 class="text-3xl font-bold text-white mb-6">Admin Dashboard</h2> | |
| <div class="grid grid-cols-1 md:grid-cols-3 gap-6"> | |
| <div class="card"> | |
| <h3 class="text-lg font-semibold text-gray-400">Pending Images</h3> | |
| <p id="pending-images-stat" class="text-4xl font-bold text-white mt-2">0</p> | |
| </div> | |
| <div class="card"> | |
| <h3 class="text-lg font-semibold text-gray-400">Text AI Model</h3> | |
| <p class="text-4xl font-bold text-white mt-2">Neurones Self</p> | |
| </div> | |
| <div class="card"> | |
| <h3 class="text-lg font-semibold text-gray-400">Image AI Model</h3> | |
| <p class="text-4xl font-bold text-white mt-2">Custom Classifier</p> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Image Verification Section --> | |
| <section id="image-verification-section" class="content-section hidden"> | |
| <div class="flex justify-between items-center mb-6"> | |
| <h2 class="text-3xl font-bold text-white">Image Verification Queue</h2> | |
| <button id="refresh-pending-btn" class="btn btn-secondary flex items-center gap-2"> | |
| <ion-icon name="refresh-outline"></ion-icon> Refresh | |
| </button> | |
| </div> | |
| <div id="pending-images-container" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"> | |
| <!-- Pending images will be loaded here --> | |
| <p id="no-pending-message">No images pending verification.</p> | |
| </div> | |
| </section> | |
| <!-- Model Training Section --> | |
| <section id="model-training-section" class="content-section hidden"> | |
| <h2 class="text-3xl font-bold text-white mb-6">Model Training</h2> | |
| <div class="grid grid-cols-1 lg:grid-cols-2 gap-8"> | |
| <!-- Manual Text Model Training --> | |
| <div class="card"> | |
| <h3 class="text-xl font-semibold mb-4">Manual Text Model Training</h3> | |
| <form id="train-text-form" class="space-y-4"> | |
| <div> | |
| <label for="prompt" class="block mb-2 text-sm font-medium">User Prompt</label> | |
| <textarea id="prompt" name="prompt" rows="3" class="input-field" required></textarea> | |
| </div> | |
| <div> | |
| <label for="reply" class="block mb-2 text-sm font-medium">AI Reply</label> | |
| <textarea id="reply" name="reply" rows="3" class="input-field" required></textarea> | |
| </div> | |
| <div> | |
| <label for="model_name_text" class="block mb-2 text-sm font-medium">Model</label> | |
| <select id="model_name_text" name="model_name" class="input-field"> | |
| <option value="neurones_self">Neurones Self</option> | |
| </select> | |
| </div> | |
| <button type="submit" class="btn btn-primary w-full">Train Text Model</button> | |
| </form> | |
| </div> | |
| <!-- Custom Image Model Training --> | |
| <div class="card"> | |
| <h3 class="text-xl font-semibold mb-4">Custom Image Model Training</h3> | |
| <p class="text-gray-400 mb-4">Trigger a training run using all approved images in the dataset. This can take some time.</p> | |
| <button id="train-image-btn" class="btn btn-primary w-full">Start Image Model Training</button> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- System Management Section --> | |
| <section id="system-management-section" class="content-section hidden"> | |
| <h2 class="text-3xl font-bold text-white mb-6">System Management</h2> | |
| <div class="card border-red-500/50"> | |
| <h3 class="text-xl font-semibold text-red-400 mb-2">Danger Zone</h3> | |
| <p class="text-gray-400 mb-4">These actions are irreversible. Please be certain before proceeding.</p> | |
| <form id="reset-ai-form" class="flex items-center gap-4"> | |
| <select id="model_name_reset" name="model_name" class="input-field"> | |
| <option value="neurones_self">Neurones Self</option> | |
| </select> | |
| <button type="submit" class="btn btn-danger flex-shrink-0">Reset AI Model</button> | |
| </form> | |
| </div> | |
| </section> | |
| </main> | |
| <div id="toast-container"></div> | |
| <script> | |
| const API_BASE_URL = window.location.origin; | |
| // --- Navigation Logic --- | |
| const navLinks = document.querySelectorAll('.sidebar-link'); | |
| const contentSections = document.querySelectorAll('.content-section'); | |
| navLinks.forEach(link => { | |
| link.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| const targetId = link.getAttribute('data-target'); | |
| navLinks.forEach(l => l.classList.remove('active')); | |
| link.classList.add('active'); | |
| contentSections.forEach(section => { | |
| section.classList.add('hidden'); | |
| if (section.id === targetId) { | |
| section.classList.remove('hidden'); | |
| } | |
| }); | |
| if (targetId === 'image-verification-section') { | |
| loadPendingImages(); | |
| } | |
| }); | |
| }); | |
| // --- Toast Notifications --- | |
| function showToast(message, type = 'success') { | |
| const toastContainer = document.getElementById('toast-container'); | |
| const toast = document.createElement('div'); | |
| toast.className = `toast ${type}`; | |
| toast.textContent = message; | |
| toastContainer.appendChild(toast); | |
| setTimeout(() => toast.remove(), 5000); | |
| } | |
| // --- Image Verification Logic --- | |
| const pendingContainer = document.getElementById('pending-images-container'); | |
| const noPendingMessage = document.getElementById('no-pending-message'); | |
| const pendingBadge = document.getElementById('pending-count-badge'); | |
| const pendingStat = document.getElementById('pending-images-stat'); | |
| async function loadPendingImages() { | |
| try { | |
| const response = await fetch(`${API_BASE_URL}/admin/pending-images`); | |
| if (!response.ok) throw new Error('Failed to fetch pending images.'); | |
| const images = await response.json(); | |
| pendingContainer.innerHTML = ''; | |
| if (images.length === 0) { | |
| pendingContainer.appendChild(noPendingMessage); | |
| noPendingMessage.classList.remove('hidden'); | |
| } else { | |
| noPendingMessage.classList.add('hidden'); | |
| } | |
| pendingBadge.textContent = images.length; | |
| pendingStat.textContent = images.length; | |
| pendingBadge.classList.toggle('hidden', images.length === 0); | |
| images.forEach(image => { | |
| const card = document.createElement('div'); | |
| card.className = 'card space-y-3'; | |
| card.innerHTML = ` | |
| <img src="data:${image.content_type};base64,${image.image_data_b64}" class="rounded-md w-full h-40 object-cover"> | |
| <div> | |
| <label class="text-xs text-gray-400">User Label</label> | |
| <p class="font-semibold">${image.user_label}</p> | |
| </div> | |
| <div class="flex gap-2 pt-2"> | |
| <button class="btn btn-success w-full" onclick="handleImageAction('${image.id}', 'approve')">Approve</button> | |
| <button class="btn btn-danger w-full" onclick="handleImageAction('${image.id}', 'reject')">Reject</button> | |
| </div> | |
| `; | |
| pendingContainer.appendChild(card); | |
| }); | |
| } catch (error) { | |
| showToast(error.message, 'error'); | |
| } | |
| } | |
| async function handleImageAction(id, action) { | |
| const endpoint = action === 'approve' ? `/admin/approve-image/${id}` : `/admin/reject-image/${id}`; | |
| try { | |
| const response = await fetch(`${API_BASE_URL}${endpoint}`, { method: 'POST' }); | |
| const result = await response.json(); | |
| if (!response.ok) throw new Error(result.detail); | |
| showToast(result.message, 'success'); | |
| loadPendingImages(); // Refresh the list | |
| } catch (error) { | |
| showToast(error.message, 'error'); | |
| } | |
| } | |
| document.getElementById('refresh-pending-btn').addEventListener('click', loadPendingImages); | |
| // --- Model Training Logic --- | |
| document.getElementById('train-text-form').addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| const formData = new FormData(e.target); | |
| try { | |
| const response = await fetch(`${API_BASE_URL}/admin/train/`, { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| const result = await response.json(); | |
| if (!response.ok) throw new Error(result.detail); | |
| showToast(result.message, 'success'); | |
| e.target.reset(); | |
| } catch (error) { | |
| showToast(error.message, 'error'); | |
| } | |
| }); | |
| document.getElementById('train-image-btn').addEventListener('click', async () => { | |
| if (!confirm('Are you sure you want to start image model training? This may take several minutes and consume resources.')) return; | |
| showToast('Starting image model training...', 'success'); | |
| try { | |
| const response = await fetch(`${API_BASE_URL}/admin/train-image-model/`, { method: 'POST' }); | |
| const result = await response.json(); | |
| if (!response.ok) throw new Error(result.detail); | |
| showToast(`Training complete! Accuracy: ${result.final_validation_accuracy}`, 'success'); | |
| } catch (error) { | |
| showToast(error.message, 'error'); | |
| } | |
| }); | |
| // --- System Management Logic --- | |
| document.getElementById('reset-ai-form').addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| const modelName = document.getElementById('model_name_reset').value; | |
| if (!confirm(`Are you absolutely sure you want to reset the '${modelName}' AI model? All its training data will be permanently deleted.`)) return; | |
| try { | |
| const response = await fetch(`${API_BASE_URL}/admin/reset-ai/?model_name=${modelName}`, { method: 'POST' }); | |
| const result = await response.json(); | |
| if (!response.ok) throw new Error(result.detail); | |
| showToast(result.message, 'success'); | |
| } catch (error) { | |
| showToast(error.message, 'error'); | |
| } | |
| }); | |
| // Initial Load | |
| window.onload = () => { | |
| loadPendingImages(); | |
| }; | |
| </script> | |
| </body> | |
| </html> | |