// Tab switching
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
tab.classList.add('active');
document.getElementById(tab.dataset.tab).classList.add('active');
});
});
function setExample(text) {
document.getElementById('singleText').value = text;
triggerLiveAnalysis(text);
}
async function post(url, body) {
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
return res.json();
}
// ── History tracking ──
let history = [];
function addToHistory(text, label, score) {
history.unshift({ text: text.substring(0, 100), label, score, time: new Date() });
if (history.length > 30) history.pop();
renderHistory();
updateHistoryChart();
}
function clearHistory() {
history = [];
renderHistory();
updateHistoryChart();
}
function renderHistory() {
const el = document.getElementById('historyList');
if (history.length === 0) {
el.innerHTML = '
No analyses yet. Go to Analyze tab and run some.
';
return;
}
el.innerHTML = history.map(h => `
${escapeHtml(h.text)}
${h.label}
${(h.score * 100).toFixed(1)}%
`).join('');
}
// ── History trend chart ──
let historyChart = null;
function updateHistoryChart() {
const ctx = document.getElementById('historyChart');
if (!ctx) return;
const labels = history.slice().reverse().map((_, i) => '#' + (i + 1));
const data = history.slice().reverse().map(h =>
h.label === 'POSITIVE' ? h.score : -h.score
);
if (historyChart) historyChart.destroy();
historyChart = new Chart(ctx, {
type: 'line',
data: {
labels,
datasets: [{
label: 'Sentiment',
data,
borderColor: '#6e8efb',
backgroundColor: 'rgba(110, 142, 251, 0.1)',
fill: true,
tension: 0.4,
pointBackgroundColor: data.map(v => v >= 0 ? '#34d399' : '#f87171'),
pointBorderColor: data.map(v => v >= 0 ? '#34d399' : '#f87171'),
pointRadius: 5,
}],
},
options: {
responsive: true,
plugins: {
legend: { display: false },
},
scales: {
y: {
min: -1, max: 1,
grid: { color: 'rgba(255,255,255,0.05)' },
ticks: { color: '#5a6080', callback: v => v === 0 ? 'Neutral' : v > 0 ? 'Pos' : 'Neg' },
},
x: {
grid: { display: false },
ticks: { color: '#5a6080' },
},
},
},
});
}
// ── Confidence ring animation ──
function drawConfidenceRing(score, label) {
const canvas = document.getElementById('confidenceRing');
const ctx = canvas.getContext('2d');
const size = 140;
const center = size / 2;
const radius = 55;
const lineWidth = 10;
ctx.clearRect(0, 0, size, size);
// Background ring
ctx.beginPath();
ctx.arc(center, center, radius, 0, Math.PI * 2);
ctx.strokeStyle = '#252a3a';
ctx.lineWidth = lineWidth;
ctx.stroke();
// Animated fill
const color = label === 'POSITIVE' ? '#34d399' : '#f87171';
const endAngle = -Math.PI / 2 + (Math.PI * 2 * score);
ctx.beginPath();
ctx.arc(center, center, radius, -Math.PI / 2, endAngle);
ctx.strokeStyle = color;
ctx.lineWidth = lineWidth;
ctx.lineCap = 'round';
ctx.stroke();
// Glow
ctx.shadowColor = color;
ctx.shadowBlur = 15;
ctx.beginPath();
ctx.arc(center, center, radius, endAngle - 0.1, endAngle);
ctx.strokeStyle = color;
ctx.lineWidth = lineWidth;
ctx.stroke();
ctx.shadowBlur = 0;
document.getElementById('ringPercent').textContent = (score * 100).toFixed(1) + '%';
}
// ── Emotion radar chart ──
let radarChart = null;
function drawEmotionRadar(emotions) {
const ctx = document.getElementById('emotionRadar');
const labels = Object.keys(emotions).map(e => e.charAt(0).toUpperCase() + e.slice(1));
const data = Object.values(emotions);
if (radarChart) radarChart.destroy();
radarChart = new Chart(ctx, {
type: 'radar',
data: {
labels,
datasets: [{
label: 'Emotion',
data,
backgroundColor: 'rgba(110, 142, 251, 0.2)',
borderColor: '#6e8efb',
borderWidth: 2,
pointBackgroundColor: '#6e8efb',
pointBorderColor: '#fff',
pointRadius: 4,
}],
},
options: {
responsive: true,
plugins: { legend: { display: false } },
scales: {
r: {
beginAtZero: true,
max: 1,
grid: { color: 'rgba(255,255,255,0.08)' },
angleLines: { color: 'rgba(255,255,255,0.08)' },
pointLabels: { color: '#c8cde0', font: { size: 12, family: 'Inter' } },
ticks: { display: false },
},
},
},
});
}
// ── Live typing analysis ──
let liveTimer = null;
document.getElementById('singleText').addEventListener('input', (e) => {
clearTimeout(liveTimer);
const text = e.target.value.trim();
if (text.length < 3) {
document.getElementById('liveIndicator').classList.add('hidden');
return;
}
liveTimer = setTimeout(() => triggerLiveAnalysis(text), 400);
});
async function triggerLiveAnalysis(text) {
if (text.length < 3) return;
document.getElementById('liveIndicator').classList.remove('hidden');
try {
const data = await post('/api/live', { text });
const el = document.getElementById('liveLabel');
el.textContent = data.label;
el.className = 'live-label ' + (data.label === 'POSITIVE' ? 'pos' : 'neg');
const meter = document.getElementById('liveMeter');
const pct = data.score * 100;
meter.style.width = pct + '%';
meter.style.background = data.label === 'POSITIVE' ? '#34d399' : '#f87171';
document.getElementById('liveScore').textContent = pct.toFixed(0) + '%';
} catch (_) {}
}
// ── Main analyze ──
async function analyzeSingle() {
const text = document.getElementById('singleText').value.trim();
if (!text) return;
const btn = document.querySelector('#single .btn-primary');
btn.disabled = true;
btn.textContent = 'Analyzing...';
try {
const data = await post('/api/analyze', { text });
const el = document.getElementById('singleResult');
el.classList.remove('hidden');
const labelEl = document.getElementById('singleLabel');
labelEl.textContent = data.label;
labelEl.className = 'label-tag ' + data.label;
document.getElementById('singleTime').textContent = data.ms + 'ms';
const bar = document.getElementById('singleBar');
bar.style.width = (data.score * 100) + '%';
bar.style.background = data.label === 'POSITIVE' ? 'var(--positive)' : 'var(--negative)';
document.getElementById('singleScore').textContent = 'Confidence: ' + (data.score * 100).toFixed(2) + '%';
drawConfidenceRing(data.score, data.label);
drawEmotionRadar(data.emotions);
addToHistory(text, data.label, data.score);
} finally {
btn.disabled = false;
btn.textContent = 'Analyze Sentiment';
}
}
// ── Batch with chart ──
let batchChart = null;
async function analyzeBatch() {
const raw = document.getElementById('batchText').value.trim();
if (!raw) return;
const texts = raw.split('\n').filter(l => l.trim());
const btn = document.querySelector('#batch .btn-primary');
btn.disabled = true;
btn.textContent = 'Analyzing ' + texts.length + ' texts...';
try {
const data = await post('/api/batch', { texts });
const el = document.getElementById('batchResult');
el.classList.remove('hidden');
const s = data.stats;
document.getElementById('batchStats').innerHTML = `
Text ${data.winner} is more positive · Gap: ${data.gap.toFixed(4)} · ${data.ms}ms
`;
addToHistory(a, data.a.label, data.a.score);
addToHistory(b, data.b.label, data.b.score);
} finally {
btn.disabled = false;
btn.textContent = 'Compare';
}
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}