"""HTML/CSS rendering for the FinAgent Gradio frontend. Provides pure rendering functions that generate HTML strings for: - Custom dark terminal theme CSS - Trading signal cards (BUY/SELL/HOLD) - Error cards for failed ticker analysis - Aggregate summary bar - Activity feed entries and container Full implementation is provided in task 4.x; this module currently contains function stubs. """ from datetime import datetime from typing import Any, List, Optional def build_css() -> str: """Generate custom CSS for the dark financial terminal theme. Returns: CSS string to be injected into the Gradio Blocks ``css`` parameter. """ return """ .gradio-container { font-family: 'JetBrains Mono', 'Fira Code', 'Courier New', monospace !important; background-color: #0d1117 !important; } .activity-feed { background: #161b22; border: 1px solid #30363d; border-radius: 6px; padding: 12px; max-height: 400px; overflow-y: auto; font-family: 'JetBrains Mono', monospace; font-size: 13px; } .activity-entry { padding: 4px 0; border-bottom: 1px solid #21262d; color: #c9d1d9; } .activity-timestamp { color: #8b949e; margin-right: 8px; } .activity-agent { color: #58a6ff; font-weight: bold; } .activity-spinner { color: #f0883e; } .signal-card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; margin: 8px 0; border-left: 4px solid; } .signal-buy { border-left-color: #3fb950; } .signal-sell { border-left-color: #f85149; } .signal-hold { border-left-color: #d29922; } .signal-error { border-left-color: #f85149; background: #1c0c0c; } .signal-ticker { font-size: 18px; font-weight: bold; color: #f0f6fc; } .signal-action-buy { color: #3fb950; } .signal-action-sell { color: #f85149; } .signal-action-hold { color: #d29922; } .signal-confidence { font-size: 14px; color: #8b949e; } .signal-prices { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-top: 8px; } .signal-price-item { text-align: center; padding: 8px; background: #0d1117; border-radius: 4px; } .signal-price-label { font-size: 11px; color: #8b949e; text-transform: uppercase; } .signal-price-value { font-size: 16px; color: #f0f6fc; font-weight: bold; } .summary-bar { display: flex; gap: 16px; padding: 12px; background: #161b22; border-radius: 6px; margin-top: 12px; border: 1px solid #30363d; } .summary-item { text-align: center; flex: 1; } """ def _format_price(value: Optional[float]) -> str: """Format an optional price as ``$X.XX`` or ``N/A`` when missing.""" if value is None: return "N/A" return f"${value:.2f}" def _action_text(action: Any) -> str: """Return the upper-case action label from an enum-like or string value.""" # Support both enum-like objects (with ``.value``) and plain strings. raw = getattr(action, "value", action) return str(raw).upper() def render_signal_card(signal: Any) -> str: """Render a TradingSignal as an HTML card. Args: signal: TradingSignal dataclass instance containing ticker, action, confidence, entry/stop-loss/target prices, and reasoning. Returns: HTML string for the signal card with action-specific color coding. """ action_text = _action_text(signal.action) action_lower = action_text.lower() action_class = f"signal-{action_lower}" action_color_class = f"signal-action-{action_lower}" entry_price = getattr(signal, "entry_price", None) stop_loss = getattr(signal, "stop_loss", None) target_price = getattr(signal, "target_price", None) prices_html = f"""
Entry
{_format_price(entry_price)}
Stop Loss
{_format_price(stop_loss)}
Target
{_format_price(target_price)}
""" reasoning = getattr(signal, "reasoning", None) or {} reasoning_html = "" if reasoning: reasoning_items = "".join( f"
  • {k}: {v}
  • " for k, v in reasoning.items() ) reasoning_html = ( f"" ) return f"""
    {signal.ticker} {action_text}
    Confidence: {signal.confidence}%
    {prices_html} {reasoning_html}
    """ def render_error_card(ticker: str, error_message: str) -> str: """Render an error card for a failed ticker analysis. Args: ticker: The ticker symbol that failed analysis. error_message: Human-readable error description. Returns: HTML string for the error card with the ``signal-error`` class. """ return f"""
    {ticker}
    ⚠️ Analysis failed: {error_message}
    """ def render_summary( total: int, buy_count: int, sell_count: int, hold_count: int, ) -> str: """Render the aggregate summary bar. Args: total: Total number of tickers analyzed. buy_count: Number of BUY signals. sell_count: Number of SELL signals. hold_count: Number of HOLD signals. Returns: HTML string for the summary bar with color-coded counts (green for BUY, red for SELL, yellow for HOLD). """ return f"""
    {total}
    ANALYZED
    {buy_count}
    BUY
    {sell_count}
    SELL
    {hold_count}
    HOLD
    """ def render_activity_entry( timestamp: datetime, agent_name: str, message: str, is_spinner: bool = False, ) -> str: """Render a single activity feed entry as HTML. Args: timestamp: Entry timestamp (formatted as HH:MM:SS). agent_name: Name of the agent producing the entry. message: Entry message content. is_spinner: When True, include a spinner indicator. Returns: HTML string for the activity entry. """ time_str = timestamp.strftime("%H:%M:%S") spinner = '' if is_spinner else "" return f"""
    [{time_str}] {agent_name}{spinner} {message}
    """ def render_activity_feed(entries: List[str]) -> str: """Wrap activity entries in the feed container with auto-scroll. Args: entries: List of pre-rendered HTML entry strings. Returns: HTML string for the activity feed container, including an auto-scroll script that pins the view to the latest entry. """ entries_html = "\n".join(entries) return f"""
    {entries_html}
    """