From 41556d3ea232e81ecae5114764111e8e3dec29b1 Mon Sep 17 00:00:00 2001 From: billisdead Date: Wed, 24 Jun 2026 10:05:28 +0200 Subject: [PATCH] Improve synthesis display, PDF exports, and priority documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - home.tsx: add SynthesisText component — detects "Sur X : / Concernant X :" thematic prefixes and renders them in serif/teal, replaces raw whitespace-pre-line - home.tsx: redesign client-side PDF — site color palette (#F9F7F1, #1b5f6a), Georgia serif typography, thematic prefix highlighting, tricolor stripe - app.py: redesign consultation print export — same visual identity, server-side paragraph parsing with theme span injection before HTML-escaping - transparence.tsx: rewrite priority explanation to be explicit about what "critique/haute/moyenne" means in practice (injection vs monitoring, why) Co-Authored-By: Claude Sonnet 4.6 --- artifacts/flask-api/app.py | 68 ++++++++++------ artifacts/voix-du-peuple/src/pages/home.tsx | 79 +++++++++++++++---- .../voix-du-peuple/src/pages/transparence.tsx | 36 +++++++-- 3 files changed, 138 insertions(+), 45 deletions(-) diff --git a/artifacts/flask-api/app.py b/artifacts/flask-api/app.py index 9c2201c..b3da851 100644 --- a/artifacts/flask-api/app.py +++ b/artifacts/flask-api/app.py @@ -907,41 +907,63 @@ def export_consultation_print(slug: str): title_esc = html_module.escape(consultation["title"]) subject_esc = html_module.escape(consultation["subject"]) organizer_esc = html_module.escape(consultation.get("organizer_name") or "") - synthesis_esc = html_module.escape(synthesis_text).replace("\n", "
") now_str = dt.datetime.now(dt.timezone.utc).strftime("%d/%m/%Y %H:%M") + # Mise en forme du texte : "Sur X :" / "Concernant X :" en gras teal + import re as _re + _THEME_RE = _re.compile( + r'^((?:Sur|Concernant|À propos d[eu]?|En ce qui concerne|Quant à)[^:]{0,100}:\s?)([\s\S]*)$', + _re.IGNORECASE, + ) + paragraphs = [p.strip() for p in synthesis_text.split("\n\n") if p.strip()] + body_html = "" + for para in paragraphs: + m = _THEME_RE.match(para) + if m: + prefix = html_module.escape(m.group(1)) + rest = html_module.escape(m.group(2)) + body_html += f'

{prefix}{rest}

\n' + else: + body_html += f'

{html_module.escape(para)}

\n' + html_content = f""" {title_esc} -
-
- - - -
+
+
+

La Voix du Peuple · Consultation citoyenne

{title_esc}

- {f'

{organizer_esc}

' if organizer_esc else ""} -

{subject_esc}

-

Exporté le {now_str} — {idea_count} contribution(s) intégrée(s)

-
-
{synthesis_esc}
- diff --git a/artifacts/voix-du-peuple/src/pages/home.tsx b/artifacts/voix-du-peuple/src/pages/home.tsx index 1b1faf6..97e91c3 100644 --- a/artifacts/voix-du-peuple/src/pages/home.tsx +++ b/artifacts/voix-du-peuple/src/pages/home.tsx @@ -82,6 +82,34 @@ const VALEURS = [ // Clé hCaptcha — activée si la variable d'environnement est définie const HCAPTCHA_SITE_KEY = import.meta.env.VITE_HCAPTCHA_SITE_KEY as string | undefined; +// Détecte "Sur X :" / "Concernant Y :" en début de bloc et met le préfixe en valeur +function SynthesisText({ text }: { text: string }) { + const THEME_RE = /^((?:Sur|Concernant|À propos d[eu]?|En ce qui concerne|Quant à)[^:]{0,100}:\s?)([\s\S]*)$/i; + const blocks = text.split(/\n{2,}/); + return ( +
+ {blocks.map((block, i) => { + const trimmed = block.trim(); + if (!trimmed) return null; + const m = trimmed.match(THEME_RE); + if (m) { + return ( +

+ {m[1]} + {m[2]} +

+ ); + } + return ( +

+ {trimmed} +

+ ); + })} +
+ ); +} + const API_BASE = import.meta.env.VITE_API_URL ?? ""; export default function Home() { @@ -246,43 +274,60 @@ export default function Home() { if (!synthesis?.text) return; const date = format(new Date(), "d MMMM yyyy 'à' HH:mm", { locale: fr }); const nb = synthesis.ideaCount ?? 0; - const safe = (s: string) => s.replace(/&/g, "&").replace(//g, ">"); + const esc = (s: string) => s.replace(/&/g, "&").replace(//g, ">"); + const THEME_RE = /^((?:Sur|Concernant|À propos d[eu]?|En ce qui concerne|Quant à)[^:]{0,100}:\s?)([\s\S]*)$/i; + const formattedText = synthesis.text + .split(/\n{2,}/) + .filter(Boolean) + .map((block) => { + const t = block.trim(); + const m = t.match(THEME_RE); + if (m) return `

${esc(m[1])}${esc(m[2])}

`; + return `

${esc(t)}

`; + }) + .join("\n"); const html = ` La Voix du Peuple — Synthèse +
-

La Voix du Peuple — Synthèse citoyenne

-
Générée le ${safe(date)}  ·  ${nb} contribution${nb !== 1 ? "s" : ""} intégrée${nb !== 1 ? "s" : ""}
-
${safe(synthesis.text)}
+

La Voix du Peuple · Synthèse citoyenne

+

Synthèse des contributions

+
Générée le ${esc(date)}  ·  ${nb} contribution${nb !== 1 ? "s" : ""} intégrée${nb !== 1 ? "s" : ""}
+
${formattedText}
+
+ `; const w = window.open("", "_blank"); if (w) { w.document.write(html); w.document.close(); - w.onload = () => w.print(); } }; @@ -682,10 +727,10 @@ export default function Home() {
) : synthesis?.text ? (
- {synthesis.text} +
) : synthesis ? (

diff --git a/artifacts/voix-du-peuple/src/pages/transparence.tsx b/artifacts/voix-du-peuple/src/pages/transparence.tsx index 2a3e907..81a82ac 100644 --- a/artifacts/voix-du-peuple/src/pages/transparence.tsx +++ b/artifacts/voix-du-peuple/src/pages/transparence.tsx @@ -97,11 +97,37 @@ export default function Transparence() {

Articles surveillés

-

- Les articles de priorité critique et haute sont injectés dans chaque prompt. - Les articles moyenne sont suivis pour détecter les changements législatifs mais ne sont pas injectés - (le cadre légal statique les couvre déjà). -

+
+

+ La priorité détermine deux choses indépendantes : est-ce que le texte est injecté dans le prompt ? + et à quelle fréquence le surveille-t-on ? +

+
    +
  • + Critique — + texte injecté en temps réel et suivi en priorité. + Ces articles sont amendés fréquemment (ex. : Art. 225-1 sur la discrimination a été élargi plusieurs fois + depuis 2021). Sans injection live, le modèle risque de travailler sur une liste de critères obsolète + et de manquer une infraction récemment ajoutée. +
  • +
  • + Haute — + texte injecté en temps réel. Le libellé exact compte pour une décision correcte + (ex. : les seuils de peines, les conditions d'aggravation, la définition précise de l'acte incriminé). + Ces articles sont plus stables, mais leur formulation spécifique ne peut pas être laissée à la mémoire d'entraînement. +
  • +
  • + Moyenne — + non injecté. Le cadre légal statique du prompt couvre déjà ces articles + (textes stables depuis des décennies : diffamation depuis 1944, crimes contre l'humanité depuis 1994). + Ils sont quand même surveillés pour détecter toute modification législative surprise. +
  • +
+

+ Dans tous les cas, si l'API Légifrance est indisponible au moment de l'analyse, le filtrage continue + sans interruption sur le cadre statique — sans dégradation visible pour l'utilisateur. +

+