noranisa commited on
Commit
a3dd3e7
Β·
verified Β·
1 Parent(s): 94b6b49

Update templates/result.html

Browse files
Files changed (1) hide show
  1. templates/result.html +580 -165
templates/result.html CHANGED
@@ -2,202 +2,617 @@
2
  <html lang="id">
3
  <head>
4
  <meta charset="UTF-8">
5
- <title>Hasil Analisis Sentimen</title>
6
-
7
- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
8
- <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
9
-
10
  <style>
11
- body { background:#0f172a; color:white; }
12
- .card {
13
- background:#1f2937;
14
- color:white;
15
- border-radius:10px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  }
17
- .table { font-size:13px; }
18
- </style>
19
-
20
- </head>
21
-
22
- <body>
23
 
24
- <div class="container mt-5">
25
-
26
- <h2>πŸ“Š Hasil Analisis Sentimen</h2>
27
- <p><b>Keyword:</b> {{ keyword }}</p>
28
- <p><b>Sumber:</b> {{ source }}</p>
29
-
30
- <!-- πŸ”₯ CHART -->
31
- <div class="row mt-3">
32
-
33
- <div class="col-md-6">
34
- <div class="card p-3">
35
- <h5>Distribusi Sentimen</h5>
36
- <canvas id="pieChart"></canvas>
37
- </div>
38
- </div>
39
-
40
- <div class="col-md-6">
41
- <div class="card p-3">
42
- <h5>Per Platform</h5>
43
- <canvas id="platformChart"></canvas>
44
- </div>
45
- </div>
46
 
47
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
- <!-- πŸ”₯ WORDCLOUD + HEATMAP -->
50
- <div class="row mt-3">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
 
52
- <div class="col-md-6">
53
- <div class="card p-3">
54
- <h5>☁️ WordCloud</h5>
55
- <img src="/static/wordcloud.png" width="100%">
56
- </div>
57
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
 
59
- <div class="col-md-6">
60
- <div class="card p-3">
61
- <h5>πŸ”₯ Heatmap</h5>
62
- <img src="/static/heatmap.png" width="100%">
63
- </div>
64
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  </div>
67
 
68
- <!-- πŸ”₯ TOP WORDS -->
69
- <div class="card mt-3 p-3">
70
- <h5>πŸ”₯ Top Kata</h5>
71
- <ul>
72
- {% for w in top_words %}
73
- <li>{{ w.word }} ({{ w.count }})</li>
74
- {% endfor %}
75
- </ul>
 
 
76
  </div>
77
 
78
- <!-- πŸ”₯ TOPIC -->
79
- <div class="card mt-3 p-3">
80
- <h5>🧠 Topic Modeling</h5>
81
- <ul>
82
- {% for t in topics %}
83
- <li><b>Topik:</b> {{ t|join(', ') }}</li>
84
- {% endfor %}
85
- </ul>
 
 
 
 
 
 
 
86
  </div>
87
 
88
- <!-- πŸ”₯ AI INSIGHT -->
89
- <div class="card mt-3 p-3">
90
- <h5>πŸ€– AI Insight</h5>
91
- <pre>{{ insight }}</pre>
 
 
92
  </div>
93
 
94
- <!-- πŸ”₯ EVALUASI -->
95
- <div class="card mt-3 p-3">
96
- <h5>πŸ“Š Evaluasi Model</h5>
97
-
98
- {% if eval_result.error %}
99
- <p class="text-danger">{{ eval_result.error }}</p>
100
- {% else %}
101
- <p><b>Accuracy:</b> {{ eval_result.accuracy }}</p>
102
-
103
- <table class="table table-bordered text-white">
104
- <tr>
105
- <th>Class</th>
106
- <th>Precision</th>
107
- <th>Recall</th>
108
- <th>F1</th>
109
- </tr>
110
-
111
- {% for label, metrics in eval_result.report.items() %}
112
- {% if label in ['positive','neutral','negative'] %}
113
- <tr>
114
- <td>{{ label }}</td>
115
- <td>{{ metrics.precision|round(2) }}</td>
116
- <td>{{ metrics.recall|round(2) }}</td>
117
- <td>{{ metrics['f1-score']|round(2) }}</td>
118
- </tr>
119
- {% endif %}
120
- {% endfor %}
121
-
122
- </table>
123
- {% endif %}
124
-
125
  </div>
126
 
127
- <!-- πŸ”₯ DOWNLOAD -->
128
- <div class="mt-3">
129
- <a href="/download" class="btn btn-success">⬇ Download CSV</a>
130
- <a href="/" class="btn btn-secondary">β¬… Kembali</a>
 
 
 
 
 
 
131
  </div>
132
 
133
- <!-- πŸ”₯ DATA -->
134
- <div class="card mt-3 p-3">
135
- <h5>Data Komentar</h5>
136
-
137
- <table class="table table-dark table-striped">
138
- <thead>
139
- <tr>
140
- <th>No</th>
141
- <th>Komentar</th>
142
- <th>Sentimen</th>
143
- <th>Source</th>
144
- </tr>
145
- </thead>
146
-
147
- <tbody>
148
- {% for t,s,src in data %}
149
- <tr>
150
- <td>{{ loop.index }}</td>
151
- <td>{{ t }}</td>
152
- <td>{{ s }}</td>
153
- <td>{{ src }}</td>
154
- </tr>
155
- {% endfor %}
156
- </tbody>
157
-
158
- </table>
159
-
160
  </div>
161
 
162
- </div>
 
 
 
 
 
 
 
163
 
164
- <!-- πŸ”₯ CHART SCRIPT -->
165
  <script>
166
-
167
- // PIE
168
- new Chart(document.getElementById('pieChart'), {
169
- type: 'pie',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  data: {
171
- labels: ["Positive","Neutral","Negative"],
172
- datasets: [{
173
- data: [
174
- {{ counts['Positive']|default(0) }},
175
- {{ counts['Neutral']|default(0) }},
176
- {{ counts['Negative']|default(0) }}
177
- ]
178
- }]
 
 
 
 
 
 
179
  }
180
- });
181
 
182
- // PLATFORM
183
- const labels = {{ platform_counts.keys()|list }};
184
- const positive = {{ platform_counts.values()|map(attribute='Positive')|list }};
185
- const negative = {{ platform_counts.values()|map(attribute='Negative')|list }};
186
- const neutral = {{ platform_counts.values()|map(attribute='Neutral')|list }};
187
 
188
- new Chart(document.getElementById('platformChart'), {
189
- type: 'bar',
190
  data: {
191
- labels: labels,
192
- datasets: [
193
- { label: 'Positive', data: positive },
194
- { label: 'Neutral', data: neutral },
195
- { label: 'Negative', data: negative }
196
- ]
 
 
 
 
 
 
 
 
 
197
  }
198
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
 
 
 
 
 
 
 
 
 
200
  </script>
201
-
202
  </body>
203
  </html>
 
2
  <html lang="id">
3
  <head>
4
  <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>SentiScope β€” Hasil Analisis</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <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">
9
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.0/chart.umd.min.js"></script>
10
  <style>
11
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
12
+ :root {
13
+ --bg: #080a0f;
14
+ --s1: #0e1117;
15
+ --s2: #141820;
16
+ --s3: #1a2030;
17
+ --b1: rgba(255,255,255,0.06);
18
+ --b2: rgba(255,255,255,0.10);
19
+ --text: #e8eaf0;
20
+ --muted: #4a5568;
21
+ --m2: #8892a4;
22
+ --accent: #4f9cf9;
23
+ --a2: #7b61ff;
24
+ --pos: #22c55e;
25
+ --neg: #ef4444;
26
+ --neu: #94a3b8;
27
+ --gold: #f59e0b;
28
+ --display:'Cabinet Grotesk', sans-serif;
29
+ --mono: 'JetBrains Mono', monospace;
30
  }
31
+ html { scroll-behavior: smooth; }
32
+ body { font-family: var(--display); background: var(--bg); color: var(--text); min-height: 100vh; }
 
 
 
 
33
 
34
+ body::before {
35
+ content:''; position:fixed; inset:0;
36
+ 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);
37
+ background-size: 64px 64px; pointer-events:none; z-index:0;
38
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
+ /* ── TOPBAR ── */
41
+ .topbar {
42
+ position: sticky; top: 0; z-index: 100;
43
+ display: grid; grid-template-columns: auto 1fr auto;
44
+ align-items: center; gap: 1.5rem;
45
+ padding: 0.9rem 2rem;
46
+ background: rgba(8,10,15,0.88); backdrop-filter: blur(20px);
47
+ border-bottom: 1px solid var(--b1);
48
+ }
49
+ .back {
50
+ display: flex; align-items: center; gap: 0.4rem;
51
+ font-family: var(--mono); font-size: 0.62rem; letter-spacing: 0.1em; text-transform: uppercase;
52
+ color: var(--m2); text-decoration: none; transition: color .2s;
53
+ }
54
+ .back:hover { color: var(--text); }
55
+ .kw-display { font-size: 0.9rem; font-weight: 700; letter-spacing: -0.01em; }
56
+ .kw-display span { color: var(--accent); }
57
+ .topbar-right { display: flex; align-items: center; gap: 1rem; }
58
+ .tb-stat { display: flex; flex-direction: column; align-items: flex-end; gap: 0.1rem; }
59
+ .tb-stat-l { font-family: var(--mono); font-size: 0.5rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted); }
60
+ .tb-stat-v { font-family: var(--mono); font-size: 0.8rem; font-weight: 500; color: var(--text); }
61
+ .btn-dl {
62
+ font-family: var(--mono); font-size: 0.62rem; letter-spacing: 0.08em; text-transform: uppercase;
63
+ background: var(--s2); color: var(--m2); border: 1px solid var(--b2);
64
+ padding: 0.5rem 1rem; border-radius: 6px; text-decoration: none;
65
+ transition: border-color .2s, color .2s;
66
+ }
67
+ .btn-dl:hover { border-color: var(--accent); color: var(--accent); }
68
+
69
+ /* ── METRIC CARDS ── */
70
+ .metrics {
71
+ display: grid; grid-template-columns: repeat(5, 1fr);
72
+ gap: 1px; background: var(--b1);
73
+ border-bottom: 1px solid var(--b1);
74
+ position: relative; z-index: 1;
75
+ }
76
+ .metric {
77
+ background: var(--s1); padding: 1.5rem 2rem;
78
+ display: flex; flex-direction: column; gap: 0.5rem;
79
+ transition: background .2s;
80
+ opacity: 0; transform: translateY(12px);
81
+ animation: fu 0.45s both;
82
+ }
83
+ .metric:hover { background: var(--s2); }
84
+ .metric:nth-child(1){animation-delay:.05s}
85
+ .metric:nth-child(2){animation-delay:.1s}
86
+ .metric:nth-child(3){animation-delay:.15s}
87
+ .metric:nth-child(4){animation-delay:.2s}
88
+ .metric:nth-child(5){animation-delay:.25s}
89
+ .metric-l { font-family: var(--mono); font-size: 0.52rem; letter-spacing: 0.16em; text-transform: uppercase; color: var(--muted); }
90
+ .metric-n { font-size: 2.8rem; font-weight: 800; letter-spacing: -0.04em; line-height: 1; }
91
+ .metric-n.c-pos { color: var(--pos); }
92
+ .metric-n.c-neg { color: var(--neg); }
93
+ .metric-n.c-neu { color: var(--neu); }
94
+ .metric-n.c-up { color: var(--pos); }
95
+ .metric-n.c-dn { color: var(--neg); }
96
+ .metric-n.c-base{ color: var(--text); }
97
+ .metric-sub { font-family: var(--mono); font-size: 0.58rem; color: var(--muted); }
98
+
99
+ /* ── DISTRIBUTION BAR ── */
100
+ .dist-bar {
101
+ position: relative; z-index: 1;
102
+ display: flex; align-items: center; gap: 1.5rem;
103
+ padding: 1rem 2rem; border-bottom: 1px solid var(--b1);
104
+ background: var(--s1);
105
+ }
106
+ .dist-l { font-family: var(--mono); font-size: 0.55rem; letter-spacing: 0.15em; text-transform: uppercase; color: var(--muted); white-space: nowrap; }
107
+ .dist-track { flex: 1; height: 5px; background: var(--s2); border-radius: 3px; display: flex; overflow: hidden; gap: 2px; }
108
+ .ds { height: 100%; transition: width 1.2s cubic-bezier(.25,.46,.45,.94); border-radius: 2px; }
109
+ .ds-p { background: var(--pos); }
110
+ .ds-n { background: var(--neg); }
111
+ .ds-u { background: var(--neu); }
112
+ .dist-info { font-family: var(--mono); font-size: 0.58rem; color: var(--m2); white-space: nowrap; }
113
+
114
+ /* ── GRID LAYOUT ── */
115
+ .main-grid {
116
+ position: relative; z-index: 1;
117
+ display: grid; grid-template-columns: 1fr 1fr;
118
+ gap: 1px; background: var(--b1);
119
+ border-bottom: 1px solid var(--b1);
120
+ }
121
+ .three-col {
122
+ display: grid; grid-template-columns: 1fr 1fr 1fr;
123
+ gap: 1px; background: var(--b1);
124
+ border-bottom: 1px solid var(--b1);
125
+ }
126
+ .full-row {
127
+ position: relative; z-index: 1;
128
+ border-bottom: 1px solid var(--b1);
129
+ }
130
 
131
+ /* ── PANEL ── */
132
+ .panel { background: var(--s1); padding: 1.75rem 2rem; }
133
+ .panel:hover { background: var(--s1); }
134
+ .panel-tag {
135
+ font-family: var(--mono); font-size: 0.5rem; letter-spacing: 0.18em; text-transform: uppercase;
136
+ color: var(--muted); margin-bottom: 1.25rem;
137
+ display: flex; align-items: center; gap: 0.8rem;
138
+ }
139
+ .panel-tag::after { content:''; flex:1; height:1px; background: var(--b2); }
140
+ .panel-title { font-size: 1.1rem; font-weight: 700; letter-spacing: -0.02em; margin-bottom: 1.25rem; }
141
+
142
+ /* ── CHART ── */
143
+ .chart-wrap { position:relative; height:240px; }
144
+
145
+ /* ── TABLE ── */
146
+ .tbl-wrap { overflow-x:auto; max-height: 320px; overflow-y:auto; }
147
+ .tbl-wrap::-webkit-scrollbar { width: 3px; height: 3px; }
148
+ .tbl-wrap::-webkit-scrollbar-track { background: transparent; }
149
+ .tbl-wrap::-webkit-scrollbar-thumb { background: var(--b2); border-radius: 2px; }
150
+ table { width:100%; border-collapse:collapse; font-family: var(--mono); font-size:0.68rem; }
151
+ th {
152
+ text-align:left; font-size:0.5rem; letter-spacing:.15em; text-transform:uppercase;
153
+ color: var(--muted); padding: 0.5rem 0.75rem; border-bottom:1px solid var(--b2);
154
+ position: sticky; top: 0; background: var(--s1);
155
+ }
156
+ td { padding: 0.65rem 0.75rem; border-bottom: 1px solid rgba(255,255,255,0.03); vertical-align:top; line-height:1.45; }
157
+ tr:last-child td { border-bottom:none; }
158
+ tr:hover td { background: var(--s2); }
159
+ .tag {
160
+ display:inline-block; font-size:0.5rem; letter-spacing:.08em; text-transform:uppercase;
161
+ padding: 0.15rem 0.4rem; border-radius: 4px; border: 1px solid; white-space:nowrap;
162
+ }
163
+ .tag-p { color: var(--pos); border-color: rgba(34,197,94,0.3); background: rgba(34,197,94,0.08); }
164
+ .tag-n { color: var(--neg); border-color: rgba(239,68,68,0.3); background: rgba(239,68,68,0.08); }
165
+ .tag-u { color: var(--neu); border-color: rgba(148,163,184,0.3); background: rgba(148,163,184,0.08); }
166
+ .src-badge {
167
+ font-size: 0.48rem; letter-spacing: .06em; text-transform:uppercase;
168
+ padding: 0.12rem 0.35rem; border-radius: 3px; border: 1px solid var(--b2); color: var(--muted);
169
+ }
170
 
171
+ /* ── WORD CLOUD ── */
172
+ .word-cloud { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: baseline; }
173
+ .w-chip { font-family: var(--mono); color: var(--m2); cursor:default; transition: color .2s; line-height: 1.2; }
174
+ .w-chip:hover { color: var(--accent); }
175
+
176
+ /* ── TOPIC CARDS ── */
177
+ .topic-list { display: flex; flex-direction: column; gap: 0.75rem; }
178
+ .topic-card { background: var(--s2); border: 1px solid var(--b2); border-radius: 8px; padding: 0.9rem 1rem; }
179
+ .topic-card-h { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.6rem; }
180
+ .topic-num { font-family: var(--mono); font-size: 0.5rem; letter-spacing: .15em; text-transform: uppercase; color: var(--muted); }
181
+ .topic-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); opacity: 0.6; }
182
+ .topic-words { display: flex; flex-wrap: wrap; gap: 0.3rem; }
183
+ .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); }
184
+
185
+ /* ── CLUSTERS ── */
186
+ .cluster-list { display: flex; flex-direction: column; gap: 0.75rem; }
187
+ .cl-item { border-left: 2px solid var(--b2); padding-left: 0.9rem; }
188
+ .cl-head { font-family: var(--mono); font-size: 0.52rem; letter-spacing: .12em; text-transform: uppercase; color: var(--muted); margin-bottom: 0.35rem; }
189
+ .cl-text { font-family: var(--mono); font-size: 0.65rem; color: var(--m2); line-height: 1.5; }
190
+
191
+ /* ── HOAX LIST ── */
192
+ .hoax-list { display: flex; flex-direction: column; gap: 0.5rem; }
193
+ .hoax-item {
194
+ display: flex; align-items: flex-start; gap: 0.7rem;
195
+ background: var(--s2); border: 1px solid var(--b1); border-radius: 6px; padding: 0.65rem 0.75rem;
196
+ transition: border-color .2s;
197
+ }
198
+ .hoax-item:hover { border-color: var(--b2); }
199
+ .h-badge {
200
+ font-family: var(--mono); font-size: 0.48rem; letter-spacing: .08em; text-transform: uppercase;
201
+ padding: 0.15rem 0.4rem; border-radius: 3px; white-space: nowrap; flex-shrink: 0; margin-top: 0.1rem;
202
+ }
203
+ .hb-hoax { background: rgba(239,68,68,0.15); color: var(--neg); border: 1px solid rgba(239,68,68,0.25); }
204
+ .hb-norm { background: var(--s3); color: var(--muted); border: 1px solid var(--b2); }
205
+ .h-text { font-family: var(--mono); font-size: 0.65rem; color: var(--m2); line-height: 1.5; }
206
+
207
+ /* ── BOT LIST ── */
208
+ .bot-list { display: flex; flex-direction: column; gap: 0.5rem; }
209
+ .bot-item { display: flex; align-items: center; gap: 0.75rem; padding: 0.5rem 0; border-bottom: 1px solid var(--b1); }
210
+ .bot-item:last-child { border-bottom: none; }
211
+ .bot-id { font-family: var(--mono); font-size: 0.62rem; color: var(--m2); min-width: 3.5rem; }
212
+ .bot-bar { flex: 1; height: 3px; background: var(--s2); border-radius: 2px; overflow: hidden; }
213
+ .bot-fill { height: 100%; background: linear-gradient(90deg, var(--accent), var(--a2)); border-radius: 2px; transition: width 1s ease; }
214
+ .bot-score { font-family: var(--mono); font-size: 0.6rem; color: var(--muted); min-width: 2.5rem; text-align: right; }
215
+
216
+ /* ── INSIGHT ── */
217
+ .insight-block {
218
+ position: relative; z-index: 1;
219
+ background: linear-gradient(135deg, var(--s2) 0%, var(--s3) 100%);
220
+ border-bottom: 1px solid var(--b1);
221
+ padding: 2.5rem 2rem;
222
+ display: grid; grid-template-columns: 1fr auto; gap: 2rem; align-items: center;
223
+ overflow: hidden;
224
+ }
225
+ .insight-block::before {
226
+ content:''; position:absolute; top:0;left:0;right:0;height:1px;
227
+ background: linear-gradient(90deg, transparent, rgba(79,156,249,0.4), transparent);
228
+ }
229
+ .insight-text { font-size: 1.2rem; font-weight: 700; letter-spacing: -0.02em; line-height: 1.5; }
230
+ .insight-text em { font-style: normal; color: var(--accent); }
231
+ .insight-meta { font-family: var(--mono); font-size: 0.58rem; color: var(--muted); line-height: 1.7; white-space: nowrap; text-align: right; }
232
+
233
+ /* ── IMAGES ── */
234
+ .img-grid {
235
+ position: relative; z-index: 1;
236
+ display: grid; grid-template-columns: 1fr 1fr;
237
+ gap: 1px; background: var(--b1);
238
+ border-bottom: 1px solid var(--b1);
239
+ }
240
+ .img-panel { background: var(--s1); padding: 1.75rem 2rem; }
241
+ .img-panel img { width:100%; height:auto; display:block; border: 1px solid var(--b2); border-radius: 6px; }
242
+ .img-ph {
243
+ width:100%; height:160px; background: var(--s2); border: 1px dashed var(--b2); border-radius: 6px;
244
+ display:flex; align-items:center; justify-content:center;
245
+ font-family: var(--mono); font-size:0.62rem; color: var(--muted);
246
+ }
247
 
