new flashcards
All checks were successful
Mindforge API Build and Deploy / Build Mindforge API Image (push) Successful in 4m4s
Mindforge Web Build and Deploy (internal) / Build Mindforge Web Image (push) Successful in 5m29s
Mindforge Web Build and Deploy (internal) / Deploy Mindforge Web (internal) (push) Successful in 9s
Mindforge API Build and Deploy / Deploy Mindforge API (internal) (push) Successful in 8s
All checks were successful
Mindforge API Build and Deploy / Build Mindforge API Image (push) Successful in 4m4s
Mindforge Web Build and Deploy (internal) / Build Mindforge Web Image (push) Successful in 5m29s
Mindforge Web Build and Deploy (internal) / Deploy Mindforge Web (internal) (push) Successful in 9s
Mindforge API Build and Deploy / Deploy Mindforge API (internal) (push) Successful in 8s
This commit is contained in:
233
Mindforge.Web/src/components/FlashcardReviewComponent.tsx
Normal file
233
Mindforge.Web/src/components/FlashcardReviewComponent.tsx
Normal file
@@ -0,0 +1,233 @@
|
||||
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';
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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(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);
|
||||
};
|
||||
|
||||
const goToPrevious = () => {
|
||||
if (currentIndex === 0) {
|
||||
return;
|
||||
}
|
||||
setCurrentIndex(currentIndex - 1);
|
||||
setShowAnswer(false);
|
||||
};
|
||||
|
||||
const goToNext = () => {
|
||||
if (currentIndex >= sessionCards.length - 1) {
|
||||
return;
|
||||
}
|
||||
setCurrentIndex(currentIndex + 1);
|
||||
setShowAnswer(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}>
|
||||
Anterior
|
||||
</Button>
|
||||
|
||||
{!showAnswer && (
|
||||
<Button variant="primary" onClick={() => setShowAnswer(true)}>
|
||||
Revelar Resposta
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{showAnswer && (
|
||||
<Button variant="primary" onClick={goToNext} disabled={currentIndex >= sessionCards.length - 1}>
|
||||
Proximo
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button variant="secondary" onClick={endSession}>
|
||||
Encerrar Sessao
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user