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
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import html
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
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; }}
|
.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>
|
</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:
|
def _apply_theme() -> None:
|
||||||
ui.add_head_html(_CUSTOM_CSS)
|
ui.add_head_html(_CUSTOM_CSS)
|
||||||
ui.dark_mode().enable()
|
ui.dark_mode().enable()
|
||||||
@@ -486,7 +555,7 @@ def index() -> None:
|
|||||||
completed_lbl = ui.label("Completed: —")
|
completed_lbl = ui.label("Completed: —")
|
||||||
ui.separator()
|
ui.separator()
|
||||||
ui.label("Config").classes("text-subtitle1")
|
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:
|
def refresh() -> None:
|
||||||
run_id = select.value
|
run_id = select.value
|
||||||
@@ -518,7 +587,7 @@ def index() -> None:
|
|||||||
|
|
||||||
started_lbl.text = f"Started: {s['started_at']}"
|
started_lbl.text = f"Started: {s['started_at']}"
|
||||||
completed_lbl.text = f"Completed: {s['completed_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:
|
def on_select_change() -> None:
|
||||||
state["run_id"] = select.value
|
state["run_id"] = select.value
|
||||||
@@ -688,10 +757,10 @@ def genomes() -> None:
|
|||||||
style_lbl = ui.label("—").classes("text-h4")
|
style_lbl = ui.label("—").classes("text-h4")
|
||||||
|
|
||||||
ui.label("System prompt").classes("text-subtitle1 q-mt-md")
|
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")
|
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(
|
parse_error_lbl = ui.label("").classes("q-mt-sm").style(
|
||||||
"color: #FF6B6B; font-weight: 600;"
|
"color: #FF6B6B; font-weight: 600;"
|
||||||
@@ -705,8 +774,12 @@ def genomes() -> None:
|
|||||||
dd_lbl.text = f"{float(row.get('max_dd', 0)):.3f}"
|
dd_lbl.text = f"{float(row.get('max_dd', 0)):.3f}"
|
||||||
trades_lbl.text = str(int(row.get("n_trades", 0)))
|
trades_lbl.text = str(int(row.get("n_trades", 0)))
|
||||||
style_lbl.text = str(row.get("cognitive_style", "—"))
|
style_lbl.text = str(row.get("cognitive_style", "—"))
|
||||||
prompt_code.content = str(row.get("system_prompt", "—"))
|
prompt_code.content = (
|
||||||
raw_code.content = str(row.get("raw_text", "—") or "—")
|
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")
|
pe = row.get("parse_error")
|
||||||
parse_error_lbl.text = f"❌ Parse error: {pe}" if pe else ""
|
parse_error_lbl.text = f"❌ Parse error: {pe}" if pe else ""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user