Varriety commited on
Commit
90cf4e3
·
verified ·
1 Parent(s): 4f84da2

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +655 -680
src/streamlit_app.py CHANGED
@@ -20,9 +20,10 @@ 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
  img_hero = os.path.join(BASE_DIR, "crypto-currency-concept-830px.png")
27
  img_batch = os.path.join(BASE_DIR, "slice3-1-1536x830.png")
28
 
@@ -30,517 +31,485 @@ img_batch = os.path.join(BASE_DIR, "slice3-1-1536x830.png")
30
  # KONFIGURASI HALAMAN & STATE NAVIGASI
31
  # ==============================
32
  st.set_page_config(
33
- page_title="Bitcoin Sentimen – Arya Galuh",
34
  page_icon="₿",
35
  layout="wide",
36
- initial_sidebar_state="collapsed",
37
  )
38
 
39
- if "page" not in st.session_state:
40
  st.session_state.page = "uji_kalimat"
41
 
42
  # ==============================
43
- # GLOBAL BASE CSS (mirip vancouverbitcoin.com)
44
  # ==============================
45
  st.markdown("""
46
  <style>
47
- /* ── Google Fonts ─────────────────────────────── */
48
- @import url('https://fonts.googleapis.com/css2?family=Nunito+Sans:wght@400;600;700;800;900&family=Nunito:wght@400;600;700;800&display=swap');
49
-
50
- /* ── Reset Streamlit chrome ────────────────────── */
51
- #MainMenu, footer, header { visibility: hidden; }
52
- .block-container {
53
- padding: 0 !important;
54
- max-width: 100% !important;
55
- }
56
-
57
- /* ── Global font ───────────────────────────────── */
58
- html, body, [class*="css"] {
59
- font-family: 'Nunito Sans', sans-serif !important;
60
- }
61
-
62
- /* ── Remove default button styling ────────────── */
63
- div[data-testid="stButton"] > button {
64
- border-radius: 50px !important;
65
- font-weight: 700 !important;
66
- font-size: 0.95rem !important;
67
- padding: 0.55rem 1.6rem !important;
68
- transition: all 0.25s ease !important;
69
- cursor: pointer !important;
70
- border: 2px solid transparent !important;
71
- line-height: 1.4 !important;
72
- letter-spacing: 0.01em !important;
73
- }
74
- div[data-testid="stButton"] > button:focus:not(:active) {
75
- box-shadow: none !important;
76
- outline: none !important;
77
- }
78
-
79
- /* ── Streamlit text area ───────────────────────── */
80
- .stTextArea label {
81
- font-weight: 600 !important;
82
- font-size: 0.95rem !important;
83
- }
84
- </style>
85
- """, unsafe_allow_html=True)
86
 
87
- # ==============================
88
- # TEMA DINAMIS PER HALAMAN
89
- # ==============================
90
- is_dark = st.session_state.page == "uji_kalimat"
 
 
 
91
 
