""" Fragments HTML de l'interface (en-tête, panneaux d'analyse, KPI, graphiques, schéma d'architecture…). Tout est rendu côté serveur en HTML + CSS (voir `theme.py`) : le rendu est identique quelle que soit la version de Gradio, entièrement traduisible, et s'adapte automatiquement au mode clair/sombre et au sens d'écriture (RTL). Toute donnée dynamique est échappée avec `html.escape`. """ from __future__ import annotations import html import math from config import SUPPORTED_LANGUAGES from core.ui.i18n import t # --------------------------------------------------------------------------- # # Icônes (traits 1.75px, style « lucide ») # --------------------------------------------------------------------------- # _ICON_PATHS = { "globe": '', "file": '', "gauge": '', "clock": '', "zap": '', "shield": '', "alert": '', "layers": '', "database": '', "search": '', "cpu": '', "scissors": '', "tag": '', "message": '', "sparkles": '', "key": '', "server": '', "repeat": '', "languages": '', "activity": '', } def icon(name: str, size: int = 16, cls: str = "pg-icon") -> str: path = _ICON_PATHS.get(name, "") return ( f'' ) LOGO_SVG = ( '" ) def _e(value) -> str: return html.escape("" if value is None else str(value)) def _n(text) -> str: """Valeur numérique isolée en LTR (évite « ms 1,240 » ou « 512 / 37 » en arabe).""" return f'{_e(text)}' def lang_name(code: str | None) -> str: info = SUPPORTED_LANGUAGES.get(code or "") return info["name"] if info else (code or "—") def lang_badge(code: str | None) -> str: code = code or "?" return f'{_e(code.upper())}' # --------------------------------------------------------------------------- # # En-tête / pied de page # --------------------------------------------------------------------------- # def render_brand(lang: str) -> str: return f"""
{LOGO_SVG}
PolyglotRAG {_e(t(lang, "app_tagline"))}
{_e(t(lang, "app_subtitle"))}
""" def render_status(lang: str, corpus: dict, config_ok: bool) -> str: if not config_ok: cls, label = "warn", t(lang, "status_config") elif corpus.get("total_chunks", 0) > 0: cls = "ok" label = t(lang, "status_ready", chunks=corpus["total_chunks"], docs=corpus.get("documents", 0)) else: cls, label = "warn", t(lang, "status_empty") langs = "".join( f'{_e(code.upper())}' for code in SUPPORTED_LANGUAGES ) return f"""
{_e(label)} {langs}
""" def render_config_banner(lang: str, config_ok: bool) -> str: if config_ok: return "" return f'
{icon("alert", 18)}{_e(t(lang, "config_warning"))}
' def render_footer(lang: str) -> str: return f'' def render_section_title(lang: str, key: str, icon_name: str) -> str: return f'
{icon(icon_name, 16)}{_e(t(lang, key))}
' def render_welcome(lang: str) -> str: """Contenu affiché dans la zone de conversation tant qu'elle est vide.""" return ( f'
{LOGO_SVG}' f"

{_e(t(lang, 'welcome_title'))}

" f"

{_e(t(lang, 'welcome_text'))}

" ) # --------------------------------------------------------------------------- # # Panneau « Analyse de la réponse » + sources # --------------------------------------------------------------------------- # def _meter(value: float, tone: str) -> str: pct = max(0.0, min(1.0, float(value or 0.0))) * 100 return f'
' def render_insights(lang: str, data: dict | None) -> str: title = render_section_title(lang, "insights_title", "activity") if not data: return f'
{title}

{_e(t(lang, "insights_empty"))}

