ui fixes
All checks were successful
Mindforge Web Build and Deploy (internal) / Build Mindforge Web Image (push) Successful in 2m34s
Mindforge Web Build and Deploy (internal) / Deploy Mindforge Web (internal) (push) Successful in 7s

This commit is contained in:
2026-06-14 15:15:39 -03:00
parent e024b403e2
commit 475a7c120d
5 changed files with 27 additions and 111 deletions

View File

@@ -5,10 +5,7 @@ import {
type FlashcardLibrarySummary, type FlashcardLibrarySummary,
} from '../services/MindforgeApiService'; } from '../services/MindforgeApiService';
import { Button } from './Button'; import { Button } from './Button';
import { import { FlashcardStudySession } from './FlashcardStudySession';
FlashcardStudySession,
type FlashcardStudySessionLibraryMeta,
} from './FlashcardStudySession';
import './FlashcardReviewComponent.css'; import './FlashcardReviewComponent.css';
function groupLibrariesBySubject(libraries: FlashcardLibrarySummary[]) { function groupLibrariesBySubject(libraries: FlashcardLibrarySummary[]) {
@@ -76,18 +73,6 @@ export function FlashcardReviewComponent() {
const groupedLibraries = useMemo(() => groupLibrariesBySubject(libraries), [libraries]); const groupedLibraries = useMemo(() => groupLibrariesBySubject(libraries), [libraries]);
const libraryMetaById = useMemo(() => {
return new Map<number, FlashcardStudySessionLibraryMeta>(
libraries.map((library) => [
library.id,
{
fileName: library.fileName,
difficultyLabel: difficultyLabel(library.difficulty),
},
]),
);
}, [libraries]);
const toggleLibrary = (libraryId: number) => { const toggleLibrary = (libraryId: number) => {
if (selectedLibraryIds.includes(libraryId)) { if (selectedLibraryIds.includes(libraryId)) {
setSelectedLibraryIds(selectedLibraryIds.filter((id) => id !== libraryId)); setSelectedLibraryIds(selectedLibraryIds.filter((id) => id !== libraryId));
@@ -189,7 +174,6 @@ export function FlashcardReviewComponent() {
{sessionCards.length > 0 && ( {sessionCards.length > 0 && (
<FlashcardStudySession <FlashcardStudySession
cards={sessionCards} cards={sessionCards}
libraryMetaById={libraryMetaById}
onAnswer={recordReviewAnswer} onAnswer={recordReviewAnswer}
onEnd={endSession} onEnd={endSession}
/> />

View File

@@ -147,7 +147,6 @@
min-height: 355px; min-height: 355px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between;
padding: clamp(24px, 5vw, 42px); padding: clamp(24px, 5vw, 42px);
border-radius: 30px; border-radius: 30px;
backface-visibility: hidden; backface-visibility: hidden;
@@ -220,7 +219,8 @@
display: grid; display: grid;
align-content: center; align-content: center;
gap: 18px; gap: 18px;
min-height: 190px; flex: 1;
min-height: 0;
color: var(--ink); color: var(--ink);
font-family: "Segoe UI", Inter, Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif; font-family: "Segoe UI", Inter, Avenir, system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
} }
@@ -262,18 +262,6 @@
line-height: 1.45; 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 { .study-controls {
position: relative; position: relative;
z-index: 1; z-index: 1;

View File

@@ -3,14 +3,8 @@ import type { FlashcardCard } from '../services/MindforgeApiService';
import { Button } from './Button'; import { Button } from './Button';
import './FlashcardStudySession.css'; import './FlashcardStudySession.css';
export interface FlashcardStudySessionLibraryMeta {
fileName?: string;
difficultyLabel?: string;
}
interface FlashcardStudySessionProps { interface FlashcardStudySessionProps {
cards: FlashcardCard[]; cards: FlashcardCard[];
libraryMetaById: Map<number, FlashcardStudySessionLibraryMeta>;
onAnswer: (card: FlashcardCard, correct: boolean) => Promise<void>; onAnswer: (card: FlashcardCard, correct: boolean) => Promise<void>;
onEnd: () => void; onEnd: () => void;
} }
@@ -98,14 +92,9 @@ function resetTimeout(timeoutRef: { current: number | null }) {
} }
} }
function formatFooter(meta: FlashcardStudySessionLibraryMeta | undefined) { export function FlashcardStudySession({ cards, onAnswer, onEnd }: FlashcardStudySessionProps) {
return meta?.fileName || 'Arquivo';
}
export function FlashcardStudySession({ cards, libraryMetaById, onAnswer, onEnd }: FlashcardStudySessionProps) {
const [currentIndex, setCurrentIndex] = useState(0); const [currentIndex, setCurrentIndex] = useState(0);
const [showAnswer, setShowAnswer] = useState(false); const [showAnswer, setShowAnswer] = useState(false);
const [showBackQuestion, setShowBackQuestion] = useState(false);
const [submittingAnswer, setSubmittingAnswer] = useState(false); const [submittingAnswer, setSubmittingAnswer] = useState(false);
const [cardExiting, setCardExiting] = useState(false); const [cardExiting, setCardExiting] = useState(false);
const [stampState, setStampState] = useState<'correct' | 'wrong' | null>(null); const [stampState, setStampState] = useState<'correct' | 'wrong' | null>(null);
@@ -117,7 +106,6 @@ export function FlashcardStudySession({ cards, libraryMetaById, onAnswer, onEnd
const advanceTimerRef = useRef<number | null>(null); const advanceTimerRef = useRef<number | null>(null);
const currentCard = cards[currentIndex]; const currentCard = cards[currentIndex];
const currentMeta = currentCard ? libraryMetaById.get(currentCard.libraryId) : undefined;
const correctCount = Object.values(sessionAnswers).filter(Boolean).length; const correctCount = Object.values(sessionAnswers).filter(Boolean).length;
const remainingCount = cards.length - currentIndex; const remainingCount = cards.length - currentIndex;
const progressPercent = cards.length > 0 const progressPercent = cards.length > 0
@@ -127,7 +115,6 @@ export function FlashcardStudySession({ cards, libraryMetaById, onAnswer, onEnd
useEffect(() => { useEffect(() => {
setCurrentIndex(0); setCurrentIndex(0);
setShowAnswer(false); setShowAnswer(false);
setShowBackQuestion(false);
setSubmittingAnswer(false); setSubmittingAnswer(false);
setCardExiting(false); setCardExiting(false);
setStampState(null); setStampState(null);
@@ -149,13 +136,12 @@ export function FlashcardStudySession({ cards, libraryMetaById, onAnswer, onEnd
if (e.code === 'Space' || e.code === 'Enter') { if (e.code === 'Space' || e.code === 'Enter') {
e.preventDefault(); e.preventDefault();
if (!showAnswer) { if (submittingAnswer) return;
setShowAnswer(true); setShowAnswer((current) => !current);
} } else if (e.code === 'KeyC' && showAnswer && !submittingAnswer) {
} else if (e.code === 'KeyC' && showAnswer && !showBackQuestion && !submittingAnswer) {
e.preventDefault(); e.preventDefault();
void registerReviewAnswer(true); void registerReviewAnswer(true);
} else if (e.code === 'KeyW' && showAnswer && !showBackQuestion && !submittingAnswer) { } else if (e.code === 'KeyW' && showAnswer && !submittingAnswer) {
e.preventDefault(); e.preventDefault();
void registerReviewAnswer(false); void registerReviewAnswer(false);
} }
@@ -163,13 +149,12 @@ export function FlashcardStudySession({ cards, libraryMetaById, onAnswer, onEnd
window.addEventListener('keydown', handleKeyDown); window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown);
}, [cards.length, currentCard, showAnswer, showBackQuestion, submittingAnswer]); }, [cards.length, currentCard, showAnswer, submittingAnswer]);
const goToPrevious = () => { const goToPrevious = () => {
if (currentIndex === 0 || submittingAnswer) return; if (currentIndex === 0 || submittingAnswer) return;
setCurrentIndex(currentIndex - 1); setCurrentIndex(currentIndex - 1);
setShowAnswer(false); setShowAnswer(false);
setShowBackQuestion(false);
setStampState(null); setStampState(null);
setCardExiting(false); setCardExiting(false);
setSubmissionError(null); setSubmissionError(null);
@@ -183,7 +168,6 @@ export function FlashcardStudySession({ cards, libraryMetaById, onAnswer, onEnd
setCurrentIndex(currentIndex + 1); setCurrentIndex(currentIndex + 1);
setShowAnswer(false); setShowAnswer(false);
setShowBackQuestion(false);
setStampState(null); setStampState(null);
setCardExiting(false); setCardExiting(false);
setSubmittingAnswer(false); setSubmittingAnswer(false);
@@ -191,7 +175,7 @@ export function FlashcardStudySession({ cards, libraryMetaById, onAnswer, onEnd
}; };
const registerReviewAnswer = async (correct: boolean) => { const registerReviewAnswer = async (correct: boolean) => {
if (!currentCard || !showAnswer || showBackQuestion || submittingAnswer) return; if (!currentCard || !showAnswer || submittingAnswer) return;
resetTimeout(stampTimerRef); resetTimeout(stampTimerRef);
resetTimeout(advanceTimerRef); resetTimeout(advanceTimerRef);
@@ -266,37 +250,20 @@ export function FlashcardStudySession({ cards, libraryMetaById, onAnswer, onEnd
class={`study-flashcard${showAnswer ? ' is-flipped' : ''}${cardExiting ? ' is-reviewed' : ''}`} class={`study-flashcard${showAnswer ? ' is-flipped' : ''}${cardExiting ? ' is-reviewed' : ''}`}
onClick={() => { onClick={() => {
if (submittingAnswer) return; if (submittingAnswer) return;
if (!showAnswer) { setShowAnswer((current) => !current);
setShowAnswer(true);
return;
}
if (!showBackQuestion) {
setShowBackQuestion(true);
}
}} }}
tabIndex={0} tabIndex={0}
role="button" role="button"
aria-label={ aria-label={
!showAnswer !showAnswer
? 'Clique ou pressione Espaço para revelar' ? 'Clique ou pressione Espaço para revelar a resposta'
: showBackQuestion : 'Clique ou pressione Espaço para voltar à pergunta'
? 'Flashcard com pergunta e resposta visíveis'
: 'Clique para mostrar a pergunta junto da resposta'
} }
> >
<div class="study-card-face"> <div class="study-card-face">
{currentMeta?.difficultyLabel && (
<div class="study-card-meta">
<span class="study-tag">{currentMeta.difficultyLabel}</span>
</div>
)}
<div class="study-card-question"> <div class="study-card-question">
{currentCard.front} {currentCard.front}
</div> </div>
<div class="study-card-footer">
<span>{formatFooter(currentMeta)}</span>
</div>
</div> </div>
<div class="study-card-face study-card-back"> <div class="study-card-face study-card-back">
@@ -307,22 +274,16 @@ export function FlashcardStudySession({ cards, libraryMetaById, onAnswer, onEnd
)} )}
<div class="study-card-meta"> <div class="study-card-meta">
<span class="study-tag">Resposta</span> <span class="study-tag">Resposta</span>
{currentMeta?.difficultyLabel && <span>{currentMeta.difficultyLabel}</span>}
</div> </div>
<div class="study-card-answer"> <div class="study-card-answer">
<div class="study-back-answer"> <div class="study-back-answer">
<span>Resposta</span> <span>Resposta</span>
<p>{currentCard.back}</p> <p>{currentCard.back}</p>
</div> </div>
{showBackQuestion && (
<div class="study-back-question"> <div class="study-back-question">
<span>Pergunta</span> <span>Pergunta</span>
<p>{currentCard.front}</p> <p>{currentCard.front}</p>
</div> </div>
)}
</div>
<div class="study-card-footer">
<span>{formatFooter(currentMeta)}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -332,14 +293,14 @@ export function FlashcardStudySession({ cards, libraryMetaById, onAnswer, onEnd
<button <button
class="study-review-button correct" class="study-review-button correct"
onClick={() => void registerReviewAnswer(true)} onClick={() => void registerReviewAnswer(true)}
disabled={!showAnswer || showBackQuestion || submittingAnswer} disabled={!showAnswer || submittingAnswer}
> >
Correto Correto
</button> </button>
<button <button
class="study-review-button wrong" class="study-review-button wrong"
onClick={() => void registerReviewAnswer(false)} onClick={() => void registerReviewAnswer(false)}
disabled={!showAnswer || showBackQuestion || submittingAnswer} disabled={!showAnswer || submittingAnswer}
> >
Incorreto Incorreto
</button> </button>

View File

@@ -7,10 +7,7 @@ import {
type FlashcardRagStatus, type FlashcardRagStatus,
} from '../services/MindforgeApiService'; } from '../services/MindforgeApiService';
import { Button } from './Button'; import { Button } from './Button';
import { import { FlashcardStudySession } from './FlashcardStudySession';
FlashcardStudySession,
type FlashcardStudySessionLibraryMeta,
} from './FlashcardStudySession';
import './SpacedReviewComponent.css'; import './SpacedReviewComponent.css';
interface RagStatusOption { interface RagStatusOption {
@@ -102,13 +99,12 @@ export function SpacedReviewComponent() {
const [dashboard, setDashboard] = useState<FlashcardRagDashboardResponse | null>(null); const [dashboard, setDashboard] = useState<FlashcardRagDashboardResponse | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [selectedStatuses, setSelectedStatuses] = useState<FlashcardRagStatus[]>(['Red', 'Amber', 'Green', 'Grey']); const [selectedStatuses, setSelectedStatuses] = useState<FlashcardRagStatus[]>(['Red', 'Amber']);
const [selectedLibraryIds, setSelectedLibraryIds] = useState<number[]>([]); const [selectedLibraryIds, setSelectedLibraryIds] = useState<number[]>([]);
const [selectedSubjects, setSelectedSubjects] = useState<string[]>([]); const [selectedSubjects, setSelectedSubjects] = useState<string[]>([]);
const [selectedSubSubjects, setSelectedSubSubjects] = useState<string[]>([]); const [selectedSubSubjects, setSelectedSubSubjects] = useState<string[]>([]);
const [startingSession, setStartingSession] = useState(false); const [startingSession, setStartingSession] = useState(false);
const [sessionCards, setSessionCards] = useState<FlashcardCard[]>([]); const [sessionCards, setSessionCards] = useState<FlashcardCard[]>([]);
const [sessionLibraries, setSessionLibraries] = useState<FlashcardRagLibrary[]>([]);
const loadDashboard = async (preserveSelection: boolean) => { const loadDashboard = async (preserveSelection: boolean) => {
setLoading(true); setLoading(true);
@@ -138,7 +134,7 @@ export function SpacedReviewComponent() {
return current; return current;
} }
return ['Red', 'Amber', 'Green', 'Grey']; return ['Red', 'Amber'];
}); });
} catch (err: any) { } catch (err: any) {
setError(err?.message || 'Falha ao carregar status de revisão espaçada.'); setError(err?.message || 'Falha ao carregar status de revisão espaçada.');
@@ -183,7 +179,6 @@ export function SpacedReviewComponent() {
setStartingSession(true); setStartingSession(true);
setError(null); setError(null);
setSessionCards([]); setSessionCards([]);
setSessionLibraries([]);
try { try {
const ragByLibraryId = new Map(selectedRagLibraries.map((library) => [library.libraryId, library])); const ragByLibraryId = new Map(selectedRagLibraries.map((library) => [library.libraryId, library]));
@@ -198,7 +193,6 @@ export function SpacedReviewComponent() {
return; return;
} }
setSessionLibraries(selectedRagLibraries);
setSessionCards(orderedCards); setSessionCards(orderedCards);
} catch (err: any) { } catch (err: any) {
setError(err?.message || 'Falha ao iniciar revisão espaçada.'); 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)); libraryIdSet.has(library.libraryId) && statusSet.has(library.ragStatus));
}, [allRagLibraries, selectedLibraryIds, selectedStatuses]); }, [allRagLibraries, selectedLibraryIds, selectedStatuses]);
const sessionLibraryMetaById = useMemo(() => {
return new Map<number, FlashcardStudySessionLibraryMeta>(
sessionLibraries.map((library) => [
library.libraryId,
{
fileName: library.fileName,
},
]),
);
}, [sessionLibraries]);
const toggleStatus = (status: FlashcardRagStatus) => { const toggleStatus = (status: FlashcardRagStatus) => {
if (selectedStatuses.includes(status)) { if (selectedStatuses.includes(status)) {
setSelectedStatuses([]); const next = selectedStatuses.filter((s) => s !== status);
setSelectedStatuses(next.length > 0 ? next : ['Red']);
return; return;
} }
setSelectedStatuses(['Red', 'Amber', 'Green', 'Grey']); setSelectedStatuses([...selectedStatuses, status]);
}; };
const toggleLibrary = (libraryId: number) => { const toggleLibrary = (libraryId: number) => {
@@ -313,7 +297,6 @@ export function SpacedReviewComponent() {
const endSession = () => { const endSession = () => {
setSessionCards([]); setSessionCards([]);
setSessionLibraries([]);
void loadDashboard(true); void loadDashboard(true);
}; };
@@ -460,7 +443,6 @@ export function SpacedReviewComponent() {
{sessionCards.length > 0 && ( {sessionCards.length > 0 && (
<FlashcardStudySession <FlashcardStudySession
cards={sessionCards} cards={sessionCards}
libraryMetaById={sessionLibraryMetaById}
onAnswer={registerAnswer} onAnswer={registerAnswer}
onEnd={endSession} onEnd={endSession}
/> />

View File

@@ -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. - **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. - **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. - **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. - **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). - **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). - **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. - **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). - **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. - **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). - **Responsivo**: Breakpoints em 1120px (sidebar colapsada) e 760px (layout single-column).
### Variáveis CSS (definidas em `index.css`) ### Variáveis CSS (definidas em `index.css`)