248
+ /* ── FOOTER ── */
249
+ footer {
250
+ position: relative; z-index: 1;
251
+ padding: 1.5rem 2rem;
252
+ display: flex; align-items: center; justify-content: space-between;
253
+ border-top: 1px solid var(--b1);
254
+ }
255
+ .ft-kw { font-family: var(--mono); font-size: 0.6rem; color: var(--muted); }
256
+ .ft-kw strong { font-family: var(--display); font-size: 0.8rem; font-weight: 700; color: var(--accent); }
257
+ .ft-r { display: flex; gap: 1rem; }
258
+ .ft-r a {
259
+ font-family: var(--mono); font-size: 0.6rem; letter-spacing: .08em; text-transform: uppercase;
260
+ color: var(--muted); text-decoration: none; transition: color .2s;
261
+ }
262
+ .ft-r a:hover { color: var(--text); }
263
+
264
+ /* ── EMPTY ── */
265
+ .empty { font-family: var(--mono); font-size: 0.65rem; color: var(--muted); font-style: italic; }
266
+
267
+ /* ── ANIM ── */
268
+ @keyframes fu { from{opacity:0;transform:translateY(14px)} to{opacity:1;transform:translateY(0)} }
269
+
270
+ /* ── RESPONSIVE ── */
271
+ @media(max-width:960px){
272
+ .metrics { grid-template-columns: repeat(3,1fr); }
273
+ .main-grid { grid-template-columns: 1fr; }
274
+ .three-col { grid-template-columns: 1fr; }
275
+ .img-grid { grid-template-columns: 1fr; }
276
+ .insight-block { grid-template-columns: 1fr; }
277
+ .insight-meta { text-align: left; white-space: normal; }
278
+ .topbar { padding: 0.8rem 1.2rem; }
279
+ .panel { padding: 1.5rem; }
280
+ }
281
+ </style>
282
+ </head>
283
+ <body>
284
 
