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.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
}
});