bhanug2026's picture
Upload 10 files
6493351 verified
# ╔══════════════════════════════════════════════════════════════════════╗
# β•‘ BubbleBusters β€” AI Bubble Sentiment Analytics β•‘
# β•‘ RX12 Group Project Β· ESCP Europe β•‘
# β•‘ app.py β€” Three-notebook pipeline + live dashboard β•‘
# β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
import os
import re
import json
import time
import traceback
import sys
import subprocess
from pathlib import Path
from typing import Dict, Any, List, Tuple, Optional
import pandas as pd
import gradio as gr
import papermill as pm
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
# ── Optional dependencies ───────────────────────────────────────────────
try:
import yfinance as yf
YFINANCE_AVAILABLE = True
except ImportError:
YFINANCE_AVAILABLE = False
try:
from huggingface_hub import InferenceClient
except Exception:
InferenceClient = None
# ══════════════════════════════════════════════════════════════════════
# CONFIG
# ══════════════════════════════════════════════════════════════════════
BASE_DIR = Path(__file__).resolve().parent
NB1 = os.environ.get("NB1", "datacreation_bubblebusters.ipynb").strip()
NB2 = os.environ.get("NB2", "pythonanalysis_bubblebusters.ipynb").strip()
NB3 = os.environ.get("NB3", "ranalysis_bubblebusters.ipynb").strip()
RUNS_DIR = BASE_DIR / "runs"
ART_DIR = BASE_DIR / "artifacts"
PY_FIG_DIR = ART_DIR / "py" / "figures"
PY_TAB_DIR = ART_DIR / "py" / "tables"
R_FIG_DIR = ART_DIR / "r" / "figures"
R_TAB_DIR = ART_DIR / "r" / "tables"
PAPERMILL_TIMEOUT = int(os.environ.get("PAPERMILL_TIMEOUT", "1800"))
MAX_PREVIEW_ROWS = int(os.environ.get("MAX_FILE_PREVIEW_ROWS", "50"))
HF_API_KEY = os.environ.get("HF_API_KEY", "").strip()
MODEL_NAME = os.environ.get("MODEL_NAME", "deepseek-ai/DeepSeek-R1").strip()
HF_PROVIDER = os.environ.get("HF_PROVIDER", "novita").strip()
# Colour palette
ESCP_PURPLE = "#00d2be"
BULLISH = "#2ec4a0" # deep mint-teal
NEUTRAL = "#5e8fef" # medium periwinkle-blue
BEARISH = "#e8537a" # deep blush-rose
AMBER = "#e8a230" # rich amber
# LLM client
LLM_ENABLED = bool(HF_API_KEY) and InferenceClient is not None
llm_client = (
InferenceClient(provider=HF_PROVIDER, api_key=HF_API_KEY)
if LLM_ENABLED else None
)
# AI-related tickers shown in the prices section
AI_TICKERS_DEFAULT = "NVDA, MSFT, GOOGL, META, AMD"
AI_PRESET_MEGA = "NVDA, MSFT, GOOGL, META, AMZN, AAPL"
AI_PRESET_SEMI = "NVDA, AMD, TSM, INTC, QCOM, SMCI"
AI_PRESET_PURE = "AI, PLTR, SOUN, PATH, BBAI, GFAI"
# ══════════════════════════════════════════════════════════════════════
# KERNEL SETUP (for papermill)
# ══════════════════════════════════════════════════════════════════════
def ensure_python_kernelspec() -> str:
from jupyter_client.kernelspec import KernelSpecManager
ksm = KernelSpecManager()
specs = ksm.find_kernel_specs()
if not specs:
try:
import ipykernel # noqa: F401
except Exception as e:
raise RuntimeError(
"ipykernel is not installed. "
"Add 'ipykernel' to requirements.txt and rebuild the Space.\n"
f"Original error: {e}"
)
subprocess.check_call([
sys.executable, "-m", "ipykernel", "install",
"--user", "--name", "python3", "--display-name", "Python 3 (Space)"
])
specs = ksm.find_kernel_specs()
if "python3" in specs:
return "python3"
for name in specs:
if "python" in name.lower():
return name
raise RuntimeError(f"No Python kernel found. Available: {list(specs.keys())}")
try:
PY_KERNEL = ensure_python_kernelspec()
KERNEL_INIT_ERROR = ""
except Exception as e:
PY_KERNEL = None
KERNEL_INIT_ERROR = str(e)
# ══════════════════════════════════════════════════════════════════════
# HELPERS
# ══════════════════════════════════════════════════════════════════════
def ensure_dirs():
for p in [RUNS_DIR, ART_DIR, PY_FIG_DIR, PY_TAB_DIR, R_FIG_DIR, R_TAB_DIR]:
p.mkdir(parents=True, exist_ok=True)
def stamp():
return time.strftime("%Y%m%d-%H%M%S")
def _ls(dir_path: Path, exts: Tuple[str, ...]) -> List[str]:
if not dir_path.is_dir():
return []
return sorted(p.name for p in dir_path.iterdir()
if p.is_file() and p.suffix.lower() in exts)
def _read_csv(path: Path) -> pd.DataFrame:
return pd.read_csv(path, nrows=MAX_PREVIEW_ROWS)
def _read_json(path: Path):
with path.open(encoding="utf-8") as f:
return json.load(f)
def artifacts_index() -> Dict[str, Any]:
return {
"python": {
"figures": _ls(PY_FIG_DIR, (".png", ".jpg", ".jpeg")),
"tables": _ls(PY_TAB_DIR, (".csv", ".json")),
},
"r": {
"figures": _ls(R_FIG_DIR, (".png", ".jpg", ".jpeg")),
"tables": _ls(R_TAB_DIR, (".csv", ".json")),
},
}
# ══════════════════════════════════════════════════════════════════════
# PIPELINE STATUS
# ══════════════════════════════════════════════════════════════════════
def get_pipeline_status() -> Dict[str, Any]:
clean_csv = BASE_DIR / "ai_bubble_clean.csv"
monthly_csv = BASE_DIR / "ai_bubble_monthly.csv"
data_ok = clean_csv.exists() and monthly_csv.exists()
py_figs = _ls(PY_FIG_DIR, (".png",))
py_tabs = _ls(PY_TAB_DIR, (".csv", ".json"))
py_ok = len(py_figs) >= 5 and len(py_tabs) >= 4
r_figs = _ls(R_FIG_DIR, (".png",))
r_tabs = _ls(R_TAB_DIR, (".csv", ".json"))
r_ok = len(r_figs) >= 3 and len(r_tabs) >= 2
return {
"data": {
"ok": data_ok,
"detail": (
f"ai_bubble_clean.csv: {'βœ…' if clean_csv.exists() else '❌'} | "
f"ai_bubble_monthly.csv: {'βœ…' if monthly_csv.exists() else '❌'}"
),
},
"python": {
"ok": py_ok,
"detail": f"{len(py_figs)} figures Β· {len(py_tabs)} tables",
},
"r": {
"ok": r_ok,
"detail": f"{len(r_figs)} figures Β· {len(r_tabs)} tables",
},
}
def render_status_html() -> str:
status = get_pipeline_status()
def badge(ok: bool, label: str, detail: str, icon: str) -> str:
colour = "#3dcba8" if ok else "#ff6b8a"
bg = "rgba(61,203,168,.10)" if ok else "rgba(255,107,138,.08)"
border = "rgba(61,203,168,.30)" if ok else "rgba(255,107,138,.25)"
pill = "READY" if ok else "PENDING"
return f"""
<div style="display:flex;align-items:flex-start;gap:12px;
padding:13px 15px;background:{bg};
border:1.5px solid {border};
border-radius:16px;margin-bottom:8px;">
<div style="font-size:20px;line-height:1;margin-top:2px;flex-shrink:0">{icon}</div>
<div style="flex:1;min-width:0;">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:3px;">
<span style="font-family:'Nunito',sans-serif;font-weight:800;
color:#2d1f4e;font-size:13px;">{label}</span>
<span style="margin-left:auto;background:{colour};color:#fff;
border-radius:50px;padding:2px 10px;
font-family:'Nunito',sans-serif;
font-size:10px;font-weight:800;letter-spacing:.8px;
flex-shrink:0;">{pill}</span>
</div>
<div style="color:#9d8fc4;font-size:11.5px;font-family:'Nunito',sans-serif;
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">
{detail}
</div>
</div>
</div>"""
html = """
<div style="background:rgba(255,255,255,.65);backdrop-filter:blur(16px);
border-radius:20px;padding:18px;
border:1.5px solid rgba(255,255,255,.7);
box-shadow:0 8px 32px rgba(124,92,191,.10);">
<div style="font-family:'Nunito',sans-serif;color:#a48de8;
font-weight:900;font-size:10.5px;text-transform:uppercase;
letter-spacing:2.5px;margin-bottom:14px;
display:flex;align-items:center;gap:10px;">
<span>πŸ” Pipeline Status</span>
<div style="flex:1;height:1.5px;
background:linear-gradient(90deg,rgba(164,141,232,.5),transparent);
border-radius:2px;"></div>
</div>
"""
html += badge(status["data"]["ok"], "Data Creation", status["data"]["detail"], "πŸ“¦")
html += badge(status["python"]["ok"], "Python Analysis", status["python"]["detail"], "🐍")
html += badge(status["r"]["ok"], "R Analysis", status["r"]["detail"], "πŸ“Š")
html += "</div>"
return html
# ══════════════════════════════════════════════════════════════════════
# PIPELINE RUNNERS
# ══════════════════════════════════════════════════════════════════════
def run_notebook(nb_name: str, kernel_name: str) -> str:
ensure_dirs()
nb_in = BASE_DIR / nb_name
if not nb_in.exists():
return f"ERROR: {nb_name} not found at {nb_in}"
nb_out = RUNS_DIR / f"run_{stamp()}_{nb_name}"
pm.execute_notebook(
input_path=str(nb_in),
output_path=str(nb_out),
cwd=str(BASE_DIR),
log_output=True,
progress_bar=False,
request_save_on_cell_execute=True,
execution_timeout=PAPERMILL_TIMEOUT,
kernel_name=kernel_name,
)
return f"βœ… Executed {nb_name}"
def run_datacreation() -> str:
try:
if not PY_KERNEL:
return f"❌ Kernel unavailable:\n{KERNEL_INIT_ERROR}"
return run_notebook(NB1, kernel_name=PY_KERNEL)
except Exception as e:
return f"❌ FAILED: {e}\n\n{traceback.format_exc()[-2000:]}"
def run_pythonanalysis() -> str:
try:
if not PY_KERNEL:
return f"❌ Kernel unavailable:\n{KERNEL_INIT_ERROR}"
return run_notebook(NB2, kernel_name=PY_KERNEL)
except Exception as e:
return f"❌ FAILED: {e}\n\n{traceback.format_exc()[-2000:]}"
def run_r() -> str:
"""Run the R analysis notebook via papermill + IRkernel."""
try:
# Check IRkernel is registered
from jupyter_client.kernelspec import KernelSpecManager
specs = KernelSpecManager().find_kernel_specs()
if "ir" not in specs:
return (
"❌ IRkernel not found in this environment.\n\n"
"If you are running locally, install it with:\n"
" Rscript -e \"install.packages('IRkernel')\"\n"
" Rscript -e \"IRkernel::installspec()\"\n\n"
"On the Hugging Face Space (Docker), this is pre-installed β€” "
"if you see this message, try rebuilding the Space."
)
return run_notebook(NB3, kernel_name="ir")
except Exception as e:
return f"❌ FAILED: {e}\n\n{traceback.format_exc()[-2000:]}"
def run_full_pipeline() -> str:
logs = []
for label, fn in [
("πŸ“¦ STEP 1/3 β€” Data Creation", run_datacreation),
("🐍 STEP 2a/3 β€” Python Analysis", run_pythonanalysis),
("πŸ“Š STEP 2b/3 β€” R Analysis", run_r),
]:
logs.append(f"\n{'─'*52}\n{label}\n{'─'*52}")
logs.append(fn())
return "\n".join(logs)
# ══════════════════════════════════════════════════════════════════════
# ASSET PRICES
# ══════════════════════════════════════════════════════════════════════
def fetch_asset_prices(
tickers_str: str,
period: str = "6mo",
) -> Tuple[go.Figure, str]:
"""Fetch prices via yfinance and return normalised Plotly chart + summary."""
def _empty(msg: str) -> Tuple[go.Figure, str]:
fig = go.Figure()
fig.update_layout(
title=msg,
template="plotly_white",
paper_bgcolor="rgba(247,244,255,0.85)",
plot_bgcolor="rgba(255,255,255,0.95)",
height=420,
)
return fig, msg
if not YFINANCE_AVAILABLE:
return _empty("⚠️ yfinance not installed β€” add it to requirements.txt")
tickers = [t.strip().upper() for t in tickers_str.split(",") if t.strip()]
if not tickers:
return _empty("Please enter at least one ticker symbol.")
try:
raw = yf.download(tickers, period=period, auto_adjust=True, progress=False)
if raw.empty:
return _empty("No price data returned. Check ticker symbols.")
# Flatten: single ticker β†’ single column
if len(tickers) == 1:
close = raw[["Close"]].rename(columns={"Close": tickers[0]})
else:
close = raw["Close"]
# Normalise to base 100
norm = close / close.iloc[0] * 100
palette = [
"#7c5cbf", "#2ec4a0", "#e8537a", "#e8a230", "#5e8fef",
"#c45ea8", "#3dbacc", "#a0522d", "#6aaa3a", "#d46060",
"#4a7fc1", "#8e6abf",
]
fig = go.Figure()
for i, col in enumerate(norm.columns):
fig.add_trace(go.Scatter(
x=norm.index, y=norm[col].round(2),
name=str(col),
mode="lines",
line=dict(color=palette[i % len(palette)], width=2),
hovertemplate=(
f"<b>{col}</b><br>%{{x|%d %b %Y}}<br>"
"Index: %{y:.1f}<extra></extra>"
),
))
fig.add_hline(
y=100, line_dash="dot",
line_color="rgba(124,92,191,0.4)",
annotation_text="Base (100)",
annotation_position="bottom right",
)
fig.update_layout(
title=dict(
text="AI-Related Asset Prices β€” Normalised (base = 100 at start of period)",
font=dict(size=15, color="#4b2d8a", family="Syne, sans-serif"),
),
template="plotly_white",
paper_bgcolor="rgba(247,244,255,0.85)",
plot_bgcolor="rgba(255,255,255,0.95)",
font=dict(color="#2d1f4e", family="Lato, sans-serif"),
height=460,
margin=dict(l=60, r=20, t=70, b=70),
legend=dict(
orientation="h",
yanchor="bottom", y=-0.22,
xanchor="center", x=0.5,
bgcolor="rgba(255,255,255,0.92)",
bordercolor="rgba(124,92,191,0.35)",
borderwidth=1,
),
hovermode="x unified",
)
fig.update_xaxes(gridcolor="rgba(124,92,191,0.18)", showgrid=True)
fig.update_yaxes(gridcolor="rgba(124,92,191,0.18)", showgrid=True, title="Index (base 100)")
# Summary markdown
latest = close.iloc[-1]
first = close.iloc[0]
rows = []
for t in close.columns:
try:
chg = ((float(latest[t]) - float(first[t])) / float(first[t])) * 100
sign = "+" if chg >= 0 else ""
col = BULLISH if chg >= 0 else BEARISH
rows.append(
f"| **{t}** | ${float(latest[t]):.2f} "
f"| <span style='color:{col}'>{sign}{chg:.1f}%</span> |"
)
except Exception:
pass
summary = (
"| Ticker | Latest Price | Period Return |\n"
"|--------|:------------:|:-------------:|\n"
+ "\n".join(rows)
) if rows else "*(no data)*"
return fig, summary
except Exception as e:
return _empty(f"Error fetching prices: {e}")
# ══════════════════════════════════════════════════════════════════════
# SENTIMENT CHARTS (interactive Plotly)
# ══════════════════════════════════════════════════════════════════════
def _dark_layout(**kwargs) -> dict:
defaults = dict(
template="plotly_white",
paper_bgcolor="rgba(247,244,255,0.85)",
plot_bgcolor="rgba(255,255,255,0.95)",
font=dict(family="Lato, sans-serif", color="#2d1f4e", size=12),
margin=dict(l=60, r=20, t=70, b=70),
legend=dict(
orientation="h",
yanchor="bottom", y=1.02,
xanchor="right", x=1,
bgcolor="rgba(255,255,255,0.92)",
bordercolor="rgba(124,92,191,0.35)",
borderwidth=1,
),
title=dict(font=dict(family="Syne, sans-serif", size=15, color="#4b2d8a")),
)
defaults.update(kwargs)
return defaults
def _grid_axes(fig: go.Figure, **kwargs):
fig.update_xaxes(gridcolor="rgba(124,92,191,0.18)", showgrid=True, **kwargs)
fig.update_yaxes(gridcolor="rgba(124,92,191,0.18)", showgrid=True)
return fig
def _empty_chart(title: str) -> go.Figure:
fig = go.Figure()
fig.update_layout(
title=title,
template="plotly_white",
paper_bgcolor="rgba(247,244,255,0.85)",
plot_bgcolor="rgba(255,255,255,0.95)",
font=dict(family="Lato, sans-serif", color="#2d1f4e"),
height=420,
annotations=[dict(
text="Run the pipeline to generate data",
x=0.5, y=0.5, xref="paper", yref="paper",
showarrow=False,
font=dict(size=14, color="rgba(124,92,191,0.5)",
family="Syne, sans-serif"),
)],
)
return fig
# ── KPI Cards ──────────────────────────────────────────────────────────
def load_kpis() -> Dict[str, Any]:
for candidate in [PY_TAB_DIR / "kpis.json"]:
if candidate.exists():
try:
return _read_json(candidate)
except Exception:
pass
return {}
def render_kpi_cards() -> str:
kpis = load_kpis()
if not kpis:
return (
'<div style="background:rgba(255,255,255,.65);backdrop-filter:blur(16px);'
'border-radius:20px;padding:28px;text-align:center;'
'border:1.5px solid rgba(255,255,255,.7);'
'box-shadow:0 8px 32px rgba(124,92,191,.08);">'
'<div style="font-size:36px;margin-bottom:10px;">🫧</div>'
'<div style="font-family:\'Nunito\',sans-serif;color:#a48de8;font-size:14px;'
'font-weight:800;margin-bottom:6px;">No data yet</div>'
'<div style="color:#9d8fc4;font-size:12px;font-family:\'Nunito\',sans-serif;">'
'Run the Python analysis pipeline to populate these cards.</div>'
'</div>'
)
def card(icon, label, value, colour):
return f"""
<div style="background:rgba(255,255,255,.72);backdrop-filter:blur(16px);
border-radius:20px;padding:18px 14px 16px;text-align:center;
border:1.5px solid rgba(255,255,255,.8);
box-shadow:0 4px 16px rgba(124,92,191,.08);
border-top:3px solid {colour};
position:relative;overflow:hidden;">
<div style="font-size:26px;margin-bottom:7px;line-height:1;">{icon}</div>
<div style="font-family:'Nunito',sans-serif;color:#9d8fc4;
font-size:9.5px;text-transform:uppercase;
letter-spacing:1.8px;margin-bottom:7px;font-weight:800;">{label}</div>
<div style="font-family:'Syne',sans-serif;color:#2d1f4e;
font-size:16px;font-weight:800;letter-spacing:-.3px;">{value}</div>
</div>"""
cards = [
("πŸ’¬", "Comments", f"{kpis.get('total_comments','β€”'):,}", "#a48de8"),
("πŸ“…", "Date Range", kpis.get("date_range","β€”"), "#7aa6f8"),
("🌐", "Platforms", str(kpis.get("n_platforms","β€”")), "#6ee7c7"),
("🏷️", "Topics", str(kpis.get("n_topics","β€”")), "#3dcba8"),
("πŸ‚", "Bullish", f"{kpis.get('pct_bullish','β€”')}%", "#3dcba8"),
("🐻", "Bearish", f"{kpis.get('pct_bearish','β€”')}%", "#ff6b8a"),
("⚠️", "Bubble Risk", f"{kpis.get('latest_bubble_risk','β€”')}", "#ffb347"),
("πŸ”¬", "ChiΒ² p-value", f"{kpis.get('chi2_p_value','β€”')}", "#8fa8f8"),
]
html = (
'<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));'
'gap:12px;margin-bottom:24px;">'
)
for icon, label, value, colour in cards:
html += card(icon, label, value, colour)
html += "</div>"
return html
# ── Overview chart (monthly sentiment over time) ────────────────────
def build_overview_chart() -> go.Figure:
path = PY_TAB_DIR / "monthly_sentiment.csv"
if not path.exists():
return _empty_chart("Sentiment Over Time β€” data not yet available")
df = pd.read_csv(path)
df["month"] = pd.to_datetime(df["month"])
fig = make_subplots(
rows=2, cols=1, shared_xaxes=True,
subplot_titles=(
"Monthly Comment Volume by Sentiment",
"3-Month Rolling Average Sentiment Score",
),
vertical_spacing=0.14,
row_heights=[0.62, 0.38],
)
for sentiment, colour in [("bullish", BULLISH), ("neutral", NEUTRAL), ("bearish", BEARISH)]:
if sentiment not in df.columns:
continue
r, g, b = int(colour[1:3], 16), int(colour[3:5], 16), int(colour[5:7], 16)
fig.add_trace(go.Scatter(
x=df["month"], y=df[sentiment],
name=sentiment.title(),
mode="lines",
stackgroup="one",
line=dict(color=colour, width=0.5),
fillcolor=f"rgba({r},{g},{b},0.7)",
hovertemplate=f"<b>{sentiment.title()}</b>: %{{y}}<extra></extra>",
), row=1, col=1)
if "avg_score" in df.columns:
rolling = df["avg_score"].rolling(3, min_periods=1).mean()
fig.add_trace(go.Scatter(
x=df["month"], y=rolling.round(3),
name="3-mo avg score",
mode="lines",
line=dict(color="#7c5cbf", width=2.5),
hovertemplate="Score: %{y:.2f}<extra></extra>",
), row=2, col=1)
fig.add_hline(
y=0, line_dash="dot",
line_color="rgba(124,92,191,0.35)",
row=2, col=1,
)
fig.update_layout(**_dark_layout(height=580, hovermode="x unified"))
_grid_axes(fig)
return fig
# ── Topic breakdown chart ──────────────────────────────────────────
def build_topic_chart() -> go.Figure:
path = PY_TAB_DIR / "sentiment_by_topic.csv"
if not path.exists():
return _empty_chart("Sentiment by Topic β€” data not yet available")
df = pd.read_csv(path)
if "Topic" not in df.columns:
return _empty_chart("Unexpected CSV format for sentiment_by_topic.csv")
cols = [c for c in ["bullish", "neutral", "bearish"] if c in df.columns]
totals = df[cols].sum(axis=1).replace(0, 1)
fig = go.Figure()
for sentiment, colour in [("bullish", BULLISH), ("neutral", NEUTRAL), ("bearish", BEARISH)]:
if sentiment not in df.columns:
continue
pct = (df[sentiment] / totals * 100).round(1)
fig.add_trace(go.Bar(
name=sentiment.title(),
x=df["Topic"], y=pct,
marker_color=colour,
hovertemplate=f"<b>{sentiment.title()}</b><br>%{{x}}: %{{y:.1f}}%<extra></extra>",
))
fig.update_layout(
**_dark_layout(
barmode="stack",
title="Sentiment Distribution by Topic (%)",
height=420,
yaxis_title="% of Comments",
)
)
_grid_axes(fig)
return fig
# ── Platform breakdown chart ───────────────────────────────────────
def build_platform_chart() -> go.Figure:
path = PY_TAB_DIR / "sentiment_by_platform.csv"
if not path.exists():
return _empty_chart("Sentiment by Platform β€” data not yet available")
df = pd.read_csv(path)
if "Platform" not in df.columns:
return _empty_chart("Unexpected CSV format for sentiment_by_platform.csv")
cols = [c for c in ["bullish", "neutral", "bearish"] if c in df.columns]
totals = df[cols].sum(axis=1).replace(0, 1)
fig = go.Figure()
for sentiment, colour in [("bullish", BULLISH), ("neutral", NEUTRAL), ("bearish", BEARISH)]:
if sentiment not in df.columns:
continue
pct = (df[sentiment] / totals * 100).round(1)
fig.add_trace(go.Bar(
name=sentiment.title(),
x=df["Platform"], y=pct,
marker_color=colour,
hovertemplate=f"<b>{sentiment.title()}</b><br>%{{x}}: %{{y:.1f}}%<extra></extra>",
))
fig.update_layout(
**_dark_layout(
barmode="stack",
title="Sentiment Distribution by Platform (%)",
height=420,
yaxis_title="% of Comments",
)
)
_grid_axes(fig)
return fig
# ── Bubble risk chart ──────────────────────────────────────────────
def build_bubble_risk_chart() -> go.Figure:
path = PY_TAB_DIR / "bubble_risk_score.csv"
if not path.exists():
return _empty_chart("Bubble Risk Score β€” data not yet available")
df = pd.read_csv(path)
if "month" not in df.columns or "bubble_risk_score" not in df.columns:
return _empty_chart("Unexpected CSV format for bubble_risk_score.csv")
df["month"] = pd.to_datetime(df["month"])
score = df["bubble_risk_score"]
fig = go.Figure()
# Shaded area: bullish zone (score < 0.5)
fig.add_trace(go.Scatter(
x=df["month"], y=score.clip(upper=0.5),
mode="none",
fill="tozeroy",
fillcolor=f"rgba({int(BULLISH[1:3],16)},{int(BULLISH[3:5],16)},{int(BULLISH[5:7],16)},0.15)",
name="Bullish zone",
showlegend=False,
hoverinfo="skip",
))
# Shaded area: bearish zone (score > 0.5)
base = pd.Series([0.5] * len(df), index=df.index)
fig.add_trace(go.Scatter(
x=df["month"], y=score.clip(lower=0.5),
mode="none",
fill="tonexty",
fillcolor=f"rgba({int(BEARISH[1:3],16)},{int(BEARISH[3:5],16)},{int(BEARISH[5:7],16)},0.15)",
name="Bearish zone",
showlegend=False,
hoverinfo="skip",
))
# Main line
fig.add_trace(go.Scatter(
x=df["month"], y=score.round(3),
name="Bubble Risk Score",
mode="lines+markers",
line=dict(color="#7c5cbf", width=2.5),
marker=dict(size=5),
hovertemplate="Risk: %{y:.3f}<extra></extra>",
))
fig.add_hline(
y=0.5, line_dash="dot",
line_color="rgba(124,92,191,0.5)",
annotation_text="Neutral threshold",
annotation_position="top right",
annotation_font_color="#7c5cbf",
)
fig.update_layout(
**_dark_layout(
title="AI Bubble Risk Score (0 = all bullish Β· 1 = all bearish)",
height=420,
hovermode="x unified",
yaxis=dict(range=[0, 1], title="Risk Score"),
)
)
_grid_axes(fig)
return fig
# ── Yearly chart ───────────────────────────────────────────────────
def build_yearly_chart() -> go.Figure:
path = PY_TAB_DIR / "yearly_sentiment.csv"
if not path.exists():
return _empty_chart("Yearly Sentiment β€” data not yet available")
df = pd.read_csv(path)
year_col = [c for c in ["Year", "year", "Year_num"] if c in df.columns]
if not year_col:
return _empty_chart("No year column found")
year_col = year_col[0]
cols = [c for c in ["bullish", "neutral", "bearish"] if c in df.columns]
totals = df[cols].sum(axis=1).replace(0, 1)
fig = go.Figure()
for sentiment, colour in [("bullish", BULLISH), ("neutral", NEUTRAL), ("bearish", BEARISH)]:
if sentiment not in df.columns:
continue
pct = (df[sentiment] / totals * 100).round(1)
fig.add_trace(go.Bar(
name=sentiment.title(),
x=df[year_col].astype(str), y=pct,
marker_color=colour,
hovertemplate=f"<b>{sentiment.title()}</b><br>%{{x}}: %{{y:.1f}}%<extra></extra>",
))
fig.update_layout(
**_dark_layout(
barmode="stack",
title="Sentiment Share by Year (%)",
height=400,
yaxis_title="% of Comments",
)
)
_grid_axes(fig)
return fig
# ── Static R figures ───────────────────────────────────────────────
def _r_fig(name: str) -> Optional[str]:
p = R_FIG_DIR / name
return str(p) if p.exists() else None
# ── Full sentiment refresh ─────────────────────────────────────────
def refresh_sentiment():
return (
render_kpi_cards(),
build_overview_chart(),
build_topic_chart(),
build_platform_chart(),
build_bubble_risk_chart(),
build_yearly_chart(),
_r_fig("r01_monthly_sentiment_trend.png"),
_r_fig("r02_rolling_sentiment_score.png"),
_r_fig("r03_chi_square_residuals.png"),
_r_fig("r04_regression_coefficients.png"),
_r_fig("r05_yearly_grouped_bars.png"),
)
# ══════════════════════════════════════════════════════════════════════
# AI CHAT
# ══════════════════════════════════════════════════════════════════════
DASHBOARD_SYSTEM = """You are a sharp, concise analytics assistant for **BubbleBusters**
β€” an AI Bubble Sentiment Analytics dashboard built for ESCP Europe (RX12).
The dataset contains online comments about whether AI is a "bubble", scraped from
platforms like HackerNews, Twitter/X, and Reddit. Each comment is classified as:
- **bullish** (AI is real / valuable / here to stay)
- **neutral** (balanced / uncertain)
- **bearish** (AI is overhyped / a bubble / will crash)
Topics covered: hype, investment, productivity, skepticism.
AVAILABLE ARTIFACTS:
{artifacts_json}
KEY METRICS:
{kpis_json}
INSTRUCTIONS:
1. Answer in 2-4 concise sentences.
2. At the END of every response, output exactly one JSON block specifying what chart to show:
```json
{{"show": "chart", "chart_type": "overview"}}
```
chart_type must be one of: "overview", "topic", "platform", "risk", "yearly", "none"
ROUTING RULES:
- Trends over time / monthly / rolling β†’ "overview"
- Topics / hype / investment / skepticism / productivity β†’ "topic"
- Platforms / HackerNews / Twitter / Reddit β†’ "platform"
- Bubble risk / danger / fear score β†’ "risk"
- Year-over-year / annual β†’ "yearly"
- General / unclear β†’ "none"
"""
_JSON_RE = re.compile(r"```json\s*(\{.*?\})\s*```", re.DOTALL)
def _parse_directive(text: str) -> Dict[str, str]:
m = _JSON_RE.search(text)
if m:
try:
return json.loads(m.group(1))
except Exception:
pass
return {"show": "none"}
def _clean(text: str) -> str:
return _JSON_RE.sub("", text).strip()
def _keyword_chat(msg: str, idx: Dict, kpis: Dict) -> Tuple[str, Dict]:
has_data = any(
idx[s]["figures"] or idx[s]["tables"] for s in ("python", "r")
)
if not has_data:
return (
"No analysis data found yet. Please run the pipeline first (βš™οΈ Pipeline tab).",
{"show": "none"},
)
ml = msg.lower()
kpi_line = ""
if kpis:
total = kpis.get("total_comments", 0)
kpi_line = (
f" The dataset contains **{total:,}** comments"
f" spanning {kpis.get('date_range', 'various dates')}."
)
if any(w in ml for w in ["risk", "bubble risk", "danger", "score"]):
return (
f"Here's the AI Bubble Risk Score over time.{kpi_line}",
{"show": "chart", "chart_type": "risk"},
)
if any(w in ml for w in ["year", "annual", "over year"]):
return (
f"Here's the year-over-year sentiment breakdown.{kpi_line}",
{"show": "chart", "chart_type": "yearly"},
)
if any(w in ml for w in ["topic", "hype", "investment", "productivity", "skepticism"]):
mb = kpis.get("most_bearish_topic", "")
mbu = kpis.get("most_bullish_topic", "")
extra = f" The most bearish topic is **{mb}** and the most bullish is **{mbu}**." if mb else ""
return (
f"Here's sentiment broken down by topic.{extra}{kpi_line}",
{"show": "chart", "chart_type": "topic"},
)
if any(w in ml for w in ["platform", "hackernews", "twitter", "reddit", "source"]):
dom = kpis.get("dominant_platform", "")
extra = f" The dominant platform is **{dom}**." if dom else ""
return (
f"Here's sentiment broken down by platform.{extra}{kpi_line}",
{"show": "chart", "chart_type": "platform"},
)
if any(w in ml for w in ["trend", "time", "monthly", "over time", "evolution", "sentiment"]):
risk = kpis.get("latest_bubble_risk", "")
extra = f" The latest 3-month bubble risk score is **{risk}**." if risk else ""
return (
f"Here are sentiment trends over time.{extra}{kpi_line}",
{"show": "chart", "chart_type": "overview"},
)
bearish = kpis.get("pct_bearish", "?")
bullish = kpis.get("pct_bullish", "?")
neutral = kpis.get("pct_neutral", "?")
return (
f"Overall: **{bullish}%** bullish Β· **{neutral}%** neutral Β· **{bearish}%** bearish.{kpi_line}\n\n"
"Try: *'Show sentiment trends'*, *'Which topics are most bearish?'*, "
"*'Compare platforms'*, *'What's the bubble risk?'*",
{"show": "none"},
)
def _directive_to_chart(directive: Dict) -> Optional[go.Figure]:
ct = directive.get("chart_type", "none")
if directive.get("show") != "chart" or ct == "none":
return None
return {
"overview": build_overview_chart,
"topic": build_topic_chart,
"platform": build_platform_chart,
"risk": build_bubble_risk_chart,
"yearly": build_yearly_chart,
}.get(ct, lambda: None)()
def ai_chat(user_msg: str, history: list):
if not user_msg or not user_msg.strip():
return history, "", None
idx = artifacts_index()
kpis = load_kpis()
if not LLM_ENABLED:
reply, directive = _keyword_chat(user_msg, idx, kpis)
else:
system = DASHBOARD_SYSTEM.format(
artifacts_json=json.dumps(idx, indent=2),
kpis_json=json.dumps(kpis, indent=2) if kpis else "(no KPIs β€” run pipeline first)",
)
msgs = [{"role": "system", "content": system}]
for entry in (history or [])[-6:]:
if isinstance(entry, dict) and "role" in entry:
msgs.append(entry)
msgs.append({"role": "user", "content": user_msg})
try:
r = llm_client.chat_completion(
model=MODEL_NAME,
messages=msgs,
temperature=0.3,
max_tokens=600,
stream=False,
)
raw = (
r["choices"][0]["message"]["content"]
if isinstance(r, dict)
else r.choices[0].message.content
)
directive = _parse_directive(raw)
reply = _clean(raw)
except Exception as e:
fallback_reply, directive = _keyword_chat(user_msg, idx, kpis)
reply = f"*(LLM error: {e})*\n\n{fallback_reply}"
chart_out = _directive_to_chart(directive)
new_history = list(history or []) + [
{"role": "user", "content": user_msg},
{"role": "assistant", "content": reply},
]
return new_history, "", chart_out
# ══════════════════════════════════════════════════════════════════════
# CSS β€” Bloomberg-terminal-inspired dark theme
# ══════════════════════════════════════════════════════════════════════
CSS = (BASE_DIR / "style.css").read_text(encoding="utf-8")
# ══════════════════════════════════════════════════════════════════════
# GRADIO APP
# ══════════════════════════════════════════════════════════════════════
ensure_dirs()
with gr.Blocks(title="BubbleBusters β€” AI Bubble Analytics") as demo:
# ── Master header ──────────────────────────────────────────────
gr.HTML("""
<div id="bb-header">
<!-- Floating soap bubbles (CSS-only) -->
<div aria-hidden="true" style="position:absolute;inset:0;pointer-events:none;overflow:hidden;">
<!-- Big iridescent bubble left -->
<div style="position:absolute;width:72px;height:72px;border-radius:50%;
left:4%;top:10%;
background: radial-gradient(circle at 35% 30%,
rgba(255,255,255,.7) 0%,
rgba(197,180,240,.3) 35%,
rgba(110,231,199,.15) 65%,
rgba(168,216,240,.08) 100%);
border: 1.5px solid rgba(255,255,255,.8);
box-shadow: inset 0 0 16px rgba(197,180,240,.4),
0 4px 20px rgba(197,180,240,.2);
animation: floatBubble 6.5s ease-in-out infinite;"></div>
<!-- Small bubble top-left cluster -->
<div style="position:absolute;width:32px;height:32px;border-radius:50%;
left:9%;top:55%;
background: radial-gradient(circle at 35% 30%,
rgba(255,255,255,.65) 0%, rgba(255,179,200,.25) 50%, transparent 100%);
border: 1.5px solid rgba(255,255,255,.75);
animation: floatBubble 5.1s ease-in-out infinite 0.8s;"></div>
<div style="position:absolute;width:18px;height:18px;border-radius:50%;
left:13%;top:75%;
background: radial-gradient(circle at 35% 30%,
rgba(255,255,255,.6) 0%, rgba(110,231,199,.3) 60%, transparent 100%);
border: 1px solid rgba(255,255,255,.7);
animation: floatBubble 4.4s ease-in-out infinite 1.4s;"></div>
<!-- Right side bubbles -->
<div style="position:absolute;width:56px;height:56px;border-radius:50%;
right:5%;top:8%;
background: radial-gradient(circle at 35% 30%,
rgba(255,255,255,.65) 0%, rgba(168,216,240,.3) 50%, transparent 100%);
border: 1.5px solid rgba(255,255,255,.75);
box-shadow: inset 0 0 12px rgba(168,216,240,.3),
0 4px 16px rgba(168,216,240,.15);
animation: floatBubble 7.2s ease-in-out infinite 1.9s;"></div>
<div style="position:absolute;width:24px;height:24px;border-radius:50%;
right:11%;top:60%;
background: radial-gradient(circle at 35% 30%,
rgba(255,255,255,.6) 0%, rgba(143,168,248,.25) 60%, transparent 100%);
border: 1px solid rgba(255,255,255,.65);
animation: floatBubble 5.8s ease-in-out infinite 0.5s;"></div>
<!-- Centre accent bubble -->
<div style="position:absolute;width:14px;height:14px;border-radius:50%;
left:50%;top:15%;
background: rgba(255,255,255,.55);
border: 1px solid rgba(255,255,255,.7);
animation: floatBubble 4.0s ease-in-out infinite 2.2s;"></div>
</div>
<!-- Content -->
<div style="position:relative;z-index:1;display:flex;align-items:center;
gap:22px;flex-wrap:wrap;">
<!-- Logo bubble -->
<div style="width:72px;height:72px;border-radius:50%;flex-shrink:0;
background: radial-gradient(circle at 35% 30%,
rgba(255,255,255,.85) 0%,
rgba(197,180,240,.45) 40%,
rgba(110,231,199,.25) 75%,
transparent 100%);
border: 2px solid rgba(255,255,255,.8);
display:flex;align-items:center;justify-content:center;
font-size:34px;
box-shadow: inset 0 0 24px rgba(197,180,240,.4),
0 8px 28px rgba(124,92,191,.2),
0 2px 8px rgba(124,92,191,.1);
animation: floatBubble 5.5s ease-in-out infinite, iridescent 8s linear infinite;">
🫧
</div>
<!-- Title block -->
<div>
<h1 style="margin:0;font-family:'Syne',sans-serif;font-size:34px;
font-weight:800;letter-spacing:-1px;line-height:1;
background: linear-gradient(125deg,
#7c5cbf 0%, #a48de8 35%, #6ee7c7 65%, #7c5cbf 100%);
background-size: 300% auto;
-webkit-background-clip:text;-webkit-text-fill-color:transparent;
animation: shimmerSlide 5s linear infinite;">
BubbleBusters
</h1>
<p style="margin:8px 0 0;color:#9d8fc4;
font-family:'Nunito',sans-serif;font-size:13.5px;
letter-spacing:.4px;font-weight:600;">
AI Bubble Sentiment Analytics &nbsp;Β·&nbsp; RX12 &nbsp;Β·&nbsp; ESCP Europe
</p>
</div>
<!-- Right pill badges -->
<div style="margin-left:auto;display:flex;flex-direction:column;gap:7px;align-items:flex-end;">
<div style="background:linear-gradient(135deg,rgba(197,180,240,.35),rgba(110,231,199,.25));
border:1.5px solid rgba(255,255,255,.65);border-radius:50px;
padding:5px 14px;font-family:'Nunito',sans-serif;
font-size:11px;font-weight:800;color:#6b4fa8;letter-spacing:.5px;">
πŸŽ“ Group Project
</div>
<div style="background:rgba(255,255,255,.5);border:1.5px solid rgba(255,255,255,.65);
border-radius:50px;padding:5px 14px;
font-family:'Nunito',sans-serif;font-size:11px;
font-weight:700;color:#9d8fc4;letter-spacing:.3px;">
🐍 Python &nbsp;Β·&nbsp; πŸ“Š R &nbsp;Β·&nbsp; πŸ€– AI
</div>
</div>
</div>
</div>
""")
with gr.Tabs():
# ══════════════════════════════════════════════════════════
# TAB 1 β€” PIPELINE
# ══════════════════════════════════════════════════════════
with gr.Tab("βš™οΈ Pipeline"):
with gr.Row(equal_height=False):
# Status column
with gr.Column(scale=1, min_width=280):
gr.HTML('<div class="section-label">System Status</div>')
status_html = gr.HTML(value=render_status_html)
refresh_status_btn = gr.Button(
"πŸ”„ Refresh", elem_classes=["btn-secondary"]
)
# Runner column
with gr.Column(scale=2):
gr.HTML('<div class="section-label">Run Pipeline</div>')
if PY_KERNEL:
gr.HTML(
f'<div style="background:rgba(34,232,120,.07);'
f'border:1px solid rgba(34,232,120,.25);'
f'border-radius:10px;padding:10px 16px;font-size:12px;'
f'color:#22e878;margin-bottom:14px;'
f'font-family:\'Lato\',sans-serif;">'
f'✦ Notebook kernel ready &nbsp;·&nbsp; '
f'<code style="font-family:\'JetBrains Mono\',monospace;">'
f'{PY_KERNEL}</code></div>'
)
else:
gr.HTML(
f'<div style="background:rgba(255,87,112,.07);'
f'border:1px solid rgba(255,87,112,.25);'
f'border-radius:10px;padding:10px 16px;font-size:12px;'
f'color:#ff5770;margin-bottom:14px;'
f'font-family:\'Lato\',sans-serif;">'
f'βœ– Kernel unavailable β€” add '
f'<code style="font-family:\'JetBrains Mono\',monospace;">'
f'ipykernel</code> to requirements.txt<br>'
f'<span style="color:rgba(240,237,230,.30);font-size:11px;">'
f'{KERNEL_INIT_ERROR[:180]}</span>'
f'</div>'
)
with gr.Row():
btn_nb1 = gr.Button("πŸ“¦ Step 1: Data", elem_classes=["btn-secondary"])
btn_nb2 = gr.Button("🐍 Step 2a: Python", elem_classes=["btn-secondary"])
btn_r = gr.Button("πŸ“Š Step 2b: R", elem_classes=["btn-secondary"])
btn_all = gr.Button("πŸš€ Run Full Pipeline", elem_classes=["btn-primary"])
run_log = gr.Textbox(
label="Execution Log",
lines=18, max_lines=18,
interactive=False,
elem_id="pipeline-log",
autoscroll=True,
)
refresh_status_btn.click(fn=render_status_html, outputs=status_html)
btn_nb1.click(fn=run_datacreation, outputs=run_log)
btn_nb2.click(fn=run_pythonanalysis, outputs=run_log)
btn_r.click(fn=run_r, outputs=run_log)
btn_all.click(fn=run_full_pipeline, outputs=run_log)
# ══════════════════════════════════════════════════════════
# TAB 2 β€” ASSET PRICES
# ══════════════════════════════════════════════════════════
with gr.Tab("πŸ“ˆ Asset Prices"):
gr.HTML(
'<div style="color:#5a5a7a;font-size:13px;margin-bottom:16px;">'
'Track AI-related stocks in real time. Select tickers and a period '
'to compare normalised performance (base = 100).</div>'
)
with gr.Row():
with gr.Column(scale=3):
ticker_box = gr.Textbox(
label="Tickers (comma-separated)",
value=AI_TICKERS_DEFAULT,
placeholder="e.g. NVDA, MSFT, GOOGL, META",
)
with gr.Column(scale=1):
period_radio = gr.Radio(
choices=["1mo", "3mo", "6mo", "1y", "2y", "5y"],
value="6mo",
label="Period",
)
with gr.Column(scale=1):
fetch_btn = gr.Button("πŸ“‘ Fetch Prices", elem_classes=["btn-primary"])
gr.HTML('<div style="color:#3a3a5a;font-size:11px;margin:6px 0 4px;">Quick presets:</div>')
with gr.Row():
preset_mega = gr.Button("🏦 Mega-Cap AI", elem_classes=["btn-secondary"])
preset_semi = gr.Button("πŸ”§ Semiconductors", elem_classes=["btn-secondary"])
preset_pure = gr.Button("πŸ€– Pure-Play AI", elem_classes=["btn-secondary"])
with gr.Row(equal_height=False):
with gr.Column(scale=3):
price_chart = gr.Plot(label="", container=False)
with gr.Column(scale=1, min_width=220):
price_summary = gr.Markdown()
fetch_btn.click(
fn=lambda t, p: fetch_asset_prices(t, p),
inputs=[ticker_box, period_radio],
outputs=[price_chart, price_summary],
)
preset_mega.click(
fn=lambda p: fetch_asset_prices(AI_PRESET_MEGA, p),
inputs=period_radio, outputs=[price_chart, price_summary],
)
preset_semi.click(
fn=lambda p: fetch_asset_prices(AI_PRESET_SEMI, p),
inputs=period_radio, outputs=[price_chart, price_summary],
)
preset_pure.click(
fn=lambda p: fetch_asset_prices(AI_PRESET_PURE, p),
inputs=period_radio, outputs=[price_chart, price_summary],
)
# ══════════════════════════════════════════════════════════
# TAB 3 β€” SENTIMENT ANALYSIS
# ══════════════════════════════════════════════════════════
with gr.Tab("🎭 Sentiment Analysis"):
gr.HTML(
'<div style="color:#5a5a7a;font-size:13px;margin-bottom:14px;">'
'Interactive charts and R figures from the full analysis pipeline. '
'Run the pipeline first if charts are empty.</div>'
)
with gr.Row():
refresh_sent_btn = gr.Button("πŸ”„ Refresh All Charts", elem_classes=["btn-secondary"])
# KPI cards
kpi_html_comp = gr.HTML(value=render_kpi_cards)
# Main interactive charts
overview_chart_comp = gr.Plot(label="Sentiment Over Time", container=False)
with gr.Row():
with gr.Column():
topic_chart_comp = gr.Plot(label="By Topic", container=False)
with gr.Column():
platform_chart_comp = gr.Plot(label="By Platform", container=False)
with gr.Row():
with gr.Column():
risk_chart_comp = gr.Plot(label="Bubble Risk Score", container=False)
with gr.Column():
yearly_chart_comp = gr.Plot(label="Year-over-Year", container=False)
# R figures (static) inside accordion
with gr.Accordion("πŸ“Š R Analysis Figures (static)", open=False):
gr.HTML(
'<div style="color:#5a5a7a;font-size:12px;margin-bottom:10px;">'
'Generated by ranalysis_bubblebusters.ipynb (run locally).'
'</div>'
)
with gr.Row():
r1 = gr.Image(label="R01 Β· Monthly Trend", show_label=True)
r2 = gr.Image(label="R02 Β· Rolling Score", show_label=True)
with gr.Row():
r3 = gr.Image(label="R03 Β· Chi-Square Residuals", show_label=True)
r4 = gr.Image(label="R04 Β· Regression Coefficients", show_label=True)
with gr.Row():
r5 = gr.Image(label="R05 Β· Yearly Grouped Bars", show_label=True)
SENT_OUTPUTS = [
kpi_html_comp,
overview_chart_comp, topic_chart_comp,
platform_chart_comp, risk_chart_comp, yearly_chart_comp,
r1, r2, r3, r4, r5,
]
refresh_sent_btn.click(fn=refresh_sentiment, outputs=SENT_OUTPUTS)
# ══════════════════════════════════════════════════════════
# TAB 4 β€” AI CHAT
# ══════════════════════════════════════════════════════════
with gr.Tab("πŸ€– AI Chat"):
llm_badge = (
f'<span style="color:#22e878;font-family:\'Syne\',sans-serif;font-weight:700;">'
f'✦ LLM active β€” {MODEL_NAME}</span>'
if LLM_ENABLED else
f'<span style="color:#ffb830;font-family:\'Syne\',sans-serif;font-weight:700;">'
f'β—ˆ Keyword mode β€” set <code>HF_API_KEY</code> secret for full AI support</span>'
)
gr.HTML(
f'<div style="color:#5a5a7a;font-size:13px;margin-bottom:14px;">'
f'Ask questions about the AI bubble data. {llm_badge}</div>'
)
with gr.Row(equal_height=True):
with gr.Column(scale=1):
chatbot = gr.Chatbot(
label="Conversation",
height=430
)
user_msg = gr.Textbox(
label="Ask about the data",
placeholder=(
"e.g. Show me sentiment trends Β· "
"Which topics are most bearish? Β· "
"What's the current bubble risk?"
),
lines=1,
)
gr.Examples(
examples=[
"Show me sentiment trends over time",
"Which topics are most bearish about AI?",
"Compare sentiment across platforms",
"What is the latest bubble risk score?",
"Is sentiment getting more bullish or bearish recently?",
"Give me an overview of the dataset",
],
inputs=user_msg,
)
with gr.Column(scale=1):
chat_chart = gr.Plot(label="Data Visualisation", container=False)
user_msg.submit(
fn=ai_chat,
inputs=[user_msg, chatbot],
outputs=[chatbot, user_msg, chat_chart],
)
# On page load, populate sentiment charts if data is available
demo.load(fn=refresh_sentiment, outputs=SENT_OUTPUTS)
demo.launch(
allowed_paths=[str(BASE_DIR)],
css=CSS,
theme=gr.themes.Base(
primary_hue=gr.themes.colors.teal,
neutral_hue=gr.themes.colors.slate,
),
)