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:
@@ -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, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
const esc = (s: string) => s.replace(/&/g, "&").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 `<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)} · ${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 · Synthèse citoyenne</p>
|
||||
<h1>Synthèse des contributions</h1>
|
||||
<div class="meta">Générée le ${esc(date)} · ${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">
|
||||
|
||||
@@ -97,11 +97,37 @@ export default function Transparence() {
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<p className="text-foreground/90 text-sm font-semibold">Articles surveillés</p>
|
||||
<p className="text-foreground/80 text-xs leading-relaxed">
|
||||
Les articles de priorité <strong>critique</strong> et <strong>haute</strong> sont injectés dans chaque prompt.
|
||||
Les articles <strong>moyenne</strong> sont suivis pour détecter les changements législatifs mais ne sont pas injectés
|
||||
(le cadre légal statique les couvre déjà).
|
||||
</p>
|
||||
<div className="text-foreground/80 text-xs leading-relaxed space-y-2">
|
||||
<p>
|
||||
La priorité détermine deux choses indépendantes : <em>est-ce que le texte est injecté dans le prompt ?</em>
|
||||
et <em>à quelle fréquence le surveille-t-on ?</em>
|
||||
</p>
|
||||
<ul className="space-y-1.5 ml-3">
|
||||
<li>
|
||||
<span className="font-semibold text-red-600 dark:text-red-400">Critique</span> —
|
||||
texte injecté en temps réel <strong>et</strong> 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.
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-semibold text-amber-600 dark:text-amber-400">Haute</span> —
|
||||
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.
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-semibold text-muted-foreground">Moyenne</span> —
|
||||
<strong>non injecté</strong>. 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.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs border-collapse mt-2">
|
||||
<thead>
|
||||
|
||||
Reference in New Issue
Block a user