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
272 lines
8.4 KiB
TypeScript
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>
|
|
);
|
|
}
|