92
- if is_dark:
93
- st.markdown("""
94
- <style>
95
- /* ── Hero page (dark-orange) ─── */
96
- .stApp { background-color: #F7931A; }
97
 
98
- /* top nav strip */
99
- .vb-nav {
100
- background: #FFFFFF;
101
- padding: 0 3rem;
102
- height: 70px;
 
 
103
  display: flex;
104
  align-items: center;
105
  justify-content: space-between;
106
- box-shadow: 0 2px 8px rgba(0,0,0,0.08);
 
 
107
  position: sticky;
108
  top: 0;
109
  z-index: 999;
 
110
  }
111
- .vb-nav-logo {
112
- font-family: 'Nunito', sans-serif;
113
- font-weight: 900;
114
- font-size: 1.4rem;
115
- color: #1E1E3F;
116
  display: flex;
117
  align-items: center;
118
- gap: 8px;
119
- text-decoration: none;
120
  }
121
- .vb-nav-logo span.btc-icon {
122
- width: 38px; height: 38px;
123
  background: #F7931A;
 
 
 
124
  border-radius: 50%;
125
  display: inline-flex;
126
  align-items: center;
127
  justify-content: center;
128
- color: #fff;
129
- font-size: 1.2rem;
130
- font-weight: 900;
131
- }
132
- .vb-nav-links {
133
- display: flex;
134
- gap: 2.2rem;
135
- list-style: none;
136
- margin: 0; padding: 0;
137
- }
138
- .vb-nav-links a {
139
- color: #1E1E3F;
140
- font-weight: 600;
141
- font-size: 0.92rem;
142
- text-decoration: none;
143
- }
144
- .vb-nav-actions { display: flex; gap: 10px; align-items: center; }
145
- .vb-btn-outline {
146
- border: 2px solid #F7931A !important;
147
- color: #F7931A !important;
148
- background: transparent !important;
149
- border-radius: 50px !important;
150
- padding: 7px 22px !important;
151
- font-weight: 700 !important;
152
- font-size: 0.88rem !important;
153
- cursor: pointer !important;
154
- transition: all .2s;
155
- }
156
- .vb-btn-solid {
157
- background: #1E1E3F !important;
158
- color: #fff !important;
159
- border: 2px solid #1E1E3F !important;
160
- border-radius: 50px !important;
161
- padding: 7px 22px !important;
162
- font-weight: 700 !important;
163
- font-size: 0.88rem !important;
164
- cursor: pointer !important;
165
- transition: all .2s;
166
  }
167
 
168
- /* ── Hero section ── */
169
  .hero-wrap {
170
  background: #F7931A;
171
- padding: 5rem 4rem 4rem;
172
- display: grid;
173
- grid-template-columns: 1fr 1fr;
174
- gap: 3rem;
175
  align-items: center;
176
- min-height: 82vh;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
  }
178
  .hero-badge {
179
  display: inline-block;
180
- background: rgba(255,255,255,0.2);
181
- color: #fff;
 
182
  font-size: 0.78rem;
183
  font-weight: 700;
184
- letter-spacing: 0.12em;
185
  text-transform: uppercase;
 
186
  border-radius: 50px;
187
- padding: 5px 16px;
188
  margin-bottom: 1.2rem;
189
  }
190
- .hero-h1 {
191
- font-family: 'Nunito', sans-serif;
192
- font-size: clamp(2.4rem, 4vw, 3.6rem);
193
- font-weight: 900;
194
- color: #FFFFFF;
195
- line-height: 1.1;
196
  letter-spacing: -1.5px;
197
- margin: 0 0 1.1rem;
 
198
  }
199
- .hero-h1 em {
200
- font-style: normal;
201
- color: #1E1E3F;
202
  }
203
  .hero-sub {
204
  font-size: 1.05rem;
205
- color: rgba(255,255,255,0.88);
206
- line-height: 1.65;
207
- margin-bottom: 1.6rem;
208
- font-weight: 400;
209
- }
210
- .hero-researcher {
211
- background: rgba(255,255,255,0.18);
212
- border-left: 4px solid #fff;
213
- border-radius: 6px;
214
- padding: 12px 16px;
215
  margin-bottom: 2rem;
216
- font-size: 0.9rem;
217
- color: #fff;
218
- font-weight: 600;
219
  }
220
- .hero-img-wrap {
221
- display: flex;
 
 
 
 
 
222
  align-items: center;
223
- justify-content: center;
 
224
  }
225
- .hero-img-wrap img { max-width: 100%; border-radius: 12px; }
226
-
227
- /* Nav buttons inside streamlit */
228
- .nav-btn-active > div[data-testid="stButton"] > button {
229
- background: #F7931A !important;
230
- color: #fff !important;
231
- border-color: #F7931A !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
  }
233
- .nav-btn-inactive > div[data-testid="stButton"] > button {
234
- background: transparent !important;
235
- color: #1E1E3F !important;
236
- border-color: #CBD5E1 !important;
 
 
 
 
237
  }
238
- .nav-btn-inactive > div[data-testid="stButton"] > button:hover {
239
- border-color: #F7931A !important;
240
- color: #F7931A !important;
 
 
 
241
  }
242
 
243
- /* Analyse button */
244
- .analyze-btn > div[data-testid="stButton"] > button {
245
- background: #1E1E3F !important;
246
- color: #fff !important;
247
- border-color: #1E1E3F !important;
248
- font-size: 1.05rem !important;
249
- padding: 0.75rem 2rem !important;
250
- width: 100%;
251
  }
252
- .analyze-btn > div[data-testid="stButton"] > button:hover {
253
- background: #2d2d5b !important;
254
- transform: translateY(-2px);
255
- box-shadow: 0 8px 20px rgba(30,30,63,0.25) !important;
 
 
 
 
 
 
 
 
 
 
 
 
256
  }
257
 
258
- /* Result section */
259
- .result-section {
260
- background: #fff;
 
261
  border-radius: 16px;
262
- padding: 2rem;
263
- margin-top: 2.5rem;
264
- }
265
- .result-title {
266
- font-family: 'Nunito', sans-serif;
267
- font-size: 1.5rem;
268
- font-weight: 800;
269
- color: #1E1E3F;
270
- text-align: center;
271
- margin-bottom: 1.2rem;
272
  }
273
 
274
- /* TextArea */
275
- .stTextArea textarea {
276
- background: rgba(255,255,255,0.15) !important;
277
- color: #fff !important;
278
- border: 2px solid rgba(255,255,255,0.4) !important;
279
- border-radius: 12px !important;
280
- font-size: 1rem !important;
281
- padding: 1rem !important;
282
- backdrop-filter: blur(4px);
283
  }
284
- .stTextArea textarea::placeholder { color: rgba(255,255,255,0.6) !important; }
285
- .stTextArea textarea:focus {
286
- border-color: #fff !important;
287
- box-shadow: 0 0 0 2px rgba(255,255,255,0.3) !important;
 
 
288
  }
289
- .stTextArea label { color: #fff !important; font-weight: 700 !important; }
290
-
291
- /* Dataframe inside result */
292
- div[data-testid="stDataFrame"] {
293
- border: 1px solid #E2E8F0 !important;
294
- border-radius: 10px !important;
295
- overflow: hidden !important;
296
  }
297
 
298
- /* Spinner */
299
- .stSpinner { color: #1E1E3F !important; }
300
-
301
- /* Warning / info */
302
- div[data-testid="stAlert"] { border-radius: 10px !important; }
303
-
304
- /* Content padding wrapper */
305
- .page-content { padding: 0 4rem 4rem; }
306
-
307
- /* Divider */
308
- .vb-divider {
309
- border: none;
310
- border-top: 1px solid rgba(255,255,255,0.25);
311
- margin: 2.5rem 0;
312
  }
313
- </style>
314
- """, unsafe_allow_html=True)
315
-
316
- else:
317
- st.markdown("""
318
- <style>
319
- /* ── Batch / cream page ─── */
320
- .stApp { background-color: #FDF6EE; }
321
-
322
- .vb-nav {
323
- background: #FFFFFF;
324
- padding: 0 3rem;
325
- height: 70px;
326
- display: flex;
327
- align-items: center;
328
- justify-content: space-between;
329
- box-shadow: 0 2px 8px rgba(0,0,0,0.06);
330
- position: sticky;
331
- top: 0;
332
- z-index: 999;
333
  }
334
- .vb-nav-logo {
335
- font-family: 'Nunito', sans-serif;
336
- font-weight: 900;
337
- font-size: 1.4rem;
338
- color: #1E1E3F;
339
- display: flex;
340
- align-items: center;
341
- gap: 8px;
342
  }
343
- .vb-nav-logo span.btc-icon {
344
- width: 38px; height: 38px;
345
- background: #F7931A;
346
- border-radius: 50%;
347
- display: inline-flex;
348
- align-items: center;
349
- justify-content: center;
350
- color: #fff;
351
- font-size: 1.2rem;
352
- font-weight: 900;
353
  }
354
 
355
- /* Nav action buttons */
356
- .nav-btn-active > div[data-testid="stButton"] > button {
357
- background: #1E1E3F !important;
358
- color: #fff !important;
359
- border-color: #1E1E3F !important;
360
- }
361
- .nav-btn-inactive > div[data-testid="stButton"] > button {
362
  background: transparent !important;
363
- color: #1E1E3F !important;
364
- border-color: #CBD5E1 !important;
365
  }
366
- .nav-btn-inactive > div[data-testid="stButton"] > button:hover {
367
- border-color: #F7931A !important;
368
- color: #F7931A !important;
369
- }
370
- .nav-btn-inactive2 > div[data-testid="stButton"] > button {
371
- background: transparent !important;
372
- color: #F7931A !important;
373
- border-color: #F7931A !important;
374
  }
375
 
376
- /* Hero batch section */
377
- .hero-wrap {
378
- background: #FDF6EE;
379
- padding: 5rem 4rem 3rem;
380
- display: grid;
381
- grid-template-columns: 1fr 1fr;
382
- gap: 3rem;
383
- align-items: center;
384
  }
385
- .hero-badge {
386
- display: inline-block;
387
- color: #F7931A;
388
- font-size: 0.78rem;
389
- font-weight: 800;
390
- letter-spacing: 0.14em;
391
- text-transform: uppercase;
392
- margin-bottom: 0.8rem;
393
  }
394
- .hero-h1 {
395
- font-family: 'Nunito', sans-serif;
396
- font-size: clamp(2rem, 3.2vw, 2.9rem);
397
- font-weight: 900;
398
- color: #1E1E3F;
399
- line-height: 1.15;
400
- letter-spacing: -1px;
401
- margin: 0 0 1rem;
402
  }
403
- .hero-sub {
404
- font-size: 1.05rem;
405
- color: #5A6478;
406
- line-height: 1.65;
407
- margin-bottom: 2rem;
408
  }
409
 
410
- /* Main CTA */
411
- .analyze-btn > div[data-testid="stButton"] > button {
412
- background: #1E1E3F !important;
413
- color: #fff !important;
414
- border-color: #1E1E3F !important;
415
- font-size: 1rem !important;
416
- padding: 0.7rem 2rem !important;
417
- width: 100%;
 
 
 
418
  }
419
- .analyze-btn > div[data-testid="stButton"] > button:hover {
420
- background: #F7931A !important;
421
- border-color: #F7931A !important;
422
- transform: translateY(-2px);
423
- box-shadow: 0 8px 20px rgba(247,147,26,0.25) !important;
 
 
 
 
 
424
  }
425
 
426
- /* Download buttons */
427
- div[data-testid="stDownloadButton"] > button {
428
- background: #1E1E3F !important;
429
- color: #fff !important;
430
- border: 2px solid #1E1E3F !important;
431
- border-radius: 50px !important;
432
- font-weight: 700 !important;
433
- padding: 0.55rem 1.4rem !important;
434
- transition: all .2s !important;
435
  }
436
- div[data-testid="stDownloadButton"] > button:hover {
437
- background: #F7931A !important;
438
- border-color: #F7931A !important;
439
  }
440
 
441
- /* FileUploader */
442
- section[data-testid="stFileUploader"] {
443
- background: #fff !important;
 
 
 
 
 
 
 
444
  border: 2px dashed #F7931A !important;
445
- border-radius: 14px !important;
446
- padding: 1.2rem !important;
 
447
  }
448
 
449
- /* Dataframe */
450
- div[data-testid="stDataFrame"] {
451
- border: 1px solid #E8E0D4 !important;
452
  border-radius: 12px !important;
453
  overflow: hidden !important;
454
- background: #fff !important;
455
  }
456
 
457
- /* Info / warning / success cards */
458
  div[data-testid="stAlert"] {
459
  border-radius: 12px !important;
460
- background: #fff !important;
461
  }
462
 
463
- /* Section divider */
464
- .vb-divider {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
465
  border: none;
466
- border-top: 1px solid #E8E0D4;
467
  margin: 3rem 0;
468
  }
469
-
470
- /* Metric cards */
471
- div[data-testid="stMetric"] {
472
- background: #fff !important;
473
- border: 1px solid #E8E0D4 !important;
474
- border-radius: 14px !important;
475
- padding: 1.2rem 1.5rem !important;
476
  }
477
- div[data-testid="stMetricLabel"] { color: #5A6478 !important; font-weight: 600 !important; }
478
- div[data-testid="stMetricValue"] { color: #1E1E3F !important; font-weight: 900 !important; }
479
 
480
- /* Expander */
481
- details summary {
482
- font-weight: 700 !important;
483
- color: #1E1E3F !important;
 
 
 
 
484
  }
485
-
486
- /* Content padding wrapper */
487
- .page-content { padding: 0 4rem 4rem; }
488
-
489
- h1,h2,h3,h4,h5,h6 { color: #1E1E3F !important; }
490
- p, span, label { color: #5A6478 !important; }
491
-
492
- /* Progress bar */
493
- div[data-testid="stProgress"] > div > div {
494
- background: #F7931A !important;
495
  }
496
- </style>
497
- """, unsafe_allow_html=True)
 
