Varriety commited on
Commit
0d81ea1
·
verified ·
1 Parent(s): a65e686

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +186 -318
src/streamlit_app.py CHANGED
@@ -5,6 +5,7 @@ import re
5
  import io
6
  import time
7
  import requests
 
8
  import matplotlib.pyplot as plt
9
  import seaborn as sns
10
  from datetime import datetime, timezone
@@ -20,7 +21,7 @@ from langdetect import detect, DetectorFactory
20
  DetectorFactory.seed = 0
21
 
22
  # ==============================
23
- # SETTING PATH ABSOLUT UNTUK GAMBAR
24
  # ==============================
25
  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
26
 
@@ -41,12 +42,12 @@ if 'page' not in st.session_state:
41
  st.session_state.page = "uji_kalimat"
42
 
43
  # ==============================
44
- # GLOBAL CSS — Vancouver Bitcoin Style
45
  # ==============================
46
  st.markdown("""
47
  <style>
48
  /* ── Google Fonts ── */
49
- @import url('https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=DM+Sans:ital,wght@0,400;0,500;0,700;1,400&display=swap');
50
 
51
  /* ── Reset Streamlit chrome ── */
52
  #MainMenu, footer, header { visibility: hidden; }
@@ -57,13 +58,19 @@ st.markdown("""
57
  }
58
 
59
  html, body, [class*="css"] {
60
- font-family: 'DM Sans', sans-serif !important;
 
 
 
 
 
61
  }
62
 
63
  /* ── Custom Scrollbar ── */
64
  ::-webkit-scrollbar { width: 6px; }
