alessandro111 commited on
Commit
3b7d5eb
·
verified ·
1 Parent(s): cf9b515

Upload 10 files

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ background_top.png filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ ENV DEBIAN_FRONTEND=noninteractive
4
+ ENV PYTHONDONTWRITEBYTECODE=1
5
+ ENV PYTHONUNBUFFERED=1
6
+
7
+ ENV GRADIO_SERVER_NAME=0.0.0.0
8
+ ENV GRADIO_SERVER_PORT=7860
9
+
10
+ WORKDIR /app
11
+ COPY . /app
12
+
13
+ # Python deps (from requirements.txt)
14
+ RUN pip install --no-cache-dir -r requirements.txt
15
+
16
+ # Notebook execution deps
17
+ RUN pip install --no-cache-dir notebook ipykernel papermill
18
+
19
+ # Pre-install packages the notebooks use via !pip install
20
+ RUN pip install --no-cache-dir textblob faker vaderSentiment transformers
21
+
22
+ RUN python -m ipykernel install --user --name python3 --display-name "Python 3"
23
+
24
+ EXPOSE 7860
25
+
26
+ CMD ["python", "app.py"]
README.md CHANGED
@@ -1,10 +1,11 @@
1
  ---
2
- title: Hospitality
3
- emoji: 👀
4
- colorFrom: red
5
- colorTo: blue
6
  sdk: docker
7
  pinned: false
 
8
  ---
9
 
10
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: SE21 App Template
3
+ emoji: 📊
4
+ colorFrom: blue
5
+ colorTo: purple
6
  sdk: docker
7
  pinned: false
8
+ short_description: AI-enhanced analytics dashboard template for SE21 students
9
  ---
10
 
