import { useEffect, useMemo, useState } from 'preact/hooks'; import { MindforgeApiService, type FlashcardCard, type FlashcardRagDashboardResponse, type FlashcardRagLibrary, type FlashcardRagStatus, } from '../services/MindforgeApiService'; import { Button } from './Button'; import './SpacedReviewComponent.css'; interface RagStatusOption { status: FlashcardRagStatus; label: string; icon: string; } interface RagStatusMeta { label: string; icon: string; className: string; } const STATUS_OPTIONS: RagStatusOption[] = [ { status: 'Red', label: 'Vermelho', icon: '!' }, { status: 'Amber', label: 'Amarelo', icon: '*' }, { status: 'Green', label: 'Verde', icon: 'v' }, { status: 'Grey', label: 'Cinza', icon: '-' }, ]; const STATUS_META_BY_STATUS: Record = { Red: { label: 'Vermelho', icon: '!', className: 'rag-red' }, Amber: { label: 'Amarelo', icon: '*', className: 'rag-amber' }, Green: { label: 'Verde', icon: 'v', className: 'rag-green' }, Grey: { label: 'Cinza', icon: '-', className: 'rag-grey' }, }; const STATUS_PRIORITY: Record = { Red: 0, Amber: 1, Green: 2, Grey: 3, }; function shuffleCards(cards: FlashcardCard[]) { const shuffled = [...cards]; for (let index = shuffled.length - 1; index > 0; index--) { const randomIndex = Math.floor(Math.random() * (index + 1)); [shuffled[index], shuffled[randomIndex]] = [shuffled[randomIndex], shuffled[index]]; } 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 formatLastReviewed(value?: string | null) { if (!value) { return 'Nunca revisado'; } const date = new Date(value); if (Number.isNaN(date.getTime())) { return 'Data invalida'; } return date.toLocaleDateString('pt-BR'); } function orderCardsForSession(cards: FlashcardCard[], ragByLibraryId: Map) { const buckets: FlashcardCard[][] = [[], [], [], []]; cards.forEach((card) => { const ragLibrary = ragByLibraryId.get(card.libraryId); const status = ragLibrary?.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', 'Green', 'Grey']); const [selectedLibraryIds, setSelectedLibraryIds] = useState([]); const [selectedSubjects, setSelectedSubjects] = useState([]); const [selectedSubSubjects, setSelectedSubSubjects] = useState([]); const [startingSession, setStartingSession] = useState(false); const [sessionCards, setSessionCards] = useState([]); const [sessionLibraries, setSessionLibraries] = 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 allLibraryIds = response.subjects.flatMap((subjectGroup) => subjectGroup.subSubjects.flatMap((subSubjectGroup) => subSubjectGroup.libraries.map((library) => library.libraryId)), ); setSelectedLibraryIds((current) => { if (!preserveSelection || current.length === 0) { return allLibraryIds; } const available = new Set(allLibraryIds); const kept = current.filter((libraryId) => available.has(libraryId)); return kept.length > 0 ? kept : allLibraryIds; }); setSelectedStatuses((current) => { if (current.length > 0) { return current; } return ['Red', 'Amber', 'Green', 'Grey']; }); } 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 startSession = async () => { if (selectedStatuses.length === 0) { setError('Selecione ao menos um status para iniciar a revisao.'); return; } if (selectedLibraryIds.length === 0) { setError('Selecione ao menos um arquivo para iniciar a revisao.'); return; } if (selectedRagLibraries.length === 0) { setError('Nenhum arquivo encontrado com os filtros atuais. Ajuste os status ou os arquivos selecionados.'); return; } setStartingSession(true); setError(null); try { const ragByLibraryId = new Map(selectedRagLibraries.map((library) => [library.libraryId, library])); const libraryIds = Array.from(new Set(selectedRagLibraries.map((library) => library.libraryId))); const response = await MindforgeApiService.createFlashcardReviewSession({ libraryIds }); const allowedLibraryIds = new Set(libraryIds); const filteredCards = response.cards.filter((card) => allowedLibraryIds.has(card.libraryId)); const orderedCards = orderCardsForSession(filteredCards, ragByLibraryId); if (orderedCards.length === 0) { setError('Os filtros selecionados nao retornaram cards para revisar.'); return; } setSessionLibraries(selectedRagLibraries); setSessionCards(orderedCards); setCurrentIndex(0); setShowAnswer(false); } catch (err: any) { setError(err?.message || 'Falha ao iniciar revisao espacada.'); } finally { setStartingSession(false); } }; const allRagLibraries = useMemo(() => { if (!dashboard) { return []; } return dashboard.subjects.flatMap((subjectGroup) => subjectGroup.subSubjects.flatMap((subSubjectGroup) => subSubjectGroup.libraries)); }, [dashboard]); const selectedRagLibraries = useMemo(() => { const statusSet = new Set(selectedStatuses); const libraryIdSet = new Set(selectedLibraryIds); return allRagLibraries.filter((library) => libraryIdSet.has(library.libraryId) && statusSet.has(library.ragStatus)); }, [allRagLibraries, selectedLibraryIds, selectedStatuses]); const sessionLibraryById = useMemo(() => { return new Map(sessionLibraries.map((library) => [library.libraryId, library])); }, [sessionLibraries]); const currentCard = sessionCards[currentIndex]; const currentLibrary = currentCard ? sessionLibraryById.get(currentCard.libraryId) : undefined; const currentStatusMeta = currentLibrary ? STATUS_META_BY_STATUS[currentLibrary.ragStatus] : STATUS_META_BY_STATUS.Grey; 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 toggleLibrary = (libraryId: number) => { if (selectedLibraryIds.includes(libraryId)) { setSelectedLibraryIds(selectedLibraryIds.filter((id) => id !== libraryId)); return; } setSelectedLibraryIds([...selectedLibraryIds, libraryId]); }; const toggleSubject = (subject: string) => { if (selectedSubjects.includes(subject)) { setSelectedSubjects(selectedSubjects.filter((s) => s !== subject)); const subjectGroup = dashboard?.subjects.find((s) => s.subject === subject); if (subjectGroup) { const libraryIdsToRemove = subjectGroup.subSubjects.flatMap((ss) => ss.libraries.map((lib) => lib.libraryId)); setSelectedLibraryIds((current) => current.filter((id) => !libraryIdsToRemove.includes(id))); const subSubjectKeysToRemove = subjectGroup.subSubjects.map((ss) => `${subject}::${ss.subSubject}`); setSelectedSubSubjects((current) => current.filter((key) => !subSubjectKeysToRemove.includes(key))); } return; } setSelectedSubjects([...selectedSubjects, subject]); const subjectGroup = dashboard?.subjects.find((s) => s.subject === subject); if (subjectGroup) { const libraryIdsToAdd = subjectGroup.subSubjects.flatMap((ss) => ss.libraries.map((lib) => lib.libraryId)); setSelectedLibraryIds((current) => [...new Set([...current, ...libraryIdsToAdd])]); const subSubjectKeysToAdd = subjectGroup.subSubjects.map((ss) => `${subject}::${ss.subSubject}`); setSelectedSubSubjects((current) => [...new Set([...current, ...subSubjectKeysToAdd])]); } }; const toggleSubSubject = (subject: string, subSubject: string) => { const key = `${subject}::${subSubject}`; if (selectedSubSubjects.includes(key)) { setSelectedSubSubjects(selectedSubSubjects.filter((k) => k !== key)); const subjectGroup = dashboard?.subjects.find((s) => s.subject === subject); const subSubjectGroup = subjectGroup?.subSubjects.find((ss) => ss.subSubject === subSubject); if (subSubjectGroup) { const libraryIdsToRemove = subSubjectGroup.libraries.map((lib) => lib.libraryId); setSelectedLibraryIds((current) => current.filter((id) => !libraryIdsToRemove.includes(id))); } const parentSubjectStillSelected = selectedSubSubjects.some((k) => k.startsWith(`${subject}::`) && k !== key); if (!parentSubjectStillSelected) { setSelectedSubjects((current) => current.filter((s) => s !== subject)); } return; } setSelectedSubSubjects([...selectedSubSubjects, key]); const subjectGroup = dashboard?.subjects.find((s) => s.subject === subject); const subSubjectGroup = subjectGroup?.subSubjects.find((ss) => ss.subSubject === subSubject); if (subSubjectGroup) { const libraryIdsToAdd = subSubjectGroup.libraries.map((lib) => lib.libraryId); setSelectedLibraryIds((current) => [...new Set([...current, ...libraryIdsToAdd])]); } const allSubSubjects = subjectGroup?.subSubjects.map((ss) => `${subject}::${ss.subSubject}`) || []; const allSelected = allSubSubjects.every((k) => k === key || selectedSubSubjects.includes(k)); if (allSelected && subject && !selectedSubjects.includes(subject)) { setSelectedSubjects((current) => [...current, subject]); } }; const startSession = async () => { if (selectedStatuses.length === 0) { setError('Selecione ao menos um status para iniciar a revisao.'); return; } if (selectedLibraryIds.length === 0) { setError('Selecione ao menos um arquivo para iniciar a revisao.'); return; } if (selectedRagLibraries.length === 0) { setError('Nenhum arquivo encontrado com os filtros atuais. Ajuste os status ou os arquivos selecionados.'); return; } setStartingSession(true); setError(null); try { const ragByLibraryId = new Map(selectedRagLibraries.map((library) => [library.libraryId, library])); const libraryIds = Array.from(new Set(selectedRagLibraries.map((library) => library.libraryId))); const response = await MindforgeApiService.createFlashcardReviewSession({ libraryIds }); const allowedLibraryIds = new Set(libraryIds); const filteredCards = response.cards.filter((card) => allowedLibraryIds.has(card.libraryId)); const orderedCards = orderCardsForSession(filteredCards, ragByLibraryId); if (orderedCards.length === 0) { setError('Os filtros selecionados nao retornaram cards para revisar.'); return; } setSessionLibraries(selectedRagLibraries); setSessionCards(orderedCards); setCurrentIndex(0); setShowAnswer(false); } catch (err: any) { setError(err?.message || 'Falha ao iniciar revisao espacada.'); } finally { setStartingSession(false); } }; const endSession = () => { setSessionCards([]); setSessionLibraries([]); 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 por arquivo de flashcards.

{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) => (

{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) => (
{summaryText( subSubjectGroup.summary.activeCount, subSubjectGroup.summary.greenPercentage, subSubjectGroup.summary.attentionPercentage, )} Arquivos: {subSubjectGroup.libraries.length} | Cinza: {subSubjectGroup.summary.greyCount}
{subSubjectGroup.libraries.map((library) => { const statusMeta = STATUS_META_BY_STATUS[library.ragStatus]; const selected = selectedLibraryIds.includes(library.libraryId); return ( ); })}
))}
))}
)}

Arquivos selecionados: {selectedRagLibraries.length}

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

Frente

{currentCard.front}

{showAnswer && ( <>

Verso

{currentCard.back}

Desempenho do arquivo: {currentLibrary ? formatPerformance(currentLibrary.performanceRate) : '-'} Ultima revisao: {formatLastReviewed(currentLibrary?.lastReviewedAt)}
)}
{!showAnswer && ( )} {showAnswer && ( <> )}
)}
); }