65
- ::-webkit-scrollbar-track { background: #FFF3E0; }
66
- ::-webkit-scrollbar-thumb { background: #F7931A; border-radius: 3px; }
 
67
 
68
  /* ── NAVBAR WRAPPER ── */
69
  .vbc-navbar {
@@ -71,397 +78,284 @@ st.markdown("""
71
  align-items: center;
72
  justify-content: space-between;
73
  padding: 0 3rem;
74
- height: 72px;
75
- background: #1A1033;
76
  position: sticky;
77
  top: 0;
78
  z-index: 999;
79
- box-shadow: 0 2px 20px rgba(0,0,0,0.25);
80
  }
81
  .vbc-logo {
82
- font-family: 'Manrope', sans-serif;
83
- font-weight: 800;
84
- font-size: 1.35rem;
85
- color: #FFFFFF !important;
86
  display: flex;
87
  align-items: center;
88
- gap: 10px;
89
- letter-spacing: -0.5px;
90
  }
91
  .vbc-logo-icon {
92
- background: #F7931A;
93
  color: white;
94
- width: 36px;
95
- height: 36px;
96
  border-radius: 50%;
97
  display: inline-flex;
98
  align-items: center;
99
  justify-content: center;
100
- font-size: 1.1rem;
101
- font-weight: 800;
102
  }
103
 
104
  /* ── HERO SECTION (Uji Kalimat) ── */
105
  .hero-wrap {
106
- background: #F7931A;
107
- min-height: calc(100vh - 72px);
108
  display: flex;
109
  align-items: center;
110
  position: relative;
111
  overflow: hidden;
112
  padding: 4rem 3rem;
113
  }
114
- .hero-wrap::before {
115
- content: '';
116
- position: absolute;
117
- inset: 0;
118
- background: radial-gradient(ellipse at 80% 50%, rgba(255,255,255,0.08) 0%, transparent 60%),
119
- radial-gradient(ellipse at 20% 80%, rgba(0,0,0,0.12) 0%, transparent 50%);
120
- pointer-events: none;
121
- }
122
- /* sparkle dots decoration */
123
- .hero-wrap::after {
124
- content: '✦ ✦ ✦';
125
- position: absolute;
126
- top: 2rem;
127
- right: 3rem;
128
- font-size: 1.4rem;
129
- color: rgba(255,255,255,0.25);
130
- letter-spacing: 1.2rem;
131
- }
132
  .hero-badge {
133
  display: inline-block;
134
- background: rgba(255,255,255,0.18);
135
- border: 1px solid rgba(255,255,255,0.35);
136
- color: #FFFFFF;
137
- font-size: 0.78rem;
138
- font-weight: 700;
139
- letter-spacing: 1.5px;
140
- text-transform: uppercase;
141
- padding: 5px 14px;
142
- border-radius: 50px;
143
- margin-bottom: 1.2rem;
144
  }
145
  .hero-title {
146
- font-family: 'Manrope', sans-serif !important;
147
- font-size: 3.8rem;
148
- font-weight: 800;
149
- line-height: 1.08;
150
- letter-spacing: -1.5px;
151
- color: #FFFFFF !important;
152
- margin: 0 0 1.2rem;
153
  }
154
  .hero-title span {
155
- color: #1A1033;
156
  }
157
  .hero-sub {
158
- font-size: 1.05rem;
159
- color: rgba(255,255,255,0.85) !important;
160
  max-width: 500px;
161
- line-height: 1.7;
162
  margin-bottom: 2rem;
163
  }
164
  .hero-card {
165
- background: rgba(255,255,255,0.13);
166
- border: 1px solid rgba(255,255,255,0.28);
167
- backdrop-filter: blur(6px);
168
- border-radius: 14px;
169
- padding: 14px 20px;
170
  display: inline-flex;
171
  align-items: center;
172
  gap: 10px;
173
- margin-bottom: 2.2rem;
 
174
  }
175
  .hero-card-dot {
176
  width: 8px; height: 8px;
177
  border-radius: 50%;
178
- background: #1A1033;
179
  flex-shrink: 0;
180
  }
181
  .hero-card p {
182
  margin: 0;
183
- font-size: 0.88rem;
184
- color: #FFFFFF !important;
185
  font-weight: 500;
186
  }
187
 
188
  /* ── BATCH SECTION ── */
189
  .batch-wrap {
190
- background: #FEF8F0;
191
- min-height: calc(100vh - 72px);
192
- padding: 5rem 3rem 4rem;
193
- position: relative;
194
- }
195
- .batch-wrap::before {
196
- content: '';
197
- position: absolute;
198
- inset: 0;
199
- background-image: radial-gradient(circle, rgba(247,147,26,0.06) 1px, transparent 1px);
200
- background-size: 28px 28px;
201
- pointer-events: none;
202
  }
203
  .batch-eyebrow {
204
- font-family: 'Manrope', sans-serif;
205
- font-size: 0.75rem;
206
- font-weight: 700;
207
- letter-spacing: 2px;
208
- text-transform: uppercase;
209
- color: #F7931A !important;
210
- margin-bottom: 0.75rem;
211
  }
212
  .batch-title {
213
- font-family: 'Manrope', sans-serif !important;
214
- font-size: 2.8rem;
215
- font-weight: 800;
216
- letter-spacing: -1px;
217
- color: #1A1033 !important;
218
- line-height: 1.1;
219
  margin-bottom: 1rem;
220
  }
221
  .batch-sub {
222
- font-size: 1.05rem;
223
- color: #64748B !important;
224
  max-width: 480px;
225
- line-height: 1.7;
226
  margin-bottom: 2rem;
227
  }
228
 
229
  /* ── RESULT / DASHBOARD SECTION ── */
230
  .result-wrap {
231
  background: #FFFFFF;
232
- padding: 3rem 3rem 4rem;
233
- border-top: 4px solid #F7931A;
234
  }
235
  .section-label {
236
- font-family: 'Manrope', sans-serif;
237
- font-size: 0.72rem;
238
- font-weight: 700;
239
- letter-spacing: 2px;
240
- text-transform: uppercase;
241
- color: #F7931A !important;
242
  margin-bottom: 0.5rem;
243
  }
244
  .section-title {
245
- font-family: 'Manrope', sans-serif !important;
246
- font-size: 1.9rem;
247
- font-weight: 800;
248
- letter-spacing: -0.5px;
249
- color: #1A1033 !important;
250
- margin-bottom: 1.8rem;
251
- }
252
-
253
- /* ── SENTIMENT RESULT CARD ── */
254
- .sent-card {
255
- background: #F8FAFC;
256
- border: 1px solid #E2E8F0;
257
- border-radius: 16px;
258
- padding: 1.5rem 2rem;
259
- margin-bottom: 1rem;
260
  }
261
 
262
  /* ── METRIC CARDS ── */
263
  div[data-testid="stMetric"] {
264
  background: #FFFFFF;
265
- border: 1px solid #E2E8F0;
266
- border-radius: 16px;
267
- padding: 1.2rem 1.5rem !important;
268
- box-shadow: 0 1px 4px rgba(0,0,0,0.04);
269
  }
270
  div[data-testid="stMetricLabel"] > div {
271
- color: #64748B !important;
272
- font-size: 0.8rem !important;
273
- font-weight: 600 !important;
274
- letter-spacing: 0.5px;
275
- text-transform: uppercase;
276
  }
277
  div[data-testid="stMetricValue"] > div {
278
- color: #1A1033 !important;
279
- font-family: 'Manrope', sans-serif !important;
280
- font-weight: 800 !important;
281
- font-size: 2rem !important;
282
  }
283
 
284
- /* ── BUTTONS — VBC style pill buttons ── */
285
  div[data-testid="stButton"] > button {
286
- font-family: 'Manrope', sans-serif !important;
287
- font-weight: 700 !important;
288
- font-size: 0.92rem !important;
289
- border-radius: 50px !important;
290
- padding: 0.6rem 1.8rem !important;
291
- border: none !important;
292
- letter-spacing: 0.2px;
293
- transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1) !important;
294
- box-shadow: 0 2px 10px rgba(0,0,0,0.08) !important;
295
  }
296
  div[data-testid="stButton"] > button:focus:not(:active) {
297
  box-shadow: none !important;
298
  }
299
 
300
- /* Primary CTA — dark navy */
301
  .btn-primary div[data-testid="stButton"] > button {
302
- background: #1A1033 !important;
303
  color: #FFFFFF !important;
 
304
  }
305
  .btn-primary div[data-testid="stButton"] > button:hover {
306
- background: #2D1B69 !important;
307
- transform: translateY(-2px) !important;
308
- box-shadow: 0 6px 20px rgba(26,16,51,0.25) !important;
309
  }
310
 
311
- /* Secondary outline — white border on dark bg */
312
  .btn-outline-white div[data-testid="stButton"] > button {
313
- background: transparent !important;
314
- color: #FFFFFF !important;
315
- border: 2px solid rgba(255,255,255,0.6) !important;
316
  }
317
  .btn-outline-white div[data-testid="stButton"] > button:hover {
318
- background: rgba(255,255,255,0.12) !important;
319
- border-color: #FFFFFF !important;
320
- transform: translateY(-2px) !important;
321
  }
322
 
323
- /* Orange pill — active nav */
324
  .btn-orange div[data-testid="stButton"] > button {
325
- background: #F7931A !important;
326
- color: #FFFFFF !important;
327
- box-shadow: 0 4px 16px rgba(247,147,26,0.35) !important;
328
- }
329
- .btn-orange div[data-testid="stButton"] > button:hover {
330
- background: #E07D08 !important;
331
- transform: translateY(-2px) !important;
332
  }
333
 
334
  /* Ghost nav — inactive */
335
  .btn-ghost div[data-testid="stButton"] > button {
336
  background: transparent !important;
337
- color: rgba(255,255,255,0.7) !important;
338
- border: 1px solid rgba(255,255,255,0.2) !important;
339
- box-shadow: none !important;
340
  }
341
  .btn-ghost div[data-testid="stButton"] > button:hover {
342
- color: #FFFFFF !important;
343
- border-color: rgba(255,255,255,0.5) !important;
344
- background: rgba(255,255,255,0.06) !important;
345
  }
346
 
347
  /* ── TEXT INPUT / TEXTAREA ── */
348
  .stTextArea textarea {
349
- background-color: rgba(255,255,255,0.12) !important;
350
- color: #FFFFFF !important;
351
- border: 1.5px solid rgba(255,255,255,0.3) !important;
352
- border-radius: 14px !important;
353
- font-family: 'DM Sans', sans-serif !important;
354
- font-size: 0.98rem !important;
355
- padding: 1rem 1.2rem !important;
356
- backdrop-filter: blur(4px);
357
  transition: border-color 0.2s !important;
358
  }
359
  .stTextArea textarea:focus {
360
- border-color: #1A1033 !important;
361
- box-shadow: 0 0 0 3px rgba(26,16,51,0.2) !important;
362
  }
363
  .stTextArea label {
364
- color: rgba(255,255,255,0.75) !important;
365
- font-size: 0.82rem !important;
366
- font-weight: 600 !important;
367
- letter-spacing: 0.5px !important;
368
- text-transform: uppercase !important;
369
- }
370
-
371
- /* Batch page file uploader */
372
- .batch-wrap .stTextArea textarea {
373
- background-color: #FFFFFF !important;
374
- color: #1A1033 !important;
375
- border: 1.5px solid #E2E8F0 !important;
376
- }
377
- .batch-wrap .stTextArea label {
378
- color: #64748B !important;
379
  }
380
 
381
  /* ── DATA TABLE ── */
382
  div[data-testid="stDataFrame"] {
383
- border: 1px solid #E2E8F0 !important;
384
- border-radius: 16px !important;
385
- overflow: hidden !important;
386
- box-shadow: 0 1px 6px rgba(0,0,0,0.04) !important;
387
  }
388
 
389
  /* ── FILE UPLOADER ── */
390
  div[data-testid="stFileUploader"] {
391
- border: 2px dashed #F7931A !important;
392
- border-radius: 16px !important;
393
- background: rgba(247,147,26,0.04) !important;
394
- padding: 1rem !important;
 
 
 
395
  }
396
 
397
  /* ── EXPANDER ── */
398
  div[data-testid="stExpander"] {
399
- border: 1px solid #E2E8F0 !important;
400
- border-radius: 12px !important;
401
- overflow: hidden !important;
402
  }
403
 
404
- /* ── ALERT / INFO ── */
405
- div[data-testid="stAlert"] {
406
- border-radius: 12px !important;
407
- }
408
-
409
- /* ── SPINNER ── */
410
- div[data-testid="stSpinner"] { color: #F7931A !important; }
411
-
412
  /* ── DOWNLOAD BUTTON ── */
413
  div[data-testid="stDownloadButton"] > button {
414
- background: #1A1033 !important;
415
- color: #FFFFFF !important;
416
- border-radius: 50px !important;
417
- font-family: 'Manrope', sans-serif !important;
418
- font-weight: 700 !important;
419
- border: none !important;
420
- padding: 0.6rem 1.4rem !important;
421
  transition: all 0.2s !important;
422
  }
423
  div[data-testid="stDownloadButton"] > button:hover {
424
- background: #F7931A !important;
425
- transform: translateY(-2px) !important;
426
  }
427
 
428
  /* ── DIVIDER ── */
429
  .vbc-divider {
430
  border: none;
431
- border-top: 1px solid #E2E8F0;
432
- margin: 3rem 0;
433
- }
434
- .vbc-divider-dark {
435
- border: none;
436
- border-top: 1px solid rgba(255,255,255,0.12);
437
- margin: 2.5rem 0;
438
  }
439
 
440
- /* ── RESULT TABLE (Uji Kalimat) ── */
441
- .result-section {
442
- background: rgba(255,255,255,0.07);
443
- backdrop-filter: blur(8px);
444
- border: 1px solid rgba(255,255,255,0.18);
445
- border-radius: 20px;
446
- padding: 2rem 2.5rem;
447
- margin-top: 2rem;
448
- }
449
- .result-title {
450
- font-family: 'Manrope', sans-serif;
451
- font-size: 1.15rem;
452
- font-weight: 700;
453
- color: #FFFFFF !important;
454
- margin-bottom: 1rem;
455
- display: flex;
456
- align-items: center;
457
- gap: 8px;
458
- }
459
  </style>
460
  """, unsafe_allow_html=True)
461
 
462
 
463
  # ==============================
464
- # HEADER / NAVBAR — Vancouver Bitcoin style
465
  # ==============================
466
  def set_page(page_name):
467
  st.session_state.page = page_name
@@ -475,7 +369,6 @@ st.markdown("""
475
  </div>
476
  """, unsafe_allow_html=True)
477
 
478
- # Nav buttons sit just below navbar strip using columns
479
  nav_col_pad, nav_col1, nav_col2, nav_col_end = st.columns([6, 1, 1, 1])
480
 
481
  with nav_col1:
@@ -494,22 +387,6 @@ with nav_col2:
494
  set_page("analisis_batch"); st.rerun()
495
  st.markdown('</div>', unsafe_allow_html=True)
496
 
497
- # ── Navbar overlay CSS (applies color context per page) ──
498
- if st.session_state.page == "uji_kalimat":
499
- st.markdown("""
500
- <style>
501
- /* Nav ghost text readable on orange bg */
502
- .btn-ghost div[data-testid="stButton"] > button { color: rgba(255,255,255,0.6) !important; }
503
- /* Override stApp bg to orange */
504
- .stApp { background-color: #F7931A !important; }
505
- </style>""", unsafe_allow_html=True)
506
- else:
507
- st.markdown("""
508
- <style>
509
- .btn-ghost div[data-testid="stButton"] > button { color: rgba(255,255,255,0.6) !important; }
510
- .stApp { background-color: #FEF8F0 !important; }
511
- </style>""", unsafe_allow_html=True)
512
-
513
 
514
  # ==============================
515
  # DOWNLOAD RESOURCES & LOAD MODELS
@@ -532,7 +409,7 @@ def load_all_models():
532
  roberta_large = pipeline("sentiment-analysis", model="siebert/sentiment-roberta-large-english", device=-1, truncation=True, max_length=512)
533
  return vader, bertweet, roberta, roberta_large
534
 
535
- with st.spinner('Mempersiapkan model NLP...'):
536
  vader, bertweet, roberta, roberta_large = load_all_models()
537
 
538
 
@@ -571,14 +448,13 @@ def get_daily_label(score):
571
  # ==============================================================================
572
  if st.session_state.page == "uji_kalimat":
573
 
574
- # ── Hero section ──
575
  st.markdown('<div class="hero-wrap">', unsafe_allow_html=True)
576
 
577
  col_text, col_img = st.columns([1.1, 1], gap="large")
578
 
579
  with col_text:
580
  st.markdown("""
581
- <div class="hero-badge">🔬 Penelitian Ilmiah · NLP · Python</div>
582
  <h1 class="hero-title">
583
  Bitcoin Volatility<br>
584
  <span>vs Public</span> Sentiment
@@ -604,11 +480,11 @@ if st.session_state.page == "uji_kalimat":
604
  col_btn1, col_btn2 = st.columns([1.6, 1])
605
  with col_btn1:
606
  st.markdown('<div class="btn-primary">', unsafe_allow_html=True)
607
- analyze_btn = st.button("🚀 Analisis Sentimen Sekarang", use_container_width=True)
608
  st.markdown('</div>', unsafe_allow_html=True)
609
  with col_btn2:
610
  st.markdown('<div class="btn-outline-white">', unsafe_allow_html=True)
611
- if st.button("📊 Analisis Batch →", use_container_width=True):
612
  set_page("analisis_batch"); st.rerun()
613
  st.markdown('</div>', unsafe_allow_html=True)
614
 
@@ -616,23 +492,22 @@ if st.session_state.page == "uji_kalimat":
616
  try:
617
  st.image(img_hero, use_container_width=True)
618
  except Exception:
619
- # Placeholder visual bila gambar belum ada
620
  st.markdown("""
621
- <div style="background:rgba(255,255,255,0.1);border:2px dashed rgba(255,255,255,0.3);
622
- border-radius:20px;height:320px;display:flex;align-items:center;
623
- justify-content:center;color:rgba(255,255,255,0.5);font-size:1rem;
624
  text-align:center;padding:2rem;">
625
  🖼️ Tambahkan<br><code>crypto-currency-concept-830px.png</code><br>ke folder project
626
  </div>""", unsafe_allow_html=True)
627
 
628
- st.markdown('</div>', unsafe_allow_html=True) # close hero-wrap
629
 
630
- # ── Result section ──
631
  if analyze_btn:
632
  st.markdown("""
633
  <div class="result-wrap">
634
  <p class="section-label">Output Analisis</p>
635
- <p class="section-title">📋 Hasil Deteksi Sentimen</p>
636
  </div>""", unsafe_allow_html=True)
637
 
638
  try:
@@ -659,16 +534,11 @@ if st.session_state.page == "uji_kalimat":
659
  try: rl_label = roberta_large(text)[0]['label'].lower()
660
  except: rl_label = "neutral"
661
 
662
- def format_label(label):
663
- if label == 'positive': return "🟢 Positive"
664
- elif label == 'negative': return "🔴 Negative"
665
- return "⚪ Neutral"
666
-
667
  def badge_color(label):
668
- return {"positive": "#D1FAE5", "negative": "#FEE2E2", "neutral": "#F1F5F9"}[label]
669
 
670
  def badge_text_color(label):
671
- return {"positive": "#065F46", "negative": "#991B1B", "neutral": "#475569"}[label]
672
 
673
  results = [
674
  ("VADER", v_label),
@@ -678,28 +548,27 @@ if st.session_state.page == "uji_kalimat":
678
  ("RoBERTa Large", rl_label),
679
  ]
680
 
681
- # Show as styled cards in 2 columns (2+2+1)
682
- st.markdown("<div style='padding: 0 3rem 3rem;'>", unsafe_allow_html=True)
683
  col_a, col_b = st.columns(2)
684
 
685
  for i, (method, label) in enumerate(results):
686
  col = col_a if i % 2 == 0 else col_b
687
  bg = badge_color(label)
688
  tc = badge_text_color(label)
689
- icon = "🟢" if label == "positive" else ("🔴" if label == "negative" else "")
690
  with col:
691
  st.markdown(f"""
692
- <div style="background:#F8FAFC;border:1px solid #E2E8F0;border-left:4px solid {'#10B981' if label=='positive' else ('#EF4444' if label=='negative' else '#94A3B8')};
693
- border-radius:14px;padding:1.1rem 1.4rem;margin-bottom:1rem;
694
- display:flex;align-items:center;justify-content:space-between;">
 
695
  <div>
696
- <div style="font-family:'Manrope',sans-serif;font-weight:700;font-size:0.78rem;
697
- letter-spacing:1px;text-transform:uppercase;color:#94A3B8;margin-bottom:4px;">{method}</div>
698
- <div style="font-family:'Manrope',sans-serif;font-weight:800;font-size:1.1rem;color:#1A1033;">{icon} {label.capitalize()}</div>
699
  </div>
700
- <div style="background:{bg};color:{tc};font-size:0.75rem;font-weight:700;
701
- padding:4px 12px;border-radius:50px;letter-spacing:0.5px;">
702
- {label.upper()}
703
  </div>
704
  </div>
705
  """, unsafe_allow_html=True)
@@ -714,23 +583,22 @@ elif st.session_state.page == "analisis_batch":
714
 
715
  plt.style.use('default')
716
  sns.set_theme(style="whitegrid", rc={
717
- "axes.facecolor": "#F8FAFC",
718
- "figure.facecolor": "#FFFFFF",
719
- "axes.edgecolor": "#E2E8F0",
720
- "text.color": "#0F172A",
721
- "xtick.color": "#64748B",
722
- "ytick.color": "#64748B",
723
- "grid.color": "#F1F5F9",
724
  })
725
 
726
- # ── Batch hero section ──
727
  st.markdown('<div class="batch-wrap">', unsafe_allow_html=True)
728
 
729
  col_upload, col_img_b = st.columns([1.4, 1], gap="large")
730
 
731
  with col_upload:
732
  st.markdown("""
733
- <p class="batch-eyebrow">⚙️ Analisis Masal · Batch Processing</p>
734
  <h2 class="batch-title">Analisis Batch<br>Data Tweet</h2>
735
  <p class="batch-sub">
736
  Unggah file rekam jejak tweet (.txt) untuk diekstraksi dan
@@ -743,7 +611,7 @@ elif st.session_state.page == "analisis_batch":
743
  accept_multiple_files=True
744
  )
745
 
746
- with st.expander("📌 Format TXT yang Didukung"):
747
  st.code(
748
  "username | 2024-03-01 14:00:00\n"
749
  "Isi tweet baris pertama di sini\n\n"
@@ -754,7 +622,7 @@ elif st.session_state.page == "analisis_batch":
754
 
755
  st.markdown("<br>", unsafe_allow_html=True)
756
  st.markdown('<div class="btn-primary">', unsafe_allow_html=True)
757
- analyze_batch_btn = st.button("⚙️ Mulai Eksekusi Analisis", key="batch_btn", use_container_width=False)
758
  st.markdown('</div>', unsafe_allow_html=True)
759
 
760
  with col_img_b:
@@ -762,20 +630,20 @@ elif st.session_state.page == "analisis_batch":
762
  st.image(img_batch, use_container_width=True)
763
  except Exception:
764
  st.markdown("""
765
- <div style="background:rgba(247,147,26,0.08);border:2px dashed #F7931A;
766
- border-radius:20px;height:280px;display:flex;align-items:center;
767
- justify-content:center;color:#F7931A;font-size:0.9rem;text-align:center;padding:2rem;">
 
768
  🖼️ Tambahkan<br><code>slice3-1-1536x830.png</code><br>ke folder project
769
  </div>""", unsafe_allow_html=True)
770
 
771
  st.markdown('</div>', unsafe_allow_html=True) # close batch-wrap
772
 
773
- # ── Processing & results ──
774
  if tweet_files and analyze_batch_btn:
775
  st.markdown('<div class="result-wrap">', unsafe_allow_html=True)
776
  st.markdown("""
777
  <p class="section-label">Hasil Pemrosesan</p>
778
- <p class="section-title">📊 Dashboard Analisis</p>
779
  """, unsafe_allow_html=True)
780
 
781
  tweet_files = sorted(tweet_files, key=lambda x: x.name)
@@ -941,7 +809,7 @@ elif st.session_state.page == "analisis_batch":
941
  # Line chart
942
  st.subheader("📈 Trend Analisis: Sentiment vs BTC Volatility")
943
  fig_line, ax_line = plt.subplots(figsize=(14, 6))
944
- ax_line.plot(df_merged["date"], df_merged["log_return"], label="BTC Log Return", color="#F7931A", linewidth=3)
945
  colors = ["#3B82F6","#10B981","#EC4899","#14B8A6","#6366F1"]
946
  for i, method in enumerate(["vader","textblob","roberta","roberta_large","bertweet"]):
947
  ax_line.plot(df_merged["date"], df_merged[method], label=f"Sentiment: {method.upper()}", color=colors[i], linewidth=1.5, linestyle="--", alpha=0.8)
@@ -959,8 +827,8 @@ elif st.session_state.page == "analisis_batch":
959
  with cols[idx2 % 3]:
960
  fig_s, ax_s = plt.subplots(figsize=(5, 4))
961
  sns.regplot(data=df_merged, x=method, y="log_return", ax=ax_s,
962
- scatter_kws={"s": 40, "color": "#1A1033", "alpha": 0.5},
963
- line_kws={"color": "#F7931A", "linewidth": 2})
964
  ax_s.set_title(f"{method.upper()}", fontweight='bold')
965
  ax_s.set_xlabel("Sentimen Score")
966
  ax_s.set_ylabel("Log Return")
@@ -999,4 +867,4 @@ elif st.session_state.page == "analisis_batch":
999
  st.markdown('</div>', unsafe_allow_html=True) # close result-wrap
1000
 
1001
  elif analyze_batch_btn and not tweet_files:
1002
- st.warning("⚠️ Silakan unggah minimal satu file .txt terlebih dahulu.")
 
5
  import io
6
  import time
7
  import requests
8
+ import matplotlib.subplots as plt
9
  import matplotlib.pyplot as plt
10
  import seaborn as sns
11
  from datetime import datetime, timezone
 
21
  DetectorFactory.seed = 0
22
 
23
  # ==============================
24
+ # SETTING PATH ABSOLUT GAMBAR
25
  # ==============================
26
  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
27
 
 
42
  st.session_state.page = "uji_kalimat"
43
 
44
  # ==============================
45
+ # GLOBAL CSS
46
  # ==============================
47
  st.markdown("""
48
  <style>
49
  /* ── Google Fonts ── */
50
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
51
 
52
  /* ── Reset Streamlit chrome ── */
53
  #MainMenu, footer, header { visibility: hidden; }
 
58
  }
59
 
60
  html, body, [class*="css"] {
61
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important;
62
+ color: #202630 !important;
63
+ }
64
+
65
+ .stApp {
66
+ background-color: #FAFAFA !important;
67
  }
68
 
69
  /* ── Custom Scrollbar ── */
70
  ::-webkit-scrollbar { width: 6px; }
71
+ ::-webkit-scrollbar-track { background: transparent; }
72
+ ::-webkit-scrollbar-thumb { background: #eaecef; border-radius: 3px; }
73
+ ::-webkit-scrollbar-thumb:hover { background: #d8dce1; }
74
 
75
  /* ── NAVBAR WRAPPER ── */
76
  .vbc-navbar {
 
78
  align-items: center;
79
  justify-content: space-between;
80
  padding: 0 3rem;
81
+ height: 64px;
82
+ background: #FFFFFF;
83
  position: sticky;
84
  top: 0;
85
  z-index: 999;
86
+ border-bottom: 1px solid #eaecef;
87
  }
88
  .vbc-logo {
89
+ font-weight: 700;
90
+ font-size: 1.15rem;
91
+ color: #202630 !important;
 
92
  display: flex;
93
  align-items: center;
94
+ gap: 8px;
 
95
  }
96
  .vbc-logo-icon {
97
+ background: #1aa64a;
98
  color: white;
99
+ width: 32px;
100
+ height: 32px;
101
  border-radius: 50%;
102
  display: inline-flex;
103
  align-items: center;
104
  justify-content: center;
105
+ font-size: 1rem;
106
+ font-weight: 700;
107
  }
108
 
109
  /* ── HERO SECTION (Uji Kalimat) ── */
110
  .hero-wrap {
111
+ background: #FAFAFA;
112
+ min-height: calc(100vh - 64px);
113
  display: flex;
114
  align-items: center;
115
  position: relative;
116
  overflow: hidden;
117
  padding: 4rem 3rem;
118
  }
119
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  .hero-badge {
121
  display: inline-block;
122
+ background: #e6fff1;
123
+ color: #1aa64a;
124
+ font-size: 0.75rem;
125
+ font-weight: 600;
126
+ padding: 6px 12px;
127
+ border-radius: 4px;
128
+ margin-bottom: 1.5rem;
 
 
 
129
  }
130
  .hero-title {
131
+ font-size: 3rem;
132
+ font-weight: 700;
133
+ line-height: 1.2;
134
+ color: #202630 !important;
135
+ margin: 0 0 1rem;
 
 
136
  }
137
  .hero-title span {
138
+ color: #1aa64a;
139
  }
140
  .hero-sub {
141
+ font-size: 1rem;
142
+ color: #707a8a !important;
143
  max-width: 500px;
144
+ line-height: 1.6;
145
  margin-bottom: 2rem;
146
  }
147
  .hero-card {
148
+ background: #FFFFFF;
149
+ border: 1px solid #eaecef;
150
+ border-radius: 8px;
151
+ padding: 12px 16px;
 
152
  display: inline-flex;
153
  align-items: center;
154
  gap: 10px;
155
+ margin-bottom: 2rem;
156
+ box-shadow: 0 2px 4px rgba(0,0,0,0.02);
157
  }
158
  .hero-card-dot {
159
  width: 8px; height: 8px;
160
  border-radius: 50%;
161
+ background: #1aa64a;
162
  flex-shrink: 0;
163
  }
164
  .hero-card p {
165
  margin: 0;
166
+ font-size: 0.85rem;
167
+ color: #474d57 !important;
168
  font-weight: 500;
169
  }
170
 
171
  /* ── BATCH SECTION ── */
172
  .batch-wrap {
173
+ background: #FAFAFA;
174
+ min-height: calc(100vh - 64px);
175
+ padding: 4rem 3rem;
 
 
 
 
 
 
 
 
 
176
  }
177
  .batch-eyebrow {
178
+ font-size: 0.85rem;
179
+ font-weight: 600;
180
+ color: #1aa64a !important;
181
+ margin-bottom: 0.5rem;
 
 
 
182
  }
183
  .batch-title {
184
+ font-size: 2.5rem;
185
+ font-weight: 700;
186
+ color: #202630 !important;
187
+ line-height: 1.2;
 
 
188
  margin-bottom: 1rem;
189
  }
190
  .batch-sub {
191
+ font-size: 1rem;
192
+ color: #707a8a !important;
193
  max-width: 480px;
194
+ line-height: 1.6;
195
  margin-bottom: 2rem;
196
  }
197
 
198
  /* ── RESULT / DASHBOARD SECTION ── */
199
  .result-wrap {
200
  background: #FFFFFF;
201
+ padding: 3rem 3rem;
202
+ border-top: 1px solid #eaecef;
203
  }
204
  .section-label {
205
+ font-size: 0.85rem;
206
+ font-weight: 600;
207
+ color: #1aa64a !important;
 
 
 
208
  margin-bottom: 0.5rem;
209
  }
210
  .section-title {
211
+ font-size: 1.5rem;
212
+ font-weight: 700;
213
+ color: #202630 !important;
214
+ margin-bottom: 1.5rem;
 
 
 
 
 
 
 
 
 
 
 
215
  }
216
 
217
  /* ── METRIC CARDS ── */
218
  div[data-testid="stMetric"] {
219
  background: #FFFFFF;
220
+ border: 1px solid #eaecef;
221
+ border-radius: 8px;
222
+ padding: 1rem 1.2rem !important;
223
+ box-shadow: 0 2px 4px rgba(0,0,0,0.02);
224
  }
225
  div[data-testid="stMetricLabel"] > div {
226
+ color: #707a8a !important;
227
+ font-size: 0.85rem !important;
228
+ font-weight: 500 !important;
 
 
229
  }
230
  div[data-testid="stMetricValue"] > div {
231
+ color: #202630 !important;
232
+ font-weight: 700 !important;
233
+ font-size: 1.8rem !important;
 
234
  }
235
 
236
+ /* ── BUTTONS ── */
237
  div[data-testid="stButton"] > button {
238
+ font-weight: 600 !important;
239
+ font-size: 0.9rem !important;
240
+ border-radius: 4px !important;
241
+ padding: 0.5rem 1.2rem !important;
242
+ transition: all 0.2s ease-in-out !important;
 
 
 
 
243
  }
244
  div[data-testid="stButton"] > button:focus:not(:active) {
245
  box-shadow: none !important;
246
  }
247
 
248
+ /* Primary CTA */
249
  .btn-primary div[data-testid="stButton"] > button {
250
+ background: #1aa64a !important;
251
  color: #FFFFFF !important;
252
+ border: none !important;
253
  }
254
  .btn-primary div[data-testid="stButton"] > button:hover {
255
+ background: #1fb653 !important;
 
 
256
  }
257
 
258
+ /* Secondary outline */
259
  .btn-outline-white div[data-testid="stButton"] > button {
260
+ background: #FFFFFF !important;
261
+ color: #202630 !important;
262
+ border: 1px solid #eaecef !important;
263
  }
264
  .btn-outline-white div[data-testid="stButton"] > button:hover {
265
+ border-color: #1aa64a !important;
266
+ color: #1aa64a !important;
 
267
  }
268
 
269
+ /* Active nav */
270
  .btn-orange div[data-testid="stButton"] > button {
271
+ background: #e6fff1 !important;
272
+ color: #1aa64a !important;
273
+ border: none !important;
 
 
 
 
274
  }
275
 
276
  /* Ghost nav — inactive */
277
  .btn-ghost div[data-testid="stButton"] > button {
278
  background: transparent !important;
279
+ color: #707a8a !important;
280
+ border: none !important;
 
281
  }
282
  .btn-ghost div[data-testid="stButton"] > button:hover {
283
+ color: #202630 !important;
284
+ background: #f5f5f5 !important;
 
285
  }
286
 
287
  /* ── TEXT INPUT / TEXTAREA ── */
288
  .stTextArea textarea {
289
+ background-color: #FFFFFF !important;
290
+ color: #202630 !important;
291
+ border: 1px solid #eaecef !important;
292
+ border-radius: 4px !important;
293
+ font-size: 0.95rem !important;
294
+ padding: 0.8rem 1rem !important;
 
 
295
  transition: border-color 0.2s !important;
296
  }
297
  .stTextArea textarea:focus {
298
+ border-color: #1aa64a !important;
299
+ box-shadow: 0 0 0 1px #1aa64a !important;
300
  }
301
  .stTextArea label {
302
+ color: #474d57 !important;
303
+ font-size: 0.85rem !important;
304
+ font-weight: 500 !important;
 
 
 
 
 
 
 
 
 
 
 
 
305
  }
306
 
307
  /* ── DATA TABLE ── */
308
  div[data-testid="stDataFrame"] {
309
+ border: 1px solid #eaecef !important;
310
+ border-radius: 8px !important;
 
 
311
  }
312
 
313
  /* ── FILE UPLOADER ── */
314
  div[data-testid="stFileUploader"] {
315
+ border: 1px dashed #d8dce1 !important;
316
+ border-radius: 8px !important;
317
+ background: #FFFFFF !important;
318
+ padding: 1.5rem !important;
319
+ }
320
+ div[data-testid="stFileUploader"]:hover {
321
+ border-color: #1aa64a !important;
322
  }
323
 
324
  /* ── EXPANDER ── */
325
  div[data-testid="stExpander"] {
326
+ border: 1px solid #eaecef !important;
327
+ border-radius: 8px !important;
328
+ background: #FFFFFF !important;
329
  }
330
 
 
 
 
 
 
 
 
 
331
  /* ── DOWNLOAD BUTTON ── */
332
  div[data-testid="stDownloadButton"] > button {
333
+ background: #FFFFFF !important;
334
+ color: #202630 !important;
335
+ border-radius: 4px !important;
336
+ font-weight: 600 !important;
337
+ border: 1px solid #eaecef !important;
338
+ padding: 0.5rem 1rem !important;
 
339
  transition: all 0.2s !important;
340
  }
341
  div[data-testid="stDownloadButton"] > button:hover {
342
+ border-color: #1aa64a !important;
343
+ color: #1aa64a !important;
344
  }
345
 
346
  /* ── DIVIDER ── */
347
  .vbc-divider {
348
  border: none;
349
+ border-top: 1px solid #eaecef;
350
+ margin: 2rem 0;
 
 
 
 
 
351
  }
352
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353
  </style>
354
  """, unsafe_allow_html=True)
355
 
356
 
357
  # ==============================
358
+ # HEADER / NAVBAR
359
  # ==============================
360
  def set_page(page_name):
361
  st.session_state.page = page_name
 
369
  </div>
370
  """, unsafe_allow_html=True)
371
 
 
372
  nav_col_pad, nav_col1, nav_col2, nav_col_end = st.columns([6, 1, 1, 1])
373
 
374
  with nav_col1:
 
387
  set_page("analisis_batch"); st.rerun()
388
  st.markdown('</div>', unsafe_allow_html=True)
389
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
 
391
  # ==============================
392
  # DOWNLOAD RESOURCES & LOAD MODELS
 
409
  roberta_large = pipeline("sentiment-analysis", model="siebert/sentiment-roberta-large-english", device=-1, truncation=True, max_length=512)
410
  return vader, bertweet, roberta, roberta_large
411
 
412
+ with st.spinner('...'):
413
  vader, bertweet, roberta, roberta_large = load_all_models()
414
 
415
 
 
448
  # ==============================================================================
449
  if st.session_state.page == "uji_kalimat":
450
 
 
451
  st.markdown('<div class="hero-wrap">', unsafe_allow_html=True)
452
 
453
  col_text, col_img = st.columns([1.1, 1], gap="large")
454
 
455
  with col_text:
456
  st.markdown("""
457
+ <div class="hero-badge">Penelitian Ilmiah · NLP · Python</div>
458
  <h1 class="hero-title">
459
  Bitcoin Volatility<br>
460
  <span>vs Public</span> Sentiment
 
480
  col_btn1, col_btn2 = st.columns([1.6, 1])
481
  with col_btn1:
482
  st.markdown('<div class="btn-primary">', unsafe_allow_html=True)
483
+ analyze_btn = st.button("Mulai Analisis Sentimen", use_container_width=True)
484
  st.markdown('</div>', unsafe_allow_html=True)
485
  with col_btn2:
486
  st.markdown('<div class="btn-outline-white">', unsafe_allow_html=True)
487
+ if st.button("Analisis Batch →", use_container_width=True):
488
  set_page("analisis_batch"); st.rerun()
489
  st.markdown('</div>', unsafe_allow_html=True)
490
 
 
492
  try:
493
  st.image(img_hero, use_container_width=True)
494
  except Exception:
495
+
496
  st.markdown("""
497
+ <div style="background:#f5f5f5;border:1px dashed #d8dce1;
498
+ border-radius:8px;height:320px;display:flex;align-items:center;
499
+ justify-content:center;color:#707a8a;font-size:0.9rem;
500
  text-align:center;padding:2rem;">
501
  🖼️ Tambahkan<br><code>crypto-currency-concept-830px.png</code><br>ke folder project
502
  </div>""", unsafe_allow_html=True)
503
 
504
+ st.markdown('</div>', unsafe_allow_html=True)
505
 
 
506
  if analyze_btn:
507
  st.markdown("""
508
  <div class="result-wrap">
509
  <p class="section-label">Output Analisis</p>
510
+ <p class="section-title">Hasil Deteksi Sentimen</p>
511
  </div>""", unsafe_allow_html=True)
512
 
513
  try:
 
534
  try: rl_label = roberta_large(text)[0]['label'].lower()
535
  except: rl_label = "neutral"
536
 
 
 
 
 
 
537
  def badge_color(label):
538
+ return {"positive": "#e6fff1", "negative": "#fef1f2", "neutral": "#f5f5f5"}[label]
539
 
540
  def badge_text_color(label):
541
+ return {"positive": "#1aa64a", "negative": "#f84960", "neutral": "#707a8a"}[label]
542
 
543
  results = [
544
  ("VADER", v_label),
 
548
  ("RoBERTa Large", rl_label),
549
  ]
550
 
551
+ st.markdown("<div style='padding: 0 3rem 3rem; background: #FFFFFF;'>", unsafe_allow_html=True)
 
552
  col_a, col_b = st.columns(2)
553
 
554
  for i, (method, label) in enumerate(results):
555
  col = col_a if i % 2 == 0 else col_b
556
  bg = badge_color(label)
557
  tc = badge_text_color(label)
558
+ icon = "" if label == "positive" else ("" if label == "negative" else "")
559
  with col:
560
  st.markdown(f"""
561
+ <div style="background:#FFFFFF;border:1px solid #eaecef;border-left:4px solid {'#1aa64a' if label=='positive' else ('#f84960' if label=='negative' else '#d8dce1')};
562
+ border-radius:8px;padding:1rem 1.2rem;margin-bottom:1rem;
563
+ display:flex;align-items:center;justify-content:space-between;
564
+ box-shadow:0 2px 4px rgba(0,0,0,0.02);">
565
  <div>
566
+ <div style="font-weight:500;font-size:0.75rem;color:#707a8a;margin-bottom:4px;">{method}</div>
567
+ <div style="font-weight:700;font-size:1.05rem;color:#202630;">{label.capitalize()}</div>
 
568
  </div>
569
+ <div style="background:{bg};color:{tc};font-size:0.75rem;font-weight:600;
570
+ padding:4px 10px;border-radius:4px;">
571
+ {icon} {label.upper()}
572
  </div>
573
  </div>
574
  """, unsafe_allow_html=True)
 
583
 
584
  plt.style.use('default')
585
  sns.set_theme(style="whitegrid", rc={
586
+ "axes.facecolor": "#FFFFFF",
587
+ "figure.facecolor": "#FAFAFA",
588
+ "axes.edgecolor": "#eaecef",
589
+ "text.color": "#202630",
590
+ "xtick.color": "#707a8a",
591
+ "ytick.color": "#707a8a",
592
+ "grid.color": "#f5f5f5",
593
  })
594
 
 
595
  st.markdown('<div class="batch-wrap">', unsafe_allow_html=True)
596
 
597
  col_upload, col_img_b = st.columns([1.4, 1], gap="large")
598
 
599
  with col_upload:
600
  st.markdown("""
601
+ <p class="batch-eyebrow">Analisis Masal · Batch Processing</p>
602
  <h2 class="batch-title">Analisis Batch<br>Data Tweet</h2>
603
  <p class="batch-sub">
604
  Unggah file rekam jejak tweet (.txt) untuk diekstraksi dan
 
611
  accept_multiple_files=True
612
  )
613
 
614
+ with st.expander("Format TXT yang Didukung"):
615
  st.code(
616
  "username | 2024-03-01 14:00:00\n"
617
  "Isi tweet baris pertama di sini\n\n"
 
622
 
623
  st.markdown("<br>", unsafe_allow_html=True)
624
  st.markdown('<div class="btn-primary">', unsafe_allow_html=True)
625
+ analyze_batch_btn = st.button("Mulai Eksekusi Analisis", key="batch_btn", use_container_width=False)
626
  st.markdown('</div>', unsafe_allow_html=True)
627
 
628
  with col_img_b:
 
630
  st.image(img_batch, use_container_width=True)
631
  except Exception:
632
  st.markdown("""
633
+ <div style="background:#f5f5f5;border:1px dashed #d8dce1;
634
+ border-radius:8px;height:280px;display:flex;align-items:center;
635
+ justify-content:center;color:#707a8a;font-size:0.9rem;
636
+ text-align:center;padding:2rem;">
637
  🖼️ Tambahkan<br><code>slice3-1-1536x830.png</code><br>ke folder project
638
  </div>""", unsafe_allow_html=True)
639
 
640
  st.markdown('</div>', unsafe_allow_html=True) # close batch-wrap
641
 
 
642
  if tweet_files and analyze_batch_btn:
643
  st.markdown('<div class="result-wrap">', unsafe_allow_html=True)
644
  st.markdown("""
645
  <p class="section-label">Hasil Pemrosesan</p>
646
+ <p class="section-title">Dashboard Analisis</p>
647
  """, unsafe_allow_html=True)
648
 
649
  tweet_files = sorted(tweet_files, key=lambda x: x.name)
 
809
  # Line chart
810
  st.subheader("📈 Trend Analisis: Sentiment vs BTC Volatility")
811
  fig_line, ax_line = plt.subplots(figsize=(14, 6))
812
+ ax_line.plot(df_merged["date"], df_merged["log_return"], label="BTC Log Return", color="#f7931a", linewidth=3)
813
  colors = ["#3B82F6","#10B981","#EC4899","#14B8A6","#6366F1"]
814
  for i, method in enumerate(["vader","textblob","roberta","roberta_large","bertweet"]):
815
  ax_line.plot(df_merged["date"], df_merged[method], label=f"Sentiment: {method.upper()}", color=colors[i], linewidth=1.5, linestyle="--", alpha=0.8)
 
827
  with cols[idx2 % 3]:
828
  fig_s, ax_s = plt.subplots(figsize=(5, 4))
829
  sns.regplot(data=df_merged, x=method, y="log_return", ax=ax_s,
830
+ scatter_kws={"s": 40, "color": "#1aa64a", "alpha": 0.5},
831
+ line_kws={"color": "#202630", "linewidth": 2})
832
  ax_s.set_title(f"{method.upper()}", fontweight='bold')
833
  ax_s.set_xlabel("Sentimen Score")
834
  ax_s.set_ylabel("Log Return")
 
867
  st.markdown('</div>', unsafe_allow_html=True) # close result-wrap
868
 
869
  elif analyze_batch_btn and not tweet_files:
870
+ st.warning("⚠️ Silakan unggah minimal satu file .txt terlebih dahulu.")