// State let appState = { username: '', logs: [], stats: { total: 0, local: 0, yolo: 0, gemini: 0, groq: 0, hf_boss: 0 }, geminiEnabled: true }; // UI Elements const els = { tabs: document.querySelectorAll('.tab-content'), menuBtns: document.querySelectorAll('.menu-btn'), adminTabBtn: document.getElementById('admin-tab-btn'), // Login loginOverlay: document.getElementById('login-overlay'), loginUsername: document.getElementById('login-username'), loginPassword: document.getElementById('login-password'), authBtn: document.getElementById('auth-btn'), googleAuthBtn: document.getElementById('google-auth-btn'), authSwitchLink: document.getElementById('auth-switch-link'), authTitle: document.getElementById('auth-title'), authSubtitle: document.getElementById('auth-subtitle'), authError: document.getElementById('auth-error'), mainApp: document.getElementById('main-app'), displayUsername: document.getElementById('display-username'), logoutBtn: document.getElementById('logout-btn'), // Image Modal imageModal: document.getElementById('image-modal'), modalImg: document.getElementById('modal-img'), closeModal: document.getElementById('close-modal'), // VQA Elements dropZone: document.getElementById('drop-zone'), imageUpload: document.getElementById('image-upload'), imagePreview: document.getElementById('image-preview'), clearImageBtn: document.getElementById('clear-image-btn'), uploadPlaceholder: document.getElementById('upload-placeholder'), modelSelector: document.getElementById('model-selector'), langSelector: document.getElementById('lang-selector'), geminiOption: document.getElementById('gemini-option'), questionInput: document.getElementById('question-input'), askBtn: document.getElementById('ask-btn'), // Output Elements welcomeState: document.getElementById('welcome-state'), loadingIndicator: document.getElementById('loading-indicator'), resultsContainer: document.getElementById('results-container'), outCaption: document.getElementById('out-caption'), outAnswer: document.getElementById('out-answer'), outExplanation: document.getElementById('out-explanation'), // Stats & Admin statTotal: document.getElementById('stat-total'), statModels: document.getElementById('stat-models'), logsBody: document.getElementById('logs-body'), toggleGemini: document.getElementById('toggle-gemini'), logSearch: document.getElementById('log-search'), refreshLogsBtn: document.getElementById('refresh-logs-btn'), // Mobile mobileMenuBtn: document.getElementById('mobile-menu-btn'), sidebar: document.getElementById('sidebar') }; // --- Initialization --- function init() { setupEventListeners(); initChart(); initRealGoogleAuth(); } function initRealGoogleAuth() { const cid = document.querySelector('meta[name="google-signin-client_id"]').content; if (!cid || !window.google) return; google.accounts.id.initialize({ client_id: cid, callback: handleGoogleCredentialResponse, cancel_on_tap_outside: false }); } function handleGoogleCredentialResponse(response) { // This is the JWT token from Google const token = response.credential; const base64Url = token.split('.')[1]; const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); const jsonPayload = decodeURIComponent(atob(base64).split('').map(c => { return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); }).join('')); const payload = JSON.parse(jsonPayload); const email = payload.email; if (email) { authenticateViaGoogleEmail(email); } } async function authenticateViaGoogleEmail(email) { els.authError.style.display = 'none'; const originalBtnText = els.googleAuthBtn.innerHTML; els.googleAuthBtn.disabled = true; els.googleAuthBtn.innerText = "Authenticating..."; try { const res = await fetch('/api/google_login', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ email }) }); const data = await res.json(); if (data.status === 'success') { onLoginSuccess(data.user); } else { els.authError.innerText = data.message; els.authError.style.display = 'block'; } } catch(err) { els.authError.innerText = "Google Auth failed."; els.authError.style.display = 'block'; } finally { els.googleAuthBtn.disabled = false; els.googleAuthBtn.innerHTML = originalBtnText; } } // --- Event Listeners --- function setupEventListeners() { // Login System els.authBtn.addEventListener('click', handleAuth); els.loginPassword.addEventListener('keypress', (e) => { if (e.key === 'Enter') handleAuth(); }); els.authSwitchLink.addEventListener('click', toggleAuthMode); els.googleAuthBtn.addEventListener('click', handleGoogleAuth); els.logoutBtn.addEventListener('click', handleLogout); // Tabs els.menuBtns.forEach(btn => { btn.addEventListener('click', () => switchTab(btn.dataset.tab)); }); // Image Upload & Zoom Modal els.dropZone.addEventListener('click', (e) => { // Stop click from uploading if they clicked the clear button if(e.target.closest('#clear-image-btn')) { clearImage(); return; } // If they click the image directly, open zoom modal if (e.target.id === 'image-preview' && !els.imagePreview.classList.contains('hidden')) { els.modalImg.src = els.imagePreview.src; els.imageModal.classList.remove('hidden'); } else { els.imageUpload.click(); } }); els.closeModal.addEventListener('click', () => { els.imageModal.classList.add('hidden'); }); els.imageModal.addEventListener('click', (e) => { if(e.target === els.imageModal) els.imageModal.classList.add('hidden'); }); // Drag and Drop els.dropZone.addEventListener('dragover', (e) => { e.preventDefault(); els.dropZone.style.borderColor = 'var(--accent)'; }); els.dropZone.addEventListener('dragleave', () => { els.dropZone.style.borderColor = 'var(--border-color)'; }); els.dropZone.addEventListener('drop', (e) => { e.preventDefault(); els.dropZone.style.borderColor = 'var(--border-color)'; if (e.dataTransfer.files.length) handleImageUpload(e.dataTransfer.files[0]); }); els.imageUpload.addEventListener('change', (e) => { if (e.target.files.length) handleImageUpload(e.target.files[0]); }); // Suggestions API (New Cards) document.querySelectorAll('.sug-card').forEach(card => { card.addEventListener('click', (e) => { const text = card.querySelector('span').innerText; els.questionInput.value = text; }); }); // Ask Button els.askBtn.addEventListener('click', handleAskAI); els.questionInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') handleAskAI(); }); // Voice Input & Output setupVoiceFeatures(); // Admin Toggle if (els.toggleGemini) { els.toggleGemini.addEventListener('change', (e) => { appState.geminiEnabled = e.target.checked; if (els.geminiOption) els.geminiOption.disabled = !appState.geminiEnabled; if (!appState.geminiEnabled && els.modelSelector.value === 'gemini') { els.modelSelector.value = 'local'; } }); } // Log search & Refresh els.logSearch.addEventListener('input', (e) => renderLogs(e.target.value)); els.refreshLogsBtn.addEventListener('click', fetchLogs); // Mobile Sidebar if (els.mobileMenuBtn) { els.mobileMenuBtn.addEventListener('click', () => { els.sidebar.classList.toggle('open'); }); } // Close sidebar on mobile when clicking a tab els.menuBtns.forEach(btn => { btn.addEventListener('click', () => { if (window.innerWidth <= 900) { els.sidebar.classList.remove('open'); } }); }); } // --- Login System --- let isLoginMode = true; function toggleAuthMode(e) { e.preventDefault(); isLoginMode = !isLoginMode; els.authError.style.display = 'none'; if (isLoginMode) { els.authTitle.innerText = "GenAI VQA Portal"; els.authSubtitle.innerText = "Sign in to access the system"; els.authBtn.innerHTML = 'Login '; document.getElementById('auth-switch-text').innerText = "Don't have an account?"; els.authSwitchLink.innerText = "Sign Up"; } else { els.authTitle.innerText = "Create Account"; els.authSubtitle.innerText = "Register to save your logs"; els.authBtn.innerHTML = 'Sign Up '; document.getElementById('auth-switch-text').innerText = "Already have an account?"; els.authSwitchLink.innerText = "Login"; } } async function handleAuth() { const username = els.loginUsername.value.trim().toLowerCase(); const password = els.loginPassword.value.trim(); if (!username || !password) { els.authError.innerText = "Username and password required."; els.authError.style.display = 'block'; return; } els.authBtn.disabled = true; els.authBtn.innerText = "Processing..."; const endpoint = isLoginMode ? '/api/login' : '/api/signup'; try { const res = await fetch(endpoint, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({username, password}) }); const data = await res.json(); if (data.status === 'success') { onLoginSuccess(data.user); } else { els.authError.innerText = data.message; els.authError.style.display = 'block'; } } catch(err) { els.authError.innerText = "Server connection error."; els.authError.style.display = 'block'; } finally { els.authBtn.disabled = false; els.authBtn.innerHTML = isLoginMode ? 'Login ' : 'Sign Up '; } } async function handleGoogleAuth() { const cid = document.querySelector('meta[name="google-signin-client_id"]').content; if (cid && window.google) { google.accounts.id.prompt(); // Shows the Google One-Tap/Login popup } else { // Fallback simulation if no Client ID is provided els.authError.style.display = 'none'; const email = prompt("Google Sign In (Simulation):\n\nPlease enter your Google email address to authenticate:"); if (!email) return; if (!email.includes("@")) return alert("Please enter a valid email."); authenticateViaGoogleEmail(email); } } function onLoginSuccess(user) { appState.username = user; els.loginOverlay.classList.add('hidden'); els.mainApp.classList.remove('hidden'); els.displayUsername.innerText = appState.username; if (appState.username === 'vignesh') { els.adminTabBtn.style.display = 'flex'; } else { els.adminTabBtn.style.display = 'none'; if (document.querySelector('.menu-btn.active').dataset.tab === 'admin') switchTab('dashboard'); } fetchLogs(); speak(`Welcome to GenAI Vision System, ${appState.username}`); } function handleLogout() { appState.username = ''; els.mainApp.classList.add('hidden'); els.loginOverlay.classList.remove('hidden'); els.loginUsername.value = ''; els.loginPassword.value = ''; switchTab('dashboard'); } // --- Voice Features (STT & TTS) --- function setupVoiceFeatures() { // Text To Speech document.querySelectorAll('.tts-btn').forEach(btn => { btn.addEventListener('click', (e) => { const targetId = e.currentTarget.dataset.target; const targetEl = document.getElementById(targetId); if (targetEl) speak(targetEl.innerText); }); }); // Speech To Text const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; const micBtn = document.getElementById('mic-btn'); if (SpeechRecognition) { const recognition = new SpeechRecognition(); recognition.continuous = false; recognition.interimResults = false; micBtn.addEventListener('click', () => { const lang = document.getElementById('lang-selector').value; recognition.lang = lang === 'en' ? 'en-US' : lang; // Convert locale code natively micBtn.classList.add('recording'); recognition.start(); }); recognition.onresult = (event) => { const transcript = event.results[0][0].transcript; els.questionInput.value = transcript; micBtn.classList.remove('recording'); }; recognition.onerror = () => micBtn.classList.remove('recording'); recognition.onend = () => micBtn.classList.remove('recording'); } else { micBtn.addEventListener('click', () => alert("Microphone not supported in this browser.")); } } function speak(text) { if(!text) return; window.speechSynthesis.cancel(); const utterance = new SpeechSynthesisUtterance(text); const lang = document.getElementById('lang-selector').value; utterance.lang = lang === 'en' ? 'en-US' : lang; utterance.rate = window.navigator.userAgent.includes("Mac OS") ? 1.0 : 1.1; window.speechSynthesis.speak(utterance); } // --- Tab System --- function switchTab(tabId) { els.menuBtns.forEach(btn => btn.classList.remove('active')); document.querySelector(`.menu-btn[data-tab="${tabId}"]`).classList.add('active'); els.tabs.forEach(tab => { tab.style.display = 'none'; tab.classList.remove('active'); }); const target = document.getElementById(tabId); target.style.display = 'block'; requestAnimationFrame(() => target.classList.add('active')); if (tabId === 'analytics') updateChart(); if (tabId === 'logs') fetchLogs(); } // --- Image Handling --- let currentFile = null; function handleImageUpload(file) { if (!file.type.startsWith('image/')) return alert("Please upload an image file."); currentFile = file; const reader = new FileReader(); reader.onload = (e) => { els.imagePreview.src = e.target.result; els.imagePreview.classList.remove('hidden'); if (els.clearImageBtn) els.clearImageBtn.classList.remove('hidden'); els.uploadPlaceholder.classList.add('hidden'); }; reader.readAsDataURL(file); } function clearImage() { currentFile = null; els.imageUpload.value = ""; els.imagePreview.src = ""; els.imagePreview.classList.add('hidden'); if (els.clearImageBtn) els.clearImageBtn.classList.add('hidden'); els.uploadPlaceholder.classList.remove('hidden'); // Reset answers els.resultsContainer.classList.add('hidden'); els.welcomeState.classList.remove('hidden'); } // --- AI Interaction --- async function handleAskAI() { const question = els.questionInput.value.trim(); if (!currentFile) return alert("Please upload an image first."); if (!question) return alert("Please ask a question."); els.askBtn.disabled = true; els.askBtn.innerHTML = ' Processing...'; els.welcomeState.classList.add('hidden'); els.resultsContainer.classList.add('hidden'); els.outCaption.innerHTML = ''; els.outAnswer.innerHTML = ''; els.outExplanation.innerHTML = ''; els.loadingIndicator.classList.remove('hidden'); const formData = new FormData(); formData.append('file', currentFile); formData.append('question', question); formData.append('model_choice', els.modelSelector.value); formData.append('lang', els.langSelector.value); formData.append('user', appState.username || "guest"); try { const response = await fetch('/ask', { method: 'POST', body: formData }); const data = await response.json(); els.loadingIndicator.classList.add('hidden'); els.resultsContainer.classList.remove('hidden'); const cards = document.querySelectorAll('.result-card'); cards.forEach(card => { card.classList.remove('fade-in'); void card.offsetWidth; card.classList.add('fade-in'); }); typeWriter(els.outCaption, data.caption || "No caption provided"); setTimeout(() => typeWriter(els.outAnswer, data.answer || "No answer provided"), 500); setTimeout(() => typeWriter(els.outExplanation, data.explanation || "No explanation provided"), 1000); fetchLogs(); } catch (error) { console.error(error); alert("Error connecting to backend API."); els.loadingIndicator.classList.add('hidden'); } finally { els.askBtn.disabled = false; els.askBtn.innerHTML = ' Ask AI'; } } function typeWriter(element, text) { let formattedText = text.replace(/\*\*(.*?)\*\*/g, '$1'); element.innerHTML = formattedText; } // --- Analytics & Logs API --- async function fetchLogs() { if(!appState.username) return; try { const url = `/api/logs?user=${encodeURIComponent(appState.username)}`; const res = await fetch(url); const data = await res.json(); appState.logs = data.logs || []; appState.stats.total = appState.logs.length; appState.stats.local = appState.logs.filter(l => ['local', 'blip'].includes(l.model.toLowerCase())).length; appState.stats.yolo = appState.logs.filter(l => ['yolo', 'local_yolo'].includes(l.model.toLowerCase())).length; appState.stats.gemini = appState.logs.filter(l => ['gemini', 'external'].includes(l.model.toLowerCase())).length; appState.stats.groq = appState.logs.filter(l => l.model.toLowerCase().includes('groq')).length; appState.stats.hf_boss = appState.logs.filter(l => l.model.toLowerCase().includes('hf_boss') || l.model.toLowerCase().includes('boss') || l.model.toLowerCase().includes('qwen')).length; updateDashboardView(); } catch(err) { console.error("Failed fetching logs:", err); } } function updateDashboardView() { els.statTotal.innerText = appState.stats.total; els.statModels.innerText = `${appState.stats.local} Local / ${appState.stats.gemini + appState.stats.groq} Cloud`; renderLogs(); updateChart(); } function renderLogs(filter = '') { els.logsBody.innerHTML = ''; appState.logs .filter(log => log.question.toLowerCase().includes(filter.toLowerCase()) || log.model.includes(filter)) .forEach(log => { const tr = document.createElement('tr'); tr.innerHTML = `