Add mobile overflow nav, law check feature, and favicon

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 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 17:57:29 +02:00
parent 5c5152a387
commit 2ef14ac765
6 changed files with 179 additions and 7 deletions
+24 -1
View File
@@ -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
+20 -1
View File
@@ -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():
+21
View File
@@ -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.