import { useEffect, useMemo, useState } from 'preact/hooks'; import { MindforgeApiService, type FlashcardCard, type FlashcardRagCard, type FlashcardRagDashboardResponse, type FlashcardRagStatus, } from '../services/MindforgeApiService'; import { Button } from './Button'; import './SpacedReviewComponent.css'; interface RagStatusOption { status: FlashcardRagStatus; label: string; } const STATUS_OPTIONS: RagStatusOption[] = [ { status: 'Red', label: 'Vermelho' }, { status: 'Amber', label: 'Amarelo' }, { status: 'Green', label: 'Verde' }, { status: 'Grey', label: 'Cinza' }, ]; const STATUS_PRIORITY: Record = { Red: 0, Amber: 1, Green: 2, Grey: 3, }; function buildGroupKey(subject: string, subSubject: string) { return `${subject}::${subSubject}`; } function shuffleCards(cards: FlashcardCard[]) { const shuffled = [...cards]; for (let i = shuffled.length - 1; i > 0; i--) { const randomIndex = Math.floor(Math.random() * (i + 1)); [shuffled[i], shuffled[randomIndex]] = [shuffled[randomIndex], shuffled[i]]; } return shuffled; } function formatPercentage(value: number) { return `${value.toFixed(2).replace('.', ',')}%`; } function summaryText(activeCount: number, greenPercentage: number, attentionPercentage: number) { if (activeCount === 0) { return 'Sem revisoes avaliaveis'; } return `Verde ${formatPercentage(greenPercentage)} | Atencao ${formatPercentage(attentionPercentage)}`; } function formatPerformance(rate: number) { return `${Math.round(rate * 100)}%`; } function orderCardsForSession(cards: FlashcardCard[], ragByCardId: Map) { const buckets: FlashcardCard[][] = [[], [], [], []]; cards.forEach((card) => { const ragCard = ragByCardId.get(card.id); const status = ragCard?.ragStatus || 'Grey'; buckets[STATUS_PRIORITY[status]].push(card); }); return buckets.flatMap((bucket) => shuffleCards(bucket)); } export function SpacedReviewComponent() { const [dashboard, setDashboard] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [selectedStatuses, setSelectedStatuses] = useState(['Red', 'Amber']); const [selectedGroupKeys, setSelectedGroupKeys] = useState([]); const [startingSession, setStartingSession] = useState(false); const [sessionCards, setSessionCards] = useState([]); const [sessionSourceCards, setSessionSourceCards] = useState([]); const [currentIndex, setCurrentIndex] = useState(0); const [showAnswer, setShowAnswer] = useState(false); const [submittingAnswer, setSubmittingAnswer] = useState(false); const loadDashboard = async (preserveSelection: boolean) => { setLoading(true); setError(null); try { const response = await MindforgeApiService.getFlashcardRagStatus(); setDashboard(response); const allGroupKeys = response.subjects.flatMap((subjectGroup) => subjectGroup.subSubjects.map((subSubjectGroup) => buildGroupKey(subjectGroup.subject, subSubjectGroup.subSubject)), ); setSelectedGroupKeys((current) => { if (!preserveSelection || current.length === 0) { return allGroupKeys; } const available = new Set(allGroupKeys); const kept = current.filter((key) => available.has(key)); return kept.length > 0 ? kept : allGroupKeys; }); } catch (err: any) { setError(err?.message || 'Falha ao carregar status de revisao espacada.'); } finally { setLoading(false); } }; useEffect(() => { let cancelled = false; async function runLoad() { if (cancelled) { return; } await loadDashboard(false); } runLoad(); return () => { cancelled = true; }; }, []); const ragCards = useMemo(() => { if (!dashboard) { return []; } return dashboard.subjects.flatMap((subjectGroup) => subjectGroup.subSubjects.flatMap((subSubjectGroup) => subSubjectGroup.cards)); }, [dashboard]); const selectedRagCards = useMemo(() => { const selectedGroups = new Set(selectedGroupKeys); const selectedStatusSet = new Set(selectedStatuses); return ragCards.filter((card) => selectedGroups.has(buildGroupKey(card.subject, card.subSubject)) && selectedStatusSet.has(card.ragStatus)); }, [ragCards, selectedGroupKeys, selectedStatuses]); const sessionSourceById = useMemo(() => { return new Map(sessionSourceCards.map((card) => [card.cardId, card])); }, [sessionSourceCards]); const currentCard = sessionCards[currentIndex]; const currentCardMetadata = currentCard ? sessionSourceById.get(currentCard.id) : undefined; const progressPercent = sessionCards.length > 0 ? ((currentIndex + 1) / sessionCards.length) * 100 : 0; const toggleStatus = (status: FlashcardRagStatus) => { if (selectedStatuses.includes(status)) { setSelectedStatuses(selectedStatuses.filter((value) => value !== status)); return; } setSelectedStatuses([...selectedStatuses, status]); }; const toggleGroup = (groupKey: string) => { if (selectedGroupKeys.includes(groupKey)) { setSelectedGroupKeys(selectedGroupKeys.filter((key) => key !== groupKey)); return; } setSelectedGroupKeys([...selectedGroupKeys, groupKey]); }; const startSession = async () => { if (selectedStatuses.length === 0) { setError('Selecione ao menos um status para iniciar a revisao.'); return; } if (selectedGroupKeys.length === 0) { setError('Selecione ao menos uma submateria para iniciar a revisao.'); return; } if (selectedRagCards.length === 0) { setError('Nenhum card encontrado com os filtros selecionados.'); return; } setStartingSession(true); setError(null); try { const ragByCardId = new Map(selectedRagCards.map((card) => [card.cardId, card])); const libraryIds = Array.from(new Set(selectedRagCards.map((card) => card.libraryId))); const response = await MindforgeApiService.createFlashcardReviewSession({ libraryIds }); const selectedCardIds = new Set(selectedRagCards.map((card) => card.cardId)); const filteredCards = response.cards.filter((card) => selectedCardIds.has(card.id)); const orderedCards = orderCardsForSession(filteredCards, ragByCardId); if (orderedCards.length === 0) { setError('Os filtros selecionados nao retornaram cards para revisar.'); return; } setSessionSourceCards(selectedRagCards); setSessionCards(orderedCards); setCurrentIndex(0); setShowAnswer(false); } catch (err: any) { setError(err?.message || 'Falha ao iniciar revisao espacada.'); } finally { setStartingSession(false); } }; const endSession = () => { setSessionCards([]); setSessionSourceCards([]); setCurrentIndex(0); setShowAnswer(false); setSubmittingAnswer(false); void loadDashboard(true); }; const goToPrevious = () => { if (currentIndex === 0) { return; } setCurrentIndex(currentIndex - 1); setShowAnswer(false); }; const registerAnswer = async (correct: boolean) => { if (!currentCard) { return; } setSubmittingAnswer(true); setError(null); try { await MindforgeApiService.recordFlashcardReviewAnswer({ cardId: currentCard.id, correct, }); if (currentIndex >= sessionCards.length - 1) { endSession(); return; } setCurrentIndex(currentIndex + 1); setShowAnswer(false); } catch (err: any) { setError(err?.message || 'Falha ao registrar resposta da revisao.'); } finally { setSubmittingAnswer(false); } }; return (

Revisao espacada

Acompanhe o status RAG dos cards por materia e submateria.

{error &&
{error}
} {sessionCards.length === 0 && (
{loading &&

Carregando painel de revisao...

} {!loading && (!dashboard || dashboard.subjects.length === 0) && (

Nenhum flashcard encontrado para revisar.

)} {!loading && dashboard && dashboard.subjects.length > 0 && ( <>
{STATUS_OPTIONS.map((option) => ( ))}
{dashboard.subjects.map((subjectGroup) => (

{subjectGroup.subject}

{summaryText( subjectGroup.summary.activeCount, subjectGroup.summary.greenPercentage, subjectGroup.summary.attentionPercentage, )}

Verde: {subjectGroup.summary.greenCount} | Amarelo: {subjectGroup.summary.amberCount} {' '} | Vermelho: {subjectGroup.summary.redCount} | Cinza: {subjectGroup.summary.greyCount}
{subjectGroup.subSubjects.map((subSubjectGroup) => { const groupKey = buildGroupKey(subjectGroup.subject, subSubjectGroup.subSubject); const checked = selectedGroupKeys.includes(groupKey); return ( ); })}
))}
)}

Cards selecionados: {selectedRagCards.length}

)} {sessionCards.length > 0 && currentCard && (
{currentIndex + 1} / {sessionCards.length}
{currentCardMetadata?.fileName || 'Arquivo'} - {currentCardMetadata?.subject || 'Geral'} - {currentCardMetadata?.subSubject || 'Geral'}

Frente

{currentCard.front}

{showAnswer && ( <>

Verso

{currentCard.back}

Desempenho: {currentCardMetadata ? formatPerformance(currentCardMetadata.performanceRate) : '-'} Status: {currentCardMetadata?.ragStatus || 'Grey'}
)}
{!showAnswer && ( )} {showAnswer && ( <> )}
)}
); }