From 2ef14ac765cc168778f954b743e537d894aadaa8 Mon Sep 17 00:00:00 2001 From: billisdead Date: Tue, 23 Jun 2026 17:57:29 +0200 Subject: [PATCH] Add mobile overflow nav, law check feature, and favicon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nav: "Fonctionnement" and "Flyer QR" restored on mobile via a ··· overflow dropdown (click-outside closes); other links always visible Law check: new POST /api/check-law endpoint (5/min, 20/h) calls mistral-small with a new LAW_CHECK_PROMPT to detect if a proposal is already covered by French or EU law. Informational only, non-blocking. Frontend: "Vérifier le cadre légal existant" button below textarea in home.tsx; result displayed with Scale icon; resets on content change. Favicon: replaced placeholder red square with a petrol rounded square containing a serif "V" (for Voix) in warm cream — matches brand palette Co-Authored-By: Claude Sonnet 4.6 --- artifacts/flask-api/ai_agent.py | 25 +++++++++- artifacts/flask-api/app.py | 21 +++++++- artifacts/flask-api/legal_framework.py | 21 ++++++++ artifacts/voix-du-peuple/public/favicon.svg | 16 ++++++- artifacts/voix-du-peuple/src/App.tsx | 50 ++++++++++++++++++- artifacts/voix-du-peuple/src/pages/home.tsx | 53 ++++++++++++++++++++- 6 files changed, 179 insertions(+), 7 deletions(-) diff --git a/artifacts/flask-api/ai_agent.py b/artifacts/flask-api/ai_agent.py index 35abeba..5412d01 100644 --- a/artifacts/flask-api/ai_agent.py +++ b/artifacts/flask-api/ai_agent.py @@ -9,7 +9,7 @@ import json import os import logging from openai import OpenAI, BadRequestError -from legal_framework import LEGAL_FILTER_PROMPT, SYNTHESIS_PROMPT +from legal_framework import LEGAL_FILTER_PROMPT, SYNTHESIS_PROMPT, LAW_CHECK_PROMPT logger = logging.getLogger(__name__) @@ -107,6 +107,29 @@ def filter_idea(content: str) -> dict: return {"accepted": False, "reason": "Service temporairement indisponible"} +def check_existing_law(content: str) -> str: + """ + Vérifie si une proposition citoyenne est déjà couverte par une loi + française ou un règlement européen en vigueur. + Retourne une phrase informative (non bloquante). + """ + try: + client = get_client() + model = os.environ.get("FILTER_MODEL", os.environ.get("OPENAI_FILTER_MODEL", "mistral-small-latest")) + response = client.chat.completions.create( + model=model, + max_tokens=200, + messages=[ + {"role": "system", "content": LAW_CHECK_PROMPT}, + {"role": "user", "content": f'Proposition : "{content}"'}, + ], + ) + return (response.choices[0].message.content or "").strip() + except Exception: + logger.exception("Erreur lors de la vérification du cadre légal existant") + return "" + + def synthesize_ideas(ideas: list[str]) -> str: """ Synthétise une liste d'idées acceptées en un texte collectif diff --git a/artifacts/flask-api/app.py b/artifacts/flask-api/app.py index ab302b8..9c2201c 100644 --- a/artifacts/flask-api/app.py +++ b/artifacts/flask-api/app.py @@ -52,7 +52,7 @@ from database import ( get_consultation_contributions, delete_consultation, check_ip_blacklist, record_ip_rejection, get_ip_blacklist, remove_ip_blacklist, ) -from ai_agent import filter_idea, synthesize_ideas +from ai_agent import filter_idea, synthesize_ideas, check_existing_law # ─── Logging ──────────────────────────────────────────────────────────────── @@ -1216,6 +1216,25 @@ def admin_delete_consultation(slug: str): return jsonify({"ok": True}) +@app.post("/api/check-law") +@limiter.limit("5 per minute;20 per hour") +def check_law_route(): + """ + Vérifie si une proposition est déjà couverte par une loi française ou européenne. + Endpoint public, rate-limité. Non bloquant — retourne une note informative. + """ + if not request.is_json: + return jsonify({"error": "bad_request", "message": "Content-Type doit être application/json."}), 400 + data = request.get_json(silent=True) or {} + content = sanitize_text(str(data.get("content", ""))).strip() + if not content or len(content) < 10: + return jsonify({"error": "validation_error", "message": "Contenu trop court."}), 400 + if len(content) > 1000: + return jsonify({"error": "validation_error", "message": "Contenu trop long (max 1000 caractères)."}), 400 + note = check_existing_law(content) + return jsonify({"note": note}) + + @app.get("/api/admin/ip-blacklist") @require_admin def admin_list_ip_blacklist(): diff --git a/artifacts/flask-api/legal_framework.py b/artifacts/flask-api/legal_framework.py index c81960f..8ecbde4 100644 --- a/artifacts/flask-api/legal_framework.py +++ b/artifacts/flask-api/legal_framework.py @@ -459,6 +459,27 @@ Si rejetée : {"accepted": false, "reason": "Explication courte en français avec référence légale précise (ex: contraire à DUDH Art. 20 — incitation à la haine raciale)", "legal_basis": "DUDH Art. 20, PIDCP Art. 20"} """ +LAW_CHECK_PROMPT = """ +Tu es un assistant juridique spécialisé en droit français et en droit de l'Union européenne. + +Ta mission : analyser une proposition citoyenne et déterminer si elle est déjà couverte, +en tout ou en partie, par une loi française ou un règlement européen en vigueur. + +INSTRUCTIONS +- Si un texte législatif ou réglementaire couvre déjà cette proposition : + • Cite le texte précis : nom complet, numéro ou date, article le cas échéant. + • Résume en une phrase ce que ce texte prévoit concrètement. + • Si le texte manque de décrets d'application ou n'est que partiellement appliqué, dis-le + clairement — car une loi non appliquée ne rend pas la revendication caduque. +- Si la proposition n'est couverte par aucun texte en vigueur, réponds simplement : + "Aucun texte législatif en vigueur ne couvre cette proposition." +- Ne te prononce pas sur la valeur ou l'opportunité de la proposition. +- Ne réponds pas avec des textes abrogés ou non ratifiés. + +TON : neutre, factuel, concis. Pas de formule introductive. +FORMAT : 2 à 4 phrases maximum. Pas de markdown, pas de tirets, pas de liste. +""" + SYNTHESIS_PROMPT = """ Tu es un outil de résumé de contributions citoyennes. Tu n'exprimes aucune opinion. Tu restitues fidèlement ce qui a été soumis, sans l'édulcorer ni l'amplifier. diff --git a/artifacts/voix-du-peuple/public/favicon.svg b/artifacts/voix-du-peuple/public/favicon.svg index 4373d3c..e2e7c9f 100644 --- a/artifacts/voix-du-peuple/public/favicon.svg +++ b/artifacts/voix-du-peuple/public/favicon.svg @@ -1,3 +1,15 @@ - - + + + + + V diff --git a/artifacts/voix-du-peuple/src/App.tsx b/artifacts/voix-du-peuple/src/App.tsx index fb0f1e9..27e7f74 100644 --- a/artifacts/voix-du-peuple/src/App.tsx +++ b/artifacts/voix-du-peuple/src/App.tsx @@ -30,17 +30,63 @@ const queryClient = new QueryClient({ }); function Navbar() { + const [mobileMenuOpen, setMobileMenuOpen] = React.useState(false); + const menuRef = React.useRef(null); + + React.useEffect(() => { + if (!mobileMenuOpen) return; + const handler = (e: MouseEvent) => { + if (menuRef.current && !menuRef.current.contains(e.target as Node)) { + setMobileMenuOpen(false); + } + }; + document.addEventListener("mousedown", handler); + return () => document.removeEventListener("mousedown", handler); + }, [mobileMenuOpen]); + return (
- + La Voix du Peuple -