285
+ <!-- TOPBAR -->
286
+ <header class="topbar">
287
+ <a href="/" class="back">← Kembali</a>
288
+ <div class="kw-display">Analisis: <span id="h-kw">β€”</span></div>
289
+ <div class="topbar-right">
290
+ <div class="tb-stat">
291
+ <span class="tb-stat-l">Total</span>
292
+ <span class="tb-stat-v" id="h-total">β€”</span>
293
+ </div>
294
+ <div class="tb-stat">
295
+ <span class="tb-stat-l">Sumber</span>
296
+ <span class="tb-stat-v" id="h-src">β€”</span>
297
+ </div>
298
+ <a href="/download" class="btn-dl">↓ CSV</a>
299
+ </div>
300
+ </header>
301
+
302
+ <!-- METRICS -->
303
+ <section class="metrics">
304
+ <div class="metric">
305
+ <span class="metric-l">Total Komentar</span>
306
+ <div class="metric-n c-base" id="m-total">β€”</div>
307
+ <span class="metric-sub">data diproses</span>
308
+ </div>
309
+ <div class="metric">
310
+ <span class="metric-l">Positif</span>
311
+ <div class="metric-n c-pos" id="m-pos">β€”</div>
312
+ <span class="metric-sub" id="m-pos-p">0%</span>
313
+ </div>
314
+ <div class="metric">
315
+ <span class="metric-l">Negatif</span>
316
+ <div class="metric-n c-neg" id="m-neg">β€”</div>
317
+ <span class="metric-sub" id="m-neg-p">0%</span>
318
+ </div>
319
+ <div class="metric">
320
+ <span class="metric-l">Netral</span>
321
+ <div class="metric-n c-neu" id="m-neu">β€”</div>
322
+ <span class="metric-sub" id="m-neu-p">0%</span>
323
+ </div>
324
+ <div class="metric">
325
+ <span class="metric-l">Prediksi Tren</span>
326
+ <div class="metric-n" id="m-trend" style="font-size:1.4rem;line-height:1.6">β€”</div>
327
+ <span class="metric-sub">regresi linier</span>
328
+ </div>
329
+ </section>
330
+
331
+ <!-- DISTRIBUTION -->
332
+ <div class="dist-bar">
333
+ <span class="dist-l">Distribusi Sentimen</span>
334
+ <div class="dist-track">
335
+ <div class="ds ds-p" id="ds-p" style="width:0%"></div>
336
+ <div class="ds ds-n" id="ds-n" style="width:0%"></div>
337
+ <div class="ds ds-u" id="ds-u" style="width:0%"></div>
338
+ </div>
339
+ <span class="dist-info" id="dist-info">β€”</span>
340
  </div>
