From 2f38562e23a81a95872507ed3d9f69f1ab4b122a Mon Sep 17 00:00:00 2001 From: AdrianoDev Date: Mon, 11 May 2026 22:35:52 +0200 Subject: [PATCH] feat(dashboard): NiceGUI Genomes page con top-K + ispezione singolo genoma MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aggiunta route /genomes: - Run selector + Top K (10/25/50) - Tabella sortable con fitness, DSR, Sharpe, max DD, trades, style, T, lookback - Selezione riga → pannello ispezione con 6 metric card e blocchi code per system_prompt + raw_text + parse_error eventuale - Auto-refresh ogni 3s (mantiene selezione corrente se ancora valida) - Header nav aggiornato con link Genomes Co-Authored-By: Claude Opus 4.7 (1M context) --- src/multi_swarm/dashboard/nicegui_app.py | 179 ++++++++++++++++++++++- 1 file changed, 178 insertions(+), 1 deletion(-) diff --git a/src/multi_swarm/dashboard/nicegui_app.py b/src/multi_swarm/dashboard/nicegui_app.py index 7280528..db4c565 100644 --- a/src/multi_swarm/dashboard/nicegui_app.py +++ b/src/multi_swarm/dashboard/nicegui_app.py @@ -25,6 +25,7 @@ from nicegui import app, ui from multi_swarm.dashboard.data import ( evaluations_df, generations_df, + genomes_df, get_repo, get_run_overview, list_runs_df, @@ -110,7 +111,11 @@ def _build_header(active: str) -> None: ui.label("⚫🔵 Multi-Swarm Coevolutivo").classes("text-h6").style( f"color: {COLOR_TEXT}; font-weight: 700;" ) - for path, label in (("/", "Overview"), ("/convergence", "GA Convergence")): + for path, label in ( + ("/", "Overview"), + ("/convergence", "GA Convergence"), + ("/genomes", "Genomes"), + ): cls = "nav-link active" if active == path else "nav-link" ui.link(label, path).classes(cls) ui.label(f"DB: {Path(DB_PATH).resolve().name}").classes("text-caption").style( @@ -416,6 +421,178 @@ def convergence() -> None: refresh() +@ui.page("/genomes") +def genomes() -> None: + _apply_theme() + _build_header(active="/genomes") + + options = _runs_options() + if not options: + ui.label("Nessuna run nel database.").classes("text-h5") + return + + state: dict[str, Any] = { + "run_id": next(iter(options)), + "selected_gid": None, + "merged": None, + } + + with ui.row().classes("w-full items-center gap-4 q-mb-md"): + select = ui.select(options=options, value=state["run_id"], label="Run").classes( + "flex-grow" + ) + top_k_select = ui.select( + options={10: "Top 10", 25: "Top 25", 50: "Top 50"}, + value=10, + label="Top K", + ) + ui.button("🔄 Refresh", on_click=lambda: refresh()).props("outline color=primary") + + ui.label("Top genomi per fitness").classes("text-subtitle1 q-mt-sm") + top_table = ui.table( + columns=[ + {"name": "genome_id", "label": "id", "field": "genome_id", "align": "left"}, + {"name": "fitness", "label": "fitness", "field": "fitness", "sortable": True}, + {"name": "dsr", "label": "DSR", "field": "dsr"}, + {"name": "sharpe", "label": "Sharpe", "field": "sharpe"}, + {"name": "max_dd", "label": "max DD", "field": "max_dd"}, + {"name": "n_trades", "label": "trades", "field": "n_trades"}, + {"name": "cognitive_style", "label": "style", "field": "cognitive_style"}, + {"name": "temperature", "label": "T", "field": "temperature"}, + {"name": "lookback_window", "label": "lookback", "field": "lookback_window"}, + ], + rows=[], + row_key="genome_id", + selection="single", + ).classes("w-full") + + ui.separator().classes("q-my-md") + + with ui.card().classes("w-full"): + ui.label("Ispezione genoma").classes("text-subtitle1") + detail_hint = ui.label("Seleziona un genoma dalla tabella sopra.").classes( + "text-caption" + ).style(f"color: {COLOR_TEXT_MUTED};") + + with ui.row().classes("w-full gap-4 q-mt-sm"): + with ui.card().classes("flex-grow metric-card"): + ui.label("fitness").classes("text-caption") + fit_lbl = ui.label("—").classes("text-h4") + with ui.card().classes("flex-grow metric-card"): + ui.label("DSR").classes("text-caption") + dsr_lbl = ui.label("—").classes("text-h4") + with ui.card().classes("flex-grow metric-card"): + ui.label("Sharpe").classes("text-caption") + sharpe_lbl = ui.label("—").classes("text-h4") + with ui.card().classes("flex-grow metric-card"): + ui.label("max DD").classes("text-caption") + dd_lbl = ui.label("—").classes("text-h4") + with ui.card().classes("flex-grow metric-card"): + ui.label("trades").classes("text-caption") + trades_lbl = ui.label("—").classes("text-h4") + with ui.card().classes("flex-grow metric-card"): + ui.label("style").classes("text-caption") + style_lbl = ui.label("—").classes("text-h4") + + ui.label("System prompt").classes("text-subtitle1 q-mt-md") + prompt_code = ui.code("—", language="text").classes("w-full") + + ui.label("Raw LLM output").classes("text-subtitle1 q-mt-md") + raw_code = ui.code("—", language="text").classes("w-full") + + parse_error_lbl = ui.label("").classes("q-mt-sm").style( + "color: #FF6B6B; font-weight: 600;" + ) + + def _render_detail(row: dict[str, Any]) -> None: + detail_hint.text = f"Genoma: {row.get('genome_id', '—')}" + fit_lbl.text = f"{float(row.get('fitness', 0)):.4f}" + dsr_lbl.text = f"{float(row.get('dsr', 0)):.4f}" + sharpe_lbl.text = f"{float(row.get('sharpe', 0)):.3f}" + dd_lbl.text = f"{float(row.get('max_dd', 0)):.3f}" + trades_lbl.text = str(int(row.get("n_trades", 0))) + style_lbl.text = str(row.get("cognitive_style", "—")) + prompt_code.content = str(row.get("system_prompt", "—")) + raw_code.content = str(row.get("raw_text", "—") or "—") + pe = row.get("parse_error") + parse_error_lbl.text = f"❌ Parse error: {pe}" if pe else "" + + def refresh() -> None: + run_id = select.value + if not run_id: + return + try: + repo = get_repo(DB_PATH) + evals = evaluations_df(repo, run_id) + gens = genomes_df(repo, run_id) + except Exception as e: # noqa: BLE001 + ui.notify(f"Errore: {e}", type="negative") + return + + if evals.empty: + top_table.rows = [] + top_table.update() + return + + merged = evals.merge( + gens, left_on="genome_id", right_on="id", how="left", suffixes=("", "_g") + ) + state["merged"] = merged + + k = int(top_k_select.value) + top = merged.sort_values("fitness", ascending=False).head(k) + + rows = [] + for _, r in top.iterrows(): + rows.append( + { + "genome_id": str(r.get("genome_id", "—"))[:12] + "…", + "fitness": round(float(r.get("fitness", 0)), 4), + "dsr": round(float(r.get("dsr", 0)), 4), + "sharpe": round(float(r.get("sharpe", 0)), 3), + "max_dd": round(float(r.get("max_dd", 0)), 3), + "n_trades": int(r.get("n_trades", 0)), + "cognitive_style": str(r.get("cognitive_style", "—")), + "temperature": round(float(r.get("temperature", 0)), 2), + "lookback_window": int(r.get("lookback_window", 0)), + "_full_id": str(r.get("genome_id", "")), + } + ) + top_table.rows = rows + top_table.update() + + sel = state.get("selected_gid") + if sel: + match = merged[merged["genome_id"] == sel] + if not match.empty: + _render_detail(match.iloc[0].to_dict()) + + def on_row_selected(e: Any) -> None: + if not e.selection: + return + full_id = e.selection[0].get("_full_id") + if not full_id: + return + state["selected_gid"] = full_id + merged = state.get("merged") + if merged is None: + return + match = merged[merged["genome_id"] == full_id] + if not match.empty: + _render_detail(match.iloc[0].to_dict()) + + def on_select_change() -> None: + state["run_id"] = select.value + state["selected_gid"] = None + refresh() + + select.on_value_change(on_select_change) + top_k_select.on_value_change(lambda _: refresh()) + top_table.on("selection", on_row_selected) + ui.timer(REFRESH_INTERVAL_S, refresh) + refresh() + + def main() -> None: app.on_startup(lambda: print(f"DB: {Path(DB_PATH).resolve()}")) ui.run(