""" app.py — enirtcod.fr Gradio HF Space entry point. Startup sequence: 1. load_all_datasets() — loads 4 FAISS indexes into RAM (~60-90s cold start) 2. Gradio Blocks layout with search bar, source selector, filter panel, synthesis panel, and tabbed result cards. """ import os import gradio as gr from data_loader import load_all_datasets, embed_query, LOADING_STATUS from search import search_all, find_related_decisions from synthesis import synthesize from ui_components import build_tabs_html, build_article_card HF_TOKEN = os.environ.get("HF_TOKEN", "") # --------------------------------------------------------------------------- # Logo — base64 inline (Gradio 5.x doesn't serve file/ paths reliably) # --------------------------------------------------------------------------- _logo_b64 = "" _logo_path = os.path.join(os.path.dirname(__file__), "logo_b64.txt") if os.path.exists(_logo_path): with open(_logo_path) as f: _logo_b64 = f.read().strip() # --------------------------------------------------------------------------- # Cold start — runs once at module import (Space startup) # --------------------------------------------------------------------------- print("[app] Starting dataset loading… (may take up to 90s)") DATASETS = load_all_datasets() print(f"[app] Loading complete. Status: {LOADING_STATUS}") # Populate filter dropdowns dynamically from loaded datasets _code_names = [] if DATASETS.get("articles"): try: _code_names = sorted(set(DATASETS["articles"]["code_name"])) except Exception: _code_names = [] _ministeres = [] if DATASETS.get("circulaires"): try: _ministeres = sorted(set(m for m in DATASETS["circulaires"]["ministere"] if m)) except Exception: _ministeres = [] # --------------------------------------------------------------------------- # Search handler # --------------------------------------------------------------------------- def run_search(query: str, source_filter: str, date_from: int, date_to: int, jurisdiction: str, code_name: str, ministere: str): if not query.strip(): return ( gr.update(value='

Veuillez entrer une question juridique.

'), gr.update(value=""), ) warning_note = "" if len(query) > 500: query = query[:500] warning_note = "\n\n*Requete tronquee a 500 caracteres.*" try: embedding = embed_query(query, HF_TOKEN) except ValueError as e: return ( gr.update(value=f'

{e}

'), gr.update(value=""), ) filters = {} if date_from and int(date_from) > 2000: filters["date_from"] = int(date_from) if date_to and int(date_to) < 2026: filters["date_to"] = int(date_to) if jurisdiction and jurisdiction != "Tous": filters["jurisdiction"] = jurisdiction if code_name and code_name != "Tous": filters["code_name"] = code_name if ministere and ministere != "Tous": filters["ministere"] = ministere results = search_all(embedding, DATASETS, source_filter=source_filter, filters=filters) enriched_articles = [] for r in results.get("articles", []): lf_id = r.get("id_legifrance", "") related = find_related_decisions(lf_id, DATASETS.get("jurisprudence")) enriched_articles.append((r, related)) synthesis_text = synthesize(query, results, HF_TOKEN) + warning_note article_html = "".join( build_article_card(r, related) for r, related in enriched_articles ) tabs_html = build_tabs_html(results, LOADING_STATUS) if article_html and enriched_articles: plain_article_html = "".join(build_article_card(r) for r, _ in enriched_articles) tabs_html = tabs_html.replace(plain_article_html, article_html) synthesis_html = f"""

Synthese juridique

