Some checks failed
Mindforge Web Build and Deploy (internal) / Build Mindforge Web Image (push) Failing after 1m57s
Mindforge Web Build and Deploy (internal) / Deploy Mindforge Web (internal) (push) Has been skipped
Mindforge API Build and Deploy / Build Mindforge API Image (push) Successful in 2m11s
Mindforge API Build and Deploy / Deploy Mindforge API (internal) (push) Successful in 8s
603 lines
22 KiB
TypeScript
603 lines
22 KiB
TypeScript
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<FlashcardRagStatus, RagStatusMeta> = {
|
|
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<FlashcardRagStatus, number> = {
|
|
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<number, FlashcardRagLibrary>) {
|
|
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<FlashcardRagDashboardResponse | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [selectedStatuses, setSelectedStatuses] = useState<FlashcardRagStatus[]>(['Red', 'Amber', 'Green', 'Grey']);
|
|
const [selectedLibraryIds, setSelectedLibraryIds] = useState<number[]>([]);
|
|
const [selectedSubjects, setSelectedSubjects] = useState<string[]>([]);
|
|
const [selectedSubSubjects, setSelectedSubSubjects] = useState<string[]>([]);
|
|
const [startingSession, setStartingSession] = useState(false);
|
|
const [sessionCards, setSessionCards] = useState<FlashcardCard[]>([]);
|
|
const [sessionLibraries, setSessionLibraries] = useState<FlashcardRagLibrary[]>([]);
|
|
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 (
|
|
<div className="spaced-review-container">
|
|
<h2 className="title spaced-review-title">Revisao espacada</h2>
|
|
<p className="subtitle">Acompanhe o status RAG por arquivo de flashcards.</p>
|
|
|
|
{error && <div className="spaced-review-error">{error}</div>}
|
|
|
|
{sessionCards.length === 0 && (
|
|
<div className="spaced-review-panel">
|
|
{loading && <p className="spaced-review-state">Carregando painel de revisao...</p>}
|
|
{!loading && (!dashboard || dashboard.subjects.length === 0) && (
|
|
<p className="spaced-review-state">Nenhum flashcard encontrado para revisar.</p>
|
|
)}
|
|
|
|
{!loading && dashboard && dashboard.subjects.length > 0 && (
|
|
<>
|
|
<div className="spaced-review-filters">
|
|
{STATUS_OPTIONS.map((option) => (
|
|
<label key={option.status} className={`spaced-review-filter ${STATUS_META_BY_STATUS[option.status].className}`}>
|
|
<input
|
|
type="checkbox"
|
|
checked={selectedStatuses.includes(option.status)}
|
|
onChange={() => toggleStatus(option.status)}
|
|
/>
|
|
<span className="rag-badge-inline">
|
|
<span className="rag-icon">{option.icon}</span>
|
|
{option.label}
|
|
</span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
|
|
<div className="spaced-review-subjects">
|
|
{dashboard.subjects.map((subjectGroup) => (
|
|
<section key={subjectGroup.subject} className="spaced-review-subject">
|
|
<header className="spaced-review-subject-header">
|
|
<label className="spaced-review-subject-toggle">
|
|
<input
|
|
type="checkbox"
|
|
checked={selectedSubjects.includes(subjectGroup.subject)}
|
|
onChange={() => toggleSubject(subjectGroup.subject)}
|
|
/>
|
|
<h3>{subjectGroup.subject}</h3>
|
|
</label>
|
|
<p>{summaryText(
|
|
subjectGroup.summary.activeCount,
|
|
subjectGroup.summary.greenPercentage,
|
|
subjectGroup.summary.attentionPercentage,
|
|
)}</p>
|
|
<small>
|
|
Verde: {subjectGroup.summary.greenCount} | Amarelo: {subjectGroup.summary.amberCount}
|
|
{' '}
|
|
| Vermelho: {subjectGroup.summary.redCount} | Cinza: {subjectGroup.summary.greyCount}
|
|
</small>
|
|
</header>
|
|
|
|
<div className="spaced-review-subsubject-list">
|
|
{subjectGroup.subSubjects.map((subSubjectGroup) => (
|
|
<div key={`${subjectGroup.subject}::${subSubjectGroup.subSubject}`} className="spaced-review-subsubject-block">
|
|
<div className="spaced-review-subsubject-header">
|
|
<label className="spaced-review-subsubject-toggle">
|
|
<input
|
|
type="checkbox"
|
|
checked={selectedSubSubjects.includes(`${subjectGroup.subject}::${subSubjectGroup.subSubject}`)}
|
|
onChange={() => toggleSubSubject(subjectGroup.subject, subSubjectGroup.subSubject)}
|
|
/>
|
|
<strong>{subSubjectGroup.subSubject}</strong>
|
|
</label>
|
|
<span>{summaryText(
|
|
subSubjectGroup.summary.activeCount,
|
|
subSubjectGroup.summary.greenPercentage,
|
|
subSubjectGroup.summary.attentionPercentage,
|
|
)}</span>
|
|
<small>
|
|
Arquivos: {subSubjectGroup.libraries.length} | Cinza: {subSubjectGroup.summary.greyCount}
|
|
</small>
|
|
</div>
|
|
|
|
<div className="spaced-review-library-list">
|
|
{subSubjectGroup.libraries.map((library) => {
|
|
const statusMeta = STATUS_META_BY_STATUS[library.ragStatus];
|
|
const selected = selectedLibraryIds.includes(library.libraryId);
|
|
|
|
return (
|
|
<label
|
|
key={library.libraryId}
|
|
className={`spaced-review-library-item ${statusMeta.className} ${selected ? 'selected' : ''}`}
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
checked={selected}
|
|
onChange={() => toggleLibrary(library.libraryId)}
|
|
/>
|
|
<div className="spaced-review-library-texts">
|
|
<strong>{library.fileName}</strong>
|
|
<span>
|
|
Cards: {library.cardCount} | Desempenho: {formatPerformance(library.performanceRate)}
|
|
</span>
|
|
<small>Ultima revisao: {formatLastReviewed(library.lastReviewedAt)}</small>
|
|
</div>
|
|
<span className={`rag-badge ${statusMeta.className}`}>
|
|
<span className="rag-icon">{statusMeta.icon}</span>
|
|
{statusMeta.label}
|
|
</span>
|
|
</label>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
))}
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
<div className="spaced-review-footer">
|
|
<p>
|
|
Arquivos selecionados: <strong>{selectedRagLibraries.length}</strong>
|
|
</p>
|
|
<Button
|
|
variant="primary"
|
|
disabled={startingSession || loading}
|
|
onClick={startSession}
|
|
>
|
|
{startingSession ? 'Iniciando...' : 'Iniciar Revisao Espacada'}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{sessionCards.length > 0 && currentCard && (
|
|
<div className="spaced-review-session-panel">
|
|
<div className="spaced-review-progress">
|
|
<span>{currentIndex + 1} / {sessionCards.length}</span>
|
|
<div className="spaced-review-progress-bar">
|
|
<div className="spaced-review-progress-fill" style={{ width: `${progressPercent}%` }} />
|
|
</div>
|
|
</div>
|
|
|
|
<article className="spaced-review-card">
|
|
<header>
|
|
<small>
|
|
{currentLibrary?.fileName || 'Arquivo'} - {currentLibrary?.subject || 'Geral'} - {currentLibrary?.subSubject || 'Geral'}
|
|
</small>
|
|
<span className={`rag-badge ${currentStatusMeta.className}`}>
|
|
<span className="rag-icon">{currentStatusMeta.icon}</span>
|
|
{currentStatusMeta.label}
|
|
</span>
|
|
</header>
|
|
|
|
<h3>Frente</h3>
|
|
<p>{currentCard.front}</p>
|
|
|
|
{showAnswer && (
|
|
<>
|
|
<h3>Verso</h3>
|
|
<p>{currentCard.back}</p>
|
|
<div className="spaced-review-card-meta">
|
|
<span>Desempenho do arquivo: {currentLibrary ? formatPerformance(currentLibrary.performanceRate) : '-'}</span>
|
|
<span>Ultima revisao: {formatLastReviewed(currentLibrary?.lastReviewedAt)}</span>
|
|
</div>
|
|
</>
|
|
)}
|
|
</article>
|
|
|
|
<div className="spaced-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={() => registerAnswer(true)} disabled={submittingAnswer}>
|
|
Acertei
|
|
</Button>
|
|
<Button variant="secondary" onClick={() => registerAnswer(false)} disabled={submittingAnswer}>
|
|
Errei
|
|
</Button>
|
|
</>
|
|
)}
|
|
|
|
<Button variant="secondary" onClick={endSession}>
|
|
Encerrar Sessao
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|