| import os |
| import json |
| import time |
| import random |
| import threading |
| from pathlib import Path |
| from dataclasses import dataclass, field |
| from typing import Dict, List, Any, Optional, Tuple |
|
|
| import gradio as gr |
| import pandas as pd |
| import matplotlib.pyplot as plt |
|
|
| |
| from openai import OpenAI |
|
|
| |
| |
| |
| OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "").strip() |
| MODEL = os.getenv("OPENAI_MODEL", "gpt-3.5-turbo-0125").strip() |
| TEACHER_PIN = os.getenv("TEACHER_PIN", "1234").strip() |
|
|
| |
| AI_ENABLED_DEFAULT = os.getenv("AI_ENABLED", "1").strip() != "0" |
|
|
| client = OpenAI(api_key=OPENAI_API_KEY) if OPENAI_API_KEY else None |
|
|
| |
| |
| |
| DATASETS = { |
| "Sales": {"desc": "Transactions by hour/product (2 years)", "cost": 5}, |
| "Inventory": {"desc": "Production, waste, stockouts", "cost": 4}, |
| "Customers": {"desc": "Loyalty behavior, visit frequency", "cost": 5}, |
| "Staffing": {"desc": "Schedules and service times", "cost": 3}, |
| "External": {"desc": "Weather + academic calendar", "cost": 2}, |
| "Feedback": {"desc": "Ratings + text reviews", "cost": 3}, |
| } |
|
|
| BASE_DIR = Path(__file__).resolve().parent |
| DATA_DIR = BASE_DIR / "data" |
| DATASET_FILES = { |
| "Sales": "sales.csv", |
| "Inventory": "inventory.csv", |
| "Customers": "customers.csv", |
| "Staffing": "staffing.csv", |
| "External": "external.csv", |
| "Feedback": "feedback.csv", |
| } |
|
|
| EVENT_CARDS = [ |
| ("Exam week", "Demand spikes +25% for 7 days. Morning queue risk increases."), |
| ("Rainy stretch", "Foot traffic drops ~15%. Afternoon sales become volatile."), |
| ("Competitor promo", "Nearby café runs 2-for-1 coffees. Price sensitivity increases."), |
| ("Supplier price increase", "Pastry input costs rise 12%. Margin pressure."), |
| ("App promo goes viral", "New users surge. More first-time buyers, less predictable demand."), |
| ] |
|
|
| DECISIONS = [ |
| ("Reduce pastry production", "Reduce baseline pastry production by 15%."), |
| ("Afternoon discounts", "Offer 10–25% discount after 4 PM on selected items."), |
| ("Add morning staff", "Add 1 staff member 8–10 AM to reduce queue time."), |
| ("Change product mix", "Add 1 vegan option, remove 1 low-selling pastry."), |
| ("Invest: Demand forecasting", "Build forecasting to reduce stockouts & waste."), |
| ("Invest: Segmentation", "Cluster customers for targeted offers."), |
| ("Invest: Sentiment analysis", "Analyze reviews for themes and priorities."), |
| ("Invest: Dynamic pricing", "Automate discounts based on predicted demand/waste."), |
| ] |
|
|
| |
| |
| |
| rooms_lock = threading.Lock() |
| rooms: Dict[str, Any] = {} |
|
|
|
|
| def _now_ms() -> int: |
| return int(time.time() * 1000) |
|
|
|
|
| def _new_room_code() -> str: |
| return f"ROOM-{random.randint(1000, 9999)}" |
|
|
|
|
| def _safe_json_loads(text: str) -> Optional[dict]: |
| try: |
| |
| return json.loads(text) |
| except Exception: |
| |
| start = text.find("{") |
| end = text.rfind("}") |
| if start != -1 and end != -1 and end > start: |
| try: |
| return json.loads(text[start : end + 1]) |
| except Exception: |
| return None |
| return None |
|
|
|
|
| |
| |
| |
| BUSINESS_CONTEXT = """ |
| You are the AI Business Assistant for SmartCampus Café (inside a university). |
| Known situation: |
| - 1,200–1,800 customers/day; peaks 8–10 AM and 1–3 PM |
| - Pastry waste about 12% |
| - Queues during morning peak |
| - Sales drop after 4 PM |
| - Competition nearby + delivery apps |
| Data exists but may need to be requested selectively with a budget. |
| |
| Your job: |
| - Provide background about the business when asked. |
| - Brainstorm approaches, variables, baselines, KPIs, risks. |
| - Give hints ONLY when students ask for hints. |
| - Ask clarifying questions when students are vague. |
| - Do NOT provide final numeric answers or “the best decision”. |
| - Do NOT invent dataset values. |
| - Encourage validation, baselines, and limitations. |
| Keep responses concise and practical. |
| """.strip() |
|
|
| STUDENT_GAME_BRIEF = """ |
| ## Business Context |
| You are the analytics team for **SmartCampus Café**, a university coffee shop with: |
| - 1,200-1,800 customers per day |
| - Peak demand at **8-10 AM** and **1-3 PM** |
| - About **12% pastry waste** |
| - Long queues during morning rush |
| - Sales drop after 4 PM |
| - Pressure from nearby competitors and delivery apps |
| |
| ## The Core Problem |
| Your team must make better operating decisions that improve: |
| - **Profit** (waste, discounts, costs, stockouts) |
| - **Customer experience** (wait times, availability, satisfaction) |
| |
| You cannot unlock all data at once, so you need to choose datasets strategically under a budget. |
| |
| ## What Is Expected From Students |
| 1. Form a clear business question. |
| 2. Request datasets with justification (what you need and why). |
| 3. Use unlocked files/plots to extract evidence (patterns, comparisons, trends). |
| 4. Propose one decision and justify it with data. |
| 5. Include risks, limitations, and a KPI/monitoring plan. |
| |
| Good answers are not just opinions. They connect **decision -> evidence -> expected impact -> risk controls**. |
| """.strip() |
|
|
|
|
| def ai_chat(team_msg: str, room_state: dict, team_name: str) -> str: |
| if not (AI_ENABLED_DEFAULT and client): |
| return "AI is disabled (missing OPENAI_API_KEY or AI_ENABLED=0)." |
|
|
| team = room_state["teams"].get(team_name, {}) |
| unlocked = sorted(list(team.get("unlocked_datasets", []))) |
| budget_left = team.get("budget_left", 20) |
|
|
| state_summary = { |
| "round": room_state["round"], |
| "event": room_state.get("active_event"), |
| "team": team_name, |
| "budget_left": budget_left, |
| "unlocked_datasets": unlocked, |
| } |
|
|
| |
| resp = client.responses.create( |
| model=MODEL, |
| input=[ |
| {"role": "system", "content": BUSINESS_CONTEXT}, |
| { |
| "role": "user", |
| "content": f"Game state: {json.dumps(state_summary)}\n\nStudent message: {team_msg}", |
| }, |
| ], |
| max_output_tokens=400, |
| ) |
|
|
| return resp.output_text.strip() if hasattr(resp, "output_text") else str(resp) |
|
|
|
|
| DATASET_GATE_PROMPT = """ |
| You are evaluating a team's request to unlock a dataset. |
| Return STRICT JSON only, with keys: |
| - allow: boolean |
| - dataset: one of ["Sales","Inventory","Customers","Staffing","External","Feedback"] or null |
| - reason: short string |
| - suggestion: short string (how to ask better or what to do next) |
| |
| Rules: |
| - Allow only if the student states a clear business question and how the dataset will be used. |
| - If unclear, deny and ask for a sharper question. |
| - Do not reveal or invent dataset contents. |
| """.strip() |
|
|
|
|
| def ai_gate_dataset(request_text: str, room_state: dict, team_name: str) -> dict: |
| if not (AI_ENABLED_DEFAULT and client): |
| return { |
| "allow": True, |
| "dataset": None, |
| "reason": "AI disabled; defaulting to manual approval.", |
| "suggestion": "Teacher can approve manually.", |
| } |
|
|
| team = room_state["teams"].get(team_name, {}) |
| unlocked = sorted(list(team.get("unlocked_datasets", []))) |
| budget_left = team.get("budget_left", 20) |
|
|
| payload = { |
| "round": room_state["round"], |
| "team": team_name, |
| "budget_left": budget_left, |
| "already_unlocked": unlocked, |
| "request": request_text, |
| "available_datasets": list(DATASETS.keys()), |
| } |
|
|
| resp = client.responses.create( |
| model=MODEL, |
| input=[ |
| {"role": "system", "content": DATASET_GATE_PROMPT}, |
| {"role": "user", "content": json.dumps(payload)}, |
| ], |
| max_output_tokens=250, |
| ) |
|
|
| text = resp.output_text.strip() if hasattr(resp, "output_text") else "" |
| data = _safe_json_loads(text) |
| if not data: |
| return { |
| "allow": False, |
| "dataset": None, |
| "reason": "Could not parse AI response as JSON.", |
| "suggestion": "Rephrase the request: state business question + intended use.", |
| } |
|
|
| |
| ds = data.get("dataset") |
| if ds not in list(DATASETS.keys()): |
| data["dataset"] = None |
| data["allow"] = bool(data.get("allow", False)) |
| data["reason"] = str(data.get("reason", ""))[:220] |
| data["suggestion"] = str(data.get("suggestion", ""))[:220] |
| return data |
|
|
|
|
| |
| |
| |
| def simulate_impact(decision_key: str, room_state: dict, team_name: str) -> Tuple[int, int, str]: |
| """ |
| Returns (profit_points_delta, customer_points_delta, note) |
| Simple rule-based impact; you can later replace with your own simulator. |
| """ |
| round_no = room_state["round"] |
| event = room_state.get("active_event", None) |
|
|
| profit = 0 |
| cust = 0 |
| note = "" |
|
|
| if decision_key == "Reduce pastry production": |
| profit += 3 |
| cust -= 1 |
| note = "Less waste, slight availability risk." |
|
|
| elif decision_key == "Afternoon discounts": |
| profit += 2 |
| cust += 1 |
| note = "Better late-day traffic; margin trade-off." |
|
|
| elif decision_key == "Add morning staff": |
| profit -= 1 |
| cust += 3 |
| note = "Queues improve; labor cost increases." |
|
|
| elif decision_key == "Change product mix": |
| profit += 1 |
| cust += 1 |
| note = "Better fit for preferences; small operational change." |
|
|
| elif decision_key == "Invest: Demand forecasting": |
| profit += 4 |
| cust += 1 |
| note = "Better planning; benefit grows with use." |
|
|
| elif decision_key == "Invest: Segmentation": |
| profit += 2 |
| cust += 1 |
| note = "More targeted promos; needs execution." |
|
|
| elif decision_key == "Invest: Sentiment analysis": |
| profit += 1 |
| cust += 2 |
| note = "Prioritizes improvements; indirect revenue impact." |
|
|
| elif decision_key == "Invest: Dynamic pricing": |
| profit += 3 |
| cust += 0 |
| note = "Reduces waste, boosts revenue; needs guardrails." |
|
|
| |
| if event and event[0] == "Exam week": |
| if decision_key in ["Add morning staff", "Invest: Demand forecasting"]: |
| profit += 1 |
| cust += 1 |
| note += " (Great timing for exam week.)" |
|
|
| if event and event[0] == "Rainy stretch": |
| if decision_key == "Afternoon discounts": |
| profit += 1 |
| note += " (Helps counter low traffic.)" |
|
|
| |
| if round_no >= 3: |
| profit = max(profit - 1, -3) |
|
|
| return profit, cust, note |
|
|
|
|
| |
| |
| |
| def create_room(teacher_pin: str, budget_per_team: int = 20) -> Tuple[str, str]: |
| if teacher_pin.strip() != TEACHER_PIN: |
| return "", "Incorrect teacher PIN." |
|
|
| code = _new_room_code() |
| with rooms_lock: |
| rooms[code] = { |
| "created_at": _now_ms(), |
| "round": 1, |
| "budget_per_team": int(budget_per_team), |
| "active_event": None, |
| "teams": {}, |
| "log": [], |
| } |
| return code, f"Room created: {code}" |
|
|
|
|
| def start_free_play(team_name: str, budget_per_team: int = 20) -> Tuple[dict, str]: |
| team_name = (team_name or "").strip() |
| if not team_name: |
| team_name = f"Solo Team {random.randint(10, 99)}" |
|
|
| code = f"FREE-{random.randint(1000, 9999)}" |
| with rooms_lock: |
| while code in rooms: |
| code = f"FREE-{random.randint(1000, 9999)}" |
| rooms[code] = { |
| "created_at": _now_ms(), |
| "round": 1, |
| "budget_per_team": int(budget_per_team), |
| "active_event": None, |
| "teams": { |
| team_name: { |
| "joined_at": _now_ms(), |
| "budget_left": int(budget_per_team), |
| "unlocked_datasets": set(), |
| "profit_points": 0, |
| "customer_points": 0, |
| "submissions": [], |
| } |
| }, |
| "log": [{"t": _now_ms(), "type": "free_play_start", "team": team_name}], |
| } |
| return {"room": code, "team": team_name}, f"Free play started in {code} as {team_name}." |
|
|
|
|
| def join_room(room_code: str, team_name: str) -> Tuple[dict, str]: |
| room_code = room_code.strip().upper() |
| team_name = team_name.strip() |
|
|
| if not room_code or not team_name: |
| return {}, "Provide room code and team name." |
|
|
| with rooms_lock: |
| if room_code not in rooms: |
| return {}, "Room not found." |
| r = rooms[room_code] |
| if team_name not in r["teams"]: |
| r["teams"][team_name] = { |
| "joined_at": _now_ms(), |
| "budget_left": r["budget_per_team"], |
| "unlocked_datasets": set(), |
| "profit_points": 0, |
| "customer_points": 0, |
| "submissions": [], |
| } |
| r["log"].append({"t": _now_ms(), "type": "join", "team": team_name}) |
| return {"room": room_code, "team": team_name}, f"Joined {room_code} as {team_name}." |
|
|
|
|
| def get_room_state(room_code: str) -> Optional[dict]: |
| with rooms_lock: |
| return rooms.get(room_code) |
|
|
|
|
| def dataset_file_path(dataset_name: str) -> Optional[Path]: |
| filename = DATASET_FILES.get(dataset_name) |
| if not filename: |
| return None |
| return DATA_DIR / filename |
|
|
|
|
| def scoreboard_df(room_state: dict) -> List[List[Any]]: |
| rows = [] |
| for team, st in room_state["teams"].items(): |
| rows.append([ |
| team, |
| st.get("budget_left", 0), |
| st.get("profit_points", 0), |
| st.get("customer_points", 0), |
| st.get("profit_points", 0) + st.get("customer_points", 0), |
| ", ".join(sorted(list(st.get("unlocked_datasets", [])))), |
| ]) |
| rows.sort(key=lambda r: r[4], reverse=True) |
| return rows |
|
|
|
|
| |
| |
| |
| def ui_refresh(session: dict) -> Tuple[str, List[List[Any]], str]: |
| if not session: |
| return "Not joined.", [], "" |
| room_state = get_room_state(session["room"]) |
| if not room_state: |
| return "Room expired or restarted.", [], "" |
| ev = room_state["active_event"] |
| ev_text = f"{ev[0]} — {ev[1]}" if ev else "No active event." |
| header = f"Room: {session['room']} | Team: {session['team']} | Round: {room_state['round']} | Event: {ev_text}" |
| return header, scoreboard_df(room_state), ev_text |
|
|
|
|
| def ui_ai_chat(session: dict, message: str, chat_history: list): |
| if not session: |
| return chat_history, "Join a room first." |
| room_state = get_room_state(session["room"]) |
| if not room_state: |
| return chat_history, "Room expired or restarted." |
| if not message.strip(): |
| return chat_history, "" |
|
|
| reply = ai_chat(message.strip(), room_state, session["team"]) |
|
|
| if chat_history is None: |
| chat_history = [] |
|
|
| chat_history = chat_history + [ |
| {"role": "user", "content": message.strip()}, |
| {"role": "assistant", "content": reply}, |
| ] |
| return chat_history, "" |
|
|
|
|
| def ui_request_dataset(session: dict, request_text: str) -> Tuple[str, List[List[Any]]]: |
| if not session: |
| return "Join a room first.", [] |
| room_state = get_room_state(session["room"]) |
| if not room_state: |
| return "Room expired or restarted.", [] |
| team_name = session["team"] |
|
|
| gate = ai_gate_dataset(request_text.strip(), room_state, team_name) |
|
|
| |
| if gate.get("allow") and not gate.get("dataset"): |
| return f"Approved in principle, but dataset not specified. Choose one dataset name. Hint: {gate.get('suggestion','')}", scoreboard_df(room_state) |
|
|
| ds = gate.get("dataset") |
| if not gate.get("allow") or not ds: |
| return f"Denied: {gate.get('reason','')} Suggestion: {gate.get('suggestion','')}", scoreboard_df(room_state) |
|
|
| |
| with rooms_lock: |
| r = rooms.get(session["room"]) |
| if not r: |
| return "Room expired or restarted.", [] |
| team = r["teams"].get(team_name) |
| if not team: |
| return "Team not found.", scoreboard_df(r) |
|
|
| if ds in team["unlocked_datasets"]: |
| return f"Dataset already unlocked: {ds}", scoreboard_df(r) |
|
|
| cost = DATASETS[ds]["cost"] |
| if team["budget_left"] < cost: |
| return f"Not enough budget for {ds} (cost {cost}). Budget left: {team['budget_left']}", scoreboard_df(r) |
|
|
| team["budget_left"] -= cost |
| team["unlocked_datasets"].add(ds) |
| r["log"].append({"t": _now_ms(), "type": "unlock", "team": team_name, "dataset": ds, "cost": cost}) |
|
|
| return f"Unlocked dataset: {ds} (cost {DATASETS[ds]['cost']}). {gate.get('reason','')}", scoreboard_df(get_room_state(session["room"])) |
|
|
|
|
| def ui_submit_work(session: dict, decision: str, model_card: str, decision_report: str) -> str: |
| if not session: |
| return "Join a room first." |
| room_state = get_room_state(session["room"]) |
| if not room_state: |
| return "Room expired or restarted." |
|
|
| decision = decision.strip() |
| if not decision: |
| return "Pick a decision." |
|
|
| |
| profit_delta, cust_delta, note = simulate_impact(decision, room_state, session["team"]) |
|
|
| submission = { |
| "t": _now_ms(), |
| "round": room_state["round"], |
| "decision": decision, |
| "model_card": (model_card or "").strip(), |
| "decision_report": (decision_report or "").strip(), |
| "profit_delta": profit_delta, |
| "customer_delta": cust_delta, |
| "note": note, |
| } |
|
|
| with rooms_lock: |
| r = rooms.get(session["room"]) |
| if not r: |
| return "Room expired or restarted." |
| team = r["teams"].get(session["team"]) |
| if not team: |
| return "Team not found." |
|
|
| team["profit_points"] += profit_delta |
| team["customer_points"] += cust_delta |
| team["submissions"].append(submission) |
| r["log"].append({"t": _now_ms(), "type": "submit", "team": session["team"], "decision": decision}) |
|
|
| return f"Submitted. Impact: Profit {profit_delta:+d}, Customer {cust_delta:+d}. {note}" |
|
|
|
|
| def _word_count(text: str) -> int: |
| return len([w for w in (text or "").strip().split() if w]) |
|
|
|
|
| def _contains_any(text: str, terms: List[str]) -> bool: |
| t = (text or "").lower() |
| return any(term in t for term in terms) |
|
|
|
|
| def ui_tutor_feedback(session: dict, decision: str, model_card: str, decision_report: str) -> str: |
| if not session: |
| return "Join a room first." |
|
|
| room_state = get_room_state(session["room"]) |
| if not room_state: |
| return "Room expired or restarted." |
|
|
| team = room_state["teams"].get(session["team"], {}) |
| unlocked = sorted(list(team.get("unlocked_datasets", []))) |
| model_card = (model_card or "").strip() |
| decision_report = (decision_report or "").strip() |
| merged_text = f"{model_card}\n{decision_report}".lower() |
|
|
| step_1 = bool(decision and decision.strip()) and _word_count(decision_report) >= 40 |
| step_2 = len(unlocked) > 0 and _contains_any(merged_text, [d.lower() for d in unlocked]) |
| step_3 = _contains_any(merged_text, ["baseline", "validation", "metric", "compare", "experiment", "assumption"]) |
| step_4 = _contains_any(merged_text, ["risk", "limitation", "trade-off", "uncertainty", "bias"]) |
| step_5 = _contains_any(merged_text, ["next", "plan", "monitor", "kpi", "target", "threshold", "weekly"]) |
|
|
| checks = [ |
| ("Step 1 - Decision clarity", step_1, "State one concrete action and a report of at least ~40 words."), |
| ("Step 2 - Evidence from data", step_2, "Reference at least one unlocked dataset and what signal it shows."), |
| ("Step 3 - Method and validation", step_3, "Add baseline/validation/metric language to show rigor."), |
| ("Step 4 - Risks and limitations", step_4, "Include at least one downside, uncertainty, or limitation."), |
| ("Step 5 - Execution plan + KPI", step_5, "Define next step, owner/timing, and KPI to monitor."), |
| ] |
|
|
| passed = sum(1 for _, ok, _ in checks if ok) |
| status_lines = [] |
| for label, ok, hint in checks: |
| status = "PASS" if ok else "NEEDS WORK" |
| status_lines.append(f"- **{label}: {status}** \n {hint}") |
|
|
| summary = ( |
| f"### Tutor Feedback (Step-by-step)\n" |
| f"Team: `{session['team']}` | Round: `{room_state['round']}` | Score: `{passed}/5`\n\n" |
| + "\n".join(status_lines) |
| ) |
|
|
| if passed <= 2: |
| summary += "\n\n<div class='feedback-box'><b>Tutor next move:</b> Improve Step 2 and Step 3 first. Add specific dataset evidence and one validation metric.</div>" |
| elif passed == 3: |
| summary += "\n\n<div class='feedback-box'><b>Tutor next move:</b> Strong draft. Tighten risks and KPI execution details to make it decision-ready.</div>" |
| else: |
| summary += "\n\n<div class='feedback-box'><b>Tutor next move:</b> This is close to publishable. Make numbers explicit for KPI targets and timeline.</div>" |
|
|
| return summary |
|
|
|
|
| def ui_get_dataset_file(session: dict, dataset_name: str) -> Tuple[str, Optional[str]]: |
| if not session: |
| return "Join a room first.", None |
| room_state = get_room_state(session["room"]) |
| if not room_state: |
| return "Room expired or restarted.", None |
|
|
| team = room_state["teams"].get(session["team"]) |
| if not team: |
| return "Team not found.", None |
|
|
| dataset_name = (dataset_name or "").strip() |
| if dataset_name not in DATASETS: |
| return "Choose a valid dataset.", None |
| if dataset_name not in team.get("unlocked_datasets", set()): |
| return f"Dataset not unlocked yet: {dataset_name}", None |
|
|
| path = dataset_file_path(dataset_name) |
| if not path or not path.exists(): |
| return f"File not found for {dataset_name}. Expected: data/{DATASET_FILES.get(dataset_name, '')}", None |
|
|
| return f"File ready: {path.name}", str(path) |
|
|
|
|
| def ui_dataset_plot(session: dict, dataset_name: str, plot_type: str): |
| if not session: |
| return "Join a room first.", None |
| room_state = get_room_state(session["room"]) |
| if not room_state: |
| return "Room expired or restarted.", None |
|
|
| team = room_state["teams"].get(session["team"]) |
| if not team: |
| return "Team not found.", None |
|
|
| dataset_name = (dataset_name or "").strip() |
| if dataset_name not in DATASETS: |
| return "Choose a valid dataset.", None |
| if dataset_name not in team.get("unlocked_datasets", set()): |
| return f"Dataset not unlocked yet: {dataset_name}", None |
|
|
| path = dataset_file_path(dataset_name) |
| if not path or not path.exists(): |
| return f"File not found for {dataset_name}.", None |
|
|
| try: |
| df = pd.read_csv(path) |
| except Exception as exc: |
| return f"Could not load CSV: {exc}", None |
|
|
| fig, ax = plt.subplots(figsize=(8, 4.5)) |
|
|
| try: |
| if dataset_name == "Sales": |
| data = df.groupby("hour", as_index=False)["revenue_eur"].sum() |
| if plot_type == "Line": |
| ax.plot(data["hour"], data["revenue_eur"], marker="o", color="#2d6a4f") |
| else: |
| ax.bar(data["hour"], data["revenue_eur"], color="#2d6a4f") |
| ax.set_title("Sales: Revenue by Hour") |
| ax.set_xlabel("Hour") |
| ax.set_ylabel("Revenue (EUR)") |
|
|
| elif dataset_name == "Inventory": |
| if plot_type == "Line": |
| ax.plot(df["item"], df["waste"], marker="o", color="#cc5803") |
| else: |
| ax.bar(df["item"], df["waste"], color="#cc5803") |
| ax.set_title("Inventory: Waste by Item") |
| ax.set_xlabel("Item") |
| ax.set_ylabel("Waste") |
| ax.tick_params(axis="x", rotation=30) |
|
|
| elif dataset_name == "Customers": |
| if plot_type == "Line": |
| data = df.groupby("segment", as_index=False)["avg_basket_eur"].mean() |
| ax.plot(data["segment"], data["avg_basket_eur"], marker="o", color="#3867d6") |
| else: |
| data = df.groupby("segment", as_index=False)["avg_basket_eur"].mean() |
| ax.bar(data["segment"], data["avg_basket_eur"], color="#3867d6") |
| ax.set_title("Customers: Avg Basket by Segment") |
| ax.set_xlabel("Segment") |
| ax.set_ylabel("Avg Basket (EUR)") |
|
|
| elif dataset_name == "Staffing": |
| data = df.groupby("shift", as_index=False)["avg_service_seconds"].mean() |
| if plot_type == "Line": |
| ax.plot(data["shift"], data["avg_service_seconds"], marker="o", color="#6a4c93") |
| else: |
| ax.bar(data["shift"], data["avg_service_seconds"], color="#6a4c93") |
| ax.set_title("Staffing: Avg Service Time by Shift") |
| ax.set_xlabel("Shift") |
| ax.set_ylabel("Seconds") |
|
|
| elif dataset_name == "External": |
| if plot_type == "Line": |
| ax.plot(df["date"], df["foot_traffic_index"], marker="o", color="#118ab2") |
| else: |
| ax.bar(df["date"], df["foot_traffic_index"], color="#118ab2") |
| ax.set_title("External: Foot Traffic Index by Date") |
| ax.set_xlabel("Date") |
| ax.set_ylabel("Foot Traffic Index") |
| ax.tick_params(axis="x", rotation=30) |
|
|
| elif dataset_name == "Feedback": |
| data = df.groupby("theme", as_index=False)["rating"].mean() |
| if plot_type == "Line": |
| ax.plot(data["theme"], data["rating"], marker="o", color="#2a9d8f") |
| else: |
| ax.bar(data["theme"], data["rating"], color="#2a9d8f") |
| ax.set_title("Feedback: Avg Rating by Theme") |
| ax.set_xlabel("Theme") |
| ax.set_ylabel("Average Rating") |
| ax.tick_params(axis="x", rotation=20) |
|
|
| ax.grid(alpha=0.2) |
| fig.tight_layout() |
| return f"Showing {plot_type.lower()} chart for {dataset_name}.", fig |
| except Exception as exc: |
| plt.close(fig) |
| return f"Could not generate plot: {exc}", None |
|
|
|
|
| def teacher_next_round(pin: str, room_code: str) -> str: |
| if pin.strip() != TEACHER_PIN: |
| return "Incorrect teacher PIN." |
| room_code = room_code.strip().upper() |
| with rooms_lock: |
| if room_code not in rooms: |
| return "Room not found." |
| rooms[room_code]["round"] += 1 |
| rooms[room_code]["log"].append({"t": _now_ms(), "type": "round_advance", "round": rooms[room_code]["round"]}) |
| return f"Advanced to round {rooms[room_code]['round']}." |
|
|
|
|
| def teacher_set_event(pin: str, room_code: str, event_name: str) -> str: |
| if pin.strip() != TEACHER_PIN: |
| return "Incorrect teacher PIN." |
| room_code = room_code.strip().upper() |
| with rooms_lock: |
| if room_code not in rooms: |
| return "Room not found." |
| if event_name == "None": |
| rooms[room_code]["active_event"] = None |
| else: |
| match = [e for e in EVENT_CARDS if e[0] == event_name] |
| rooms[room_code]["active_event"] = match[0] if match else None |
| rooms[room_code]["log"].append({"t": _now_ms(), "type": "event_set", "event": event_name}) |
| return f"Event set: {event_name}" |
|
|
|
|
| def teacher_draw_random_event(pin: str, room_code: str) -> str: |
| if pin.strip() != TEACHER_PIN: |
| return "Incorrect teacher PIN." |
| room_code = room_code.strip().upper() |
| ev = random.choice(EVENT_CARDS) |
| with rooms_lock: |
| if room_code not in rooms: |
| return "Room not found." |
| rooms[room_code]["active_event"] = ev |
| rooms[room_code]["log"].append({"t": _now_ms(), "type": "event_draw", "event": ev[0]}) |
| return f"Drew event: {ev[0]} — {ev[1]}" |
|
|
|
|
| def teacher_room_overview(pin: str, room_code: str) -> str: |
| if pin.strip() != TEACHER_PIN: |
| return "Incorrect teacher PIN." |
| room_code = room_code.strip().upper() |
| r = get_room_state(room_code) |
| if not r: |
| return "Room not found." |
| lines = [f"Room {room_code} | Round {r['round']} | Teams {len(r['teams'])}"] |
| ev = r.get("active_event") |
| lines.append(f"Event: {ev[0]} — {ev[1]}" if ev else "Event: None") |
| lines.append("") |
| for team, st in r["teams"].items(): |
| lines.append(f"- {team}: budget {st['budget_left']}, profit {st['profit_points']}, customer {st['customer_points']}, unlocked {sorted(list(st['unlocked_datasets']))}") |
| return "\n".join(lines) |
|
|
|
|
| |
| |
| |
| with gr.Blocks( |
| title="AI Café Challenge (Session-Based)", |
| theme=gr.themes.Soft(primary_hue="green", secondary_hue="orange", neutral_hue="stone"), |
| ) as demo: |
| gr.Markdown( |
| """ |
| <div class="hero"> |
| <h1>AI Café Challenge</h1> |
| <p>Run your campus café like a data team: unlock evidence, test decisions, and iterate with tutor feedback.</p> |
| </div> |
| """ |
| ) |
| gr.Markdown( |
| """ |
| ### How to play |
| 1. Join an existing room, or click **Start Free Play** to explore solo. |
| 2. Ask the assistant for hints and request datasets with a clear business question. |
| 3. Use unlocked files and plots to build evidence, then justify your choice with facts (which dataset, what pattern, and why that supports your decision). |
| 4. Submit a decision with model card + decision report. |
| 5. Use **Tutor Feedback (Step-by-step)** to improve your work each round. |
| """ |
| ) |
| with gr.Accordion("Business Context, Problem, and Expectations", open=True): |
| gr.Markdown(STUDENT_GAME_BRIEF) |
|
|
| session = gr.State({}) |
|
|
| with gr.Tab("Student"): |
| with gr.Row(): |
| room_in = gr.Textbox(label="Room code", placeholder="ROOM-1234") |
| team_in = gr.Textbox(label="Team name", placeholder="e.g., Team Orion") |
| join_btn = gr.Button("Join") |
| free_play_btn = gr.Button("Start Free Play", variant="primary") |
|
|
| join_status = gr.Markdown("Not joined.", elem_classes=["block-panel"]) |
| header = gr.Markdown("") |
| score_table = gr.Dataframe( |
| headers=["Team", "Budget left", "Profit", "Customer", "Total", "Unlocked datasets"], |
| datatype=["str", "number", "number", "number", "number", "str"], |
| row_count=8, |
| col_count=(6, "fixed"), |
| interactive=False, |
| label="Scoreboard", |
| ) |
| event_text = gr.Markdown("") |
|
|
| refresh_btn = gr.Button("Refresh scoreboard") |
|
|
| gr.Markdown("## AI Business Assistant (Hints / Background / Brainstorming)") |
| chatbot = gr.Chatbot(label="Assistant", height=320) |
| chat_msg = gr.Textbox(label="Message", placeholder="Ask for background, or request a hint, or brainstorm approaches.") |
| chat_btn = gr.Button("Send") |
|
|
| gr.Markdown("## Request a dataset (AI will approve/deny)") |
| gr.Markdown( |
| "Justify the request in 1-2 sentences: **business question + chosen dataset + how you will use it + expected decision impact**." |
| ) |
| ds_request = gr.Textbox(label="Dataset request", placeholder='Example: "We want to forecast hourly pastry demand; please unlock Sales to model seasonality and peaks."') |
| ds_btn = gr.Button("Request dataset") |
| ds_status = gr.Markdown("", elem_classes=["block-panel"]) |
|
|
| gr.Markdown("## Access unlocked data files") |
| file_ds_dd = gr.Dropdown(list(DATASETS.keys()), label="Unlocked dataset", value="Sales") |
| file_btn = gr.Button("Get dataset file") |
| file_status = gr.Markdown("", elem_classes=["block-panel"]) |
| data_file = gr.File(label="Dataset file", interactive=False) |
|
|
| gr.Markdown("## Data Plots") |
| with gr.Row(): |
| plot_ds_dd = gr.Dropdown(list(DATASETS.keys()), label="Dataset", value="Sales") |
| plot_type_dd = gr.Dropdown(["Bar", "Line"], label="Plot type", value="Bar") |
| plot_btn = gr.Button("Show plot", variant="primary") |
| plot_status = gr.Markdown("", elem_classes=["block-panel"]) |
| plot_view = gr.Plot(label="Dataset chart") |
|
|
| gr.Markdown("## Submit work (Decision + Model Card + Decision Report)") |
| decision_dd = gr.Dropdown([d[0] for d in DECISIONS], label="Decision", value=DECISIONS[0][0]) |
| model_card = gr.Textbox( |
| label="Model Card (short)", |
| lines=8, |
| placeholder="Business objective...\nData used...\nMethod...\nValidation...\nKey insight...\nLimitations...", |
| ) |
| decision_report = gr.Textbox( |
| label="Decision Report (150–200 words)", |
| lines=6, |
| placeholder="What action? Evidence? Expected impact? Confidence (Low/Med/High).", |
| ) |
| submit_btn = gr.Button("Submit") |
| submit_status = gr.Markdown("", elem_classes=["block-panel"]) |
|
|
| gr.Markdown("## Tutor Feedback (Step-by-step)") |
| tutor_btn = gr.Button("Review Draft") |
| tutor_feedback = gr.Markdown("Tutor feedback will appear here after you review a draft.", elem_classes=["block-panel"]) |
|
|
| def _join(room_code, team_name): |
| sess, msg = join_room(room_code, team_name) |
| return sess, msg |
|
|
| def _free_play(team_name): |
| return start_free_play(team_name) |
|
|
| join_btn.click(_join, [room_in, team_in], [session, join_status]).then( |
| ui_refresh, [session], [header, score_table, event_text] |
| ) |
| free_play_btn.click(_free_play, [team_in], [session, join_status]).then( |
| ui_refresh, [session], [header, score_table, event_text] |
| ) |
|
|
| refresh_btn.click(ui_refresh, [session], [header, score_table, event_text]) |
|
|
| chat_btn.click(ui_ai_chat, [session, chat_msg, chatbot], [chatbot, chat_msg]) |
|
|
| ds_btn.click(ui_request_dataset, [session, ds_request], [ds_status, score_table]) |
| file_btn.click(ui_get_dataset_file, [session, file_ds_dd], [file_status, data_file]) |
| plot_btn.click(ui_dataset_plot, [session, plot_ds_dd, plot_type_dd], [plot_status, plot_view]) |
| tutor_btn.click(ui_tutor_feedback, [session, decision_dd, model_card, decision_report], [tutor_feedback]) |
|
|
| submit_btn.click(ui_submit_work, [session, decision_dd, model_card, decision_report], [submit_status]).then( |
| ui_refresh, [session], [header, score_table, event_text] |
| ).then( |
| ui_tutor_feedback, [session, decision_dd, model_card, decision_report], [tutor_feedback] |
| ) |
|
|
| with gr.Tab("Teacher"): |
| gr.Markdown("## Teacher Controls (PIN-protected)") |
|
|
| t_pin = gr.Textbox(label="Teacher PIN", type="password", placeholder="Set TEACHER_PIN in HF Secrets") |
| budget = gr.Number(label="Budget per team", value=20, precision=0) |
| create_btn = gr.Button("Create new room") |
| room_out = gr.Textbox(label="Room code (share with students)") |
| create_msg = gr.Markdown("") |
|
|
| create_btn.click(create_room, [t_pin, budget], [room_out, create_msg]) |
|
|
| gr.Markdown("### Manage existing room") |
| t_room = gr.Textbox(label="Room code", placeholder="ROOM-1234") |
| next_round_btn = gr.Button("Next round") |
| next_round_msg = gr.Markdown("") |
|
|
| next_round_btn.click(teacher_next_round, [t_pin, t_room], [next_round_msg]) |
|
|
| gr.Markdown("### Events") |
| ev_names = ["None"] + [e[0] for e in EVENT_CARDS] |
| ev_dd = gr.Dropdown(ev_names, label="Set event", value="None") |
| set_ev_btn = gr.Button("Set event") |
| draw_ev_btn = gr.Button("Draw random event") |
| ev_msg = gr.Markdown("") |
|
|
| set_ev_btn.click(teacher_set_event, [t_pin, t_room, ev_dd], [ev_msg]) |
| draw_ev_btn.click(teacher_draw_random_event, [t_pin, t_room], [ev_msg]) |
|
|
| gr.Markdown("### Overview") |
| overview_btn = gr.Button("Show room overview") |
| overview_txt = gr.Textbox(label="Overview", lines=12) |
|
|
| overview_btn.click(teacher_room_overview, [t_pin, t_room], [overview_txt]) |
|
|
| gr.Markdown("") |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| if __name__ == "__main__": |
| demo.launch() |
| |
| |
| |
| |
|
|