341
 
342
+ <!-- CHART + WORDS -->
343
+ <div class="main-grid">
344
+ <div class="panel">
345
+ <div class="panel-tag">01 β€” Distribusi Sentimen</div>
346
+ <div class="chart-wrap"><canvas id="c-pie"></canvas></div>
347
+ </div>
348
+ <div class="panel">
349
+ <div class="panel-tag">02 β€” Kata Dominan</div>
350
+ <div class="word-cloud" id="words"><span class="empty">Memuat…</span></div>
351
+ </div>
352
  </div>
353
 
354
+ <!-- TABLE + TOPICS -->
355
+ <div class="main-grid">
356
+ <div class="panel">
357
+ <div class="panel-tag">03 β€” Data Komentar</div>
358
+ <div class="tbl-wrap">
359
+ <table>
360
+ <thead><tr><th>#</th><th>Sumber</th><th>Komentar</th><th>Sentimen</th></tr></thead>
361
+ <tbody id="tbl"></tbody>
362
+ </table>
363
+ </div>
364
+ </div>
365
+ <div class="panel">
366
+ <div class="panel-tag">04 β€” Topic Modeling (LDA)</div>
367
+ <div class="topic-list" id="topics"><span class="empty">Memuat…</span></div>
368
+ </div>
369
  </div>
