enirtcod / ui_components.py
ArthurSrz's picture
redesign: polished card components — subtle shadows, refined badges, navy accents
ce4addb verified
"""
ui_components.py — HTML card builders for each legal source type + tab panel.
Doctrine.fr-inspired design: clean white cards, subtle borders, navy accents.
"""
# ── Shared card wrapper ──
_CARD_STYLE = (
"border:1px solid #e2e8f0;border-radius:10px;padding:16px 20px;"
"margin-bottom:12px;background:#ffffff;"
"box-shadow:0 1px 3px rgba(0,0,0,0.04);"
"transition:box-shadow 0.15s,border-color 0.15s"
)
# ── Badge colors per source ──
_BADGE = {
"article": ("background:#dbeafe;color:#1e40af", "Code"),
"decision": ("background:#fce7f3;color:#9d174d", "Jurisprudence"),
"circulaire": ("background:#d1fae5;color:#065f46", "Circulaire"),
"reponse": ("background:#fef3c7;color:#92400e", "Q&R"),
}
def _badge(style: str, text: str) -> str:
return (
f'<span style="{style};font-size:11px;font-weight:600;'
f'padding:3px 10px;border-radius:20px;letter-spacing:0.2px">{text}</span>'
)
def _meta_line(*parts: str) -> str:
items = [p for p in parts if p]
return (
'<div style="font-size:12px;color:#94a3b8;display:flex;align-items:center;gap:6px">'
+ " · ".join(items)
+ '</div>'
)
def build_article_card(result: dict, related_decisions: list[dict] | None = None) -> str:
code = result.get("code_name", "Code")
num = result.get("num", result.get("id_legifrance", ""))
snippet = (result.get("chunk_text") or "")[:250]
lf_id = result.get("id_legifrance", "")
url = f"https://www.legifrance.gouv.fr/codes/article_lc/{lf_id}" if lf_id else "#"
etat = result.get("article_etat", "")
etat_html = ""
if etat:
color = "#059669" if etat == "VIGUEUR" else "#94a3b8"
etat_html = f'<span style="font-size:11px;color:{color};font-weight:500">{etat}</span>'
cross_ref_html = ""
if related_decisions:
mini_cards = ""
for dec in related_decisions:
dec_url = dec.get("url_judilibre", "#")
dec_date = dec.get("date_decision", "")
dec_jur = dec.get("jurisdiction", "")
dec_snip = dec.get("chunk_text", "")[:120]
mini_cards += f"""
<div style="border-left:3px solid #1e3a5f;padding:8px 12px;margin-top:8px;
font-size:12px;color:#334155;background:#f8fafc;border-radius:0 6px 6px 0">
<strong>{dec_jur}</strong> · {dec_date}
<div style="color:#64748b;margin-top:3px">{dec_snip}…</div>
<a href="{dec_url}" target="_blank"
style="color:#1e3a5f;font-size:11px;text-decoration:none;font-weight:500">Voir la decision →</a>
</div>"""
n = len(related_decisions)
cross_ref_html = f"""
<details style="margin-top:10px">
<summary style="cursor:pointer;color:#1e3a5f;font-size:13px;font-weight:600">
{n} decision{"s" if n > 1 else ""} liee{"s" if n > 1 else ""}
</summary>
{mini_cards}
</details>"""
return f"""
<div style="{_CARD_STYLE}">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;flex-wrap:wrap">
{_badge(*_BADGE["article"][:1], code)}
<strong style="font-size:15px;color:#1e293b;letter-spacing:-0.2px">Art. {num}</strong>
{etat_html}
</div>
<p style="font-size:13.5px;color:#475569;margin:0 0 10px;line-height:1.65">{snippet}…</p>
{_meta_line(f'<a href="{url}" target="_blank" style="color:#1e3a5f;text-decoration:none;font-weight:500">Legifrance →</a>')}
{cross_ref_html}
</div>"""
def build_decision_card(result: dict) -> str:
juris = result.get("jurisdiction", "")
chamber = result.get("chamber", "")
date = result.get("date_decision", "")
fiche = result.get("fiche_arret") or ""
snippet = fiche[:250] if fiche else (result.get("chunk_text") or "")[:250]
url = result.get("url_judilibre", "#")
src_id = result.get("source_id", "")
label = juris + (f" · {chamber}" if chamber else "")
return f"""
<div style="{_CARD_STYLE}">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;flex-wrap:wrap">
{_badge(_BADGE["decision"][0], label)}
<strong style="font-size:14px;color:#1e293b">{date}</strong>
{f'<span style="font-size:11px;color:#94a3b8">n° {src_id}</span>' if src_id else ""}
</div>
<p style="font-size:13.5px;color:#475569;margin:0 0 10px;line-height:1.65">{snippet}…</p>
{_meta_line(f'<a href="{url}" target="_blank" style="color:#1e3a5f;text-decoration:none;font-weight:500">Cour de cassation →</a>')}
</div>"""
def build_circulaire_card(result: dict) -> str:
ministere = result.get("ministere", "")
numero = result.get("numero", result.get("source_id", ""))
objet = (result.get("objet") or result.get("chunk_text") or "")[:250]
date = (result.get("date_parution") or "")[:10]
url = result.get("url_legifrance", "#")
return f"""
<div style="{_CARD_STYLE}">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;flex-wrap:wrap">
{_badge(_BADGE["circulaire"][0], f"Min. {ministere}")}
<strong style="font-size:14px;color:#1e293b">Circ. n° {numero}</strong>
</div>
<p style="font-size:13.5px;color:#475569;margin:0 0 10px;line-height:1.65">{objet}…</p>
{_meta_line(date, f'<a href="{url}" target="_blank" style="color:#1e3a5f;text-decoration:none;font-weight:500">Legifrance →</a>')}
</div>"""
def build_reponse_card(result: dict) -> str:
ministere = result.get("ministere", "")
num_q = result.get("numero_question", result.get("source_id", ""))
question = (result.get("question_text") or result.get("chunk_text") or "")[:250]
date = (result.get("date_reponse") or "")[:10]
url = result.get("url_legifrance", "#")
return f"""
<div style="{_CARD_STYLE}">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;flex-wrap:wrap">
{_badge(_BADGE["reponse"][0], ministere)}
<strong style="font-size:14px;color:#1e293b">Q. n° {num_q}</strong>
</div>
<p style="font-size:13.5px;color:#475569;margin:0 0 10px;line-height:1.65">{question}…</p>
{_meta_line(date, f'<a href="{url}" target="_blank" style="color:#1e3a5f;text-decoration:none;font-weight:500">Legifrance →</a>')}
</div>"""
def build_tabs_html(results_dict: dict, loading_status: dict) -> str:
tabs_config = [
("articles", "Articles", build_article_card),
("jurisprudence", "Jurisprudence", build_decision_card),
("circulaires", "Circulaires", build_circulaire_card),
("reponses", "Q&R", build_reponse_card),
]
tab_buttons = ""
tab_panels = ""
for i, (key, label, builder) in enumerate(tabs_config):
results = results_dict.get(key, [])
count = len(results)
# Active tab styling
if i == 0:
bg, color, weight, border = "#e8f0fe", "#1e3a5f", "600", "2px solid #1e3a5f"
else:
bg, color, weight, border = "transparent", "#64748b", "400", "2px solid transparent"
tab_buttons += f"""
<button onclick="showTab('{key}')" id="tab-btn-{key}"
style="padding:10px 20px;border:none;background:{bg};
color:{color};font-weight:{weight};
border-bottom:{border};
cursor:pointer;font-size:14px;border-radius:6px 6px 0 0;
transition:all 0.15s;letter-spacing:-0.1px">
{label} ({count})
</button>"""
if not loading_status.get(key, False):
content = '<p style="color:#94a3b8;font-style:italic;padding:24px 0">Source temporairement indisponible</p>'
elif not results:
content = '<p style="color:#94a3b8;font-style:italic;padding:24px 0">Aucun resultat pour cette source.</p>'
else:
content = "".join(builder(r) for r in results)
display = "block" if i == 0 else "none"
tab_panels += f"""
<div id="tab-{key}" style="display:{display};padding:16px 0">
{content}
</div>"""
js = """
<script>
function showTab(key) {
['articles','jurisprudence','circulaires','reponses'].forEach(k => {
document.getElementById('tab-' + k).style.display = (k === key) ? 'block' : 'none';
var btn = document.getElementById('tab-btn-' + k);
btn.style.background = (k === key) ? '#e8f0fe' : 'transparent';
btn.style.color = (k === key) ? '#1e3a5f' : '#64748b';
btn.style.fontWeight = (k === key) ? '600' : '400';
btn.style.borderBottom = (k === key) ? '2px solid #1e3a5f' : '2px solid transparent';
});
}
</script>"""
return f"""
<div style="font-family:'Inter',system-ui,sans-serif">
<div style="border-bottom:1px solid #e2e8f0;display:flex;gap:2px;margin-bottom:4px">
{tab_buttons}
</div>
{tab_panels}
{js}
</div>"""