Selecione os arquivos do repositorio para gerar bibliotecas de flashcards.
Selecione os arquivos do repositório para gerar bibliotecas de flashcards.
-
Arquivos do Repositorio
+
Arquivos do Repositório
{selectedPaths.length > 0 && (
- {selectedPaths.length} arquivo{selectedPaths.length !== 1 ? 's' : ''} selecionado
- {selectedPaths.length !== 1 ? 's' : ''}
+ {selectedPaths.length === 1 ? '1 arquivo selecionado' : `${selectedPaths.length} arquivos selecionados`}
)}
-
Quantidade por Arquivo ({minAmount} - {maxAmount})
+
Quantidade por arquivo ({minAmount} - {maxAmount})
setDifficulty('Easy')}
/>
- Facil
+ Fácil
@@ -104,8 +103,8 @@ export function FlashcardComponent() {
checked={difficulty === 'Medium'}
onChange={() => setDifficulty('Medium')}
/>
-
- Medio
+
+ Médio
@@ -133,10 +132,10 @@ export function FlashcardComponent() {
{library.fileName}
- {library.cardCount} cards
+ {library.cardCount} cartões
- Materia: {library.subject}
+ Matéria: {library.subject}
Dificuldade: {difficultyLabel(library.difficulty)}
diff --git a/Mindforge.Web/src/components/FlashcardReviewComponent.css b/Mindforge.Web/src/components/FlashcardReviewComponent.css
index b732bf4..3206ebe 100644
--- a/Mindforge.Web/src/components/FlashcardReviewComponent.css
+++ b/Mindforge.Web/src/components/FlashcardReviewComponent.css
@@ -29,7 +29,6 @@
margin-bottom: 1rem;
}
-/* Library Selection */
.review-select-panel {
background: rgba(255, 250, 239, .68);
border: 1px solid rgba(104, 69, 22, .13);
@@ -109,548 +108,8 @@
justify-content: flex-end;
}
-/* Session Panel - Content Grid */
-.content-grid {
- display: grid;
- grid-template-columns: minmax(0, 1fr) 330px;
- gap: 24px;
- align-items: start;
-}
-
-/* Review Panel (main flashcard area) */
-.review-panel {
- position: relative;
- min-height: 650px;
- padding: clamp(18px, 3vw, 34px);
- border-radius: var(--radius-xl);
- background:
- linear-gradient(145deg, rgba(255,255,255,.54), rgba(255,240,202,.46)),
- radial-gradient(circle at 15% 20%, rgba(255, 201, 101, .25), transparent 35%),
- radial-gradient(circle at 90% 10%, rgba(63, 124, 172, .16), transparent 32%);
- border: 1px solid rgba(104, 69, 22, .13);
- box-shadow: var(--shadow);
- overflow: hidden;
-}
-
-.review-panel::before,
-.review-panel::after {
- content: "";
- position: absolute;
- border-radius: 999px;
- pointer-events: none;
- filter: blur(2px);
- opacity: .5;
-}
-
-.review-panel::before {
- width: 220px;
- height: 220px;
- right: -92px;
- top: 100px;
- background: rgba(63, 124, 172, .13);
-}
-
-.review-panel::after {
- width: 180px;
- height: 180px;
- left: -70px;
- bottom: 60px;
- background: rgba(199, 149, 57, .18);
-}
-
-.session-header {
- position: relative;
- z-index: 1;
- display: flex;
- justify-content: space-between;
- align-items: flex-start;
- gap: 16px;
- margin-bottom: 24px;
-}
-
-.session-title h3 {
- margin: 0;
- font-family: Georgia, serif;
- font-size: clamp(24px, 3vw, 36px);
- letter-spacing: -.04em;
- color: var(--ink);
-}
-
-.session-title p {
- max-width: 660px;
- color: var(--muted);
- font-size: 15px;
- line-height: 1.55;
- margin: 4px 0 0;
-}
-
-.score-pill {
- min-width: 76px;
- padding: 11px 12px;
- border-radius: 18px;
- background: rgba(255,255,255,.55);
- border: 1px solid rgba(100, 65, 18, .12);
- text-align: center;
- box-shadow: inset 0 1px 0 rgba(255,255,255,.72);
-}
-
-.score-pill b {
- display: block;
- font-size: 19px;
- color: var(--ink);
-}
-
-.score-pill span {
- color: var(--muted);
- font-size: 10px;
- font-weight: 950;
- letter-spacing: .12em;
- text-transform: uppercase;
-}
-
-/* Stage (flashcard container) */
-.stage {
- position: relative;
- z-index: 1;
- display: grid;
- place-items: center;
- min-height: 390px;
- perspective: 1400px;
- padding: 22px 0;
-}
-
-/* Flashcard */
-.flashcard {
- position: relative;
- width: min(680px, 100%);
- min-height: 355px;
- cursor: pointer;
- transform-style: preserve-3d;
- transition: transform .78s var(--ease), filter .35s var(--ease);
- outline: none;
-}
-
-.flashcard:hover .card-face {
- border-color: rgba(63, 124, 172, .28);
-}
-
-.flashcard.is-flipped {
- transform: rotateY(180deg);
-}
-
-.flashcard.is-reviewed {
- animation: cardExit .58s var(--ease);
-}
-
-.card-face {
- position: absolute;
- inset: 0;
- min-height: 355px;
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- padding: clamp(24px, 5vw, 42px);
- border-radius: 30px;
- backface-visibility: hidden;
- background:
- linear-gradient(90deg, rgba(168, 111, 36, .09) 0 1px, transparent 1px 22px),
- linear-gradient(rgba(168, 111, 36, .08) 0 1px, transparent 1px 30px),
- linear-gradient(145deg, #fffaf0, #f5dfaa);
- border: 1px solid rgba(82, 54, 17, .18);
- box-shadow: var(--card-shadow), inset 0 0 0 8px rgba(255,255,255,.24);
- overflow: hidden;
-}
-
-.card-face::before {
- content: "";
- position: absolute;
- inset: 18px;
- border: 1px dashed rgba(82, 54, 17, .18);
- border-radius: 22px;
- pointer-events: none;
-}
-
-.card-face::after {
- content: "";
- position: absolute;
- width: 140px;
- height: 140px;
- right: -46px;
- bottom: -50px;
- border-radius: 50%;
- background: radial-gradient(circle, rgba(199,149,57,.25), transparent 66%);
- pointer-events: none;
-}
-
-.card-back {
- transform: rotateY(180deg);
- background:
- linear-gradient(90deg, rgba(63,124,172,.08) 0 1px, transparent 1px 22px),
- linear-gradient(rgba(63,124,172,.07) 0 1px, transparent 1px 30px),
- linear-gradient(145deg, #fffaf1, #dfeef2);
-}
-
-.card-meta {
- position: relative;
- z-index: 1;
- display: flex;
- justify-content: space-between;
- gap: 12px;
- align-items: center;
- color: var(--muted);
- font-size: 12px;
- font-weight: 950;
- letter-spacing: .12em;
- text-transform: uppercase;
-}
-
-.tag {
- display: inline-flex;
- align-items: center;
- gap: 8px;
- padding: 9px 12px;
- border-radius: 999px;
- background: rgba(255,255,255,.54);
- border: 1px solid rgba(82, 54, 17, .12);
-}
-
-.card-question,
-.card-answer {
- position: relative;
- z-index: 1;
- display: grid;
- align-content: center;
- gap: 12px;
- min-height: 190px;
- color: var(--ink);
- font-family: "Segoe UI", Inter, Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
- font-size: clamp(17px, 2.2vw, 20px);
- line-height: 1.55;
-}
-
-.card-footer {
- position: relative;
- z-index: 1;
- display: flex;
- justify-content: space-between;
- gap: 14px;
- align-items: center;
- color: var(--muted);
- font-size: 13px;
- font-weight: 800;
-}
-
-.spacebar {
- padding: 4px 9px;
- border-radius: 8px;
- color: #4f3a1d;
- background: rgba(255,255,255,.58);
- border: 1px solid rgba(82, 54, 17, .14);
- box-shadow: inset 0 -2px 0 rgba(82, 54, 17, .08);
- font-size: 11px;
- font-weight: 950;
- letter-spacing: .08em;
- text-transform: uppercase;
-}
-
-/* Controls */
-.controls {
- position: relative;
- z-index: 1;
- display: flex;
- justify-content: center;
- gap: 16px;
- flex-wrap: wrap;
- opacity: .36;
- transform: translateY(10px);
- pointer-events: none;
- transition: .35s var(--ease);
- margin-top: 20px;
-}
-
-.controls.ready {
- opacity: 1;
- transform: translateY(0);
- pointer-events: auto;
-}
-
-.review-button {
- position: relative;
- min-width: 170px;
- min-height: 60px;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 10px;
- border: 0;
- border-radius: 20px;
- color: white;
- font-family: inherit;
- font-weight: 950;
- font-size: 16px;
- cursor: pointer;
- box-shadow: 0 16px 34px rgba(63, 44, 20, .18), inset 0 1px 0 rgba(255,255,255,.32);
- overflow: hidden;
- transition: .25s var(--ease);
-}
-
-.review-button.correct {
- background: linear-gradient(135deg, var(--green), var(--green-deep));
-}
-
-.review-button.wrong {
- background: linear-gradient(135deg, var(--red), var(--red-deep));
-}
-
-.review-button:hover {
- transform: translateY(-4px);
- box-shadow: 0 22px 42px rgba(63, 44, 20, .22), inset 0 1px 0 rgba(255,255,255,.32);
-}
-
-.review-button::before {
- content: "";
- position: absolute;
- inset: -90% -40%;
- background: linear-gradient(90deg, transparent, rgba(255,255,255,.32), transparent);
- transform: rotate(20deg) translateX(-80%);
- transition: .55s var(--ease);
-}
-
-.review-button:hover::before {
- transform: rotate(20deg) translateX(80%);
-}
-
-.review-button:disabled {
- opacity: 0.45;
- cursor: not-allowed;
- transform: none !important;
-}
-
-/* Stamp */
-.stamp {
- position: absolute;
- right: 38px;
- top: 36px;
- z-index: 4;
- padding: 12px 18px;
- border: 4px double currentColor;
- border-radius: 10px;
- font-family: Georgia, serif;
- font-size: 26px;
- font-weight: 900;
- letter-spacing: .08em;
- text-transform: uppercase;
- opacity: 0;
- transform: rotate(-12deg) scale(1.3);
- pointer-events: none;
-}
-
-.stamp.correct {
- color: var(--green-deep);
-}
-
-.stamp.wrong {
- color: var(--red-deep);
-}
-
-.stamp.show {
- animation: stampIn .7s var(--ease);
-}
-
-/* Side Panel */
-.side-panel {
- display: grid;
- gap: 18px;
-}
-
-.panel-card {
- padding: 20px;
- border-radius: 26px;
- background: rgba(255, 250, 239, .68);
- border: 1px solid rgba(104, 69, 22, .13);
- box-shadow: 0 18px 48px rgba(86, 57, 17, .09);
- backdrop-filter: blur(16px);
-}
-
-.panel-card h3 {
- margin: 0 0 14px;
- font-family: Georgia, serif;
- font-size: 22px;
- letter-spacing: -.03em;
- color: var(--ink);
-}
-
-.stat-grid {
- display: grid;
- grid-template-columns: repeat(2, 1fr);
- gap: 10px;
-}
-
-.stat {
- padding: 14px;
- border-radius: 18px;
- background: rgba(255,255,255,.52);
- border: 1px solid rgba(82, 54, 17, .10);
-}
-
-.stat b {
- display: block;
- font-size: 24px;
- letter-spacing: -.04em;
- color: var(--ink);
-}
-
-.stat span {
- color: var(--muted);
- font-size: 11px;
- font-weight: 950;
- letter-spacing: .10em;
- text-transform: uppercase;
-}
-
-.track {
- height: 14px;
- border-radius: 999px;
- overflow: hidden;
- background: rgba(80, 54, 18, .12);
- margin-top: 8px;
-}
-
-.track span {
- display: block;
- height: 100%;
- border-radius: inherit;
- background: linear-gradient(90deg, var(--red), var(--gold), var(--green));
-}
-
-.queue {
- display: grid;
- gap: 10px;
-}
-
-.queue-item {
- display: grid;
- grid-template-columns: 38px minmax(0, 1fr) auto;
- gap: 10px;
- align-items: center;
- padding: 10px;
- border-radius: 16px;
- background: rgba(255,255,255,.48);
- border: 1px solid rgba(82, 54, 17, .10);
-}
-
-.queue-item strong {
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- color: var(--ink);
- font-size: 13px;
-}
-
-.queue-item span {
- color: var(--muted);
- font-size: 11px;
-}
-
-.queue-number {
- width: 38px;
- height: 38px;
- display: grid;
- place-items: center;
- border-radius: 13px;
- background: #fff5d8;
- border: 1px solid rgba(82, 54, 17, .12);
- color: #74531c;
- font-family: Georgia, serif;
- font-weight: 900;
- font-size: 14px;
-}
-
-/* Confetti Canvas */
-.confetti-canvas {
- position: fixed;
- inset: 0;
- width: 100%;
- height: 100%;
- pointer-events: none;
- z-index: 9999;
-}
-
-/* Keyframes */
-@keyframes cardExit {
- 0% { transform: translateX(0) rotateY(180deg) rotateZ(0); opacity: 1; }
- 45% { transform: translateX(28px) rotateY(180deg) rotateZ(2deg); opacity: .9; }
- 100% { transform: translateX(-32px) rotateY(180deg) rotateZ(-2deg); opacity: 0; }
-}
-
-@keyframes stampIn {
- 0% { opacity: 0; transform: rotate(-18deg) scale(1.8); }
- 38% { opacity: 1; transform: rotate(-10deg) scale(.9); }
- 58% { transform: rotate(-12deg) scale(1.04); }
- 100% { opacity: 0; transform: rotate(-12deg) scale(1); }
-}
-
-/* Session End */
-.session-end {
- text-align: center;
- padding: 3rem 1rem;
- color: var(--muted);
-}
-
-.session-end h3 {
- font-family: Georgia, serif;
- font-size: 28px;
- color: var(--ink);
- margin: 0 0 0.5rem;
-}
-
-/* Navigation buttons row */
-.review-nav-row {
- display: flex;
- gap: 10px;
- flex-wrap: wrap;
- margin-top: 16px;
- justify-content: center;
-}
-
-@media (max-width: 1120px) {
- .content-grid {
- grid-template-columns: 1fr;
- }
-
- .side-panel {
- grid-template-columns: repeat(2, minmax(0, 1fr));
- }
-}
-
@media (max-width: 760px) {
- .session-header {
- flex-direction: column;
- }
-
- .review-panel {
- min-height: auto;
- }
-
- .stage {
- min-height: 420px;
- }
-
- .flashcard,
- .card-face {
- min-height: 380px;
- }
-
- .side-panel {
- grid-template-columns: 1fr;
- }
-
- .review-button {
- width: 100%;
- }
-
- .controls {
- flex-direction: column;
- align-items: stretch;
+ .review-actions {
+ justify-content: stretch;
}
}
diff --git a/Mindforge.Web/src/components/FlashcardReviewComponent.tsx b/Mindforge.Web/src/components/FlashcardReviewComponent.tsx
index 77c5c07..f4df952 100644
--- a/Mindforge.Web/src/components/FlashcardReviewComponent.tsx
+++ b/Mindforge.Web/src/components/FlashcardReviewComponent.tsx
@@ -1,10 +1,14 @@
-import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
+import { useEffect, useMemo, useState } from 'preact/hooks';
import {
MindforgeApiService,
type FlashcardCard,
type FlashcardLibrarySummary,
} from '../services/MindforgeApiService';
import { Button } from './Button';
+import {
+ FlashcardStudySession,
+ type FlashcardStudySessionLibraryMeta,
+} from './FlashcardStudySession';
import './FlashcardReviewComponent.css';
function groupLibrariesBySubject(libraries: FlashcardLibrarySummary[]) {
@@ -21,7 +25,7 @@ function groupLibrariesBySubject(libraries: FlashcardLibrarySummary[]) {
}
function difficultyLabel(difficulty: string) {
- return difficulty === 'Medium' ? 'Medio' : 'Facil';
+ return difficulty === 'Medium' ? 'Médio' : 'Fácil';
}
function shuffleCards(cards: FlashcardCard[]) {
@@ -33,86 +37,6 @@ function shuffleCards(cards: FlashcardCard[]) {
return shuffled;
}
-interface ConfettiParticle {
- x: number;
- y: number;
- vx: number;
- vy: number;
- color: string;
- size: number;
- life: number;
- maxLife: number;
- rotation: number;
- rotationSpeed: number;
-}
-
-function fireConfetti(canvas: HTMLCanvasElement) {
- const ctx = canvas.getContext('2d');
- if (!ctx) return;
- const c = ctx;
-
- const w = canvas.width = window.innerWidth;
- const h = canvas.height = window.innerHeight;
-
- const colors = ['#4f8f5a', '#3f7cac', '#c79539', '#7e65a8', '#f2dfb3', '#b75b4d'];
- const particles: ConfettiParticle[] = [];
-
- for (let i = 0; i < 120; i++) {
- particles.push({
- x: Math.random() * w,
- y: -20 - Math.random() * h * 0.5,
- vx: (Math.random() - 0.5) * 6,
- vy: Math.random() * 5 + 2,
- color: colors[Math.floor(Math.random() * colors.length)],
- size: Math.random() * 8 + 4,
- life: 0,
- maxLife: 80 + Math.random() * 60,
- rotation: Math.random() * Math.PI * 2,
- rotationSpeed: (Math.random() - 0.5) * 0.3,
- });
- }
-
- let animating = true;
-
- function animate() {
- if (!animating) return;
- c.clearRect(0, 0, w, h);
-
- let alive = 0;
- for (const p of particles) {
- p.life++;
- if (p.life >= p.maxLife) continue;
- alive++;
- p.x += p.vx;
- p.y += p.vy;
- p.vy += 0.08;
- p.vx *= 0.995;
- p.rotation += p.rotationSpeed;
- const alpha = 1 - p.life / p.maxLife;
- c.save();
- c.globalAlpha = alpha;
- c.translate(p.x, p.y);
- c.rotate(p.rotation);
- c.fillStyle = p.color;
- c.fillRect(-p.size / 2, -p.size / 4, p.size, p.size / 2);
- c.restore();
- }
-
- if (alive > 0) {
- requestAnimationFrame(animate);
- } else {
- c.clearRect(0, 0, w, h);
- animating = false;
- }
- }
-
- requestAnimationFrame(animate);
-
- return () => {
- animating = false;
- };
-}
-
export function FlashcardReviewComponent() {
const [libraries, setLibraries] = useState
([]);
const [selectedLibraryIds, setSelectedLibraryIds] = useState([]);
@@ -120,16 +44,6 @@ export function FlashcardReviewComponent() {
const [loadingSession, setLoadingSession] = useState(false);
const [error, setError] = useState(null);
const [sessionCards, setSessionCards] = useState([]);
- const [currentIndex, setCurrentIndex] = useState(0);
- const [showAnswer, setShowAnswer] = useState(false);
- const [submittingAnswer, setSubmittingAnswer] = useState(false);
- const [cardExiting, setCardExiting] = useState(false);
- const [stampState, setStampState] = useState<'correct' | 'wrong' | null>(null);
- const [flipped, setFlipped] = useState(false);
- const [sessionAnswers, setSessionAnswers] = useState>({});
-
- const confettiRef = useRef(null);
- const flashcardRef = useRef(null);
useEffect(() => {
let cancelled = false;
@@ -160,46 +74,21 @@ export function FlashcardReviewComponent() {
};
}, []);
- useEffect(() => {
- if (showAnswer && flipped) return;
- if (!showAnswer && !flipped) return;
- setFlipped(showAnswer);
- }, [showAnswer, flipped]);
-
const groupedLibraries = useMemo(() => groupLibrariesBySubject(libraries), [libraries]);
- const libraryById = useMemo(() => {
- return new Map(libraries.map((library) => [library.id, library]));
+ const libraryMetaById = useMemo(() => {
+ return new Map(
+ libraries.map((library) => [
+ library.id,
+ {
+ fileName: library.fileName,
+ subject: library.subject || 'Geral',
+ difficultyLabel: difficultyLabel(library.difficulty),
+ },
+ ]),
+ );
}, [libraries]);
- const currentCard = sessionCards[currentIndex];
- const progressPercent = sessionCards.length > 0
- ? ((currentIndex + 1) / sessionCards.length) * 100
- : 0;
-
- useEffect(() => {
- function handleKeyDown(e: KeyboardEvent) {
- if (sessionCards.length === 0 || !currentCard) return;
- if (e.target instanceof HTMLInputElement || e.target instanceof HTMLSelectElement || e.target instanceof HTMLTextAreaElement) return;
-
- if (e.code === 'Space' || e.code === 'Enter') {
- e.preventDefault();
- if (!showAnswer) {
- setShowAnswer(true);
- }
- } else if (e.code === 'KeyC' && showAnswer && !submittingAnswer) {
- e.preventDefault();
- registerReviewAnswer(true);
- } else if (e.code === 'KeyW' && showAnswer && !submittingAnswer) {
- e.preventDefault();
- registerReviewAnswer(false);
- }
- }
-
- window.addEventListener('keydown', handleKeyDown);
- return () => window.removeEventListener('keydown', handleKeyDown);
- }, [sessionCards.length, showAnswer, submittingAnswer, currentCard]);
-
const toggleLibrary = (libraryId: number) => {
if (selectedLibraryIds.includes(libraryId)) {
setSelectedLibraryIds(selectedLibraryIds.filter((id) => id !== libraryId));
@@ -210,27 +99,28 @@ export function FlashcardReviewComponent() {
const startReviewSession = async () => {
if (selectedLibraryIds.length === 0) {
- setError('Selecione ao menos uma biblioteca para iniciar a revisao.');
+ setError('Selecione ao menos uma biblioteca para iniciar a revisão.');
return;
}
setLoadingSession(true);
setError(null);
+ setSessionCards([]);
try {
const response = await MindforgeApiService.createFlashcardReviewSession({
libraryIds: selectedLibraryIds,
});
+ const shuffledCards = shuffleCards(response.cards);
- setSessionCards(shuffleCards(response.cards));
- setCurrentIndex(0);
- setShowAnswer(false);
- setFlipped(false);
- setStampState(null);
- setCardExiting(false);
- setSessionAnswers({});
+ if (shuffledCards.length === 0) {
+ setError('As bibliotecas selecionadas não possuem cartões para revisar.');
+ return;
+ }
+
+ setSessionCards(shuffledCards);
} catch (err: any) {
- setError(err?.message || 'Falha ao iniciar sessao de revisao.');
+ setError(err?.message || 'Falha ao iniciar sessão de revisão.');
} finally {
setLoadingSession(false);
}
@@ -238,87 +128,20 @@ export function FlashcardReviewComponent() {
const endSession = () => {
setSessionCards([]);
- setCurrentIndex(0);
- setShowAnswer(false);
- setSubmittingAnswer(false);
- setFlipped(false);
- setStampState(null);
- setCardExiting(false);
- setSessionAnswers({});
};
- const goToPrevious = () => {
- if (currentIndex === 0) return;
- setCurrentIndex(currentIndex - 1);
- setShowAnswer(false);
- setFlipped(false);
- setStampState(null);
- };
-
- const advanceCard = () => {
- if (currentIndex >= sessionCards.length - 1) {
- endSession();
- return;
- }
- setCurrentIndex(currentIndex + 1);
- setShowAnswer(false);
- setFlipped(false);
- setStampState(null);
- };
-
- const registerReviewAnswer = async (correct: boolean) => {
- if (!currentCard) return;
-
- setSubmittingAnswer(true);
+ const recordReviewAnswer = async (card: FlashcardCard, correct: boolean) => {
setError(null);
-
- if (correct) {
- setStampState('correct');
- if (confettiRef.current) {
- fireConfetti(confettiRef.current);
- }
- } else {
- setStampState('wrong');
- }
-
- setCardExiting(true);
-
- setTimeout(() => {
- setCardExiting(false);
- setStampState(null);
- }, 600);
-
- try {
- await MindforgeApiService.recordFlashcardReviewAnswer({
- cardId: currentCard.id,
- correct,
- });
-
- setSessionAnswers((currentAnswers) => ({
- ...currentAnswers,
- [currentCard.id]: correct,
- }));
-
- setTimeout(() => {
- advanceCard();
- setSubmittingAnswer(false);
- }, 580);
- } catch (err: any) {
- setError(err?.message || 'Falha ao registrar resposta da revisao.');
- setSubmittingAnswer(false);
- setCardExiting(false);
- setStampState(null);
- }
+ await MindforgeApiService.recordFlashcardReviewAnswer({
+ cardId: card.id,
+ correct,
+ });
};
- const correctCount = Object.values(sessionAnswers).filter(Boolean).length;
- const remainingCount = sessionCards.length - currentIndex;
-
return (
-
-
Revisao Flashcards
-
Escolha as bibliotecas para estudar e inicie uma sessao de revisao.
+
Revisão de Flashcards
+
Escolha as bibliotecas para estudar e inicie uma sessão de revisão.
{error &&
{error}
}
@@ -326,7 +149,7 @@ export function FlashcardReviewComponent() {
{loadingLibraries &&
Carregando bibliotecas...
}
{!loadingLibraries && libraries.length === 0 && (
-
Nenhuma biblioteca encontrada. Gere flashcards para comecar.
+
Nenhuma biblioteca encontrada. Gere flashcards para começar.
)}
{!loadingLibraries && libraries.length > 0 && (
@@ -345,7 +168,7 @@ export function FlashcardReviewComponent() {
{library.fileName}
- {library.cardCount} cards - {difficultyLabel(library.difficulty)}
+ {library.cardCount} cartões - {difficultyLabel(library.difficulty)}
@@ -358,144 +181,19 @@ export function FlashcardReviewComponent() {
- {loadingSession ? 'Iniciando...' : 'Iniciar Revisao'}
+ {loadingSession ? 'Iniciando...' : 'Iniciar Revisão'}
)}
- {sessionCards.length > 0 && currentCard && (
-
-
-
-
-
-
{ if (!showAnswer && !submittingAnswer) setShowAnswer(true); }}
- tabIndex={0}
- role="button"
- aria-label={showAnswer ? 'Flashcard revelado' : 'Clique ou pressione Espaco para revelar'}
- >
-
- {stampState && (
-
- {stampState === 'correct' ? 'Correto!' : 'Errado'}
-
- )}
-
- {libraryById.get(currentCard.libraryId)?.subject || 'Geral'}
- {difficultyLabel(libraryById.get(currentCard.libraryId)?.difficulty || 'Easy')}
-
-
- {currentCard.front}
-
-
-
-
-
-
- Resposta
- {difficultyLabel(libraryById.get(currentCard.libraryId)?.difficulty || 'Easy')}
-
-
- {currentCard.back}
-
-
-
-
-
-
-
- registerReviewAnswer(true)}
- disabled={!showAnswer || submittingAnswer}
- >
- Correto
-
- registerReviewAnswer(false)}
- disabled={!showAnswer || submittingAnswer}
- >
- Incorreto
-
-
-
-
-
- Anterior
-
-
- Encerrar Sessao
-
-
-
-
-
-
-
Progresso
-
-
- {currentIndex + 1}/{sessionCards.length}
- Atual
-
-
- {correctCount}
- Corretos
-
-
-
-
-
-
-
-
-
Fila
-
- {sessionCards.slice(currentIndex, currentIndex + 5).map((card, idx) => (
-
- {currentIndex + idx + 1}
- {card.front.substring(0, 40)}{card.front.length > 40 ? '...' : ''}
- {libraryById.get(card.libraryId)?.subject || ''}
-
- ))}
- {remainingCount > 5 && (
-
- +{remainingCount - 5} restantes
-
- )}
-
-
-
-
- )}
-
- {sessionCards.length > 0 && !currentCard && (
-
-
Sessao concluida!
-
Todos os cards foram revisados.
-
- Voltar a selecao
-
-
+ {sessionCards.length > 0 && (
+
)}
);
diff --git a/Mindforge.Web/src/components/FlashcardStudySession.css b/Mindforge.Web/src/components/FlashcardStudySession.css
new file mode 100644
index 0000000..15f2dc3
--- /dev/null
+++ b/Mindforge.Web/src/components/FlashcardStudySession.css
@@ -0,0 +1,644 @@
+.flashcard-study {
+ width: 100%;
+}
+
+.study-error {
+ color: var(--red);
+ font-size: 0.9rem;
+ background: rgba(183, 91, 77, 0.08);
+ border: 1px solid rgba(183, 91, 77, 0.2);
+ border-radius: var(--radius-md);
+ padding: 0.8rem 1rem;
+ margin-bottom: 1rem;
+}
+
+.study-content-grid {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) 330px;
+ gap: 24px;
+ align-items: start;
+}
+
+.study-panel {
+ position: relative;
+ min-height: 650px;
+ padding: clamp(18px, 3vw, 34px);
+ border-radius: var(--radius-xl);
+ background:
+ linear-gradient(145deg, rgba(255,255,255,.54), rgba(255,240,202,.46)),
+ radial-gradient(circle at 15% 20%, rgba(255, 201, 101, .25), transparent 35%),
+ radial-gradient(circle at 90% 10%, rgba(63, 124, 172, .16), transparent 32%);
+ border: 1px solid rgba(104, 69, 22, .13);
+ box-shadow: var(--shadow);
+ overflow: hidden;
+}
+
+.study-panel::before,
+.study-panel::after {
+ content: "";
+ position: absolute;
+ border-radius: 999px;
+ pointer-events: none;
+ filter: blur(2px);
+ opacity: .5;
+}
+
+.study-panel::before {
+ width: 220px;
+ height: 220px;
+ right: -92px;
+ top: 100px;
+ background: rgba(63, 124, 172, .13);
+}
+
+.study-panel::after {
+ width: 180px;
+ height: 180px;
+ left: -70px;
+ bottom: 60px;
+ background: rgba(199, 149, 57, .18);
+}
+
+.study-session-header {
+ position: relative;
+ z-index: 1;
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ gap: 16px;
+ margin-bottom: 24px;
+}
+
+.study-session-title h3 {
+ margin: 0;
+ font-family: Georgia, serif;
+ font-size: clamp(24px, 3vw, 36px);
+ letter-spacing: -.04em;
+ color: var(--ink);
+}
+
+.study-session-title p {
+ max-width: 660px;
+ color: var(--muted);
+ font-size: 15px;
+ line-height: 1.55;
+ margin: 4px 0 0;
+}
+
+.study-score-pill {
+ min-width: 76px;
+ padding: 11px 12px;
+ border-radius: 18px;
+ background: rgba(255,255,255,.55);
+ border: 1px solid rgba(100, 65, 18, .12);
+ text-align: center;
+ box-shadow: inset 0 1px 0 rgba(255,255,255,.72);
+}
+
+.study-score-pill b {
+ display: block;
+ font-size: 19px;
+ color: var(--ink);
+}
+
+.study-score-pill span {
+ color: var(--muted);
+ font-size: 10px;
+ font-weight: 950;
+ letter-spacing: .12em;
+ text-transform: uppercase;
+}
+
+.study-stage {
+ position: relative;
+ z-index: 1;
+ display: grid;
+ place-items: center;
+ min-height: 390px;
+ perspective: 1400px;
+ padding: 22px 0;
+}
+
+.study-flashcard {
+ position: relative;
+ width: min(680px, 100%);
+ min-height: 355px;
+ cursor: pointer;
+ transform-style: preserve-3d;
+ transition: transform .78s var(--ease), filter .35s var(--ease);
+ outline: none;
+}
+
+.study-flashcard:hover .study-card-face {
+ border-color: rgba(63, 124, 172, .28);
+}
+
+.study-flashcard.is-flipped {
+ transform: rotateY(180deg);
+}
+
+.study-flashcard.is-reviewed {
+ animation: studyCardExit .58s var(--ease);
+}
+
+.study-card-face {
+ position: absolute;
+ inset: 0;
+ min-height: 355px;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ padding: clamp(24px, 5vw, 42px);
+ border-radius: 30px;
+ backface-visibility: hidden;
+ background:
+ linear-gradient(90deg, rgba(168, 111, 36, .09) 0 1px, transparent 1px 22px),
+ linear-gradient(rgba(168, 111, 36, .08) 0 1px, transparent 1px 30px),
+ linear-gradient(145deg, #fffaf0, #f5dfaa);
+ border: 1px solid rgba(82, 54, 17, .18);
+ box-shadow: var(--card-shadow), inset 0 0 0 8px rgba(255,255,255,.24);
+ overflow: hidden;
+}
+
+.study-card-face::before {
+ content: "";
+ position: absolute;
+ inset: 18px;
+ border: 1px dashed rgba(82, 54, 17, .18);
+ border-radius: 22px;
+ pointer-events: none;
+}
+
+.study-card-face::after {
+ content: "";
+ position: absolute;
+ width: 140px;
+ height: 140px;
+ right: -46px;
+ bottom: -50px;
+ border-radius: 50%;
+ background: radial-gradient(circle, rgba(199,149,57,.25), transparent 66%);
+ pointer-events: none;
+}
+
+.study-card-back {
+ transform: rotateY(180deg);
+ background:
+ linear-gradient(90deg, rgba(63,124,172,.08) 0 1px, transparent 1px 22px),
+ linear-gradient(rgba(63,124,172,.07) 0 1px, transparent 1px 30px),
+ linear-gradient(145deg, #fffaf1, #dfeef2);
+}
+
+.study-card-meta {
+ position: relative;
+ z-index: 1;
+ display: flex;
+ justify-content: space-between;
+ gap: 12px;
+ align-items: center;
+ color: var(--muted);
+ font-size: 12px;
+ font-weight: 950;
+ letter-spacing: .12em;
+ text-transform: uppercase;
+}
+
+.study-tag,
+.study-status-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ padding: 9px 12px;
+ border-radius: 999px;
+ background: rgba(255,255,255,.54);
+ border: 1px solid rgba(82, 54, 17, .12);
+}
+
+.study-status-badge {
+ font-size: 11px;
+ line-height: 1;
+}
+
+.study-status-icon {
+ width: 16px;
+ height: 16px;
+ border-radius: 999px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ background: rgba(82, 54, 17, .10);
+ font-size: 11px;
+}
+
+.study-status-badge.rag-red {
+ color: var(--red-deep);
+ background: rgba(183, 91, 77, 0.12);
+ border-color: rgba(183, 91, 77, 0.22);
+}
+
+.study-status-badge.rag-amber {
+ color: #74531c;
+ background: rgba(199, 149, 57, 0.14);
+ border-color: rgba(199, 149, 57, 0.24);
+}
+
+.study-status-badge.rag-green {
+ color: var(--green-deep);
+ background: rgba(79, 143, 90, 0.12);
+ border-color: rgba(79, 143, 90, 0.22);
+}
+
+.study-status-badge.rag-grey {
+ color: var(--muted);
+ background: rgba(123, 106, 80, 0.10);
+ border-color: rgba(123, 106, 80, 0.18);
+}
+
+.study-card-question,
+.study-card-answer {
+ position: relative;
+ z-index: 1;
+ display: grid;
+ align-content: center;
+ gap: 16px;
+ min-height: 190px;
+ color: var(--ink);
+ font-family: "Segoe UI", Inter, Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
+}
+
+.study-card-question {
+ font-size: clamp(20px, 2.65vw, 24px);
+ line-height: 1.5;
+}
+
+.study-back-question,
+.study-back-answer {
+ display: grid;
+ gap: 6px;
+}
+
+.study-back-question span,
+.study-back-answer span {
+ color: var(--blue-deep);
+ font-size: 11px;
+ font-weight: 950;
+ letter-spacing: .12em;
+ text-transform: uppercase;
+}
+
+.study-back-question p,
+.study-back-answer p {
+ margin: 0;
+ color: var(--ink);
+}
+
+.study-back-question p {
+ color: #66543d;
+ font-size: clamp(15px, 1.7vw, 18px);
+ line-height: 1.45;
+}
+
+.study-back-answer p {
+ font-size: clamp(20px, 2.65vw, 24px);
+ line-height: 1.5;
+}
+
+.study-card-details {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ color: var(--muted);
+ font-size: 12px;
+ font-weight: 800;
+}
+
+.study-card-footer {
+ position: relative;
+ z-index: 1;
+ display: flex;
+ justify-content: space-between;
+ gap: 14px;
+ align-items: center;
+ color: var(--muted);
+ font-size: 13px;
+ font-weight: 800;
+}
+
+.study-spacebar {
+ padding: 4px 9px;
+ border-radius: 8px;
+ color: #4f3a1d;
+ background: rgba(255,255,255,.58);
+ border: 1px solid rgba(82, 54, 17, .14);
+ box-shadow: inset 0 -2px 0 rgba(82, 54, 17, .08);
+ font-size: 11px;
+ font-weight: 950;
+ letter-spacing: .08em;
+ text-transform: uppercase;
+}
+
+.study-controls {
+ position: relative;
+ z-index: 1;
+ display: flex;
+ justify-content: center;
+ gap: 16px;
+ flex-wrap: wrap;
+ opacity: .36;
+ transform: translateY(10px);
+ pointer-events: none;
+ transition: .35s var(--ease);
+ margin-top: 20px;
+}
+
+.study-controls.ready {
+ opacity: 1;
+ transform: translateY(0);
+ pointer-events: auto;
+}
+
+.study-review-button {
+ position: relative;
+ min-width: 170px;
+ min-height: 60px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 10px;
+ border: 0;
+ border-radius: 20px;
+ color: white;
+ font-family: inherit;
+ font-weight: 950;
+ font-size: 16px;
+ cursor: pointer;
+ box-shadow: 0 16px 34px rgba(63, 44, 20, .18), inset 0 1px 0 rgba(255,255,255,.32);
+ overflow: hidden;
+ transition: .25s var(--ease);
+}
+
+.study-review-button.correct {
+ background: linear-gradient(135deg, var(--green), var(--green-deep));
+}
+
+.study-review-button.wrong {
+ background: linear-gradient(135deg, var(--red), var(--red-deep));
+}
+
+.study-review-button:hover {
+ transform: translateY(-4px);
+ box-shadow: 0 22px 42px rgba(63, 44, 20, .22), inset 0 1px 0 rgba(255,255,255,.32);
+}
+
+.study-review-button::before {
+ content: "";
+ position: absolute;
+ inset: -90% -40%;
+ background: linear-gradient(90deg, transparent, rgba(255,255,255,.32), transparent);
+ transform: rotate(20deg) translateX(-80%);
+ transition: .55s var(--ease);
+}
+
+.study-review-button:hover::before {
+ transform: rotate(20deg) translateX(80%);
+}
+
+.study-review-button:disabled {
+ opacity: 0.45;
+ cursor: not-allowed;
+ transform: none !important;
+}
+
+.study-stamp {
+ position: absolute;
+ right: 38px;
+ top: 36px;
+ z-index: 4;
+ padding: 12px 18px;
+ border: 4px double currentColor;
+ border-radius: 10px;
+ font-family: Georgia, serif;
+ font-size: 26px;
+ font-weight: 900;
+ letter-spacing: .08em;
+ text-transform: uppercase;
+ opacity: 0;
+ transform: rotate(-12deg) scale(1.3);
+ pointer-events: none;
+}
+
+.study-stamp.correct {
+ color: var(--green-deep);
+}
+
+.study-stamp.wrong {
+ color: var(--red-deep);
+}
+
+.study-stamp.show {
+ animation: studyStampIn .7s var(--ease);
+}
+
+.study-nav-row {
+ display: flex;
+ gap: 10px;
+ flex-wrap: wrap;
+ margin-top: 16px;
+ justify-content: center;
+}
+
+.study-side-panel {
+ display: grid;
+ gap: 18px;
+}
+
+.study-panel-card {
+ padding: 20px;
+ border-radius: 26px;
+ background: rgba(255, 250, 239, .68);
+ border: 1px solid rgba(104, 69, 22, .13);
+ box-shadow: 0 18px 48px rgba(86, 57, 17, .09);
+ backdrop-filter: blur(16px);
+}
+
+.study-panel-card h3 {
+ margin: 0 0 14px;
+ font-family: Georgia, serif;
+ font-size: 22px;
+ letter-spacing: -.03em;
+ color: var(--ink);
+}
+
+.study-stat-grid {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 10px;
+}
+
+.study-stat {
+ padding: 14px;
+ border-radius: 18px;
+ background: rgba(255,255,255,.52);
+ border: 1px solid rgba(82, 54, 17, .10);
+}
+
+.study-stat b {
+ display: block;
+ font-size: 24px;
+ letter-spacing: -.04em;
+ color: var(--ink);
+}
+
+.study-stat span {
+ color: var(--muted);
+ font-size: 11px;
+ font-weight: 950;
+ letter-spacing: .10em;
+ text-transform: uppercase;
+}
+
+.study-track {
+ height: 14px;
+ border-radius: 999px;
+ overflow: hidden;
+ background: rgba(80, 54, 18, .12);
+ margin-top: 8px;
+}
+
+.study-track span {
+ display: block;
+ height: 100%;
+ border-radius: inherit;
+ background: linear-gradient(90deg, var(--red), var(--gold), var(--green));
+}
+
+.study-queue {
+ display: grid;
+ gap: 10px;
+}
+
+.study-queue-item {
+ display: grid;
+ grid-template-columns: 38px minmax(0, 1fr) auto;
+ gap: 10px;
+ align-items: center;
+ padding: 10px;
+ border-radius: 16px;
+ background: rgba(255,255,255,.48);
+ border: 1px solid rgba(82, 54, 17, .10);
+}
+
+.study-queue-item strong {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ color: var(--ink);
+ font-size: 13px;
+}
+
+.study-queue-item span {
+ color: var(--muted);
+ font-size: 11px;
+}
+
+.study-queue-number {
+ width: 38px;
+ height: 38px;
+ display: grid;
+ place-items: center;
+ border-radius: 13px;
+ background: #fff5d8;
+ border: 1px solid rgba(82, 54, 17, .12);
+ color: #74531c;
+ font-family: Georgia, serif;
+ font-weight: 900;
+ font-size: 14px;
+}
+
+.study-queue-more {
+ text-align: center;
+ color: var(--muted);
+ font-size: 12px;
+ padding: 4px;
+}
+
+.study-confetti-canvas {
+ position: fixed;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+ z-index: 9999;
+}
+
+.study-session-end {
+ text-align: center;
+ padding: 3rem 1rem;
+ color: var(--muted);
+}
+
+.study-session-end h3 {
+ font-family: Georgia, serif;
+ font-size: 28px;
+ color: var(--ink);
+ margin: 0 0 0.5rem;
+}
+
+.study-session-end-actions {
+ margin-top: 16px;
+}
+
+@keyframes studyCardExit {
+ 0% { transform: translateX(0) rotateY(180deg) rotateZ(0); opacity: 1; }
+ 45% { transform: translateX(28px) rotateY(180deg) rotateZ(2deg); opacity: .9; }
+ 100% { transform: translateX(-32px) rotateY(180deg) rotateZ(-2deg); opacity: 0; }
+}
+
+@keyframes studyStampIn {
+ 0% { opacity: 0; transform: rotate(-18deg) scale(1.8); }
+ 38% { opacity: 1; transform: rotate(-10deg) scale(.9); }
+ 58% { transform: rotate(-12deg) scale(1.04); }
+ 100% { opacity: 0; transform: rotate(-12deg) scale(1); }
+}
+
+@media (max-width: 1120px) {
+ .study-content-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .study-side-panel {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+}
+
+@media (max-width: 760px) {
+ .study-session-header {
+ flex-direction: column;
+ }
+
+ .study-panel {
+ min-height: auto;
+ }
+
+ .study-stage {
+ min-height: 420px;
+ }
+
+ .study-flashcard,
+ .study-card-face {
+ min-height: 380px;
+ }
+
+ .study-side-panel {
+ grid-template-columns: 1fr;
+ }
+
+ .study-review-button {
+ width: 100%;
+ }
+
+ .study-controls {
+ flex-direction: column;
+ align-items: stretch;
+ }
+}
diff --git a/Mindforge.Web/src/components/FlashcardStudySession.tsx b/Mindforge.Web/src/components/FlashcardStudySession.tsx
new file mode 100644
index 0000000..d03dcfb
--- /dev/null
+++ b/Mindforge.Web/src/components/FlashcardStudySession.tsx
@@ -0,0 +1,410 @@
+import { useEffect, useRef, useState } from 'preact/hooks';
+import type { FlashcardCard } from '../services/MindforgeApiService';
+import { Button } from './Button';
+import './FlashcardStudySession.css';
+
+export interface FlashcardStudySessionLibraryMeta {
+ fileName?: string;
+ subject?: string;
+ subSubject?: string;
+ difficultyLabel?: string;
+ statusLabel?: string;
+ statusIcon?: string;
+ statusClassName?: string;
+ footerDetails?: string[];
+}
+
+interface FlashcardStudySessionProps {
+ cards: FlashcardCard[];
+ libraryMetaById: Map;
+ onAnswer: (card: FlashcardCard, correct: boolean) => Promise;
+ onEnd: () => void;
+}
+
+interface ConfettiParticle {
+ x: number;
+ y: number;
+ vx: number;
+ vy: number;
+ color: string;
+ size: number;
+ life: number;
+ maxLife: number;
+ rotation: number;
+ rotationSpeed: number;
+}
+
+function fireConfetti(canvas: HTMLCanvasElement) {
+ const ctx = canvas.getContext('2d');
+ if (!ctx) return;
+ const c = ctx;
+
+ const w = canvas.width = window.innerWidth;
+ const h = canvas.height = window.innerHeight;
+
+ const colors = ['#4f8f5a', '#3f7cac', '#c79539', '#7e65a8', '#f2dfb3', '#b75b4d'];
+ const particles: ConfettiParticle[] = [];
+
+ for (let i = 0; i < 120; i++) {
+ particles.push({
+ x: Math.random() * w,
+ y: -20 - Math.random() * h * 0.5,
+ vx: (Math.random() - 0.5) * 6,
+ vy: Math.random() * 5 + 2,
+ color: colors[Math.floor(Math.random() * colors.length)],
+ size: Math.random() * 8 + 4,
+ life: 0,
+ maxLife: 80 + Math.random() * 60,
+ rotation: Math.random() * Math.PI * 2,
+ rotationSpeed: (Math.random() - 0.5) * 0.3,
+ });
+ }
+
+ let animating = true;
+
+ function animate() {
+ if (!animating) return;
+ c.clearRect(0, 0, w, h);
+
+ let alive = 0;
+ for (const particle of particles) {
+ particle.life++;
+ if (particle.life >= particle.maxLife) continue;
+ alive++;
+ particle.x += particle.vx;
+ particle.y += particle.vy;
+ particle.vy += 0.08;
+ particle.vx *= 0.995;
+ particle.rotation += particle.rotationSpeed;
+ const alpha = 1 - particle.life / particle.maxLife;
+ c.save();
+ c.globalAlpha = alpha;
+ c.translate(particle.x, particle.y);
+ c.rotate(particle.rotation);
+ c.fillStyle = particle.color;
+ c.fillRect(-particle.size / 2, -particle.size / 4, particle.size, particle.size / 2);
+ c.restore();
+ }
+
+ if (alive > 0) {
+ requestAnimationFrame(animate);
+ } else {
+ c.clearRect(0, 0, w, h);
+ animating = false;
+ }
+ }
+
+ requestAnimationFrame(animate);
+}
+
+function resetTimeout(timeoutRef: { current: number | null }) {
+ if (timeoutRef.current !== null) {
+ window.clearTimeout(timeoutRef.current);
+ timeoutRef.current = null;
+ }
+}
+
+function formatFooter(meta: FlashcardStudySessionLibraryMeta | undefined) {
+ const main = [meta?.fileName, meta?.subSubject].filter(Boolean).join(' - ');
+ return main || 'Arquivo';
+}
+
+export function FlashcardStudySession({ cards, libraryMetaById, onAnswer, onEnd }: FlashcardStudySessionProps) {
+ const [currentIndex, setCurrentIndex] = useState(0);
+ const [showAnswer, setShowAnswer] = useState(false);
+ const [submittingAnswer, setSubmittingAnswer] = useState(false);
+ const [cardExiting, setCardExiting] = useState(false);
+ const [stampState, setStampState] = useState<'correct' | 'wrong' | null>(null);
+ const [flipped, setFlipped] = useState(false);
+ const [sessionAnswers, setSessionAnswers] = useState>({});
+ const [submissionError, setSubmissionError] = useState(null);
+
+ const confettiRef = useRef(null);
+ const stampTimerRef = useRef(null);
+ const advanceTimerRef = useRef(null);
+
+ const currentCard = cards[currentIndex];
+ const currentMeta = currentCard ? libraryMetaById.get(currentCard.libraryId) : undefined;
+ const correctCount = Object.values(sessionAnswers).filter(Boolean).length;
+ const remainingCount = cards.length - currentIndex;
+ const progressPercent = cards.length > 0
+ ? ((currentIndex + 1) / cards.length) * 100
+ : 0;
+
+ useEffect(() => {
+ setCurrentIndex(0);
+ setShowAnswer(false);
+ setSubmittingAnswer(false);
+ setCardExiting(false);
+ setStampState(null);
+ setFlipped(false);
+ setSessionAnswers({});
+ setSubmissionError(null);
+ }, [cards]);
+
+ useEffect(() => {
+ return () => {
+ resetTimeout(stampTimerRef);
+ resetTimeout(advanceTimerRef);
+ };
+ }, []);
+
+ useEffect(() => {
+ if (showAnswer && flipped) return;
+ if (!showAnswer && !flipped) return;
+ setFlipped(showAnswer);
+ }, [showAnswer, flipped]);
+
+ useEffect(() => {
+ function handleKeyDown(e: KeyboardEvent) {
+ if (cards.length === 0 || !currentCard) return;
+ if (e.target instanceof HTMLInputElement || e.target instanceof HTMLSelectElement || e.target instanceof HTMLTextAreaElement) return;
+
+ if (e.code === 'Space' || e.code === 'Enter') {
+ e.preventDefault();
+ if (!showAnswer) {
+ setShowAnswer(true);
+ }
+ } else if (e.code === 'KeyC' && showAnswer && !submittingAnswer) {
+ e.preventDefault();
+ void registerReviewAnswer(true);
+ } else if (e.code === 'KeyW' && showAnswer && !submittingAnswer) {
+ e.preventDefault();
+ void registerReviewAnswer(false);
+ }
+ }
+
+ window.addEventListener('keydown', handleKeyDown);
+ return () => window.removeEventListener('keydown', handleKeyDown);
+ }, [cards.length, showAnswer, submittingAnswer, currentCard]);
+
+ const goToPrevious = () => {
+ if (currentIndex === 0 || submittingAnswer) return;
+ setCurrentIndex(currentIndex - 1);
+ setShowAnswer(false);
+ setFlipped(false);
+ setStampState(null);
+ setCardExiting(false);
+ setSubmissionError(null);
+ };
+
+ const advanceCard = () => {
+ if (currentIndex >= cards.length - 1) {
+ onEnd();
+ return;
+ }
+
+ setCurrentIndex(currentIndex + 1);
+ setShowAnswer(false);
+ setFlipped(false);
+ setStampState(null);
+ setCardExiting(false);
+ setSubmittingAnswer(false);
+ setSubmissionError(null);
+ };
+
+ const registerReviewAnswer = async (correct: boolean) => {
+ if (!currentCard || !showAnswer || submittingAnswer) return;
+
+ resetTimeout(stampTimerRef);
+ resetTimeout(advanceTimerRef);
+ setSubmittingAnswer(true);
+ setSubmissionError(null);
+
+ try {
+ await onAnswer(currentCard, correct);
+
+ setStampState(correct ? 'correct' : 'wrong');
+
+ if (correct && confettiRef.current) {
+ fireConfetti(confettiRef.current);
+ }
+
+ setSessionAnswers((currentAnswers) => ({
+ ...currentAnswers,
+ [currentCard.id]: correct,
+ }));
+
+ stampTimerRef.current = window.setTimeout(() => {
+ setCardExiting(true);
+ stampTimerRef.current = null;
+ }, 90);
+
+ advanceTimerRef.current = window.setTimeout(() => {
+ advanceCard();
+ advanceTimerRef.current = null;
+ }, 760);
+ } catch (err: any) {
+ resetTimeout(stampTimerRef);
+ resetTimeout(advanceTimerRef);
+ setSubmissionError(err?.message || 'Falha ao registrar resposta da revisão.');
+ setSubmittingAnswer(false);
+ setCardExiting(false);
+ setStampState(null);
+ }
+ };
+
+ if (!currentCard) {
+ return (
+
+
Sessão concluída!
+
Todos os cartões foram revisados.
+
+ Voltar à seleção
+
+
+ );
+ }
+
+ return (
+
+
+ {submissionError &&
{submissionError}
}
+
+
+
+
+
+
+
{ if (!showAnswer && !submittingAnswer) setShowAnswer(true); }}
+ tabIndex={0}
+ role="button"
+ aria-label={showAnswer ? 'Flashcard revelado' : 'Clique ou pressione Espaço para revelar'}
+ >
+
+
+ {currentMeta?.subject || 'Geral'}
+ {currentMeta?.difficultyLabel || currentMeta?.statusLabel || 'Revisão'}
+
+
+ {currentCard.front}
+
+
+
+
+
+ {stampState && (
+
+ {stampState === 'correct' ? 'Correto' : 'Incorreto'}
+
+ )}
+
+ Resposta
+ {currentMeta?.statusLabel ? (
+
+ {currentMeta.statusIcon && {currentMeta.statusIcon} }
+ {currentMeta.statusLabel}
+
+ ) : (
+ {currentMeta?.difficultyLabel || 'Revisão'}
+ )}
+
+
+
+
Pergunta
+
{currentCard.front}
+
+
+
Resposta
+
{currentCard.back}
+
+ {currentMeta?.footerDetails && currentMeta.footerDetails.length > 0 && (
+
+ {currentMeta.footerDetails.map((detail) => (
+ {detail}
+ ))}
+
+ )}
+
+
+
+
+
+
+
+ void registerReviewAnswer(true)}
+ disabled={!showAnswer || submittingAnswer}
+ >
+ Correto
+
+ void registerReviewAnswer(false)}
+ disabled={!showAnswer || submittingAnswer}
+ >
+ Incorreto
+
+
+
+
+
+ Anterior
+
+
+ Encerrar Sessão
+
+
+
+
+
+
+
Progresso
+
+
+ {currentIndex + 1}/{cards.length}
+ Atual
+
+
+ {correctCount}
+ Corretos
+
+
+
+
+
+
+
+
+
Fila
+
+ {cards.slice(currentIndex, currentIndex + 5).map((card, index) => {
+ const meta = libraryMetaById.get(card.libraryId);
+ return (
+
+ {currentIndex + index + 1}
+ {card.front.substring(0, 40)}{card.front.length > 40 ? '...' : ''}
+ {meta?.subject || ''}
+
+ );
+ })}
+ {remainingCount > 5 && (
+
+ +{remainingCount - 5} restantes
+
+ )}
+
+
+
+
+
+ );
+}
diff --git a/Mindforge.Web/src/components/Header.tsx b/Mindforge.Web/src/components/Header.tsx
index 624dcee..67eb0f8 100644
--- a/Mindforge.Web/src/components/Header.tsx
+++ b/Mindforge.Web/src/components/Header.tsx
@@ -24,7 +24,7 @@ export function Header({ onGoHome }: HeaderProps) {
{repoName && (
- Repo
+ Repositório
{repoName}
diff --git a/Mindforge.Web/src/components/Sidebar.css b/Mindforge.Web/src/components/Sidebar.css
index 2765870..40f290b 100644
--- a/Mindforge.Web/src/components/Sidebar.css
+++ b/Mindforge.Web/src/components/Sidebar.css
@@ -18,6 +18,24 @@
align-items: center;
gap: 14px;
padding: 12px 12px 24px;
+ width: 100%;
+ border: 0;
+ border-radius: 18px;
+ background: transparent;
+ color: inherit;
+ font: inherit;
+ text-align: left;
+ cursor: pointer;
+ transition: background .25s var(--ease);
+}
+
+.brand:hover {
+ background: rgba(255,255,255,.34);
+}
+
+.brand:focus-visible {
+ outline: 2px solid rgba(63, 124, 172, .52);
+ outline-offset: 3px;
}
.brand-mark {
@@ -66,11 +84,13 @@
gap: 4px;
flex: 1;
overflow-y: auto;
+ overflow-x: hidden;
+ padding-right: 4px;
}
.nav-item {
position: relative;
- width: 100%;
+ width: calc(100% - 4px);
display: flex;
align-items: center;
gap: 12px;
@@ -165,9 +185,13 @@
.nav-list {
flex-direction: row;
+ flex: 0 0 auto;
+ overflow: visible;
+ padding-right: 0;
}
.nav-item {
+ width: auto;
min-width: 56px;
justify-content: center;
}
diff --git a/Mindforge.Web/src/components/Sidebar.tsx b/Mindforge.Web/src/components/Sidebar.tsx
index 099abfe..bf69c54 100644
--- a/Mindforge.Web/src/components/Sidebar.tsx
+++ b/Mindforge.Web/src/components/Sidebar.tsx
@@ -8,20 +8,20 @@ interface SidebarProps {
const NAV_ITEMS: { module: SidebarProps['activeModule']; icon: string; label: string }[] = [
{ module: 'verificador', icon: '\u2713', label: 'Verificador' },
{ module: 'flashcards', icon: '\u25A6', label: 'Flashcards' },
- { module: 'revisao-flashcards', icon: '\u25B3', label: 'Revisao Flashcards' },
- { module: 'revisao-espacada', icon: '\u25CB', label: 'Revisao Espacada' },
+ { module: 'revisao-flashcards', icon: '\u25B3', label: 'Revisão de Flashcards' },
+ { module: 'revisao-espacada', icon: '\u25CB', label: 'Revisão espaçada' },
];
export function Sidebar({ onModuleChange, activeModule }: SidebarProps) {
return (
);
diff --git a/Mindforge.Web/src/components/VerificadorComponent.tsx b/Mindforge.Web/src/components/VerificadorComponent.tsx
index 854d31a..bdadcb1 100644
--- a/Mindforge.Web/src/components/VerificadorComponent.tsx
+++ b/Mindforge.Web/src/components/VerificadorComponent.tsx
@@ -32,7 +32,7 @@ export function VerificadorComponent() {
const handleSubmit = async () => {
if (selectedPaths.length === 0) {
- setError('Selecione pelo menos um arquivo do repositorio.');
+ setError('Selecione pelo menos um arquivo do repositório.');
return;
}
@@ -98,29 +98,29 @@ export function VerificadorComponent() {
return (