fix(dashboard): JSON config renderizzato come HTML colorato custom (no Prism/hljs)
Soluzione robusta al bug "chiavi JSON invisibili": invece di tentare
override CSS su Prism.js/highlight.js (che nelle build NiceGUI possono
non essere attivi o variare), il JSON viene serializzato server-side con
span colorati espliciti e renderizzato via ui.html.
Implementato helper _json_to_html(obj) che produce HTML con classi:
- .cb-key (chiavi) → cyan #00D9FF
- .cb-string → green #00E676
- .cb-number → pink #FF2D87
- .cb-bool / .cb-null → amber #FFB800
- .cb-punct (graffe, virgole, due punti) → muted
Stessa logica per system_prompt e raw LLM output in /genomes: sostituito
ui.code(language=text) con ui.html('<pre class="raw-block">') e
html.escape() per safety XSS. Background dedicato #1A1A24 e font
JetBrains Mono 13.5px, line-height 1.7, ottimo contrasto su qualsiasi
browser senza dipendere da syntax highlighter esterno.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
}}
|
||||
</style>
|
||||
"""
|
||||
|
||||
|
||||
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 '<span class="cb-punct">{}</span>'
|
||||
items = []
|
||||
for k, v in obj.items():
|
||||
key = f'<span class="cb-key">"{html.escape(str(k))}"</span>'
|
||||
val = _json_to_html(v, indent + 1)
|
||||
items.append(f"{inner_pad}{key}<span class=\"cb-punct\">:</span> {val}")
|
||||
return ('<span class="cb-punct">{</span>\n'
|
||||
+ '<span class="cb-punct">,</span>\n'.join(items)
|
||||
+ f'\n{pad}<span class="cb-punct">}}</span>')
|
||||
if isinstance(obj, list):
|
||||
if not obj:
|
||||
return '<span class="cb-punct">[]</span>'
|
||||
items = [_json_to_html(x, indent + 1) for x in obj]
|
||||
return ('<span class="cb-punct">[</span>\n'
|
||||
+ '<span class="cb-punct">,</span>\n'.join(inner_pad + i for i in items)
|
||||
+ f'\n{pad}<span class="cb-punct">]</span>')
|
||||
if isinstance(obj, bool):
|
||||
return f'<span class="cb-bool">{str(obj).lower()}</span>'
|
||||
if obj is None:
|
||||
return '<span class="cb-null">null</span>'
|
||||
if isinstance(obj, (int, float)):
|
||||
return f'<span class="cb-number">{obj}</span>'
|
||||
return f'<span class="cb-string">"{html.escape(str(obj))}"</span>'
|
||||
|
||||
|
||||
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('<pre class="config-block"></pre>').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'<pre class="config-block">{_json_to_html(s["config"])}</pre>'
|
||||
|
||||
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('<pre class="raw-block">—</pre>').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('<pre class="raw-block">—</pre>').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'<pre class="raw-block">{html.escape(str(row.get("system_prompt", "—")))}</pre>'
|
||||
)
|
||||
raw_code.content = (
|
||||
f'<pre class="raw-block">{html.escape(str(row.get("raw_text", "—") or "—"))}</pre>'
|
||||
)
|
||||
pe = row.get("parse_error")
|
||||
parse_error_lbl.text = f"❌ Parse error: {pe}" if pe else ""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user