maya369 commited on
Commit
9f95eb5
Β·
1 Parent(s): edb62bf

feat: update source labels, fix news feed

Browse files
Files changed (5) hide show
  1. app/ai_panel.py +123 -49
  2. app/analysis_panel.py +1 -1
  3. app/dashboard.py +33 -20
  4. app/intel_panel.py +2 -2
  5. app/legend.py +5 -5
app/ai_panel.py CHANGED
@@ -120,6 +120,7 @@ When the user asks a question:
120
  - "y": column for y-axis (string or list)
121
  - "by": column to color-group by (optional)
122
  - "title": chart title
 
123
  - "show_table": bool β€” whether to show raw data table
124
 
125
  Rules:
@@ -130,6 +131,7 @@ Rules:
130
  - "by" only when grouping adds value.
131
  - For tickers: default line chart with x="date", y="close", by="symbol" if multiple.
132
  - Use "search" for news, policy, sanctions, geopolitical updates.
 
133
  - Respond ONLY with the JSON object, nothing else.
134
  """
135
 
@@ -215,7 +217,7 @@ def _make_geo_map(df: pd.DataFrame, spec: dict) -> pn.viewable.Viewable:
215
  "tiles = gv.tile_sources.CartoDark()\n"
216
  "map_ = (tiles * points).opts(projection=crs.GOOGLE_MERCATOR)"
217
  )
218
- return _wrap_with_code(pn.pane.HoloViews(chart, sizing_mode="stretch_both", min_height=420), code)
219
 
220
 
221
  def _make_chart(df: pd.DataFrame, spec: dict) -> pn.viewable.Viewable:
@@ -226,7 +228,15 @@ def _make_chart(df: pd.DataFrame, spec: dict) -> pn.viewable.Viewable:
226
  y = spec.get("y")
227
  by = spec.get("by")
228
 
229
- # Route geographic data to GeoViews
 
 
 
 
 
 
 
 
230
  if kind == "map" or ({"lat", "lon"}.issubset(df.columns) and kind in ("scatter", "points", "map")):
231
  return _make_geo_map(df, spec)
232
 
@@ -246,21 +256,73 @@ def _make_chart(df: pd.DataFrame, spec: dict) -> pn.viewable.Viewable:
246
  plot_fn = getattr(df.hvplot, kind, df.hvplot.line)
247
  chart = plot_fn(**kwargs)
248
  code = _hvplot_code("df", kind, kwargs)
249
- return _wrap_with_code(pn.pane.HoloViews(chart, sizing_mode="stretch_both", min_height=400), code)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
 
251
 
252
  def _wrap_with_code(chart_pane: pn.viewable.Viewable, code: str) -> pn.viewable.Viewable:
253
- """Wrap a chart with a collapsible HoloViz code block beneath it."""
254
- code_block = pn.pane.Markdown(
255
- f"```python\n{code}\n```",
256
- sizing_mode="stretch_width",
 
 
 
 
257
  )
258
- accordion = pn.Accordion(
259
- ("πŸ“‹ View HoloViz Code", code_block),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
  sizing_mode="stretch_width",
261
- styles={"border": f"1px solid {_BORDER}", "border-radius": "4px"},
262
  )
263
- return pn.Column(chart_pane, accordion, sizing_mode="stretch_both")
264
 
265
 
266
  def _make_multi_chart(df: pd.DataFrame, specs: list[dict]) -> pn.viewable.Viewable:
@@ -275,7 +337,7 @@ def _make_multi_chart(df: pd.DataFrame, specs: list[dict]) -> pn.viewable.Viewab
275
  return pn.pane.Markdown("*Could not render charts.*")
276
  if len(panels) == 1:
277
  return panels[0]
278
- return pn.GridBox(*panels, ncols=2, sizing_mode="stretch_both")
279
 
280
 
281
  def _make_table(df: pd.DataFrame) -> pn.widgets.Tabulator:
@@ -297,19 +359,53 @@ def _make_table(df: pd.DataFrame) -> pn.widgets.Tabulator:
297
  _history: list[dict] = []
298
 
299
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
300
  async def _chat_callback(contents: str, user: str, instance: pn.chat.ChatInterface):
301
  con = _init_db()
302
  _history.append({"role": "user", "text": contents})
303
 
 
 
 
 
 
 
 
 
304
  try:
305
- result = _ask_llm(contents, _history)
306
  except json.JSONDecodeError:
 
307
  yield "I had trouble parsing the response. Could you rephrase your question?"
308
  return
309
  except Exception as e:
 
310
  yield f"**Error contacting AI:** {e}"
311
  return
312
 
 
 
313
  answer = result.get("answer", "")
314
  sql = result.get("sql")
315
  tickers = result.get("tickers") or ([result["ticker"]] if result.get("ticker") else None)
@@ -408,7 +504,7 @@ _SUGGESTIONS = [
408
  "Show the top 10 most recent critical events",
409
  "Plot Apple vs Microsoft",
410
  "What's the latest news on oil prices?",
411
- "Which market is up the most today?",
412
  ]
413
 
414
 
@@ -419,7 +515,7 @@ def _build_sidebar() -> pn.Column:
419
 
420
  def _label(text: str) -> pn.pane.HTML:
421
  return pn.pane.HTML(
422
- f'<div style="font-size:10px;font-weight:bold;color:{_ACCENT};'
423
  f'letter-spacing:2px;text-transform:uppercase;'
424
  f'font-family:\'Courier New\',monospace;margin-bottom:4px;">{text}</div>',
425
  sizing_mode="stretch_width",
@@ -427,7 +523,7 @@ def _build_sidebar() -> pn.Column:
427
 
428
  def _hint(text: str) -> pn.pane.HTML:
429
  return pn.pane.HTML(
430
- f'<div style="font-size:10px;color:{_MUTED};line-height:1.5;margin-top:4px;">{text}</div>',
431
  sizing_mode="stretch_width",
432
  )
433
 
@@ -473,28 +569,6 @@ def _build_sidebar() -> pn.Column:
473
 
474
  model_select.param.watch(_on_model, "value")
475
 
476
- capabilities = pn.pane.HTML(
477
- f"""
478
- <div style="font-size:11px;color:#94a3b8;line-height:1.8;">
479
- <b style="color:{_ACCENT};">Data sources:</b><br>
480
- β€’ Risk events (ACLED, FIRMS…)<br>
481
- β€’ Commodities history<br>
482
- β€’ Currency FX rates<br>
483
- β€’ Market prices<br>
484
- β€’ Live stocks via Yahoo Finance<br>
485
- β€’ Web search via Google<br><br>
486
- <b style="color:{_ACCENT};">Chart types:</b><br>
487
- β€’ Line, bar, scatter, area<br>
488
- β€’ GeoViews maps (lat/lon data)<br>
489
- β€’ Multi-chart grid layouts<br><br>
490
- <b style="color:{_ACCENT};">HoloViz code:</b><br>
491
- Every chart shows the code<br>
492
- that generated it.
493
- </div>
494
- """,
495
- sizing_mode="stretch_width",
496
- )
497
-
498
  get_key_link = pn.pane.HTML(
499
  f'<a href="https://aistudio.google.com/apikey" target="_blank" '
500
  f'style="font-size:10px;color:{_ACCENT};text-decoration:none;">'
@@ -511,8 +585,6 @@ def _build_sidebar() -> pn.Column:
511
  _label("Model"),
512
  model_select,
513
  _divider(),
514
- _label("Capabilities"),
515
- capabilities,
516
  pn.Spacer(),
517
  width=220,
518
  sizing_mode="stretch_height",
@@ -531,24 +603,26 @@ def build_ai_tab() -> pn.viewable.Viewable:
531
 
532
  chat = pn.chat.ChatInterface(
533
  callback=_chat_callback,
534
- callback_user="HoloIntel AI",
535
  show_rerun=False,
536
  show_undo=True,
537
  show_clear=True,
538
- placeholder_text="Ask about risk events, commodities, stocks, or world news...",
539
  sizing_mode="stretch_both",
540
  min_height=600,
541
  callback_exception="verbose",
 
 
 
 
 
 
 
542
  )
543
 
544
  chat.send(
545
- pn.pane.Markdown(
546
- "**Welcome to HoloIntel AI**\n\n"
547
- "I can query risk events, commodities, currencies, live stocks, and search the web. "
548
- "Charts include the HoloViz code that generated them.\n\n"
549
- + "\n".join(f"- *{s}*" for s in _SUGGESTIONS),
550
- ),
551
- user="HoloIntel AI",
552
  respond=False,
553
  )
554
 
 
120
  - "y": column for y-axis (string or list)
121
  - "by": column to color-group by (optional)
122
  - "title": chart title
123
+ - "filter": dict | null β€” row filter applied before charting, e.g. {{"commodity": "Gold"}}
124
  - "show_table": bool β€” whether to show raw data table
125
 
126
  Rules:
 
131
  - "by" only when grouping adds value.
132
  - For tickers: default line chart with x="date", y="close", by="symbol" if multiple.
133
  - Use "search" for news, policy, sanctions, geopolitical updates.
134
+ - IMPORTANT: When comparing commodities or assets with very different price scales (e.g. Gold ~$2000 vs Oil ~$70), return SEPARATE chart specs for each β€” one chart per commodity β€” so each renders with its own y-axis scale. Never combine them into a single chart with "by", as the smaller-scale asset becomes invisible. Use the "filter" field on each chart spec to slice the data, e.g. {{"commodity": "Gold"}} and {{"commodity": "Global Oil Price"}}.
135
  - Respond ONLY with the JSON object, nothing else.
136
  """
