Files
Momento/architectural-grid/src/components/RevisionView.tsx
Antigravity e2672cd2c2
Some checks failed
CI / Lint, Test & Build (push) Failing after 1m19s
CI / Deploy production (on server) (push) Has been skipped
feat(notes): liens internes, onglet Réseau, living blocks et consentement IA
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>
2026-05-24 14:27:29 +00:00

666 lines
30 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
};