' grounded = not data.get("used_fallback") status = ( f'{icon("shield", 13)}{_e(t(lang, "grounded"))}' if grounded else f'{icon("alert", 13)}{_e(t(lang, "abstained"))}' ) cache = ( f'{icon("zap", 13)}{_e(t(lang, "cached"))}' if data.get("from_cache") else "" ) conf = float(data.get("confidence", 0.0) or 0.0) qconf = float(data.get("query_language_confidence", 0.0) or 0.0) latency = float(data.get("latency_ms", 0.0) or 0.0) return f"""
{title}
{status}{cache}
{_e(t(lang, "detected_lang"))}
{lang_badge(data.get("query_language"))} {_e(lang_name(data.get("query_language")))} {_n(f"{qconf:.0%}")}
{_e(t(lang, "answer_lang"))}
{lang_badge(data.get("target_language"))} {_e(lang_name(data.get("target_language")))}
{_e(t(lang, "confidence"))}
{_n(f"{conf:.2f}")}
{_meter(conf, "ok" if grounded else "warn")}
{_e(t(lang, "latency"))}
{_n(f"{latency:,.0f} ms")}
""" def render_sources(lang: str, sources: list[dict] | None, visible: bool = True) -> str: title = render_section_title(lang, "sources_title", "file") if not visible: return "" if not sources: return f'
{title}

{_e(t(lang, "no_sources"))}

' items = [] for i, s in enumerate(sources, start=1): doc_title = s.get("title") if not isinstance(doc_title, str) or not doc_title.strip(): doc_title = "" score = float(s.get("score", 0.0) or 0.0) items.append( f"""
  • {i}
    {icon("file", 14)}{_e(s.get("file"))}
    {f'
    {_e(doc_title)}
    ' if doc_title else ""}
    {lang_badge(s.get("language"))} {_e(t(lang, "page"))} {_e(s.get("page"))} · {_e(t(lang, "score"))} {_n(f"{score:.2f}")}
    {_meter(score, "ok")}
  • """ ) return f'
    {title}
      {"".join(items)}
    ' # --------------------------------------------------------------------------- # # Administration # --------------------------------------------------------------------------- # def _kpi(label: str, value: str, icon_name: str, hint: str = "") -> str: hint_html = f'
    {_e(hint)}
    ' if hint else "" return ( f'
    {icon(icon_name, 15)}' f"{_e(label)}
    " f'
    {_n(value)}
    {hint_html}
    ' ) def _bars(lang: str, title_key: str, data: dict) -> str: title = f'
    {_e(t(lang, title_key))}
    ' clean = {k: v for k, v in (data or {}).items() if v} if not clean: return f'
    {title}

    {_e(t(lang, "no_data"))}

    ' total = sum(clean.values()) peak = max(clean.values()) rows = [] for code, value in sorted(clean.items(), key=lambda kv: kv[1], reverse=True): width = (value / peak) * 100 if peak else 0 share = value / total if total else 0 rows.append( f"""
    {lang_badge(code)}{_e(lang_name(code))}
    {_n(f"{value:,}")} {_n(f"{share:.0%}")}
    """ ) return f'
    {title}{"".join(rows)}
    ' def _recent_table(lang: str, rows: list[dict]) -> str: title = render_section_title(lang, "recent_title", "message") if not rows: return f'
    {title}

    {_e(t(lang, "no_data"))}

    ' head = "".join( f"{_e(t(lang, k))}" for k in ("col_question", "col_qlang", "col_alang", "col_conf", "col_status", "col_latency") ) body = [] for r in rows: question = str(r.get("question") or "") short = question if len(question) <= 110 else question[:107] + "…" abst = bool(r.get("used_fallback")) status = ( f'{_e(t(lang, "status_abst"))}' if abst else f'{_e(t(lang, "status_ok"))}' ) conf = float(r.get("confidence_score") or 0.0) lat = float(r.get("total_latency_ms") or 0.0) body.append( f"{_e(short)}" f"{lang_badge(r.get('query_language'))}" f"{lang_badge(r.get('target_language'))}" f"{_n(f'{conf:.2f}')}{status}" f"{_n(f'{lat:,.0f} ms')}" ) return ( f'
    {title}
    ' f"{head}{''.join(body)}
    " ) def render_admin_placeholder(lang: str) -> str: return f'
    {icon("key", 22)}

    {_e(t(lang, "admin_hint"))}

    ' def render_admin_denied(lang: str) -> str: return f'
    {icon("alert", 18)}{_e(t(lang, "denied"))}
    ' def render_admin_dashboard(lang: str, data: dict) -> str: m = data["metrics"] c = data["corpus"] ac = data["answer_cache"] ec = data["embedding_cache"] def cache_value(stats: dict) -> str: return f"{stats['size']} / {stats['maxsize']}" if stats.get("enabled") else t(lang, "disabled") quality = "".join( [ _kpi(t(lang, "kpi_queries"), f"{m['total_queries']:,}", "message"), _kpi(t(lang, "kpi_abstention"), f"{m['abstention_rate']:.0%}", "shield"), _kpi(t(lang, "kpi_confidence"), f"{m['avg_confidence']:.2f}", "gauge"), _kpi(t(lang, "kpi_latency"), f"{m['avg_latency_ms']:,.0f} ms", "clock"), _kpi(t(lang, "kpi_crossling"), f"{m.get('cross_lingual_rate', 0.0):.0%}", "languages"), ] ) corpus = "".join( [ _kpi(t(lang, "kpi_chunks"), f"{c['total_chunks']:,}", "layers"), _kpi(t(lang, "kpi_docs"), f"{c['documents']:,}", "file"), _kpi(t(lang, "kpi_index"), str(c.get("ann_index_type", "—")).upper(), "database"), _kpi(t(lang, "kpi_answer_cache"), cache_value(ac), "zap"), _kpi(t(lang, "kpi_embed_cache"), cache_value(ec), "cpu"), ] ) return f"""
    {render_section_title(lang, "section_quality", "activity")}
    {quality}
    {render_section_title(lang, "section_corpus", "server")}
    {corpus}
    {_bars(lang, "chart_queries", m.get("by_query_language", {}))} {_bars(lang, "chart_corpus", c.get("by_language", {}))}
    {_recent_table(lang, data.get("recent", []))}
    """ # --------------------------------------------------------------------------- # # Architecture # --------------------------------------------------------------------------- # def _step(n: int, lang: str, key: str, icon_name: str) -> str: return f"""
    {icon(icon_name, 18)}
    {n:02d}
    {_e(t(lang, key + "_t"))}
    {_e(t(lang, key + "_d"))}
    """ def render_architecture(lang: str) -> str: ingest = "".join( _step(i, lang, k, ic) for i, (k, ic) in enumerate( [("s1", "file"), ("s2", "tag"), ("s3", "scissors"), ("s4", "cpu"), ("s5", "database")], start=1 ) ) query = "".join( _step(i, lang, k, ic) for i, (k, ic) in enumerate( [("q1", "languages"), ("q2", "search"), ("q3", "shield"), ("q4", "sparkles"), ("q5", "zap")], start=1 ) ) why = "".join( f'
    {icon(ic, 18)}
    {_e(t(lang, k + "_t"))}' f"

    {_e(t(lang, k + '_d'))}

    " for k, ic in [("w1", "globe"), ("w2", "key"), ("w3", "server"), ("w4", "shield")] ) return f"""

    {_e(t(lang, "arch_title"))}

    {_e(t(lang, "arch_intro"))}

    {_e(t(lang, "lane_ingest"))}
    {ingest}
    {_e(t(lang, "lane_query"))}
    {query}
    {render_section_title(lang, "why_title", "layers")}
    {why}
    """ def safe_number(value, default: float = 0.0) -> float: try: v = float(value) return default if math.isnan(v) else v except (TypeError, ValueError): return default