137
 
 
217
  "tiles = gv.tile_sources.CartoDark()\n"
218
  "map_ = (tiles * points).opts(projection=crs.GOOGLE_MERCATOR)"
219
  )
220
+ return _wrap_with_code(pn.pane.HoloViews(chart, sizing_mode="stretch_width", height=420, linked_axes=False), code)
221
 
222
 
223
  def _make_chart(df: pd.DataFrame, spec: dict) -> pn.viewable.Viewable:
 
228
  y = spec.get("y")
229
  by = spec.get("by")
230
 
231
+ # Apply row filter before plotting (e.g. {"commodity": "Gold"})
232
+ row_filter = spec.get("filter")
233
+ if row_filter and isinstance(row_filter, dict):
234
+ df = df.copy()
235
+ for col, val in row_filter.items():
236
+ if col in df.columns:
237
+ df = df[df[col] == val]
238
+
239
+ # Route geographic data to GeoViews (pass already-filtered df)
240
  if kind == "map" or ({"lat", "lon"}.issubset(df.columns) and kind in ("scatter", "points", "map")):
241
  return _make_geo_map(df, spec)
242
 
 
256
  plot_fn = getattr(df.hvplot, kind, df.hvplot.line)
257
  chart = plot_fn(**kwargs)
258
  code = _hvplot_code("df", kind, kwargs)
