From 031cbb7f42215af55b51e73c0dca1612e6cb0f58 Mon Sep 17 00:00:00 2001 From: Jose Henrique Date: Sun, 14 Jun 2026 11:26:24 -0300 Subject: [PATCH] improving ui quality --- .../src/components/FlashcardComponent.tsx | 23 +- .../components/FlashcardReviewComponent.css | 545 +-------------- .../components/FlashcardReviewComponent.tsx | 390 ++--------- .../src/components/FlashcardStudySession.css | 644 ++++++++++++++++++ .../src/components/FlashcardStudySession.tsx | 410 +++++++++++ Mindforge.Web/src/components/Header.tsx | 2 +- Mindforge.Web/src/components/Sidebar.css | 26 +- Mindforge.Web/src/components/Sidebar.tsx | 10 +- .../src/components/SpacedReviewComponent.css | 185 ----- .../src/components/SpacedReviewComponent.tsx | 212 ++---- .../src/components/VerificadorComponent.tsx | 18 +- .../src/services/MindforgeApiService.ts | 10 +- project-context.md | 6 +- 13 files changed, 1212 insertions(+), 1269 deletions(-) create mode 100644 Mindforge.Web/src/components/FlashcardStudySession.css create mode 100644 Mindforge.Web/src/components/FlashcardStudySession.tsx diff --git a/Mindforge.Web/src/components/FlashcardComponent.tsx b/Mindforge.Web/src/components/FlashcardComponent.tsx index 4f7f91c..e130fcf 100644 --- a/Mindforge.Web/src/components/FlashcardComponent.tsx +++ b/Mindforge.Web/src/components/FlashcardComponent.tsx @@ -12,7 +12,7 @@ const minAmount = 10; const maxAmount = 50; function difficultyLabel(difficulty: string) { - return difficulty === 'Medium' ? 'Medio' : 'Facil'; + return difficulty === 'Medium' ? 'Médio' : 'Fácil'; } export function FlashcardComponent() { @@ -25,7 +25,7 @@ export function FlashcardComponent() { const handleGenerate = async () => { if (selectedPaths.length === 0) { - setError('Selecione pelo menos um arquivo do repositorio para gerar os flashcards.'); + setError('Selecione pelo menos um arquivo do repositório para gerar os flashcards.'); return; } @@ -50,22 +50,21 @@ export function FlashcardComponent() { return (

Gerador de Flashcards

-

Selecione os arquivos do repositorio para gerar bibliotecas de flashcards.

+

Selecione os arquivos do repositório para gerar bibliotecas de flashcards.

- + {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`}
)}
- +
setDifficulty('Easy')} />
@@ -104,8 +103,8 @@ export function FlashcardComponent() { checked={difficulty === 'Medium'} onChange={() => setDifficulty('Medium')} /> -
@@ -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() {
)} - {sessionCards.length > 0 && currentCard && ( -
-
-
-
-

Sessao de Revisao

-

{currentIndex + 1} de {sessionCards.length} cards

-
-
- {correctCount} - Corretos -
-
- -
-
{ 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} -
- -
-
-
- -
- - -
- -
- - -
-
- -
-
-

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.

-
- -
-
+ {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.

+
+ +
+
+ ); + } + + return ( +
+ + {submissionError &&
{submissionError}
} + +
+
+
+
+

Sessão de Revisão

+

{currentIndex + 1} de {cards.length} cartões

+
+
+ {correctCount} + Corretos +
+
+ +
+
{ 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} + ))} +
+ )} +
+ +
+
+
+ +
+ + +
+ +
+ + +
+
+ +
+
+

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 (
)} - {sessionCards.length > 0 && currentCard && ( -
-
-
- {currentIndex + 1} / {sessionCards.length} -
-
-
-
- -
-
- - {currentLibrary?.fileName || 'Arquivo'} - {currentLibrary?.subject || 'Geral'} - {currentLibrary?.subSubject || 'Geral'} - - - {currentStatusMeta.icon} - {currentStatusMeta.label} - -
- -

Frente

-

{currentCard.front}

- - {showAnswer && ( - <> -

Verso

-

{currentCard.back}

-
- Desempenho do arquivo: {currentLibrary ? formatPerformance(currentLibrary.performanceRate) : '-'} - Ultima revisao: {formatLastReviewed(currentLibrary?.lastReviewedAt)} -
- - )} -
- -
- - - {!showAnswer && ( - - )} - - {showAnswer && ( - <> - - - - )} - - -
-
- -
-
-

Progresso

-
-
- {currentIndex + 1}/{sessionCards.length} - Atual -
-
- - {currentStatusMeta.icon} - {currentStatusMeta.label} - - Status RAG -
-
-
- -
-
-
-
- )} - - {sessionCards.length > 0 && !currentCard && ( -
-

Sessao concluida!

-

Todos os cards foram revisados.

-
+ {sessionCards.length > 0 && ( + )}
); 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 (

Verificador de Arquivos

-

Selecione os arquivos do repositorio para validação de linguagem ou conteudo.

+

Selecione os arquivos do repositório para validação de linguagem ou conteúdo.

- + {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`}
)}
- +
@@ -159,7 +159,7 @@ export function VerificadorComponent() { {!fileResult.error && checkType === 'content' && fileResult.contentResult && (
-
Conteudo
+
Conteúdo
-
Conteudo
+
Conteúdo
{ const response = await fetch(`${BASE_URL}/api/v1/flashcard/rag-status`); - await throwIfNotOk(response, `Erro ao buscar status RAG de revisao: ${response.statusText}`); + await throwIfNotOk(response, `Erro ao buscar status RAG de revisão: ${response.statusText}`); return response.json(); }, async getRepositoryInfo(): Promise { const response = await fetch(`${BASE_URL}/api/v1/repository/info`); - await throwIfNotOk(response, `Erro ao buscar info do repositorio: ${response.statusText}`); + await throwIfNotOk(response, `Erro ao buscar info do repositório: ${response.statusText}`); return response.json(); }, async getRepositoryTree(): Promise { const response = await fetch(`${BASE_URL}/api/v1/repository/tree`); - await throwIfNotOk(response, `Erro ao buscar arvore do repositorio: ${response.statusText}`); + await throwIfNotOk(response, `Erro ao buscar árvore do repositório: ${response.statusText}`); return response.json(); }, diff --git a/project-context.md b/project-context.md index 60559f2..866b6fe 100644 --- a/project-context.md +++ b/project-context.md @@ -138,14 +138,16 @@ Formas de requisição principais: - **Tratamento de erros**: `ExceptionHandlingMiddleware` captura `UserException` (400) e exceções genéricas (500). ### UI/UX -- **Idioma**: Todo texto em **português brasileiro**. +- **Idioma**: Todo texto em **português brasileiro**, com acentuação e grafia corretas nas telas e mensagens. +- **Navegação**: Os rótulos visíveis da revisão usam "Revisão de Flashcards" e "Revisão espaçada" como nomes canônicos. A marca Mindforge na sidebar navega para a página inicial. +- **Revisão de flashcards**: "Revisão de Flashcards" e "Revisão espaçada" mantêm seleção/filtros próprios, mas compartilham a mesma sessão visual (`FlashcardStudySession`) com título "Sessão de Revisão", flip 3D, atalhos, fila/progresso, carimbo e confete. - **Tema**: Claro quente vintage ("mesa de estudo pessoal"). Fundos creme/pergaminho/dourado com textura de papel (grid via `body::before`). Vidro translúcido com `backdrop-filter` em tons quentes. - **Botões**: Estilo pill quente com sombras marrons. Variantes `primary` (gradiente azul, com brilho hover) e `secondary` (translúcido pergaminho). - **Tipografia**: Inter (UI global), Georgia/Times New Roman (marca e cabeçalhos), Segoe UI/Inter (conteúdo do flashcard). - **Background**: Gradiente radial quente (dourado/azul) sobre base `#fff8e6` com overlay de grid de papel 24px. - **Layout**: CSS Grid (`grid-template-columns: 288px minmax(0, 1fr)`). Sidebar sticky com textura diagonal. Topbar integrada ao fluxo (não fixa). - **Animações**: Apenas sob ação do usuário (flip 3D do flashcard, carimbo, saída do cartão, confete canvas). Sem animações infinitas ou spinners. -- **Flashcard**: Cartão 3D com efeito `rotateY(180deg)`, frente papel pautado com borda tracejada, verso azulado. Carimbo de feedback (correto/incorreto). Confete canvas ao acertar. +- **Flashcard**: Cartão 3D com efeito `rotateY(180deg)`, frente papel pautado com borda tracejada, verso azulado que mostra pergunta menor + resposta principal. Carimbo de feedback ("Correto"/"Incorreto") é renderizado no verso visível antes da saída do cartão. Confete canvas ao acertar. - **Responsivo**: Breakpoints em 1120px (sidebar colapsada) e 760px (layout single-column). ### Variáveis CSS (definidas em `index.css`)