Spaces:
Sleeping
Sleeping
| <html lang="id"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>SentiScope โ Hasil Analisis</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link href="https://fonts.googleapis.com/css2?family=Cabinet+Grotesk:wght@400;500;700;800&family=JetBrains+Mono:wght@300;400;500&display=swap" rel="stylesheet"> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.0/chart.umd.min.js"></script> | |
| <style> | |
| *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } | |
| :root { | |
| --bg: #080a0f; | |
| --s1: #0e1117; | |
| --s2: #141820; | |
| --s3: #1a2030; | |
| --b1: rgba(255,255,255,0.06); | |
| --b2: rgba(255,255,255,0.10); | |
| --text: #e8eaf0; | |
| --muted: #4a5568; | |
| --m2: #8892a4; | |
| --accent: #4f9cf9; | |
| --a2: #7b61ff; | |
| --pos: #22c55e; | |
| --neg: #ef4444; | |
| --neu: #94a3b8; | |
| --gold: #f59e0b; | |
| --display:'Cabinet Grotesk', sans-serif; | |
| --mono: 'JetBrains Mono', monospace; | |
| } | |
| html { scroll-behavior: smooth; } | |
| body { font-family: var(--display); background: var(--bg); color: var(--text); min-height: 100vh; } | |
| body::before { | |
| content:''; position:fixed; inset:0; | |
| background-image: linear-gradient(rgba(79,156,249,0.02) 1px, transparent 1px), linear-gradient(90deg, rgba(79,156,249,0.02) 1px, transparent 1px); | |
| background-size: 64px 64px; pointer-events:none; z-index:0; | |
| } | |
| /* โโ TOPBAR โโ */ | |
| .topbar { | |
| position: sticky; top: 0; z-index: 100; | |
| display: grid; grid-template-columns: auto 1fr auto; | |
| align-items: center; gap: 1.5rem; | |
| padding: 0.9rem 2rem; | |
| background: rgba(8,10,15,0.88); backdrop-filter: blur(20px); | |
| border-bottom: 1px solid var(--b1); | |
| } | |
| .back { | |
| display: flex; align-items: center; gap: 0.4rem; | |
| font-family: var(--mono); font-size: 0.62rem; letter-spacing: 0.1em; text-transform: uppercase; | |
| color: var(--m2); text-decoration: none; transition: color .2s; | |
| } | |
| .back:hover { color: var(--text); } | |
| .kw-display { font-size: 0.9rem; font-weight: 700; letter-spacing: -0.01em; } | |
| .kw-display span { color: var(--accent); } | |
| .topbar-right { display: flex; align-items: center; gap: 1rem; } | |
| .tb-stat { display: flex; flex-direction: column; align-items: flex-end; gap: 0.1rem; } | |
| .tb-stat-l { font-family: var(--mono); font-size: 0.5rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted); } | |
| .tb-stat-v { font-family: var(--mono); font-size: 0.8rem; font-weight: 500; color: var(--text); } | |
| .btn-dl { | |
| font-family: var(--mono); font-size: 0.62rem; letter-spacing: 0.08em; text-transform: uppercase; | |
| background: var(--s2); color: var(--m2); border: 1px solid var(--b2); | |
| padding: 0.5rem 1rem; border-radius: 6px; text-decoration: none; | |
| transition: border-color .2s, color .2s; | |
| } | |
| .btn-dl:hover { border-color: var(--accent); color: var(--accent); } | |
| /* โโ METRIC CARDS โโ */ | |
| .metrics { | |
| display: grid; grid-template-columns: repeat(5, 1fr); | |
| gap: 1px; background: var(--b1); | |
| border-bottom: 1px solid var(--b1); | |
| position: relative; z-index: 1; | |
| } | |
| .metric { | |
| background: var(--s1); padding: 1.5rem 2rem; | |
| display: flex; flex-direction: column; gap: 0.5rem; | |
| transition: background .2s; | |
| opacity: 0; transform: translateY(12px); | |
| animation: fu 0.45s both; | |
| } | |
| .metric:hover { background: var(--s2); } | |
| .metric:nth-child(1){animation-delay:.05s} | |
| .metric:nth-child(2){animation-delay:.1s} | |
| .metric:nth-child(3){animation-delay:.15s} | |
| .metric:nth-child(4){animation-delay:.2s} | |
| .metric:nth-child(5){animation-delay:.25s} | |
| .metric-l { font-family: var(--mono); font-size: 0.52rem; letter-spacing: 0.16em; text-transform: uppercase; color: var(--muted); } | |
| .metric-n { font-size: 2.8rem; font-weight: 800; letter-spacing: -0.04em; line-height: 1; } | |
| .metric-n.c-pos { color: var(--pos); } | |
| .metric-n.c-neg { color: var(--neg); } | |
| .metric-n.c-neu { color: var(--neu); } | |
| .metric-n.c-up { color: var(--pos); } | |
| .metric-n.c-dn { color: var(--neg); } | |
| .metric-n.c-base{ color: var(--text); } | |
| .metric-sub { font-family: var(--mono); font-size: 0.58rem; color: var(--muted); } | |
| /* โโ DISTRIBUTION BAR โโ */ | |
| .dist-bar { | |
| position: relative; z-index: 1; | |
| display: flex; align-items: center; gap: 1.5rem; | |
| padding: 1rem 2rem; border-bottom: 1px solid var(--b1); | |
| background: var(--s1); | |
| } | |
| .dist-l { font-family: var(--mono); font-size: 0.55rem; letter-spacing: 0.15em; text-transform: uppercase; color: var(--muted); white-space: nowrap; } | |
| .dist-track { flex: 1; height: 5px; background: var(--s2); border-radius: 3px; display: flex; overflow: hidden; gap: 2px; } | |
| .ds { height: 100%; transition: width 1.2s cubic-bezier(.25,.46,.45,.94); border-radius: 2px; } | |
| .ds-p { background: var(--pos); } | |
| .ds-n { background: var(--neg); } | |
| .ds-u { background: var(--neu); } | |
| .dist-info { font-family: var(--mono); font-size: 0.58rem; color: var(--m2); white-space: nowrap; } | |
| /* โโ GRID LAYOUT โโ */ | |
| .main-grid { | |
| position: relative; z-index: 1; | |
| display: grid; grid-template-columns: 1fr 1fr; | |
| gap: 1px; background: var(--b1); | |
| border-bottom: 1px solid var(--b1); | |
| } | |
| .three-col { | |
| display: grid; grid-template-columns: 1fr 1fr 1fr; | |
| gap: 1px; background: var(--b1); | |
| border-bottom: 1px solid var(--b1); | |
| } | |
| .full-row { | |
| position: relative; z-index: 1; | |
| border-bottom: 1px solid var(--b1); | |
| } | |
| /* โโ PANEL โโ */ | |
| .panel { background: var(--s1); padding: 1.75rem 2rem; } | |
| .panel:hover { background: var(--s1); } | |
| .panel-tag { | |
| font-family: var(--mono); font-size: 0.5rem; letter-spacing: 0.18em; text-transform: uppercase; | |
| color: var(--muted); margin-bottom: 1.25rem; | |
| display: flex; align-items: center; gap: 0.8rem; | |
| } | |
| .panel-tag::after { content:''; flex:1; height:1px; background: var(--b2); } | |
| .panel-title { font-size: 1.1rem; font-weight: 700; letter-spacing: -0.02em; margin-bottom: 1.25rem; } | |
| /* โโ CHART โโ */ | |
| .chart-wrap { position:relative; height:240px; } | |
| /* โโ TABLE โโ */ | |
| .tbl-wrap { overflow-x:auto; max-height: 320px; overflow-y:auto; } | |
| .tbl-wrap::-webkit-scrollbar { width: 3px; height: 3px; } | |
| .tbl-wrap::-webkit-scrollbar-track { background: transparent; } | |
| .tbl-wrap::-webkit-scrollbar-thumb { background: var(--b2); border-radius: 2px; } | |
| table { width:100%; border-collapse:collapse; font-family: var(--mono); font-size:0.68rem; } | |
| th { | |
| text-align:left; font-size:0.5rem; letter-spacing:.15em; text-transform:uppercase; | |
| color: var(--muted); padding: 0.5rem 0.75rem; border-bottom:1px solid var(--b2); | |
| position: sticky; top: 0; background: var(--s1); | |
| } | |
| td { padding: 0.65rem 0.75rem; border-bottom: 1px solid rgba(255,255,255,0.03); vertical-align:top; line-height:1.45; } | |
| tr:last-child td { border-bottom:none; } | |
| tr:hover td { background: var(--s2); } | |
| .tag { | |
| display:inline-block; font-size:0.5rem; letter-spacing:.08em; text-transform:uppercase; | |
| padding: 0.15rem 0.4rem; border-radius: 4px; border: 1px solid; white-space:nowrap; | |
| } | |
| .tag-p { color: var(--pos); border-color: rgba(34,197,94,0.3); background: rgba(34,197,94,0.08); } | |
| .tag-n { color: var(--neg); border-color: rgba(239,68,68,0.3); background: rgba(239,68,68,0.08); } | |
| .tag-u { color: var(--neu); border-color: rgba(148,163,184,0.3); background: rgba(148,163,184,0.08); } | |
| .src-badge { | |
| font-size: 0.48rem; letter-spacing: .06em; text-transform:uppercase; | |
| padding: 0.12rem 0.35rem; border-radius: 3px; border: 1px solid var(--b2); color: var(--muted); | |
| } | |
| /* โโ WORD CLOUD โโ */ | |
| .word-cloud { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: baseline; } | |
| .w-chip { font-family: var(--mono); color: var(--m2); cursor:default; transition: color .2s; line-height: 1.2; } | |
| .w-chip:hover { color: var(--accent); } | |
| /* โโ TOPIC CARDS โโ */ | |
| .topic-list { display: flex; flex-direction: column; gap: 0.75rem; } | |
| .topic-card { background: var(--s2); border: 1px solid var(--b2); border-radius: 8px; padding: 0.9rem 1rem; } | |
| .topic-card-h { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.6rem; } | |
| .topic-num { font-family: var(--mono); font-size: 0.5rem; letter-spacing: .15em; text-transform: uppercase; color: var(--muted); } | |
| .topic-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); opacity: 0.6; } | |
| .topic-words { display: flex; flex-wrap: wrap; gap: 0.3rem; } | |
| .tw { font-family: var(--mono); font-size: 0.62rem; background: var(--s3); border: 1px solid var(--b2); padding: 0.18rem 0.45rem; border-radius: 4px; color: var(--m2); } | |
| /* โโ CLUSTERS โโ */ | |
| .cluster-list { display: flex; flex-direction: column; gap: 0.75rem; } | |
| .cl-item { border-left: 2px solid var(--b2); padding-left: 0.9rem; } | |
| .cl-head { font-family: var(--mono); font-size: 0.52rem; letter-spacing: .12em; text-transform: uppercase; color: var(--muted); margin-bottom: 0.35rem; } | |
| .cl-text { font-family: var(--mono); font-size: 0.65rem; color: var(--m2); line-height: 1.5; } | |
| /* โโ HOAX LIST โโ */ | |
| .hoax-list { display: flex; flex-direction: column; gap: 0.5rem; } | |
| .hoax-item { | |
| display: flex; align-items: flex-start; gap: 0.7rem; | |
| background: var(--s2); border: 1px solid var(--b1); border-radius: 6px; padding: 0.65rem 0.75rem; | |
| transition: border-color .2s; | |
| } | |
| .hoax-item:hover { border-color: var(--b2); } | |
| .h-badge { | |
| font-family: var(--mono); font-size: 0.48rem; letter-spacing: .08em; text-transform: uppercase; | |
| padding: 0.15rem 0.4rem; border-radius: 3px; white-space: nowrap; flex-shrink: 0; margin-top: 0.1rem; | |
| } | |
| .hb-hoax { background: rgba(239,68,68,0.15); color: var(--neg); border: 1px solid rgba(239,68,68,0.25); } | |
| .hb-norm { background: var(--s3); color: var(--muted); border: 1px solid var(--b2); } | |
| .h-text { font-family: var(--mono); font-size: 0.65rem; color: var(--m2); line-height: 1.5; } | |
| /* โโ BOT LIST โโ */ | |
| .bot-list { display: flex; flex-direction: column; gap: 0.5rem; } | |
| .bot-item { display: flex; align-items: center; gap: 0.75rem; padding: 0.5rem 0; border-bottom: 1px solid var(--b1); } | |
| .bot-item:last-child { border-bottom: none; } | |
| .bot-id { font-family: var(--mono); font-size: 0.62rem; color: var(--m2); min-width: 3.5rem; } | |
| .bot-bar { flex: 1; height: 3px; background: var(--s2); border-radius: 2px; overflow: hidden; } | |
| .bot-fill { height: 100%; background: linear-gradient(90deg, var(--accent), var(--a2)); border-radius: 2px; transition: width 1s ease; } | |
| .bot-score { font-family: var(--mono); font-size: 0.6rem; color: var(--muted); min-width: 2.5rem; text-align: right; } | |
| /* โโ INSIGHT โโ */ | |
| .insight-block { | |
| position: relative; z-index: 1; | |
| background: linear-gradient(135deg, var(--s2) 0%, var(--s3) 100%); | |
| border-bottom: 1px solid var(--b1); | |
| padding: 2.5rem 2rem; | |
| display: grid; grid-template-columns: 1fr auto; gap: 2rem; align-items: center; | |
| overflow: hidden; | |
| } | |
| .insight-block::before { | |
| content:''; position:absolute; top:0;left:0;right:0;height:1px; | |
| background: linear-gradient(90deg, transparent, rgba(79,156,249,0.4), transparent); | |
| } | |
| .insight-text { font-size: 1.2rem; font-weight: 700; letter-spacing: -0.02em; line-height: 1.5; } | |
| .insight-text em { font-style: normal; color: var(--accent); } | |
| .insight-meta { font-family: var(--mono); font-size: 0.58rem; color: var(--muted); line-height: 1.7; white-space: nowrap; text-align: right; } | |
| /* โโ IMAGES โโ */ | |
| .img-grid { | |
| position: relative; z-index: 1; | |
| display: grid; grid-template-columns: 1fr 1fr; | |
| gap: 1px; background: var(--b1); | |
| border-bottom: 1px solid var(--b1); | |
| } | |
| .img-panel { background: var(--s1); padding: 1.75rem 2rem; } | |
| .img-panel img { width:100%; height:auto; display:block; border: 1px solid var(--b2); border-radius: 6px; } | |
| .img-ph { | |
| width:100%; height:160px; background: var(--s2); border: 1px dashed var(--b2); border-radius: 6px; | |
| display:flex; align-items:center; justify-content:center; | |
| font-family: var(--mono); font-size:0.62rem; color: var(--muted); | |
| } | |
| /* โโ FOOTER โโ */ | |
| footer { | |
| position: relative; z-index: 1; | |
| padding: 1.5rem 2rem; | |
| display: flex; align-items: center; justify-content: space-between; | |
| border-top: 1px solid var(--b1); | |
| } | |
| .ft-kw { font-family: var(--mono); font-size: 0.6rem; color: var(--muted); } | |
| .ft-kw strong { font-family: var(--display); font-size: 0.8rem; font-weight: 700; color: var(--accent); } | |
| .ft-r { display: flex; gap: 1rem; } | |
| .ft-r a { | |
| font-family: var(--mono); font-size: 0.6rem; letter-spacing: .08em; text-transform: uppercase; | |
| color: var(--muted); text-decoration: none; transition: color .2s; | |
| } | |
| .ft-r a:hover { color: var(--text); } | |
| /* โโ EMPTY โโ */ | |
| .empty { font-family: var(--mono); font-size: 0.65rem; color: var(--muted); font-style: italic; } | |
| /* โโ ANIM โโ */ | |
| @keyframes fu { from{opacity:0;transform:translateY(14px)} to{opacity:1;transform:translateY(0)} } | |
| /* โโ RESPONSIVE โโ */ | |
| @media(max-width:960px){ | |
| .metrics { grid-template-columns: repeat(3,1fr); } | |
| .main-grid { grid-template-columns: 1fr; } | |
| .three-col { grid-template-columns: 1fr; } | |
| .img-grid { grid-template-columns: 1fr; } | |
| .insight-block { grid-template-columns: 1fr; } | |
| .insight-meta { text-align: left; white-space: normal; } | |
| .topbar { padding: 0.8rem 1.2rem; } | |
| .panel { padding: 1.5rem; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- TOPBAR --> | |
| <header class="topbar"> | |
| <a href="/" class="back">โ Kembali</a> | |
| <div class="kw-display">Analisis: <span id="h-kw">โ</span></div> | |
| <div class="topbar-right"> | |
| <div class="tb-stat"> | |
| <span class="tb-stat-l">Total</span> | |
| <span class="tb-stat-v" id="h-total">โ</span> | |
| </div> | |
| <div class="tb-stat"> | |
| <span class="tb-stat-l">Sumber</span> | |
| <span class="tb-stat-v" id="h-src">โ</span> | |
| </div> | |
| <a href="/download" class="btn-dl">โ CSV</a> | |
| </div> | |
| </header> | |
| <!-- METRICS --> | |
| <section class="metrics"> | |
| <div class="metric"> | |
| <span class="metric-l">Total Komentar</span> | |
| <div class="metric-n c-base" id="m-total">โ</div> | |
| <span class="metric-sub">data diproses</span> | |
| </div> | |
| <div class="metric"> | |
| <span class="metric-l">Positif</span> | |
| <div class="metric-n c-pos" id="m-pos">โ</div> | |
| <span class="metric-sub" id="m-pos-p">0%</span> | |
| </div> | |
| <div class="metric"> | |
| <span class="metric-l">Negatif</span> | |
| <div class="metric-n c-neg" id="m-neg">โ</div> | |
| <span class="metric-sub" id="m-neg-p">0%</span> | |
| </div> | |
| <div class="metric"> | |
| <span class="metric-l">Netral</span> | |
| <div class="metric-n c-neu" id="m-neu">โ</div> | |
| <span class="metric-sub" id="m-neu-p">0%</span> | |
| </div> | |
| <div class="metric"> | |
| <span class="metric-l">Prediksi Tren</span> | |
| <div class="metric-n" id="m-trend" style="font-size:1.4rem;line-height:1.6">โ</div> | |
| <span class="metric-sub">regresi linier</span> | |
| </div> | |
| </section> | |
| <!-- DISTRIBUTION --> | |
| <div class="dist-bar"> | |
| <span class="dist-l">Distribusi Sentimen</span> | |
| <div class="dist-track"> | |
| <div class="ds ds-p" id="ds-p" style="width:0%"></div> | |
| <div class="ds ds-n" id="ds-n" style="width:0%"></div> | |
| <div class="ds ds-u" id="ds-u" style="width:0%"></div> | |
| </div> | |
| <span class="dist-info" id="dist-info">โ</span> | |
| </div> | |
| <!-- CHART + WORDS --> | |
| <div class="main-grid"> | |
| <div class="panel"> | |
| <div class="panel-tag">01 โ Distribusi Sentimen</div> | |
| <div class="chart-wrap"><canvas id="c-pie"></canvas></div> | |
| </div> | |
| <div class="panel"> | |
| <div class="panel-tag">02 โ Kata Dominan</div> | |
| <div class="word-cloud" id="words"><span class="empty">Memuatโฆ</span></div> | |
| </div> | |
| </div> | |
| <!-- TABLE + TOPICS --> | |
| <div class="main-grid"> | |
| <div class="panel"> | |
| <div class="panel-tag">03 โ Data Komentar</div> | |
| <div class="tbl-wrap"> | |
| <table> | |
| <thead><tr><th>#</th><th>Sumber</th><th>Komentar</th><th>Sentimen</th><th>Conf.</th></tr></thead> | |
| <tbody id="tbl"></tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <div class="panel"> | |
| <div class="panel-tag">04 โ Topic Modeling (LDA)</div> | |
| <div class="topic-list" id="topics"><span class="empty">Memuatโฆ</span></div> | |
| </div> | |
| </div> | |
| <!-- TIMELINE --> | |
| <div class="full-row"> | |
| <div class="panel"> | |
| <div class="panel-tag">05 โ Timeline Sentimen</div> | |
| <div class="chart-wrap" style="height:200px"><canvas id="c-line"></canvas></div> | |
| </div> | |
| </div> | |
| <!-- CLUSTER + HOAX + BOT --> | |
| <div class="three-col"> | |
| <div class="panel"> | |
| <div class="panel-tag">06 โ Kluster Opini</div> | |
| <div class="cluster-list" id="clusters"><span class="empty">Memuatโฆ</span></div> | |
| </div> | |
| <div class="panel"> | |
| <div class="panel-tag">07 โ Deteksi Konten Hoaks</div> | |
| <div class="hoax-list" id="hoax"><span class="empty">Memuatโฆ</span></div> | |
| </div> | |
| <div class="panel"> | |
| <div class="panel-tag">08 โ Bot Network Score</div> | |
| <div class="bot-list" id="bots"><span class="empty">Memuatโฆ</span></div> | |
| </div> | |
| </div> | |
| <!-- IMAGES --> | |
| <div class="img-grid"> | |
| <div class="img-panel"> | |
| <div class="panel-tag">09 โ Word Cloud</div> | |
| <div id="wc-box"><div class="img-ph">Wordcloud diproses server</div></div> | |
| </div> | |
| <div class="img-panel"> | |
| <div class="panel-tag">10 โ Heatmap Sumber ร Sentimen</div> | |
| <div id="hm-box"><div class="img-ph">Heatmap diproses server</div></div> | |
| </div> | |
| </div> | |
| <!-- CROSS-PLATFORM ANALYSIS --> | |
| <div class="full-row" id="cross-section" style="display:none"> | |
| <div class="panel" style="border-bottom:1px solid var(--b1)"> | |
| <div class="panel-tag">11 โ Analisis Komparatif Lintas Platform</div> | |
| <div style="display:grid;grid-template-columns:1fr 1fr;gap:1px;background:var(--b1);border-radius:8px;overflow:hidden;margin-bottom:1.25rem"> | |
| <div style="background:var(--s2);padding:1rem 1.25rem"> | |
| <div style="font-family:var(--mono);font-size:0.5rem;letter-spacing:.15em;text-transform:uppercase;color:var(--muted);margin-bottom:.75rem">Platform Insights</div> | |
| <div id="cross-insights" class="empty">Memuatโฆ</div> | |
| </div> | |
| <div style="background:var(--s2);padding:1rem 1.25rem"> | |
| <div style="font-family:var(--mono);font-size:0.5rem;letter-spacing:.15em;text-transform:uppercase;color:var(--muted);margin-bottom:.75rem">Pairwise Comparison</div> | |
| <div id="cross-pairwise" class="empty">Memuatโฆ</div> | |
| </div> | |
| </div> | |
| <div id="cross-img-box"><div class="img-ph">Chart komparatif diproses server</div></div> | |
| </div> | |
| </div> | |
| <!-- CONFIDENCE STATS --> | |
| <div class="full-row"> | |
| <div class="panel" style="border-bottom:1px solid var(--b1)"> | |
| <div class="panel-tag">12 โ Statistik Confidence Model</div> | |
| <div style="display:grid;grid-template-columns:repeat(4,1fr);gap:1px;background:var(--b1);border-radius:8px;overflow:hidden"> | |
| <div id="cs-pos" style="background:var(--s2);padding:1rem 1.25rem"></div> | |
| <div id="cs-neg" style="background:var(--s2);padding:1rem 1.25rem"></div> | |
| <div id="cs-neu" style="background:var(--s2);padding:1rem 1.25rem"></div> | |
| <div id="cs-unc" style="background:var(--s2);padding:1rem 1.25rem"></div> | |
| </div> | |
| <div style="display:flex;gap:1.5rem;margin-top:1rem;flex-wrap:wrap" id="conf-buckets"></div> | |
| </div> | |
| </div> | |
| <!-- INSIGHT --> | |
| <div class="insight-block"> | |
| <p class="insight-text" id="insight-text">Mengolah data analisisโฆ</p> | |
| <p class="insight-meta" id="insight-meta"></p> | |
| </div> | |
| <!-- ABSA + NER --> | |
| <div class="main-grid" style="border-bottom:1px solid var(--b1);position:relative;z-index:1"> | |
| <div class="panel"> | |
| <div class="panel-tag">13 โ Aspect-Based Sentiment Analysis (ABSA)</div> | |
| <div id="absa-grid" class="empty">Memuatโฆ</div> | |
| </div> | |
| <div class="panel"> | |
| <div class="panel-tag">14 โ Named Entity Recognition (NER)</div> | |
| <div id="ner-grid" class="empty">Memuatโฆ</div> | |
| </div> | |
| </div> | |
| <!-- STANCE + EMOTION --> | |
| <div class="main-grid" style="border-bottom:1px solid var(--b1);position:relative;z-index:1"> | |
| <div class="panel"> | |
| <div class="panel-tag">15 โ Stance Detection</div> | |
| <div id="stance-grid" class="empty">Memuatโฆ</div> | |
| </div> | |
| <div class="panel"> | |
| <div class="panel-tag">16 โ Emotion Detection</div> | |
| <div id="emotion-grid" class="empty">Memuatโฆ</div> | |
| </div> | |
| </div> | |
| <!-- KEYWORDS + SUMMARY --> | |
| <div class="main-grid" style="border-bottom:1px solid var(--b1);position:relative;z-index:1"> | |
| <div class="panel"> | |
| <div class="panel-tag">17 โ Keyword & Phrase Extraction</div> | |
| <div id="kw-grid" class="empty">Memuatโฆ</div> | |
| </div> | |
| <div class="panel"> | |
| <div class="panel-tag">18 โ Ringkasan Otomatis per Platform</div> | |
| <div id="sum-grid" class="empty">Memuatโฆ</div> | |
| </div> | |
| </div> | |
| <!-- FOOTER --> | |
| <footer> | |
| <div class="ft-kw">Kata kunci: <strong id="ft-kw">โ</strong></div> | |
| <div class="ft-r"> | |
| <a href="/">โ Analisis Baru</a> | |
| <a href="/download">โ Unduh CSV</a> | |
| <a href="/download/summary" class="ft-r" style="text-decoration:none;font-family:var(--mono);font-size:0.6rem;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)">โ Summary CSV</a> | |
| </div> | |
| </footer> | |
| <script> | |
| // โโ CHART.JS DEFAULTS โโ | |
| Chart.defaults.color = '#4a5568'; | |
| Chart.defaults.borderColor = 'rgba(255,255,255,0.05)'; | |
| Chart.defaults.font.family = "'JetBrains Mono', monospace"; | |
| // โโ HELPERS โโ | |
| const pct = (n, t) => Math.round((n / (t || 1)) * 100); | |
| const esc = s => String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); | |
| const sc = s => { if(!s)return'u'; const l=s.toLowerCase(); return l.includes('pos')?'p':l.includes('neg')?'n':'u'; }; | |
| const $ = id => document.getElementById(id); | |
| // โโ LOAD DATA โโ | |
| let D = null; | |
| try { const r = sessionStorage.getItem('analysisResult'); if(r) D = JSON.parse(r); } catch(e){} | |
| if(!D && typeof window.__DATA__!=='undefined') D = window.__DATA__; | |
| if(D) render(D); | |
| else $('insight-text').textContent = 'Tidak ada data. Kembali ke halaman utama untuk memulai analisis baru.'; | |
| function render(d) { | |
| const data = d.data || []; | |
| const crossData = d.cross_platform || {}; | |
| const confStats = d.conf_stats || {}; | |
| const expSum = d.export_summary || {}; | |
| const words = d.top_words || []; | |
| const topics= d.topics || []; | |
| const clust = d.clusters || []; | |
| const hoax = d.hoax || []; | |
| const botN = d.bot_network || { nodes:[], edges:[], bots:[] }; | |
| const trend = d.trend || 'โ'; | |
| const kw = d.keyword || 'โ'; | |
| const src = d.source || 'all'; | |
| const pos = data.filter(x=>x.sentiment==='Positive').length; | |
| const neg = data.filter(x=>x.sentiment==='Negative').length; | |
| const neu = data.filter(x=>x.sentiment==='Neutral').length; | |
| const tot = data.length || 1; | |
| // HEADER | |
| $('h-kw').textContent = kw; | |
| $('h-total').textContent = data.length; | |
| const srcLabel = { | |
| 'all': 'All (5 Sources)', 'youtube': 'YouTube', | |
| 'reddit': 'Reddit', 'instagram': 'Instagram', | |
| 'tiktok': 'TikTok', 'news': 'Google News', | |
| 'youtube,tiktok': 'YT+TT', 'instagram,tiktok': 'IG+TT', | |
| 'tiktok,news': 'TT+News', 'youtube,news': 'YT+News', | |
| 'youtube,instagram': 'YT+IG', 'reddit,news': 'RD+News' | |
| }; | |
| $('h-src').textContent = srcLabel[src] || src; | |
| $('ft-kw').textContent = kw; | |
| // METRICS | |
| $('m-total').textContent = data.length; | |
| $('m-pos').textContent = pos; | |
| $('m-neg').textContent = neg; | |
| $('m-neu').textContent = neu; | |
| $('m-pos-p').textContent = pct(pos,tot)+'%'; | |
| $('m-neg-p').textContent = pct(neg,tot)+'%'; | |
| $('m-neu-p').textContent = pct(neu,tot)+'%'; | |
| // TREND โ sekarang object, bukan string | |
| const tEl = $('m-trend'); | |
| const tObj = (typeof trend === 'object' && trend !== null) ? trend : { label: trend, dominant: '', confidence: 0 }; | |
| tEl.textContent = tObj.label || trend; | |
| const isUp = (tObj.label || '').toLowerCase().includes('positif'); | |
| tEl.className = 'metric-n ' + (isUp ? 'c-up' : 'c-dn'); | |
| // DIST BAR | |
| setTimeout(() => { | |
| $('ds-p').style.width = pct(pos,tot)+'%'; | |
| $('ds-n').style.width = pct(neg,tot)+'%'; | |
| $('ds-u').style.width = pct(neu,tot)+'%'; | |
| }, 100); | |
| $('dist-info').textContent = `${pct(pos,tot)}% Pos ยท ${pct(neg,tot)}% Neg ยท ${pct(neu,tot)}% Neu`; | |
| // PIE | |
| new Chart($('c-pie').getContext('2d'), { | |
| type: 'doughnut', | |
| data: { | |
| labels: ['Positif','Negatif','Netral'], | |
| datasets: [{ | |
| data: [pos, neg, neu], | |
| backgroundColor: ['rgba(34,197,94,0.85)','rgba(239,68,68,0.85)','rgba(148,163,184,0.85)'], | |
| borderColor: ['rgba(34,197,94,0.2)','rgba(239,68,68,0.2)','rgba(148,163,184,0.2)'], | |
| borderWidth: 1, hoverOffset: 6 | |
| }] | |
| }, | |
| options: { | |
| cutout: '68%', responsive: true, maintainAspectRatio: false, | |
| plugins: { | |
| legend: { position:'right', labels:{ font:{size:11}, color:'#8892a4', padding:16, usePointStyle:true, pointStyleWidth:8 } }, | |
| tooltip: { callbacks: { label: ctx => ` ${ctx.label}: ${ctx.parsed} (${pct(ctx.parsed,tot)}%)` } } | |
| } | |
| } | |
| }); | |
| // LINE | |
| const roll = (arr, n=5) => arr.map((_,i)=>{ const sl=arr.slice(Math.max(0,i-n),i+1); return sl.reduce((a,b)=>a+b,0)/sl.length; }); | |
| const pl = data.map(x=>x.sentiment==='Positive'?1:0); | |
| const nl = data.map(x=>x.sentiment==='Negative'?1:0); | |
| const ul = data.map(x=>x.sentiment==='Neutral'?1:0); | |
| new Chart($('c-line').getContext('2d'), { | |
| type: 'line', | |
| data: { | |
| labels: data.map((_,i)=>i+1), | |
| datasets: [ | |
| { label:'Positif', data:roll(pl), borderColor:'rgba(34,197,94,0.8)', backgroundColor:'rgba(34,197,94,0.06)', borderWidth:1.5, tension:0.4, pointRadius:0, fill:true }, | |
| { label:'Negatif', data:roll(nl), borderColor:'rgba(239,68,68,0.8)', backgroundColor:'rgba(239,68,68,0.06)', borderWidth:1.5, tension:0.4, pointRadius:0, fill:true }, | |
| { label:'Netral', data:roll(ul), borderColor:'rgba(148,163,184,0.5)',backgroundColor:'rgba(148,163,184,0.03)',borderWidth:1, tension:0.4, pointRadius:0, fill:true }, | |
| ] | |
| }, | |
| options: { | |
| responsive: true, maintainAspectRatio: false, | |
| interaction: { mode:'index', intersect:false }, | |
| scales: { | |
| x: { ticks:{ maxTicksLimit:12, font:{size:9} }, grid:{ color:'rgba(255,255,255,0.04)' } }, | |
| y: { ticks:{ font:{size:9} }, grid:{ color:'rgba(255,255,255,0.04)' } } | |
| }, | |
| plugins: { legend:{ labels:{ font:{size:10}, color:'#8892a4', usePointStyle:true } } } | |
| } | |
| }); | |
| // WORDS | |
| const wg = $('words'); | |
| if(words.length) { | |
| const mx = words[0].count || 1; | |
| wg.innerHTML = words.map(w=>{ | |
| const sz = 0.65 + (w.count/mx)*0.95; | |
| const op = 0.45 + (w.count/mx)*0.55; | |
| return `<span class="w-chip" style="font-size:${sz}rem;opacity:${op}" title="${w.count}x">${w.word}</span>`; | |
| }).join(''); | |
| } else wg.innerHTML = '<span class="empty">Tidak ada data kata.</span>'; | |
| // TABLE โ dengan confidence score | |
| const tb = $('tbl'); | |
| if(data.length) { | |
| tb.innerHTML = data.slice(0,50).map((r,i)=>{ | |
| const conf = r.confidence ? Math.round(r.confidence*100)+'%' : 'โ'; | |
| const confColor = r.confidence >= 0.8 ? 'var(--pos)' : r.confidence >= 0.6 ? 'var(--gold)' : 'var(--neg)'; | |
| return `<tr> | |
| <td style="color:var(--muted)">${i+1}</td> | |
| <td><span class="src-badge">${esc(r.source||'โ')}</span></td> | |
| <td style="max-width:240px;color:var(--m2)">${esc((r.text||'').substring(0,90))}</td> | |
| <td><span class="tag tag-${sc(r.sentiment)}">${r.sentiment||'โ'}</span></td> | |
| <td style="font-family:var(--mono);font-size:0.6rem;color:${confColor}">${conf}</td> | |
| </tr>`; | |
| }).join(''); | |
| } else tb.innerHTML = '<tr><td colspan="5" class="empty" style="padding:.75rem">Tidak ada data.</td></tr>'; | |
| // TOPICS | |
| const tg = $('topics'); | |
| if(topics.length) { | |
| tg.innerHTML = topics.map((t,i)=>` | |
| <div class="topic-card"> | |
| <div class="topic-card-h"><span class="topic-num">Topik ${i+1}</span><span class="topic-dot"></span></div> | |
| <div class="topic-words">${(Array.isArray(t)?t:[]).map(w=>`<span class="tw">${w}</span>`).join('')}</div> | |
| </div>`).join(''); | |
| } else tg.innerHTML = '<span class="empty">Data tidak cukup untuk topic modeling.</span>'; | |
| // CLUSTERS | |
| const cg = $('clusters'); | |
| if(clust.length) { | |
| cg.innerHTML = clust.map(c=>` | |
| <div class="cl-item"> | |
| <div class="cl-head">Kluster ${c.cluster}</div> | |
| ${(c.samples||[]).slice(0,2).map(s=>`<div class="cl-text">"${esc(s.substring(0,85))}โฆ"</div>`).join('')} | |
| </div>`).join(''); | |
| } else cg.innerHTML = '<span class="empty">Data tidak cukup untuk clustering.</span>'; | |
| // HOAX โ dengan confidence score dan method | |
| const hg = $('hoax'); | |
| if(hoax.length) { | |
| hg.innerHTML = hoax.map(h=>{ | |
| const conf = h.confidence ? Math.round(h.confidence*100)+'%' : ''; | |
| const method = h.method === 'ml' ? 'ยทML' : 'ยทKW'; | |
| return `<div class="hoax-item"> | |
| <span class="h-badge ${h.label==='Hoax'?'hb-hoax':'hb-norm'}">${h.label}</span> | |
| <span class="h-text">${esc((h.text||'').substring(0,80))}</span> | |
| ${conf ? `<span style="font-family:var(--mono);font-size:0.5rem;color:var(--muted);white-space:nowrap;margin-left:auto">${conf}${method}</span>` : ''} | |
| </div>`; | |
| }).join(''); | |
| } else hg.innerHTML = '<span class="empty">Tidak ada konten terdeteksi.</span>'; | |
| // BOT | |
| const bg = $('bots'); | |
| const bots = (botN.bots||[]); | |
| if(bots.length) { | |
| bg.innerHTML = bots.slice(0,8).map(b=>` | |
| <div class="bot-item"> | |
| <span class="bot-id">Node ${b.node}</span> | |
| <div class="bot-bar"><div class="bot-fill" style="width:${b.score*100}%"></div></div> | |
| <span class="bot-score">${b.score}</span> | |
| </div>`).join(''); | |
| } else bg.innerHTML = '<span class="empty">Tidak ada bot terdeteksi.</span>'; | |
| // INSIGHT โ gunakan trend object | |
| const avgConf = data.length ? (data.reduce((s,r)=>s+(r.confidence||0),0)/data.length*100).toFixed(1) : 0; | |
| const trendLabel = tObj.label || String(trend); | |
| const trendSum = tObj.summary || trendLabel; | |
| const polarity = tObj.polarity !== undefined ? tObj.polarity : 'โ'; | |
| $('insight-text').innerHTML = `Dari <em>${data.length}</em> komentar yang dianalisis, opini publik terhadap "<em>${esc(kw)}</em>" bersifat <em>${trendLabel.toLowerCase()}</em>. ${trendSum}`; | |
| $('insight-meta').innerHTML = `Model: IndoBERT (w11wo/indonesian-roberta-base)<br>Avg. confidence: ${avgConf}% ยท Polarisasi: ${polarity}<br>${new Date().toLocaleDateString('id-ID',{weekday:'long',year:'numeric',month:'long',day:'numeric'})}`; | |
| // IMAGES | |
| loadImg('static/wordcloud.png','wc-box'); | |
| loadImg('static/heatmap.png','hm-box'); | |
| // โโ NEW FEATURES โโ | |
| renderNewFeatures(d); | |
| // โโ CROSS-PLATFORM ANALYSIS โโ | |
| const platforms = crossData.platforms || {}; | |
| if (Object.keys(platforms).length >= 2) { | |
| document.getElementById('cross-section').style.display = ''; | |
| // insights | |
| const ins = crossData.insights || []; | |
| const insEl = document.getElementById('cross-insights'); | |
| insEl.innerHTML = ins.length | |
| ? ins.map(i => `<div style="font-family:var(--mono);font-size:0.65rem;color:var(--m2);line-height:1.6;margin-bottom:.4rem">โข ${esc(i)}</div>`).join('') | |
| : '<span class="empty">Hanya satu platform โ tidak ada perbandingan.</span>'; | |
| // pairwise | |
| const pw = crossData.pairwise || []; | |
| const pwEl = document.getElementById('cross-pairwise'); | |
| pwEl.innerHTML = pw.length | |
| ? pw.map(p => `<div style="font-family:var(--mono);font-size:0.62rem;color:var(--m2);line-height:1.7"> | |
| <span style="color:var(--accent)">${esc(p.platform_a)}</span> vs | |
| <span style="color:var(--accent)">${esc(p.platform_b)}</span>: | |
| selisih positif <strong style="color:var(--text)">${p.pos_diff}%</strong> | |
| </div>`).join('') | |
| : '<span class="empty">โ</span>'; | |
| loadImg('static/comparative.png', 'cross-img-box'); | |
| } | |
| // โโ CONFIDENCE STATS โโ | |
| const byClass = confStats.by_class || {}; | |
| const clsMap = [ | |
| ['Positive','cs-pos','var(--pos)'], | |
| ['Negative','cs-neg','var(--neg)'], | |
| ['Neutral', 'cs-neu','var(--neu)'], | |
| ['Uncertain','cs-unc','var(--gold)'], | |
| ]; | |
| clsMap.forEach(([cls, elId, color]) => { | |
| const s = byClass[cls] || {}; | |
| document.getElementById(elId).innerHTML = ` | |
| <div style="font-family:var(--mono);font-size:0.5rem;letter-spacing:.12em;text-transform:uppercase;color:${color};margin-bottom:.5rem">${cls}</div> | |
| <div style="font-size:1.6rem;font-weight:800;color:${color};letter-spacing:-0.03em">${s.count||0}</div> | |
| <div style="font-family:var(--mono);font-size:0.58rem;color:var(--muted);margin-top:.35rem"> | |
| mean: <b style="color:var(--m2)">${s.mean||0}</b><br> | |
| std: <b style="color:var(--m2)">${s.std||0}</b> | |
| </div>`; | |
| }); | |
| const buckets = confStats.buckets || {}; | |
| const bEl = document.getElementById('conf-buckets'); | |
| bEl.innerHTML = Object.entries(buckets).map(([k,v]) => ` | |
| <div style="font-family:var(--mono);font-size:0.6rem;background:var(--s2);border:1px solid var(--b2);border-radius:6px;padding:.4rem .8rem;color:var(--m2)"> | |
| ${k}: <b style="color:var(--text)">${v}</b> | |
| </div>`).join(''); | |
| } | |
| function loadImg(src, boxId) { | |
| const img = new Image(); | |
| img.onload = () => { | |
| img.style.width='100%'; img.style.borderRadius='6px'; | |
| $(boxId).innerHTML = ''; $(boxId).appendChild(img); | |
| }; | |
| img.src = '/' + src + '?t=' + Date.now(); | |
| } | |
| // โโ ABSA โโ | |
| function renderABSA(absa) { | |
| const el = $('absa-grid'); | |
| const top = absa.top_aspects || []; | |
| if (!top.length) { el.innerHTML = '<span class="empty">Tidak ada aspek terdeteksi.</span>'; return; } | |
| const COLORS = {Positive:'var(--pos)',Negative:'var(--neg)',Neutral:'var(--neu)',Uncertain:'var(--gold)'}; | |
| el.innerHTML = ` | |
| <div style="display:grid;grid-template-columns:1fr 1fr;gap:0.5rem"> | |
| ${top.map(a => ` | |
| <div style="background:var(--s2);border:1px solid var(--b2);border-radius:7px;padding:0.75rem 1rem"> | |
| <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.4rem"> | |
| <span style="font-weight:700;font-size:0.78rem;text-transform:capitalize">${esc(a.aspect)}</span> | |
| <span style="font-family:var(--mono);font-size:0.55rem;padding:0.1rem 0.4rem;border-radius:3px;border:1px solid;color:${COLORS[a.dominant]||'var(--neu)'};border-color:${COLORS[a.dominant]||'var(--neu)'}">${a.dominant}</span> | |
| </div> | |
| <div style="display:flex;gap:3px;height:4px;border-radius:2px;overflow:hidden"> | |
| <div style="width:${a.pos_pct}%;background:var(--pos);border-radius:2px"></div> | |
| <div style="width:${a.neg_pct}%;background:var(--neg);border-radius:2px"></div> | |
| <div style="width:${a.neu_pct}%;background:var(--neu);border-radius:2px"></div> | |
| </div> | |
| <div style="font-family:var(--mono);font-size:0.5rem;color:var(--muted);margin-top:0.3rem">${a.total} sebutan ยท ${a.pos_pct}% pos ยท ${a.neg_pct}% neg</div> | |
| </div>`).join('')} | |
| </div> | |
| <div style="font-family:var(--mono);font-size:0.58rem;color:var(--muted);margin-top:0.75rem"> | |
| ${absa.aspects_found || 0} aspek ditemukan dari ${absa.total_texts_analyzed || 0} komentar | |
| </div>`; | |
| } | |
| // โโ NER โโ | |
| function renderNER(ner) { | |
| const el = $('ner-grid'); | |
| const top = ner.top_entities || []; | |
| if (!top.length) { el.innerHTML = '<span class="empty">Tidak ada entitas terdeteksi.</span>'; return; } | |
| const TYPE_COLOR = {PER:'#4f9cf9',ORG:'#7b61ff',LOC:'#22c55e',PROD:'#f59e0b',EVENT:'#ef4444',MISC:'#94a3b8'}; | |
| const TYPE_LABEL = {PER:'Tokoh',ORG:'Institusi',LOC:'Lokasi',PROD:'Produk',EVENT:'Kejadian',MISC:'Lainnya'}; | |
| el.innerHTML = ` | |
| <div style="display:flex;flex-wrap:wrap;gap:0.4rem;margin-bottom:1rem"> | |
| ${top.map(e => ` | |
| <div style="display:flex;align-items:center;gap:0.4rem;background:var(--s2);border:1px solid var(--b2);border-radius:6px;padding:0.3rem 0.65rem"> | |
| <span style="font-family:var(--mono);font-size:0.48rem;padding:0.1rem 0.35rem;border-radius:3px;background:${TYPE_COLOR[e.type]||'#94a3b8'}22;color:${TYPE_COLOR[e.type]||'#94a3b8'};border:1px solid ${TYPE_COLOR[e.type]||'#94a3b8'}44">${TYPE_LABEL[e.type]||e.type}</span> | |
| <span style="font-size:0.72rem;font-weight:600">${esc(e.entity)}</span> | |
| <span style="font-family:var(--mono);font-size:0.55rem;color:var(--muted)">${e.count}ร</span> | |
| </div>`).join('')} | |
| </div> | |
| <div style="font-family:var(--mono);font-size:0.58rem;color:var(--muted)">${ner.total_entities||0} entitas unik ยท ${ner.total_mentions||0} total sebutan</div>`; | |
| } | |
| // โโ STANCE โโ | |
| function renderStance(stance) { | |
| const el = $('stance-grid'); | |
| const c = stance.counts || {}; | |
| const total = (c.Favor||0) + (c.Against||0) + (c.Neutral||0) || 1; | |
| el.innerHTML = ` | |
| <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:0.5rem;margin-bottom:1rem"> | |
| ${[['Favor','var(--pos)','Mendukung'],['Against','var(--neg)','Menolak'],['Neutral','var(--neu)','Netral']].map(([s,color,label]) => ` | |
| <div style="background:var(--s2);border:1px solid var(--b2);border-radius:7px;padding:0.75rem;text-align:center"> | |
| <div style="font-size:1.6rem;font-weight:800;color:${color};letter-spacing:-0.03em">${c[s]||0}</div> | |
| <div style="font-family:var(--mono);font-size:0.5rem;letter-spacing:.1em;text-transform:uppercase;color:var(--muted);margin-top:0.2rem">${label}</div> | |
| <div style="font-family:var(--mono);font-size:0.6rem;color:${color};margin-top:0.2rem">${Math.round((c[s]||0)/total*100)}%</div> | |
| </div>`).join('')} | |
| </div> | |
| <div style="height:5px;background:var(--s2);border-radius:3px;display:flex;overflow:hidden;gap:2px"> | |
| <div style="width:${stance.favor_pct||0}%;background:var(--pos);border-radius:2px;transition:width 1s ease"></div> | |
| <div style="width:${stance.against_pct||0}%;background:var(--neg);border-radius:2px;transition:width 1s ease"></div> | |
| <div style="width:${stance.neutral_pct||0}%;background:var(--neu);border-radius:2px;transition:width 1s ease"></div> | |
| </div> | |
| <div style="font-family:var(--mono);font-size:0.58rem;color:var(--muted);margin-top:0.6rem"> | |
| Dominan: <b style="color:var(--text)">${stance.dominant||'โ'}</b> ยท Target: <b style="color:var(--accent)">${esc(stance.target||'โ')}</b> | |
| </div>`; | |
| } | |
| // โโ EMOTIONS โโ | |
| function renderEmotions(emotions) { | |
| const el = $('emotion-grid'); | |
| const dist = emotions.distribution || {}; | |
| const EMO_ICON = {joy:'๐',anger:'๐ก',sadness:'๐ข',fear:'๐จ',surprise:'๐ฎ',disgust:'๐คข',neutral:'๐'}; | |
| const EMO_COLOR = {joy:'#22c55e',anger:'#ef4444',sadness:'#4f9cf9',fear:'#f59e0b',surprise:'#7b61ff',disgust:'#94a3b8',neutral:'#5a6070'}; | |
| const entries = Object.entries(dist).sort((a,b)=>(b[1].count||0)-(a[1].count||0)); | |
| el.innerHTML = ` | |
| <div style="display:grid;grid-template-columns:repeat(4,1fr);gap:0.4rem;margin-bottom:0.75rem"> | |
| ${entries.slice(0,8).map(([emo,data]) => ` | |
| <div style="background:var(--s2);border:1px solid var(--b2);border-radius:7px;padding:0.6rem;text-align:center;${emotions.dominant===emo?'border-color:'+EMO_COLOR[emo]||'var(--accent)':''}"> | |
| <div style="font-size:1.2rem">${EMO_ICON[emo]||'โ'}</div> | |
| <div style="font-family:var(--mono);font-size:0.5rem;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);margin-top:0.2rem">${emo}</div> | |
| <div style="font-size:1rem;font-weight:700;color:${EMO_COLOR[emo]||'var(--text)'}">${data.count||0}</div> | |
| <div style="font-family:var(--mono);font-size:0.5rem;color:var(--muted)">${data.pct||0}%</div> | |
| </div>`).join('')} | |
| </div> | |
| <div style="font-family:var(--mono);font-size:0.58rem;color:var(--muted)"> | |
| Emosi dominan: <b style="color:var(--text)">${emotions.dominant||'โ'}</b> ยท ${emotions.emotional_pct||0}% bersifat emosional | |
| </div>`; | |
| } | |
| // โโ KEYWORDS โโ | |
| function renderKeywords(keywords) { | |
| const el = $('kw-grid'); | |
| if (!keywords.length) { el.innerHTML = '<span class="empty">Tidak ada frasa kunci.</span>'; return; } | |
| const maxScore = keywords[0].score || 1; | |
| const TYPE_COLOR = {'word':'var(--accent)','phrase':'var(--a2)','multi-phrase':'var(--gold)'}; | |
| el.innerHTML = ` | |
| <div style="display:flex;flex-wrap:wrap;gap:0.4rem"> | |
| ${keywords.map(kw => { | |
| const sz = 0.62 + (kw.score/maxScore)*0.5; | |
| const color = TYPE_COLOR[kw.type]||'var(--m2)'; | |
| return `<span style="font-family:var(--mono);font-size:${sz}rem;background:var(--s2);border:1px solid var(--b2);border-radius:5px;padding:0.2rem 0.55rem;color:${color};cursor:default;transition:border-color .15s" title="${kw.type} ยท ${kw.frequency}ร ยท score:${kw.score}">${esc(kw.phrase)}</span>`; | |
| }).join('')} | |
| </div> | |
| <div style="font-family:var(--mono);font-size:0.55rem;color:var(--muted);margin-top:0.75rem;display:flex;gap:1rem"> | |
| <span><span style="color:var(--accent)">โ </span> Kata</span> | |
| <span><span style="color:var(--a2)">โ </span> Frasa 2 kata</span> | |
| <span><span style="color:var(--gold)">โ </span> Frasa 3+ kata</span> | |
| </div>`; | |
| } | |
| // โโ SUMMARIES โโ | |
| function renderSummaries(summaries) { | |
| const el = $('sum-grid'); | |
| if (!summaries || !Object.keys(summaries).length) { | |
| el.innerHTML = '<span class="empty">Tidak ada ringkasan.</span>'; return; | |
| } | |
| const overall = summaries['_overall']; | |
| const platforms = Object.entries(summaries).filter(([k]) => k !== '_overall'); | |
| el.innerHTML = ` | |
| ${overall ? ` | |
| <div style="background:var(--s2);border:1px solid rgba(79,156,249,0.2);border-radius:7px;padding:1rem;margin-bottom:0.75rem"> | |
| <div style="font-family:var(--mono);font-size:0.5rem;letter-spacing:.15em;text-transform:uppercase;color:var(--accent);margin-bottom:0.5rem">Ringkasan Keseluruhan</div> | |
| <div style="font-size:0.8rem;line-height:1.65;color:var(--text)">${esc(overall.summary||'')}</div> | |
| <div style="font-family:var(--mono);font-size:0.52rem;color:var(--muted);margin-top:0.4rem">${overall.text_count} komentar dirangkum</div> | |
| </div>` : ''} | |
| ${platforms.map(([src,data]) => ` | |
| <div style="background:var(--s2);border:1px solid var(--b2);border-radius:7px;padding:0.75rem;margin-bottom:0.5rem"> | |
| <div style="font-family:var(--mono);font-size:0.5rem;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);margin-bottom:0.4rem">${esc(src.capitalize?.() || src)}</div> | |
| <div style="font-size:0.75rem;line-height:1.6;color:var(--m2)">${esc(data.summary||'')}</div> | |
| </div>`).join('')}`; | |
| } | |
| // โโ CALL ALL NEW RENDERS โโ | |
| function renderNewFeatures(d) { | |
| if (d.absa) renderABSA(d.absa); | |
| if (d.ner) renderNER(d.ner); | |
| if (d.stance) renderStance(d.stance); | |
| if (d.emotions) renderEmotions(d.emotions); | |
| if (d.keywords) renderKeywords(d.keywords); | |
| if (d.summaries)renderSummaries(d.summaries); | |
| } | |
| </script> | |
| </body> | |
| </html> |