259
+ return _wrap_with_code(pn.pane.HoloViews(chart, sizing_mode="stretch_width", height=400, linked_axes=False), code)
260
+
261
+
262
+ _TOGGLE_CSS = """
263
+ :host .bk-btn {
264
+ background: transparent !important;
265
+ border: 1px solid #1e3a5f !important;
266
+ color: #475569 !important;
267
+ font-size: 10px !important;
268
+ font-family: 'Courier New', monospace !important;
269
+ letter-spacing: 1px !important;
270
+ border-radius: 3px !important;
271
+ padding: 2px 10px !important;
272
+ transition: color 0.15s, border-color 0.15s;
273
+ }
274
+ :host .bk-btn:hover {
275
+ color: #7dd3fc !important;
276
+ border-color: #7dd3fc88 !important;
277
+ }
278
+ :host .bk-btn.bk-active {
279
+ color: #7dd3fc !important;
280
+ border-color: #7dd3fc !important;
281
+ background: #1e3a5f33 !important;
282
+ }
283
+ """
284
+
285
+ _CODE_PANE_CSS = """
286
+ :host {
287
+ background: #0d1b2a !important;
288
+ border-radius: 6px !important;
289
+ border: 1px solid #1e3a5f !important;
290
+ padding: 2px 6px !important;
291
+ }
292
+ """
293
 