370
 
371
+ <!-- TIMELINE -->
372
+ <div class="full-row">
373
+ <div class="panel">
374
+ <div class="panel-tag">05 β€” Timeline Sentimen</div>
375
+ <div class="chart-wrap" style="height:200px"><canvas id="c-line"></canvas></div>
376
+ </div>
377
  </div>
378
 
379
+ <!-- CLUSTER + HOAX + BOT -->
380
+ <div class="three-col">
381
+ <div class="panel">
382
+ <div class="panel-tag">06 β€” Kluster Opini</div>
383
+ <div class="cluster-list" id="clusters"><span class="empty">Memuat…</span></div>
384
+ </div>
385
+ <div class="panel">
386
+ <div class="panel-tag">07 β€” Deteksi Konten Hoaks</div>
387
+ <div class="hoax-list" id="hoax"><span class="empty">Memuat…</span></div>
388
+ </div>
389
+ <div class="panel">
390
+ <div class="panel-tag">08 β€” Bot Network Score</div>
391
+ <div class="bot-list" id="bots"><span class="empty">Memuat…</span></div>
392
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
393
  </div>
394
 
395
+ <!-- IMAGES -->
396
+ <div class="img-grid">
397
+ <div class="img-panel">
398
+ <div class="panel-tag">09 β€” Word Cloud</div>
399
+ <div id="wc-box"><div class="img-ph">Wordcloud diproses server</div></div>
400
+ </div>
401
+ <div class="img-panel">
402
+ <div class="panel-tag">10 β€” Heatmap Sumber Γ— Sentimen</div>
403
+ <div id="hm-box"><div class="img-ph">Heatmap diproses server</div></div>
404
+ </div>
405
  </div>