498
 
499
  # ==============================
500
- # NAVBAR HTML (identical look to Vancouver Bitcoin)
501
  # ==============================
502
- nav_bg = "#FFFFFF"
503
- logo_text = "₿itcoin Sentimen"
504
- st.markdown(f"""
505
- <div class="vb-nav">
506
- <div class="vb-nav-logo">
507
- <span class="btc-icon">₿</span>
508
- {logo_text}
509
- </div>
510
- <ul style="display:flex;gap:2.5rem;list-style:none;margin:0;padding:0;">
511
- <li><a href="#" style="color:#1E1E3F;font-weight:600;font-size:0.93rem;text-decoration:none;">Uji Kalimat</a></li>
512
- <li><a href="#" style="color:#1E1E3F;font-weight:600;font-size:0.93rem;text-decoration:none;">Analisis Batch</a></li>
513
- <li><a href="#" style="color:#1E1E3F;font-weight:600;font-size:0.93rem;text-decoration:none;">Tentang</a></li>
514
- </ul>
515
- <div style="display:flex;gap:10px;align-items:center;">
516
- <button class="vb-btn-outline" onclick="void(0)">Sign Up</button>
517
- <button class="vb-btn-solid" onclick="void(0)">📞 Hubungi</button>
518
- </div>
519
  </div>
520
  """, unsafe_allow_html=True)
521
 
522
- # ── Streamlit nav tab buttons (below the HTML nav) ──────────────────────────
523
- tab_col1, tab_col2, spacer = st.columns([1.8, 1.8, 10])
524
 
525
- with tab_col1:
526
- cls1 = "nav-btn-active" if st.session_state.page == "uji_kalimat" else "nav-btn-inactive"
527
- st.markdown(f'<div class="{cls1}">', unsafe_allow_html=True)
 
 
 
528
  if st.button("📝 Uji Kalimat", use_container_width=True, key="nav_uji"):
529
- st.session_state.page = "uji_kalimat"
530
- st.rerun()
531
- st.markdown("</div>", unsafe_allow_html=True)
532
 
533
- with tab_col2:
534
- cls2 = "nav-btn-active" if st.session_state.page == "analisis_batch" else ("nav-btn-inactive2" if not is_dark else "nav-btn-inactive")
535
- st.markdown(f'<div class="{cls2}">', unsafe_allow_html=True)
 
536
  if st.button("📊 Analisis Batch", use_container_width=True, key="nav_batch"):
