Spaces:
Sleeping
Sleeping
| """ | |
| LifeOS β Streamlit UI (File 12 of 15) | |
| Adaptive Student Schedule Optimizer with multi-agent AI backend. | |
| Features: | |
| - Pre-loads SAMPLE_TASKS by default (no typing required β Section 6 rule 10) | |
| - GROQ_API_KEY validation with red error banner | |
| - Sidebar with dynamic task editor + preferences | |
| - Tabbed layout: Schedule Results | Before vs After Training | |
| - 3-column main area: iteration history | reward chart | best schedule | |
| - Reward chart via matplotlib + st.pyplot() | |
| - Spinner wrapping entire run_lifeos() call (Section 6 rule 6) | |
| - Coloured badges for study/break/review slots | |
| GAP 6: "Before vs After Training" tab with side-by-side component comparison. | |
| IMPROVEMENT 3: Per-component horizontal bar chart in iteration history. | |
| """ | |
| import json | |
| import os | |
| import sys | |
| from datetime import date, timedelta | |
| from pathlib import Path | |
| import re | |
| # SUPER IMPORTANT: Force Python to look in the current directory for modules | |
| # This permanently fixes "No module named 'env'" on Hugging Face Spaces | |
| sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) | |
| import streamlit as st | |
| from dotenv import load_dotenv | |
| load_dotenv() | |
| # --------------------------------------------------------------------------- | |
| # Page config β must be first Streamlit call | |
| # --------------------------------------------------------------------------- | |
| st.set_page_config( | |
| page_title="LifeOS β Study Schedule Optimizer", | |
| page_icon="π", | |
| layout="wide", | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # Global styling | |
| # --------------------------------------------------------------------------- | |
| st.markdown(""" | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700;800&display=swap'); | |
| html, body, [class*="css"] { font-family: 'Inter', sans-serif; } | |
| .hero-title { font-size: 2.4rem; font-weight: 800; color: #1e3a5f; margin-bottom: 0; } | |
| .hero-sub { font-size: 1.05rem; color: #64748b; margin-top: 0.2rem; } | |
| .study-badge { display:inline-block; background:#1e40af; color:#fff; | |
| padding:2px 10px; border-radius:6px; font-size:0.78rem; | |
| font-weight:600; letter-spacing:0.03em; } | |
| .break-badge { display:inline-block; background:#065f46; color:#fff; | |
| padding:2px 10px; border-radius:6px; font-size:0.78rem; | |
| font-weight:600; } | |
| .review-badge { display:inline-block; background:#92400e; color:#fff; | |
| padding:2px 10px; border-radius:6px; font-size:0.78rem; | |
| font-weight:600; } | |
| .score-good { color:#16a34a; font-size:2.2rem; font-weight:800; } | |
| .score-mid { color:#d97706; font-size:2.2rem; font-weight:800; } | |
| .score-bad { color:#dc2626; font-size:2.2rem; font-weight:800; } | |
| .issue-line { color:#dc2626; font-size:0.88rem; margin:1px 0; } | |
| .pass-line { color:#16a34a; font-size:0.88rem; margin:1px 0; } | |
| .comp-bar { display:inline-block; height:12px; border-radius:3px; min-width:2px; } | |
| .comp-bar-green { background:#16a34a; } | |
| .comp-bar-orange { background:#d97706; } | |
| .comp-bar-red { background:#dc2626; } | |
| .comp-bar-gray { background:#94a3b8; } | |
| .comp-label { font-size:0.72rem; color:#64748b; display:inline-block; width:65px; } | |
| div[data-testid="stExpander"] { border: 1px solid #e2e8f0; border-radius: 8px; } | |
| [data-testid="column"] h2 { white-space: nowrap; font-size: 1.1rem; } | |
| .best-header { margin:0; font-size:1.8rem; font-weight:700; line-height:1.2; white-space:normal; } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # --------------------------------------------------------------------------- | |
| # Hero header | |
| # --------------------------------------------------------------------------- | |
| st.markdown('<p class="hero-title">π LifeOS</p>', unsafe_allow_html=True) | |
| st.markdown( | |
| '<p class="hero-sub">Adaptive Student Schedule Optimizer Β· ' | |
| 'Multi-agent AI Β· Powered by Groq LLaMA 3.1</p>', | |
| unsafe_allow_html=True, | |
| ) | |
| st.divider() | |
| # --------------------------------------------------------------------------- | |
| # GROQ_API_KEY validation | |
| # --------------------------------------------------------------------------- | |
| GROQ_KEY = os.getenv("GROQ_API_KEY", "") | |
| if not GROQ_KEY or GROQ_KEY == "your_groq_api_key_here": | |
| st.error( | |
| "**GROQ_API_KEY not configured.** \n" | |
| "1. Create `.env` in the project root \n" | |
| "2. Add `GROQ_API_KEY=gsk_your_key_here` \n" | |
| "3. Get a free key at [console.groq.com](https://console.groq.com) \n\n" | |
| "*The app will still run using the heuristic fallback β " | |
| "but LLM-powered scheduling and memory insights require a key.*" | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # Session state bootstrap | |
| # --------------------------------------------------------------------------- | |
| if "results" not in st.session_state: | |
| st.session_state.results = [] | |
| if "best_summary" not in st.session_state: | |
| st.session_state.best_summary = {"best_reward": None, "best_plan": None, "best_iter": None} | |
| if "tasks" not in st.session_state: | |
| # Pre-load sample tasks so the demo works out of the box | |
| from data.sample_tasks import SAMPLE_TASKS as _DEFAULT | |
| st.session_state.tasks = [dict(t) for t in _DEFAULT] | |
| # --------------------------------------------------------------------------- | |
| # Main layout β left controls, right results (matches reference UI) | |
| # --------------------------------------------------------------------------- | |
| left_col, right_col = st.columns([1, 2], gap="large") | |
| with left_col: | |
| st.markdown('<div class="lifeos-card">', unsafe_allow_html=True) | |
| st.markdown('<p class="lifeos-card-title">Tasks</p>', unsafe_allow_html=True) | |
| st.markdown('<p class="lifeos-card-sub">Define your study tasks and deadlines</p>', unsafe_allow_html=True) | |
| tool_a, tool_b = st.columns(2) | |
| with tool_a: | |
| if st.button("π¦ Load Sample Scenario", use_container_width=True, type="secondary"): | |
| from data.sample_tasks import SAMPLE_TASKS | |
| st.session_state.tasks = [dict(t) for t in SAMPLE_TASKS] | |
| st.session_state.results = [] | |
| st.success("Sample scenario loaded!") | |
| with tool_b: | |
| if st.button("β Add Task", use_container_width=True): | |
| next_id = max((t.get("id", 0) for t in st.session_state.tasks), default=0) + 1 | |
| st.session_state.tasks.append({ | |
| "id": next_id, | |
| "name": f"New Task {next_id}", | |
| "deadline": str(date.today() + timedelta(days=5)) + " 23:59", | |
| "duration_hrs": 2.0, | |
| "priority": "medium", | |
| "subject": "General", | |
| }) | |
| for idx, task in enumerate(st.session_state.tasks): | |
| with st.container(border=True): | |
| top_l, top_r = st.columns([8, 1]) | |
| with top_l: | |
| st.session_state.tasks[idx]["name"] = st.text_input( | |
| "Task name", | |
| value=task.get("name", ""), | |
| key=f"n_{idx}", | |
| label_visibility="collapsed", | |
| placeholder="Task name", | |
| ) | |
| with top_r: | |
| if st.button("π", key=f"rm_{idx}", use_container_width=True): | |
| st.session_state.tasks.pop(idx) | |
| st.rerun() | |
| row_1a, row_1b = st.columns(2) | |
| with row_1a: | |
| st.session_state.tasks[idx]["subject"] = st.text_input( | |
| "Subject", value=task.get("subject", ""), key=f"s_{idx}") | |
| with row_1b: | |
| st.session_state.tasks[idx]["priority"] = st.selectbox( | |
| "Priority", | |
| ["high", "medium", "low"], | |
| index=["high", "medium", "low"].index(task.get("priority", "medium")), | |
| key=f"p_{idx}", | |
| ) | |
| row_2a, row_2b = st.columns(2) | |
| with row_2a: | |
| dl_str = task.get("deadline", str(date.today()) + " 23:59") | |
| dl_date = date.fromisoformat(dl_str.split(" ")[0]) | |
| new_dl = st.date_input("Deadline", value=dl_date, key=f"d_{idx}") | |
| st.session_state.tasks[idx]["deadline"] = str(new_dl) + " 23:59" | |
| with row_2b: | |
| st.session_state.tasks[idx]["duration_hrs"] = st.number_input( | |
| "Duration (hrs)", min_value=0.5, max_value=24.0, | |
| value=float(task.get("duration_hrs", 2.0)), | |
| step=0.5, key=f"h_{idx}", | |
| ) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| st.markdown('<div class="lifeos-card">', unsafe_allow_html=True) | |
| st.markdown('<p class="lifeos-card-title">Preferences</p>', unsafe_allow_html=True) | |
| st.markdown('<p class="lifeos-card-sub">Customize your study preferences</p>', unsafe_allow_html=True) | |
| max_daily_hours = st.number_input( | |
| "Max Daily Hours", min_value=1, max_value=16, value=8) | |
| min_break = st.number_input( | |
| "Break Gap (minutes)", min_value=30, max_value=180, value=90) | |
| end_by = st.text_input("End By", value="22:00") | |
| max_iter = st.slider("Max Iterations", min_value=1, max_value=5, value=3) | |
| prefers_morning = st.checkbox("Prefer morning study sessions", value=True) | |
| generate_btn = st.button("β· Generate Schedule", type="primary", use_container_width=True) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| # --------------------------------------------------------------------------- | |
| # Run orchestrator | |
| # --------------------------------------------------------------------------- | |
| if generate_btn: | |
| if not st.session_state.tasks: | |
| st.warning("Please add at least one task first.") | |
| st.stop() | |
| preferences = { | |
| "prefers_morning": prefers_morning, | |
| "max_daily_hours": max_daily_hours, | |
| "min_break_gap_mins": min_break, | |
| "end_by": end_by, | |
| } | |
| # Build a 5-day calendar starting from next Monday | |
| today = date.today() | |
| days_to_monday = (7 - today.weekday()) % 7 or 7 | |
| week_start = today + timedelta(days=days_to_monday) | |
| calendar = {} | |
| for i in range(5): | |
| d = week_start + timedelta(days=i) | |
| calendar[str(d)] = { | |
| "slots": ["09:00-12:00", "14:00-17:00", "19:00-21:00"], | |
| "day_name": d.strftime("%A"), | |
| } | |
| with st.spinner("π€ Multi-agent negotiation in progressβ¦ (Planner β Critic β Memory β Reward)"): | |
| try: | |
| from orchestrator import run_lifeos | |
| results = run_lifeos( | |
| tasks=st.session_state.tasks, | |
| calendar=calendar, | |
| preferences=preferences, | |
| max_iterations=max_iter, | |
| verbose=False, | |
| ) | |
| st.session_state.results = results | |
| if results: | |
| best_result = max(results, key=lambda r: float(r.get("reward", float("-inf")))) | |
| st.session_state.best_summary = { | |
| "best_reward": float(best_result.get("reward", 0.0)), | |
| "best_plan": best_result.get("plan", {}), | |
| "best_iter": int(best_result.get("iteration", 0)), | |
| } | |
| except Exception as e: | |
| st.error(f"Error during generation: {e}") | |
| st.stop() | |
| # --------------------------------------------------------------------------- | |
| # Helper: per-component horizontal bar chart HTML | |
| # --------------------------------------------------------------------------- | |
| def _component_bars_html(reward_breakdown: dict) -> str: | |
| """Generate HTML for per-component mini bar chart.""" | |
| from agents.reward import COMPONENT_MAX, COMPONENT_NAMES | |
| lines = [] | |
| for comp in COMPONENT_NAMES: | |
| val = float(reward_breakdown.get(comp, 0.0)) | |
| max_val = COMPONENT_MAX.get(comp, 20) | |
| pct = max(0, min(100, (abs(val) / max_val * 100) if max_val > 0 else 0)) | |
| comp_short = comp.replace("_score", "") | |
| if comp_short == "load" and val < 0: | |
| color_cls = "comp-bar-red" | |
| elif val > 0: | |
| color_cls = "comp-bar-green" | |
| else: | |
| color_cls = "comp-bar-gray" | |
| short_name = comp_short[:8] | |
| lines.append( | |
| f'<div style="display:flex; align-items:center; margin-bottom:4px;">' | |
| f'<span style="width:65px; font-size:0.75rem; color:#64748b;">{short_name}</span>' | |
| f'<div style="flex-grow:1; background:rgba(128,128,128,0.2); height:10px; border-radius:3px; margin:0 8px;">' | |
| f'<div class="{color_cls}" style="width:{pct}%; height:100%; border-radius:3px;"></div>' | |
| f'</div>' | |
| f'<span style="width:30px; font-size:0.75rem; text-align:right;">{val:+.0f}</span>' | |
| f'</div>' | |
| ) | |
| return "".join(lines) | |
| def show_reward_breakdown(reward_breakdown, iteration_num): | |
| try: | |
| import matplotlib | |
| matplotlib.use("Agg") | |
| import matplotlib.pyplot as plt | |
| except ImportError: | |
| st.warning("Install matplotlib: `pip install matplotlib`") | |
| return | |
| components = { | |
| 'deadline': reward_breakdown.get('deadline_score', 0), | |
| 'break': reward_breakdown.get('break_score', 0), | |
| 'load': reward_breakdown.get('load_score', 0), | |
| 'energy': reward_breakdown.get('energy_score', 0), | |
| 'revision': reward_breakdown.get('revision_score', 0), | |
| 'format': reward_breakdown.get('format_score', 0), | |
| } | |
| labels = list(components.keys()) | |
| values = list(components.values()) | |
| colors = ['#E24B4A' if v < 0 else '#639922' for v in values] | |
| fig, ax = plt.subplots(figsize=(6, 4)) | |
| bars = ax.barh(labels, values, color=colors) | |
| # Add value labels | |
| for bar, val in zip(bars, values): | |
| ax.text(val + (1 if val >= 0 else -1), | |
| bar.get_y() + bar.get_height()/2, | |
| str(int(val)), va='center', | |
| ha='left' if val >= 0 else 'right', | |
| fontweight='bold') | |
| ax.axvline(x=0, color='black', linewidth=0.8) | |
| ax.set_title(f'Reward Components β Iteration {iteration_num}') | |
| ax.grid(True, axis='x', alpha=0.3) | |
| plt.tight_layout() | |
| st.pyplot(fig) | |
| plt.close() | |
| def _parse_slot_time_range(time_str: str): | |
| """Parse HH:MM-HH:MM from slot time and return (start_h, start_m, end_h, end_m).""" | |
| if not isinstance(time_str, str): | |
| return None | |
| match = re.search(r"(\d{1,2}):(\d{2})\s*-\s*(\d{1,2}):(\d{2})", time_str) | |
| if not match: | |
| return None | |
| sh, sm, eh, em = map(int, match.groups()) | |
| return sh, sm, eh, em | |
| def _slot_warning_style(slot: dict): | |
| """Return warning style for a schedule slot.""" | |
| slot_type = slot.get("type", "") | |
| if slot_type != "study": | |
| return "", "" | |
| parsed = _parse_slot_time_range(slot.get("time", "")) | |
| if not parsed: | |
| return "", "" | |
| sh, sm, eh, em = parsed | |
| start_mins = sh * 60 + sm | |
| end_mins = eh * 60 + em | |
| if end_mins < start_mins: | |
| end_mins += 24 * 60 | |
| duration_hours = (end_mins - start_mins) / 60 | |
| ends_after_21 = (eh > 21) or (eh == 21 and em > 0) | |
| long_block = duration_hours > 3.0 | |
| if ends_after_21: | |
| return "color:#b91c1c; background:#fee2e2; padding:4px 8px; border-radius:6px;", " β οΈ Late-night slot" | |
| if long_block: | |
| return "color:#c2410c; background:#ffedd5; padding:4px 8px; border-radius:6px;", " β οΈ Long block (>3h)" | |
| return "", "" | |
| # --------------------------------------------------------------------------- | |
| # Dialogs for detailed views | |
| # --------------------------------------------------------------------------- | |
| def show_iteration_history(results): | |
| from orchestrator import format_plan_for_display | |
| prev_issue_count = None | |
| for r in results: | |
| rew = r["reward"] | |
| score_cls = "score-good" if rew >= 80 else ("score-mid" if rew >= 40 else "score-bad") | |
| icon = "β " if rew >= 80 else ("β οΈ" if rew >= 50 else "β") | |
| with st.expander( | |
| f"Iteration {r['iteration']} | Reward: {rew:.0f} {icon}", | |
| expanded=(r["iteration"] == len(results)), | |
| ): | |
| st.markdown( | |
| f"<span class='{score_cls}'>{rew:.0f}</span>", | |
| unsafe_allow_html=True, | |
| ) | |
| drift_event = r.get("drift_event") | |
| if drift_event: | |
| st.markdown( | |
| f"<div style='font-size:0.9rem; color:#0f766e; margin:4px 0;'>" | |
| f"Schema Drift Active: <strong>{drift_event.get('id', '')}</strong> β " | |
| f"{drift_event.get('description', '')}</div>", | |
| unsafe_allow_html=True, | |
| ) | |
| breakdown = r.get("reward_breakdown", {}) | |
| if breakdown: | |
| st.markdown("**Component Scores:**") | |
| st.markdown(_component_bars_html(breakdown), unsafe_allow_html=True) | |
| if r.get("process_bonus", 0) > 0: | |
| st.markdown(f"<div class='pass-line'>π Process bonus: +{r['process_bonus']:.0f}</div>", unsafe_allow_html=True) | |
| current_issue_count = len(r.get("issues", [])) | |
| if prev_issue_count is None: | |
| st.markdown( | |
| f"<div style='color:#64748b; font-size:0.9rem; margin:3px 0;'>Issues: {current_issue_count} (starting point)</div>", | |
| unsafe_allow_html=True, | |
| ) | |
| else: | |
| trend_color = "#16a34a" if current_issue_count <= prev_issue_count else "#dc2626" | |
| st.markdown( | |
| f"<div style='color:{trend_color}; font-size:0.9rem; margin:3px 0;'>" | |
| f"Issues: {prev_issue_count} β {current_issue_count}</div>", | |
| unsafe_allow_html=True, | |
| ) | |
| if r["issues"]: | |
| st.markdown("**Critic Issues:**") | |
| for iss in r["issues"]: | |
| st.markdown(f"<div class='issue-line'>β {iss}</div>", unsafe_allow_html=True) | |
| else: | |
| st.markdown("<div class='pass-line'>β All checks passed!</div>", unsafe_allow_html=True) | |
| st.code(format_plan_for_display(r["plan"]), language=None) | |
| prev_issue_count = current_issue_count | |
| def show_reward_details(results, best): | |
| try: | |
| import matplotlib | |
| matplotlib.use("Agg") | |
| import matplotlib.pyplot as plt | |
| except ImportError: | |
| st.warning("Install matplotlib: `pip install matplotlib`") | |
| return | |
| st.subheader("Reward Curve") | |
| iters = [r["iteration"] for r in results] | |
| rewards = [r["reward"] for r in results] | |
| fig, ax = plt.subplots(figsize=(6, 3.8)) | |
| best_so_far = [max(rewards[:i + 1]) for i in range(len(rewards))] | |
| ax.plot(iters, rewards, color="royalblue", linewidth=2.5, | |
| linestyle="-", marker="o", markersize=8, label="Iteration reward") | |
| ax.plot(iters, best_so_far, color="forestgreen", linewidth=2.2, | |
| linestyle="--", marker=None, label="Best so far") | |
| for x, y in zip(iters, rewards): | |
| ax.annotate(f"{y:.0f}", xy=(x, y), xytext=(0, 9), | |
| textcoords="offset points", ha="center", | |
| fontsize=9, fontweight="bold", color="royalblue") | |
| ax.axhline(50, color="darkorange", linestyle="--", linewidth=1.3, | |
| alpha=0.85, label="Acceptable (50)") | |
| ax.axhline(80, color="red", linestyle="--", linewidth=1.3, | |
| alpha=0.85, label="Optimal (80)") | |
| ax.set_ylim(bottom=0, top=max(max(rewards)+25, 105)) | |
| ax.set_xticks(iters) | |
| ax.xaxis.set_major_locator(plt.MaxNLocator(integer=True)) | |
| ax.set_xlabel("Iteration", fontsize=10) | |
| ax.set_ylabel("Reward", fontsize=10) | |
| ax.set_title("Reward Improvement", fontsize=11, fontweight="bold") | |
| ax.legend(fontsize=8, loc="best") | |
| ax.grid(axis="y", alpha=0.3, linestyle=":") | |
| ax.spines["top"].set_visible(False) | |
| ax.spines["right"].set_visible(False) | |
| plt.tight_layout() | |
| st.pyplot(fig) | |
| plt.close(fig) | |
| st.divider() | |
| st.subheader("Reward Breakdown") | |
| rb = best.get('reward_breakdown', {}) | |
| iteration_num = best.get('iteration', 1) | |
| components = { | |
| 'deadline': rb.get('deadline_score', 0), | |
| 'break': rb.get('break_score', 0), | |
| 'load': rb.get('load_score', 0), | |
| 'energy': rb.get('energy_score', 0), | |
| 'revision': rb.get('revision_score', 0), | |
| 'format': rb.get('format_score', 0), | |
| } | |
| labels = list(components.keys()) | |
| values = list(components.values()) | |
| colors = ['#E24B4A' if v < 0 else '#2E8B57' for v in values] | |
| fig2, ax2 = plt.subplots(figsize=(6, 4)) | |
| bars = ax2.barh(labels, values, color=colors, height=0.5) | |
| for bar, val in zip(bars, values): | |
| ax2.text( | |
| val + (0.5 if val >= 0 else -0.5), | |
| bar.get_y() + bar.get_height()/2, | |
| str(int(val)), va='center', | |
| ha='left' if val >= 0 else 'right', | |
| fontweight='bold', fontsize=11 | |
| ) | |
| ax2.axvline(x=0, color='black', linewidth=0.8) | |
| ax2.set_title(f'Reward Components β Best Iteration {iteration_num}') | |
| ax2.set_facecolor('#1a1a2e') | |
| fig2.patch.set_facecolor('#1a1a2e') | |
| ax2.tick_params(colors='white') | |
| ax2.title.set_color('white') | |
| ax2.xaxis.label.set_color('white') | |
| for spine in ax2.spines.values(): | |
| spine.set_edgecolor('white') | |
| ax2.grid(True, axis='x', alpha=0.3, color='white') | |
| plt.tight_layout() | |
| st.pyplot(fig2) | |
| plt.close(fig2) | |
| # --------------------------------------------------------------------------- | |
| # Tabbed layout (right panel) | |
| # --------------------------------------------------------------------------- | |
| with right_col: | |
| tab_schedule, tab_comparison, tab_training = st.tabs(["Iterations", "Reward Curve", "Best Schedule"]) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # TAB 1: Iterations | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with tab_schedule: | |
| results = st.session_state.results | |
| if not results: | |
| st.info( | |
| "π **Configure your tasks** in the sidebar, then click **Generate Schedule** " | |
| "to start the multi-agent loop. Sample tasks are pre-loaded β just click Generate!" | |
| ) | |
| else: | |
| best_summary = st.session_state.get("best_summary", {}) | |
| if not best_summary.get("best_plan") and results: | |
| best_iter_dict = max(results, key=lambda r: r["reward"]) | |
| best_summary = { | |
| "best_reward": float(best_iter_dict.get("reward", 0.0)), | |
| "best_plan": best_iter_dict.get("plan", {}), | |
| "best_iter": int(best_iter_dict.get("iteration", 0)), | |
| } | |
| st.session_state.best_summary = best_summary | |
| best_reward = float(best_summary.get("best_reward", 0.0)) | |
| best_plan = best_summary.get("best_plan", {}) | |
| best_iter = int(best_summary.get("best_iter", 0)) | |
| best_iter_dict = max(results, key=lambda r: r["reward"]) | |
| # ββ Top Section: Best Schedule & Buttons ββββββββββββββββββββ | |
| col_title, col_btn1, col_btn2 = st.columns([1.6, 1.2, 1.2]) | |
| with col_title: | |
| st.markdown( | |
| f"<h3 class='best-header'>π Best: Iter {best_iter} | Reward: {best_reward:.0f}</h3>", | |
| unsafe_allow_html=True, | |
| ) | |
| with col_btn1: | |
| st.write("") # Vertical alignment spacing | |
| if st.button("π View Iteration History", use_container_width=True): | |
| show_iteration_history(results) | |
| with col_btn2: | |
| st.write("") # Vertical alignment spacing | |
| if st.button("π View Reward Details", use_container_width=True): | |
| show_reward_details(results, best_iter_dict) | |
| st.divider() | |
| schedule = best_plan.get("schedule", {}) | |
| notes = best_plan.get("notes", "") | |
| badge = { | |
| "study": "<span class='study-badge'>STUDY</span>", | |
| "break": "<span class='break-badge'>BREAK</span>", | |
| "review": "<span class='review-badge'>REVIEW</span>", | |
| } | |
| days = sorted(schedule.keys()) | |
| if days: | |
| for date_str in days: | |
| st.markdown(f"#### π {date_str}") | |
| slots = schedule[date_str] | |
| for slot in (slots if isinstance(slots, list) else []): | |
| b = badge.get(slot.get("type", "study"), "") | |
| warn_style, warn_text = _slot_warning_style(slot) | |
| st.markdown( | |
| f"<div style='margin-bottom: 8px; font-size: 1.05rem; {warn_style}'>" | |
| f"{b} <span style='font-family: monospace; color: #64748b; margin: 0 12px;'>{slot.get('time', '?')}</span> " | |
| f"<strong>{slot.get('task', '?')}</strong>{warn_text}</div>", | |
| unsafe_allow_html=True, | |
| ) | |
| st.markdown("<br>", unsafe_allow_html=True) | |
| if notes: | |
| st.info(f"π‘ {notes}") | |
| st.divider() | |
| # Summary footer banner | |
| first_reward = float(results[0].get("reward", 0.0)) | |
| delta = best_reward - first_reward | |
| pct = (delta / first_reward * 100) if first_reward > 0 else 0.0 | |
| issues_start = len(results[0].get("issues", [])) | |
| issues_end = len(best_iter_dict.get("issues", [])) | |
| if delta > 0: | |
| st.success(f"Started: {first_reward:.0f} β Best: {best_reward:.0f} β Improvement: +{pct:.0f}%") | |
| else: | |
| st.info(f"Started: {first_reward:.0f} β Best: {best_reward:.0f} β Issues resolved: {issues_start} β {issues_end}") | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # TAB 2: Reward Curve | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with tab_comparison: | |
| st.subheader("Reward Progression") | |
| st.caption("Multi-agent learning curve") | |
| # Direct chart display - no dialog | |
| import matplotlib.pyplot as plt | |
| import matplotlib | |
| matplotlib.use('Agg') | |
| if st.session_state.get('results'): | |
| rewards = [r.get('reward', 0) for r in st.session_state.results] | |
| best_so_far = [max(rewards[:i+1]) for i in range(len(rewards))] | |
| fig, ax = plt.subplots(figsize=(8, 4)) | |
| ax.plot(range(1, len(rewards)+1), rewards, | |
| 'b-o', label='Iteration reward', linewidth=2) | |
| ax.plot(range(1, len(best_so_far)+1), best_so_far, | |
| 'g--', label='Best so far', linewidth=2) | |
| ax.axhline(y=50, color='orange', linestyle='--', | |
| label='Acceptable (50)', alpha=0.7) | |
| ax.axhline(y=80, color='red', linestyle='--', | |
| label='Optimal (80)', alpha=0.7) | |
| ax.xaxis.set_major_locator(plt.MaxNLocator(integer=True)) | |
| ax.set_xlabel('Iteration') | |
| ax.set_ylabel('Reward') | |
| ax.set_title('Reward Improvement') | |
| ax.legend() | |
| ax.grid(True, alpha=0.3) | |
| for i, (r_val, b_val) in enumerate(zip(rewards, best_so_far)): | |
| ax.annotate(str(int(r_val)), (i+1, r_val), textcoords="offset points", | |
| xytext=(0,10), ha='center', fontsize=10) | |
| st.pyplot(fig) | |
| plt.close() | |
| else: | |
| st.info("Run a scenario first to see the reward curve") | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # TAB 3: Best Schedule | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with tab_training: | |
| results = st.session_state.results | |
| if not results: | |
| st.info("Generate a schedule to view the best schedule.") | |
| else: | |
| best = max(results, key=lambda r: r.get("reward", 0.0)) | |
| st.markdown("### Best Schedule") | |
| st.caption(f"Iteration {best.get('iteration', 0)} - Highest scoring schedule") | |
| st.markdown(f"<div class='score-chip'>{float(best.get('reward', 0.0)):.1f}</div>", unsafe_allow_html=True) | |
| plan = best.get("plan", {}) | |
| schedule = plan.get("schedule", {}) | |
| for date_str in sorted(schedule.keys()): | |
| st.markdown(f"<div class='day-card'><h4 style='margin:0 0 10px 0;'>{date_str}</h4>", unsafe_allow_html=True) | |
| for slot in schedule.get(date_str, []): | |
| badge = "study-badge" if slot.get("type") == "study" else ("break-badge" if slot.get("type") == "break" else "review-badge") | |
| st.markdown( | |
| f"<div class='slot-row'><span class='slot-time'>{slot.get('time', '?')}</span>" | |
| f"<span><span class='{badge}'>{slot.get('type', '').upper()}</span>: {slot.get('task', '?')}</span></div>", | |
| unsafe_allow_html=True, | |
| ) | |
| st.markdown("</div>", unsafe_allow_html=True) | |