294
 
295
  def _wrap_with_code(chart_pane: pn.viewable.Viewable, code: str) -> pn.viewable.Viewable:
296
+ """Wrap a chart with a small right-aligned toggle that reveals a code block."""
297
+ toggle = pn.widgets.Toggle(
298
+ name="</> code",
299
+ value=False,
300
+ button_type="light",
301
+ width=72,
302
+ height=24,
303
+ stylesheets=[_TOGGLE_CSS],
304
  )
305
+
306
+ code_md = f"```python\n{code}\n```"
307
+
308
+ # pn.depends swaps the whole element in/out of the DOM β€” reliable show/hide
309
+ @pn.depends(toggle.param.value)
310
+ def _code_block(show):
311
+ if not show:
312
+ return pn.pane.HTML("", width=0, height=0, margin=0)
313
+ return pn.pane.Markdown(
314
+ code_md,
315
+ sizing_mode="stretch_width",
316
+ margin=(0, 0, 6, 0),
317
+ stylesheets=[_CODE_PANE_CSS],
318
+ )
319
+
320
+ return pn.Column(
321
+ chart_pane,
322
+ pn.Row(pn.Spacer(), toggle, margin=(4, 0, 2, 0)),
323
+ _code_block,
324
  sizing_mode="stretch_width",
 
325
  )
 
326
 
327
 
328
  def _make_multi_chart(df: pd.DataFrame, specs: list[dict]) -> pn.viewable.Viewable:
 
337
  return pn.pane.Markdown("*Could not render charts.*")
338
  if len(panels) == 1:
339
  return panels[0]
340
+ return pn.GridBox(*panels, ncols=2, sizing_mode="stretch_width")
341
 
342
 
343
  def _make_table(df: pd.DataFrame) -> pn.widgets.Tabulator:
 
359
  _history: list[dict] = []
360
 
361
 
362
+ _LOADING_HTML = """
363
+ <div style="display:flex;align-items:center;gap:10px;padding:6px 2px;">
364
+ <div style="display:flex;gap:4px;align-items:center;">
365
+ <span style="display:inline-block;width:7px;height:7px;border-radius:50%;
366
+ background:#7dd3fc;animation:ai-dot 1.2s ease-in-out infinite 0s;"></span>
367
+ <span style="display:inline-block;width:7px;height:7px;border-radius:50%;
368
+ background:#7dd3fc;animation:ai-dot 1.2s ease-in-out infinite 0.2s;"></span>
369
+ <span style="display:inline-block;width:7px;height:7px;border-radius:50%;
370
+ background:#7dd3fc;animation:ai-dot 1.2s ease-in-out infinite 0.4s;"></span>
371
+ </div>
372
+ <span style="color:#475569;font-size:12px;font-family:'Courier New',monospace;
373
+ letter-spacing:1px;">Analyzing…</span>
374
+ </div>
375
+ <style>
376
+ @keyframes ai-dot {
377
+ 0%, 80%, 100% { opacity: 0.15; transform: scale(0.7); }
378
+ 40% { opacity: 1; transform: scale(1.15); }
379
+ }
380
+ </style>
381
+ """
382
+
383
+
384
  async def _chat_callback(contents: str, user: str, instance: pn.chat.ChatInterface):
385
  con = _init_db()
386
  _history.append({"role": "user", "text": contents})
387
 