537
- st.session_state.page = "analisis_batch"
538
- st.rerun()
539
- st.markdown("</div>", unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
540
 
541
- # ── thin top-border under tab strip ──────────────────────────────────────────
542
- divider_color = "rgba(255,255,255,0.25)" if is_dark else "#E8E0D4"
543
- st.markdown(f"<hr style='border:none;border-top:1px solid {divider_color};margin:0 0 0 0;'>", unsafe_allow_html=True)
544
 
545
  # ==============================
546
  # DOWNLOAD RESOURCES & LOAD MODELS
@@ -558,24 +527,26 @@ stop_words = set(stopwords.words('english'))
558
  @st.cache_resource
559
  def load_all_models():
560
  vader = SentimentIntensityAnalyzer()
561
- bertweet = pipeline("sentiment-analysis", model="finiteautomata/bertweet-base-sentiment-analysis", device=-1, truncation=True, max_length=128)
562
- roberta = pipeline("sentiment-analysis", model="cardiffnlp/twitter-roberta-base-sentiment", device=-1, truncation=True, max_length=512)
563
- roberta_large = pipeline("sentiment-analysis", model="siebert/sentiment-roberta-large-english", device=-1, truncation=True, max_length=512)
564
  return vader, bertweet, roberta, roberta_large
565
 
566
- with st.spinner('Mempersiapkan model NLP…'):
567
  vader, bertweet, roberta, roberta_large = load_all_models()
568
 
 
569
  # ==============================
570
- # HELPER FUNCTIONS (unchanged logic)
571
  # ==============================
572
  def clean_text(text):
573
  text = str(text).lower()
574
  text = re.sub(r"http\S+", "", text)
575
- text = re.sub(r"@\w+", "", text)
576
- text = re.sub(r"#\w+", "", text)
577
  text = re.sub(r"[^\w\s]", "", text)
578
- tokens = [w for w in text.split() if w not in stop_words]
 
579
  return " ".join(tokens)
580
 
581
  def classify_tb(score):
@@ -592,230 +563,228 @@ def map_bertweet(label):
592
  def get_daily_label(score):
593
  if score > 0.05: return 'Positive'
594
  elif score < -0.05: return 'Negative'
595
- return 'Neutral'
596
-
597
- def fmt_label(label):
598
- if label == 'positive': return "🟢 Positive"
599
- if label == 'negative': return "🔴 Negative"
600
- return "⚪ Neutral"
601
 
602
 
603
  # ==============================================================================
604
- # HALAMAN 1: UJI KALIMAT (orange hero – mirip Vancouver Bitcoin hero section)
605
  # ==============================================================================
606
  if st.session_state.page == "uji_kalimat":
607
 
608
- # ── Hero two-column layout ────────────────────────────────────────────────
609
- col_text, col_img = st.columns([1.05, 1], gap="large")
 
 
610
 
611
  with col_text:
612
  st.markdown("""
613
- <div style="padding: 4rem 0 2rem 4rem;">
614
- <span class="hero-badge">Analisis Sentimen Kripto</span>
615
- <h1 class="hero-h1">
616
- Bitcoin Volatility<br><em>vs Public Sentiment</em>
617
- </h1>
618
- <p class="hero-sub">
619
- Analisis Volatilitas Harga Bitcoin Terhadap Sentimen Publik
620
- Pada Platform X Berbasis Python &amp; 5 Model NLP.
621
- </p>
622
- <div class="hero-researcher">
623
- 🎓 &nbsp;<strong>Peneliti:</strong> Arya Galuh Saputra &nbsp;(H1D022022)
624
- </div>
625
  </div>
626
  """, unsafe_allow_html=True)
627
 
628
- with st.container():
629
- # input wrapped in same left padding
630
- st.markdown('<div style="padding: 0 0 0 4rem;">', unsafe_allow_html=True)
631
- user_input = st.text_area(
632
- "✍️ Masukkan Tweet (Bahasa Inggris):",
633
- "Great, Bitcoin just crashed another 10% today.",
634
- height=130,
635
- )
636
- st.markdown("<br>", unsafe_allow_html=True)
637
- st.markdown('<div class="analyze-btn">', unsafe_allow_html=True)
638
- analyze_btn = st.button("🚀 Analisis Sentimen Sekarang", use_container_width=True)
639
- st.markdown("</div></div>", unsafe_allow_html=True)
 
 
 
 
 
 
640
 
641
  with col_img:
642
- st.markdown("<div style='padding: 3rem 3rem 2rem 0;'>", unsafe_allow_html=True)
643
  try:
644
  st.image(img_hero, use_container_width=True)
645
  except Exception:
646
- # Placeholder visual when image is missing
647
  st.markdown("""
648
- <div style="
649
- background: rgba(255,255,255,0.15);
650
- border-radius: 20px;
651
- height: 380px;
652
- display: flex;
653
- flex-direction: column;
654
- align-items: center;
655
- justify-content: center;
656
- gap: 1rem;
657
- backdrop-filter: blur(4px);
658
- border: 2px dashed rgba(255,255,255,0.4);">
659
- <div style="font-size:5rem;">₿</div>
660
- <p style="color:rgba(255,255,255,0.8);font-size:1rem;margin:0;font-weight:600;">
661
- crypto-currency-concept-830px.png
662
- </p>
663
- <p style="color:rgba(255,255,255,0.55);font-size:0.85rem;margin:0;">
664
- Letakkan file gambar di direktori yang sama
665
- </p>
666
- </div>
667
- """, unsafe_allow_html=True)
668
- st.markdown("</div>", unsafe_allow_html=True)
669
-
670
- # ── Results section ───────────────────────────────────────────────────────
671
  if analyze_btn:
672
- st.markdown('<div style="padding: 0 4rem 4rem;">', unsafe_allow_html=True)
673
- st.markdown(f"<hr class='vb-divider'>", unsafe_allow_html=True)
 
 
 
674
 
675
- # Language check
676
  try:
677
  if detect(user_input) != 'en':
678
- st.warning("⚠️ Teks sepertinya bukan bahasa Inggris. Hasil mungkin memiliki bias.")
679
  except:
680
  pass
681
 
682
  text = clean_text(user_input)
683
 
684
- with st.spinner("Model NLP sedang memproses teks…"):
685
- try: v_label = "positive" if vader.polarity_scores(text)['compound'] > 0.05 else "negative" if vader.polarity_scores(text)['compound'] < -0.05 else "neutral"
686
  except: v_label = "neutral"
687
 
688
- try: t_label = classify_tb(TextBlob(text).sentiment.polarity)
689
  except: t_label = "neutral"
690
 
691
- try: b_label = map_bertweet(bertweet(text)[0]['label'])
692
  except: b_label = "neutral"
693
 
694
- try: r_label = map_roberta(roberta(text)[0]['label'])
695
  except: r_label = "neutral"
696
 
697
- try: rl_label = roberta_large(text)[0]['label'].lower()
698
  except: rl_label = "neutral"
699
 
700
- # Result card (white box on orange bg)
701
- st.markdown("""
702
- <div style="
703
- background:#ffffff;
704
- border-radius:20px;
705
- padding:2rem 2.5rem;
706
- box-shadow: 0 20px 60px rgba(0,0,0,0.15);">
707
- <h3 style="
708
- font-family:'Nunito',sans-serif;
709
- font-weight:900;
710
- font-size:1.5rem;
711
- color:#1E1E3F !important;
712
- text-align:center;
713
- margin-bottom:1.5rem;">
714
- 📋 Hasil Deteksi Sentimen
715
- </h3>
716
- """, unsafe_allow_html=True)
717
-
718
- result_df = pd.DataFrame({
719
- "Metode NLP": ["VADER", "TextBlob", "BERTweet", "RoBERTa Base", "RoBERTa Large"],
720
- "Hasil Sentimen": [fmt_label(v_label), fmt_label(t_label),
721
- fmt_label(b_label), fmt_label(r_label), fmt_label(rl_label)],
722
- })
723
-
724
- col_tbl, _ = st.columns([2, 1])
725
- with col_tbl:
726
- st.dataframe(result_df, use_container_width=True, hide_index=True)
727
-
728
- st.markdown("</div>", unsafe_allow_html=True)
729
- st.markdown("</div>", unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
730
 
731
 
732
  # ==============================================================================
733
- # HALAMAN 2: ANALISIS BATCH (cream/light – mirip Vancouver Bitcoin slice section)
734
  # ==============================================================================
735
  elif st.session_state.page == "analisis_batch":
736
 
737
  plt.style.use('default')
738
  sns.set_theme(style="whitegrid", rc={
739
- "axes.facecolor": "#FDFAF7",
740
- "figure.facecolor":"#FFFFFF",
741
- "axes.edgecolor": "#E8E0D4",
742
- "text.color": "#1E1E3F",
743
- "xtick.color": "#5A6478",
744
- "ytick.color": "#5A6478",
745
- "grid.color": "#F1EBE3",
746
  })
747
 
748
- # ── Hero two-column (image left / form right – mirrors image 2) ──────────
749
- col_img_batch, col_form = st.columns([1, 1.3], gap="large")
750
 
751
- with col_img_batch:
752
- st.markdown("<div style='padding: 4rem 0 2rem 4rem;'>", unsafe_allow_html=True)
753
- try:
754
- st.image(img_batch, use_container_width=True)
755
- except Exception:
756
- st.markdown("""
757
- <div style="
758
- background: #fff;
759
- border-radius: 20px;
760
- height: 340px;
761
- display: flex;
762
- flex-direction: column;
763
- align-items: center;
764
- justify-content: center;
765
- gap: 1rem;
766
- border: 2px dashed #F7931A;">
767
- <div style="font-size:4rem;">📊</div>
768
- <p style="color:#5A6478;font-size:0.9rem;margin:0;font-weight:600;">
769
- slice3-1-1536x830.png
770
- </p>
771
- </div>
772
- """, unsafe_allow_html=True)
773
- st.markdown("</div>", unsafe_allow_html=True)
774
-
775
- with col_form:
776
  st.markdown("""
777
- <div style="padding: 4rem 4rem 2rem 1rem;">
778
- <span class="hero-badge">Analisis Massal Tweet Bitcoin</span>
779
- <h2 class="hero-h1">Analisis Batch Data</h2>
780
- <p class="hero-sub">
781
- Unggah file rekam jejak tweet (.txt) untuk diekstraksi dan dianalisis
782
- secara massal terhadap volatilitas pasar Bitcoin.
783
- </p>
784
- </div>
785
- """, unsafe_allow_html=True)
786
 
787
- st.markdown('<div style="padding: 0 4rem 0 1rem;">', unsafe_allow_html=True)
788
  tweet_files = st.file_uploader(
789
- "📁 Pilih file Tweet (.txt)",
790
  type=['txt'],
791
- accept_multiple_files=True,
792
  )
793
 
794
- with st.expander("📌 Format TXT yang Didukung"):
795
  st.code(
796
  "username | 2024-03-01 14:00:00\n"
797
  "Isi tweet baris pertama di sini\n\n"
798
  "username2 | 2024-03-01 15:30:00\n"
799
  "Isi tweet baris kedua di sini",
800
- language="text",
801
  )
802
 
803
  st.markdown("<br>", unsafe_allow_html=True)
804
- st.markdown('<div class="analyze-btn">', unsafe_allow_html=True)
805
- analyze_batch_btn = st.button("⚙️ Mulai Eksekusi Analisis", key="batch_btn", use_container_width=True)
806
- st.markdown("</div></div>", unsafe_allow_html=True)
807
 
808
- # ── Batch processing logic (unchanged) ───────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
809
  if tweet_files and analyze_batch_btn:
810
- st.markdown('<div style="padding: 0 4rem 4rem;">', unsafe_allow_html=True)
811
- st.markdown("<hr class='vb-divider'>", unsafe_allow_html=True)
 
 
 
812
 
813
  tweet_files = sorted(tweet_files, key=lambda x: x.name)
 
814
  data = []
815
- progress_bar = st.progress(0, text="Mengekstrak sentimen dari data")
816
 
817
- total_uploaded = 0
818
- total_skipped = 0
819
 
820
  for idx, file in enumerate(tweet_files):
821
  content = file.getvalue().decode("utf-8").replace("\r\n", "\n").strip()
@@ -826,38 +795,42 @@ elif st.session_state.page == "analisis_batch":
826
  if len(parts) != 2: continue
827
 
828
  meta, text_raw = parts
 
829
  try:
830
  DetectorFactory.seed = 0
831
  lang = detect(text_raw)
832
  if lang != 'en':
833
- total_skipped += 1
834
  continue
835
  except:
836
- total_skipped += 1
837
  continue
838
 
839
- username, date_val = (meta.split(" | ") if " | " in meta else ("unknown", "unknown"))
840
  short_date = date_val[:10]
841
  text = clean_text(text_raw)
842
 
843
- try: v_l = "positive" if vader.polarity_scores(text)['compound'] > 0.05 else "negative" if vader.polarity_scores(text)['compound'] < -0.05 else "neutral"
844
- except: v_l = "neutral"
845
- try: tb_l = classify_tb(TextBlob(text).sentiment.polarity)
846
- except: tb_l = "neutral"
847
- try: bt_l = map_bertweet(bertweet(text)[0]['label'])
848
- except: bt_l = "neutral"
849
- try: ro_l = map_roberta(roberta(text)[0]['label'])
850
- except: ro_l = "neutral"
851
- try: rl_l = roberta_large(text)[0]['label'].lower()
852
- except: rl_l = "neutral"
 
 
 
 
853
 
854
  data.append({
855
- "date": short_date, "raw_tweet": text_raw.strip(),
856
- "cleaned_tweet": text,
857
- "vader": v_l, "textblob": tb_l, "bertweet": bt_l,
858
- "roberta": ro_l, "roberta_large": rl_l,
859
  })
860
- total_uploaded += 1
861
 
862
  progress_bar.progress((idx + 1) / len(tweet_files),
863
  text=f"Memproses file {idx+1} dari {len(tweet_files)}")
@@ -867,19 +840,17 @@ elif st.session_state.page == "analisis_batch":
867
  if df.empty:
868
  st.error("❌ Data kosong. Pastikan format TXT benar dan tweet berbahasa Inggris.")
869
  else:
870
- # ── Metrics ──────────────────────────────────────────────────────
871
- st.markdown("### 📊 Ringkasan Pemrosesan")
872
- c1, c2, c3 = st.columns(3)
873
- c1.metric("Tweet Diproses", f"{total_uploaded}", border=True)
874
- c2.metric("Tweet Diabaikan (Non-EN)", f"{total_skipped}", border=True)
875
- c3.metric("Total Model NLP", "5 Model", border=True)
876
-
877
- # ── Bitcoin price API ─────────────────────────────────────────────
878
  target_dates = sorted(df['date'].unique())
879
- start_unix = int(datetime.strptime(target_dates[0], "%Y-%m-%d").replace(tzinfo=timezone.utc).timestamp()) - 86400
880
- end_unix = int(datetime.strptime(target_dates[-1], "%Y-%m-%d").replace(tzinfo=timezone.utc).timestamp()) + 86400
 
 
881
 
882
- st.info("📡 Mengambil data harga Bitcoin dari CoinGecko API…")
883
  url = "https://api.coingecko.com/api/v3/coins/bitcoin/market_chart/range"
884
  params = {"vs_currency": "usd", "from": start_unix, "to": end_unix}
885
  headers = {"accept": "application/json", "User-Agent": "Mozilla/5.0"}
@@ -892,10 +863,13 @@ elif st.session_state.page == "analisis_batch":
892
  st.error(f"API Error {res.status_code}: {res.text}")
893
  else:
894
  data_json = res.json()
 
895
  if "prices" not in data_json:
896
  st.error("Data harga tidak ditemukan di respons API.")
897
  else:
898
- df_price = pd.DataFrame(data_json["prices"], columns=["timestamp", "price"])
 
 
899
  df_price["date"] = pd.to_datetime(df_price["timestamp"], unit="ms").dt.date
900
  df_price = df_price.groupby("date")["price"].mean().reset_index()
901
  df_price["pct_change"] = df_price["price"].pct_change() * 100
@@ -904,122 +878,123 @@ elif st.session_state.page == "analisis_batch":
904
  df_price = df_price[df_price["date"].isin(pd.to_datetime(target_dates).date)]
905
 
906
  if df_price.empty:
907
- st.warning("⚠️ Data Harga API kosong. Periksa rentang tanggal di .txt (yyyy-mm-dd).")
908
  else:
909
- st.markdown("<hr class='vb-divider'>", unsafe_allow_html=True)
910
 
911
- st.markdown("#### 🗣️ Data Sentimen Mentah")
912
- raw_cols = ["date", "raw_tweet", "vader", "textblob", "bertweet", "roberta", "roberta_large"]
913
- st.dataframe(df[raw_cols], use_container_width=True, hide_index=True)
914
 
915
  sentiment_map = {"positive": 1, "neutral": 0, "negative": -1}
916
  df_score = df.copy()
917
- models = ["vader", "textblob", "bertweet", "roberta", "roberta_large"]
918
- for col in models:
919
  df_score[col] = df_score[col].map(sentiment_map)
920
 
921
- df_sent_daily = df_score.groupby("date")[models].mean().reset_index()
922
- df_sent_daily["date"] = pd.to_datetime(df_sent_daily["date"]).dt.date
 
 
923
  for col in models:
924
- df_sent_daily[f"{col}_label"] = df_sent_daily[col].apply(get_daily_label)
925
 
926
- daily_cols = ["date"]
927
- for col in models: daily_cols.extend([col, f"{col}_label"])
 
928
 
929
- st.markdown("<br>#### Historis Harga & Volatilitas Bitcoin", unsafe_allow_html=True)
930
  st.dataframe(df_price[["date","price","pct_change","log_return"]], use_container_width=True, hide_index=True)
931
 
932
- df_merged = pd.merge(df_price, df_sent_daily, on="date", how="inner")
 
 
 
 
933
 
934
- st.markdown("<br>### 🗂️ Dataset Final (Terintegrasi)", unsafe_allow_html=True)
935
- final_cols = ["date","price","pct_change","log_return"] + [c for c in daily_cols if c != "date"]
936
- st.dataframe(df_merged[final_cols], use_container_width=True, hide_index=True)
937
 
938
- # Download
939
- dl1, dl2, _ = st.columns([1, 1, 3])
940
- csv_bytes = df_merged.to_csv(index=False).encode('utf-8')
941
- dl1.download_button("📥 Unduh CSV", data=csv_bytes, file_name="sentiment_volatility.csv", mime="text/csv", use_container_width=True)
942
- buf = io.BytesIO()
943
- with pd.ExcelWriter(buf, engine='xlsxwriter') as w: df_merged.to_excel(w, index=False)
944
- dl2.download_button("📥 Unduh Excel", data=buf.getvalue(), file_name="sentiment_volatility.xlsx", mime="application/vnd.ms-excel", use_container_width=True)
945
 
946
- st.markdown("<hr class='vb-divider'>", unsafe_allow_html=True)
947
 
948
- # ── Pearson correlation ───────────────────────────────────────
949
  st.subheader("🔬 Uji Korelasi Pearson")
950
- st.caption("Menganalisis hubungan antara skor sentimen harian dan volatilitas log-return BTC.")
951
 
952
- corr_data, raw_corr = [], []
953
- for method in models:
 
 
954
  corr, pval = pearsonr(df_merged["log_return"], df_merged[method])
955
- corr_data.append({
956
- "Metode": method.upper(),
957
- "r (Korelasi)": f"{corr:.4f}",
958
- "Arah": "Positif" if corr > 0 else "Negatif",
959
- "p-value": f"{pval:.4f}",
960
- "Status": "Signifikan" if pval < 0.05 else "Tidak Signifikan",
961
- })
962
- raw_corr.append({"metode": method.upper(), "r": corr, "p": pval})
963
 
964
  st.table(pd.DataFrame(corr_data))
965
 
966
- # ── Line chart ────────────────────────────────────────────────
967
- st.markdown("<br>", unsafe_allow_html=True)
968
- st.subheader("📈 Trend: Sentimen vs BTC Volatility")
969
-
970
- fig_line, ax = plt.subplots(figsize=(14, 5))
971
- ax.plot(df_merged["date"], df_merged["log_return"],
972
- label="BTC Log Return", color="#F7931A", linewidth=3)
973
- colors = ["#1E1E3F","#10B981","#EC4899","#14B8A6","#6366F1"]
974
- for i, m in enumerate(["vader","textblob","roberta","roberta_large","bertweet"]):
975
- ax.plot(df_merged["date"], df_merged[m],
976
- label=f"Sentimen: {m.upper()}", color=colors[i],
977
- linewidth=1.5, linestyle="--", alpha=0.8)
978
- ax.set_title("Pergerakan Sentimen vs Log Return Bitcoin",
979
- fontsize=14, pad=14, fontweight='bold', color="#1E1E3F")
980
- ax.set_xlabel("Tanggal", fontsize=11); ax.set_ylabel("Nilai Metrik", fontsize=11)
981
- ax.legend(loc='upper left', bbox_to_anchor=(1, 1), frameon=True)
982
- plt.tight_layout(); st.pyplot(fig_line)
983
-
984
- # ── Scatter plots ─────────────────────────────────────────────
985
- st.markdown("<br>### 🔵 Pola Distribusi Scatter", unsafe_allow_html=True)
986
- cols_sc = st.columns(3)
987
- for i, method in enumerate(models):
988
- with cols_sc[i % 3]:
989
- fig_sc, ax_sc = plt.subplots(figsize=(5, 4))
990
- sns.regplot(data=df_merged, x=method, y="log_return", ax=ax_sc,
991
- scatter_kws={"s": 40, "color": "#1E1E3F", "alpha": 0.5},
992
  line_kws={"color": "#F7931A", "linewidth": 2})
993
- ax_sc.set_title(method.upper(), fontweight='bold', color="#1E1E3F")
994
- ax_sc.set_xlabel("Sentimen Score"); ax_sc.set_ylabel("Log Return")
995
- plt.tight_layout(); st.pyplot(fig_sc)
996
-
997
- # ── Conclusion ────────────────────────────────────────────────
998
- st.markdown("<hr class='vb-divider'>", unsafe_allow_html=True)
 
 
999
  st.subheader("📝 Kesimpulan Otomatis")
1000
 
1001
- date_max = df_merged.loc[df_merged["log_return"].idxmax(), "date"]
1002
- date_min = df_merged.loc[df_merged["log_return"].idxmin(), "date"]
1003
- sig_models = [r["metode"] for r in raw_corr if r["p"] < 0.05]
1004
- strongest_model = max(raw_corr, key=lambda x: abs(x["r"]))
1005
- arah_text = "berbanding lurus (positif)" if strongest_model["r"] > 0 else "berbanding terbalik (negatif)"
 
 
 
1006
 
1007
- st.write(f"Puncak lonjakan positif (*max log return*) terjadi pada **{date_max}**, "
1008
- f"sedangkan penurunan ekstrem terjadi pada **{date_min}**.")
1009
 
1010
  if sig_models:
1011
- st.success(
1012
- f"**Hipotesis Diterima (H1):** Ditemukan korelasi linier signifikan pada metode "
1013
- f"**{', '.join(sig_models)}** (*p-value* < 0.05). "
1014
- f"Metode terkuat: **{strongest_model['metode']}** — hubungan **{arah_text}**."
1015
- )
1016
  else:
1017
- st.warning(
1018
- "**Hipotesis Ditolak (H0 Diterima):** Tidak ditemukan korelasi linier signifikan "
1019
- "(seluruh *p-value* ≥ 0.05). Volatilitas cenderung dipengaruhi faktor di luar sentimen X."
1020
- )
1021
 
1022
- except Exception as e:
1023
- st.error(f"❌ Terjadi kesalahan sistem: {e}")
1024
 
1025
- st.markdown("</div>", unsafe_allow_html=True)
 
 
20
  DetectorFactory.seed = 0
21
 
22
  # ==============================
23
+ # SETTING PATH
24
  # ==============================
25
  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
26
+
27
  img_hero = os.path.join(BASE_DIR, "crypto-currency-concept-830px.png")
28
  img_batch = os.path.join(BASE_DIR, "slice3-1-1536x830.png")
29
 
 
31
  # KONFIGURASI HALAMAN & STATE NAVIGASI
32
  # ==============================
33
  st.set_page_config(
34
+ page_title="Bitcoin Sentimen Analyzer",
35
  page_icon="₿",
36
  layout="wide",
37
+ initial_sidebar_state="collapsed"
38
  )
39
 
40
+ if 'page' not in st.session_state:
41
  st.session_state.page = "uji_kalimat"
42
 
43
  # ==============================
44
+ # GLOBAL CSS
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; }
53
+ .block-container {
54
+ padding-top: 0 !important;
55
+ padding-bottom: 0 !important;
56
+ max-width: 100% !important;
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 {
70
  display: flex;
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
465
  # ==============================
466
+ def set_page(page_name):
467
+ st.session_state.page = page_name
468
+
469
+ st.markdown("""
470
+ <div class="vbc-navbar">
471
+ <div class="vbc-logo">
472
+ <span class="vbc-logo-icon">₿</span>
473
+ Bitcoin Sentimen
474
+ </div>
 
 
 
 
 
 
 
 
475
  </div>
476
  """, unsafe_allow_html=True)
477
 
 
 
478
 
479
+ nav_col_pad, nav_col1, nav_col2, nav_col_end = st.columns([6, 1, 1, 1])
480
+
481
+ with nav_col1:
482
+ is_uji = st.session_state.page == "uji_kalimat"
483
+ css_class = "btn-orange" if is_uji else "btn-ghost"
484
+ st.markdown(f'<div class="{css_class}" style="padding: 6px 0;">', unsafe_allow_html=True)
485
  if st.button("📝 Uji Kalimat", use_container_width=True, key="nav_uji"):
486
+ set_page("uji_kalimat"); st.rerun()
487
+ st.markdown('</div>', unsafe_allow_html=True)
 
488
 
489
+ with nav_col2:
490
+ is_batch = st.session_state.page == "analisis_batch"
491
+ css_class = "btn-orange" if is_batch else "btn-ghost"
492
+ st.markdown(f'<div class="{css_class}" style="padding: 6px 0;">', unsafe_allow_html=True)
493
  if st.button("📊 Analisis Batch", use_container_width=True, key="nav_batch"):
494
+ set_page("analisis_batch"); st.rerun()
495
+ st.markdown('</div>', unsafe_allow_html=True)
496
+
497
+
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
 
527
  @st.cache_resource
528
  def load_all_models():
529
  vader = SentimentIntensityAnalyzer()
530
+ bertweet = pipeline("sentiment-analysis", model="finiteautomata/bertweet-base-sentiment-analysis", device=-1, truncation=True, max_length=128)
531
+ roberta = pipeline("sentiment-analysis", model="cardiffnlp/twitter-roberta-base-sentiment", device=-1, truncation=True, max_length=512)
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('...'):
536
  vader, bertweet, roberta, roberta_large = load_all_models()
537
 
538
+
539
  # ==============================
540
+ # FUNGSI CLEAN TEXT & MAPPING
541
  # ==============================
542
  def clean_text(text):
543
  text = str(text).lower()
544
  text = re.sub(r"http\S+", "", text)
545
+ text = re.sub(r"@\w+", "", text)
546
+ text = re.sub(r"#\w+", "", text)
547
  text = re.sub(r"[^\w\s]", "", text)
548
+ tokens = text.split()
549
+ tokens = [word for word in tokens if word not in stop_words]
550
  return " ".join(tokens)
551
 
552
  def classify_tb(score):
 
563
  def get_daily_label(score):
564
  if score > 0.05: return 'Positive'
565
  elif score < -0.05: return 'Negative'
566
+ else: return 'Neutral'
 
 
 
 
 
567
 
568
 
569
  # ==============================================================================
570
+ # HALAMAN 1 UJI KALIMAT
571
  # ==============================================================================
572
  if st.session_state.page == "uji_kalimat":
573
 
574
+
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
585
+ </h1>
586
+ <p class="hero-sub">
587
+ Analisis Volatilitas Harga Bitcoin Terhadap Sentimen Publik
588
+ Pada Platform X Berbasis Python 5 Model NLP.
589
+ </p>
590
+ <div class="hero-card">
591
+ <div class="hero-card-dot"></div>
592
+ <p><b>Peneliti:</b> Arya Galuh Saputra &nbsp;·&nbsp; H1D022022</p>
593
  </div>
594
  """, unsafe_allow_html=True)
595
 
596
+ user_input = st.text_area(
597
+ "Masukkan Tweet (Bahasa Inggris):",
598
+ "Great, Bitcoin just crashed another 10% today.",
599
+ height=120
600
+ )
601
+
602
+ st.markdown("<br>", unsafe_allow_html=True)
603
+
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
 
615
  with col_img:
 
616
  try:
617
  st.image(img_hero, use_container_width=True)
618
  except Exception:
619
+
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)
629
+
630
+
 
 
 
 
 
 
 
 
 
 
 
 
 
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:
639
  if detect(user_input) != 'en':
640
+ st.warning("⚠️ Teks sepertinya bukan bahasa Inggris. Hasil prediksi mungkin memiliki bias.")
641
  except:
642
  pass
643
 
644
  text = clean_text(user_input)
645
 
646
+ with st.spinner("Mesin NLP sedang memproses..."):
647
+ try: v_label = "positive" if vader.polarity_scores(text)['compound'] > 0.05 else ("negative" if vader.polarity_scores(text)['compound'] < -0.05 else "neutral")
648
  except: v_label = "neutral"
649
 
650
+ try: t_label = classify_tb(TextBlob(text).sentiment.polarity)
651
  except: t_label = "neutral"
652
 
653
+ try: b_label = map_bertweet(bertweet(text)[0]['label'])
654
  except: b_label = "neutral"
655
 
656
+ try: r_label = map_roberta(roberta(text)[0]['label'])
657
  except: r_label = "neutral"
658
 
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),
675
+ ("TextBlob", t_label),
676
+ ("BERTweet", b_label),
677
+ ("RoBERTa Base", r_label),
678
+ ("RoBERTa Large", rl_label),
679
+ ]
680
+
681
+
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)
706
+
707
+ st.markdown("</div>", unsafe_allow_html=True)
708
 
709
 
710
  # ==============================================================================
711
+ # HALAMAN 2 ANALISIS BATCH
712
  # ==============================================================================
713
  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
 
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 · Batch Processing</p>
734
+ <h2 class="batch-title">Analisis Batch<br>Data Tweet</h2>
735
+ <p class="batch-sub">
736
+ Unggah file tweet (.txt) untuk diekstraksi dan
737
+ dianalisis secara masal terhadap volatilitas pasar Bitcoin.
738
+ </p>""", unsafe_allow_html=True)
 
 
 
739
 
 
740
  tweet_files = st.file_uploader(
741
+ "Pilih file Tweet (.txt)",
742
  type=['txt'],
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"
750
  "username2 | 2024-03-01 15:30:00\n"
751
  "Isi tweet baris kedua di sini",
752
+ language="text"
753
  )
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:
761
+ try:
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)
772
+
773
+
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)
782
+
783
  data = []