11
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
app_hospitality_gradio.py ADDED
@@ -0,0 +1,638 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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, Optional
8
+
9
+ import pandas as pd
10
+ import gradio as gr
11
+ import plotly.graph_objects as go
12
+
13
+ try:
14
+ import papermill as pm
15
+ except Exception:
16
+ pm = None
17
+
18
+ try:
19
+ from huggingface_hub import InferenceClient
20
+ except Exception:
21
+ InferenceClient = None
22
+
23
+ # =========================================================
24
+ # CONFIG — ITALY HOSPITALITY MARKET INSIGHT ASSISTANT
25
+ # =========================================================
26
+
27
+ BASE_DIR = Path(__file__).resolve().parent
28
+
29
+ NB1 = os.environ.get("NB1", "1_Data_Creation_Italy_Hospitality.ipynb").strip()
30
+ NB2 = os.environ.get("NB2", "2a_Python_Analysis_Italy_Hospitality.ipynb").strip()
31
+
32
+ CLEANED_CSV = os.environ.get("CLEANED_CSV", "italy_hospitality_market_cleaned.csv").strip()
33
+ ENRICHED_CSV = os.environ.get("ENRICHED_CSV", "italy_hospitality_market_enriched_synthetic.csv").strip()
34
+
35
+ RUNS_DIR = BASE_DIR / "runs"
36
+ ART_DIR = BASE_DIR / "artifacts"
37
+ PY_FIG_DIR = ART_DIR / "py" / "figures"
38
+ PY_TAB_DIR = ART_DIR / "py" / "tables"
39
+
40
+ PAPERMILL_TIMEOUT = int(os.environ.get("PAPERMILL_TIMEOUT", "1800"))
41
+ MAX_PREVIEW_ROWS = int(os.environ.get("MAX_FILE_PREVIEW_ROWS", "80"))
42
+ MAX_LOG_CHARS = int(os.environ.get("MAX_LOG_CHARS", "8000"))
43
+
44
+ # Hugging Face Inference API
45
+ HF_API_KEY = os.environ.get("HF_API_KEY", "").strip()
46
+ MODEL_NAME = os.environ.get("MODEL_NAME", "deepseek-ai/DeepSeek-R1").strip()
47
+ HF_PROVIDER = os.environ.get("HF_PROVIDER", "novita").strip()
48
+
49
+ # Optional n8n automation webhook. Expected JSON response:
50
+ # {"answer": "...", "chart": "risk|investment|city|region|overview|none"}
51
+ N8N_WEBHOOK_URL = os.environ.get("N8N_WEBHOOK_URL", "").strip()
52
+
53
+ LLM_ENABLED = bool(HF_API_KEY) and InferenceClient is not None
54
+ llm_client = InferenceClient(provider=HF_PROVIDER, api_key=HF_API_KEY) if LLM_ENABLED else None
55
+
56
+ CHART_PALETTE = ["#7c5cbf", "#2ec4a0", "#e8537a", "#e8a230", "#5e8fef", "#c45ea8"]
57
+
58
+ # =========================================================
59
+ # HELPERS
60
+ # =========================================================
61
+
62
+ def ensure_dirs():
63
+ for p in [RUNS_DIR, ART_DIR, PY_FIG_DIR, PY_TAB_DIR]:
64
+ p.mkdir(parents=True, exist_ok=True)
65
+
66
+ def stamp() -> str:
67
+ return time.strftime("%Y%m%d-%H%M%S")
68
+
69
+ def tail(text: str, n: int = MAX_LOG_CHARS) -> str:
70
+ return (text or "")[-n:]
71
+
72
+ def csv_path(prefer_enriched: bool = True) -> Path:
73
+ enriched = BASE_DIR / ENRICHED_CSV
74
+ cleaned = BASE_DIR / CLEANED_CSV
75
+ if prefer_enriched and enriched.exists():
76
+ return enriched
77
+ if cleaned.exists():
78
+ return cleaned
79
+ return enriched
80
+
81
+ def read_market_data(prefer_enriched: bool = True) -> pd.DataFrame:
82
+ path = csv_path(prefer_enriched)
83
+ if not path.exists():
84
+ return pd.DataFrame(columns=["section", "location", "entity", "metric_name", "value", "unit", "period", "source_page", "note"])
85
+ df = pd.read_csv(path)
86
+ df["value"] = pd.to_numeric(df.get("value"), errors="coerce")
87
+ return df
88
+
89
+ def _ls(dir_path: Path, exts: Tuple[str, ...]) -> List[str]:
90
+ if not dir_path.is_dir():
91
+ return []
92
+ return sorted(p.name for p in dir_path.iterdir() if p.is_file() and p.suffix.lower() in exts)
93
+
94
+ def _read_csv(path: Path) -> pd.DataFrame:
95
+ return pd.read_csv(path, nrows=MAX_PREVIEW_ROWS)
96
+
97
+ def _read_json(path: Path):
98
+ with path.open(encoding="utf-8") as f:
99
+ return json.load(f)
100
+
101
+ def artifacts_index() -> Dict[str, Any]:
102
+ return {
103
+ "core_files": {
104
+ "cleaned_dataset": CLEANED_CSV if (BASE_DIR / CLEANED_CSV).exists() else None,
105
+ "enriched_synthetic_dataset": ENRICHED_CSV if (BASE_DIR / ENRICHED_CSV).exists() else None,
106
+ },
107
+ "python": {
108
+ "figures": _ls(PY_FIG_DIR, (".png", ".jpg", ".jpeg")),
109
+ "tables": _ls(PY_TAB_DIR, (".csv", ".json")),
110
+ },
111
+ }
112
+
113
+ def pivot_metrics(df: pd.DataFrame, section: Optional[str] = None, entity: Optional[str] = None) -> pd.DataFrame:
114
+ d = df.copy()
115
+ if section:
116
+ d = d[d["section"].eq(section)]
117
+ if entity:
118
+ d = d[d["entity"].eq(entity)]
119
+ if d.empty:
120
+ return pd.DataFrame()
121
+ wide = d.pivot_table(index=["location", "entity"], columns="metric_name", values="value", aggfunc="first").reset_index()
122
+ wide.columns.name = None
123
+ return wide
124
+
125
+ # =========================================================
126
+ # PIPELINE RUNNERS
127
+ # =========================================================
128
+
129
+ def run_notebook(nb_name: str) -> str:
130
+ ensure_dirs()
131
+ if pm is None:
132
+ return "ERROR: papermill is not installed. Add it to requirements.txt."
133
+ nb_in = BASE_DIR / nb_name
134
+ if not nb_in.exists():
135
+ return f"ERROR: {nb_name} not found in {BASE_DIR}."
136
+ nb_out = RUNS_DIR / f"run_{stamp()}_{nb_name}"
137
+ pm.execute_notebook(
138
+ input_path=str(nb_in),
139
+ output_path=str(nb_out),
140
+ cwd=str(BASE_DIR),
141
+ log_output=True,
142
+ progress_bar=False,
143
+ request_save_on_cell_execute=True,
144
+ execution_timeout=PAPERMILL_TIMEOUT,
145
+ )
146
+ return f"Executed {nb_name}. Output saved to {nb_out.name}"
147
+
148
+ def run_datacreation() -> str:
149
+ try:
150
+ log = run_notebook(NB1)
151
+ csvs = sorted(p.name for p in BASE_DIR.glob("*.csv"))
152
+ return f"OK — {log}\n\nCSVs available:\n" + "\n".join(f" - {c}" for c in csvs)
153
+ except Exception as e:
154
+ return f"FAILED — {e}\n\n{traceback.format_exc()[-2000:]}"
155
+
156
+ def run_pythonanalysis() -> str:
157
+ try:
158
+ log = run_notebook(NB2)
159
+ idx = artifacts_index()
160
+ figs = idx["python"]["figures"]
161
+ tabs = idx["python"]["tables"]
162
+ return f"OK — {log}\n\nFigures: {', '.join(figs) or '(none)'}\nTables: {', '.join(tabs) or '(none)'}"
163
+ except Exception as e:
164
+ return f"FAILED — {e}\n\n{traceback.format_exc()[-2000:]}"
165
+
166
+ def run_full_pipeline() -> str:
167
+ return "\n".join([
168
+ "=" * 58,
169
+ "STEP 1/2: Data Creation — real-world PwC hospitality indicators",
170
+ "=" * 58,
171
+ run_datacreation(),
172
+ "",
173
+ "=" * 58,
174
+ "STEP 2/2: Python Analysis — synthetic scores + dashboard artifacts",
175
+ "=" * 58,
176
+ run_pythonanalysis(),
177
+ ])
178
+
179
+ # =========================================================
180
+ # DATA / TABLE LOADERS
181
+ # =========================================================
182
+
183
+ def load_table_safe(path: Path) -> pd.DataFrame:
184
+ try:
185
+ if path.suffix.lower() == ".json":
186
+ obj = _read_json(path)
187
+ return pd.DataFrame([obj]) if isinstance(obj, dict) else pd.DataFrame(obj)
188
+ return _read_csv(path)
189
+ except Exception as e:
190
+ return pd.DataFrame([{"error": str(e)}])
191
+
192
+ def refresh_gallery():
193
+ figures = [(str(p), p.stem.replace("_", " ").title()) for p in sorted(PY_FIG_DIR.glob("*.png"))]
194
+ idx = artifacts_index()
195
+ table_choices = list(idx["python"]["tables"])
196
+
197
+ # Always include core datasets in table dropdown
198
+ for core in [CLEANED_CSV, ENRICHED_CSV]:
199
+ if (BASE_DIR / core).exists() and core not in table_choices:
200
+ table_choices.insert(0, core)
201
+
202
+ default_df = pd.DataFrame()
203
+ if table_choices:
204
+ chosen = table_choices[0]
205
+ path = BASE_DIR / chosen if (BASE_DIR / chosen).exists() else PY_TAB_DIR / chosen
206
+ default_df = load_table_safe(path)
207
+
208
+ return figures, gr.update(choices=table_choices, value=table_choices[0] if table_choices else None), default_df
209
+
210
+ def on_table_select(choice: str):
211
+ if not choice:
212
+ return pd.DataFrame([{"hint": "Select a table above."}])
213
+ path = BASE_DIR / choice if (BASE_DIR / choice).exists() else PY_TAB_DIR / choice
214
+ if not path.exists():
215
+ return pd.DataFrame([{"error": f"File not found: {choice}"}])
216
+ return load_table_safe(path)
217
+
218
+ # =========================================================
219
+ # KPIs
220
+ # =========================================================
221
+
222
+ def load_kpis() -> Dict[str, Any]:
223
+ df = read_market_data(prefer_enriched=True)
224
+ if df.empty:
225
+ return {}
226
+ syn = df[df["section"].eq("synthetic_features")]
227
+ risk = syn[syn["metric_name"].eq("risk_score")]
228
+ investment = syn[syn["metric_name"].eq("investment_potential_score")]
229
+ attractiveness = syn[syn["metric_name"].eq("market_attractiveness_score")]
230
+
231
+ return {
232
+ "locations": int(df["location"].nunique()),
233
+ "metrics": int(df["metric_name"].nunique()),
234
+ "real_rows": int((df["section"] != "synthetic_features").sum()),
235
+ "synthetic_rows": int((df["section"] == "synthetic_features").sum()),
236
+ "avg_risk_score": round(float(risk["value"].mean()), 1) if not risk.empty else None,
237
+ "avg_investment_potential": round(float(investment["value"].mean()), 1) if not investment.empty else None,
238
+ "avg_market_attractiveness": round(float(attractiveness["value"].mean()), 1) if not attractiveness.empty else None,
239
+ }
240
+
241
+ def render_kpi_cards() -> str:
242
+ kpis = load_kpis()
243
+ if not kpis:
244
+ return """
245
+ <div style='background:rgba(255,255,255,.72);border-radius:20px;padding:28px;text-align:center;border:1px solid #ddd;'>
246
+ <div style='font-size:34px;'>🏨</div>
247
+ <b>No hospitality data found yet</b><br>
248
+ <span>Run the pipeline or place the CSV files in the app folder.</span>
249
+ </div>"""
250
+
251
+ cards = [
252
+ ("📍", "Locations", kpis.get("locations"), "#a48de8"),
253
+ ("📊", "Metrics", kpis.get("metrics"), "#7aa6f8"),
254
+ ("🧪", "Synthetic Rows", kpis.get("synthetic_rows"), "#6ee7c7"),
255
+ ("⚠️", "Avg Risk Score", kpis.get("avg_risk_score"), "#e8537a"),
256
+ ("💼", "Avg Investment Potential", kpis.get("avg_investment_potential"), "#2ec4a0"),
257
+ ("⭐", "Avg Market Attractiveness", kpis.get("avg_market_attractiveness"), "#e8a230"),
258
+ ]
259
+
260
+ html = "<div style='display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-bottom:24px;'>"
261
+ for icon, label, value, colour in cards:
262
+ value = "—" if value is None else value
263
+ html += f"""
264
+ <div style='background:rgba(255,255,255,.78);border-radius:20px;padding:18px;text-align:center;
265
+ border:1px solid rgba(124,92,191,.18);box-shadow:0 4px 16px rgba(124,92,191,.08);
266
+ border-top:3px solid {colour};'>
267
+ <div style='font-size:26px;margin-bottom:7px;'>{icon}</div>
268
+ <div style='color:#8b78c6;font-size:10px;text-transform:uppercase;letter-spacing:1.4px;font-weight:800;'>{label}</div>
269
+ <div style='color:#2d1f4e;font-size:18px;font-weight:800;margin-top:6px;'>{value}</div>
270
+ </div>"""
271
+ html += "</div>"
272
+ return html
273
+
274
+ # =========================================================
275
+ # INTERACTIVE CHARTS
276
+ # =========================================================
277
+
278
+ def styled_layout(**kwargs) -> dict:
279
+ defaults = dict(
280
+ template="plotly_white",
281
+ paper_bgcolor="rgba(255,255,255,0.96)",
282
+ plot_bgcolor="rgba(255,255,255,0.98)",
283
+ font=dict(family="system-ui, sans-serif", color="#2d1f4e", size=12),
284
+ margin=dict(l=60, r=20, t=70, b=70),
285
+ title=dict(font=dict(size=16, color="#4b2d8a")),
286
+ legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
287
+ )
288
+ defaults.update(kwargs)
289
+ return defaults
290
+
291
+ def empty_chart(title: str) -> go.Figure:
292
+ fig = go.Figure()
293
+ fig.update_layout(
294
+ title=title,
295
+ height=420,
296
+ template="plotly_white",
297
+ annotations=[dict(text="No data available yet", x=0.5, y=0.5, xref="paper", yref="paper", showarrow=False)],
298
+ )
299
+ return fig
300
+
301
+ def build_city_performance_chart() -> go.Figure:
302
+ df = read_market_data(True)
303
+ wide = pivot_metrics(df, section="city_performance", entity="city")
304
+ needed = [c for c in ["occupancy_yoy", "adr_yoy", "revpar_yoy"] if c in wide.columns]
305
+ if wide.empty or not needed:
306
+ return empty_chart("City Performance — Occupancy, ADR, RevPAR")
307
+ fig = go.Figure()
308
+ for i, col in enumerate(needed):
309
+ fig.add_trace(go.Bar(x=wide["location"], y=wide[col], name=col.replace("_", " ").upper(), marker_color=CHART_PALETTE[i]))
310
+ fig.update_layout(**styled_layout(title=dict(text="City Performance YoY — Occupancy, ADR, RevPAR"), barmode="group", height=430))
311
+ fig.update_yaxes(title="YoY change (%)")
312
+ return fig
313
+
314
+ def build_region_demand_chart() -> go.Figure:
315
+ df = read_market_data(True)
316
+ wide = pivot_metrics(df, section="regional_demand", entity="region")
317
+ needed = [c for c in ["domestic_demand_growth", "international_demand_growth"] if c in wide.columns]
318
+ if wide.empty or not needed:
319
+ return empty_chart("Regional Demand Growth")
320
+ fig = go.Figure()
321
+ for i, col in enumerate(needed):
322
+ fig.add_trace(go.Bar(y=wide["location"], x=wide[col], orientation="h", name=col.replace("_", " ").title(), marker_color=CHART_PALETTE[i]))
323
+ fig.update_layout(**styled_layout(title=dict(text="Regional Demand Growth — Domestic vs International"), barmode="group", height=max(420, len(wide)*42)))
324
+ fig.update_xaxes(title="Growth (%)")
325
+ fig.update_yaxes(autorange="reversed")
326
+ return fig
327
+
328
+ def build_synthetic_scores_chart() -> go.Figure:
329
+ df = read_market_data(True)
330
+ wide = pivot_metrics(df, section="synthetic_features")
331
+ score_cols = [c for c in ["growth_score", "market_attractiveness_score", "investment_potential_score", "risk_score"] if c in wide.columns]
332
+ if wide.empty or not score_cols:
333
+ return empty_chart("Synthetic Scores")
334
+ fig = go.Figure()
335
+ for i, col in enumerate(score_cols):
336
+ fig.add_trace(go.Bar(x=wide["location"], y=wide[col], name=col.replace("_", " ").title(), marker_color=CHART_PALETTE[i % len(CHART_PALETTE)]))
337
+ fig.update_layout(**styled_layout(title=dict(text="Synthetic Market Scores by Location"), barmode="group", height=470))
338
+ fig.update_yaxes(title="Score (0–100)", range=[0, 105])
339
+ return fig
340
+
341
+ def build_risk_chart() -> go.Figure:
342
+ df = read_market_data(True)
343
+ wide = pivot_metrics(df, section="synthetic_features")
344
+ if wide.empty or "risk_score" not in wide.columns:
345
+ return empty_chart("Risk Score")
346
+ wide = wide.sort_values("risk_score", ascending=True)
347
+ fig = go.Figure(go.Bar(
348
+ y=wide["location"],
349
+ x=wide["risk_score"],
350
+ orientation="h",
351
+ text=[f"{v:.1f}" for v in wide["risk_score"]],
352
+ marker=dict(color=wide["risk_score"], colorscale=[[0, "#2ec4a0"], [0.5, "#e8a230"], [1, "#e8537a"]]),
353
+ ))
354
+ fig.update_layout(**styled_layout(title=dict(text="Risk Score by Location"), showlegend=False, height=max(420, len(wide)*35)))
355
+ fig.update_xaxes(title="Risk score (0–100)", range=[0, 105])
356
+ return fig
357
+
358
+ def build_investment_chart() -> go.Figure:
359
+ df = read_market_data(True)
360
+ wide = pivot_metrics(df, section="synthetic_features")
361
+ if wide.empty or "investment_potential_score" not in wide.columns:
362
+ return empty_chart("Investment Potential")
363
+ wide = wide.sort_values("investment_potential_score", ascending=True)
364
+ fig = go.Figure(go.Bar(
365
+ y=wide["location"],
366
+ x=wide["investment_potential_score"],
367
+ orientation="h",
368
+ text=[f"{v:.1f}" for v in wide["investment_potential_score"]],
369
+ marker=dict(color=wide["investment_potential_score"], colorscale=[[0, "#c5b4f0"], [1, "#7c5cbf"]]),
370
+ ))
371
+ fig.update_layout(**styled_layout(title=dict(text="Investment Potential Score by Location"), showlegend=False, height=max(420, len(wide)*35)))
372
+ fig.update_xaxes(title="Investment potential score (0–100)", range=[0, 105])
373
+ return fig
374
+
375
+ def build_opportunity_table() -> pd.DataFrame:
376
+ df = read_market_data(True)
377
+ wide = pivot_metrics(df, section="synthetic_features")
378
+ if wide.empty:
379
+ return pd.DataFrame([{"hint": "No synthetic features found yet."}])
380
+ keep = [c for c in ["location", "entity", "growth_score", "market_attractiveness_score", "investment_potential_score", "risk_score", "risk_level", "opportunity_category"] if c in wide.columns]
381
+ out = wide[keep].copy()
382
+ for col in ["growth_score", "market_attractiveness_score", "investment_potential_score", "risk_score"]:
383
+ if col in out:
384
+ out[col] = out[col].round(1)
385
+ return out.sort_values(["entity", "investment_potential_score"], ascending=[True, False], na_position="last") if "investment_potential_score" in out else out
386
+
387
+ def refresh_dashboard():
388
+ figs, dd, df = refresh_gallery()
389
+ return (
390
+ render_kpi_cards(),
391
+ build_city_performance_chart(),
392
+ build_region_demand_chart(),
393
+ build_synthetic_scores_chart(),
394
+ build_risk_chart(),
395
+ build_investment_chart(),
396
+ build_opportunity_table(),
397
+ figs,
398
+ dd,
399
+ df,
400
+ )
401
+
402
+ # =========================================================
403
+ # AI DASHBOARD — N8N / HUGGING FACE / KEYWORD FALLBACK
404
+ # =========================================================
405
+
406
+ DASHBOARD_SYSTEM = """You are the AI assistant for an Italy Hospitality Market Insight Assistant.
407
+ The app uses a real-world PwC Italy Hospitality Market Snapshot dataset and an enriched synthetic dataset.
408
+ The dataset has long-format columns: section, location, entity, metric_name, value, unit, period, source_page, note.
409
+
410
+ Available artifacts:
411
+ {artifacts_json}
412
+
413
+ KPI summary:
414
+ {kpis_json}
415
+
416
+ Key concepts:
417
+ - city performance: occupancy_yoy, adr_yoy, revpar_yoy for Milan, Rome, Florence, Venice.
418
+ - regional demand: domestic_demand_growth and international_demand_growth.
419
+ - synthetic features: growth_score, market_attractiveness_score, investment_potential_score, risk_score, risk_level, opportunity_category.
420
+
421
+ Answer briefly and practically. At the END, output a JSON block exactly like:
422
+ ```json
423
+ {{"show": "chart"|"table"|"none", "chart": "city|region|scores|risk|investment|none", "table": "opportunities|raw|none"}}
424
+ ```
425
+ Choose:
426
+ - city for occupancy / ADR / RevPAR / city performance questions.
427
+ - region for domestic/international regional demand questions.
428
+ - scores for comparing synthetic scores.
429
+ - risk for risk score or risk level.
430
+ - investment for investment potential / attractiveness / opportunity.
431
+ - opportunities table for opportunity categories or strategic recommendations.
432
+ """
433
+
434
+ JSON_BLOCK_RE = re.compile(r"```json\s*(\{.*?\})\s*```", re.DOTALL)
435
+ FALLBACK_JSON_RE = re.compile(r"\{[^{}]*\"show\"[^{}]*\}", re.DOTALL)
436
+
437
+ def parse_display_directive(text: str) -> Dict[str, str]:
438
+ for regex in [JSON_BLOCK_RE, FALLBACK_JSON_RE]:
439
+ m = regex.search(text)
440
+ if m:
441
+ try:
442
+ return json.loads(m.group(1) if regex is JSON_BLOCK_RE else m.group(0))
443
+ except Exception:
444
+ continue
445
+ return {"show": "none", "chart": "none", "table": "none"}
446
+
447
+ def clean_response(text: str) -> str:
448
+ return JSON_BLOCK_RE.sub("", text).strip()
449
+
450
+ def n8n_call(msg: str) -> Tuple[str, Optional[Dict[str, str]]]:
451
+ import requests
452
+ try:
453
+ resp = requests.post(N8N_WEBHOOK_URL, json={"question": msg, "project": "italy_hospitality_market"}, timeout=25)
454
+ resp.raise_for_status()
455
+ data = resp.json()
456
+ answer = data.get("answer") or data.get("reply") or "No answer returned by n8n."
457
+ chart = data.get("chart", "none")
458
+ table = data.get("table", "none")
459
+ return answer, {"show": "chart" if chart != "none" else ("table" if table != "none" else "none"), "chart": chart, "table": table}
460
+ except Exception as e:
461
+ return f"n8n error: {e}. Falling back to local logic.", None
462
+
463
+ def keyword_fallback(msg: str, kpis: Dict[str, Any]) -> Tuple[str, Dict[str, str]]:
464
+ m = msg.lower()
465
+ kpi_text = ""
466
+ if kpis:
467
+ kpi_text = f"The dataset covers {kpis.get('locations', '?')} locations and {kpis.get('metrics', '?')} metrics."
468
+
469
+ if any(w in m for w in ["occupancy", "adr", "revpar", "city", "milan", "rome", "florence", "venice"]):
470
+ return f"Here is the city performance view for occupancy, ADR, and RevPAR. {kpi_text}", {"show": "chart", "chart": "city", "table": "none"}
471
+ if any(w in m for w in ["region", "regional", "domestic", "international", "demand", "lazio", "puglia", "sicilia"]):
472
+ return f"Here is the regional demand comparison between domestic and international growth. {kpi_text}", {"show": "chart", "chart": "region", "table": "none"}
473
+ if any(w in m for w in ["risk", "risky", "safe", "low risk", "high risk"]):
474
+ return f"Here is the risk score view. Lower scores indicate safer or more stable opportunities. {kpi_text}", {"show": "chart", "chart": "risk", "table": "opportunities"}
475
+ if any(w in m for w in ["investment", "potential", "attractive", "attractiveness", "opportunity", "recommend"]):
476
+ return f"Here is the investment potential view, supported by the opportunity-category table. {kpi_text}", {"show": "chart", "chart": "investment", "table": "opportunities"}
477
+ if any(w in m for w in ["score", "scores", "growth", "synthetic", "compare"]):
478
+ return f"Here is the synthetic score comparison across locations. {kpi_text}", {"show": "chart", "chart": "scores", "table": "opportunities"}
479
+ if any(w in m for w in ["table", "data", "raw", "dataset"]):
480
+ return f"Here is the enriched hospitality dataset table preview. {kpi_text}", {"show": "table", "chart": "none", "table": "raw"}
481
+
482
+ return (
483
+ f"You can ask about city performance, regional demand, risk, investment potential, synthetic scores, or opportunity categories. {kpi_text}",
484
+ {"show": "none", "chart": "none", "table": "none"},
485
+ )
486
+
487
+ def resolve_chart(name: str):
488
+ return {
489
+ "city": build_city_performance_chart,
490
+ "region": build_region_demand_chart,
491
+ "scores": build_synthetic_scores_chart,
492
+ "risk": build_risk_chart,
493
+ "investment": build_investment_chart,
494
+ }.get(name, lambda: None)()
495
+
496
+ def resolve_table(name: str):
497
+ if name == "opportunities":
498
+ return build_opportunity_table()
499
+ if name == "raw":
500
+ return read_market_data(True).head(MAX_PREVIEW_ROWS)
501
+ return None
502
+
503
+ def ai_chat(user_msg: str, history: list):
504
+ if not user_msg or not user_msg.strip():
505
+ return history, "", None, None
506
+
507
+ idx = artifacts_index()
508
+ kpis = load_kpis()
509
+
510
+ if N8N_WEBHOOK_URL:
511
+ reply, directive = n8n_call(user_msg)
512
+ if directive is None:
513
+ reply_fb, directive = keyword_fallback(user_msg, kpis)
514
+ reply = reply + "\n\n" + reply_fb
515
+ elif LLM_ENABLED:
516
+ system = DASHBOARD_SYSTEM.format(
517
+ artifacts_json=json.dumps(idx, indent=2),
518
+ kpis_json=json.dumps(kpis, indent=2) if kpis else "No KPIs available yet.",
519
+ )
520
+ msgs = [{"role": "system", "content": system}]
521
+ for entry in (history or [])[-6:]:
522
+ msgs.append(entry)
523
+ msgs.append({"role": "user", "content": user_msg})
524
+ try:
525
+ r = llm_client.chat_completion(
526
+ model=MODEL_NAME,
527
+ messages=msgs,
528
+ temperature=0.25,
529
+ max_tokens=650,
530
+ stream=False,
531
+ )
532
+ raw = r["choices"][0]["message"]["content"] if isinstance(r, dict) else r.choices[0].message.content
533
+ directive = parse_display_directive(raw)
534
+ reply = clean_response(raw)
535
+ except Exception as e:
536
+ reply_fb, directive = keyword_fallback(user_msg, kpis)
537
+ reply = f"Hugging Face error: {e}. Falling back to local logic.\n\n{reply_fb}"
538
+ else:
539
+ reply, directive = keyword_fallback(user_msg, kpis)
540
+
541
+ chart_out = resolve_chart(directive.get("chart", "none")) if directive.get("show") in ["chart", "figure"] or directive.get("chart") != "none" else None
542
+ table_out = resolve_table(directive.get("table", "none")) if directive.get("table") != "none" else None
543
+
544
+ new_history = (history or []) + [
545
+ {"role": "user", "content": user_msg},
546
+ {"role": "assistant", "content": reply},
547
+ ]
548
+ return new_history, "", chart_out, table_out
549
+
550
+ # =========================================================
551
+ # UI
552
+ # =========================================================
553
+
554
+ ensure_dirs()
555
+
556
+ def load_css() -> str:
557
+ css_path = BASE_DIR / "style.css"
558
+ return css_path.read_text(encoding="utf-8") if css_path.exists() else ""
559
+
560
+ with gr.Blocks(title="Italy Hospitality Market Insight Assistant") as demo:
561
+ gr.Markdown(
562
+ "# Italy Hospitality Market Insight Assistant\n"
563
+ "*A Gradio app for PwC-based hospitality indicators, synthetic market scores, n8n automation and Hugging Face AI Q&A.*",
564
+ elem_id="escp_title",
565
+ )
566
+
567
+ with gr.Tab("Pipeline Runner"):
568
+ gr.Markdown("Run the project notebooks. Step 1 creates/cleans the dataset; Step 2 creates analysis outputs and synthetic insights.")
569
+ with gr.Row():
570
+ btn_nb1 = gr.Button("Step 1: Data Creation", variant="secondary")
571
+ btn_nb2 = gr.Button("Step 2: Python Analysis", variant="secondary")
572
+ btn_all = gr.Button("Run Full Pipeline", variant="primary")
573
+ run_log = gr.Textbox(label="Execution Log", lines=18, max_lines=30, interactive=False)
574
+ btn_nb1.click(run_datacreation, outputs=[run_log])
575
+ btn_nb2.click(run_pythonanalysis, outputs=[run_log])
576
+ btn_all.click(run_full_pipeline, outputs=[run_log])
577
+
578
+ with gr.Tab("Dashboard"):
579
+ kpi_html = gr.HTML(value=render_kpi_cards)
580
+ refresh_btn = gr.Button("Refresh Dashboard", variant="primary")
581
+
582
+ gr.Markdown("#### Interactive Hospitality Charts")
583
+ with gr.Row():
584
+ chart_city = gr.Plot(label="City Performance")
585
+ chart_region = gr.Plot(label="Regional Demand")
586
+ with gr.Row():
587
+ chart_scores = gr.Plot(label="Synthetic Scores")
588
+ chart_risk = gr.Plot(label="Risk Score")
589
+ chart_investment = gr.Plot(label="Investment Potential")
590
+
591
+ gr.Markdown("#### Opportunity Categories")
592
+ opportunity_table = gr.Dataframe(label="Synthetic Strategy Table", interactive=False)
593
+
594
+ gr.Markdown("#### Static Figures and Data Tables")
595
+ gallery = gr.Gallery(label="Generated Figures", columns=2, height=430, object_fit="contain")
596
+ table_dropdown = gr.Dropdown(label="Select a table to view", choices=[], interactive=True)
597
+ table_display = gr.Dataframe(label="Table Preview", interactive=False)
598
+
599
+ refresh_btn.click(
600
+ refresh_dashboard,
601
+ outputs=[kpi_html, chart_city, chart_region, chart_scores, chart_risk, chart_investment, opportunity_table, gallery, table_dropdown, table_display],
602
+ )
603
+ table_dropdown.change(on_table_select, inputs=[table_dropdown], outputs=[table_display])
604
+
605
+ with gr.Tab('"AI" Dashboard'):
606
+ ai_status = (
607
+ "Connected to your **n8n workflow**." if N8N_WEBHOOK_URL else
608
+ "**Hugging Face LLM active.**" if LLM_ENABLED else
609
+ "Using **local keyword matching**. To activate AI, set `HF_API_KEY`; to activate automations, set `N8N_WEBHOOK_URL`."
610
+ )
611
+ gr.Markdown(
612
+ "### Ask questions about the Italy hospitality market\n\n"
613
+ f"{ai_status}\n\n"
614
+ "Examples: *Which city has the strongest RevPAR?*, *Show risk scores*, *Which regions are investment opportunities?*"
615
+ )
616
+ with gr.Row(equal_height=True):
617
+ with gr.Column(scale=1):
618
+ chatbot = gr.Chatbot(label="Conversation", height=400, type="messages")
619
+ user_input = gr.Textbox(label="Ask about your data", placeholder="e.g. Show me investment potential by location", lines=1)
620
+ gr.Examples(
621
+ examples=[
622
+ "Show me city performance for occupancy, ADR and RevPAR",
623
+ "Which locations have the highest risk?",
624
+ "Show investment potential by location",
625
+ "Compare synthetic scores",
626
+ "What is regional domestic vs international demand?",
627
+ "Give me strategic recommendations",
628
+ ],
629
+ inputs=user_input,
630
+ )
631
+ with gr.Column(scale=1):
632
+ ai_figure = gr.Plot(label="Interactive Chart")
633
+ ai_table = gr.Dataframe(label="Relevant Table", interactive=False)
634
+
635
+ user_input.submit(ai_chat, inputs=[user_input, chatbot], outputs=[chatbot, user_input, ai_figure, ai_table])
636
+
637
+ if __name__ == "__main__":
638
+ demo.launch(css=load_css(), allowed_paths=[str(BASE_DIR)])
background_bottom.png ADDED
background_mid.png ADDED
background_top.png ADDED

