|
|
const form = document.getElementById("analyze-form"); |
|
|
const statusBox = document.getElementById("status"); |
|
|
const metaBox = document.getElementById("meta"); |
|
|
|
|
|
const mHashtag = document.getElementById("m-hashtag"); |
|
|
const mGemini = document.getElementById("m-gemini"); |
|
|
const mFallback = document.getElementById("m-fallback"); |
|
|
const mModels = document.getElementById("m-models"); |
|
|
|
|
|
const pieDiv = document.getElementById("pie"); |
|
|
const lineDiv = document.getElementById("line"); |
|
|
const tableDiv = document.getElementById("table"); |
|
|
|
|
|
|
|
|
const cursor = document.getElementById("parallax-cursor"); |
|
|
window.addEventListener("mousemove", (e) => { |
|
|
const x = e.clientX, y = e.clientY; |
|
|
cursor.style.opacity = ".9"; |
|
|
cursor.style.left = x + "px"; |
|
|
cursor.style.top = y + "px"; |
|
|
}); |
|
|
|
|
|
|
|
|
function fmtPct(n){ return (Math.round(n * 100) / 100).toFixed(2); } |
|
|
|
|
|
function renderMeta(meta) { |
|
|
mHashtag.textContent = meta.hashtag; |
|
|
mGemini.textContent = `Gemini: ${meta.generated_by.gemini}`; |
|
|
mFallback.textContent = `Fallback: ${meta.generated_by.fallback}`; |
|
|
mModels.textContent = `Gen: ${meta.model.generation} • Sentiment: ${meta.model.sentiment}`; |
|
|
|
|
|
|
|
|
metaBox.style.opacity = "0"; |
|
|
metaBox.style.transform = "translateY(10px)"; |
|
|
requestAnimationFrame(() => { |
|
|
metaBox.style.transition = "all .6s ease"; |
|
|
metaBox.style.opacity = "1"; |
|
|
metaBox.style.transform = "translateY(0)"; |
|
|
}); |
|
|
} |
|
|
|
|
|
function renderPie(percent) { |
|
|
const data = [{ |
|
|
values: [percent.positive, percent.neutral, percent.negative], |
|
|
labels: ['Positive', 'Neutral', 'Negative'], |
|
|
type: 'pie', |
|
|
textinfo: 'label+percent', |
|
|
hoverinfo: 'label+percent', |
|
|
hole: .35 |
|
|
}]; |
|
|
const layout = { |
|
|
paper_bgcolor: 'rgba(0,0,0,0)', |
|
|
plot_bgcolor: 'rgba(0,0,0,0)', |
|
|
font: {color: '#eaf2ff'}, |
|
|
margin: {l: 4, r: 4, t: 0, b: 0}, |
|
|
showlegend: false, |
|
|
transition: {duration: 500, easing: "cubic-in-out"} |
|
|
}; |
|
|
Plotly.newPlot(pieDiv, data, layout, {displayModeBar:false, responsive:true}).then(() => { |
|
|
pieDiv.style.opacity = "0"; |
|
|
pieDiv.style.transform = "scale(0.9)"; |
|
|
requestAnimationFrame(() => { |
|
|
pieDiv.style.transition = "all .6s ease"; |
|
|
pieDiv.style.opacity = "1"; |
|
|
pieDiv.style.transform = "scale(1)"; |
|
|
}); |
|
|
}); |
|
|
} |
|
|
|
|
|
function renderLine(rolling) { |
|
|
const data = [{ |
|
|
x: [...Array(rolling.length).keys()].map(i => i+1), |
|
|
y: rolling, |
|
|
type: 'scatter', |
|
|
mode: 'lines+markers', |
|
|
line: {shape: 'spline', smoothing: 1.3} |
|
|
}]; |
|
|
const layout = { |
|
|
paper_bgcolor: 'rgba(0,0,0,0)', |
|
|
plot_bgcolor: 'rgba(0,0,0,0)', |
|
|
font: {color: '#eaf2ff'}, |
|
|
margin: {l: 30, r: 10, t: 0, b: 24}, |
|
|
yaxis: {range:[0,1], tickformat: '.0%'}, |
|
|
transition: {duration: 600, easing: "cubic-in-out"} |
|
|
}; |
|
|
Plotly.newPlot(lineDiv, data, layout, {displayModeBar:false, responsive:true}).then(() => { |
|
|
lineDiv.style.opacity = "0"; |
|
|
lineDiv.style.transform = "translateY(20px)"; |
|
|
requestAnimationFrame(() => { |
|
|
lineDiv.style.transition = "all .7s ease"; |
|
|
lineDiv.style.opacity = "1"; |
|
|
lineDiv.style.transform = "translateY(0)"; |
|
|
}); |
|
|
}); |
|
|
} |
|
|
|
|
|
function renderTable(rows) { |
|
|
tableDiv.innerHTML = ""; |
|
|
rows.forEach((r, i) => { |
|
|
const row = document.createElement("div"); |
|
|
row.className = "row"; |
|
|
|
|
|
|
|
|
const c1 = document.createElement("div"); |
|
|
c1.className = "cell"; |
|
|
c1.textContent = r.text; |
|
|
|
|
|
|
|
|
const c2 = document.createElement("div"); |
|
|
c2.className = "cell"; |
|
|
const chip = document.createElement("span"); |
|
|
chip.className = "chip " + (r.source === "gemini" ? "chip-gemini" : "chip-fallback"); |
|
|
chip.textContent = r.source === "gemini" ? "Gemini" : "Fallback"; |
|
|
c2.appendChild(chip); |
|
|
|
|
|
|
|
|
const c3 = document.createElement("div"); |
|
|
c3.className = "cell"; |
|
|
const badge = document.createElement("span"); |
|
|
const s = r.sentiment; |
|
|
badge.className = "badge " + (s === "POSITIVE" ? "pos" : s === "NEGATIVE" ? "neg" : "neu"); |
|
|
badge.textContent = s + " " + (r.score.toFixed(2)); |
|
|
c3.appendChild(badge); |
|
|
|
|
|
row.appendChild(c1); |
|
|
row.appendChild(c2); |
|
|
row.appendChild(c3); |
|
|
|
|
|
|
|
|
row.style.opacity = "0"; |
|
|
row.style.transform = "translateX(-15px)"; |
|
|
setTimeout(() => { |
|
|
row.style.transition = "all .4s ease"; |
|
|
row.style.opacity = "1"; |
|
|
row.style.transform = "translateX(0)"; |
|
|
}, 100 * i); |
|
|
|
|
|
tableDiv.appendChild(row); |
|
|
}); |
|
|
} |
|
|
|
|
|
form.addEventListener("submit", async (e) => { |
|
|
e.preventDefault(); |
|
|
const hashtag = document.getElementById("hashtag").value.trim(); |
|
|
const count = parseInt(document.getElementById("count").value || "20", 10); |
|
|
|
|
|
if(!hashtag){ |
|
|
alert("Please enter a hashtag (e.g., #gla)"); |
|
|
return; |
|
|
} |
|
|
|
|
|
statusBox.classList.remove("hidden"); |
|
|
metaBox.classList.add("hidden"); |
|
|
|
|
|
try { |
|
|
const resp = await fetch("/api/analyze", { |
|
|
method: "POST", |
|
|
headers: {"Content-Type": "application/json"}, |
|
|
body: JSON.stringify({hashtag, count}) |
|
|
}); |
|
|
if(!resp.ok){ |
|
|
const err = await resp.json().catch(()=>({})); |
|
|
throw new Error(err.error || `HTTP ${resp.status}`); |
|
|
} |
|
|
const data = await resp.json(); |
|
|
|
|
|
|
|
|
renderMeta(data.meta); |
|
|
metaBox.classList.remove("hidden"); |
|
|
|
|
|
|
|
|
renderPie(data.aggregate.percent); |
|
|
renderLine(data.aggregate.rolling); |
|
|
|
|
|
|
|
|
renderTable(data.rows); |
|
|
|
|
|
} catch (err) { |
|
|
console.error(err); |
|
|
alert("Failed: " + err.message); |
|
|
} finally { |
|
|
statusBox.classList.add("hidden"); |
|
|
} |
|
|
}); |
|
|
|