784
+ progress_bar = st.progress(0, text="Mengekstrak sentimen dari data...")
785
 
786
+ total_tweets_uploaded = 0
787
+ total_tweets_skipped = 0
788
 
789
  for idx, file in enumerate(tweet_files):
790
  content = file.getvalue().decode("utf-8").replace("\r\n", "\n").strip()
 
795
  if len(parts) != 2: continue
796
 
797
  meta, text_raw = parts
798
+
799
  try:
800
  DetectorFactory.seed = 0
801
  lang = detect(text_raw)
802
  if lang != 'en':
803
+ total_tweets_skipped += 1
804
  continue
805
  except:
806
+ total_tweets_skipped += 1
807
  continue
808
 
809
+ username, date_val = meta.split(" | ") if " | " in meta else ("unknown", "unknown")
810
  short_date = date_val[:10]
811
  text = clean_text(text_raw)
812
 
813
+ try: v_score = vader.polarity_scores(text)['compound']; vader_label = "positive" if v_score > 0.05 else ("negative" if v_score < -0.05 else "neutral")
814
+ except: vader_label = "neutral"
815
+
816
+ try: tb_label = classify_tb(TextBlob(text).sentiment.polarity)
817
+ except: tb_label = "neutral"
818
+
819
+ try: bertweet_label = map_bertweet(bertweet(text)[0]['label'])
820
+ except: bertweet_label = "neutral"
821
+
822
+ try: roberta_label = map_roberta(roberta(text)[0]['label'])
823
+ except: roberta_label = "neutral"
824
+
825
+ try: roberta_large_label = roberta_large(text)[0]['label'].lower()
826
+ except: roberta_large_label = "neutral"
827
 
