/** * 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 = `
${c} ${thText ? `🇹🇭 ${thText}` : ''}
`; } return `
  • ${i + 1} ${contentHtml}
  • `; }) .join(''); card.innerHTML = `
    ${item.label} ${isFineTuned ? '🎯 Fine-Tuned' : '🌐 Pretrained'}
      ${captionsHtml}
    `; resultsGrid.appendChild(card); }); } // ============================================================ // INITIALIZATION ON DOM READY // ============================================================ document.addEventListener('DOMContentLoaded', () => { checkModelStatus(); const dropZone = getElem('drop-zone'); const fileInput = getElem('file-input'); const clearBtn = getElem('clear-btn'); if (dropZone) { 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'); const files = e.dataTransfer.files; if (files.length > 0) { handleFile(files[0]); } }); dropZone.addEventListener('click', () => { if (fileInput) fileInput.click(); }); } if (fileInput) { fileInput.addEventListener('change', (e) => { if (e.target.files.length > 0) { handleFile(e.target.files[0]); } }); } if (clearBtn) { clearBtn.addEventListener('click', showDropZone); } // Re-trigger generation if user switches model choice or language radio buttons while viewing results const retrigger = () => { const resultArea = getElem('result-area'); if (currentFile && resultArea && !resultArea.classList.contains('hidden')) { generateCaption(currentFile); } }; document.querySelectorAll('input[name="model_choice"]').forEach(radio => { radio.addEventListener('change', retrigger); }); document.querySelectorAll('input[name="lang_choice"]').forEach(radio => { radio.addEventListener('change', retrigger); }); }); // Global drag behaviors document.addEventListener('dragover', (e) => e.preventDefault()); document.addEventListener('drop', (e) => e.preventDefault());