Rtkhoury/Coding_workshop_2
0
1import os2import re3import json4import time5import traceback6from pathlib import Path7from typing import Dict, Any, List, Tuple8 9import pandas as pd10import gradio as gr11import papermill as pm12import plotly.graph_objects as go13 14# Optional LLM (HuggingFace Inference API)15try:16 from huggingface_hub import InferenceClient17except Exception:18 InferenceClient = None19 20# =========================================================21# CONFIG22# =========================================================23 24BASE_DIR = Path(__file__).resolve().parent25 26NB1 = os.environ.get("NB1", "datacreation.ipynb").strip()27NB2 = os.environ.get("NB2", "pythonanalysis.ipynb").strip()28 29RUNS_DIR = BASE_DIR / "runs"30ART_DIR = BASE_DIR / "artifacts"31PY_FIG_DIR = ART_DIR / "py" / "figures"32PY_TAB_DIR = ART_DIR / "py" / "tables"33 34PAPERMILL_TIMEOUT = int(os.environ.get("PAPERMILL_TIMEOUT", "1800"))35MAX_PREVIEW_ROWS = int(os.environ.get("MAX_FILE_PREVIEW_ROWS", "50"))36MAX_LOG_CHARS = int(os.environ.get("MAX_LOG_CHARS", "8000"))37 38HF_API_KEY = os.environ.get("HF_API_KEY", "").strip()39MODEL_NAME = os.environ.get("MODEL_NAME", "deepseek-ai/DeepSeek-R1").strip()40HF_PROVIDER = os.environ.get("HF_PROVIDER", "novita").strip()41N8N_WEBHOOK_URL = os.environ.get("N8N_WEBHOOK_URL", "").strip()42 43LLM_ENABLED = bool(HF_API_KEY) and InferenceClient is not None44llm_client = (45 InferenceClient(provider=HF_PROVIDER, api_key=HF_API_KEY)46 if LLM_ENABLED47 else None48)49 50# =========================================================51# HELPERS52# =========================================================53 54def ensure_dirs():55 for p in [RUNS_DIR, ART_DIR, PY_FIG_DIR, PY_TAB_DIR]:56 p.mkdir(parents=True, exist_ok=True)57 58def stamp():59 return time.strftime("%Y%m%d-%H%M%S")60 61def tail(text: str, n: int = MAX_LOG_CHARS) -> str:62 return (text or "")[-n:]63 64def _ls(dir_path: Path, exts: Tuple[str, ...]) -> List[str]:65 if not dir_path.is_dir():66 return []67 return sorted(p.name for p in dir_path.iterdir() if p.is_file() and p.suffix.lower() in exts)68 69def _read_csv(path: Path) -> pd.DataFrame:70 return pd.read_csv(path, nrows=MAX_PREVIEW_ROWS)71 72def _read_json(path: Path):73 with path.open(encoding="utf-8") as f:74 return json.load(f)75 76def artifacts_index() -> Dict[str, Any]:77 return {78 "python": {79 "figures": _ls(PY_FIG_DIR, (".png", ".jpg", ".jpeg")),80 "tables": _ls(PY_TAB_DIR, (".csv", ".json")),81 },82 }83 84# =========================================================85# PIPELINE RUNNERS86# =========================================================87 88def run_notebook(nb_name: str) -> str:89 ensure_dirs()90 nb_in = BASE_DIR / nb_name91 if not nb_in.exists():92 return f"ERROR: {nb_name} not found."93 nb_out = RUNS_DIR / f"run_{stamp()}_{nb_name}"94 pm.execute_notebook(95 input_path=str(nb_in),96 output_path=str(nb_out),97 cwd=str(BASE_DIR),98 log_output=True,99 progress_bar=False,100 request_save_on_cell_execute=True,101 execution_timeout=PAPERMILL_TIMEOUT,102 )103 return f"Executed {nb_name}"104 105 106def run_datacreation() -> str:107 try:108 log = run_notebook(NB1)109 csvs = [f.name for f in BASE_DIR.glob("*.csv")]110 return f"OK {log}\n\nCSVs now in /app:\n" + "\n".join(f" - {c}" for c in sorted(csvs))111 except Exception as e:112 return f"FAILED {e}\n\n{traceback.format_exc()[-2000:]}"113 114 115def run_pythonanalysis() -> str:116 try:117 log = run_notebook(NB2)118 idx = artifacts_index()119 figs = idx["python"]["figures"]120 tabs = idx["python"]["tables"]121 return (122 f"OK {log}\n\n"123 f"Figures: {', '.join(figs) or '(none)'}\n"124 f"Tables: {', '.join(tabs) or '(none)'}"125 )126 except Exception as e:127 return f"FAILED {e}\n\n{traceback.format_exc()[-2000:]}"128 129 130def run_full_pipeline() -> str:131 logs = []132 logs.append("=" * 50)133 logs.append("STEP 1/2: Data Creation (web scraping + synthetic data)")134 logs.append("=" * 50)135 logs.append(run_datacreation())136 logs.append("")137 logs.append("=" * 50)138 logs.append("STEP 2/2: Python Analysis (sentiment, ARIMA, dashboard)")139 logs.append("=" * 50)140 logs.append(run_pythonanalysis())141 return "\n".join(logs)142 143 144# =========================================================145# GALLERY LOADERS146# =========================================================147 148def _load_all_figures() -> List[Tuple[str, str]]:149 """Return list of (filepath, caption) for Gallery."""150 items = []151 for p in sorted(PY_FIG_DIR.glob("*.png")):152 items.append((str(p), p.stem.replace('_', ' ').title()))153 return items154 155 156def _load_table_safe(path: Path) -> pd.DataFrame:157 try:158 if path.suffix == ".json":159 obj = _read_json(path)160 if isinstance(obj, dict):161 return pd.DataFrame([obj])162 return pd.DataFrame(obj)163 return _read_csv(path)164 except Exception as e:165 return pd.DataFrame([{"error": str(e)}])166 167 168def refresh_gallery():169 """Called when user clicks Refresh on Gallery tab."""170 figures = _load_all_figures()171 idx = artifacts_index()172 173 table_choices = list(idx["python"]["tables"])174 175 default_df = pd.DataFrame()176 if table_choices:177 default_df = _load_table_safe(PY_TAB_DIR / table_choices[0])178 179 return (180 figures if figures else [],181 gr.update(choices=table_choices, value=table_choices[0] if table_choices else None),182 default_df,183 )184 185 186def on_table_select(choice: str):187 if not choice:188 return pd.DataFrame([{"hint": "Select a table above."}])189 path = PY_TAB_DIR / choice190 if not path.exists():191 return pd.DataFrame([{"error": f"File not found: {choice}"}])192 return _load_table_safe(path)193 194 195# =========================================================196# KPI LOADER197# =========================================================198 199def load_kpis() -> Dict[str, Any]:200 for candidate in [PY_TAB_DIR / "kpis.json", PY_FIG_DIR / "kpis.json"]:201 if candidate.exists():202 try:203 return _read_json(candidate)204 except Exception:205 pass206 return {}207 208 209# =========================================================210# AI DASHBOARD -- LLM picks what to display211# =========================================================212 213DASHBOARD_SYSTEM = """You are an AI dashboard assistant for a book-sales analytics app.214The user asks questions or requests about their data. You have access to pre-computed215artifacts from a Python analysis pipeline.216 217AVAILABLE ARTIFACTS (only reference ones that exist):218{artifacts_json}219 220KPI SUMMARY: {kpis_json}221 222YOUR JOB:2231. Answer the user's question conversationally using the KPIs and your knowledge of the artifacts.2242. At the END of your response, output a JSON block (fenced with ```json ... ```) that tells225 the dashboard which artifact to display. The JSON must have this shape:226 {{"show": "figure"|"table"|"none", "scope": "python", "filename": "..."}}227 228 - Use "show": "figure" to display a chart image.229 - Use "show": "table" to display a CSV/JSON table.230 - Use "show": "none" if no artifact is relevant.231 232RULES:233- If the user asks about sales trends or forecasting by title, show sales_trends or arima figures.234- If the user asks about sentiment, show sentiment figure or sentiment_counts table.235- If the user asks about forecast accuracy or ARIMA, show arima figures.236- If the user asks about top sellers, show top_titles_by_units_sold.csv.237- If the user asks a general data question, pick the most relevant artifact.238- Keep your answer concise (2-4 sentences), then the JSON block.239"""240 241JSON_BLOCK_RE = re.compile(r"```json\s*(\{.*?\})\s*```", re.DOTALL)242FALLBACK_JSON_RE = re.compile(r"\{[^{}]*\"show\"[^{}]*\}", re.DOTALL)243 244 245def _parse_display_directive(text: str) -> Dict[str, str]:246 m = JSON_BLOCK_RE.search(text)247 if m:248 try:249 return json.loads(m.group(1))250 except json.JSONDecodeError:251 pass252 m = FALLBACK_JSON_RE.search(text)253 if m:254 try:255 return json.loads(m.group(0))256 except json.JSONDecodeError:257 pass258 return {"show": "none"}259 260 261def _clean_response(text: str) -> str:262 """Strip the JSON directive block from the displayed response."""263 return JSON_BLOCK_RE.sub("", text).strip()264 265 266def _n8n_call(msg: str) -> Tuple[str, Dict]:267 """Call the student's n8n webhook and return (reply, directive)."""268 import requests as req269 try:270 resp = req.post(N8N_WEBHOOK_URL, json={"question": msg}, timeout=20)271 data = resp.json()272 answer = data.get("answer", "No response from n8n workflow.")273 chart = data.get("chart", "none")274 if chart and chart != "none":275 return answer, {"show": "figure", "chart": chart}276 return answer, {"show": "none"}277 except Exception as e:278 return f"n8n error: {e}. Falling back to keyword matching.", None279 280 281def ai_chat(user_msg: str, history: list):282 """Chat function for the AI Dashboard tab."""283 if not user_msg or not user_msg.strip():284 return history, "", None, None285 286 idx = artifacts_index()287 kpis = load_kpis()288 289 # Priority: n8n webhook > HF LLM > keyword fallback290 if N8N_WEBHOOK_URL:291 reply, directive = _n8n_call(user_msg)292 if directive is None:293 reply_fb, directive = _keyword_fallback(user_msg, idx, kpis)294 reply += "\n\n" + reply_fb295 elif not LLM_ENABLED:296 reply, directive = _keyword_fallback(user_msg, idx, kpis)297 else:298 system = DASHBOARD_SYSTEM.format(299 artifacts_json=json.dumps(idx, indent=2),300 kpis_json=json.dumps(kpis, indent=2) if kpis else "(no KPIs yet, run the pipeline first)",301 )302 msgs = [{"role": "system", "content": system}]303 for entry in (history or [])[-6:]:304 msgs.append(entry)305 msgs.append({"role": "user", "content": user_msg})306 307 try:308 r = llm_client.chat_completion(309 model=MODEL_NAME,310 messages=msgs,311 temperature=0.3,312 max_tokens=600,313 stream=False,314 )315 raw = (316 r["choices"][0]["message"]["content"]317 if isinstance(r, dict)318 else r.choices[0].message.content319 )320 directive = _parse_display_directive(raw)321 reply = _clean_response(raw)322 except Exception as e:323 reply = f"LLM error: {e}. Falling back to keyword matching."324 reply_fb, directive = _keyword_fallback(user_msg, idx, kpis)325 reply += "\n\n" + reply_fb326 327 # Resolve artifacts — build interactive Plotly charts when possible328 chart_out = None329 tab_out = None330 show = directive.get("show", "none")331 fname = directive.get("filename", "")332 chart_name = directive.get("chart", "")333 334 # Interactive chart builders keyed by name335 chart_builders = {336 "sales": build_sales_chart,337 "sentiment": build_sentiment_chart,338 "top_sellers": build_top_sellers_chart,339 }340 341 if chart_name and chart_name in chart_builders:342 chart_out = chart_builders[chart_name]()343 elif show == "figure" and fname:344 # Fallback: try to match filename to a chart builder345 if "sales_trend" in fname:346 chart_out = build_sales_chart()347 elif "sentiment" in fname:348 chart_out = build_sentiment_chart()349 elif "arima" in fname or "forecast" in fname:350 chart_out = build_sales_chart() # closest interactive equivalent351 else:352 chart_out = _empty_chart(f"No interactive chart for {fname}")353 354 if show == "table" and fname:355 fp = PY_TAB_DIR / fname356 if fp.exists():357 tab_out = _load_table_safe(fp)358 else:359 reply += f"\n\n*(Could not find table: {fname})*"360 361 new_history = (history or []) + [362 {"role": "user", "content": user_msg},363 {"role": "assistant", "content": reply},364 ]365 366 return new_history, "", chart_out, tab_out367 368 369def _keyword_fallback(msg: str, idx: Dict, kpis: Dict) -> Tuple[str, Dict]:370 """Simple keyword matcher when LLM is unavailable."""371 msg_lower = msg.lower()372 373 if not idx["python"]["figures"] and not idx["python"]["tables"]:374 return (375 "No artifacts found yet. Please run the pipeline first (Tab 1), "376 "then come back here to explore the results.",377 {"show": "none"},378 )379 380 kpi_text = ""381 if kpis:382 total = kpis.get("total_units_sold", 0)383 kpi_text = (384 f"Quick summary: **{kpis.get('n_titles', '?')}** book titles across "385 f"**{kpis.get('n_months', '?')}** months, with **{total:,.0f}** total units sold."386 )387 388 if any(w in msg_lower for w in ["trend", "sales trend", "monthly sale"]):389 return (390 f"Here are the sales trends. {kpi_text}",391 {"show": "figure", "chart": "sales"},392 )393 394 if any(w in msg_lower for w in ["sentiment", "review", "positive", "negative"]):395 return (396 f"Here is the sentiment distribution across sampled book titles. {kpi_text}",397 {"show": "figure", "chart": "sentiment"},398 )399 400 if any(w in msg_lower for w in ["arima", "forecast", "predict"]):401 return (402 f"Here are the sales trends and forecasts. {kpi_text}",403 {"show": "figure", "chart": "sales"},404 )405 406 if any(w in msg_lower for w in ["top", "best sell", "popular", "rank"]):407 return (408 f"Here are the top-selling titles by units sold. {kpi_text}",409 {"show": "table", "scope": "python", "filename": "top_titles_by_units_sold.csv"},410 )411 412 if any(w in msg_lower for w in ["price", "pricing", "decision"]):413 return (414 f"Here are the pricing decisions. {kpi_text}",415 {"show": "table", "scope": "python", "filename": "pricing_decisions.csv"},416 )417 418 if any(w in msg_lower for w in ["dashboard", "overview", "summary", "kpi"]):419 return (420 f"Dashboard overview: {kpi_text}\n\nAsk me about sales trends, sentiment, forecasts, "421 "pricing, or top sellers to see specific visualizations.",422 {"show": "table", "scope": "python", "filename": "df_dashboard.csv"},423 )424 425 # Default426 return (427 f"I can show you various analyses. {kpi_text}\n\n"428 "Try asking about: **sales trends**, **sentiment**, **ARIMA forecasts**, "429 "**pricing decisions**, **top sellers**, or **dashboard overview**.",430 {"show": "none"},431 )432 433 434# =========================================================435# KPI CARDS (BubbleBusters style)436# =========================================================437 438def render_kpi_cards() -> str:439 kpis = load_kpis()440 if not kpis:441 return (442 '<div style="background:rgba(255,255,255,.65);backdrop-filter:blur(16px);'443 'border-radius:20px;padding:28px;text-align:center;'444 'border:1.5px solid rgba(255,255,255,.7);'445 'box-shadow:0 8px 32px rgba(124,92,191,.08);">'446 '<div style="font-size:36px;margin-bottom:10px;">📊</div>'447 '<div style="color:#a48de8;font-size:14px;'448 'font-weight:800;margin-bottom:6px;">No data yet</div>'449 '<div style="color:#9d8fc4;font-size:12px;">'450 'Run the pipeline to populate these cards.</div>'451 '</div>'452 )453 454 def card(icon, label, value, colour):455 return f"""456 <div style="background:rgba(255,255,255,.72);backdrop-filter:blur(16px);457 border-radius:20px;padding:18px 14px 16px;text-align:center;458 border:1.5px solid rgba(255,255,255,.8);459 box-shadow:0 4px 16px rgba(124,92,191,.08);460 border-top:3px solid {colour};">461 <div style="font-size:26px;margin-bottom:7px;line-height:1;">{icon}</div>462 <div style="color:#9d8fc4;font-size:9.5px;text-transform:uppercase;463 letter-spacing:1.8px;margin-bottom:7px;font-weight:800;">{label}</div>464 <div style="color:#2d1f4e;font-size:16px;font-weight:800;">{value}</div>465 </div>"""466 467 kpi_config = [468 ("n_titles", "📚", "Book Titles", "#a48de8"),469 ("n_months", "📅", "Time Periods", "#7aa6f8"),470 ("total_units_sold", "📦", "Units Sold", "#6ee7c7"),471 ("total_revenue", "💰", "Revenue", "#3dcba8"),472 ]473 474 html = (475 '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));'476 'gap:12px;margin-bottom:24px;">'477 )478 for key, icon, label, colour in kpi_config:479 val = kpis.get(key)480 if val is None:481 continue482 if isinstance(val, (int, float)) and val > 100:483 val = f"{val:,.0f}"484 html += card(icon, label, str(val), colour)485 # Extra KPIs not in config486 known = {k for k, *_ in kpi_config}487 for key, val in kpis.items():488 if key not in known:489 label = key.replace("_", " ").title()490 if isinstance(val, (int, float)) and val > 100:491 val = f"{val:,.0f}"492 html += card("📈", label, str(val), "#8fa8f8")493 html += "</div>"494 return html495 496 497# =========================================================498# INTERACTIVE PLOTLY CHARTS (BubbleBusters style)499# =========================================================500 501CHART_PALETTE = ["#7c5cbf", "#2ec4a0", "#e8537a", "#e8a230", "#5e8fef",502 "#c45ea8", "#3dbacc", "#a0522d", "#6aaa3a", "#d46060"]503 504def _styled_layout(**kwargs) -> dict:505 defaults = dict(506 template="plotly_white",507 paper_bgcolor="rgba(255,255,255,0.95)",508 plot_bgcolor="rgba(255,255,255,0.98)",509 font=dict(family="system-ui, sans-serif", color="#2d1f4e", size=12),510 margin=dict(l=60, r=20, t=70, b=70),511 legend=dict(512 orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1,513 bgcolor="rgba(255,255,255,0.92)",514 bordercolor="rgba(124,92,191,0.35)", borderwidth=1,515 ),516 title=dict(font=dict(size=15, color="#4b2d8a")),517 )518 defaults.update(kwargs)519 return defaults520 521 522def _empty_chart(title: str) -> go.Figure:523 fig = go.Figure()524 fig.update_layout(525 title=title, height=420, template="plotly_white",526 paper_bgcolor="rgba(255,255,255,0.95)",527 annotations=[dict(text="Run the pipeline to generate data",528 x=0.5, y=0.5, xref="paper", yref="paper", showarrow=False,529 font=dict(size=14, color="rgba(124,92,191,0.5)"))],530 )531 return fig532 533 534def build_sales_chart() -> go.Figure:535 path = PY_TAB_DIR / "df_dashboard.csv"536 if not path.exists():537 return _empty_chart("Sales Trends — run the pipeline first")538 df = pd.read_csv(path)539 date_col = next((c for c in df.columns if "month" in c.lower() or "date" in c.lower()), None)540 val_cols = [c for c in df.columns if c != date_col and df[c].dtype in ("float64", "int64")]541 if not date_col or not val_cols:542 return _empty_chart("Could not auto-detect columns in df_dashboard.csv")543 df[date_col] = pd.to_datetime(df[date_col], errors="coerce")544 fig = go.Figure()545 for i, col in enumerate(val_cols):546 fig.add_trace(go.Scatter(547 x=df[date_col], y=df[col], name=col.replace("_", " ").title(),548 mode="lines+markers", line=dict(color=CHART_PALETTE[i % len(CHART_PALETTE)], width=2),549 marker=dict(size=4),550 hovertemplate=f"<b>{col.replace('_',' ').title()}</b><br>%{{x|%b %Y}}: %{{y:,.0f}}<extra></extra>",551 ))552 fig.update_layout(**_styled_layout(height=450, hovermode="x unified",553 title=dict(text="Monthly Overview")))554 fig.update_xaxes(gridcolor="rgba(124,92,191,0.15)", showgrid=True)555 fig.update_yaxes(gridcolor="rgba(124,92,191,0.15)", showgrid=True)556 return fig557 558 559def build_sentiment_chart() -> go.Figure:560 path = PY_TAB_DIR / "sentiment_counts_sampled.csv"561 if not path.exists():562 return _empty_chart("Sentiment Distribution — run the pipeline first")563 df = pd.read_csv(path)564 title_col = df.columns[0]565 sent_cols = [c for c in ["negative", "neutral", "positive"] if c in df.columns]566 if not sent_cols:567 return _empty_chart("No sentiment columns found in CSV")568 colors = {"negative": "#e8537a", "neutral": "#5e8fef", "positive": "#2ec4a0"}569 fig = go.Figure()570 for col in sent_cols:571 fig.add_trace(go.Bar(572 name=col.title(), y=df[title_col], x=df[col],573 orientation="h", marker_color=colors.get(col, "#888"),574 hovertemplate=f"<b>{col.title()}</b>: %{{x}}<extra></extra>",575 ))576 fig.update_layout(**_styled_layout(577 height=max(400, len(df) * 28), barmode="stack",578 title=dict(text="Sentiment Distribution by Book"),579 ))580 fig.update_xaxes(title="Number of Reviews")581 fig.update_yaxes(autorange="reversed")582 return fig583 584 585def build_top_sellers_chart() -> go.Figure:586 path = PY_TAB_DIR / "top_titles_by_units_sold.csv"587 if not path.exists():588 return _empty_chart("Top Sellers — run the pipeline first")589 df = pd.read_csv(path).head(15)590 title_col = next((c for c in df.columns if "title" in c.lower()), df.columns[0])591 val_col = next((c for c in df.columns if "unit" in c.lower() or "sold" in c.lower()), df.columns[-1])592 fig = go.Figure(go.Bar(593 y=df[title_col], x=df[val_col], orientation="h",594 marker=dict(color=df[val_col], colorscale=[[0, "#c5b4f0"], [1, "#7c5cbf"]]),595 hovertemplate="<b>%{y}</b><br>Units: %{x:,.0f}<extra></extra>",596 ))597 fig.update_layout(**_styled_layout(598 height=max(400, len(df) * 30),599 title=dict(text="Top Selling Titles"), showlegend=False,600 ))601 fig.update_yaxes(autorange="reversed")602 fig.update_xaxes(title="Total Units Sold")603 return fig604 605 606def refresh_dashboard():607 return render_kpi_cards(), build_sales_chart(), build_sentiment_chart(), build_top_sellers_chart()608 609 610# =========================================================611# UI612# =========================================================613 614ensure_dirs()615 616def load_css() -> str:617 css_path = BASE_DIR / "style.css"618 return css_path.read_text(encoding="utf-8") if css_path.exists() else ""619 620 621with gr.Blocks(title="AIBDM 2026 Workshop App") as demo:622 623 gr.Markdown(624 "# SE21 App Template\n"625 "*This is an app template for SE21 students*",626 elem_id="escp_title",627 )628 629 # ===========================================================630 # TAB 1 -- Pipeline Runner631 # ===========================================================632 with gr.Tab("Pipeline Runner"):633 gr.Markdown()634 635 with gr.Row():636 with gr.Column(scale=1):637 btn_nb1 = gr.Button("Step 1: Data Creation", variant="secondary")638 with gr.Column(scale=1):639 btn_nb2 = gr.Button("Step 2: Python Analysis", variant="secondary")640 641 with gr.Row():642 btn_all = gr.Button("Run Full Pipeline (Both Steps)", variant="primary")643 644 run_log = gr.Textbox(645 label="Execution Log",646 lines=18,647 max_lines=30,648 interactive=False,649 )650 651 btn_nb1.click(run_datacreation, outputs=[run_log])652 btn_nb2.click(run_pythonanalysis, outputs=[run_log])653 btn_all.click(run_full_pipeline, outputs=[run_log])654 655 # ===========================================================656 # TAB 2 -- Dashboard (KPIs + Interactive Charts + Gallery)657 # ===========================================================658 with gr.Tab("Dashboard"):659 kpi_html = gr.HTML(value=render_kpi_cards)660 661 refresh_btn = gr.Button("Refresh Dashboard", variant="primary")662 663 gr.Markdown("#### Interactive Charts")664 chart_sales = gr.Plot(label="Monthly Overview")665 chart_sentiment = gr.Plot(label="Sentiment Distribution")666 chart_top = gr.Plot(label="Top Sellers")667 668 gr.Markdown("#### Static Figures (from notebooks)")669 gallery = gr.Gallery(670 label="Generated Figures",671 columns=2,672 height=480,673 object_fit="contain",674 )675 676 gr.Markdown("#### Data Tables")677 table_dropdown = gr.Dropdown(678 label="Select a table to view",679 choices=[],680 interactive=True,681 )682 table_display = gr.Dataframe(683 label="Table Preview",684 interactive=False,685 )686 687 def _on_refresh():688 kpi, c1, c2, c3 = refresh_dashboard()689 figs, dd, df = refresh_gallery()690 return kpi, c1, c2, c3, figs, dd, df691 692 refresh_btn.click(693 _on_refresh,694 outputs=[kpi_html, chart_sales, chart_sentiment, chart_top,695 gallery, table_dropdown, table_display],696 )697 table_dropdown.change(698 on_table_select,699 inputs=[table_dropdown],700 outputs=[table_display],701 )702 703 # ===========================================================704 # TAB 3 -- AI Dashboard705 # ===========================================================706 with gr.Tab('"AI" Dashboard'):707 _ai_status = (708 "Connected to your **n8n workflow**." if N8N_WEBHOOK_URL709 else "**LLM active.**" if LLM_ENABLED710 else "Using **keyword matching**. Upgrade options: "711 "set `N8N_WEBHOOK_URL` to connect your n8n workflow, "712 "or set `HF_API_KEY` for direct LLM access."713 )714 gr.Markdown(715 "### Ask questions, get interactive visualisations\n\n"716 f"Type a question and the system will pick the right interactive chart or table. {_ai_status}"717 )718 719 with gr.Row(equal_height=True):720 with gr.Column(scale=1):721 chatbot = gr.Chatbot(722 label="Conversation",723 height=380,724 )725 user_input = gr.Textbox(726 label="Ask about your data",727 placeholder="e.g. Show me sales trends / What are the top sellers? / Sentiment analysis",728 lines=1,729 )730 gr.Examples(731 examples=[732 "Show me the sales trends",733 "What does the sentiment look like?",734 "Which titles sell the most?",735 "Show the ARIMA forecasts",736 "What are the pricing decisions?",737 "Give me a dashboard overview",738 ],739 inputs=user_input,740 )741 742 with gr.Column(scale=1):743 ai_figure = gr.Plot(744 label="Interactive Chart",745 )746 ai_table = gr.Dataframe(747 label="Data Table",748 interactive=False,749 )750 751 user_input.submit(752 ai_chat,753 inputs=[user_input, chatbot],754 outputs=[chatbot, user_input, ai_figure, ai_table],755 )756 757 758demo.launch(css=load_css(), allowed_paths=[str(BASE_DIR)])759 