828
  data.append({
829
+ "date": short_date, "raw_tweet": text_raw.strip(), "cleaned_tweet": text,
830
+ "vader": vader_label, "textblob": tb_label, "bertweet": bertweet_label,
831
+ "roberta": roberta_label, "roberta_large": roberta_large_label,
 
832
  })
833
+ total_tweets_uploaded += 1
834
 
835
  progress_bar.progress((idx + 1) / len(tweet_files),
836
  text=f"Memproses file {idx+1} dari {len(tweet_files)}")
 
840
  if df.empty:
841
  st.error("❌ Data kosong. Pastikan format TXT benar dan tweet berbahasa Inggris.")
842
  else:
843
+ col_m1, col_m2, col_m3 = st.columns(3)
844
+ col_m1.metric("Tweet Diproses", f"{total_tweets_uploaded}", border=True)
845
+ col_m2.metric("Tweet Diabaikan (Non-EN)", f"{total_tweets_skipped}", border=True)
846
+ col_m3.metric("Model NLP", "5 Model", border=True)
847
+
 
 
 
848
  target_dates = sorted(df['date'].unique())
849
+ start_unix = int(datetime.strptime(target_dates[0], "%Y-%m-%d").replace(tzinfo=timezone.utc).timestamp()) - 86400
850
+ end_unix = int(datetime.strptime(target_dates[-1], "%Y-%m-%d").replace(tzinfo=timezone.utc).timestamp()) + 86400
851
+
852
+ st.info("📡 Mengambil data harga Bitcoin dari CoinGecko API...")
853
 
 
854
  url = "https://api.coingecko.com/api/v3/coins/bitcoin/market_chart/range"
