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 = ({ notes, flashcards, onUpdateFlashcards, onSelectNote, onOpenSidebar, initialActiveDeckId, onClearActiveDeckId }) => { // Active states const [activeDeckId, setActiveDeckId] = useState(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([]); const [sessionHistory, setSessionHistory] = useState>({}); 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(); 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 (
{/* 1. Header Toolbar */}
{onOpenSidebar && ( )} {isSessionActive ? ( ) : (

Focal de Révision

)}
{isSessionActive && activeDeck && (
deck : {activeDeck.title}
)}
{/* 2. Main Display Area */}
{/* SCREEN A: Decks Collection list view */} {!isSessionActive && (

Decks de Révision Active

Révisez vos connaissances de manière ciblée grâce au système d'espacement algorithmique Leitner. L’apprentissage actif commence ici.

{decks.length > 0 ? (
{decks.map(deck => { const nowStr = new Date().toISOString(); const dueCount = deck.cards.filter(c => c.nextReviewDate <= nowStr).length; return (

{deck.title}

{deck.cardsCount} cartes de mémoire

{/* Circular progress bar rendering */}
{Math.round(deck.masteryScore * 100)}%
{dueCount > 0 ? ( {dueCount} à réviser ) : ( À jour )} Prochain : {formattingDate(deck.nextReviewDate)}
); })}
) : (

Aucun deck de flashcards

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.

)}
)} {/* SCREEN B: Active Deck session review state */} {isSessionActive && !isSessionFinished && ( {/* Header navigation bar */}
{currentCardIndex + 1} / {sessionCards.length}
{/* Centered Flashcard */}
{/* RECTO - Front */}
Recto : Question Cliquer pour tourner

{sessionCards[currentCardIndex]?.question}

Raccourci : [Espace] pour révéler la réponse
{/* VERSO - Back */}
Verso : Réponse Duo Mémoire

{sessionCards[currentCardIndex]?.answer}

Raccourcis : [1] Raté, [2] Hésitant, [3] Sûr
{/* Grading Buttons - Rendered after Verso is revealed */}
{isFlipped ? ( ) : ( Révéler la réponse (Espace) )}
)} {/* SCREEN C: Finishing dashboard view with Donut Chart and actions */} {isSessionFinished && (

Félicitations !

Vous venez de finir votre session de révision de la note active.

{/* Custom SVG Donut Chart showing score */}
{finishedStats.percentage}%

Sûr de soi

{/* Core Analytics parameters (Stats) */}

{sessionCards.length}

Révisées

{finishedStats.failCount}

À revoir

{finishedStats.sureCount}

Maîtrisées

{finishedStats.failCount > 0 && ( )}
)}
); };