rocky250 commited on
Commit
41e5d7a
Β·
verified Β·
1 Parent(s): d869d6c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +316 -181
app.py CHANGED
@@ -36,29 +36,33 @@ from charts import (
36
 
37
 
38
  CSS = """
39
- @import url('https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Syne:wght@400;600;700;800&family=IBM+Plex+Sans:wght@300;400;500&display=swap');
40
 
41
- /* Variables*/
42
  :root {
43
- --bg: #0d0f14;
44
- --card: #13161e;
45
- --border: #1e2330;
46
- --text: #e8eaf0;
47
- --dim: #5a6070;
48
- --cyan: #00d4ff;
49
- --green: #00e5a0;
50
- --red: #ff4757;
51
- --amber: #ffb347;
52
- --blue: #4a8eff;
53
- }
54
-
55
- /* Force dark everywhere β€” prevent white bleed */
 
 
 
 
56
  html, body {
57
  background: var(--bg) !important;
58
- color: var(--text) !important;
59
  margin: 0; padding: 0;
60
  }
61
- .gradio-container, #root, #app, main, .main, .wrap, .svelte-1kyws56 {
62
  background: var(--bg) !important;
63
  max-width: 100% !important;
64
  width: 100% !important;
@@ -66,125 +70,135 @@ html, body {
66
  padding: 0 1.5rem !important;
67
  box-sizing: border-box !important;
68
  }
69
- /* kill Gradio's default white blocks */
70
  .block, .wrap, .panel, .padded, div.form,
71
  div[class*="block"], div[class*="wrap"],
72
  div[class*="panel"], div[class*="gap"],
73
  .gap { background: transparent !important; border: none !important; }
74
 
75
- /* Cards / Groups ─ */
76
  .gr-group, .gr-box, .vv-section {
77
  background: var(--card) !important;
78
  border: 1px solid var(--border) !important;
79
- border-radius: 12px !important;
80
  padding: 1rem 1.25rem !important;
 
81
  }
82
 
83
- /* Tabs */
84
  .tab-nav button {
85
  background: transparent !important;
86
  border: none !important;
87
  color: var(--dim) !important;
88
- font-family: 'DM Mono', monospace !important;
89
- font-size: 0.82rem !important;
90
- letter-spacing: 0.05em !important;
 
91
  border-bottom: 2px solid transparent !important;
92
  padding: 0.5rem 1.2rem !important;
93
  transition: color 0.18s;
94
  }
95
  .tab-nav button.selected {
96
- color: var(--cyan) !important;
97
- border-bottom-color: var(--cyan) !important;
98
  }
99
  .tab-nav { border-bottom: 1px solid var(--border) !important; }
100
 
101
- /* Inputs */
102
  input[type="text"], input[type="password"], input[type="number"], textarea, select {
103
- background: #1a1d27 !important;
104
- border: 1px solid var(--border) !important;
105
  color: var(--text) !important;
106
- border-radius: 8px !important;
107
- font-family: 'DM Mono', monospace !important;
108
  font-size: 0.88rem !important;
109
  }
110
  input:focus, textarea:focus, select:focus {
111
- border-color: var(--cyan) !important;
112
- box-shadow: 0 0 0 2px rgba(0,212,255,0.15) !important;
113
  outline: none !important;
114
  }
115
  label, .gr-label, span.svelte-1b6s6s {
116
  color: var(--dim) !important;
117
- font-family: 'DM Mono', monospace !important;
118
  font-size: 0.75rem !important;
119
- letter-spacing: 0.08em !important;
120
  text-transform: uppercase;
 
121
  }
122
 
123
- /* Slider */
124
- input[type="range"] { accent-color: var(--cyan); }
125
 
126
- /* Buttons ─ */
127
  button.primary, button[variant="primary"], .primary {
128
- background: linear-gradient(135deg, var(--cyan), var(--blue)) !important;
129
  border: none !important;
130
- color: #0d0f14 !important;
131
- font-weight: 700 !important;
132
- font-family: 'DM Mono', monospace !important;
133
- border-radius: 8px !important;
134
- letter-spacing: 0.06em !important;
 
135
  }
136
  button.secondary {
137
- background: rgba(0,212,255,0.08) !important;
138
- border: 1px solid var(--cyan) !important;
139
- color: var(--cyan) !important;
140
- border-radius: 8px !important;
141
- font-family: 'DM Mono', monospace !important;
 
142
  }
143
  button:hover { opacity: 0.88; transform: translateY(-1px); transition: all 0.15s; }
144
 
145
- /* Dropdowns ─ */
146
  .dropdown, ul[role="listbox"], li[role="option"] {
147
- background: #1a1d27 !important;
148
  border-color: var(--border) !important;
149
  color: var(--text) !important;
150
  }
151
- li[role="option"]:hover { background: #242736 !important; }
152
 
153
- /* Dataframe ─ */
154
- .gr-dataframe, table { background: var(--card) !important; }
 
 
 
 
155
  .gr-dataframe th {
156
- background: #1a1d27 !important;
157
- color: var(--cyan) !important;
158
- font-family: 'DM Mono', monospace !important;
159
  font-size: 0.72rem !important;
160
- padding: 6px 10px;
161
  border-bottom: 1px solid var(--border);
162
  text-transform: uppercase;
163
- letter-spacing: 0.08em;
 
164
  }
165
  .gr-dataframe td {
166
  color: var(--text) !important;
167
- font-size: 0.77rem !important;
168
- padding: 5px 10px;
169
  border-bottom: 1px solid var(--border);
170
  }
171
- .gr-dataframe tr:hover td { background: rgba(0,212,255,0.04) !important; }
172
 
173
- /* Accordion ─ */
174
  details > summary {
175
  color: var(--dim) !important;
176
- font-family: 'DM Mono', monospace !important;
177
- font-size: 0.82rem !important;
178
  cursor: pointer;
179
  list-style: none;
 
180
  }
181
- details[open] > summary { color: var(--cyan) !important; }
182
 
183
- /* Plot containers ─ */
184
  .js-plotly-plot, .plotly { background: transparent !important; }
185
  .modebar { display: none !important; }
186
 
187
- /* Scrollbar ─ */
188
  ::-webkit-scrollbar { width: 6px; height: 6px; }
189
  ::-webkit-scrollbar-track { background: var(--bg); }
190
  ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
@@ -194,10 +208,10 @@ details[open] > summary { color: var(--cyan) !important; }
194
  /* Shared HTML component classes */
195
 
196
  .vv-hero {
197
- font-family: 'Syne', sans-serif;
198
- font-size: 1.65rem;
199
  font-weight: 800;
200
- background: linear-gradient(135deg, #00d4ff, #4a8eff);
201
  -webkit-background-clip: text;
202
  -webkit-text-fill-color: transparent;
203
  background-clip: text;
@@ -205,93 +219,185 @@ details[open] > summary { color: var(--cyan) !important; }
205
  line-height: 1.2;
206
  }
207
  .vv-section-title {
208
- font-family: 'Syne', sans-serif;
209
  font-size: 0.68rem;
210
  font-weight: 700;
211
- letter-spacing: 0.18em;
212
  text-transform: uppercase;
213
- color: #5a6070;
214
  margin-bottom: 0.5rem;
215
  margin-top: 0;
216
  }
217
- .vv-card {
218
- background: #13161e;
219
- border: 1px solid #1e2330;
220
- border-radius: 12px;
221
- padding: 1.1rem 1.3rem;
222
- margin-bottom: 0.7rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
  }
224
- .vv-stat {
 
 
 
 
 
 
 
 
225
  display: inline-block;
226
- background: #1a1d27;
227
- border: 1px solid #1e2330;
228
  border-radius: 6px;
229
- padding: 0.25rem 0.75rem;
230
- font-family: 'DM Mono', monospace;
231
- font-size: 0.77rem;
232
- color: #00d4ff;
233
- margin: 0.15rem 0.2rem;
 
 
 
 
 
 
 
 
 
 
 
234
  }
235
  .vv-badge-green {
236
  display: inline-block;
237
- background: rgba(0,229,160,0.12);
238
- border: 1px solid #00e5a0;
239
- color: #00e5a0;
240
  border-radius: 20px;
241
  padding: 0.32rem 1.1rem;
242
  font-size: 0.85rem;
243
- font-family: 'DM Mono', monospace;
244
- font-weight: 600;
245
  }
246
  .vv-badge-red {
247
  display: inline-block;
248
- background: rgba(255,71,87,0.12);
249
- border: 1px solid #ff4757;
250
- color: #ff4757;
251
  border-radius: 20px;
252
  padding: 0.32rem 1.1rem;
253
  font-size: 0.85rem;
254
- font-family: 'DM Mono', monospace;
255
- font-weight: 600;
256
  }
257
  .vv-badge-amber {
258
  display: inline-block;
259
- background: rgba(255,179,71,0.12);
260
- border: 1px solid #ffb347;
261
- color: #ffb347;
262
  border-radius: 20px;
263
  padding: 0.32rem 1.1rem;
264
  font-size: 0.85rem;
265
- font-family: 'DM Mono', monospace;
266
- font-weight: 600;
267
  }
268
  .vv-reasoning {
269
- background: #0d1119;
270
- border-left: 3px solid #ffb347;
271
  padding: 0.8rem 1rem;
272
- border-radius: 0 8px 8px 0;
273
  font-size: 0.83rem;
274
- color: #c0c4cc;
275
  line-height: 1.65;
276
- font-family: 'IBM Plex Sans', sans-serif;
277
  margin-top: 8px;
278
  }
279
- .vv-tag {
280
- display: inline-block;
281
- background: #1a1d27;
282
- border: 1px solid #1e2330;
283
- border-radius: 4px;
284
- padding: 2px 8px;
285
- font-family: 'DM Mono', monospace;
286
- font-size: 0.7rem;
287
- color: #8090a0;
288
- margin: 2px;
289
- }
290
- .vv-stat-big-green { font-family: 'DM Mono', monospace; font-size: 1.6rem; font-weight: 700; color: #00e5a0; margin: 0; }
291
- .vv-stat-big-red { font-family: 'DM Mono', monospace; font-size: 1.6rem; font-weight: 700; color: #ff4757; margin: 0; }
292
- .vv-stat-big-dim { font-family: 'DM Mono', monospace; font-size: 1.6rem; font-weight: 700; color: #5a6070; margin: 0; }
293
- .vv-log-line { font-size: 0.72rem; color: #5a6070; font-family: 'DM Mono', monospace; margin: 2px 0; }
294
- .vv-hr { border: none; border-top: 1px solid #1e2330; margin: 1.1rem 0; }
295
  """
296
 
297
 
@@ -302,12 +408,12 @@ def _empty_plotly(msg: str = "Run analysis to see data", h: int = 230):
302
  import plotly.graph_objects as go
303
  fig = go.Figure()
304
  fig.update_layout(
305
- paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)",
306
- font=dict(color="#5a6070"), margin=dict(l=10, r=10, t=10, b=10), height=h,
307
  )
