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