improving ui quality
This commit is contained in:
@@ -12,7 +12,7 @@ const minAmount = 10;
|
|||||||
const maxAmount = 50;
|
const maxAmount = 50;
|
||||||
|
|
||||||
function difficultyLabel(difficulty: string) {
|
function difficultyLabel(difficulty: string) {
|
||||||
return difficulty === 'Medium' ? 'Medio' : 'Facil';
|
return difficulty === 'Medium' ? 'Médio' : 'Fácil';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FlashcardComponent() {
|
export function FlashcardComponent() {
|
||||||
@@ -25,7 +25,7 @@ export function FlashcardComponent() {
|
|||||||
|
|
||||||
const handleGenerate = async () => {
|
const handleGenerate = async () => {
|
||||||
if (selectedPaths.length === 0) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,22 +50,21 @@ export function FlashcardComponent() {
|
|||||||
return (
|
return (
|
||||||
<div className="flashcard-container">
|
<div className="flashcard-container">
|
||||||
<h2 className="flashcard-title">Gerador de Flashcards</h2>
|
<h2 className="flashcard-title">Gerador de Flashcards</h2>
|
||||||
<p className="flashcard-subtitle">Selecione os arquivos do repositorio para gerar bibliotecas de flashcards.</p>
|
<p className="flashcard-subtitle">Selecione os arquivos do repositório para gerar bibliotecas de flashcards.</p>
|
||||||
|
|
||||||
<div className="flashcard-form">
|
<div className="flashcard-form">
|
||||||
<div className="input-group">
|
<div className="input-group">
|
||||||
<label>Arquivos do Repositorio</label>
|
<label>Arquivos do Repositório</label>
|
||||||
<FileTreeComponent selectedPaths={selectedPaths} onSelectionChange={setSelectedPaths} />
|
<FileTreeComponent selectedPaths={selectedPaths} onSelectionChange={setSelectedPaths} />
|
||||||
{selectedPaths.length > 0 && (
|
{selectedPaths.length > 0 && (
|
||||||
<div className="selection-meta">
|
<div className="selection-meta">
|
||||||
{selectedPaths.length} arquivo{selectedPaths.length !== 1 ? 's' : ''} selecionado
|
{selectedPaths.length === 1 ? '1 arquivo selecionado' : `${selectedPaths.length} arquivos selecionados`}
|
||||||
{selectedPaths.length !== 1 ? 's' : ''}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="input-group">
|
<div className="input-group">
|
||||||
<label>Quantidade por Arquivo ({minAmount} - {maxAmount})</label>
|
<label>Quantidade por arquivo ({minAmount} - {maxAmount})</label>
|
||||||
<div className="slider-wrapper">
|
<div className="slider-wrapper">
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
@@ -92,7 +91,7 @@ export function FlashcardComponent() {
|
|||||||
onChange={() => setDifficulty('Easy')}
|
onChange={() => setDifficulty('Easy')}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="difficulty-easy" className="radio-label" title="Perguntas mais diretas e objetivas">
|
<label htmlFor="difficulty-easy" className="radio-label" title="Perguntas mais diretas e objetivas">
|
||||||
Facil
|
Fácil
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="radio-item">
|
<div className="radio-item">
|
||||||
@@ -104,8 +103,8 @@ export function FlashcardComponent() {
|
|||||||
checked={difficulty === 'Medium'}
|
checked={difficulty === 'Medium'}
|
||||||
onChange={() => setDifficulty('Medium')}
|
onChange={() => setDifficulty('Medium')}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="difficulty-medium" className="radio-label" title="Perguntas de nivel intermediario">
|
<label htmlFor="difficulty-medium" className="radio-label" title="Perguntas de nível intermediário">
|
||||||
Medio
|
Médio
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -133,10 +132,10 @@ export function FlashcardComponent() {
|
|||||||
<article key={library.id} className="flashcard-result-item">
|
<article key={library.id} className="flashcard-result-item">
|
||||||
<div className="flashcard-result-header">
|
<div className="flashcard-result-header">
|
||||||
<strong>{library.fileName}</strong>
|
<strong>{library.fileName}</strong>
|
||||||
<span>{library.cardCount} cards</span>
|
<span>{library.cardCount} cartões</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flashcard-result-meta">
|
<div className="flashcard-result-meta">
|
||||||
<span>Materia: {library.subject}</span>
|
<span>Matéria: {library.subject}</span>
|
||||||
<span>Dificuldade: {difficultyLabel(library.difficulty)}</span>
|
<span>Dificuldade: {difficultyLabel(library.difficulty)}</span>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
@@ -29,7 +29,6 @@
|
|||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Library Selection */
|
|
||||||
.review-select-panel {
|
.review-select-panel {
|
||||||
background: rgba(255, 250, 239, .68);
|
background: rgba(255, 250, 239, .68);
|
||||||
border: 1px solid rgba(104, 69, 22, .13);
|
border: 1px solid rgba(104, 69, 22, .13);
|
||||||
@@ -109,548 +108,8 @@
|
|||||||
justify-content: flex-end;
|
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) {
|
@media (max-width: 760px) {
|
||||||
.session-header {
|
.review-actions {
|
||||||
flex-direction: column;
|
justify-content: stretch;
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
import { useEffect, useMemo, useState } from 'preact/hooks';
|
||||||
import {
|
import {
|
||||||
MindforgeApiService,
|
MindforgeApiService,
|
||||||
type FlashcardCard,
|
type FlashcardCard,
|
||||||
type FlashcardLibrarySummary,
|
type FlashcardLibrarySummary,
|
||||||
} from '../services/MindforgeApiService';
|
} from '../services/MindforgeApiService';
|
||||||
import { Button } from './Button';
|
import { Button } from './Button';
|
||||||
|
import {
|
||||||
|
FlashcardStudySession,
|
||||||
|
type FlashcardStudySessionLibraryMeta,
|
||||||
|
} from './FlashcardStudySession';
|
||||||
import './FlashcardReviewComponent.css';
|
import './FlashcardReviewComponent.css';
|
||||||
|
|
||||||
function groupLibrariesBySubject(libraries: FlashcardLibrarySummary[]) {
|
function groupLibrariesBySubject(libraries: FlashcardLibrarySummary[]) {
|
||||||
@@ -21,7 +25,7 @@ function groupLibrariesBySubject(libraries: FlashcardLibrarySummary[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function difficultyLabel(difficulty: string) {
|
function difficultyLabel(difficulty: string) {
|
||||||
return difficulty === 'Medium' ? 'Medio' : 'Facil';
|
return difficulty === 'Medium' ? 'Médio' : 'Fácil';
|
||||||
}
|
}
|
||||||
|
|
||||||
function shuffleCards(cards: FlashcardCard[]) {
|
function shuffleCards(cards: FlashcardCard[]) {
|
||||||
@@ -33,86 +37,6 @@ function shuffleCards(cards: FlashcardCard[]) {
|
|||||||
return shuffled;
|
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() {
|
export function FlashcardReviewComponent() {
|
||||||
const [libraries, setLibraries] = useState<FlashcardLibrarySummary[]>([]);
|
const [libraries, setLibraries] = useState<FlashcardLibrarySummary[]>([]);
|
||||||
const [selectedLibraryIds, setSelectedLibraryIds] = useState<number[]>([]);
|
const [selectedLibraryIds, setSelectedLibraryIds] = useState<number[]>([]);
|
||||||
@@ -120,16 +44,6 @@ export function FlashcardReviewComponent() {
|
|||||||
const [loadingSession, setLoadingSession] = useState(false);
|
const [loadingSession, setLoadingSession] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [sessionCards, setSessionCards] = useState<FlashcardCard[]>([]);
|
const [sessionCards, setSessionCards] = useState<FlashcardCard[]>([]);
|
||||||
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<Record<number, boolean>>({});
|
|
||||||
|
|
||||||
const confettiRef = useRef<HTMLCanvasElement>(null);
|
|
||||||
const flashcardRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
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 groupedLibraries = useMemo(() => groupLibrariesBySubject(libraries), [libraries]);
|
||||||
|
|
||||||
const libraryById = useMemo(() => {
|
const libraryMetaById = useMemo(() => {
|
||||||
return new Map(libraries.map((library) => [library.id, library]));
|
return new Map<number, FlashcardStudySessionLibraryMeta>(
|
||||||
|
libraries.map((library) => [
|
||||||
|
library.id,
|
||||||
|
{
|
||||||
|
fileName: library.fileName,
|
||||||
|
subject: library.subject || 'Geral',
|
||||||
|
difficultyLabel: difficultyLabel(library.difficulty),
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
}, [libraries]);
|
}, [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) => {
|
const toggleLibrary = (libraryId: number) => {
|
||||||
if (selectedLibraryIds.includes(libraryId)) {
|
if (selectedLibraryIds.includes(libraryId)) {
|
||||||
setSelectedLibraryIds(selectedLibraryIds.filter((id) => id !== libraryId));
|
setSelectedLibraryIds(selectedLibraryIds.filter((id) => id !== libraryId));
|
||||||
@@ -210,27 +99,28 @@ export function FlashcardReviewComponent() {
|
|||||||
|
|
||||||
const startReviewSession = async () => {
|
const startReviewSession = async () => {
|
||||||
if (selectedLibraryIds.length === 0) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoadingSession(true);
|
setLoadingSession(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
setSessionCards([]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await MindforgeApiService.createFlashcardReviewSession({
|
const response = await MindforgeApiService.createFlashcardReviewSession({
|
||||||
libraryIds: selectedLibraryIds,
|
libraryIds: selectedLibraryIds,
|
||||||
});
|
});
|
||||||
|
const shuffledCards = shuffleCards(response.cards);
|
||||||
|
|
||||||
setSessionCards(shuffleCards(response.cards));
|
if (shuffledCards.length === 0) {
|
||||||
setCurrentIndex(0);
|
setError('As bibliotecas selecionadas não possuem cartões para revisar.');
|
||||||
setShowAnswer(false);
|
return;
|
||||||
setFlipped(false);
|
}
|
||||||
setStampState(null);
|
|
||||||
setCardExiting(false);
|
setSessionCards(shuffledCards);
|
||||||
setSessionAnswers({});
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err?.message || 'Falha ao iniciar sessao de revisao.');
|
setError(err?.message || 'Falha ao iniciar sessão de revisão.');
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingSession(false);
|
setLoadingSession(false);
|
||||||
}
|
}
|
||||||
@@ -238,87 +128,20 @@ export function FlashcardReviewComponent() {
|
|||||||
|
|
||||||
const endSession = () => {
|
const endSession = () => {
|
||||||
setSessionCards([]);
|
setSessionCards([]);
|
||||||
setCurrentIndex(0);
|
|
||||||
setShowAnswer(false);
|
|
||||||
setSubmittingAnswer(false);
|
|
||||||
setFlipped(false);
|
|
||||||
setStampState(null);
|
|
||||||
setCardExiting(false);
|
|
||||||
setSessionAnswers({});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const goToPrevious = () => {
|
const recordReviewAnswer = async (card: FlashcardCard, correct: boolean) => {
|
||||||
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);
|
|
||||||
setError(null);
|
setError(null);
|
||||||
|
await MindforgeApiService.recordFlashcardReviewAnswer({
|
||||||
if (correct) {
|
cardId: card.id,
|
||||||
setStampState('correct');
|
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);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const correctCount = Object.values(sessionAnswers).filter(Boolean).length;
|
|
||||||
const remainingCount = sessionCards.length - currentIndex;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="review-container">
|
<div className="review-container">
|
||||||
<canvas ref={confettiRef} class="confetti-canvas" />
|
<h2 className="review-title">Revisão de Flashcards</h2>
|
||||||
<h2 className="review-title">Revisao Flashcards</h2>
|
<p className="review-subtitle">Escolha as bibliotecas para estudar e inicie uma sessão de revisão.</p>
|
||||||
<p className="review-subtitle">Escolha as bibliotecas para estudar e inicie uma sessao de revisao.</p>
|
|
||||||
|
|
||||||
{error && <div className="review-error">{error}</div>}
|
{error && <div className="review-error">{error}</div>}
|
||||||
|
|
||||||
@@ -326,7 +149,7 @@ export function FlashcardReviewComponent() {
|
|||||||
<div className="review-select-panel">
|
<div className="review-select-panel">
|
||||||
{loadingLibraries && <p className="review-state">Carregando bibliotecas...</p>}
|
{loadingLibraries && <p className="review-state">Carregando bibliotecas...</p>}
|
||||||
{!loadingLibraries && libraries.length === 0 && (
|
{!loadingLibraries && libraries.length === 0 && (
|
||||||
<p className="review-state">Nenhuma biblioteca encontrada. Gere flashcards para comecar.</p>
|
<p className="review-state">Nenhuma biblioteca encontrada. Gere flashcards para começar.</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!loadingLibraries && libraries.length > 0 && (
|
{!loadingLibraries && libraries.length > 0 && (
|
||||||
@@ -345,7 +168,7 @@ export function FlashcardReviewComponent() {
|
|||||||
<div className="review-library-texts">
|
<div className="review-library-texts">
|
||||||
<strong>{library.fileName}</strong>
|
<strong>{library.fileName}</strong>
|
||||||
<span>
|
<span>
|
||||||
{library.cardCount} cards - {difficultyLabel(library.difficulty)}
|
{library.cardCount} cartões - {difficultyLabel(library.difficulty)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
@@ -358,144 +181,19 @@ export function FlashcardReviewComponent() {
|
|||||||
|
|
||||||
<div className="review-actions">
|
<div className="review-actions">
|
||||||
<Button variant="primary" onClick={startReviewSession} disabled={loadingSession || selectedLibraryIds.length === 0}>
|
<Button variant="primary" onClick={startReviewSession} disabled={loadingSession || selectedLibraryIds.length === 0}>
|
||||||
{loadingSession ? 'Iniciando...' : 'Iniciar Revisao'}
|
{loadingSession ? 'Iniciando...' : 'Iniciar Revisão'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{sessionCards.length > 0 && currentCard && (
|
{sessionCards.length > 0 && (
|
||||||
<div class="content-grid">
|
<FlashcardStudySession
|
||||||
<div class="review-panel">
|
cards={sessionCards}
|
||||||
<div class="session-header">
|
libraryMetaById={libraryMetaById}
|
||||||
<div class="session-title">
|
onAnswer={recordReviewAnswer}
|
||||||
<h3>Sessao de Revisao</h3>
|
onEnd={endSession}
|
||||||
<p>{currentIndex + 1} de {sessionCards.length} cards</p>
|
/>
|
||||||
</div>
|
|
||||||
<div class="score-pill">
|
|
||||||
<b>{correctCount}</b>
|
|
||||||
<span>Corretos</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stage">
|
|
||||||
<div
|
|
||||||
ref={flashcardRef}
|
|
||||||
class={`flashcard${flipped ? ' is-flipped' : ''}${cardExiting ? ' is-reviewed' : ''}`}
|
|
||||||
onClick={() => { if (!showAnswer && !submittingAnswer) setShowAnswer(true); }}
|
|
||||||
tabIndex={0}
|
|
||||||
role="button"
|
|
||||||
aria-label={showAnswer ? 'Flashcard revelado' : 'Clique ou pressione Espaco para revelar'}
|
|
||||||
>
|
|
||||||
<div class="card-face">
|
|
||||||
{stampState && (
|
|
||||||
<div class={`stamp ${stampState}${stampState ? ' show' : ''}`}>
|
|
||||||
{stampState === 'correct' ? 'Correto!' : 'Errado'}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div class="card-meta">
|
|
||||||
<span class="tag">{libraryById.get(currentCard.libraryId)?.subject || 'Geral'}</span>
|
|
||||||
<span>{difficultyLabel(libraryById.get(currentCard.libraryId)?.difficulty || 'Easy')}</span>
|
|
||||||
</div>
|
|
||||||
<div class="card-question">
|
|
||||||
{currentCard.front}
|
|
||||||
</div>
|
|
||||||
<div class="card-footer">
|
|
||||||
<span>{libraryById.get(currentCard.libraryId)?.fileName || 'Arquivo'}</span>
|
|
||||||
<span class="spacebar">Espaco</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card-face card-back">
|
|
||||||
<div class="card-meta">
|
|
||||||
<span class="tag">Resposta</span>
|
|
||||||
<span>{difficultyLabel(libraryById.get(currentCard.libraryId)?.difficulty || 'Easy')}</span>
|
|
||||||
</div>
|
|
||||||
<div class="card-answer">
|
|
||||||
{currentCard.back}
|
|
||||||
</div>
|
|
||||||
<div class="card-footer">
|
|
||||||
<span>{libraryById.get(currentCard.libraryId)?.fileName || 'Arquivo'}</span>
|
|
||||||
<span class="spacebar">C / W</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class={`controls${showAnswer ? ' ready' : ''}`}>
|
|
||||||
<button
|
|
||||||
class="review-button correct"
|
|
||||||
onClick={() => registerReviewAnswer(true)}
|
|
||||||
disabled={!showAnswer || submittingAnswer}
|
|
||||||
>
|
|
||||||
Correto
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="review-button wrong"
|
|
||||||
onClick={() => registerReviewAnswer(false)}
|
|
||||||
disabled={!showAnswer || submittingAnswer}
|
|
||||||
>
|
|
||||||
Incorreto
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="review-nav-row">
|
|
||||||
<Button variant="secondary" onClick={goToPrevious} disabled={currentIndex === 0 || submittingAnswer}>
|
|
||||||
Anterior
|
|
||||||
</Button>
|
|
||||||
<Button variant="secondary" onClick={endSession}>
|
|
||||||
Encerrar Sessao
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="side-panel">
|
|
||||||
<div class="panel-card">
|
|
||||||
<h3>Progresso</h3>
|
|
||||||
<div class="stat-grid">
|
|
||||||
<div class="stat">
|
|
||||||
<b>{currentIndex + 1}/{sessionCards.length}</b>
|
|
||||||
<span>Atual</span>
|
|
||||||
</div>
|
|
||||||
<div class="stat">
|
|
||||||
<b>{correctCount}</b>
|
|
||||||
<span>Corretos</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="track">
|
|
||||||
<span style={{ width: `${progressPercent}%` }} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="panel-card">
|
|
||||||
<h3>Fila</h3>
|
|
||||||
<div class="queue">
|
|
||||||
{sessionCards.slice(currentIndex, currentIndex + 5).map((card, idx) => (
|
|
||||||
<div key={card.id} class="queue-item">
|
|
||||||
<span class="queue-number">{currentIndex + idx + 1}</span>
|
|
||||||
<strong>{card.front.substring(0, 40)}{card.front.length > 40 ? '...' : ''}</strong>
|
|
||||||
<span>{libraryById.get(card.libraryId)?.subject || ''}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{remainingCount > 5 && (
|
|
||||||
<div style="text-align:center;color:var(--muted);font-size:12px;padding:4px">
|
|
||||||
+{remainingCount - 5} restantes
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{sessionCards.length > 0 && !currentCard && (
|
|
||||||
<div class="session-end">
|
|
||||||
<h3>Sessao concluida!</h3>
|
|
||||||
<p>Todos os cards foram revisados.</p>
|
|
||||||
<div style="margin-top:16px">
|
|
||||||
<Button variant="primary" onClick={endSession}>Voltar a selecao</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
644
Mindforge.Web/src/components/FlashcardStudySession.css
Normal file
644
Mindforge.Web/src/components/FlashcardStudySession.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
410
Mindforge.Web/src/components/FlashcardStudySession.tsx
Normal file
410
Mindforge.Web/src/components/FlashcardStudySession.tsx
Normal file
@@ -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<number, FlashcardStudySessionLibraryMeta>;
|
||||||
|
onAnswer: (card: FlashcardCard, correct: boolean) => Promise<void>;
|
||||||
|
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<Record<number, boolean>>({});
|
||||||
|
const [submissionError, setSubmissionError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const confettiRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
const stampTimerRef = useRef<number | null>(null);
|
||||||
|
const advanceTimerRef = useRef<number | null>(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 (
|
||||||
|
<div class="study-session-end">
|
||||||
|
<h3>Sessão concluída!</h3>
|
||||||
|
<p>Todos os cartões foram revisados.</p>
|
||||||
|
<div class="study-session-end-actions">
|
||||||
|
<Button variant="primary" onClick={onEnd}>Voltar à seleção</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="flashcard-study">
|
||||||
|
<canvas ref={confettiRef} class="study-confetti-canvas" />
|
||||||
|
{submissionError && <div className="study-error">{submissionError}</div>}
|
||||||
|
|
||||||
|
<div class="study-content-grid">
|
||||||
|
<div class="study-panel">
|
||||||
|
<div class="study-session-header">
|
||||||
|
<div class="study-session-title">
|
||||||
|
<h3>Sessão de Revisão</h3>
|
||||||
|
<p>{currentIndex + 1} de {cards.length} cartões</p>
|
||||||
|
</div>
|
||||||
|
<div class="study-score-pill">
|
||||||
|
<b>{correctCount}</b>
|
||||||
|
<span>Corretos</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="study-stage">
|
||||||
|
<div
|
||||||
|
class={`study-flashcard${flipped ? ' is-flipped' : ''}${cardExiting ? ' is-reviewed' : ''}`}
|
||||||
|
onClick={() => { if (!showAnswer && !submittingAnswer) setShowAnswer(true); }}
|
||||||
|
tabIndex={0}
|
||||||
|
role="button"
|
||||||
|
aria-label={showAnswer ? 'Flashcard revelado' : 'Clique ou pressione Espaço para revelar'}
|
||||||
|
>
|
||||||
|
<div class="study-card-face">
|
||||||
|
<div class="study-card-meta">
|
||||||
|
<span class="study-tag">{currentMeta?.subject || 'Geral'}</span>
|
||||||
|
<span>{currentMeta?.difficultyLabel || currentMeta?.statusLabel || 'Revisão'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="study-card-question">
|
||||||
|
{currentCard.front}
|
||||||
|
</div>
|
||||||
|
<div class="study-card-footer">
|
||||||
|
<span>{formatFooter(currentMeta)}</span>
|
||||||
|
<span class="study-spacebar">Espaço</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="study-card-face study-card-back">
|
||||||
|
{stampState && (
|
||||||
|
<div class={`study-stamp ${stampState} show`}>
|
||||||
|
{stampState === 'correct' ? 'Correto' : 'Incorreto'}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div class="study-card-meta">
|
||||||
|
<span class="study-tag">Resposta</span>
|
||||||
|
{currentMeta?.statusLabel ? (
|
||||||
|
<span class={`study-status-badge ${currentMeta.statusClassName || ''}`}>
|
||||||
|
{currentMeta.statusIcon && <span class="study-status-icon">{currentMeta.statusIcon}</span>}
|
||||||
|
{currentMeta.statusLabel}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span>{currentMeta?.difficultyLabel || 'Revisão'}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="study-card-answer">
|
||||||
|
<div class="study-back-question">
|
||||||
|
<span>Pergunta</span>
|
||||||
|
<p>{currentCard.front}</p>
|
||||||
|
</div>
|
||||||
|
<div class="study-back-answer">
|
||||||
|
<span>Resposta</span>
|
||||||
|
<p>{currentCard.back}</p>
|
||||||
|
</div>
|
||||||
|
{currentMeta?.footerDetails && currentMeta.footerDetails.length > 0 && (
|
||||||
|
<div class="study-card-details">
|
||||||
|
{currentMeta.footerDetails.map((detail) => (
|
||||||
|
<span key={detail}>{detail}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="study-card-footer">
|
||||||
|
<span>{formatFooter(currentMeta)}</span>
|
||||||
|
<span class="study-spacebar">C / W</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class={`study-controls${showAnswer ? ' ready' : ''}`}>
|
||||||
|
<button
|
||||||
|
class="study-review-button correct"
|
||||||
|
onClick={() => void registerReviewAnswer(true)}
|
||||||
|
disabled={!showAnswer || submittingAnswer}
|
||||||
|
>
|
||||||
|
Correto
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="study-review-button wrong"
|
||||||
|
onClick={() => void registerReviewAnswer(false)}
|
||||||
|
disabled={!showAnswer || submittingAnswer}
|
||||||
|
>
|
||||||
|
Incorreto
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="study-nav-row">
|
||||||
|
<Button variant="secondary" onClick={goToPrevious} disabled={currentIndex === 0 || submittingAnswer}>
|
||||||
|
Anterior
|
||||||
|
</Button>
|
||||||
|
<Button variant="secondary" onClick={onEnd}>
|
||||||
|
Encerrar Sessão
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="study-side-panel">
|
||||||
|
<div class="study-panel-card">
|
||||||
|
<h3>Progresso</h3>
|
||||||
|
<div class="study-stat-grid">
|
||||||
|
<div class="study-stat">
|
||||||
|
<b>{currentIndex + 1}/{cards.length}</b>
|
||||||
|
<span>Atual</span>
|
||||||
|
</div>
|
||||||
|
<div class="study-stat">
|
||||||
|
<b>{correctCount}</b>
|
||||||
|
<span>Corretos</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="study-track">
|
||||||
|
<span style={{ width: `${progressPercent}%` }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="study-panel-card">
|
||||||
|
<h3>Fila</h3>
|
||||||
|
<div class="study-queue">
|
||||||
|
{cards.slice(currentIndex, currentIndex + 5).map((card, index) => {
|
||||||
|
const meta = libraryMetaById.get(card.libraryId);
|
||||||
|
return (
|
||||||
|
<div key={card.id} class="study-queue-item">
|
||||||
|
<span class="study-queue-number">{currentIndex + index + 1}</span>
|
||||||
|
<strong>{card.front.substring(0, 40)}{card.front.length > 40 ? '...' : ''}</strong>
|
||||||
|
<span>{meta?.subject || ''}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{remainingCount > 5 && (
|
||||||
|
<div class="study-queue-more">
|
||||||
|
+{remainingCount - 5} restantes
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@ export function Header({ onGoHome }: HeaderProps) {
|
|||||||
{repoName && (
|
{repoName && (
|
||||||
<div class="topbar-right">
|
<div class="topbar-right">
|
||||||
<span class="chip">
|
<span class="chip">
|
||||||
<span style="font-size:10px;font-weight:950;letter-spacing:.14em;text-transform:uppercase;color:var(--blue-deep)">Repo</span>
|
<span style="font-size:10px;font-weight:950;letter-spacing:.14em;text-transform:uppercase;color:var(--blue-deep)">Repositório</span>
|
||||||
{repoName}
|
{repoName}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,6 +18,24 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
padding: 12px 12px 24px;
|
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 {
|
.brand-mark {
|
||||||
@@ -66,11 +84,13 @@
|
|||||||
gap: 4px;
|
gap: 4px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item {
|
.nav-item {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: calc(100% - 4px);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
@@ -165,9 +185,13 @@
|
|||||||
|
|
||||||
.nav-list {
|
.nav-list {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
overflow: visible;
|
||||||
|
padding-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item {
|
.nav-item {
|
||||||
|
width: auto;
|
||||||
min-width: 56px;
|
min-width: 56px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,20 +8,20 @@ interface SidebarProps {
|
|||||||
const NAV_ITEMS: { module: SidebarProps['activeModule']; icon: string; label: string }[] = [
|
const NAV_ITEMS: { module: SidebarProps['activeModule']; icon: string; label: string }[] = [
|
||||||
{ module: 'verificador', icon: '\u2713', label: 'Verificador' },
|
{ module: 'verificador', icon: '\u2713', label: 'Verificador' },
|
||||||
{ module: 'flashcards', icon: '\u25A6', label: 'Flashcards' },
|
{ module: 'flashcards', icon: '\u25A6', label: 'Flashcards' },
|
||||||
{ module: 'revisao-flashcards', icon: '\u25B3', label: 'Revisao Flashcards' },
|
{ module: 'revisao-flashcards', icon: '\u25B3', label: 'Revisão de Flashcards' },
|
||||||
{ module: 'revisao-espacada', icon: '\u25CB', label: 'Revisao Espacada' },
|
{ module: 'revisao-espacada', icon: '\u25CB', label: 'Revisão espaçada' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function Sidebar({ onModuleChange, activeModule }: SidebarProps) {
|
export function Sidebar({ onModuleChange, activeModule }: SidebarProps) {
|
||||||
return (
|
return (
|
||||||
<aside class="sidebar">
|
<aside class="sidebar">
|
||||||
<div class="brand">
|
<button class="brand" type="button" onClick={() => onModuleChange('home')} aria-label="Ir para a página inicial">
|
||||||
<div class="brand-mark">
|
<div class="brand-mark">
|
||||||
<img src="/assets/mindforge.png" alt="M" />
|
<img src="/assets/mindforge.png" alt="M" />
|
||||||
</div>
|
</div>
|
||||||
<h1>Mindforge</h1>
|
<h1>Mindforge</h1>
|
||||||
</div>
|
</button>
|
||||||
<div class="nav-section-title">Modulos</div>
|
<div class="nav-section-title">Módulos</div>
|
||||||
<nav class="nav-list">
|
<nav class="nav-list">
|
||||||
{NAV_ITEMS.map(({ module, icon, label }) => (
|
{NAV_ITEMS.map(({ module, icon, label }) => (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -318,193 +318,8 @@
|
|||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Session Panel */
|
|
||||||
.spaced-review-session-panel {
|
|
||||||
background: rgba(255, 250, 239, .68);
|
|
||||||
border: 1px solid rgba(104, 69, 22, .13);
|
|
||||||
border-radius: var(--radius-xl);
|
|
||||||
padding: clamp(18px, 3vw, 34px);
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
backdrop-filter: blur(16px);
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spaced-review-progress {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.9rem;
|
|
||||||
margin-bottom: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spaced-review-progress span {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: var(--ink);
|
|
||||||
min-width: 60px;
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spaced-review-progress-bar {
|
|
||||||
width: 100%;
|
|
||||||
height: 10px;
|
|
||||||
border-radius: 999px;
|
|
||||||
overflow: hidden;
|
|
||||||
background: rgba(80, 54, 18, .12);
|
|
||||||
}
|
|
||||||
|
|
||||||
.spaced-review-progress-fill {
|
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(90deg, var(--blue), #79a9c8, var(--gold));
|
|
||||||
border-radius: 999px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spaced-review-card {
|
|
||||||
border: 1px solid rgba(82, 54, 17, .14);
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
padding: 1.25rem;
|
|
||||||
background: rgba(255,255,255,.52);
|
|
||||||
min-height: 260px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spaced-review-card header {
|
|
||||||
margin-bottom: 0.8rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 0.8rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spaced-review-card small {
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spaced-review-card h3 {
|
|
||||||
margin: 0 0 0.4rem;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
font-weight: 950;
|
|
||||||
letter-spacing: .12em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: var(--blue-deep);
|
|
||||||
}
|
|
||||||
|
|
||||||
.spaced-review-card p {
|
|
||||||
margin: 0 0 1rem;
|
|
||||||
font-size: 1.05rem;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: var(--ink);
|
|
||||||
font-family: "Segoe UI", Inter, system-ui, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spaced-review-card-meta {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 0.84rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spaced-review-session-actions {
|
|
||||||
margin-top: 1rem;
|
|
||||||
display: flex;
|
|
||||||
gap: 0.65rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Review content grid for session mode */
|
|
||||||
.spaced-review-content-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(0, 1fr) 330px;
|
|
||||||
gap: 24px;
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spaced-review-side-panel {
|
|
||||||
display: grid;
|
|
||||||
gap: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spaced-review-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);
|
|
||||||
}
|
|
||||||
|
|
||||||
.spaced-review-panel-card h3 {
|
|
||||||
margin: 0 0 14px;
|
|
||||||
font-family: Georgia, serif;
|
|
||||||
font-size: 22px;
|
|
||||||
letter-spacing: -.03em;
|
|
||||||
color: var(--ink);
|
|
||||||
}
|
|
||||||
|
|
||||||
.spaced-review-stat-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spaced-review-stat {
|
|
||||||
padding: 14px;
|
|
||||||
border-radius: 18px;
|
|
||||||
background: rgba(255,255,255,.52);
|
|
||||||
border: 1px solid rgba(82, 54, 17, .10);
|
|
||||||
}
|
|
||||||
|
|
||||||
.spaced-review-stat b {
|
|
||||||
display: block;
|
|
||||||
font-size: 24px;
|
|
||||||
letter-spacing: -.04em;
|
|
||||||
color: var(--ink);
|
|
||||||
}
|
|
||||||
|
|
||||||
.spaced-review-stat span {
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 950;
|
|
||||||
letter-spacing: .10em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spaced-review-track {
|
|
||||||
height: 14px;
|
|
||||||
border-radius: 999px;
|
|
||||||
overflow: hidden;
|
|
||||||
background: rgba(80, 54, 18, .12);
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spaced-review-track span {
|
|
||||||
display: block;
|
|
||||||
height: 100%;
|
|
||||||
border-radius: inherit;
|
|
||||||
background: linear-gradient(90deg, var(--red), var(--gold), var(--green));
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1120px) {
|
|
||||||
.spaced-review-content-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spaced-review-side-panel {
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 760px) {
|
@media (max-width: 760px) {
|
||||||
.spaced-review-library-item {
|
.spaced-review-library-item {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spaced-review-session-actions {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
.spaced-review-side-panel {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ import {
|
|||||||
type FlashcardRagStatus,
|
type FlashcardRagStatus,
|
||||||
} from '../services/MindforgeApiService';
|
} from '../services/MindforgeApiService';
|
||||||
import { Button } from './Button';
|
import { Button } from './Button';
|
||||||
|
import {
|
||||||
|
FlashcardStudySession,
|
||||||
|
type FlashcardStudySessionLibraryMeta,
|
||||||
|
} from './FlashcardStudySession';
|
||||||
import './SpacedReviewComponent.css';
|
import './SpacedReviewComponent.css';
|
||||||
|
|
||||||
interface RagStatusOption {
|
interface RagStatusOption {
|
||||||
@@ -59,10 +63,10 @@ function formatPercentage(value: number) {
|
|||||||
|
|
||||||
function summaryText(activeCount: number, greenPercentage: number, attentionPercentage: number) {
|
function summaryText(activeCount: number, greenPercentage: number, attentionPercentage: number) {
|
||||||
if (activeCount === 0) {
|
if (activeCount === 0) {
|
||||||
return 'Sem revisoes avaliaveis';
|
return 'Sem revisões avaliáveis';
|
||||||
}
|
}
|
||||||
|
|
||||||
return `Verde ${formatPercentage(greenPercentage)} | Atencao ${formatPercentage(attentionPercentage)}`;
|
return `Verde ${formatPercentage(greenPercentage)} | Atenção ${formatPercentage(attentionPercentage)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatPerformance(rate: number) {
|
function formatPerformance(rate: number) {
|
||||||
@@ -76,7 +80,7 @@ function formatLastReviewed(value?: string | null) {
|
|||||||
|
|
||||||
const date = new Date(value);
|
const date = new Date(value);
|
||||||
if (Number.isNaN(date.getTime())) {
|
if (Number.isNaN(date.getTime())) {
|
||||||
return 'Data invalida';
|
return 'Data inválida';
|
||||||
}
|
}
|
||||||
|
|
||||||
return date.toLocaleDateString('pt-BR');
|
return date.toLocaleDateString('pt-BR');
|
||||||
@@ -105,9 +109,6 @@ export function SpacedReviewComponent() {
|
|||||||
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 [sessionLibraries, setSessionLibraries] = useState<FlashcardRagLibrary[]>([]);
|
||||||
const [currentIndex, setCurrentIndex] = useState(0);
|
|
||||||
const [showAnswer, setShowAnswer] = useState(false);
|
|
||||||
const [submittingAnswer, setSubmittingAnswer] = useState(false);
|
|
||||||
|
|
||||||
const loadDashboard = async (preserveSelection: boolean) => {
|
const loadDashboard = async (preserveSelection: boolean) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -140,7 +141,7 @@ export function SpacedReviewComponent() {
|
|||||||
return ['Red', 'Amber', 'Green', 'Grey'];
|
return ['Red', 'Amber', 'Green', 'Grey'];
|
||||||
});
|
});
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err?.message || 'Falha ao carregar status de revisao espacada.');
|
setError(err?.message || 'Falha ao carregar status de revisão espaçada.');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -165,12 +166,12 @@ export function SpacedReviewComponent() {
|
|||||||
|
|
||||||
const startSession = async () => {
|
const startSession = async () => {
|
||||||
if (selectedStatuses.length === 0) {
|
if (selectedStatuses.length === 0) {
|
||||||
setError('Selecione ao menos um status para iniciar a revisao.');
|
setError('Selecione ao menos um status para iniciar a revisão.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedLibraryIds.length === 0) {
|
if (selectedLibraryIds.length === 0) {
|
||||||
setError('Selecione ao menos um arquivo para iniciar a revisao.');
|
setError('Selecione ao menos um arquivo para iniciar a revisão.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,6 +182,8 @@ export function SpacedReviewComponent() {
|
|||||||
|
|
||||||
setStartingSession(true);
|
setStartingSession(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
setSessionCards([]);
|
||||||
|
setSessionLibraries([]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const ragByLibraryId = new Map(selectedRagLibraries.map((library) => [library.libraryId, library]));
|
const ragByLibraryId = new Map(selectedRagLibraries.map((library) => [library.libraryId, library]));
|
||||||
@@ -191,16 +194,14 @@ export function SpacedReviewComponent() {
|
|||||||
const orderedCards = orderCardsForSession(filteredCards, ragByLibraryId);
|
const orderedCards = orderCardsForSession(filteredCards, ragByLibraryId);
|
||||||
|
|
||||||
if (orderedCards.length === 0) {
|
if (orderedCards.length === 0) {
|
||||||
setError('Os filtros selecionados nao retornaram cards para revisar.');
|
setError('Os filtros selecionados não retornaram cartões para revisar.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setSessionLibraries(selectedRagLibraries);
|
setSessionLibraries(selectedRagLibraries);
|
||||||
setSessionCards(orderedCards);
|
setSessionCards(orderedCards);
|
||||||
setCurrentIndex(0);
|
|
||||||
setShowAnswer(false);
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err?.message || 'Falha ao iniciar revisao espacada.');
|
setError(err?.message || 'Falha ao iniciar revisão espaçada.');
|
||||||
} finally {
|
} finally {
|
||||||
setStartingSession(false);
|
setStartingSession(false);
|
||||||
}
|
}
|
||||||
@@ -223,20 +224,29 @@ 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 sessionLibraryById = useMemo(() => {
|
const sessionLibraryMetaById = useMemo(() => {
|
||||||
return new Map(sessionLibraries.map((library) => [library.libraryId, library]));
|
return new Map<number, FlashcardStudySessionLibraryMeta>(
|
||||||
|
sessionLibraries.map((library) => {
|
||||||
|
const statusMeta = STATUS_META_BY_STATUS[library.ragStatus];
|
||||||
|
return [
|
||||||
|
library.libraryId,
|
||||||
|
{
|
||||||
|
fileName: library.fileName,
|
||||||
|
subject: library.subject || 'Geral',
|
||||||
|
subSubject: library.subSubject || 'Geral',
|
||||||
|
statusLabel: statusMeta.label,
|
||||||
|
statusIcon: statusMeta.icon,
|
||||||
|
statusClassName: statusMeta.className,
|
||||||
|
footerDetails: [
|
||||||
|
`Desempenho do arquivo: ${formatPerformance(library.performanceRate)}`,
|
||||||
|
`Última revisão: ${formatLastReviewed(library.lastReviewedAt)}`,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}),
|
||||||
|
);
|
||||||
}, [sessionLibraries]);
|
}, [sessionLibraries]);
|
||||||
|
|
||||||
const currentCard = sessionCards[currentIndex];
|
|
||||||
const currentLibrary = currentCard ? sessionLibraryById.get(currentCard.libraryId) : undefined;
|
|
||||||
const currentStatusMeta = currentLibrary
|
|
||||||
? STATUS_META_BY_STATUS[currentLibrary.ragStatus]
|
|
||||||
: STATUS_META_BY_STATUS.Grey;
|
|
||||||
|
|
||||||
const progressPercent = sessionCards.length > 0
|
|
||||||
? ((currentIndex + 1) / sessionCards.length) * 100
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
const toggleStatus = (status: FlashcardRagStatus) => {
|
const toggleStatus = (status: FlashcardRagStatus) => {
|
||||||
if (selectedStatuses.includes(status)) {
|
if (selectedStatuses.includes(status)) {
|
||||||
setSelectedStatuses(selectedStatuses.filter((value) => value !== status));
|
setSelectedStatuses(selectedStatuses.filter((value) => value !== status));
|
||||||
@@ -316,59 +326,27 @@ export function SpacedReviewComponent() {
|
|||||||
const endSession = () => {
|
const endSession = () => {
|
||||||
setSessionCards([]);
|
setSessionCards([]);
|
||||||
setSessionLibraries([]);
|
setSessionLibraries([]);
|
||||||
setCurrentIndex(0);
|
|
||||||
setShowAnswer(false);
|
|
||||||
setSubmittingAnswer(false);
|
|
||||||
void loadDashboard(true);
|
void loadDashboard(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const goToPrevious = () => {
|
const registerAnswer = async (card: FlashcardCard, correct: boolean) => {
|
||||||
if (currentIndex === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setCurrentIndex(currentIndex - 1);
|
|
||||||
setShowAnswer(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const registerAnswer = async (correct: boolean) => {
|
|
||||||
if (!currentCard) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSubmittingAnswer(true);
|
|
||||||
setError(null);
|
setError(null);
|
||||||
|
await MindforgeApiService.recordFlashcardReviewAnswer({
|
||||||
try {
|
cardId: card.id,
|
||||||
await MindforgeApiService.recordFlashcardReviewAnswer({
|
correct,
|
||||||
cardId: currentCard.id,
|
});
|
||||||
correct,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (currentIndex >= sessionCards.length - 1) {
|
|
||||||
endSession();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setCurrentIndex(currentIndex + 1);
|
|
||||||
setShowAnswer(false);
|
|
||||||
} catch (err: any) {
|
|
||||||
setError(err?.message || 'Falha ao registrar resposta da revisao.');
|
|
||||||
} finally {
|
|
||||||
setSubmittingAnswer(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="spaced-review-container">
|
<div className="spaced-review-container">
|
||||||
<h2 className="spaced-review-title">Revisao espacada</h2>
|
<h2 className="spaced-review-title">Revisão espaçada</h2>
|
||||||
<p className="spaced-review-subtitle">Acompanhe o status RAG por arquivo de flashcards.</p>
|
<p className="spaced-review-subtitle">Acompanhe o status RAG por arquivo de flashcards.</p>
|
||||||
|
|
||||||
{error && <div className="spaced-review-error">{error}</div>}
|
{error && <div className="spaced-review-error">{error}</div>}
|
||||||
|
|
||||||
{sessionCards.length === 0 && (
|
{sessionCards.length === 0 && (
|
||||||
<div className="spaced-review-panel">
|
<div className="spaced-review-panel">
|
||||||
{loading && <p className="spaced-review-state">Carregando painel de revisao...</p>}
|
{loading && <p className="spaced-review-state">Carregando painel de revisão...</p>}
|
||||||
{!loading && (!dashboard || dashboard.subjects.length === 0) && (
|
{!loading && (!dashboard || dashboard.subjects.length === 0) && (
|
||||||
<p className="spaced-review-state">Nenhum flashcard encontrado para revisar.</p>
|
<p className="spaced-review-state">Nenhum flashcard encontrado para revisar.</p>
|
||||||
)}
|
)}
|
||||||
@@ -455,9 +433,9 @@ export function SpacedReviewComponent() {
|
|||||||
<div className="spaced-review-library-texts">
|
<div className="spaced-review-library-texts">
|
||||||
<strong>{library.fileName}</strong>
|
<strong>{library.fileName}</strong>
|
||||||
<span>
|
<span>
|
||||||
Cards: {library.cardCount} | Desempenho: {formatPerformance(library.performanceRate)}
|
Cartões: {library.cardCount} | Desempenho: {formatPerformance(library.performanceRate)}
|
||||||
</span>
|
</span>
|
||||||
<small>Ultima revisao: {formatLastReviewed(library.lastReviewedAt)}</small>
|
<small>Última revisão: {formatLastReviewed(library.lastReviewedAt)}</small>
|
||||||
</div>
|
</div>
|
||||||
<span className={`rag-badge ${statusMeta.className}`}>
|
<span className={`rag-badge ${statusMeta.className}`}>
|
||||||
<span className="rag-icon">{statusMeta.icon}</span>
|
<span className="rag-icon">{statusMeta.icon}</span>
|
||||||
@@ -485,105 +463,19 @@ export function SpacedReviewComponent() {
|
|||||||
disabled={startingSession || loading}
|
disabled={startingSession || loading}
|
||||||
onClick={startSession}
|
onClick={startSession}
|
||||||
>
|
>
|
||||||
{startingSession ? 'Iniciando...' : 'Iniciar Revisao Espacada'}
|
{startingSession ? 'Iniciando...' : 'Iniciar Revisão Espaçada'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{sessionCards.length > 0 && currentCard && (
|
{sessionCards.length > 0 && (
|
||||||
<div class="spaced-review-content-grid">
|
<FlashcardStudySession
|
||||||
<div className="spaced-review-session-panel">
|
cards={sessionCards}
|
||||||
<div className="spaced-review-progress">
|
libraryMetaById={sessionLibraryMetaById}
|
||||||
<span>{currentIndex + 1} / {sessionCards.length}</span>
|
onAnswer={registerAnswer}
|
||||||
<div className="spaced-review-progress-bar">
|
onEnd={endSession}
|
||||||
<div className="spaced-review-progress-fill" style={{ width: `${progressPercent}%` }} />
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<article className="spaced-review-card">
|
|
||||||
<header>
|
|
||||||
<small>
|
|
||||||
{currentLibrary?.fileName || 'Arquivo'} - {currentLibrary?.subject || 'Geral'} - {currentLibrary?.subSubject || 'Geral'}
|
|
||||||
</small>
|
|
||||||
<span className={`rag-badge ${currentStatusMeta.className}`}>
|
|
||||||
<span className="rag-icon">{currentStatusMeta.icon}</span>
|
|
||||||
{currentStatusMeta.label}
|
|
||||||
</span>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<h3>Frente</h3>
|
|
||||||
<p>{currentCard.front}</p>
|
|
||||||
|
|
||||||
{showAnswer && (
|
|
||||||
<>
|
|
||||||
<h3>Verso</h3>
|
|
||||||
<p>{currentCard.back}</p>
|
|
||||||
<div className="spaced-review-card-meta">
|
|
||||||
<span>Desempenho do arquivo: {currentLibrary ? formatPerformance(currentLibrary.performanceRate) : '-'}</span>
|
|
||||||
<span>Ultima revisao: {formatLastReviewed(currentLibrary?.lastReviewedAt)}</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<div className="spaced-review-session-actions">
|
|
||||||
<Button variant="secondary" onClick={goToPrevious} disabled={currentIndex === 0 || submittingAnswer}>
|
|
||||||
Anterior
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{!showAnswer && (
|
|
||||||
<Button variant="primary" onClick={() => setShowAnswer(true)}>
|
|
||||||
Revelar Resposta
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showAnswer && (
|
|
||||||
<>
|
|
||||||
<Button variant="primary" onClick={() => registerAnswer(true)} disabled={submittingAnswer}>
|
|
||||||
Acertei
|
|
||||||
</Button>
|
|
||||||
<Button variant="secondary" onClick={() => registerAnswer(false)} disabled={submittingAnswer}>
|
|
||||||
Errei
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button variant="secondary" onClick={endSession}>
|
|
||||||
Encerrar Sessao
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="spaced-review-side-panel">
|
|
||||||
<div class="spaced-review-panel-card">
|
|
||||||
<h3>Progresso</h3>
|
|
||||||
<div class="spaced-review-stat-grid">
|
|
||||||
<div class="spaced-review-stat">
|
|
||||||
<b>{currentIndex + 1}/{sessionCards.length}</b>
|
|
||||||
<span>Atual</span>
|
|
||||||
</div>
|
|
||||||
<div class="spaced-review-stat">
|
|
||||||
<b class={`rag-badge ${currentStatusMeta.className}`} style="font-size:14px;padding:4px 8px">
|
|
||||||
<span class="rag-icon">{currentStatusMeta.icon}</span>
|
|
||||||
{currentStatusMeta.label}
|
|
||||||
</b>
|
|
||||||
<span>Status RAG</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="spaced-review-track">
|
|
||||||
<span style={{ width: `${progressPercent}%` }} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{sessionCards.length > 0 && !currentCard && (
|
|
||||||
<div className="spaced-review-panel" style="text-align:center;padding:3rem 1rem">
|
|
||||||
<h3 style="font-family:Georgia,serif;font-size:28px;color:var(--ink);margin:0 0 0.5rem">Sessao concluida!</h3>
|
|
||||||
<p style="color:var(--muted)">Todos os cards foram revisados.</p>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export function VerificadorComponent() {
|
|||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (selectedPaths.length === 0) {
|
if (selectedPaths.length === 0) {
|
||||||
setError('Selecione pelo menos um arquivo do repositorio.');
|
setError('Selecione pelo menos um arquivo do repositório.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,29 +98,29 @@ export function VerificadorComponent() {
|
|||||||
return (
|
return (
|
||||||
<div className="verificador-container">
|
<div className="verificador-container">
|
||||||
<h2 className="verificador-title">Verificador de Arquivos</h2>
|
<h2 className="verificador-title">Verificador de Arquivos</h2>
|
||||||
<p className="verificador-subtitle">Selecione os arquivos do repositorio para validação de linguagem ou conteudo.</p>
|
<p className="verificador-subtitle">Selecione os arquivos do repositório para validação de linguagem ou conteúdo.</p>
|
||||||
|
|
||||||
<div className="verificador-form">
|
<div className="verificador-form">
|
||||||
<div className="input-group">
|
<div className="input-group">
|
||||||
<label>Arquivos do Repositorio</label>
|
<label>Arquivos do Repositório</label>
|
||||||
<FileTreeComponent selectedPaths={selectedPaths} onSelectionChange={setSelectedPaths} />
|
<FileTreeComponent selectedPaths={selectedPaths} onSelectionChange={setSelectedPaths} />
|
||||||
{selectedPaths.length > 0 && (
|
{selectedPaths.length > 0 && (
|
||||||
<div class="select-status">
|
<div class="select-status">
|
||||||
{selectedPaths.length} arquivo{selectedPaths.length !== 1 ? 's' : ''} selecionado{selectedPaths.length !== 1 ? 's' : ''}
|
{selectedPaths.length === 1 ? '1 arquivo selecionado' : `${selectedPaths.length} arquivos selecionados`}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="input-group">
|
<div className="input-group">
|
||||||
<label>Tipo de Verificacao</label>
|
<label>Tipo de Verificação</label>
|
||||||
<select
|
<select
|
||||||
className="select-input"
|
className="select-input"
|
||||||
value={checkType}
|
value={checkType}
|
||||||
onChange={(e) => setCheckType((e.target as HTMLSelectElement).value as CheckTypeEnum)}
|
onChange={(e) => setCheckType((e.target as HTMLSelectElement).value as CheckTypeEnum)}
|
||||||
>
|
>
|
||||||
<option value="language">Linguagem</option>
|
<option value="language">Linguagem</option>
|
||||||
<option value="content">Conteudo</option>
|
<option value="content">Conteúdo</option>
|
||||||
<option value="both">Linguagem e Conteudo</option>
|
<option value="both">Linguagem e Conteúdo</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -159,7 +159,7 @@ export function VerificadorComponent() {
|
|||||||
|
|
||||||
{!fileResult.error && checkType === 'content' && fileResult.contentResult && (
|
{!fileResult.error && checkType === 'content' && fileResult.contentResult && (
|
||||||
<div className="side-pane">
|
<div className="side-pane">
|
||||||
<div className="pane-title">Conteudo</div>
|
<div className="pane-title">Conteúdo</div>
|
||||||
<div
|
<div
|
||||||
className="response-content markdown-body"
|
className="response-content markdown-body"
|
||||||
dangerouslySetInnerHTML={{ __html: marked.parse(fileResult.contentResult) as string }}
|
dangerouslySetInnerHTML={{ __html: marked.parse(fileResult.contentResult) as string }}
|
||||||
@@ -176,7 +176,7 @@ export function VerificadorComponent() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="side-pane">
|
<div className="side-pane">
|
||||||
<div className="pane-title">Conteudo</div>
|
<div className="pane-title">Conteúdo</div>
|
||||||
<div
|
<div
|
||||||
className="response-content markdown-body"
|
className="response-content markdown-body"
|
||||||
style={{ minHeight: '200px' }}
|
style={{ minHeight: '200px' }}
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ export const MindforgeApiService = {
|
|||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
});
|
});
|
||||||
|
|
||||||
await throwIfNotOk(response, `Erro ao iniciar revisao: ${response.statusText}`);
|
await throwIfNotOk(response, `Erro ao iniciar revisão: ${response.statusText}`);
|
||||||
return response.json();
|
return response.json();
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -201,24 +201,24 @@ export const MindforgeApiService = {
|
|||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
});
|
});
|
||||||
|
|
||||||
await throwIfNotOk(response, `Erro ao registrar resposta da revisao: ${response.statusText}`);
|
await throwIfNotOk(response, `Erro ao registrar resposta da revisão: ${response.statusText}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
async getFlashcardRagStatus(): Promise<FlashcardRagDashboardResponse> {
|
async getFlashcardRagStatus(): Promise<FlashcardRagDashboardResponse> {
|
||||||
const response = await fetch(`${BASE_URL}/api/v1/flashcard/rag-status`);
|
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();
|
return response.json();
|
||||||
},
|
},
|
||||||
|
|
||||||
async getRepositoryInfo(): Promise<RepositoryInfo> {
|
async getRepositoryInfo(): Promise<RepositoryInfo> {
|
||||||
const response = await fetch(`${BASE_URL}/api/v1/repository/info`);
|
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();
|
return response.json();
|
||||||
},
|
},
|
||||||
|
|
||||||
async getRepositoryTree(): Promise<FileTreeNode[]> {
|
async getRepositoryTree(): Promise<FileTreeNode[]> {
|
||||||
const response = await fetch(`${BASE_URL}/api/v1/repository/tree`);
|
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();
|
return response.json();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -138,14 +138,16 @@ Formas de requisição principais:
|
|||||||
- **Tratamento de erros**: `ExceptionHandlingMiddleware` captura `UserException` (400) e exceções genéricas (500).
|
- **Tratamento de erros**: `ExceptionHandlingMiddleware` captura `UserException` (400) e exceções genéricas (500).
|
||||||
|
|
||||||
### UI/UX
|
### 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.
|
- **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, 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).
|
- **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`)
|
||||||
|
|||||||
Reference in New Issue
Block a user