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
+62 -17
View File
@@ -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 (
<div className="space-y-5">
{blocks.map((block, i) => {
const trimmed = block.trim();
if (!trimmed) return null;
const m = trimmed.match(THEME_RE);
if (m) {
return (
<p key={i} className="text-sm md:text-base leading-[1.85] text-foreground">
<span className="font-serif font-semibold text-primary">{m[1]}</span>
{m[2]}
</p>
);
}
return (
<p key={i} className="text-sm md:text-base leading-[1.85] text-foreground">
{trimmed}
</p>
);
})}
</div>
);
}
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
const esc = (s: string) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
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 `<p><span class="theme">${esc(m[1])}</span>${esc(m[2])}</p>`;
return `<p>${esc(t)}</p>`;
})
.join("\n");
const html = `<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>La Voix du Peuple — Synthèse</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap');
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Inter', 'Segoe UI', sans-serif; max-width: 680px; margin: 48px auto; color: #1a1a2e; line-height: 1.75; padding: 0 24px; }
.tricolor { display: flex; height: 4px; width: 80px; margin-bottom: 32px; border-radius: 2px; overflow: hidden; }
body { font-family: Georgia, 'Times New Roman', serif; background: #F9F7F1; color: #1a1a1a; }
.page { max-width: 680px; 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: #EDEDED; }
.tricolor span:nth-child(2) { flex: 1; background: #E5E5E5; }
.tricolor span:nth-child(3) { flex: 1; background: #ED2939; }
h1 { font-size: 1.35rem; font-weight: 700; letter-spacing: -0.01em; margin-bottom: 4px; color: #1a1a2e; }
.meta { font-size: 0.72rem; color: #888; font-family: monospace; margin-bottom: 36px; }
.text { font-size: 1rem; white-space: pre-line; color: #1a1a2e; }
.footer { margin-top: 48px; padding-top: 16px; border-top: 1px solid #eee; font-size: 0.7rem; color: #bbb; font-family: monospace; display: flex; justify-content: space-between; }
@media print { body { margin: 32px auto; } }
.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.55rem; font-weight: 700; color: #111; line-height: 1.2; margin-bottom: 6px; }
.meta { font-family: 'Courier New', monospace; font-size: 0.68rem; color: #999; margin-bottom: 36px; padding-bottom: 24px; border-bottom: 1px solid #E8E4DC; }
.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.62rem; 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="page">
<div class="tricolor"><span></span><span></span><span></span></div>
<h1>La Voix du Peuple Synthèse citoyenne</h1>
<div class="meta">Générée le ${safe(date)} &nbsp;·&nbsp; ${nb} contribution${nb !== 1 ? "s" : ""} intégrée${nb !== 1 ? "s" : ""}</div>
<div class="text">${safe(synthesis.text)}</div>
<p class="label">La Voix du Peuple &middot; Synthèse citoyenne</p>
<h1>Synthèse des contributions</h1>
<div class="meta">Générée le ${esc(date)} &nbsp;&middot;&nbsp; ${nb} contribution${nb !== 1 ? "s" : ""} intégrée${nb !== 1 ? "s" : ""}</div>
<div class="body">${formattedText}</div>
<div class="footer">
<span>lavoixdupeuple.fr</span>
<span>Document généré automatiquement — modération selon DUDH / PIDCP / CEDH</span>
<span>Modération DUDH · PIDCP · CEDH · droit pénal français</span>
</div>
</div>
<script>window.print();</script>
</body>
</html>`;
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() {
</div>
) : synthesis?.text ? (
<div
className="animate-in fade-in slide-in-from-bottom-2 duration-700 ease-out text-sm md:text-base leading-relaxed text-foreground space-y-4 whitespace-pre-line"
className="animate-in fade-in slide-in-from-bottom-2 duration-700 ease-out"
data-testid="text-synthesis-content"
>
{synthesis.text}
<SynthesisText text={synthesis.text} />
</div>
) : synthesis ? (
<p className="text-muted-foreground italic text-sm py-16 text-center">