atascioglu commited on
Commit
f7155bb
·
verified ·
1 Parent(s): 2b32d51

Delete app.py

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