406
 
407
+ <!-- INSIGHT -->
408
+ <div class="insight-block">
409
+ <p class="insight-text" id="insight-text">Mengolah data analisis…</p>
410
+ <p class="insight-meta" id="insight-meta"></p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
411
  </div>
412
 
413
+ <!-- FOOTER -->
414
+ <footer>
415
+ <div class="ft-kw">Kata kunci: <strong id="ft-kw">β€”</strong></div>
416
+ <div class="ft-r">
417
+ <a href="/">← Analisis Baru</a>
418
+ <a href="/download">↓ Unduh CSV</a>
419
+ </div>
420
+ </footer>
421
 
 
422
  <script>
423
+ // ── CHART.JS DEFAULTS ──
424
+ Chart.defaults.color = '#4a5568';
425
+ Chart.defaults.borderColor = 'rgba(255,255,255,0.05)';
426
+ Chart.defaults.font.family = "'JetBrains Mono', monospace";
427
+
428
+ // ── HELPERS ──
429
+ const pct = (n, t) => Math.round((n / (t || 1)) * 100);
430
+ const esc = s => String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
431
+ const sc = s => { if(!s)return'u'; const l=s.toLowerCase(); return l.includes('pos')?'p':l.includes('neg')?'n':'u'; };
432
+ const $ = id => document.getElementById(id);
433
+
434
+ // ── LOAD DATA ──
435
+ let D = null;
436
+ try { const r = sessionStorage.getItem('analysisResult'); if(r) D = JSON.parse(r); } catch(e){}
437
+ if(!D && typeof window.__DATA__!=='undefined') D = window.__DATA__;
438
+ if(D) render(D);
439
+ else $('insight-text').textContent = 'Tidak ada data. Kembali ke halaman utama untuk memulai analisis baru.';
440
+
441
+ function render(d) {
442
+ const data = d.data || [];
443
+ const words = d.top_words || [];
444
+ const topics= d.topics || [];
445
+ const clust = d.clusters || [];
446
+ const hoax = d.hoax || [];
447
+ const botN = d.bot_network || { nodes:[], edges:[], bots:[] };
448
+ const trend = d.trend || 'β€”';
449
+ const kw = d.keyword || 'β€”';
450
+ const src = d.source || 'all';
451
+
452
+ const pos = data.filter(x=>x.sentiment==='Positive').length;
453
+ const neg = data.filter(x=>x.sentiment==='Negative').length;
454
+ const neu = data.filter(x=>x.sentiment==='Neutral').length;
455
+ const tot = data.length || 1;
456
+
457
+ // HEADER
458
+ $('h-kw').textContent = kw;
459
+ $('h-total').textContent = data.length;
460
+ $('h-src').textContent = src==='all'?'YT+Reddit':src;
461
+ $('ft-kw').textContent = kw;
462
+
463
+ // METRICS
464
+ $('m-total').textContent = data.length;
465
+ $('m-pos').textContent = pos;
466
+ $('m-neg').textContent = neg;
467
+ $('m-neu').textContent = neu;
468
+ $('m-pos-p').textContent = pct(pos,tot)+'%';
469
+ $('m-neg-p').textContent = pct(neg,tot)+'%';
470
+ $('m-neu-p').textContent = pct(neu,tot)+'%';
471
+
472
+ const tEl = $('m-trend');
473
+ tEl.textContent = trend;
474
+ const isUp = trend && trend.toLowerCase().includes('positif');
475
+ tEl.className = 'metric-n ' + (isUp ? 'c-up' : 'c-dn');
476
+
477
+ // DIST BAR
478
+ setTimeout(() => {
479
+ $('ds-p').style.width = pct(pos,tot)+'%';
480
+ $('ds-n').style.width = pct(neg,tot)+'%';
481
+ $('ds-u').style.width = pct(neu,tot)+'%';
482
+ }, 100);
483
+ $('dist-info').textContent = `${pct(pos,tot)}% Pos Β· ${pct(neg,tot)}% Neg Β· ${pct(neu,tot)}% Neu`;
484
+
485
+ // PIE
486
+ new Chart($('c-pie').getContext('2d'), {
487
+ type: 'doughnut',
488
  data: {
489
+ labels: ['Positif','Negatif','Netral'],
490
+ datasets: [{
491
+ data: [pos, neg, neu],
492
+ backgroundColor: ['rgba(34,197,94,0.85)','rgba(239,68,68,0.85)','rgba(148,163,184,0.85)'],
493
+ borderColor: ['rgba(34,197,94,0.2)','rgba(239,68,68,0.2)','rgba(148,163,184,0.2)'],
494
+ borderWidth: 1, hoverOffset: 6
495
+ }]
496
+ },
497
+ options: {
498
+ cutout: '68%', responsive: true, maintainAspectRatio: false,
499
+ plugins: {
500
+ legend: { position:'right', labels:{ font:{size:11}, color:'#8892a4', padding:16, usePointStyle:true, pointStyleWidth:8 } },
501
+ tooltip: { callbacks: { label: ctx => ` ${ctx.label}: ${ctx.parsed} (${pct(ctx.parsed,tot)}%)` } }
502
+ }
503
  }
504
+ });
505
 
506
+ // LINE
507
+ 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; });
508
+ const pl = data.map(x=>x.sentiment==='Positive'?1:0);
509
+ const nl = data.map(x=>x.sentiment==='Negative'?1:0);
510
+ const ul = data.map(x=>x.sentiment==='Neutral'?1:0);
511
 