388
+ # Show animated loading dots immediately while the LLM call is in flight.
389
+ # _ask_llm is synchronous, so we run it in a thread executor to avoid
390
+ # blocking the event loop (which would prevent the UI from rendering the dots).
391
+ loading = pn.pane.HTML(_LOADING_HTML, sizing_mode="stretch_width")
392
+ yield loading
393
+ await asyncio.sleep(0) # flush UI update to client before blocking call
394
+
395
+ loop = asyncio.get_event_loop()
396
  try:
397
+ result = await loop.run_in_executor(None, lambda: _ask_llm(contents, _history))
398
  except json.JSONDecodeError:
399
+ loading.object = ""
400
  yield "I had trouble parsing the response. Could you rephrase your question?"
401
  return
402
  except Exception as e:
403
+ loading.object = ""
404
  yield f"**Error contacting AI:** {e}"
405
  return
406
 
407
+ loading.object = "" # clear spinner β€” content follows
408
+
409
  answer = result.get("answer", "")
410
  sql = result.get("sql")
411
  tickers = result.get("tickers") or ([result["ticker"]] if result.get("ticker") else None)
 
504
  "Show the top 10 most recent critical events",
505
  "Plot Apple vs Microsoft",
506
  "What's the latest news on oil prices?",
507
+ "Show earthquake events on a map",
508
  ]
509
 
510
 
 
515
 
516
  def _label(text: str) -> pn.pane.HTML:
