Files
mindforge/Mindforge.Web/src/components/FlashcardReviewComponent.tsx
Jose Henrique f03bcc40e3
All checks were successful
Mindforge API Build and Deploy / Build Mindforge API Image (push) Successful in 3m58s
Mindforge API Build and Deploy / Deploy Mindforge API (internal) (push) Successful in 37s
Mindforge Web Build and Deploy (internal) / Build Mindforge Web Image (push) Successful in 5m19s
Mindforge Web Build and Deploy (internal) / Deploy Mindforge Web (internal) (push) Successful in 11s
timed revision
2026-06-01 19:08:48 -03:00

272 lines
8.4 KiB
TypeScript

import { useEffect, useMemo, useState } from 'preact/hooks';
import {
MindforgeApiService,
type FlashcardCard,
type FlashcardLibrarySummary,
} from '../services/MindforgeApiService';
import { Button } from './Button';
import './FlashcardReviewComponent.css';
function groupLibrariesBySubject(libraries: FlashcardLibrarySummary[]) {
const grouped: Record<string, FlashcardLibrarySummary[]> = {};
libraries.forEach((library) => {
const subject = library.subject || 'Geral';
if (!grouped[subject]) {
grouped[subject] = [];
}
grouped[subject].push(library);
});
return Object.entries(grouped).sort(([a], [b]) => a.localeCompare(b));
}
function difficultyLabel(difficulty: string) {
return difficulty === 'Hard' ? 'Dificil' : 'Facil';
}
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;
}
export function FlashcardReviewComponent() {
const [libraries, setLibraries] = useState<FlashcardLibrarySummary[]>([]);
const [selectedLibraryIds, setSelectedLibraryIds] = useState<number[]>([]);
const [loadingLibraries, setLoadingLibraries] = useState(true);
const [loadingSession, setLoadingSession] = useState(false);
const [error, setError] = useState<string | null>(null);
const [sessionCards, setSessionCards] = useState<FlashcardCard[]>([]);
const [currentIndex, setCurrentIndex] = useState(0);
const [showAnswer, setShowAnswer] = useState(false);
const [submittingAnswer, setSubmittingAnswer] = useState(false);
useEffect(() => {
let cancelled = false;
async function loadLibraries() {
setLoadingLibraries(true);
setError(null);
try {
const result = await MindforgeApiService.getFlashcardLibraries();
if (!cancelled) {
setLibraries(result);
}
} catch (err: any) {
if (!cancelled) {
setError(err?.message || 'Falha ao carregar bibliotecas de flashcards.');
}
} finally {
if (!cancelled) {
setLoadingLibraries(false);
}
}
}
loadLibraries();
return () => {
cancelled = true;
};
}, []);
const groupedLibraries = useMemo(() => groupLibrariesBySubject(libraries), [libraries]);
const libraryById = useMemo(() => {
return new Map(libraries.map((library) => [library.id, library]));
}, [libraries]);
const currentCard = sessionCards[currentIndex];
const progressPercent = sessionCards.length > 0
? ((currentIndex + 1) / sessionCards.length) * 100
: 0;
const toggleLibrary = (libraryId: number) => {
if (selectedLibraryIds.includes(libraryId)) {
setSelectedLibraryIds(selectedLibraryIds.filter((id) => id !== libraryId));
return;
}
setSelectedLibraryIds([...selectedLibraryIds, libraryId]);
};
const startReviewSession = async () => {
if (selectedLibraryIds.length === 0) {
setError('Selecione ao menos uma biblioteca para iniciar a revisao.');
return;
}
setLoadingSession(true);
setError(null);
try {
const response = await MindforgeApiService.createFlashcardReviewSession({
libraryIds: selectedLibraryIds,
});
setSessionCards(shuffleCards(response.cards));
setCurrentIndex(0);
setShowAnswer(false);
} catch (err: any) {
setError(err?.message || 'Falha ao iniciar sessao de revisao.');
} finally {
setLoadingSession(false);
}
};
const endSession = () => {
setSessionCards([]);
setCurrentIndex(0);
setShowAnswer(false);
setSubmittingAnswer(false);
};
const goToPrevious = () => {
if (currentIndex === 0) {
return;
}
setCurrentIndex(currentIndex - 1);
setShowAnswer(false);
};
const registerReviewAnswer = 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 (
<div className="review-container">
<h2 className="title review-title">Revisao Flashcards</h2>
<p className="subtitle">Escolha as bibliotecas para estudar e inicie uma sessao de revisao.</p>
{error && <div className="review-error">{error}</div>}
{sessionCards.length === 0 && (
<div className="review-select-panel">
{loadingLibraries && <p className="review-state">Carregando bibliotecas...</p>}
{!loadingLibraries && libraries.length === 0 && (
<p className="review-state">Nenhuma biblioteca encontrada. Gere flashcards para comecar.</p>
)}
{!loadingLibraries && libraries.length > 0 && (
<div className="review-subjects">
{groupedLibraries.map(([subject, subjectLibraries]) => (
<section key={subject} className="review-subject-section">
<h3>{subject}</h3>
<div className="review-library-list">
{subjectLibraries.map((library) => (
<label key={library.id} className="review-library-item">
<input
type="checkbox"
checked={selectedLibraryIds.includes(library.id)}
onChange={() => toggleLibrary(library.id)}
/>
<div className="review-library-texts">
<strong>{library.fileName}</strong>
<span>
{library.cardCount} cards - {difficultyLabel(library.difficulty)}
</span>
</div>
</label>
))}
</div>
</section>
))}
</div>
)}
<div className="review-actions">
<Button variant="primary" onClick={startReviewSession} disabled={loadingSession || selectedLibraryIds.length === 0}>
{loadingSession ? 'Iniciando...' : 'Iniciar Revisao'}
</Button>
</div>
</div>
)}
{sessionCards.length > 0 && currentCard && (
<div className="review-session-panel">
<div className="review-progress">
<span>{currentIndex + 1} / {sessionCards.length}</span>
<div className="review-progress-bar">
<div className="review-progress-fill" style={{ width: `${progressPercent}%` }} />
</div>
</div>
<article className="review-card">
<header>
<small>
{libraryById.get(currentCard.libraryId)?.fileName || 'Arquivo'} -
{' '}
{libraryById.get(currentCard.libraryId)?.subject || 'Geral'}
</small>
</header>
<h3>Frente</h3>
<p>{currentCard.front}</p>
{showAnswer && (
<>
<h3>Verso</h3>
<p>{currentCard.back}</p>
</>
)}
</article>
<div className="review-session-actions">
<Button variant="secondary" onClick={goToPrevious} disabled={currentIndex === 0 || submittingAnswer}>
Anterior
</Button>
{!showAnswer && (
<Button variant="primary" onClick={() => setShowAnswer(true)}>
Revelar Resposta
</Button>
)}
{showAnswer && (
<>
<Button variant="primary" onClick={() => registerReviewAnswer(true)} disabled={submittingAnswer}>
Acertei
</Button>
<Button variant="secondary" onClick={() => registerReviewAnswer(false)} disabled={submittingAnswer}>
Errei
</Button>
</>
)}
<Button variant="secondary" onClick={endSession}>
Encerrar Sessao
</Button>
</div>
</div>
)}
</div>
);
}