Improve synthesis display, PDF exports, and priority documentation

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-06-24 10:05:28 +02:00
parent 8f904f4cf0
commit 41556d3ea2
3 changed files with 138 additions and 45 deletions
+45 -23
View File
@@ -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", "<br>")
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'<p><span class="theme">{prefix}</span>{rest}</p>\n'
else:
body_html += f'<p>{html_module.escape(para)}</p>\n'
html_content = f"""<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>{title_esc}</title>
<style>
body {{ font-family: Georgia, serif; max-width: 800px; margin: 40px auto; padding: 0 20px; color: #1a1a1a; }}
.header {{ border-bottom: 3px solid #1b5f6a; padding-bottom: 16px; margin-bottom: 32px; }}
.tricolore {{ display: flex; height: 6px; width: 60px; margin-bottom: 12px; }}
.tricolore span {{ flex: 1; }}
h1 {{ font-size: 24px; margin: 0 0 6px; color: #1b5f6a; }}
.meta {{ font-size: 13px; color: #666; margin: 4px 0; }}
.synthesis {{ font-size: 15px; line-height: 1.8; }}
.footer {{ margin-top: 40px; border-top: 1px solid #ccc; padding-top: 12px; font-size: 11px; color: #999; }}
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
body {{ font-family: Georgia, 'Times New Roman', serif; background: #F9F7F1; color: #1a1a1a; }}
.page {{ max-width: 700px; margin: 0 auto; background: #fff; min-height: 100vh; padding: 52px 56px; }}
.tricolor {{ display: flex; height: 3px; margin-bottom: 40px; }}
.tricolor span:nth-child(1) {{ flex: 1; background: #002395; }}
.tricolor span:nth-child(2) {{ flex: 1; background: #E5E5E5; }}
.tricolor span:nth-child(3) {{ flex: 1; background: #ED2939; }}
.label {{ font-family: 'Courier New', monospace; font-size: 0.6rem; text-transform: uppercase; letter-spacing: 0.14em; color: #1b5f6a; margin-bottom: 10px; }}
h1 {{ font-size: 1.5rem; font-weight: 700; color: #111; line-height: 1.25; margin-bottom: 4px; }}
.organizer {{ font-size: 0.85rem; font-weight: 600; color: #1b5f6a; margin-top: 6px; }}
.subject {{ font-size: 0.85rem; color: #555; margin-top: 4px; font-style: italic; }}
.meta {{ font-family: 'Courier New', monospace; font-size: 0.68rem; color: #999; margin-top: 16px; padding-top: 16px; border-top: 1px solid #E8E4DC; margin-bottom: 36px; }}
.body {{ font-size: 0.95rem; line-height: 1.85; color: #1a1a1a; }}
.body p {{ margin-bottom: 1.1em; }}
.theme {{ font-family: Georgia, serif; font-weight: 600; color: #1b5f6a; }}
.footer {{ margin-top: 52px; padding-top: 14px; border-top: 1px solid #E8E4DC; font-family: 'Courier New', monospace; font-size: 0.6rem; color: #bbb; display: flex; justify-content: space-between; gap: 16px; }}
@media print {{ body {{ background: white; }} .page {{ box-shadow: none; padding: 32px 40px; }} }}
</style>
</head>
<body>
<div class="header">
<div class="tricolore">
<span style="background:#002395"></span>
<span style="background:#ededed"></span>
<span style="background:#ed2939"></span>
</div>
<div class="page">
<div class="tricolor"><span></span><span></span><span></span></div>
<p class="label">La Voix du Peuple &middot; Consultation citoyenne</p>
<h1>{title_esc}</h1>
{f'<p class="meta"><strong>{organizer_esc}</strong></p>' if organizer_esc else ""}
<p class="meta">{subject_esc}</p>
<p class="meta">Exporté le {now_str} {idea_count} contribution(s) intégrée(s)</p>
</div>
<div class="synthesis">{synthesis_esc}</div>
<div class="footer">
La Voix du Peuple — Document généré automatiquement à partir de contributions citoyennes.
Ce contenu reflète l'expression des participants, pas une vérité établie.
{f'<p class="organizer">{organizer_esc}</p>' if organizer_esc else ""}
<p class="subject">{subject_esc}</p>
<div class="meta">Exporté le {now_str} &nbsp;&middot;&nbsp; {idea_count} contribution(s) intégrée(s)</div>
<div class="body">{body_html}</div>
<div class="footer">
<span>lavoixdupeuple.fr</span>
<span>Modération DUDH &middot; PIDCP &middot; CEDH &middot; droit pénal français</span>
</div>
</div>
<script>window.print();</script>
</body>