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>
164 lines
6.6 KiB
TypeScript
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>
|
|
)
|
|
}
|