document.addEventListener('DOMContentLoaded', () => { // Configure API endpoint: prefer window.SENSECATCH_API_BASE if set const DEFAULT_API = '/analyze'; const API_BASE = (typeof window !== 'undefined' && window.SENSECATCH_API_BASE) ? window.SENSECATCH_API_BASE : DEFAULT_API; const textInput = document.getElementById('text-input'); const modelSelector = document.getElementById('model-selector'); const analyzeBtn = document.getElementById('analyze-btn'); const resultDisplay = document.getElementById('result-display'); const historyList = document.getElementById('history-list'); // Show the current year in the footer const yearEl = document.getElementById('year'); if (yearEl) { yearEl.textContent = new Date().getFullYear(); } // Store analysis history const analysisHistory = []; // Banner helpers const infoBanner = document.getElementById('inline-info'); const infoText = document.getElementById('info-text'); const infoClose = document.getElementById('info-close'); const FIRST_VISIT_KEY = 'sc_first_visit_shown_v1'; const LAST_SUCCESS_TS = 'sc_last_success_ts_v1'; const REWARM_SECS = 15 * 60; // 15 minutes function showInfo(message) { if (!infoBanner) return; infoText.textContent = message; infoBanner.classList.remove('hidden'); } function hideInfo() { if (!infoBanner) return; infoBanner.classList.add('hidden'); } if (infoClose) { infoClose.addEventListener('click', hideInfo); } // Initial banner for first click per session function maybeShowFirstVisitBanner() { if (!sessionStorage.getItem(FIRST_VISIT_KEY)) { showInfo('Waking up the machine learning models!\nFirst request may take up to ~40 seconds.'); sessionStorage.setItem(FIRST_VISIT_KEY, '1'); } } // Re-warm banner if app likely slept again (no success for 15+ mins) function maybeShowRewarmBanner() { const last = Number(sessionStorage.getItem(LAST_SUCCESS_TS) || '0'); const now = Date.now() / 1000; if (!last || (now - last) > REWARM_SECS) { showInfo('The ML models went to sleep due to inactivity. Waking them up now…\n(this may take up to ~40 seconds)'); } } // Handle analyze button click analyzeBtn.addEventListener('click', async () => { const text = textInput.value.trim(); const model = modelSelector.value; if (!text) { alert('Please enter some text to analyze.'); return; } // Show inline info if first visit and if rewarm needed maybeShowFirstVisitBanner(); maybeShowRewarmBanner(); // Show loading state analyzeBtn.disabled = true; analyzeBtn.textContent = 'Analyzing...'; resultDisplay.innerHTML = '

Analyzing your text...

'; try { // Send request to server const response = await fetch(API_BASE, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text, model }) }); if (!response.ok) { throw new Error('Server error: ' + response.statusText); } const result = await response.json(); // Success - record timestamp and hide banner sessionStorage.setItem(LAST_SUCCESS_TS, String(Math.floor(Date.now()/1000))); hideInfo(); // Display result displayResult(result); // Add to history addToHistory(result); } catch (error) { console.error('Error analyzing text:', error); resultDisplay.innerHTML = `

Error analyzing text: ${error.message || 'Unknown error'}. Please try again.

`; } finally { // Reset button state analyzeBtn.disabled = false; analyzeBtn.textContent = 'Analyze Sentiment'; } }); // Display analysis result function displayResult(result) { const sentimentClass = result.sentiment === 'Positive' ? 'positive' : 'negative'; // Build the influential-words block only when the model returned words. let wordsBlock = ''; if (result.important_words && result.important_words.length > 0) { const wordChips = result.important_words.map(wordInfo => { const chipClass = wordInfo.sentiment === 'positive' ? 'positive' : 'negative'; // Add strikethrough styling for negated words const negatedStyle = wordInfo.negated ? 'text-decoration: line-through;' : ''; return `${wordInfo.word}`; }).join(''); wordsBlock = `

Influential Words:

${wordChips}
`; } resultDisplay.innerHTML = `
${result.sentiment} ${result.confidence}% confidence
"${result.text}"
${wordsBlock}

Model: ${formatModelName(result.model)}

`; } // Add result to history function addToHistory(result) { // Add to history array (limit to 10 items) analysisHistory.unshift(result); if (analysisHistory.length > 10) { analysisHistory.pop(); } // Update history display updateHistoryDisplay(); } // Update history display function updateHistoryDisplay() { // Clear "no history" message historyList.innerHTML = ''; if (analysisHistory.length === 0) { historyList.innerHTML = '

Previous analyses will appear here.

'; return; } analysisHistory.forEach(item => { const sentimentClass = item.sentiment === 'Positive' ? 'positive' : 'negative'; const historyItem = document.createElement('div'); historyItem.className = 'history-item'; historyItem.innerHTML = `
${item.sentiment} (${item.confidence}%)
${truncateText(item.text, 60)}
Model: ${formatModelName(item.model)}
`; // Add click event to load this analysis again historyItem.addEventListener('click', () => { textInput.value = item.text; modelSelector.value = item.model; // Scroll to input textInput.scrollIntoView({ behavior: 'smooth' }); textInput.focus(); }); historyList.appendChild(historyItem); }); } // Helper function to format model name function formatModelName(modelKey) { switch(modelKey) { case 'naive_bayes': return 'Naive Bayes'; case 'logistic_regression': return 'Logistic Regression'; case 'linear_svc': return 'Linear SVC'; case 'nbsvm': return 'NBSVM'; case 'distilbert': return 'DistilBERT (fine-tuned)'; case 'stack': return 'Stacked Ensemble'; case 'rule_based': return 'Rule-based (linguistic)'; default: return modelKey; } } // Helper function to truncate text function truncateText(text, maxLength) { if (text.length <= maxLength) return text; return text.substr(0, maxLength) + '...'; } // Add focus to the text input on page load textInput.focus(); // Set up clear history button const clearHistoryBtn = document.getElementById('clear-history-btn'); clearHistoryBtn.addEventListener('click', () => { if (analysisHistory.length === 0) { return; // Nothing to clear } if (confirm('Are you sure you want to clear your analysis history?')) { // Clear history array analysisHistory.length = 0; // Update display updateHistoryDisplay(); } }); // Check backend health on load; show the banner only if it is slow to answer. const HEALTH_URL = API_BASE.replace(/\/analyze\/?$/, '/healthz'); if (HEALTH_URL !== API_BASE) { let healthTries = 0; const bannerTimer = setTimeout(() => { showInfo('Waking up the machine learning models!\nThe first analysis may take up to ~40 seconds.'); }, 1800); const checkHealth = () => { fetch(HEALTH_URL) .then(response => { if (response.ok) { clearTimeout(bannerTimer); hideInfo(); return; } scheduleHealthRetry(); }) .catch(() => scheduleHealthRetry()); }; const scheduleHealthRetry = () => { healthTries += 1; if (healthTries < 24) { setTimeout(checkHealth, 5000); } }; checkHealth(); } // Optional: background warm-up ping to reduce free-tier cold start delay try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 5000); fetch(API_BASE, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: 'warmup', model: 'naive_bayes' }), signal: controller.signal }).finally(() => clearTimeout(timeoutId)); } catch (_) { // ignore warm-up failures } });