855
  params = {"vs_currency": "usd", "from": start_unix, "to": end_unix}
856
  headers = {"accept": "application/json", "User-Agent": "Mozilla/5.0"}
 
863
  st.error(f"API Error {res.status_code}: {res.text}")
864
  else:
865
  data_json = res.json()
866
+
867
  if "prices" not in data_json:
868
  st.error("Data harga tidak ditemukan di respons API.")
869
  else:
870
+ prices = data_json["prices"]
871
+
872
+ df_price = pd.DataFrame(prices, columns=["timestamp", "price"])
873
  df_price["date"] = pd.to_datetime(df_price["timestamp"], unit="ms").dt.date
874
  df_price = df_price.groupby("date")["price"].mean().reset_index()
875
  df_price["pct_change"] = df_price["price"].pct_change() * 100
 
878
  df_price = df_price[df_price["date"].isin(pd.to_datetime(target_dates).date)]
879
 
880
  if df_price.empty:
881
+ st.warning("⚠️ Data Harga API kosong. Pastikan rentang tanggal di .txt sesuai (yyyy-mm-dd).")
882
  else:
883
+ st.markdown("<hr class='vbc-divider'>", unsafe_allow_html=True)
884
 
885
+ st.markdown("🗣️ Data Sentimen Mentah")
886
+ raw_display_cols = ["date","raw_tweet","vader","textblob","bertweet","roberta","roberta_large"]
887
+ st.dataframe(df[raw_display_cols], use_container_width=True, hide_index=True)
888
 
