diff --git a/src/multi_swarm/dashboard/nicegui_app.py b/src/multi_swarm/dashboard/nicegui_app.py index 918f178..701b152 100644 --- a/src/multi_swarm/dashboard/nicegui_app.py +++ b/src/multi_swarm/dashboard/nicegui_app.py @@ -18,6 +18,7 @@ Palette "Neon Trading Dashboard" (ispirata screenshot 2026-05-11): from __future__ import annotations +import html import json import os from pathlib import Path @@ -269,10 +270,78 @@ pre[class*="language-"], code[class*="language-"] {{ }} .q-badge {{ border-radius: 6px !important; font-weight: 500 !important; padding: 4px 10px !important; font-size: 12px !important; }} + +.config-block {{ + background: #1A1A24; + color: {COLOR_TEXT}; + border: 1px solid {COLOR_BORDER}; + border-radius: 10px; + padding: 18px 20px; + font-family: 'JetBrains Mono', 'Fira Code', monospace; + font-size: 13.5px; + line-height: 1.7; + overflow-x: auto; + white-space: pre; + margin: 0; +}} +.config-block .cb-key {{ color: {COLOR_SECONDARY}; font-weight: 500; }} +.config-block .cb-string {{ color: {COLOR_SUCCESS}; }} +.config-block .cb-number {{ color: {COLOR_PRIMARY}; font-weight: 500; }} +.config-block .cb-bool {{ color: {COLOR_ACCENT}; }} +.config-block .cb-null {{ color: {COLOR_ACCENT}; font-style: italic; }} +.config-block .cb-punct {{ color: {COLOR_TEXT_MUTED}; }} + +.raw-block {{ + background: #1A1A24; + color: {COLOR_TEXT}; + border: 1px solid {COLOR_BORDER}; + border-radius: 10px; + padding: 18px 20px; + font-family: 'JetBrains Mono', 'Fira Code', monospace; + font-size: 13px; + line-height: 1.6; + overflow-x: auto; + white-space: pre-wrap; + word-break: break-word; + max-height: 400px; + overflow-y: auto; + margin: 0; +}} """ +def _json_to_html(obj: Any, indent: int = 0) -> str: + """Render JSON con span colorati espliciti. Garantisce leggibilità ovunque.""" + pad = " " * indent + inner_pad = " " * (indent + 1) + if isinstance(obj, dict): + if not obj: + return '{}' + items = [] + for k, v in obj.items(): + key = f'"{html.escape(str(k))}"' + val = _json_to_html(v, indent + 1) + items.append(f"{inner_pad}{key}: {val}") + return ('{\n' + + ',\n'.join(items) + + f'\n{pad}}}') + if isinstance(obj, list): + if not obj: + return '[]' + items = [_json_to_html(x, indent + 1) for x in obj] + return ('[\n' + + ',\n'.join(inner_pad + i for i in items) + + f'\n{pad}]') + if isinstance(obj, bool): + return f'{str(obj).lower()}' + if obj is None: + return 'null' + if isinstance(obj, (int, float)): + return f'{obj}' + return f'"{html.escape(str(obj))}"' + + def _apply_theme() -> None: ui.add_head_html(_CUSTOM_CSS) ui.dark_mode().enable() @@ -486,7 +555,7 @@ def index() -> None: completed_lbl = ui.label("Completed: —") ui.separator() ui.label("Config").classes("text-subtitle1") - cfg_code = ui.code("", language="json").classes("w-full") + cfg_code = ui.html('
').classes("w-full") def refresh() -> None: run_id = select.value @@ -518,7 +587,7 @@ def index() -> None: started_lbl.text = f"Started: {s['started_at']}" completed_lbl.text = f"Completed: {s['completed_at']}" - cfg_code.content = json.dumps(s["config"], indent=2) + cfg_code.content = f'{_json_to_html(s["config"])}'
def on_select_change() -> None:
state["run_id"] = select.value
@@ -688,10 +757,10 @@ def genomes() -> None:
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")
+ prompt_code = ui.html('—').classes("w-full") ui.label("Raw LLM output").classes("text-subtitle1 q-mt-md") - raw_code = ui.code("—", language="text").classes("w-full") + raw_code = ui.html('
—').classes("w-full") parse_error_lbl = ui.label("").classes("q-mt-sm").style( "color: #FF6B6B; font-weight: 600;" @@ -705,8 +774,12 @@ def genomes() -> None: 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 "—") + prompt_code.content = ( + f'
{html.escape(str(row.get("system_prompt", "—")))}'
+ )
+ raw_code.content = (
+ f'{html.escape(str(row.get("raw_text", "—") or "—"))}'
+ )
pe = row.get("parse_error")
parse_error_lbl.text = f"❌ Parse error: {pe}" if pe else ""