feat(notes): liens internes, onglet Réseau, living blocks et consentement IA
Some checks failed
CI / Lint, Test & Build (push) Failing after 1m19s
CI / Deploy production (on server) (push) Has been skipped

Rend les liens entre notes visibles et persistants (sync NoteLink au save, auto-save, graphe réseau rafraîchi), ajoute living blocks, Memory Echo, recherche globale, consentement IA explicite et consolide les prototypes design en architectural-grid.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Antigravity
2026-05-24 14:27:29 +00:00
parent 077e665dfc
commit e2672cd2c2
323 changed files with 20670 additions and 42431 deletions

View File

@@ -0,0 +1,665 @@
import React, { useState, useEffect, useMemo } from 'react';
import { motion, AnimatePresence } from 'motion/react';
import {
GraduationCap,
Layers,
ArrowLeft,
ChevronLeft,
ChevronRight,
RotateCcw,
CheckCircle2,
X,
Inbox,
BookOpen,
Calendar,
Sparkles,
Award
} from 'lucide-react';
import { Note, Flashcard, FlashcardDeck, FlashcardEvaluation } from '../types';
interface RevisionViewProps {
notes: Note[];
flashcards: Flashcard[];
onUpdateFlashcards: (updated: Flashcard[]) => void;
onSelectNote: (noteId: string) => void;
onOpenSidebar?: () => void;
initialActiveDeckId?: string | null;
onClearActiveDeckId?: () => void;
}
export const RevisionView: React.FC<RevisionViewProps> = ({
notes,
flashcards,
onUpdateFlashcards,
onSelectNote,
onOpenSidebar,
initialActiveDeckId,
onClearActiveDeckId
}) => {
// Active states
const [activeDeckId, setActiveDeckId] = useState<string | null>(initialActiveDeckId || null);
const [isSessionActive, setIsSessionActive] = useState(false);
const [isSessionFinished, setIsSessionFinished] = useState(false);
// Active review states
const [currentCardIndex, setCurrentCardIndex] = useState(0);
const [isFlipped, setIsFlipped] = useState(false);
const [sessionCards, setSessionCards] = useState<Flashcard[]>([]);
const [sessionHistory, setSessionHistory] = useState<Record<string, FlashcardEvaluation>>({});
const [onlyFailedCardsSession, setOnlyFailedCardsSession] = useState(false);
// Sync initial deck selection from outer prop/reminder
useEffect(() => {
if (initialActiveDeckId) {
setActiveDeckId(initialActiveDeckId);
// Auto-trigger session
const deckCards = flashcards.filter(c => c.noteId === initialActiveDeckId);
if (deckCards.length > 0) {
setSessionCards([...deckCards]);
setCurrentCardIndex(0);
setIsFlipped(false);
setIsSessionActive(true);
setIsSessionFinished(false);
setSessionHistory({});
}
}
}, [initialActiveDeckId, flashcards]);
// Compute Decks based on current flashcards and notes
const decks = useMemo(() => {
const deckMap = new Map<string, Flashcard[]>();
flashcards.forEach(card => {
if (!deckMap.has(card.noteId)) {
deckMap.set(card.noteId, []);
}
deckMap.get(card.noteId)!.push(card);
});
const list: FlashcardDeck[] = [];
deckMap.forEach((cardsInDeck, noteId) => {
const parentNote = notes.find(n => n.id === noteId);
if (!parentNote || parentNote.isDeleted) return;
// Find min nextReviewDate
let minDate = cardsInDeck[0]?.nextReviewDate || new Date().toISOString();
cardsInDeck.forEach(c => {
if (c.nextReviewDate < minDate) {
minDate = c.nextReviewDate;
}
});
// Mastery score: portion of mastered/sure cards in last evaluation
const totalCards = cardsInDeck.length;
let masteredCount = 0;
cardsInDeck.forEach(c => {
if (c.mastered) masteredCount++;
});
list.push({
noteId,
title: parentNote.title,
cardsCount: totalCards,
nextReviewDate: minDate,
masteryScore: totalCards > 0 ? masteredCount / totalCards : 0,
cards: cardsInDeck
});
});
// Sort decks: first those that need review (past nextReviewDate), then alphabetical
const nowStr = new Date().toISOString();
return list.sort((a, b) => {
const aNeeds = a.nextReviewDate <= nowStr;
const bNeeds = b.nextReviewDate <= nowStr;
if (aNeeds && !bNeeds) return -1;
if (!aNeeds && bNeeds) return 1;
return a.title.localeCompare(b.title);
});
}, [flashcards, notes]);
const activeDeck = useMemo(() => {
return decks.find(d => d.noteId === activeDeckId);
}, [decks, activeDeckId]);
// Launch review session for a deck
const handleStartReview = (noteId: string, failedOnly = false) => {
const deck = decks.find(d => d.noteId === noteId);
if (!deck) return;
let cardsToReview = [...deck.cards];
if (failedOnly) {
// Filter for cards graded as 'fail' or 'hesitant' in session history, or simply subset of session cards
const failedIds = Object.keys(sessionHistory).filter(id => sessionHistory[id] === 'fail');
cardsToReview = deck.cards.filter(c => failedIds.includes(c.id));
if (cardsToReview.length === 0) {
// Fallback to active session's rated fail
cardsToReview = deck.cards.filter(c => sessionHistory[c.id] === 'fail');
}
setOnlyFailedCardsSession(true);
} else {
setOnlyFailedCardsSession(false);
}
if (cardsToReview.length === 0) return;
// Shuffle cards for better learning cognitive effect
const shuffled = [...cardsToReview].sort(() => Math.random() - 0.5);
setActiveDeckId(noteId);
setSessionCards(shuffled);
setCurrentCardIndex(0);
setIsFlipped(false);
setIsSessionActive(true);
setIsSessionFinished(false);
setSessionHistory({});
};
const handleCardFlip = () => {
setIsFlipped(!isFlipped);
};
// Keyboard support during review
useEffect(() => {
if (!isSessionActive || isSessionFinished) return;
const handleKeyDown = (e: KeyboardEvent) => {
if (e.code === 'Space') {
e.preventDefault();
handleCardFlip();
} else if (isFlipped) {
if (e.key === '1') {
handleEvaluate('fail');
} else if (e.key === '2') {
handleEvaluate('hesitant');
} else if (e.key === '3') {
handleEvaluate('sure');
}
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isSessionActive, isSessionFinished, isFlipped, currentCardIndex, sessionCards]);
// Simple Spaced Repetition Logic (Leitner system variation)
const handleEvaluate = (evaluation: FlashcardEvaluation) => {
const currentCard = sessionCards[currentCardIndex];
if (!currentCard) return;
// Record evaluation in session context
setSessionHistory(prev => ({
...prev,
[currentCard.id]: evaluation
}));
// Calculate new intervals
let interval = currentCard.intervalDays || 1;
let ease = currentCard.easeFactor || 2.5;
let mastered = currentCard.mastered || false;
if (evaluation === 'fail') {
interval = 1; // back to review tomorrow
ease = Math.max(1.3, ease - 0.2);
mastered = false;
} else if (evaluation === 'hesitant') {
interval = Math.max(2, Math.floor(interval * 1.2));
mastered = false;
} else { // sure
interval = Math.ceil(interval * ease);
ease = Math.min(3.5, ease + 0.15);
mastered = true;
}
// Calculate next review date
const nextDate = new Date();
nextDate.setDate(nextDate.getDate() + interval);
// Build historical entry
const historyItem = {
reviewedAt: new Date().toISOString(),
evaluation
};
const updatedCard: Flashcard = {
...currentCard,
intervalDays: interval,
nextReviewDate: nextDate.toISOString(),
easeFactor: ease,
mastered,
history: [...(currentCard.history || []), historyItem]
};
// Propagate up to global storage
const updatedGlobal = flashcards.map(c => c.id === currentCard.id ? updatedCard : c);
onUpdateFlashcards(updatedGlobal);
// Update in-place session cards to preserve intermediate updates
setSessionCards(prev => prev.map((c, i) => i === currentCardIndex ? updatedCard : c));
// Progress flow
if (currentCardIndex < sessionCards.length - 1) {
setTimeout(() => {
setCurrentCardIndex(prev => prev + 1);
setIsFlipped(false);
}, 300);
} else {
setTimeout(() => {
setIsSessionFinished(true);
}, 300);
}
};
const handleNext = () => {
if (currentCardIndex < sessionCards.length - 1) {
setCurrentCardIndex(currentCardIndex + 1);
setIsFlipped(false);
}
};
const handlePrev = () => {
if (currentCardIndex > 0) {
setCurrentCardIndex(currentCardIndex - 1);
setIsFlipped(false);
}
};
const handleExitSession = () => {
setIsSessionActive(false);
setIsSessionFinished(false);
onClearActiveDeckId?.();
};
// Statistics summaries
const finishedStats = useMemo(() => {
if (sessionCards.length === 0) return { sureCount: 0, hesitantCount: 0, failCount: 0, percentage: 0 };
let sureCount = 0;
let hesitantCount = 0;
let failCount = 0;
sessionCards.forEach(c => {
const evaluation = sessionHistory[c.id];
if (evaluation === 'sure') sureCount++;
else if (evaluation === 'hesitant') hesitantCount++;
else if (evaluation === 'fail') failCount++;
});
const totalRated = Object.keys(sessionHistory).length || 1;
const percentage = Math.round((sureCount / totalRated) * 100);
return {
sureCount,
hesitantCount,
failCount,
percentage
};
}, [sessionCards, sessionHistory]);
const formattingDate = (isoStr: string) => {
const diff = new Date(isoStr).getTime() - Date.now();
if (diff <= 0) return 'Dû aujourd\'hui';
const days = Math.ceil(diff / (24 * 60 * 60 * 1000));
return `Dans ${days}j`;
};
return (
<div className="h-full flex flex-col bg-white dark:bg-dark-paper overflow-y-auto w-full transition-colors duration-500">
{/* 1. Header Toolbar */}
<div className="px-6 sm:px-12 py-6 flex items-center justify-between sticky top-0 bg-white/90 dark:bg-dark-paper/90 backdrop-blur-sm z-40 border-b border-border gap-4">
<div className="flex items-center gap-4">
{onOpenSidebar && (
<button
onClick={onOpenSidebar}
className="lg:hidden p-2 -ml-2 text-ink dark:text-dark-ink hover:bg-black/5 rounded-lg transition-colors"
>
<ChevronLeft size={20} />
</button>
)}
{isSessionActive ? (
<button
onClick={handleExitSession}
className="flex items-center gap-2 text-concrete hover:text-ink dark:text-dark-concrete dark:hover:text-dark-ink transition-colors"
>
<ArrowLeft size={16} />
<span className="text-xs font-bold uppercase tracking-widest">Abandonner</span>
</button>
) : (
<div className="flex items-center gap-2.5">
<GraduationCap className="text-accent shrink-0" size={20} />
<h2 className="text-xs font-black tracking-widest uppercase text-ink dark:text-dark-ink">Focal de Révision</h2>
</div>
)}
</div>
{isSessionActive && activeDeck && (
<div className="text-[11px] font-mono bg-paper dark:bg-dark-paper text-concrete border border-border px-3 py-1.5 rounded-full lowercase tracking-wider">
deck : <span className="font-bold text-accent">{activeDeck.title}</span>
</div>
)}
</div>
{/* 2. Main Display Area */}
<div className="flex-1 flex flex-col items-center justify-center p-6 md:p-12 max-w-5xl mx-auto w-full">
<AnimatePresence mode="wait">
{/* SCREEN A: Decks Collection list view */}
{!isSessionActive && (
<motion.div
key="decks-list"
initial={{ opacity: 0, y: 15 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -15 }}
className="w-full space-y-10"
>
<div className="space-y-2">
<h1 className="text-4xl font-serif font-black text-ink dark:text-dark-ink">Decks de Révision Active</h1>
<p className="text-sm font-light text-muted-ink dark:text-dark-muted max-w-2xl">
Révisez vos connaissances de manière ciblée grâce au système d'espacement algorithmique Leitner. Lapprentissage actif commence ici.
</p>
</div>
{decks.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{decks.map(deck => {
const nowStr = new Date().toISOString();
const dueCount = deck.cards.filter(c => c.nextReviewDate <= nowStr).length;
return (
<div
key={deck.noteId}
id={`deck-card-${deck.noteId}`}
className="p-6 bg-[#FCFCFA] dark:bg-white/[0.02] border border-border/60 hover:border-accent/40 rounded-2xl flex flex-col justify-between gap-5 transition-all shadow-sm hover:shadow-xs relative group"
>
<div className="space-y-4">
<div className="flex justify-between items-start gap-3">
<div className="space-y-1 truncate">
<h3 className="text-lg font-serif font-semibold text-ink dark:text-dark-ink truncate group-hover:text-accent transition-colors">
{deck.title}
</h3>
<p className="text-xs text-concrete flex items-center gap-1">
<Layers size={12} />
<span>{deck.cardsCount} cartes de mémoire</span>
</p>
</div>
{/* Circular progress bar rendering */}
<div className="relative w-12 h-12 flex items-center justify-center shrink-0">
<svg className="w-full h-full transform -rotate-90">
<circle cx="24" cy="24" r="19" stroke="currentColor" strokeWidth="2" className="text-zinc-100 dark:text-zinc-800" fill="transparent" />
<circle cx="24" cy="24" r="19" stroke="currentColor" strokeWidth="3" className="text-sage" fill="transparent" strokeDasharray={2 * Math.PI * 19} strokeDashoffset={2 * Math.PI * 19 * (1 - deck.masteryScore)} />
</svg>
<span className="absolute text-[10px] font-mono font-black text-sage">
{Math.round(deck.masteryScore * 100)}%
</span>
</div>
</div>
<div className="flex flex-wrap gap-2 items-center text-[10.5px] font-medium text-concrete pt-1">
{dueCount > 0 ? (
<span className="bg-amber-500/10 text-amber-600 dark:text-amber-400 border border-amber-500/15 px-2.5 py-1 rounded-full flex items-center gap-1 font-bold animate-pulse">
{dueCount} à réviser
</span>
) : (
<span className="bg-sage/10 text-sage dark:text-sage border border-sage/15 px-2.5 py-1 rounded-full flex items-center gap-1 font-bold">
À jour
</span>
)}
<span className="bg-slate-500/5 dark:bg-white/5 border border-border px-2.5 py-1 rounded-full flex items-center gap-1 font-mono">
<Calendar size={10} />
Prochain : {formattingDate(deck.nextReviewDate)}
</span>
</div>
</div>
<div className="flex gap-2 pt-1 border-t border-border/40">
<button
onClick={() => onSelectNote(deck.noteId)}
className="flex-1 h-9 flex items-center justify-center text-[10.5px] uppercase tracking-wider font-bold text-muted-ink hover:text-ink dark:text-dark-muted dark:hover:text-dark-ink border border-border rounded-lg bg-white/50 dark:bg-transparent hover:bg-slate-50 dark:hover:bg-white/5 transition-colors"
>
Ouvrir note
</button>
<button
onClick={() => handleStartReview(deck.noteId)}
className="flex-1 h-9 flex items-center justify-center bg-accent text-white text-[10.5px] uppercase tracking-wider font-bold rounded-lg hover:opacity-90 transition-opacity flex items-center gap-1.5 shadow-sm shadow-accent/10"
>
<GraduationCap size={14} />
Réviser
</button>
</div>
</div>
);
})}
</div>
) : (
<div className="flex flex-col items-center justify-center text-center p-16 border border-dashed border-border/60 rounded-3xl bg-[#FAF9F6]/30 py-24">
<div className="w-16 h-16 rounded-2xl bg-accent/5 text-accent flex items-center justify-center mb-6">
<GraduationCap size={32} />
</div>
<h3 className="text-2xl font-serif font-black text-ink dark:text-dark-ink mb-2">Aucun deck de flashcards</h3>
<p className="text-sm text-concrete max-w-md font-light mb-8">
Démarrez votre apprentissage en générant des flashcards à l'aide de l'IA directement depuis la barre d'outils de vos notes architecturales.
</p>
<button
onClick={() => onSelectNote('n1')}
className="h-11 px-6 bg-ink dark:bg-ochre text-paper dark:text-ink rounded-xl text-xs font-bold uppercase tracking-widest hover:opacity-90 transition-all flex items-center gap-2"
>
<BookOpen size={15} />
Essayer sur la Note "Grid Systems"
</button>
</div>
)}
</motion.div>
)}
{/* SCREEN B: Active Deck session review state */}
{isSessionActive && !isSessionFinished && (
<motion.div
key="active-session"
initial={{ opacity: 0, scale: 0.98 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.98 }}
className="w-full max-w-2xl flex flex-col items-center gap-10"
>
{/* Header navigation bar */}
<div className="w-full flex items-center justify-between px-2 text-xs font-medium text-concrete">
<button
onClick={handlePrev}
disabled={currentCardIndex === 0}
className="flex items-center gap-1 hover:text-ink dark:hover:text-dark-ink disabled:opacity-30 transition-colors cursor-pointer"
>
<ChevronLeft size={16} />
<span>Précédent</span>
</button>
<div className="px-3.5 py-1.5 bg-slate-100 dark:bg-white/5 border border-border/40 text-[11px] font-mono tracking-widest font-bold rounded-full text-ink dark:text-dark-ink">
{currentCardIndex + 1} / {sessionCards.length}
</div>
<button
onClick={handleNext}
disabled={currentCardIndex === sessionCards.length - 1}
className="flex items-center gap-1 hover:text-ink dark:hover:text-dark-ink disabled:opacity-30 transition-colors cursor-pointer"
>
<span>Suivant</span>
<ChevronRight size={16} />
</button>
</div>
{/* Centered Flashcard */}
<div
id="flashcard-container"
onClick={handleCardFlip}
className="w-[480px] h-[280px] cursor-pointer select-none perspective group"
>
<div className={`relative w-full h-full transition-transform duration-500 transform-style preserve-3d ${isFlipped ? 'rotate-y-180' : ''}`}>
{/* RECTO - Front */}
<div className="absolute inset-0 w-full h-full backface-hidden bg-[#FAF9F5] dark:bg-slate-900 border border-border hover:border-accent/40 rounded-2xl p-8 flex flex-col justify-between shadow-md transition-colors">
<div className="flex justify-between items-start text-[10px] font-mono text-concrete/75 uppercase tracking-widest">
<span>Recto : Question</span>
<span className="bg-slate-200/50 dark:bg-white/10 px-2 py-0.5 rounded text-[8.5px]">Cliquer pour tourner</span>
</div>
<div className="flex-1 flex items-center justify-center p-2 text-center">
<p className="text-xl font-serif font-black text-ink dark:text-dark-ink leading-relaxed">
{sessionCards[currentCardIndex]?.question}
</p>
</div>
<div className="text-[10px] text-center text-concrete italic font-light pt-2 shrink-0 border-t border-border/10">
Raccourci : [Espace] pour révéler la réponse
</div>
</div>
{/* VERSO - Back */}
<div className="absolute inset-0 w-full h-full backface-hidden rotate-y-180 bg-white dark:bg-paper dark:text-ink border border-border rounded-2xl p-8 flex flex-col justify-between shadow-xl">
<div className="flex justify-between items-start text-[10px] font-mono text-concrete/75 uppercase tracking-widest">
<span className="text-accent font-bold">Verso : Réponse</span>
<span className="bg-accent/10 px-2 py-0.5 rounded text-[8.5px] text-accent">Duo Mémoire</span>
</div>
<div className="flex-1 flex items-center justify-center p-2 text-center overflow-y-auto max-h-[160px] custom-scrollbar">
<p className="text-sm font-light text-ink leading-relaxed">
{sessionCards[currentCardIndex]?.answer}
</p>
</div>
<div className="text-[10px] text-center text-concrete/60 italic font-light pt-2 shrink-0 border-t border-border/15">
Raccourcis : [1] Raté, [2] Hésitant, [3] Sûr
</div>
</div>
</div>
</div>
{/* Grading Buttons - Rendered after Verso is revealed */}
<div className="h-16 flex items-center justify-center w-full">
<AnimatePresence mode="wait">
{isFlipped ? (
<motion.div
key="grading-expanded"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="flex items-center gap-4 w-full justify-center max-w-md"
>
<button
id="grade-btn-fail"
onClick={(e) => { e.stopPropagation(); handleEvaluate('fail'); }}
className="flex-1 flex flex-col items-center justify-center h-14 rounded-2xl border border-rust/10 bg-rust/10 font-black text-rust cursor-pointer hover:bg-rust/15 transition-all text-xs"
>
<span className="text-sm">Raté</span>
<span className="opacity-70 text-[9px] font-light font-mono mt-0.5">Touche 1</span>
</button>
<button
id="grade-btn-hesitant"
onClick={(e) => { e.stopPropagation(); handleEvaluate('hesitant'); }}
className="flex-1 flex flex-col items-center justify-center h-14 rounded-2xl border border-ochre/15 bg-ochre/10 font-black text-ochre cursor-pointer hover:bg-ochre/15 transition-all text-xs"
>
<span className="text-sm">Hésitant</span>
<span className="opacity-70 text-[9px] font-light font-mono mt-0.5">Touche 2</span>
</button>
<button
id="grade-btn-sure"
onClick={(e) => { e.stopPropagation(); handleEvaluate('sure'); }}
className="flex-1 flex flex-col items-center justify-center h-14 rounded-2xl border border-sage/15 bg-sage/10 font-black text-sage cursor-pointer hover:bg-sage/15 transition-all text-xs"
>
<span className="text-sm">Sûr</span>
<span className="opacity-70 text-[9px] font-light font-mono mt-0.5">Touche 3</span>
</button>
</motion.div>
) : (
<motion.button
key="reveal-btn"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={handleCardFlip}
className="h-12 px-8 bg-ink dark:bg-dark-ink text-paper dark:text-dark-paper text-xs uppercase font-bold tracking-widest rounded-xl hover:opacity-90 active:scale-98 transition-all shadow-md shrink-0 cursor-pointer"
>
Révéler la réponse (Espace)
</motion.button>
)}
</AnimatePresence>
</div>
</motion.div>
)}
{/* SCREEN C: Finishing dashboard view with Donut Chart and actions */}
{isSessionFinished && (
<motion.div
key="finished-stats"
initial={{ opacity: 0, scale: 0.96 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.96 }}
className="w-full max-w-lg flex flex-col items-center text-center gap-8"
>
<div className="space-y-2">
<div className="w-12 h-12 rounded-full bg-sage/10 text-sage flex items-center justify-center mx-auto mb-3">
<Award size={26} />
</div>
<h1 className="text-3xl font-serif font-black text-ink dark:text-dark-ink">Félicitations !</h1>
<p className="text-sm font-light text-muted-ink dark:text-dark-muted">
Vous venez de finir votre session de révision de la note active.
</p>
</div>
{/* Custom SVG Donut Chart showing score */}
<div className="relative w-44 h-44 flex items-center justify-center my-2">
<svg className="w-full h-full transform -rotate-90">
<circle cx="88" cy="88" r="64" stroke="currentColor" strokeWidth="12" className="text-zinc-100 dark:text-zinc-900" fill="transparent" />
<circle cx="88" cy="88" r="64" stroke="currentColor" strokeWidth="12" className="text-sage" fill="transparent" strokeDasharray={2 * Math.PI * 64} strokeDashoffset={2 * Math.PI * 64 * (1 - (finishedStats.percentage / 100))} strokeLinecap="round" />
</svg>
<div className="absolute inset-0 flex flex-col items-center justify-center">
<span className="text-4.5xl font-serif font-black text-ink dark:text-dark-ink leading-none">
{finishedStats.percentage}%
</span>
<p className="text-[10px] uppercase font-bold tracking-widest text-concrete mt-1">Sûr de soi</p>
</div>
</div>
{/* Core Analytics parameters (Stats) */}
<div className="grid grid-cols-3 gap-4 w-full border-t border-b border-border py-6 select-none bg-[#FCFCFA] dark:bg-white/[0.01] rounded-2xl px-6">
<div className="space-y-1">
<p className="text-2xl font-serif font-bold text-ink dark:text-dark-ink">{sessionCards.length}</p>
<p className="text-[10px] uppercase tracking-wide font-medium text-concrete">Révisées</p>
</div>
<div className="space-y-1 border-l border-r border-border/60">
<p className="text-2xl font-serif font-bold text-rust">{finishedStats.failCount}</p>
<p className="text-[10px] uppercase tracking-wide font-medium text-concrete">À revoir</p>
</div>
<div className="space-y-1">
<p className="text-2xl font-serif font-bold text-sage">{finishedStats.sureCount}</p>
<p className="text-[10px] uppercase tracking-wide font-medium text-concrete">Maîtrisées</p>
</div>
</div>
<div className="flex flex-col sm:flex-row gap-3 w-full">
<button
onClick={handleExitSession}
className="flex-1 h-11 border border-border text-ink dark:text-dark-ink rounded-xl text-xs font-bold uppercase tracking-widest hover:bg-slate-50 dark:hover:bg-white/5 transition-all cursor-pointer"
>
Retour aux decks
</button>
{finishedStats.failCount > 0 && (
<button
onClick={() => handleStartReview(activeDeckId!, true)}
className="flex-1 h-11 bg-[#8F4C38] text-white rounded-xl text-xs font-bold uppercase tracking-widest hover:bg-[#8F4C38]/95 transition-all flex items-center justify-center gap-1.5 shadow-sm cursor-pointer"
>
<RotateCcw size={14} />
-réviser les ratées
</button>
)}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
);
};