889
  sentiment_map = {"positive": 1, "neutral": 0, "negative": -1}
890
  df_score = df.copy()
891
+ for col in ["vader","textblob","bertweet","roberta","roberta_large"]:
 
892
  df_score[col] = df_score[col].map(sentiment_map)
893
 
894
+ models = ["vader","textblob","bertweet","roberta","roberta_large"]
895
+ df_sentiment_daily = df_score.groupby("date")[models].mean().reset_index()
896
+ df_sentiment_daily["date"] = pd.to_datetime(df_sentiment_daily["date"]).dt.date
897
+
898
  for col in models:
899
+ df_sentiment_daily[f"{col}_label"] = df_sentiment_daily[col].apply(get_daily_label)
900
 
901
+ daily_display_cols = ["date"]
902
+ for col in models:
903
+ daily_display_cols.extend([col, f"{col}_label"])
904
 
905
+ st.markdown("₿ Data Harga Bitcoin & Volatilitas Bitcoin")
906
  st.dataframe(df_price[["date","price","pct_change","log_return"]], use_container_width=True, hide_index=True)
907
 
908
+ df_merged = pd.merge(df_price, df_sentiment_daily, on="date", how="inner")
909
+
910
+ st.markdown("🗂️ Dataset Final (Terintegrasi)")
911
+ final_display_cols = ["date","price","pct_change","log_return"] + [c for c in daily_display_cols if c != "date"]
912
+ st.dataframe(df_merged[final_display_cols], use_container_width=True, hide_index=True)
913
 
914
+ col_dl1, col_dl2, _ = st.columns([1, 1, 3])
915
+ csv_data = df_merged.to_csv(index=False).encode('utf-8')
916
+ col_dl1.download_button("📥 Unduh CSV", data=csv_data, file_name="sentiment_volatility.csv", mime="text/csv", use_container_width=True)
917
 
918
+ buffer = io.BytesIO()
919
+ with pd.ExcelWriter(buffer, engine='xlsxwriter') as writer:
920
+ df_merged.to_excel(writer, index=False)
921
+ col_dl2.download_button("📥 Unduh Excel", data=buffer.getvalue(), file_name="sentiment_volatility.xlsx", mime="application/vnd.ms-excel", use_container_width=True)
 
 
 
922
 
923
+ st.markdown("<hr class='vbc-divider'>", unsafe_allow_html=True)
924
 
925
+ # Pearson
926
  st.subheader("🔬 Uji Korelasi Pearson")
927
+ st.caption("Menganalisis hubungan statistik antara skor sentimen harian dan volatilitas log-return BTC.")
928
 
929
+ corr_data = []
930
+ raw_corr_results = []
931
+
932
+ for method in ["vader","textblob","bertweet","roberta","roberta_large"]:
933
  corr, pval = pearsonr(df_merged["log_return"], df_merged[method])
934
+ arah = "Positif" if corr > 0 else "Negatif"
935
+ sig = "Signifikan" if pval < 0.05 else "Tidak Signifikan"
936
+ corr_data.append({"Metode": method.upper(), "r (Korelasi)": f"{corr:.4f}", "Arah": arah, "p-value": f"{pval:.4f}", "Status": sig})
937
+ raw_corr_results.append({"metode": method.upper(), "r": corr, "p": pval})
 
 
 
 
938
 
939
  st.table(pd.DataFrame(corr_data))
940
 
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)
948
+ ax_line.set_title("Pergerakan Sentimen vs Log Return Bitcoin", fontsize=14, pad=15, fontweight='bold')
949
+ ax_line.set_xlabel("Tanggal", fontsize=11)
950
+ ax_line.set_ylabel("Nilai Metrik", fontsize=11)
951
+ ax_line.legend(loc='upper left', bbox_to_anchor=(1, 1), frameon=True)
952
+ plt.tight_layout()
953
+ st.pyplot(fig_line)
954
+
955
+ # Scatter
956
+ st.markdown("🔵 Pola Distribusi Scatter")
957
+ cols = st.columns(3)
958
+ for idx2, method in enumerate(["vader","textblob","bertweet","roberta","roberta_large"]):
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")
967
+ plt.tight_layout()
968
+ st.pyplot(fig_s)
969
+
970
+ # Kesimpulan
971
+ st.markdown("<hr class='vbc-divider'>", unsafe_allow_html=True)
972
  st.subheader("📝 Kesimpulan Otomatis")
973
 
974
+ max_idx = df_merged["log_return"].idxmax()
975
+ min_idx = df_merged["log_return"].idxmin()
976
+ date_max = df_merged.loc[max_idx, "date"]
977
+ date_min = df_merged.loc[min_idx, "date"]
978
+
979
+ sig_models = [r["metode"] for r in raw_corr_results if r["p"] < 0.05]
980
+ strongest = max(raw_corr_results, key=lambda x: abs(x["r"]))
981
+ arah_text = "berbanding lurus (positif)" if strongest["r"] > 0 else "berbanding terbalik (negatif)"
982
 
983
+ st.write(f"Puncak lonjakan positif (*max log return*) terjadi pada **{date_max}**, sedangkan penurunan ekstrem terjadi pada **{date_min}**.")
 
984
 
985
  if sig_models:
986
+ st.success(f"""
987
+ **Hipotesis Diterima (H1):** Ditemukan korelasi linier yang signifikan pada metode **{', '.join(sig_models)}** (*p-value* < 0.05).
988
+ Metode dengan pemetaan respons pasar terkuat adalah **{strongest['metode']}**, dengan sifat hubungan **{arah_text}**.
989
+ """)
 
990
  else:
991
+ st.warning("""
992
+ **Hipotesis Ditolak (H0 Diterima):** Tidak ditemukan bukti empiris korelasi linier yang signifikan (seluruh *p-value* >= 0.05).
993
+ Volatilitas harga cenderung dipengaruhi oleh faktor teknikal/fundamental di luar sentimen X.
994
+ """)
995
 
996
+
997
+ st.markdown('</div>', unsafe_allow_html=True)
998
 
999
+ elif analyze_batch_btn and not tweet_files:
1000
+ st.warning("⚠️ Silakan unggah minimal satu file .txt terlebih dahulu.")