517
  return pn.pane.HTML(
518
+ f'<div style="font-size:13px;font-weight:bold;color:{_ACCENT};'
519
  f'letter-spacing:2px;text-transform:uppercase;'
520
  f'font-family:\'Courier New\',monospace;margin-bottom:4px;">{text}</div>',
521
  sizing_mode="stretch_width",
 
523
 
524
  def _hint(text: str) -> pn.pane.HTML:
525
  return pn.pane.HTML(
526
+ f'<div style="font-size:12px;color:{_MUTED};line-height:1.5;margin-top:4px;">{text}</div>',
527
  sizing_mode="stretch_width",
528
  )
529
 
 
569
 
570
  model_select.param.watch(_on_model, "value")
571
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
572
  get_key_link = pn.pane.HTML(
573
  f'<a href="https://aistudio.google.com/apikey" target="_blank" '
574
  f'style="font-size:10px;color:{_ACCENT};text-decoration:none;">'
 
585
  _label("Model"),
586
  model_select,
587
  _divider(),
 
 
588
  pn.Spacer(),
589
  width=220,
590
  sizing_mode="stretch_height",
 
603
 
604
  chat = pn.chat.ChatInterface(
605
  callback=_chat_callback,
606
+ callback_user="Crisis AI",
607
  show_rerun=False,
608
  show_undo=True,
609
  show_clear=True,
610
+ placeholder_text="Ask anything about risk events, commodities, news...",
611
  sizing_mode="stretch_both",
612
  min_height=600,
613
  callback_exception="verbose",
614
+ stylesheets=["""
615
+ :host { background: #0a0f1e; }
616
+ .chat-interface { background: #0a0f1e; }
617
+ .message { font-size: 14px; line-height: 1.6; }
618
+ .chat-entry { overflow: hidden; }
619
+ .chat-feed-entry { overflow: hidden; contain: layout; }
620
+ """],
621
  )
622
 
623
  chat.send(
624
+ "Hello! I'm your Crisis Intelligence AI. Ask me about risk events, commodities, currencies, stocks, or world news.",
625
+ user="Crisis AI",
 
 
 
 
 
626
  respond=False,
627
  )
628
 
app/analysis_panel.py CHANGED
@@ -33,7 +33,7 @@ _ACCENT = "#7dd3fc"
33
  _MUTED = "#475569"
34
 
35
  _HDR_CSS = (
36
- "font-size:10px;font-weight:bold;color:{a};"
37
  "letter-spacing:2px;text-transform:uppercase;"
38
  "font-family:'Courier New',monospace;padding:6px 0 2px;"
39
  ).format(a=_ACCENT)
 
33
  _MUTED = "#475569"
34
 
35
  _HDR_CSS = (
36
+ "font-size:13px;font-weight:bold;color:{a};"
37
  "letter-spacing:2px;text-transform:uppercase;"
38
  "font-family:'Courier New',monospace;padding:6px 0 2px;"
39
  ).format(a=_ACCENT)
app/dashboard.py CHANGED
@@ -70,23 +70,33 @@ _ACCENT = "#7dd3fc"
70
  _PANEL_W = 320
71
 
72
  _HDR_CSS = (
73
- "font-size:10px;font-weight:bold;color:{a};"
74
  "letter-spacing:2px;text-transform:uppercase;"
75
  "margin-bottom:8px;font-family:'Courier New',monospace;"
76
  ).format(a=_ACCENT)
77
 
78
- _HINT_CSS = "font-size:10px;color:#475569;margin-top:4px;line-height:1.4;"
79
 
80
  _TAB_CSS = """
81
- :host .bk-header { background: #0a0f1e; border-bottom: 1px solid #1e3a5f; }
 
 
 
 
 
 
 
 
 
 
82
  :host .bk-tab {
83
- background: #0a0f1e; color: #475569;
84
  border: none; border-bottom: 2px solid transparent;
85
- font-family: 'Courier New', monospace; font-size: 10px;
86
- letter-spacing: 1.5px; text-transform: uppercase;
87
- padding: 9px 18px;
88
  }
89
- :host .bk-tab:hover { color: #94a3b8; }
90
  :host .bk-tab.bk-active { color: #7dd3fc; border-bottom: 2px solid #7dd3fc; }
91
  """
92
 
@@ -121,10 +131,13 @@ def build_dashboard() -> pn.Column:
121
  header = pn.pane.HTML(
122
  f"""
123
  <div style="background:{_BG};padding:13px 20px;
124
- border-bottom:1px solid {_BORDER};display:flex;align-items:center;">
125
- <span style="color:#e2e8f0;font-size:17px;font-weight:bold;
126
- font-family:sans-serif;letter-spacing:0.5px;">
127
- HoloIntel &mdash; Global Risk Map
 
 
 
128
  </span>
129
  </div>
130
  """,
@@ -140,10 +153,10 @@ def build_dashboard() -> pn.Column:
140
  _SRC_ICON_STYLE = {
141
  "GDELT": ("#ef4444", "22px"), # red β€” conflict
142
  "FIRMS": ("#f97316", "18px"), # orange β€” fire
143
- "OpenSky": ("#38bdf8", "18px"), # blue β€” aviation
144
- "NOAA": ("#22d3ee", "18px"), # teal β€” weather
145
- "Maritime": ("#0ea5e9", "18px"), # sky β€” ships
146
- "Rocket": ("#f43f5e", "22px"), # rose β€” alerts are critical
147
  "Seismic": ("#a78bfa", "18px"), # violet β€” earthquakes
148
  "Cyber": ("#34d399", "18px"), # emerald β€” cyber
149
  }
@@ -158,10 +171,10 @@ def build_dashboard() -> pn.Column:
158
  pn.Row(
159
  _src_cbs[src],
160
  pn.pane.HTML(
161
- f'<span style="font-size:{_SRC_ICON_STYLE[src][1]};'
 
162
  f'color:{_SRC_ICON_STYLE[src][0]};line-height:1;">{glyph}</span>'
163
- f'&nbsp;<span style="font-size:12px;color:#e2e8f0;">{short}</span>'
164
- f'<span style="font-size:10px;color:#475569;"> β€” {desc}</span>',
165
  sizing_mode="stretch_width",
166
  margin=0,
167
  ),
@@ -337,7 +350,7 @@ def build_dashboard() -> pn.Column:
337
 
338
  tabs = pn.Tabs(
339
  ("πŸ—Ί Risk Map", map_body),
340
- ("πŸ” Risk Analysis", build_analysis_tab(state.filtered_events())),
341
  ("πŸ“ˆ Global Prices", build_commodities_tab()),
342
  ("πŸ’± Currency FX", build_currency_tab()),
343
  ("πŸ€– AI Explorer", build_ai_tab()),
 
70
  _PANEL_W = 320
71
 
72
  _HDR_CSS = (
73
+ "font-size:12px;font-weight:bold;color:{a};"
74
  "letter-spacing:2px;text-transform:uppercase;"
75
  "margin-bottom:8px;font-family:'Courier New',monospace;"
76
  ).format(a=_ACCENT)
77
 
78
+ _HINT_CSS = "font-size:12px;color:#7a8fa6;margin-top:4px;line-height:1.4;"
79
 
80
  _TAB_CSS = """
81
+ :host .bk-header {
82
+ background: #0a0f1e;
83
+ border-bottom: 1px solid #1e3a5f;
84
+ display: flex;
85
+ justify-content: center;
86
+ }
87
+ :host .bk-header .bk-btn-group {
88
+ display: flex;
89
+ justify-content: center;
90
+ width: 100%;
91
+ }
92
  :host .bk-tab {
93
+ background: #0a0f1e; color: #94a3b8;
94
  border: none; border-bottom: 2px solid transparent;
95
+ font-family: 'Courier New', monospace; font-size: 13px;
96
+ font-weight: bold; letter-spacing: 1px; text-transform: uppercase;
97
+ padding: 10px 20px;
98
  }
99
+ :host .bk-tab:hover { color: #cbd5e1; }
100
  :host .bk-tab.bk-active { color: #7dd3fc; border-bottom: 2px solid #7dd3fc; }
101
  """
102
 
 
131
  header = pn.pane.HTML(
132
  f"""
133
  <div style="background:{_BG};padding:13px 20px;
134
+ border-bottom:3px solid {_ACCENT};
135
+ box-shadow:0 3px 10px {_ACCENT}66;
136
+ display:flex;align-items:center;justify-content:center;">
137
+ <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@700&display=swap" rel="stylesheet">
138
+ <span style="color:#e2e8f0;font-size:22px;font-weight:700;
139
+ font-family:'Orbitron',sans-serif;letter-spacing:3px;">
140
+ Crisis Intelligence Platform
141
  </span>
142
  </div>
143
  """,
 
153
  _SRC_ICON_STYLE = {
154
  "GDELT": ("#ef4444", "22px"), # red β€” conflict
155
  "FIRMS": ("#f97316", "18px"), # orange β€” fire
156
+ "NOAA": ("#fbbf24", "18px"), # yellow β€” weather
157
+ "OpenSky": ("#38bdf8", "18px"), # blue β€” flight tracking
158
+ "Maritime": ("#10b981", "18px"), # green β€” ships
159
+ "Rocket": ("#f43f5e", "22px"), # rose β€” rocket alerts
160
  "Seismic": ("#a78bfa", "18px"), # violet β€” earthquakes
161
  "Cyber": ("#34d399", "18px"), # emerald β€” cyber
162
  }
 
171
  pn.Row(
172
  _src_cbs[src],
173
  pn.pane.HTML(
174
+ f'<span style="font-size:14px;color:#e2e8f0;font-weight:bold;">{short}</span>'
175
+ f'&nbsp;<span style="font-size:{_SRC_ICON_STYLE[src][1]};'
176
  f'color:{_SRC_ICON_STYLE[src][0]};line-height:1;">{glyph}</span>'
177
+ f'<span style="font-size:11px;color:#94a3b8;"> β€” {desc}</span>',
 
178
  sizing_mode="stretch_width",
179
  margin=0,
180
  ),
 
350
 
351
  tabs = pn.Tabs(
352
  ("πŸ—Ί Risk Map", map_body),
353
+ ("πŸ“° News & Events", build_analysis_tab(state.filtered_events())),
354
  ("πŸ“ˆ Global Prices", build_commodities_tab()),
355
  ("πŸ’± Currency FX", build_currency_tab()),
356
  ("πŸ€– AI Explorer", build_ai_tab()),
app/intel_panel.py CHANGED
@@ -24,7 +24,7 @@ _ACCENT = "#7dd3fc"
24
  _MUTED = "#475569"
25
 
26
  _HDR_CSS = (
27
- "font-size:10px;font-weight:bold;color:{a};"
28
  "letter-spacing:2px;text-transform:uppercase;"
29
  "font-family:'Courier New',monospace;padding:16px 16px 10px;"
30
  ).format(a=_ACCENT)
@@ -40,7 +40,7 @@ _COMMODITY_COLORS = [
40
  "#94a3b8", "#c084fc", # Silver / Palladium
41
  ]
42
 
43
- _PLOT_KW = dict(responsive=True, height=520, grid=True, fontscale=0.9)
44
 
45
 
46
  def _no_data(msg: str) -> pn.pane.HTML:
 
24
  _MUTED = "#475569"
25
 
26
  _HDR_CSS = (
27
+ "font-size:13px;font-weight:bold;color:{a};"
28
  "letter-spacing:2px;text-transform:uppercase;"
29
  "font-family:'Courier New',monospace;padding:16px 16px 10px;"
30
  ).format(a=_ACCENT)
 
40
  "#94a3b8", "#c084fc", # Silver / Palladium
41
  ]
42
 
43
+ _PLOT_KW = dict(responsive=True, height=520, grid=True, fontscale=1.2)
44
 
45
 
46
  def _no_data(msg: str) -> pn.pane.HTML:
app/legend.py CHANGED
@@ -66,8 +66,8 @@ def build_source_legend() -> pn.pane.HTML:
66
  margin-right:10px;margin-top:1px;">{glyph}</span>
67
  <div>
68
  <span style="font-size:12px;font-weight:bold;color:#e2e8f0;">{src}</span>
69
- <span style="font-size:11px;color:#475569;"> β€” {short}</span><br>
70
- <span style="font-size:10px;color:#334155;">{desc}</span>
71
  </div>
72
  </div>"""
73
  for src, (glyph, short, desc) in SOURCE_SHAPES.items()
@@ -80,10 +80,10 @@ def build_severity_legend() -> pn.pane.HTML:
80
  items = list(SEVERITY_CMAP.items())
81
  cells = "".join(
82
  f"""<div style="display:flex;align-items:center;margin-bottom:5px;">
83
- <span style="display:inline-block;width:8px;height:8px;border-radius:50%;
84
  background:{color};flex-shrink:0;
85
- box-shadow:0 0 4px {color}88;"></span>
86
- <span style="margin-left:7px;font-size:11px;color:#cbd5e1;">
87
  <b style="color:{color};">{k}</b>&nbsp;{SEV_LABEL[int(k)]}
88
  </span>
89
  </div>"""
 
66
  margin-right:10px;margin-top:1px;">{glyph}</span>
67
  <div>
68
  <span style="font-size:12px;font-weight:bold;color:#e2e8f0;">{src}</span>
69
+ <span style="font-size:11px;color:#94a3b8;"> β€” {short}</span><br>
70
+ <span style="font-size:10px;color:#64748b;">{desc}</span>
71
  </div>
72
  </div>"""
73
  for src, (glyph, short, desc) in SOURCE_SHAPES.items()
 
80
  items = list(SEVERITY_CMAP.items())
81
  cells = "".join(
82
  f"""<div style="display:flex;align-items:center;margin-bottom:5px;">
83
+ <span style="display:inline-block;width:10px;height:10px;border-radius:50%;
84
  background:{color};flex-shrink:0;
85
+ box-shadow:0 0 5px {color}aa;"></span>
86
+ <span style="margin-left:8px;font-size:13px;color:#e2e8f0;">
87
  <b style="color:{color};">{k}</b>&nbsp;{SEV_LABEL[int(k)]}
88
  </span>
89
  </div>"""