/** * app.js — Robust Frontend logic for Dual Model Image Captioning Web App */ // Helper to get DOM elements safely const getElem = (id) => document.getElementById(id); let currentFile = null; // ============================================================ // CHECK MODEL STATUS // ============================================================ async function checkModelStatus() { const modelBadge = getElem('model-badge'); if (!modelBadge) return; try { const res = await fetch('/api/status'); const data = await res.json(); modelBadge.classList.remove('hidden'); if (data.status === 'ready') { const hasFt = data.has_finetuned ? 'Fine-Tuned & Pretrained Available' : 'Pretrained Available'; modelBadge.textContent = `⚡ Status: Ready (${hasFt}) | Device: ${data.device}`; modelBadge.className = 'model-badge ready'; } else { modelBadge.textContent = '⏳ Models loading...'; modelBadge.className = 'model-badge loading'; } } catch (e) { console.error('Status check failed:', e); } } // Get selected model mode function getSelectedModelChoice() { const radio = document.querySelector('input[name="model_choice"]:checked'); return radio ? radio.value : 'both'; } // Get selected language choice function getSelectedLangChoice() { const radio = document.querySelector('input[name="lang_choice"]:checked'); return radio ? radio.value : 'both'; } // ============================================================ // UI VISIBILITY HELPERS // ============================================================ function showResultArea() { const dropZone = getElem('drop-zone'); const resultArea = getElem('result-area'); const resultsGrid = getElem('results-grid'); const loadingSpinner = getElem('loading-spinner'); if (dropZone) dropZone.classList.add('hidden'); if (resultArea) resultArea.classList.remove('hidden'); if (resultsGrid) resultsGrid.innerHTML = ''; if (loadingSpinner) loadingSpinner.classList.add('active'); } function showDropZone() { const dropZone = getElem('drop-zone'); const resultArea = getElem('result-area'); const fileInput = getElem('file-input'); if (resultArea) resultArea.classList.add('hidden'); if (dropZone) dropZone.classList.remove('hidden'); if (fileInput) fileInput.value = ''; currentFile = null; hideError(); } function showError(message) { const errorMsg = getElem('error-msg'); if (errorMsg) { errorMsg.textContent = message; errorMsg.classList.remove('hidden'); } } function hideError() { const errorMsg = getElem('error-msg'); if (errorMsg) errorMsg.classList.add('hidden'); } // ============================================================ // HANDLE FILE // ============================================================ function handleFile(file) { if (!file.type.startsWith('image/')) { showError('Please upload an image file (JPEG, PNG, GIF, WebP).'); return; } currentFile = file; hideError(); const previewImg = getElem('preview-img'); const reader = new FileReader(); reader.onload = (e) => { if (previewImg) previewImg.src = e.target.result; showResultArea(); generateCaption(file); }; reader.readAsDataURL(file); } // ============================================================ // GENERATE CAPTION (API CALL) // ============================================================ async function generateCaption(file) { const loadingSpinner = getElem('loading-spinner'); const resultsGrid = getElem('results-grid'); const modelChoice = getSelectedModelChoice(); const langChoice = getSelectedLangChoice(); const formData = new FormData(); formData.append('file', file); formData.append('model_choice', modelChoice); formData.append('language', langChoice); if (loadingSpinner) loadingSpinner.classList.add('active'); if (resultsGrid) resultsGrid.innerHTML = ''; try { const res = await fetch('/api/predict', { method: 'POST', body: formData, }); const data = await res.json(); if (!res.ok) { throw new Error(data.detail || 'Inference failed'); } if (loadingSpinner) loadingSpinner.classList.remove('active'); renderResults(data.results, langChoice); } catch (err) { if (loadingSpinner) loadingSpinner.classList.remove('active'); showError(err.message); console.error('Prediction error:', err); } } // ============================================================ // RENDER RESULTS GRID // ============================================================ function renderResults(results, langChoice = 'both') { let resultsGrid = getElem('results-grid'); // Fallback: if results-grid is missing, create it dynamically inside result-area if (!resultsGrid) { const resultArea = getElem('result-area'); if (resultArea) { resultsGrid = document.createElement('div'); resultsGrid.id = 'results-grid'; resultsGrid.className = 'results-grid'; const clearBtn = getElem('clear-btn'); if (clearBtn) { resultArea.insertBefore(resultsGrid, clearBtn); } else { resultArea.appendChild(resultsGrid); } } } if (!resultsGrid) return; resultsGrid.innerHTML = ''; if (!results) return; const keys = Object.keys(results); const isGrid = keys.length > 1; if (isGrid) { resultsGrid.classList.add('dual-grid'); } else { resultsGrid.classList.remove('dual-grid'); } keys.forEach(key => { const item = results[key]; const isFineTuned = key === 'fine-tuned'; const card = document.createElement('div'); card.className = `caption-card ${isFineTuned ? 'fine-tuned-card' : 'pretrained-card'}`; const captions = item.captions || []; const captionsTh = item.captions_th || []; const captionsHtml = captions .map((c, i) => { const thText = captionsTh[i] || ''; let contentHtml = ''; if (langChoice === 'en') { contentHtml = `${c}`; } else if (langChoice === 'th') { contentHtml = `${thText || c}`; } else { // Both contentHtml = `