Spaces:
Running
Running
Download core/ui/components.py from nvikou/polyglot-rag: direct link, hf CLI and curl.
- Browser
- Download file 17.2 kB
-
https://huggingface.co/spaces/nvikou/polyglot-rag/resolve/main/core/ui/components.py
- Command line
-
hf download hf://spaces/nvikou/polyglot-rag/core/ui/components.py
-
curl -L -o components.py https://huggingface.co/spaces/nvikou/polyglot-rag/resolve/main/core/ui/components.py
17.2 kB
| """ | |
| 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": '<circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>', | |
| "file": '<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><path d="M14 2v6h6"/><path d="M16 13H8"/><path d="M16 17H8"/>', | |
| "gauge": '<path d="m12 14 4-4"/><path d="M3.34 19a10 10 0 1 1 17.32 0"/>', | |
| "clock": '<circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/>', | |
| "zap": '<path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z"/>', | |
| "shield": '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="m9 12 2 2 4-4"/>', | |
| "alert": '<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3z"/><path d="M12 9v4"/><path d="M12 17h.01"/>', | |
| "layers": '<path d="m12 2 10 5-10 5L2 7l10-5z"/><path d="m2 17 10 5 10-5"/><path d="m2 12 10 5 10-5"/>', | |
| "database": '<ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/><path d="M3 12c0 1.66 4 3 9 3s9-1.34 9-3"/>', | |
| "search": '<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>', | |
| "cpu": '<rect x="4" y="4" width="16" height="16" rx="2"/><rect x="9" y="9" width="6" height="6"/><path d="M15 2v2M15 20v2M2 15h2M2 9h2M20 15h2M20 9h2M9 2v2M9 20v2"/>', | |
| "scissors": '<circle cx="6" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M20 4 8.12 15.88"/><path d="M14.47 14.48 20 20"/><path d="M8.12 8.12 12 12"/>', | |
| "tag": '<path d="M12 2H2v10l9.29 9.29a1 1 0 0 0 1.41 0l8.59-8.59a1 1 0 0 0 0-1.41L12 2z"/><path d="M7 7h.01"/>', | |
| "message": '<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>', | |
| "sparkles": '<path d="m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3L12 3z"/>', | |
| "key": '<circle cx="7.5" cy="15.5" r="5.5"/><path d="m21 2-9.6 9.6"/><path d="m15.5 7.5 3 3L22 7l-3-3"/>', | |
| "server": '<rect x="2" y="2" width="20" height="8" rx="2"/><rect x="2" y="14" width="20" height="8" rx="2"/><path d="M6 6h.01M6 18h.01"/>', | |
| "repeat": '<path d="m17 2 4 4-4 4"/><path d="M3 11v-1a4 4 0 0 1 4-4h14"/><path d="m7 22-4-4 4-4"/><path d="M21 13v1a4 4 0 0 1-4 4H3"/>', | |
| "languages": '<path d="m5 8 6 6"/><path d="m4 14 6-6 2-3"/><path d="M2 5h12"/><path d="M7 2h1"/><path d="m22 22-5-10-5 10"/><path d="M14 18h6"/>', | |
| "activity": '<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>', | |
| } | |
| def icon(name: str, size: int = 16, cls: str = "pg-icon") -> str: | |
| path = _ICON_PATHS.get(name, "") | |
| return ( | |
| f'<svg class="{cls}" width="{size}" height="{size}" viewBox="0 0 24 24" fill="none" ' | |
| f'stroke="currentColor" stroke-width="1.75" stroke-linecap="round" ' | |
| f'stroke-linejoin="round" aria-hidden="true">{path}</svg>' | |
| ) | |
| LOGO_SVG = ( | |
| '<svg class="pg-logo" width="36" height="36" viewBox="0 0 40 40" aria-hidden="true">' | |
| '<defs><linearGradient id="pgLogoGrad" x1="0" y1="0" x2="1" y2="1">' | |
| '<stop offset="0" stop-color="#6D7CFF"/><stop offset="1" stop-color="#22D3EE"/>' | |
| "</linearGradient></defs>" | |
| '<rect width="40" height="40" rx="11" fill="url(#pgLogoGrad)"/>' | |
| '<circle cx="20" cy="20" r="10.5" fill="none" stroke="#fff" stroke-width="2"/>' | |
| '<path d="M9.5 20h21M20 9.5c3 3 4.4 6.6 4.4 10.5S23 27.5 20 30.5M20 9.5c-3 3-4.4 6.6-4.4 10.5S17 27.5 20 30.5" ' | |
| 'fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round"/>' | |
| "</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'<bdi dir="ltr">{_e(text)}</bdi>' | |
| 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'<span class="pg-lang-badge" data-lang="{_e(code)}">{_e(code.upper())}</span>' | |
| # --------------------------------------------------------------------------- # | |
| # En-tête / pied de page | |
| # --------------------------------------------------------------------------- # | |
| def render_brand(lang: str) -> str: | |
| return f""" | |
| <div class="pg-brand"> | |
| {LOGO_SVG} | |
| <div class="pg-brand-text"> | |
| <div class="pg-brand-title">PolyglotRAG <span class="pg-brand-tag">{_e(t(lang, "app_tagline"))}</span></div> | |
| <div class="pg-brand-sub">{_e(t(lang, "app_subtitle"))}</div> | |
| </div> | |
| </div>""" | |
| 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'<span class="pg-chip-lang">{_e(code.upper())}</span>' for code in SUPPORTED_LANGUAGES | |
| ) | |
| return f""" | |
| <div class="pg-status-wrap"> | |
| <span class="pg-status pg-status-{cls}"><span class="pg-dot"></span>{_e(label)}</span> | |
| <span class="pg-status-langs">{langs}</span> | |
| </div>""" | |
| def render_config_banner(lang: str, config_ok: bool) -> str: | |
| if config_ok: | |
| return "" | |
| return f'<div class="pg-banner pg-banner-warn">{icon("alert", 18)}<span>{_e(t(lang, "config_warning"))}</span></div>' | |
| def render_footer(lang: str) -> str: | |
| return f'<div class="pg-footer">{icon("sparkles", 14)}<span>{_e(t(lang, "footer"))}</span></div>' | |
| def render_section_title(lang: str, key: str, icon_name: str) -> str: | |
| return f'<div class="pg-section-title">{icon(icon_name, 16)}<span>{_e(t(lang, key))}</span></div>' | |
| def render_welcome(lang: str) -> str: | |
| """Contenu affiché dans la zone de conversation tant qu'elle est vide.""" | |
| return ( | |
| f'<div class="pg-welcome">{LOGO_SVG}' | |
| f"<h3>{_e(t(lang, 'welcome_title'))}</h3>" | |
| f"<p>{_e(t(lang, 'welcome_text'))}</p></div>" | |
| ) | |
| # --------------------------------------------------------------------------- # | |
| # 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'<div class="pg-meter pg-meter-{tone}"><span style="width:{pct:.0f}%"></span></div>' | |
| def render_insights(lang: str, data: dict | None) -> str: | |
| title = render_section_title(lang, "insights_title", "activity") | |
| if not data: | |
| return f'<div class="pg-card">{title}<p class="pg-muted">{_e(t(lang, "insights_empty"))}</p></div>' | |
| grounded = not data.get("used_fallback") | |
| status = ( | |
| f'<span class="pg-pill pg-pill-ok">{icon("shield", 13)}{_e(t(lang, "grounded"))}</span>' | |
| if grounded | |
| else f'<span class="pg-pill pg-pill-warn">{icon("alert", 13)}{_e(t(lang, "abstained"))}</span>' | |
| ) | |
| cache = ( | |
| f'<span class="pg-pill pg-pill-info">{icon("zap", 13)}{_e(t(lang, "cached"))}</span>' | |
| 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""" | |
| <div class="pg-card"> | |
| {title} | |
| <div class="pg-pills">{status}{cache}</div> | |
| <dl class="pg-stats"> | |
| <div><dt>{_e(t(lang, "detected_lang"))}</dt> | |
| <dd>{lang_badge(data.get("query_language"))} {_e(lang_name(data.get("query_language")))} | |
| <small>{_n(f"{qconf:.0%}")}</small></dd></div> | |
| <div><dt>{_e(t(lang, "answer_lang"))}</dt> | |
| <dd>{lang_badge(data.get("target_language"))} {_e(lang_name(data.get("target_language")))}</dd></div> | |
| <div><dt>{_e(t(lang, "confidence"))}</dt> | |
| <dd class="pg-num">{_n(f"{conf:.2f}")}</dd> | |
| {_meter(conf, "ok" if grounded else "warn")}</div> | |
| <div><dt>{_e(t(lang, "latency"))}</dt> | |
| <dd class="pg-num">{_n(f"{latency:,.0f} ms")}</dd></div> | |
| </dl> | |
| </div>""" | |
| 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'<div class="pg-card">{title}<p class="pg-muted">{_e(t(lang, "no_sources"))}</p></div>' | |
| 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""" | |
| <li class="pg-source"> | |
| <span class="pg-source-idx">{i}</span> | |
| <div class="pg-source-body"> | |
| <div class="pg-source-file">{icon("file", 14)}<span dir="auto">{_e(s.get("file"))}</span></div> | |
| {f'<div class="pg-source-title" dir="auto">{_e(doc_title)}</div>' if doc_title else ""} | |
| <div class="pg-source-meta">{lang_badge(s.get("language"))} | |
| <span>{_e(t(lang, "page"))} {_e(s.get("page"))}</span> | |
| <span class="pg-sep">·</span> | |
| <span>{_e(t(lang, "score"))} <b class="pg-num">{_n(f"{score:.2f}")}</b></span> | |
| </div> | |
| {_meter(score, "ok")} | |
| </div> | |
| </li>""" | |
| ) | |
| return f'<div class="pg-card">{title}<ol class="pg-sources">{"".join(items)}</ol></div>' | |
| # --------------------------------------------------------------------------- # | |
| # Administration | |
| # --------------------------------------------------------------------------- # | |
| def _kpi(label: str, value: str, icon_name: str, hint: str = "") -> str: | |
| hint_html = f'<div class="pg-kpi-hint">{_e(hint)}</div>' if hint else "" | |
| return ( | |
| f'<div class="pg-kpi"><div class="pg-kpi-head">{icon(icon_name, 15)}' | |
| f"<span>{_e(label)}</span></div>" | |
| f'<div class="pg-kpi-value pg-num">{_n(value)}</div>{hint_html}</div>' | |
| ) | |
| def _bars(lang: str, title_key: str, data: dict) -> str: | |
| title = f'<div class="pg-chart-title">{_e(t(lang, title_key))}</div>' | |
| clean = {k: v for k, v in (data or {}).items() if v} | |
| if not clean: | |
| return f'<div class="pg-card pg-chart">{title}<p class="pg-muted">{_e(t(lang, "no_data"))}</p></div>' | |
| 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""" | |
| <div class="pg-bar-row"> | |
| <div class="pg-bar-label">{lang_badge(code)}<span>{_e(lang_name(code))}</span></div> | |
| <div class="pg-bar-track"><span style="width:{width:.1f}%"></span></div> | |
| <div class="pg-bar-value pg-num">{_n(f"{value:,}")} <small>{_n(f"{share:.0%}")}</small></div> | |
| </div>""" | |
| ) | |
| return f'<div class="pg-card pg-chart">{title}{"".join(rows)}</div>' | |
| def _recent_table(lang: str, rows: list[dict]) -> str: | |
| title = render_section_title(lang, "recent_title", "message") | |
| if not rows: | |
| return f'<div class="pg-card">{title}<p class="pg-muted">{_e(t(lang, "no_data"))}</p></div>' | |
| head = "".join( | |
| f"<th>{_e(t(lang, k))}</th>" | |
| 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'<span class="pg-pill pg-pill-warn">{_e(t(lang, "status_abst"))}</span>' | |
| if abst | |
| else f'<span class="pg-pill pg-pill-ok">{_e(t(lang, "status_ok"))}</span>' | |
| ) | |
| conf = float(r.get("confidence_score") or 0.0) | |
| lat = float(r.get("total_latency_ms") or 0.0) | |
| body.append( | |
| f"<tr><td class='pg-td-q' dir='auto' title='{_e(question)}'>{_e(short)}</td>" | |
| f"<td>{lang_badge(r.get('query_language'))}</td>" | |
| f"<td>{lang_badge(r.get('target_language'))}</td>" | |
| f"<td class='pg-num'>{_n(f'{conf:.2f}')}</td><td>{status}</td>" | |
| f"<td class='pg-num'>{_n(f'{lat:,.0f} ms')}</td></tr>" | |
| ) | |
| return ( | |
| f'<div class="pg-card">{title}<div class="pg-table-wrap"><table class="pg-table">' | |
| f"<thead><tr>{head}</tr></thead><tbody>{''.join(body)}</tbody></table></div></div>" | |
| ) | |
| def render_admin_placeholder(lang: str) -> str: | |
| return f'<div class="pg-card pg-empty-state">{icon("key", 22)}<p>{_e(t(lang, "admin_hint"))}</p></div>' | |
| def render_admin_denied(lang: str) -> str: | |
| return f'<div class="pg-banner pg-banner-error">{icon("alert", 18)}<span>{_e(t(lang, "denied"))}</span></div>' | |
| 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""" | |
| <div class="pg-admin"> | |
| {render_section_title(lang, "section_quality", "activity")} | |
| <div class="pg-kpi-grid">{quality}</div> | |
| {render_section_title(lang, "section_corpus", "server")} | |
| <div class="pg-kpi-grid">{corpus}</div> | |
| <div class="pg-chart-grid"> | |
| {_bars(lang, "chart_queries", m.get("by_query_language", {}))} | |
| {_bars(lang, "chart_corpus", c.get("by_language", {}))} | |
| </div> | |
| {_recent_table(lang, data.get("recent", []))} | |
| </div>""" | |
| # --------------------------------------------------------------------------- # | |
| # Architecture | |
| # --------------------------------------------------------------------------- # | |
| def _step(n: int, lang: str, key: str, icon_name: str) -> str: | |
| return f""" | |
| <div class="pg-step"> | |
| <div class="pg-step-icon">{icon(icon_name, 18)}</div> | |
| <div class="pg-step-n">{n:02d}</div> | |
| <div class="pg-step-t">{_e(t(lang, key + "_t"))}</div> | |
| <div class="pg-step-d">{_e(t(lang, key + "_d"))}</div> | |
| </div>""" | |
| 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'<div class="pg-why">{icon(ic, 18)}<div><b>{_e(t(lang, k + "_t"))}</b>' | |
| f"<p>{_e(t(lang, k + '_d'))}</p></div></div>" | |
| for k, ic in [("w1", "globe"), ("w2", "key"), ("w3", "server"), ("w4", "shield")] | |
| ) | |
| return f""" | |
| <div class="pg-arch"> | |
| <div class="pg-arch-hero"> | |
| <h2>{_e(t(lang, "arch_title"))}</h2> | |
| <p>{_e(t(lang, "arch_intro"))}</p> | |
| </div> | |
| <div class="pg-lane"> | |
| <div class="pg-lane-head"><span class="pg-lane-dot pg-lane-a"></span>{_e(t(lang, "lane_ingest"))}</div> | |
| <div class="pg-steps">{ingest}</div> | |
| </div> | |
| <div class="pg-lane"> | |
| <div class="pg-lane-head"><span class="pg-lane-dot pg-lane-b"></span>{_e(t(lang, "lane_query"))}</div> | |
| <div class="pg-steps">{query}</div> | |
| </div> | |
| {render_section_title(lang, "why_title", "layers")} | |
| <div class="pg-why-grid">{why}</div> | |
| </div>""" | |
| 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 | |