Git LFS Details

  • SHA256: 27e963d20dbb7ae88368fb527d475c85ef0de3df63d8f0d7d5e2af7403a5b365
  • Pointer size: 131 Bytes
  • Size of remote file: 726 kB
gitattributes ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ background_top.png filter=lfs diff=lfs merge=lfs -text
gitattributes (1) ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ background_top.png filter=lfs diff=lfs merge=lfs -text
requirements.txt ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ gradio==6.0.0
2
+ pandas>=2.0.0
3
+ numpy>=1.24.0
4
+ matplotlib>=3.7.0
5
+ seaborn>=0.13.0
6
+ statsmodels>=0.14.0
7
+ scikit-learn>=1.3.0
8
+ papermill>=2.5.0
9
+ nbformat>=5.9.0
10
+ pillow>=10.0.0
11
+ requests>=2.31.0
12
+ beautifulsoup4>=4.12.0
13
+ vaderSentiment>=3.3.2
14
+ huggingface_hub>=0.20.0
15
+ textblob>=0.18.0
16
+ faker>=20.0.0
17
+ plotly>=5.18.0
style.css ADDED
@@ -0,0 +1,326 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* --- Target the Gradio app wrapper for backgrounds --- */
2
+ gradio-app,
3
+ .gradio-app,
4
+ .main,
5
+ #app,
6
+ [data-testid="app"] {
7
+ background-color: rgb(40,9,109) !important;
8
+ background-image:
9
+ url('https://huggingface.co/spaces/atascioglu/SE21AppTemplate/resolve/main/background_top.png'),
10
+ url('https://huggingface.co/spaces/atascioglu/SE21AppTemplate/resolve/main/background_mid.png'),
11
+ url('https://huggingface.co/spaces/atascioglu/SE21AppTemplate/resolve/main/background_bottom.png') !important;
12
+ background-position:
13
+ top center,
14
+ 0 913px,
15
+ bottom center !important;
16
+ background-repeat:
17
+ no-repeat,
18
+ repeat-y,
19
+ no-repeat !important;
20
+ background-size:
21
+ 100% auto,
22
+ 100% auto,
23
+ 100% auto !important;
24
+ min-height: 100vh !important;
25
+ }
26
+
27
+ /* --- Fallback on html/body --- */
28
+ html, body {
29
+ background-color: rgb(40,9,109) !important;
30
+ margin: 0 !important;
31
+ padding: 0 !important;
32
+ min-height: 100vh !important;
33
+ }
34
+
35
+ /* Bottom image is now part of the main background layers (positioned at bottom center) */
36
+
37
+ /* --- Main container --- */
38
+ .gradio-container {
39
+ max-width: 1400px !important;
40
+ width: 94vw !important;
41
+ margin: 0 auto !important;
42
+ padding-top: 220px !important;
43
+ padding-bottom: 150px !important;
44
+ background: transparent !important;
45
+ }
46
+
47
+ /* --- Title in ESCP gold --- */
48
+ #escp_title h1 {
49
+ color: rgb(242,198,55) !important;
50
+ font-size: 3rem !important;
51
+ font-weight: 800 !important;
52
+ text-align: center !important;
53
+ margin: 0 0 12px 0 !important;
54
+ }
55
+
56
+ /* --- Subtitle --- */
57
+ #escp_title p, #escp_title em {
58
+ color: rgba(255,255,255,0.85) !important;
59
+ text-align: center !important;
60
+ }
61
+
62
+ /* --- Tab bar background --- */
63
+ .tabs > .tab-nav,
64
+ .tab-nav,
65
+ div[role="tablist"],
66
+ .svelte-tabs > .tab-nav {
67
+ background: rgba(40,9,109,0.6) !important;
68
+ border-radius: 10px 10px 0 0 !important;
69
+ padding: 4px !important;
70
+ }
71
+
72
+ /* --- ALL tab buttons: force white text --- */
73
+ .tabs > .tab-nav button,
74
+ .tab-nav button,
75
+ div[role="tablist"] button,
76
+ button[role="tab"],
77
+ .svelte-tabs button,
78
+ .tab-nav > button,
79
+ .tabs button {
80
+ color: #ffffff !important;
81
+ font-weight: 600 !important;
82
+ border: none !important;
83
+ background: transparent !important;
84
+ padding: 10px 20px !important;
85
+ border-radius: 8px 8px 0 0 !important;
86
+ opacity: 1 !important;
87
+ }
88
+
89
+ /* --- Selected tab: ESCP gold --- */
90
+ .tabs > .tab-nav button.selected,
91
+ .tab-nav button.selected,
92
+ button[role="tab"][aria-selected="true"],
93
+ button[role="tab"].selected,
94
+ div[role="tablist"] button[aria-selected="true"],
95
+ .svelte-tabs button.selected {
96
+ color: rgb(242,198,55) !important;
97
+ background: rgba(255,255,255,0.12) !important;
98
+ }
99
+
100
+ /* --- Unselected tabs: ensure visibility --- */
101
+ .tabs > .tab-nav button:not(.selected),
102
+ .tab-nav button:not(.selected),
103
+ button[role="tab"][aria-selected="false"],
104
+ button[role="tab"]:not(.selected),
105
+ div[role="tablist"] button:not([aria-selected="true"]) {
106
+ color: #ffffff !important;
107
+ opacity: 1 !important;
108
+ }
109
+
110
+ /* --- White card panels --- */
111
+ .gradio-container .gr-block,
112
+ .gradio-container .gr-box,
113
+ .gradio-container .gr-panel,
114
+ .gradio-container .gr-group {
115
+ background: #ffffff !important;
116
+ border-radius: 10px !important;
117
+ }
118
+
119
+ /* --- Tab content area --- */
120
+ .tabitem {
121
+ background: rgba(255,255,255,0.95) !important;
122
+ border-radius: 0 0 10px 10px !important;
123
+ padding: 16px !important;
124
+ }
125
+
126
+ /* --- Inputs --- */
127
+ .gradio-container input,
128
+ .gradio-container textarea,
129
+ .gradio-container select {
130
+ background: #ffffff !important;
131
+ border: 1px solid #d1d5db !important;
132
+ border-radius: 8px !important;
133
+ }
134
+
135
+ /* --- Buttons: ESCP purple primary --- */
136
+ .gradio-container button:not([role="tab"]) {
137
+ font-weight: 600 !important;
138
+ padding: 10px 16px !important;
139
+ border-radius: 10px !important;
140
+ }
141
+
142
+ button.primary {
143
+ background-color: rgb(40,9,109) !important;
144
+ color: #ffffff !important;
145
+ border: none !important;
146
+ }
147
+
148
+ button.primary:hover {
149
+ background-color: rgb(60,20,140) !important;
150
+ }
151
+
152
+ button.secondary {
153
+ background-color: #ffffff !important;
154
+ color: rgb(40,9,109) !important;
155
+ border: 2px solid rgb(40,9,109) !important;
156
+ }
157
+
158
+ button.secondary:hover {
159
+ background-color: rgb(240,238,250) !important;
160
+ }
161
+
162
+ /* --- Dataframes --- */
163
+ [data-testid="dataframe"] {
164
+ background-color: #ffffff !important;
165
+ border-radius: 10px !important;
166
+ }
167
+
168
+ table {
169
+ font-size: 0.85rem !important;
170
+ }
171
+
172
+ /* --- Chatbot (AI Dashboard tab) --- */
173
+ .gr-chatbot {
174
+ min-height: 380px !important;
175
+ background-color: #ffffff !important;
176
+ border-radius: 12px !important;
177
+ }
178
+
179
+ .gr-chatbot .message.user {
180
+ background-color: rgb(232,225,250) !important;
181
+ border-radius: 12px !important;
182
+ }
183
+
184
+ .gr-chatbot .message.bot {
185
+ background-color: #f3f4f6 !important;
186
+ border-radius: 12px !important;
187
+ }
188
+
189
+ /* --- Gallery --- */
190
+ .gallery {
191
+ background: #ffffff !important;
192
+ border-radius: 10px !important;
193
+ }
194
+
195
+ /* --- Log textbox --- */
196
+ textarea {
197
+ font-family: monospace !important;
198
+ font-size: 0.8rem !important;
199
+ }
200
+
201
+ /* --- Markdown headings inside tabs --- */
202
+ .tabitem h3 {
203
+ color: rgb(40,9,109) !important;
204
+ font-weight: 700 !important;
205
+ }
206
+
207
+ .tabitem h4 {
208
+ color: #374151 !important;
209
+ }
210
+
211
+ /* --- Examples row (AI Dashboard) --- */
212
+ .examples-row button {
213
+ background: rgb(240,238,250) !important;
214
+ color: rgb(40,9,109) !important;
215
+ border: 1px solid rgb(40,9,109) !important;
216
+ border-radius: 8px !important;
217
+ font-size: 0.85rem !important;
218
+ }
219
+
220
+ .examples-row button:hover {
221
+ background: rgb(232,225,250) !important;
222
+ }
223
+
224
+ /* --- Header / footer: transparent over banner --- */
225
+ header, header *,
226
+ footer, footer * {
227
+ background: transparent !important;
228
+ box-shadow: none !important;
229
+ }
230
+
231
+ footer a, footer button,
232
+ header a, header button {
233
+ background: transparent !important;
234
+ border: none !important;
235
+ box-shadow: none !important;
236
+ }
237
+
238
+ #footer, #footer *,
239
+ [class*="footer"], [class*="footer"] *,
240
+ [class*="chip"], [class*="pill"], [class*="chip"] *, [class*="pill"] * {
241
+ background: transparent !important;
242
+ border: none !important;
243
+ box-shadow: none !important;
244
+ }
245
+
246
+ [data-testid*="api"], [data-testid*="settings"],
247
+ [id*="api"], [id*="settings"],
248
+ [class*="api"], [class*="settings"],
249
+ [class*="bottom"], [class*="toolbar"], [class*="controls"] {
250
+ background: transparent !important;
251
+ box-shadow: none !important;
252
+ }
253
+
254
+ [data-testid*="api"] *, [data-testid*="settings"] *,
255
+ [id*="api"] *, [id*="settings"] *,
256
+ [class*="api"] *, [class*="settings"] * {
257
+ background: transparent !important;
258
+ box-shadow: none !important;
259
+ }
260
+
261
+ section footer {
262
+ background: transparent !important;
263
+ }
264
+
265
+ section footer button,
266
+ section footer a {
267
+ background: transparent !important;
268
+ background-color: transparent !important;
269
+ border: none !important;
270
+ box-shadow: none !important;
271
+ color: white !important;
272
+ }
273
+
274
+ section footer button:hover,
275
+ section footer button:focus,
276
+ section footer a:hover,
277
+ section footer a:focus {
278
+ background: transparent !important;
279
+ background-color: transparent !important;
280
+ box-shadow: none !important;
281
+ }
282
+
283
+ section footer button,
284
+ section footer button * {
285
+ background: transparent !important;
286
+ background-color: transparent !important;
287
+ background-image: none !important;
288
+ box-shadow: none !important;
289
+ filter: none !important;
290
+ }
291
+
292
+ section footer button::before,
293
+ section footer button::after {
294
+ background: transparent !important;
295
+ background-color: transparent !important;
296
+ background-image: none !important;
297
+ box-shadow: none !important;
298
+ filter: none !important;
299
+ }
300
+
301
+ section footer a,
302
+ section footer a * {
303
+ background: transparent !important;
304
+ background-color: transparent !important;
305
+ box-shadow: none !important;
306
+ }
307
+
308
+ .gradio-container footer button,
309
+ .gradio-container footer button *,
310
+ .gradio-container .footer button,
311
+ .gradio-container .footer button * {
312
+ background: transparent !important;
313
+ background-color: transparent !important;
314
+ background-image: none !important;
315
+ box-shadow: none !important;
316
+ }
317
+
318
+ .gradio-container footer button::before,
319
+ .gradio-container footer button::after,
320
+ .gradio-container .footer button::before,
321
+ .gradio-container .footer button::after {
322
+ background: transparent !important;
323
+ background-color: transparent !important;
324
+ background-image: none !important;
325
+ box-shadow: none !important;
326
+ }