From 6a9a190af520ffe274b1c6daeff09d2ef4b49b3d Mon Sep 17 00:00:00 2001 From: Youssef Aitousarrah Date: Fri, 20 Feb 2026 11:33:15 -0800 Subject: [PATCH] fix(ui): fix signal cards rendering as raw HTML in Streamlit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs were causing the signal card HTML to display as literal text: 1. CommonMark HTML block termination: Streamlit's markdown parser (CommonMark-compliant) terminates a
block at the first blank line. Empty optional fields (name_html, desc_html, risk_badge_html) left whitespace-only lines in the f-string template, ending the HTML block and causing the parser to render subsequent tags as text. Fixed by building HTML from a parts list and only appending optional elements when non-empty — no blank lines can appear in output. 2. Unescaped HTML chars in LLM-generated text: reason fields from the ranker contained raw > and & characters (e.g. '>5% move', '50 & 200 SMA') that corrupted the HTML structure. Fixed by running all LLM-generated fields through html.escape() before interpolation. Co-Authored-By: Claude Sonnet 4.6 --- tradingagents/ui/theme.py | 92 ++++++++++++++++++++++----------------- 1 file changed, 52 insertions(+), 40 deletions(-) diff --git a/tradingagents/ui/theme.py b/tradingagents/ui/theme.py index 5b19674a..414a4eb5 100644 --- a/tradingagents/ui/theme.py +++ b/tradingagents/ui/theme.py @@ -629,46 +629,58 @@ def signal_card( safe_risk_title = safe_risk.title() if safe_risk else "—" - return f""" -
-
-
- {ticker} - #{rank} - {name_html} -
-
- {desc_html} -
- {safe_strategy} - Score {score} - Conf {confidence}/10 - {risk_badge_html} -
-
-
-
Entry
-
{entry_str}
-
-
-
Score
-
{score}/100
-
-
-
Confidence
-
{confidence}/10
-
-
-
Risk
-
{safe_risk_title}
-
-
-
{safe_reason}
-
-
-
-
- """ + # Build HTML piece-by-piece to avoid blank/whitespace-only lines. + # CommonMark terminates a
HTML block at the FIRST blank line, so any + # empty interpolation (name_html, desc_html, risk_badge_html = "") would leave + # a whitespace-only line that makes Streamlit's markdown parser stop treating + # subsequent content as HTML and render it as literal text instead. + parts = [ + f'
', + '
', + '
', + f'{ticker}', + f'#{rank}', + ] + if name_html: + parts.append(name_html) + parts += ["
", "
"] + if desc_html: + parts.append(desc_html) + parts += [ + '
', + f'{safe_strategy}', + f'Score {score}', + f'Conf {confidence}/10', + ] + if risk_badge_html: + parts.append(risk_badge_html) + parts += [ + "
", + '
', + '
', + '
Entry
', + f'
{entry_str}
', + "
", + '
', + '
Score
', + f'
{score}/100
', + "
", + '
', + '
Confidence
', + f'
{confidence}/10
', + "
", + '
', + '
Risk
', + f'
{safe_risk_title}
', + "
", + "
", + f'
{safe_reason}
', + '
', + f'
', + "
", + "
", + ] + return "\n".join(parts) def pnl_color(value: float) -> str: