From 475a7c120de3eb5941d0ebb9c743ac42b145cc76 Mon Sep 17 00:00:00 2001 From: Jose Henrique Date: Sun, 14 Jun 2026 15:15:39 -0300 Subject: [PATCH] ui fixes --- .../components/FlashcardReviewComponent.tsx | 18 +---- .../src/components/FlashcardStudySession.css | 16 +---- .../src/components/FlashcardStudySession.tsx | 71 +++++-------------- .../src/components/SpacedReviewComponent.tsx | 30 ++------ project-context.md | 3 +- 5 files changed, 27 insertions(+), 111 deletions(-) diff --git a/Mindforge.Web/src/components/FlashcardReviewComponent.tsx b/Mindforge.Web/src/components/FlashcardReviewComponent.tsx index a4da8a8..9690c80 100644 --- a/Mindforge.Web/src/components/FlashcardReviewComponent.tsx +++ b/Mindforge.Web/src/components/FlashcardReviewComponent.tsx @@ -5,10 +5,7 @@ import { type FlashcardLibrarySummary, } from '../services/MindforgeApiService'; import { Button } from './Button'; -import { - FlashcardStudySession, - type FlashcardStudySessionLibraryMeta, -} from './FlashcardStudySession'; +import { FlashcardStudySession } from './FlashcardStudySession'; import './FlashcardReviewComponent.css'; function groupLibrariesBySubject(libraries: FlashcardLibrarySummary[]) { @@ -76,18 +73,6 @@ export function FlashcardReviewComponent() { const groupedLibraries = useMemo(() => groupLibrariesBySubject(libraries), [libraries]); - const libraryMetaById = useMemo(() => { - return new Map( - libraries.map((library) => [ - library.id, - { - fileName: library.fileName, - difficultyLabel: difficultyLabel(library.difficulty), - }, - ]), - ); - }, [libraries]); - const toggleLibrary = (libraryId: number) => { if (selectedLibraryIds.includes(libraryId)) { setSelectedLibraryIds(selectedLibraryIds.filter((id) => id !== libraryId)); @@ -189,7 +174,6 @@ export function FlashcardReviewComponent() { {sessionCards.length > 0 && ( diff --git a/Mindforge.Web/src/components/FlashcardStudySession.css b/Mindforge.Web/src/components/FlashcardStudySession.css index f567f54..e2a8d4d 100644 --- a/Mindforge.Web/src/components/FlashcardStudySession.css +++ b/Mindforge.Web/src/components/FlashcardStudySession.css @@ -147,7 +147,6 @@ min-height: 355px; display: flex; flex-direction: column; - justify-content: space-between; padding: clamp(24px, 5vw, 42px); border-radius: 30px; backface-visibility: hidden; @@ -220,7 +219,8 @@ display: grid; align-content: center; gap: 18px; - min-height: 190px; + flex: 1; + min-height: 0; color: var(--ink); font-family: "Segoe UI", Inter, Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; } @@ -262,18 +262,6 @@ line-height: 1.45; } -.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-controls { position: relative; z-index: 1; diff --git a/Mindforge.Web/src/components/FlashcardStudySession.tsx b/Mindforge.Web/src/components/FlashcardStudySession.tsx index fc84ed6..9a32bad 100644 --- a/Mindforge.Web/src/components/FlashcardStudySession.tsx +++ b/Mindforge.Web/src/components/FlashcardStudySession.tsx @@ -3,14 +3,8 @@ import type { FlashcardCard } from '../services/MindforgeApiService'; import { Button } from './Button'; import './FlashcardStudySession.css'; -export interface FlashcardStudySessionLibraryMeta { - fileName?: string; - difficultyLabel?: string; -} - interface FlashcardStudySessionProps { cards: FlashcardCard[]; - libraryMetaById: Map; onAnswer: (card: FlashcardCard, correct: boolean) => Promise; onEnd: () => void; } @@ -98,14 +92,9 @@ function resetTimeout(timeoutRef: { current: number | null }) { } } -function formatFooter(meta: FlashcardStudySessionLibraryMeta | undefined) { - return meta?.fileName || 'Arquivo'; -} - -export function FlashcardStudySession({ cards, libraryMetaById, onAnswer, onEnd }: FlashcardStudySessionProps) { +export function FlashcardStudySession({ cards, onAnswer, onEnd }: FlashcardStudySessionProps) { const [currentIndex, setCurrentIndex] = useState(0); const [showAnswer, setShowAnswer] = useState(false); - const [showBackQuestion, setShowBackQuestion] = useState(false); const [submittingAnswer, setSubmittingAnswer] = useState(false); const [cardExiting, setCardExiting] = useState(false); const [stampState, setStampState] = useState<'correct' | 'wrong' | null>(null); @@ -117,7 +106,6 @@ export function FlashcardStudySession({ cards, libraryMetaById, onAnswer, onEnd 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 @@ -127,7 +115,6 @@ export function FlashcardStudySession({ cards, libraryMetaById, onAnswer, onEnd useEffect(() => { setCurrentIndex(0); setShowAnswer(false); - setShowBackQuestion(false); setSubmittingAnswer(false); setCardExiting(false); setStampState(null); @@ -149,13 +136,12 @@ export function FlashcardStudySession({ cards, libraryMetaById, onAnswer, onEnd if (e.code === 'Space' || e.code === 'Enter') { e.preventDefault(); - if (!showAnswer) { - setShowAnswer(true); - } - } else if (e.code === 'KeyC' && showAnswer && !showBackQuestion && !submittingAnswer) { + if (submittingAnswer) return; + setShowAnswer((current) => !current); + } else if (e.code === 'KeyC' && showAnswer && !submittingAnswer) { e.preventDefault(); void registerReviewAnswer(true); - } else if (e.code === 'KeyW' && showAnswer && !showBackQuestion && !submittingAnswer) { + } else if (e.code === 'KeyW' && showAnswer && !submittingAnswer) { e.preventDefault(); void registerReviewAnswer(false); } @@ -163,13 +149,12 @@ export function FlashcardStudySession({ cards, libraryMetaById, onAnswer, onEnd window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); - }, [cards.length, currentCard, showAnswer, showBackQuestion, submittingAnswer]); + }, [cards.length, currentCard, showAnswer, submittingAnswer]); const goToPrevious = () => { if (currentIndex === 0 || submittingAnswer) return; setCurrentIndex(currentIndex - 1); setShowAnswer(false); - setShowBackQuestion(false); setStampState(null); setCardExiting(false); setSubmissionError(null); @@ -183,7 +168,6 @@ export function FlashcardStudySession({ cards, libraryMetaById, onAnswer, onEnd setCurrentIndex(currentIndex + 1); setShowAnswer(false); - setShowBackQuestion(false); setStampState(null); setCardExiting(false); setSubmittingAnswer(false); @@ -191,7 +175,7 @@ export function FlashcardStudySession({ cards, libraryMetaById, onAnswer, onEnd }; const registerReviewAnswer = async (correct: boolean) => { - if (!currentCard || !showAnswer || showBackQuestion || submittingAnswer) return; + if (!currentCard || !showAnswer || submittingAnswer) return; resetTimeout(stampTimerRef); resetTimeout(advanceTimerRef); @@ -266,37 +250,20 @@ export function FlashcardStudySession({ cards, libraryMetaById, onAnswer, onEnd class={`study-flashcard${showAnswer ? ' is-flipped' : ''}${cardExiting ? ' is-reviewed' : ''}`} onClick={() => { if (submittingAnswer) return; - if (!showAnswer) { - setShowAnswer(true); - return; - } - - if (!showBackQuestion) { - setShowBackQuestion(true); - } + setShowAnswer((current) => !current); }} tabIndex={0} role="button" aria-label={ !showAnswer - ? 'Clique ou pressione Espaço para revelar' - : showBackQuestion - ? 'Flashcard com pergunta e resposta visíveis' - : 'Clique para mostrar a pergunta junto da resposta' + ? 'Clique ou pressione Espaço para revelar a resposta' + : 'Clique ou pressione Espaço para voltar à pergunta' } >
- {currentMeta?.difficultyLabel && ( -
- {currentMeta.difficultyLabel} -
- )}
{currentCard.front}
-
@@ -307,22 +274,16 @@ export function FlashcardStudySession({ cards, libraryMetaById, onAnswer, onEnd )}
Resposta - {currentMeta?.difficultyLabel && {currentMeta.difficultyLabel}}
Resposta

{currentCard.back}

- {showBackQuestion && ( -
- Pergunta -

{currentCard.front}

-
- )} -
-
@@ -332,14 +293,14 @@ export function FlashcardStudySession({ cards, libraryMetaById, onAnswer, onEnd diff --git a/Mindforge.Web/src/components/SpacedReviewComponent.tsx b/Mindforge.Web/src/components/SpacedReviewComponent.tsx index ef82f10..2354df8 100644 --- a/Mindforge.Web/src/components/SpacedReviewComponent.tsx +++ b/Mindforge.Web/src/components/SpacedReviewComponent.tsx @@ -7,10 +7,7 @@ import { type FlashcardRagStatus, } from '../services/MindforgeApiService'; import { Button } from './Button'; -import { - FlashcardStudySession, - type FlashcardStudySessionLibraryMeta, -} from './FlashcardStudySession'; +import { FlashcardStudySession } from './FlashcardStudySession'; import './SpacedReviewComponent.css'; interface RagStatusOption { @@ -102,13 +99,12 @@ export function SpacedReviewComponent() { const [dashboard, setDashboard] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [selectedStatuses, setSelectedStatuses] = useState(['Red', 'Amber', 'Green', 'Grey']); + const [selectedStatuses, setSelectedStatuses] = useState(['Red', 'Amber']); const [selectedLibraryIds, setSelectedLibraryIds] = useState([]); const [selectedSubjects, setSelectedSubjects] = useState([]); const [selectedSubSubjects, setSelectedSubSubjects] = useState([]); const [startingSession, setStartingSession] = useState(false); const [sessionCards, setSessionCards] = useState([]); - const [sessionLibraries, setSessionLibraries] = useState([]); const loadDashboard = async (preserveSelection: boolean) => { setLoading(true); @@ -138,7 +134,7 @@ export function SpacedReviewComponent() { return current; } - return ['Red', 'Amber', 'Green', 'Grey']; + return ['Red', 'Amber']; }); } catch (err: any) { setError(err?.message || 'Falha ao carregar status de revisão espaçada.'); @@ -183,7 +179,6 @@ export function SpacedReviewComponent() { setStartingSession(true); setError(null); setSessionCards([]); - setSessionLibraries([]); try { const ragByLibraryId = new Map(selectedRagLibraries.map((library) => [library.libraryId, library])); @@ -198,7 +193,6 @@ export function SpacedReviewComponent() { return; } - setSessionLibraries(selectedRagLibraries); setSessionCards(orderedCards); } catch (err: any) { setError(err?.message || 'Falha ao iniciar revisão espaçada.'); @@ -224,24 +218,14 @@ export function SpacedReviewComponent() { libraryIdSet.has(library.libraryId) && statusSet.has(library.ragStatus)); }, [allRagLibraries, selectedLibraryIds, selectedStatuses]); - const sessionLibraryMetaById = useMemo(() => { - return new Map( - sessionLibraries.map((library) => [ - library.libraryId, - { - fileName: library.fileName, - }, - ]), - ); - }, [sessionLibraries]); - const toggleStatus = (status: FlashcardRagStatus) => { if (selectedStatuses.includes(status)) { - setSelectedStatuses([]); + const next = selectedStatuses.filter((s) => s !== status); + setSelectedStatuses(next.length > 0 ? next : ['Red']); return; } - setSelectedStatuses(['Red', 'Amber', 'Green', 'Grey']); + setSelectedStatuses([...selectedStatuses, status]); }; const toggleLibrary = (libraryId: number) => { @@ -313,7 +297,6 @@ export function SpacedReviewComponent() { const endSession = () => { setSessionCards([]); - setSessionLibraries([]); void loadDashboard(true); }; @@ -460,7 +443,6 @@ export function SpacedReviewComponent() { {sessionCards.length > 0 && ( diff --git a/project-context.md b/project-context.md index 14e221e..f649988 100644 --- a/project-context.md +++ b/project-context.md @@ -141,13 +141,14 @@ Formas de requisição principais: - **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. +- **Filtro RAG da "Revisão espaçada"**: o painel inicia com apenas Vermelho e Amarelo selecionados. Os checkboxes de status funcionam como multi-select comum (clicar num selecionado o remove, clicar num não selecionado o adiciona); se o usuário chegar a uma seleção vazia, o sistema força `['Red']` automaticamente para impedir revisão sem status. A lista de arquivos é filtrada pela união dos status marcados. - **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 e verso azulado focado na resposta. No verso, um clique extra revela a pergunta; enquanto essa pergunta auxiliar estiver visível, os botões "Correto" e "Incorreto" ficam bloqueados. O card não exibe status RAG nem metadados de assunto/subassunto/desempenho/última revisão. O carimbo de feedback ("Correto"/"Incorreto") é renderizado no verso visível antes da saída do cartão, e há confete canvas ao acertar. +- **Flashcard**: Cartão 3D com efeito `rotateY(180deg)`, frente papel pautado com borda tracejada e verso azulado. O verso exibe a resposta em cima e a pergunta original embaixo, ambos visíveis ao virar o cartão. Clicar no verso retorna à frente. Os botões "Correto" e "Incorreto" ficam desabilitados enquanto a frente está visível. O card não exibe status RAG, metadados de assunto/subassunto/desempenho/última revisão, dificuldade nem nome do arquivo de origem. O carimbo de feedback ("Correto"/"Incorreto") é renderizado no verso visível antes da saída do cartão, e há confete canvas ao acertar. - **Responsivo**: Breakpoints em 1120px (sidebar colapsada) e 760px (layout single-column). ### Variáveis CSS (definidas em `index.css`)