308
  fig.add_annotation(
309
  text=msg, x=0.5, y=0.5, xref="paper", yref="paper",
310
- showarrow=False, font=dict(size=12, color="#5a6070"),
311
  )
312
  return fig
313
 
@@ -316,9 +422,9 @@ def _blank_outputs(status_msg: str):
316
  """19-tuple for ALL_OUTPUTS when nothing has run."""
317
  ep = _empty_plotly()
318
  return (
319
- f'<p style="color:#ff4757;font-family:DM Mono,monospace;padding:8px">{status_msg}</p>', # 0 status
320
- "<p class='vv-log-line'>β€”</p>", # 1 log
321
- "<div style='padding:3rem;text-align:center;color:#5a6070;font-family:DM Mono,monospace'>No data yet.</div>", # 2 left panel
322
  "", "", # 3 badge, 4 reasoning
323
  ep, ep, ep, # 5 modality_dist, 6 trust, 7 uncertainty
324
  ep, ep, ep, ep, # 8 donut, 9 timeline, 10 kw_bar, 11 kw_comp
@@ -432,8 +538,8 @@ def _build_outputs(
432
  ):
433
  # Status
434
  status_html = (
435
- '<p style="color:#00e5a0;font-family:DM Mono,monospace;font-size:0.82rem;padding:6px 0">'
436
- " Analysis complete</p>"
437
  )
438
 
439
  # Log
@@ -453,36 +559,66 @@ def _build_outputs(
453
  left_html = f"""
454
  {thumb_html}
455
  <a href="https://www.youtube.com/watch?v={video_id}" target="_blank"
456
- style="display:block;text-align:center;font-family:'DM Mono',monospace;
457
- font-size:0.75rem;color:#5a6070;text-decoration:none;margin:4px 0 10px">
 
458
  β–Ά Open on YouTube
459
  </a>
460
  <div class="vv-card">
461
  <p class="vv-section-title">Video</p>
462
- <p style="font-family:'Syne',sans-serif;font-size:1.05rem;font-weight:700;margin:0 0 4px;color:#e8eaf0">
463
  {meta['title']}
464
  </p>
465
- <p style="font-size:0.82rem;color:#5a6070;margin:0">
466
- by <b style="color:#b0b4c0">{meta['channel_title']}</b> Β· {meta['published_at']}
467
- </p>
 
 
 
 
 
 
 
468
  </div>
469
 
470
  <p class="vv-section-title">Metrics</p>
471
- <span class="vv-stat">πŸ‘ {meta['view_count']:,}</span>
472
- <span class="vv-stat">πŸ‘ {meta['like_count']:,}</span>
473
- <span class="vv-stat">πŸ’¬ {meta['comment_count']:,}</span>
474
- <span class="vv-stat">⏱ {meta['duration']}</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
475
 
476
- <p class="vv-section-title" style="margin-top:1rem">Tags</p>
477
- {tag_html or '<span style="color:#5a6070;font-size:0.78rem">(none)</span>'}
 
 
478
 
479
  <details style="margin-top:1rem">
480
  <summary>πŸ“„ Description</summary>
481
- <p style="font-size:0.78rem;color:#8090a0;line-height:1.65;white-space:pre-wrap;margin-top:6px">{desc_short}</p>
 
482
  </details>
483
  <details style="margin-top:0.5rem">
484
  <summary>πŸ“ Transcript ({word_count} words)</summary>
485
- <p style="font-size:0.75rem;color:#8090a0;line-height:1.65;margin-top:6px">{transcript_short}</p>
 
486
  </details>
487
  """
488
 
@@ -533,7 +669,7 @@ def _build_outputs(
533
  fig_timeline = _empty_plotly()
534
 
535
  try:
536
- fig_kw = keyword_bar(keywords, title="Top Video Keywords", color="#00d4ff")
537
  except Exception:
538
  fig_kw = _empty_plotly()
539
 
@@ -549,24 +685,24 @@ def _build_outputs(
549
  # Sentiment stat boxes (unchanged)
550
  if sent_sum:
551
  stat_pos = (
552
- f'<div class="vv-card" style="text-align:center">'
553
  f'<p class="vv-stat-big-green">{sent_sum["pos_pct"]}%</p>'
554
- f'<p style="color:#5a6070;font-size:0.75rem;margin:4px 0 0">Positively Engagement</p></div>'
555
  )
556
  stat_neg = (
557
- f'<div class="vv-card" style="text-align:center">'
558
  f'<p class="vv-stat-big-red">{sent_sum["neg_pct"]}%</p>'
559
- f'<p style="color:#5a6070;font-size:0.75rem;margin:4px 0 0">Negatively Engagement</p></div>'
560
  )
561
  stat_neu = (
562
- f'<div class="vv-card" style="text-align:center">'
563
  f'<p class="vv-stat-big-dim">{sent_sum["neu_pct"]}%</p>'
564
- f'<p style="color:#5a6070;font-size:0.75rem;margin:4px 0 0">Neutral</p></div>'
565
  )
566
  else:
567
  placeholder = (
568
- '<div class="vv-card" style="text-align:center;color:#5a6070;'
569
- 'font-family:DM Mono,monospace;font-size:0.8rem;padding:1.2rem">N/A</div>'
570
  )
571
  stat_pos = stat_neg = stat_neu = placeholder
572
 
@@ -623,19 +759,19 @@ def do_search(keyword: str):
623
  api_key = os.environ.get("YT_API_KEY", "").strip()
624
  if not api_key:
625
  return (
626
- "<p style='color:#ff4757;font-family:DM Mono,monospace'> YT_API_KEY secret not set.</p>",
627
  gr.update(choices=[], value=None, visible=False),
628
  )
629
  if not (keyword or "").strip():
630
  return (
631
- "<p style='color:#ffb347;font-family:DM Mono,monospace'>Enter a keyword to search.</p>",
632
  gr.update(choices=[], value=None, visible=False),
633
  )
634
 
635
  results = search_videos_by_title(keyword.strip(), api_key, max_results=5)
636
  if not results:
637
  return (
638
- "<p style='color:#ffb347;font-family:DM Mono,monospace'>No results found.</p>",
639
  gr.update(choices=[], value=None, visible=False),
640
  )
641
 
@@ -648,12 +784,12 @@ def do_search(keyword: str):
648
  html += (
649
  f'<div class="vv-card" style="display:flex;align-items:center;gap:12px;margin-bottom:6px">'
650
  f'<img src="{r["thumbnail_url"]}" '
651
- f' style="width:72px;height:54px;object-fit:cover;border-radius:6px;flex-shrink:0">'
652
  f'<div>'
653
- f'<p style="margin:0;font-size:0.85rem;font-weight:600;color:#e8eaf0">{r["title"][:80]}</p>'
654
- f'<p style="margin:0;font-size:0.75rem;color:#5a6070">'
655
  f'{r["channel_title"]} Β· {r["published_at"]} Β· '
656
- f'<code style="color:#00d4ff">v={vid}</code></p>'
657
  f'</div></div>'
658
  )
659
  return html, gr.update(choices=choices, value=None, visible=True)
@@ -673,21 +809,19 @@ with gr.Blocks(title="Misinformation Detection & Public Engagement ") as demo:
673
 
674
  # Header
675
  gr.HTML("""
676
- <div style="padding:1.5rem 0 0.8rem;border-bottom:1px solid #1e2330;margin-bottom:1.2rem">
677
- <h1 class="vv-hero">πŸ”¬ Misinformation Detection & Public Engagement </h1>
678
- # <p style="color:#5a6070;font-size:0.85rem;margin-top:4px;font-family:'DM Mono',monospace">
679
- # Misinformation detection
680
- # </p>
681
  </div>
682
  """)
683
 
684
  # Settings β€” NO API key field
685
  with gr.Accordion("βš™οΈ Settings", open=False):
686
  gr.HTML("""
687
- <div style="background:#0d1119;border:1px solid #1e2330;border-radius:8px;
688
- padding:0.7rem 1rem;margin-bottom:0.8rem;font-family:'DM Mono',monospace;
689
- font-size:0.78rem;color:#5a6070">
690
- πŸ”‘ YouTube API key is read from the <code style="color:#00d4ff">YT_API_KEY</code>
 
691
  Space secret β€” it is never exposed in the UI.
692
  </div>
693
  """)
@@ -724,7 +858,7 @@ with gr.Blocks(title="Misinformation Detection & Public Engagement ") as demo:
724
  gr.HTML("""
725
  <div class="vv-card" style="margin-bottom:8px">
726
  <p class="vv-section-title">Search by video title or keyword</p>
727
- <p style="font-size:0.82rem;color:#5a6070;line-height:1.6;margin:0">
728
  Upload your file, then type the title or keyword below to locate the matching YouTube entry.
729
  </p>
730
  </div>
@@ -741,7 +875,8 @@ with gr.Blocks(title="Misinformation Detection & Public Engagement ") as demo:
741
 
742
  # Status
743
  status_box = gr.HTML(
744
- '<p style="color:#5a6070;font-family:DM Mono,monospace;font-size:0.8rem;padding:6px 0">'
 
745
  "Enter a URL above and click Analyze.</p>"
746
  )
747
 
@@ -751,8 +886,8 @@ with gr.Blocks(title="Misinformation Detection & Public Engagement ") as demo:
751
  # LEFT β€” video info
752
  with gr.Column(scale=2):
753
  left_panel_html = gr.HTML(
754
- "<div style='padding:3rem;text-align:center;color:#5a6070;"
755
- "font-family:DM Mono,monospace'>No data yet.</div>"
756
  )
757
 
758
  # RIGHT β€” analytics
@@ -813,8 +948,8 @@ with gr.Blocks(title="Misinformation Detection & Public Engagement ") as demo:
813
 
814
  # Footer
815
  gr.HTML("""
816
- <div style="margin-top:2rem;padding-top:1rem;border-top:1px solid #1e2330;
817
- text-align:center;font-family:'DM Mono',monospace;font-size:0.72rem;color:#3a3f50">
818
  4-stream SeTa-Attention BiGRU Β· CCM / DMTE / Uncertainty Fusion Β·
819
  Test ROC-AUC 0.967
820
  </div>
@@ -869,8 +1004,8 @@ if __name__ == "__main__":
869
  demo.launch(
870
  css=CSS,
871
  theme=gr.themes.Base(
872
- primary_hue=gr.themes.colors.cyan,
873
- neutral_hue=gr.themes.colors.gray,
874
- font=[gr.themes.GoogleFont("IBM Plex Sans"), "sans-serif"],
875
  ),
876
- )
 
36
 
37
 
38
  CSS = """
39
+ @import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500;600&family=Nunito:wght@400;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');
40
 
41
+ /* Variables β€” Light Professional Theme */
42
  :root {
43
+ --bg: #f4f7fb;
44
+ --card: #ffffff;
45
+ --border: #dde3ed;
46
+ --text: #1e293b;
47
+ --dim: #7b7b7b;
48
+ --primary: #269ccc;
49
+ --algae: #9ed2c5;
50
+ --green: #16a34a;
51
+ --red: #dc2626;
52
+ --amber: #d97706;
53
+ --primary-light: #e8f5fc;
54
+ --algae-light: #e8f6f3;
55
+ --shadow-sm: 0 1px 3px rgba(0,0,0,0.07), 0 1px 2px rgba(0,0,0,0.04);
56
+ --shadow-md: 0 4px 12px rgba(38,156,204,0.12), 0 1px 3px rgba(0,0,0,0.06);
57
+ }
58
+
59
+ /* Base */
60
  html, body {
61
  background: var(--bg) !important;
62
+ color: var(--text) !important;
63
  margin: 0; padding: 0;
64
  }
65
+ .gradio-container, #root, #app, main, .main, .wrap {
66
  background: var(--bg) !important;
67
  max-width: 100% !important;
68
  width: 100% !important;
 
70
  padding: 0 1.5rem !important;
71
  box-sizing: border-box !important;
72
  }
 
73
  .block, .wrap, .panel, .padded, div.form,
74
  div[class*="block"], div[class*="wrap"],
75
  div[class*="panel"], div[class*="gap"],
76
  .gap { background: transparent !important; border: none !important; }
77
 
78
+ /* Cards / Groups */
79
  .gr-group, .gr-box, .vv-section {
80
  background: var(--card) !important;
81
  border: 1px solid var(--border) !important;
82
+ border-radius: 14px !important;
83
  padding: 1rem 1.25rem !important;
84
+ box-shadow: var(--shadow-sm) !important;
85
  }
86
 
87
+ /* Tabs */
88
  .tab-nav button {
89
  background: transparent !important;
90
  border: none !important;
91
  color: var(--dim) !important;
92
+ font-family: 'DM Sans', sans-serif !important;
93
+ font-size: 0.84rem !important;
94
+ font-weight: 500 !important;
95
+ letter-spacing: 0.02em !important;
96
  border-bottom: 2px solid transparent !important;
97
  padding: 0.5rem 1.2rem !important;
98
  transition: color 0.18s;
99
  }
100
  .tab-nav button.selected {
101
+ color: var(--primary) !important;
102
+ border-bottom-color: var(--primary) !important;
103
  }
104
  .tab-nav { border-bottom: 1px solid var(--border) !important; }
105
 
106
+ /* Inputs */
107
  input[type="text"], input[type="password"], input[type="number"], textarea, select {
108
+ background: #f8fafc !important;
109
+ border: 1.5px solid var(--border) !important;
110
  color: var(--text) !important;
111
+ border-radius: 10px !important;
112
+ font-family: 'DM Sans', sans-serif !important;
113
  font-size: 0.88rem !important;
114
  }
115
  input:focus, textarea:focus, select:focus {
116
+ border-color: var(--primary) !important;
117
+ box-shadow: 0 0 0 3px rgba(38,156,204,0.15) !important;
118
  outline: none !important;
119
  }
120
  label, .gr-label, span.svelte-1b6s6s {
121
  color: var(--dim) !important;
122
+ font-family: 'DM Sans', sans-serif !important;
123
  font-size: 0.75rem !important;
124
+ letter-spacing: 0.05em !important;
125
  text-transform: uppercase;
126
+ font-weight: 600 !important;
127
  }
128
 
129
+ /* Slider */
130
+ input[type="range"] { accent-color: var(--primary); }
131
 
132
+ /* Buttons */
133
  button.primary, button[variant="primary"], .primary {
134
+ background: linear-gradient(135deg, var(--primary), #1a7faa) !important;
135
  border: none !important;
136
+ color: #ffffff !important;
137
+ font-weight: 600 !important;
138
+ font-family: 'DM Sans', sans-serif !important;
139
+ border-radius: 10px !important;
140
+ letter-spacing: 0.03em !important;
141
+ box-shadow: 0 2px 8px rgba(38,156,204,0.35) !important;
142
  }
143
  button.secondary {
144
+ background: var(--primary-light) !important;
145
+ border: 1.5px solid var(--primary) !important;
146
+ color: var(--primary) !important;
147
+ border-radius: 10px !important;
148
+ font-family: 'DM Sans', sans-serif !important;
149
+ font-weight: 500 !important;
150
  }
151
  button:hover { opacity: 0.88; transform: translateY(-1px); transition: all 0.15s; }
152
 
153
+ /* Dropdowns */
154
  .dropdown, ul[role="listbox"], li[role="option"] {
155
+ background: #ffffff !important;
156
  border-color: var(--border) !important;
157
  color: var(--text) !important;
158
  }
159
+ li[role="option"]:hover { background: var(--primary-light) !important; }
160
 
161
+ /* Dataframe */
162
+ .gr-dataframe, table {
163
+ background: var(--card) !important;
164
+ border-radius: 10px !important;
165
+ overflow: hidden;
166
+ }
167
  .gr-dataframe th {
168
+ background: var(--primary-light) !important;
169
+ color: var(--primary) !important;
170
+ font-family: 'DM Sans', sans-serif !important;
171
  font-size: 0.72rem !important;
172
+ padding: 8px 12px;
173
  border-bottom: 1px solid var(--border);
174
  text-transform: uppercase;
175
+ letter-spacing: 0.07em;
176
+ font-weight: 700;
177
  }
178
  .gr-dataframe td {
179
  color: var(--text) !important;
180
+ font-size: 0.8rem !important;
181
+ padding: 7px 12px;
182
  border-bottom: 1px solid var(--border);
183
  }
184
+ .gr-dataframe tr:hover td { background: var(--primary-light) !important; }
185
 
186
+ /* Accordion */
187
  details > summary {
188
  color: var(--dim) !important;
189
+ font-family: 'DM Sans', sans-serif !important;
190
+ font-size: 0.84rem !important;
191
  cursor: pointer;
192
  list-style: none;
193
+ font-weight: 500;
194
  }
195
+ details[open] > summary { color: var(--primary) !important; }
196
 
197
+ /* Plot containers */
198
  .js-plotly-plot, .plotly { background: transparent !important; }
199
  .modebar { display: none !important; }
200
 
201
+ /* Scrollbar */
202
  ::-webkit-scrollbar { width: 6px; height: 6px; }
203
  ::-webkit-scrollbar-track { background: var(--bg); }
204
  ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
 
208
  /* Shared HTML component classes */
209
 
210
  .vv-hero {
211
+ font-family: 'Nunito', sans-serif;
212
+ font-size: 1.7rem;
213
  font-weight: 800;
214
+ background: linear-gradient(135deg, #269ccc, #9ed2c5);
215
  -webkit-background-clip: text;
216
  -webkit-text-fill-color: transparent;
217
  background-clip: text;
 
219
  line-height: 1.2;
220
  }
221
  .vv-section-title {
222
+ font-family: 'DM Sans', sans-serif;
223
  font-size: 0.68rem;
224
  font-weight: 700;
225
+ letter-spacing: 0.16em;
226
  text-transform: uppercase;
227
+ color: var(--dim, #7b7b7b);
228
  margin-bottom: 0.5rem;
229
  margin-top: 0;
230
  }
231
+
232
+ /* Stylish interactive metric cards */
233
+ .vv-metric-grid {
234
+ display: grid;
235
+ grid-template-columns: repeat(2, 1fr);
236
+ gap: 10px;
237
+ margin: 0.5rem 0 1rem;
238
+ }
239
+ .vv-metric-card {
240
+ background: #ffffff;
241
+ border: 1.5px solid #dde3ed;
242
+ border-radius: 14px;
243
+ padding: 1rem 0.85rem;
244
+ text-align: center;
245
+ cursor: default;
246
+ transition: all 0.22s ease;
247
+ box-shadow: 0 1px 3px rgba(0,0,0,0.05);
248
+ position: relative;
249
+ overflow: hidden;
250
+ }
251
+ .vv-metric-card::before {
252
+ content: '';
253
+ position: absolute;
254
+ top: 0; left: 0; right: 0;
255
+ height: 3px;
256
+ background: linear-gradient(90deg, #269ccc, #9ed2c5);
257
+ opacity: 0;
258
+ transition: opacity 0.22s ease;
259
+ }
260
+ .vv-metric-card:hover {
261
+ transform: translateY(-3px);
262
+ box-shadow: 0 8px 22px rgba(38,156,204,0.18);
263
+ border-color: #269ccc;
264
+ }
265
+ .vv-metric-card:hover::before { opacity: 1; }
266
+ .vv-metric-icon {
267
+ font-size: 1.4rem;
268
+ margin-bottom: 4px;
269
+ display: block;
270
+ }
271
+ .vv-metric-value {
272
+ font-family: 'Nunito', sans-serif;
273
+ font-size: 1.3rem;
274
+ font-weight: 800;
275
+ color: #269ccc;
276
+ margin: 0;
277
+ line-height: 1.2;
278
+ }
279
+ .vv-metric-label {
280
+ font-family: 'DM Sans', sans-serif;
281
+ font-size: 0.68rem;
282
+ font-weight: 600;
283
+ letter-spacing: 0.1em;
284
+ text-transform: uppercase;
285
+ color: #7b7b7b;
286
+ margin-top: 2px;
287
+ }
288
+
289
+ /* Video info grid */
290
+ .vv-info-grid {
291
+ display: grid;
292
+ grid-template-columns: 1fr 1fr;
293
+ gap: 8px;
294
+ margin: 0.5rem 0;
295
+ }
296
+ .vv-info-item {
297
+ background: #f4f7fb;
298
+ border: 1px solid #dde3ed;
299
+ border-radius: 9px;
300
+ padding: 0.55rem 0.75rem;
301
+ }
302
+ .vv-info-label {
303
+ display: block;
304
+ font-family: 'DM Sans', sans-serif;
305
+ font-size: 0.62rem;
306
+ font-weight: 700;
307
+ letter-spacing: 0.12em;
308
+ text-transform: uppercase;
309
+ color: #7b7b7b;
310
+ margin-bottom: 2px;
311
+ }
312
+ .vv-info-value {
313
+ display: block;
314
+ font-family: 'DM Sans', sans-serif;
315
+ font-size: 0.82rem;
316
+ font-weight: 600;
317
+ color: #1e293b;
318
+ white-space: nowrap;
319
+ overflow: hidden;
320
+ text-overflow: ellipsis;
321
  }
322
+
323
+ /* Tags grid */
324
+ .vv-tags-grid {
325
+ display: flex;
326
+ flex-wrap: wrap;
327
+ gap: 5px;
328
+ margin-top: 0.4rem;
329
+ }
330
+ .vv-tag {
331
  display: inline-block;
332
+ background: #e8f5fc;
333
+ border: 1px solid #b8dff0;
334
  border-radius: 6px;
335
+ padding: 2px 10px;
336
+ font-family: 'DM Sans', sans-serif;
337
+ font-size: 0.7rem;
338
+ font-weight: 500;
339
+ color: #1a7faa;
340
+ transition: background 0.15s;
341
+ }
342
+ .vv-tag:hover { background: #cceaf8; }
343
+
344
+ .vv-card {
345
+ background: #ffffff;
346
+ border: 1px solid #dde3ed;
347
+ border-radius: 12px;
348
+ padding: 1rem 1.1rem;
349
+ margin-bottom: 0.7rem;
350
+ box-shadow: 0 1px 3px rgba(0,0,0,0.05);
351
  }
352
  .vv-badge-green {
353
  display: inline-block;
354
+ background: #dcfce7;
355
+ border: 1.5px solid #16a34a;
356
+ color: #15803d;
357
  border-radius: 20px;
358
  padding: 0.32rem 1.1rem;
359
  font-size: 0.85rem;
360
+ font-family: 'DM Sans', sans-serif;
361
+ font-weight: 700;
362
  }
363
  .vv-badge-red {
364
  display: inline-block;
365
+ background: #fee2e2;
366
+ border: 1.5px solid #dc2626;
367
+ color: #b91c1c;
368
  border-radius: 20px;
369
  padding: 0.32rem 1.1rem;
370
  font-size: 0.85rem;
371
+ font-family: 'DM Sans', sans-serif;
372
+ font-weight: 700;
373
  }
374
  .vv-badge-amber {
375
  display: inline-block;
376
+ background: #fef3c7;
377
+ border: 1.5px solid #d97706;
378
+ color: #b45309;
379
  border-radius: 20px;
380
  padding: 0.32rem 1.1rem;
381
  font-size: 0.85rem;
382
+ font-family: 'DM Sans', sans-serif;
383
+ font-weight: 700;
384
  }
385
  .vv-reasoning {
386
+ background: #fefce8;
387
+ border-left: 3px solid #d97706;
388
  padding: 0.8rem 1rem;
389
+ border-radius: 0 10px 10px 0;
390
  font-size: 0.83rem;
391
+ color: #78350f;
392
  line-height: 1.65;
393
+ font-family: 'DM Sans', sans-serif;
394
  margin-top: 8px;
395
  }
396
+ .vv-stat-big-green { font-family: 'Nunito', sans-serif; font-size: 1.6rem; font-weight: 800; color: #16a34a; margin: 0; }
397
+ .vv-stat-big-red { font-family: 'Nunito', sans-serif; font-size: 1.6rem; font-weight: 800; color: #dc2626; margin: 0; }
398
+ .vv-stat-big-dim { font-family: 'Nunito', sans-serif; font-size: 1.6rem; font-weight: 800; color: #7b7b7b; margin: 0; }
399
+ .vv-log-line { font-size: 0.72rem; color: #7b7b7b; font-family: 'JetBrains Mono', monospace; margin: 2px 0; }
400
+ .vv-hr { border: none; border-top: 1px solid #dde3ed; margin: 1.1rem 0; }
 
 
 
 
 
 
 
 
 
 
 
401
  """
402
 
403
 
 
408
  import plotly.graph_objects as go
409
  fig = go.Figure()
410
  fig.update_layout(
411
+ paper_bgcolor="rgba(255,255,255,0)", plot_bgcolor="rgba(248,250,252,0.6)",
412
+ font=dict(color="#7b7b7b"), margin=dict(l=10, r=10, t=10, b=10), height=h,
413
  )
414
  fig.add_annotation(
415
  text=msg, x=0.5, y=0.5, xref="paper", yref="paper",
416
+ showarrow=False, font=dict(size=12, color="#7b7b7b"),
417
  )
418
  return fig
419
 
 
422
  """19-tuple for ALL_OUTPUTS when nothing has run."""
423
  ep = _empty_plotly()
424
  return (
425
+ f'<p style="color:#dc2626;font-family:DM Sans,sans-serif;padding:8px;font-weight:500">{status_msg}</p>', # 0 status
426
+ "<p class='vv-log-line'>β€”</p>", # 1 log
427
+ "<div style='padding:3rem;text-align:center;color:#7b7b7b;font-family:DM Sans,sans-serif'>No data yet.</div>", # 2 left panel
428
  "", "", # 3 badge, 4 reasoning
429
  ep, ep, ep, # 5 modality_dist, 6 trust, 7 uncertainty
430
  ep, ep, ep, ep, # 8 donut, 9 timeline, 10 kw_bar, 11 kw_comp
 
538
  ):
539
  # Status
540
  status_html = (
541
+ '<p style="color:#16a34a;font-family:DM Sans,sans-serif;font-size:0.85rem;'
542
+ 'font-weight:600;padding:6px 0"> Analysis complete</p>'
543
  )
544
 
545
  # Log
 
559
  left_html = f"""
560
  {thumb_html}
561
  <a href="https://www.youtube.com/watch?v={video_id}" target="_blank"
562
+ style="display:block;text-align:center;font-family:'DM Sans',sans-serif;
563
+ font-size:0.78rem;color:#269ccc;text-decoration:none;margin:4px 0 10px;
564
+ font-weight:600;letter-spacing:0.03em">
565
  β–Ά Open on YouTube
566
  </a>
567
  <div class="vv-card">
568
  <p class="vv-section-title">Video</p>
569
+ <p style="font-family:'Nunito',sans-serif;font-size:1.05rem;font-weight:800;margin:0 0 6px;color:#1e293b;line-height:1.35">
570
  {meta['title']}
571
  </p>
572
+ <div class="vv-info-grid">
573
+ <div class="vv-info-item">
574
+ <span class="vv-info-label">Author</span>
575
+ <span class="vv-info-value" title="{meta['channel_title']}">{meta['channel_title']}</span>
576
+ </div>
577
+ <div class="vv-info-item">
578
+ <span class="vv-info-label">Published</span>
579
+ <span class="vv-info-value">{meta['published_at']}</span>
580
+ </div>
581
+ </div>
582
  </div>
583
 
584
  <p class="vv-section-title">Metrics</p>
585
+ <div class="vv-metric-grid">
586
+ <div class="vv-metric-card">
587
+ <span class="vv-metric-icon">πŸ‘</span>
588
+ <div class="vv-metric-value">{meta['view_count']:,}</div>
589
+ <div class="vv-metric-label">Views</div>
590
+ </div>
591
+ <div class="vv-metric-card">
592
+ <span class="vv-metric-icon">πŸ‘</span>
593
+ <div class="vv-metric-value">{meta['like_count']:,}</div>
594
+ <div class="vv-metric-label">Likes</div>
595
+ </div>
596
+ <div class="vv-metric-card">
597
+ <span class="vv-metric-icon">πŸ’¬</span>
598
+ <div class="vv-metric-value">{meta['comment_count']:,}</div>
599
+ <div class="vv-metric-label">Comments</div>
600
+ </div>
601
+ <div class="vv-metric-card">
602
+ <span class="vv-metric-icon">⏱</span>
603
+ <div class="vv-metric-value" style="font-size:1.05rem">{meta['duration']}</div>
604
+ <div class="vv-metric-label">Duration</div>
605
+ </div>
606
+ </div>
607
 
608
+ <p class="vv-section-title" style="margin-top:0.8rem">Tags</p>
609
+ <div class="vv-tags-grid">
610
+ {tag_html or '<span style="color:#7b7b7b;font-size:0.78rem">(none)</span>'}
611
+ </div>
612
 
613
  <details style="margin-top:1rem">
614
  <summary>πŸ“„ Description</summary>
615
+ <p style="font-size:0.79rem;color:#4a5568;line-height:1.7;white-space:pre-wrap;margin-top:8px;
616
+ background:#f4f7fb;border-radius:8px;padding:0.7rem">{desc_short}</p>
617
  </details>
618
  <details style="margin-top:0.5rem">
619
  <summary>πŸ“ Transcript ({word_count} words)</summary>
620
+ <p style="font-size:0.76rem;color:#4a5568;line-height:1.7;margin-top:8px;
621
+ background:#f4f7fb;border-radius:8px;padding:0.7rem">{transcript_short}</p>
622
  </details>
623
  """
624
 
 
669
  fig_timeline = _empty_plotly()
670
 
671
  try:
672
+ fig_kw = keyword_bar(keywords, title="Top Video Keywords", color="#269ccc")
673
  except Exception:
674
  fig_kw = _empty_plotly()
675
 
 
685
  # Sentiment stat boxes (unchanged)
686
  if sent_sum:
687
  stat_pos = (
688
+ f'<div class="vv-card" style="text-align:center;border-top:3px solid #16a34a">'
689
  f'<p class="vv-stat-big-green">{sent_sum["pos_pct"]}%</p>'
690
+ f'<p style="color:#7b7b7b;font-size:0.75rem;margin:4px 0 0;font-family:DM Sans,sans-serif;font-weight:600">Positively Engagement</p></div>'
691
  )
692
  stat_neg = (
693
+ f'<div class="vv-card" style="text-align:center;border-top:3px solid #dc2626">'
694
  f'<p class="vv-stat-big-red">{sent_sum["neg_pct"]}%</p>'
695
+ f'<p style="color:#7b7b7b;font-size:0.75rem;margin:4px 0 0;font-family:DM Sans,sans-serif;font-weight:600">Negatively Engagement</p></div>'
696
  )
697
  stat_neu = (
698
+ f'<div class="vv-card" style="text-align:center;border-top:3px solid #7b7b7b">'
699
  f'<p class="vv-stat-big-dim">{sent_sum["neu_pct"]}%</p>'
700
+ f'<p style="color:#7b7b7b;font-size:0.75rem;margin:4px 0 0;font-family:DM Sans,sans-serif;font-weight:600">Neutral</p></div>'
701
  )
702
  else:
703
  placeholder = (
704
+ '<div class="vv-card" style="text-align:center;color:#7b7b7b;'
705
+ 'font-family:DM Sans,sans-serif;font-size:0.8rem;padding:1.2rem">N/A</div>'
706
  )
707
  stat_pos = stat_neg = stat_neu = placeholder
708
 
 
759
  api_key = os.environ.get("YT_API_KEY", "").strip()
760
  if not api_key:
761
  return (
762
+ "<p style='color:#dc2626;font-family:DM Sans,sans-serif;font-weight:500'> YT_API_KEY secret not set.</p>",
763
  gr.update(choices=[], value=None, visible=False),
764
  )
765
  if not (keyword or "").strip():
766
  return (
767
+ "<p style='color:#d97706;font-family:DM Sans,sans-serif;font-weight:500'>Enter a keyword to search.</p>",
768
  gr.update(choices=[], value=None, visible=False),
769
  )
770
 
771
  results = search_videos_by_title(keyword.strip(), api_key, max_results=5)
772
  if not results:
773
  return (
774
+ "<p style='color:#d97706;font-family:DM Sans,sans-serif;font-weight:500'>No results found.</p>",
775
  gr.update(choices=[], value=None, visible=False),
776
  )
777
 
 
784
  html += (
785
  f'<div class="vv-card" style="display:flex;align-items:center;gap:12px;margin-bottom:6px">'
786
  f'<img src="{r["thumbnail_url"]}" '
787
+ f' style="width:72px;height:54px;object-fit:cover;border-radius:8px;flex-shrink:0">'
788
  f'<div>'
789
+ f'<p style="margin:0;font-size:0.85rem;font-weight:700;color:#1e293b">{r["title"][:80]}</p>'
790
+ f'<p style="margin:0;font-size:0.75rem;color:#7b7b7b">'
791
  f'{r["channel_title"]} Β· {r["published_at"]} Β· '
792
+ f'<code style="color:#269ccc;background:#e8f5fc;padding:1px 4px;border-radius:4px">v={vid}</code></p>'
793
  f'</div></div>'
794
  )
795
  return html, gr.update(choices=choices, value=None, visible=True)
 
809
 
810
  # Header
811
  gr.HTML("""
812
+ <div style="padding:1.5rem 0 0.8rem;border-bottom:1.5px solid #dde3ed;margin-bottom:1.2rem">
813
+ <h1 class="vv-hero">πŸ”¬ Misinformation Detection & Public Engagement</h1>
 
 
 
814
  </div>
815
  """)
816
 
817
  # Settings β€” NO API key field
818
  with gr.Accordion("βš™οΈ Settings", open=False):
819
  gr.HTML("""
820
+ <div style="background:#e8f5fc;border:1px solid #b8dff0;border-radius:10px;
821
+ padding:0.7rem 1rem;margin-bottom:0.8rem;font-family:'DM Sans',sans-serif;
822
+ font-size:0.78rem;color:#1a7faa;font-weight:500">
823
+ πŸ”‘ YouTube API key is read from the <code style="color:#269ccc;background:#d0ebf7;
824
+ padding:1px 5px;border-radius:4px">YT_API_KEY</code>
825
  Space secret β€” it is never exposed in the UI.
826
  </div>
827
  """)
 
858
  gr.HTML("""
859
  <div class="vv-card" style="margin-bottom:8px">
860
  <p class="vv-section-title">Search by video title or keyword</p>
861
+ <p style="font-size:0.82rem;color:#7b7b7b;line-height:1.6;margin:0">
862
  Upload your file, then type the title or keyword below to locate the matching YouTube entry.
863
  </p>
864
  </div>
 
875
 
876
  # Status
877
  status_box = gr.HTML(
878
+ '<p style="color:#7b7b7b;font-family:DM Sans,sans-serif;font-size:0.82rem;'
879
+ 'font-weight:500;padding:6px 0">'
880
  "Enter a URL above and click Analyze.</p>"
881
  )
882
 
 
886
  # LEFT β€” video info
887
  with gr.Column(scale=2):
888
  left_panel_html = gr.HTML(
889
+ "<div style='padding:3rem;text-align:center;color:#7b7b7b;"
890
+ "font-family:DM Sans,sans-serif'>No data yet.</div>"
891
  )
892
 
893
  # RIGHT β€” analytics
 
948
 
949
  # Footer
950
  gr.HTML("""
951
+ <div style="margin-top:2rem;padding-top:1rem;border-top:1px solid #dde3ed;
952
+ text-align:center;font-family:'DM Sans',sans-serif;font-size:0.72rem;color:#9ed2c5">
953
  4-stream SeTa-Attention BiGRU Β· CCM / DMTE / Uncertainty Fusion Β·
954
  Test ROC-AUC 0.967
955
  </div>
 
1004
  demo.launch(
1005
  css=CSS,
1006
  theme=gr.themes.Base(
1007
+ primary_hue=gr.themes.colors.sky,
1008
+ neutral_hue=gr.themes.colors.slate,
1009
+ font=[gr.themes.GoogleFont("DM Sans"), "sans-serif"],
1010
  ),
1011
+ )