feat(dashboard): NiceGUI Genomes page con top-K + ispezione singolo genoma

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) <noreply@anthropic.com>
This commit is contained in:
2026-05-11 22:35:52 +02:00
parent 56e22584d9
commit 2f38562e23
+178 -1
View File
@@ -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(