Files
Momento/memento-note/components/legal/ai-consent-modal.tsx
Antigravity 0784c94242
Some checks failed
CI / Lint, Test & Build (push) Failing after 57s
CI / Deploy production (on server) (push) Has been skipped
feat(notes): vues structurées tableau/kanban, flashcards et MCP robuste
Ajoute la base organisable par carnet (schéma, champs partagés, valeurs par note)
avec activation guidée, tableau éditable, kanban et suppression de colonnes.
Corrige le multiselect en vue tableau et enrichit sidebar, grille et i18n FR/EN.
Inclut aussi les améliorations flashcards SM-2, l'audit consentement IA et la
robustesse du serveur MCP (config, validation, rate-limit, métriques).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-24 23:03:16 +00:00

164 lines
6.6 KiB
TypeScript

'use client'
import { useEffect, useState } from 'react'
import { createPortal } from 'react-dom'
import { Sparkles, X, ShieldAlert, Check } from 'lucide-react'
import { useLanguage } from '@/lib/i18n'
import { motion, AnimatePresence } from 'motion/react'
import { cn } from '@/lib/utils'
interface AiConsentModalProps {
open: boolean
onClose: () => void
onConfirm: (remember: boolean) => void
}
export function AiConsentModal({ open, onClose, onConfirm }: AiConsentModalProps) {
const { t } = useLanguage()
const [remember, setRemember] = useState(true)
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
useEffect(() => {
if (!open) return
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose()
}
document.addEventListener('keydown', onKey)
return () => document.removeEventListener('keydown', onKey)
}, [open, onClose])
if (!mounted) return null
return createPortal(
<AnimatePresence>
{open && (
<div className="fixed inset-0 z-[200] flex items-center justify-center p-4 sm:p-6">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
/>
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 10 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 10 }}
transition={{ type: 'spring', duration: 0.4 }}
className={cn(
'relative w-full max-w-lg overflow-hidden rounded-2xl border border-border',
'bg-memento-paper dark:bg-background text-foreground shadow-2xl p-6',
'flex flex-col gap-5',
)}
onClick={(e) => e.stopPropagation()}
>
<button
type="button"
onClick={onClose}
className="absolute top-4 right-4 p-1.5 rounded-lg text-muted-foreground hover:text-foreground hover:bg-black/5 dark:hover:bg-white/5 transition-colors"
>
<X className="w-5 h-5" />
</button>
<div className="flex items-start gap-4">
<div className="p-3 bg-brand-accent/10 text-brand-accent rounded-xl">
<BrainIcon className="w-6 h-6" />
</div>
<div className="flex flex-col gap-1 pr-8">
<h3 className="text-lg font-semibold tracking-tight">
{t('consent.ai.modalTitle') || 'Consentement requis pour le traitement par IA'}
</h3>
<span className="text-xs uppercase tracking-wider text-muted-foreground font-medium">
{t('consent.ai.complianceBadge') || 'Conformité RGPD'}
</span>
</div>
</div>
<div className="text-sm leading-relaxed text-muted-foreground flex flex-col gap-3">
<p className="text-foreground/90">
{t('consent.ai.modalDescription') ||
"Pour analyser vos notes, PDFs ou sessions de remue-méninges, Memento transmet de manière sécurisée ces données à des API d'IA tierces (OpenAI, Gemini, DeepSeek). Nous appliquons une politique de rétention de données nulle. En acceptant, vous autorisez ce traitement."}
</p>
<div className="p-3 bg-black/[0.03] dark:bg-white/[0.04] border border-border rounded-xl text-xs flex gap-3 items-start">
<ShieldAlert className="w-4 h-4 text-brand-accent shrink-0 mt-0.5" />
<div className="flex flex-col gap-0.5">
<span className="font-semibold text-foreground">
{t('consent.ai.zeroRetentionTitle') || 'Zéro Rétention de Données'}
</span>
<span>
{t('consent.ai.zeroRetentionDesc') ||
'Toutes les requêtes sortantes incluent des indicateurs de non-apprentissage pour protéger votre propriété intellectuelle.'}
</span>
</div>
</div>
</div>
<label className="flex items-center gap-3 cursor-pointer select-none py-1">
<div className="relative">
<input
type="checkbox"
checked={remember}
onChange={(e) => setRemember(e.target.checked)}
className="sr-only"
/>
<div
className={cn(
'w-5 h-5 rounded border border-border transition-colors flex items-center justify-center',
remember ? 'bg-brand-accent border-brand-accent' : 'bg-transparent',
)}
>
{remember && <Check className="w-3.5 h-3.5 text-white stroke-[3px]" />}
</div>
</div>
<span className="text-xs font-medium text-foreground/80">
{t('consent.ai.rememberMe') || 'Se souvenir de mon choix (ne plus demander)'}
</span>
</label>
<div className="flex items-center justify-end gap-3 mt-1">
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-xs font-semibold rounded-lg border border-border hover:bg-black/[0.03] dark:hover:bg-white/[0.04] transition-colors bg-transparent text-foreground"
>
{t('consent.ai.rejectButton') || 'Refuser'}
</button>
<button
type="button"
onClick={() => onConfirm(remember)}
className="px-4 py-2 text-xs font-semibold rounded-lg text-white shadow-sm transition-opacity hover:opacity-90 bg-brand-accent flex items-center gap-2"
>
<Sparkles className="w-3.5 h-3.5" />
{t('consent.ai.acceptButton') || 'Autoriser et continuer'}
</button>
</div>
</motion.div>
</div>
)}
</AnimatePresence>,
document.body,
)
}
function BrainIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
{...props}
>
<path d="M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96-.44 2.5 2.5 0 0 1 0-3.12 3 3 0 0 1 0-3.88 2.5 2.5 0 0 1 0-3.12A2.5 2.5 0 0 1 9.5 2Z" />
<path d="M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96-.44 2.5 2.5 0 0 0 0-3.12 3 3 0 0 0 0-3.88 2.5 2.5 0 0 0 0-3.12A2.5 2.5 0 0 0 14.5 2Z" />
</svg>
)
}