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:
billisdeadandClaude Sonnet 4.6 committed 2026-06-23 17:57:29 +02:00
1 parent 5c5152a387
commit 2ef14ac765
6 files changed
+179 -7

No files matched your search

+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.