512
+ new Chart($('c-line').getContext('2d'), {
513
+ type: 'line',
514
  data: {
515
+ labels: data.map((_,i)=>i+1),
516
+ datasets: [
517
+ { 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 },
518
+ { 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 },
519
+ { 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 },
520
+ ]
521
+ },
522
+ options: {
523
+ responsive: true, maintainAspectRatio: false,
524
+ interaction: { mode:'index', intersect:false },
525
+ scales: {
526
+ x: { ticks:{ maxTicksLimit:12, font:{size:9} }, grid:{ color:'rgba(255,255,255,0.04)' } },
527
+ y: { ticks:{ font:{size:9} }, grid:{ color:'rgba(255,255,255,0.04)' } }
528
+ },
529
+ plugins: { legend:{ labels:{ font:{size:10}, color:'#8892a4', usePointStyle:true } } }
530
  }
531
+ });
532
+
533
+ // WORDS
534
+ const wg = $('words');
535
+ if(words.length) {
536
+ const mx = words[0].count || 1;
537
+ wg.innerHTML = words.map(w=>{
538
+ const sz = 0.65 + (w.count/mx)*0.95;
539
+ const op = 0.45 + (w.count/mx)*0.55;
540
+ return `<span class="w-chip" style="font-size:${sz}rem;opacity:${op}" title="${w.count}x">${w.word}</span>`;
541
+ }).join('');
542
+ } else wg.innerHTML = '<span class="empty">Tidak ada data kata.</span>';
543
+
544
+ // TABLE
545
+ const tb = $('tbl');
546
+ if(data.length) {
547
+ tb.innerHTML = data.slice(0,50).map((r,i)=>`
548
+ <tr>
549
+ <td style="color:var(--muted)">${i+1}</td>
550
+ <td><span class="src-badge">${esc(r.source||'β€”')}</span></td>
551
+ <td style="max-width:280px;color:var(--m2)">${esc((r.text||'').substring(0,100))}</td>
552
+ <td><span class="tag tag-${sc(r.sentiment)}">${r.sentiment||'β€”'}</span></td>
553
+ </tr>`).join('');
554
+ } else tb.innerHTML = '<tr><td colspan="4" class="empty" style="padding:.75rem">Tidak ada data.</td></tr>';
555
+
556
+ // TOPICS
557
+ const tg = $('topics');
558
+ if(topics.length) {
559
+ tg.innerHTML = topics.map((t,i)=>`
560
+ <div class="topic-card">
561
+ <div class="topic-card-h"><span class="topic-num">Topik ${i+1}</span><span class="topic-dot"></span></div>
562
+ <div class="topic-words">${(Array.isArray(t)?t:[]).map(w=>`<span class="tw">${w}</span>`).join('')}</div>
563
+ </div>`).join('');
564
+ } else tg.innerHTML = '<span class="empty">Data tidak cukup untuk topic modeling.</span>';
565
+
566
+ // CLUSTERS
567
+ const cg = $('clusters');
568
+ if(clust.length) {
569
+ cg.innerHTML = clust.map(c=>`
570
+ <div class="cl-item">
571
+ <div class="cl-head">Kluster ${c.cluster}</div>
572
+ ${(c.samples||[]).slice(0,2).map(s=>`<div class="cl-text">"${esc(s.substring(0,85))}…"</div>`).join('')}
573
+ </div>`).join('');
574
+ } else cg.innerHTML = '<span class="empty">Data tidak cukup untuk clustering.</span>';
575
+
576
+ // HOAX
577
+ const hg = $('hoax');
578
+ if(hoax.length) {
579
+ hg.innerHTML = hoax.map(h=>`
580
+ <div class="hoax-item">
581
+ <span class="h-badge ${h.label==='Hoax'?'hb-hoax':'hb-norm'}">${h.label}</span>
582
+ <span class="h-text">${esc((h.text||'').substring(0,90))}</span>
583
+ </div>`).join('');
584
+ } else hg.innerHTML = '<span class="empty">Tidak ada konten terdeteksi.</span>';
585
+
586
+ // BOT
587
+ const bg = $('bots');
588
+ const bots = (botN.bots||[]);
589
+ if(bots.length) {
590
+ bg.innerHTML = bots.slice(0,8).map(b=>`
591
+ <div class="bot-item">
592
+ <span class="bot-id">Node ${b.node}</span>
593
+ <div class="bot-bar"><div class="bot-fill" style="width:${b.score*100}%"></div></div>
594
+ <span class="bot-score">${b.score}</span>
595
+ </div>`).join('');
596
+ } else bg.innerHTML = '<span class="empty">Tidak ada bot terdeteksi.</span>';
597
+
598
+ // INSIGHT
599
+ const dom = pos>=neg&&pos>=neu?'positif':neg>=pos&&neg>=neu?'negatif':'netral';
600
+ $('insight-text').innerHTML = `Dari <em>${data.length}</em> komentar, opini publik terhadap "<em>${esc(kw)}</em>" cenderung <em>${dom}</em> β€” ${pct(pos,tot)}% positif, ${pct(neg,tot)}% negatif, ${pct(neu,tot)}% netral.`;
601
+ $('insight-meta').innerHTML = `Model: IndoBERT (w11wo/indonesian-roberta-base)<br>${new Date().toLocaleDateString('id-ID',{weekday:'long',year:'numeric',month:'long',day:'numeric'})}<br>Tren: ${trend}`;
602
+
603
+ // IMAGES
604
+ loadImg('static/wordcloud.png','wc-box');
605
+ loadImg('static/heatmap.png','hm-box');
606
+ }
607
 
608
+ function loadImg(src, boxId) {
609
+ const img = new Image();
610
+ img.onload = () => {
611
+ img.style.width='100%'; img.style.borderRadius='6px';
612
+ $(boxId).innerHTML = ''; $(boxId).appendChild(img);
613
+ };
614
+ img.src = '/' + src + '?t=' + Date.now();
615
+ }
616
  </script>
 
617
  </body>
618
  </html>