{synthesis_text}
""" return gr.update(value=synthesis_html), gr.update(value=tabs_html) # --------------------------------------------------------------------------- # Theme & CSS — Doctrine.fr-inspired design # --------------------------------------------------------------------------- CUSTOM_CSS = """ /* ── Global theme overrides ── */ :root { --color-accent: #0f172a !important; --color-accent-soft: #dbeafe !important; --button-primary-background-fill: #0f172a !important; --button-primary-background-fill-hover: #1e293b !important; --button-primary-text-color: #ffffff !important; --block-border-color: #cbd5e1 !important; --block-background-fill: #ffffff !important; --body-background-fill: #f1f5f9 !important; --input-background-fill: #ffffff !important; --block-label-text-color: #334155 !important; --slider-color: #0f172a !important; } .gradio-container { max-width: 100% !important; margin: 0 auto !important; width: 100% !important; padding: 0 48px !important; box-sizing: border-box !important; background: #f1f5f9 !important; } /* ── Typography ── */ .gradio-container, .gradio-container * { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif !important; } /* ── Search input ── */ .gradio-container textarea { border: 2px solid #94a3b8 !important; border-radius: 10px !important; font-size: 16px !important; padding: 14px 18px !important; transition: border-color 0.2s, box-shadow 0.2s !important; background: #ffffff !important; } .gradio-container textarea:focus { border-color: #0f172a !important; box-shadow: 0 0 0 3px rgba(15, 23, 42, 0.12) !important; outline: none !important; } /* ── Primary button ── */ .gradio-container button.primary { background: #0f172a !important; border: none !important; border-radius: 10px !important; font-weight: 700 !important; font-size: 16px !important; padding: 14px 32px !important; letter-spacing: 0.3px !important; transition: background 0.2s, transform 0.1s, box-shadow 0.2s !important; box-shadow: 0 2px 8px rgba(15, 23, 42, 0.2) !important; } .gradio-container button.primary:hover { background: #1e293b !important; transform: translateY(-1px) !important; box-shadow: 0 4px 12px rgba(15, 23, 42, 0.3) !important; } /* ── Dropdowns ── */ .gradio-container select, .gradio-container .wrap input { border: 2px solid #94a3b8 !important; border-radius: 8px !important; font-size: 14px !important; font-weight: 500 !important; } /* ── Slider: override orange track ── */ .gradio-container input[type="range"] { accent-color: #0f172a !important; } .gradio-container .range-slider { accent-color: #0f172a !important; } /* ── Accordion ── */ .gradio-container .accordion { border: 2px solid #cbd5e1 !important; border-radius: 10px !important; background: #ffffff !important; } .gradio-container .accordion summary { font-weight: 600 !important; color: #1e293b !important; } /* ── Block panels ── */ .gradio-container .block { border-radius: 10px !important; border: 1px solid #cbd5e1 !important; box-shadow: 0 1px 2px rgba(0,0,0,0.03) !important; } /* ── Hide footer ── */ footer { display: none !important; } /* ── Responsive ── */ @media (max-width: 768px) { .gradio-container { max-width: 100% !important; padding: 0 12px !important; } } """ # --------------------------------------------------------------------------- # Gradio layout # --------------------------------------------------------------------------- theme = gr.themes.Soft( primary_hue=gr.themes.colors.blue, secondary_hue=gr.themes.colors.slate, neutral_hue=gr.themes.colors.slate, font=gr.themes.GoogleFont("Inter"), ) with gr.Blocks(title="enirtcod.fr — Recherche juridique francaise", css=CUSTOM_CSS, theme=theme) as demo: # ── Header ── _logo_html = ( f'enirtcod logo' if _logo_b64 else "" ) gr.HTML(f"""
{_logo_html}

enirtcod.fr

Recherchez dans les codes, la jurisprudence et les circulaires

""") # ── Search bar ── gr.HTML('
') query_box = gr.Textbox( placeholder="Posez votre question : responsabilite civile, article 1240, licenciement abusif…", label="", lines=1, show_label=False, ) search_btn = gr.Button("Rechercher", variant="primary", size="lg") # ── Example queries ── gr.HTML("""
droit de retractation achat en ligne obligation de securite employeur article L1232-1 code du travail vice cache immobilier
""") # ── Filters ── with gr.Accordion("Affiner la recherche", open=False): source_selector = gr.Dropdown( choices=["Tous", "Articles", "Jurisprudence", "Circulaires", "Q&R"], value="Tous", label="Source", ) with gr.Row(): date_from = gr.Slider(minimum=2000, maximum=2026, step=1, value=2000, label="Depuis") date_to = gr.Slider(minimum=2000, maximum=2026, step=1, value=2026, label="Jusqu'a") with gr.Row(): juris_filter = gr.Dropdown( choices=["Tous", "Cour de cassation", "Cour d'appel"], value="Tous", label="Tribunal", ) code_filter = gr.Dropdown( choices=["Tous"] + _code_names, value="Tous", label="Code juridique", ) min_filter = gr.Dropdown( choices=["Tous"] + _ministeres, value="Tous", label="Ministere", ) # ── Results ── gr.HTML('
') synthesis_out = gr.HTML(label="Synthese") results_out = gr.HTML(label="Resultats") # ── Event handlers ── search_btn.click( fn=run_search, inputs=[query_box, source_selector, date_from, date_to, juris_filter, code_filter, min_filter], outputs=[synthesis_out, results_out], ) query_box.submit( fn=run_search, inputs=[query_box, source_selector, date_from, date_to, juris_filter, code_filter, min_filter], outputs=[synthesis_out, results_out], ) if __name__ == "__main__": demo.launch()