timed revision
All checks were successful
Mindforge API Build and Deploy / Build Mindforge API Image (push) Successful in 3m58s
Mindforge API Build and Deploy / Deploy Mindforge API (internal) (push) Successful in 37s
Mindforge Web Build and Deploy (internal) / Build Mindforge Web Image (push) Successful in 5m19s
Mindforge Web Build and Deploy (internal) / Deploy Mindforge Web (internal) (push) Successful in 11s

This commit is contained in:
2026-06-01 19:08:48 -03:00
parent b80d28f671
commit f03bcc40e3
14 changed files with 1138 additions and 14 deletions

View File

@@ -5,9 +5,10 @@ import { Sidebar } from './components/Sidebar';
import { VerificadorComponent } from './components/VerificadorComponent';
import { FlashcardComponent } from './components/FlashcardComponent';
import { FlashcardReviewComponent } from './components/FlashcardReviewComponent';
import { SpacedReviewComponent } from './components/SpacedReviewComponent';
export function App() {
const [activeModule, setActiveModule] = useState<'home' | 'verificador' | 'flashcards' | 'revisao-flashcards'>('home');
const [activeModule, setActiveModule] = useState<'home' | 'verificador' | 'flashcards' | 'revisao-flashcards' | 'revisao-espacada'>('home');
return (
<>
@@ -31,6 +32,9 @@ export function App() {
<div style={{ display: activeModule === 'revisao-flashcards' ? 'block' : 'none', height: '100%', width: '100%' }}>
<FlashcardReviewComponent />
</div>
<div style={{ display: activeModule === 'revisao-espacada' ? 'block' : 'none', height: '100%', width: '100%' }}>
<SpacedReviewComponent />
</div>
</main>
</div>
</>

View File

@@ -24,6 +24,17 @@ function difficultyLabel(difficulty: string) {
return difficulty === 'Hard' ? 'Dificil' : 'Facil';
}
function shuffleCards(cards: FlashcardCard[]) {
const shuffled = [...cards];
for (let i = shuffled.length - 1; i > 0; i--) {
const randomIndex = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[randomIndex]] = [shuffled[randomIndex], shuffled[i]];
}
return shuffled;
}
export function FlashcardReviewComponent() {
const [libraries, setLibraries] = useState<FlashcardLibrarySummary[]>([]);
const [selectedLibraryIds, setSelectedLibraryIds] = useState<number[]>([]);
@@ -33,6 +44,7 @@ export function FlashcardReviewComponent() {
const [sessionCards, setSessionCards] = useState<FlashcardCard[]>([]);
const [currentIndex, setCurrentIndex] = useState(0);
const [showAnswer, setShowAnswer] = useState(false);
const [submittingAnswer, setSubmittingAnswer] = useState(false);
useEffect(() => {
let cancelled = false;
@@ -97,7 +109,7 @@ export function FlashcardReviewComponent() {
libraryIds: selectedLibraryIds,
});
setSessionCards(response.cards);
setSessionCards(shuffleCards(response.cards));
setCurrentIndex(0);
setShowAnswer(false);
} catch (err: any) {
@@ -111,6 +123,7 @@ export function FlashcardReviewComponent() {
setSessionCards([]);
setCurrentIndex(0);
setShowAnswer(false);
setSubmittingAnswer(false);
};
const goToPrevious = () => {
@@ -121,12 +134,32 @@ export function FlashcardReviewComponent() {
setShowAnswer(false);
};
const goToNext = () => {
if (currentIndex >= sessionCards.length - 1) {
const registerReviewAnswer = async (correct: boolean) => {
if (!currentCard) {
return;
}
setCurrentIndex(currentIndex + 1);
setShowAnswer(false);
setSubmittingAnswer(true);
setError(null);
try {
await MindforgeApiService.recordFlashcardReviewAnswer({
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 (
@@ -206,7 +239,7 @@ export function FlashcardReviewComponent() {
</article>
<div className="review-session-actions">
<Button variant="secondary" onClick={goToPrevious} disabled={currentIndex === 0}>
<Button variant="secondary" onClick={goToPrevious} disabled={currentIndex === 0 || submittingAnswer}>
Anterior
</Button>
@@ -217,9 +250,14 @@ export function FlashcardReviewComponent() {
)}
{showAnswer && (
<Button variant="primary" onClick={goToNext} disabled={currentIndex >= sessionCards.length - 1}>
Proximo
</Button>
<>
<Button variant="primary" onClick={() => registerReviewAnswer(true)} disabled={submittingAnswer}>
Acertei
</Button>
<Button variant="secondary" onClick={() => registerReviewAnswer(false)} disabled={submittingAnswer}>
Errei
</Button>
</>
)}
<Button variant="secondary" onClick={endSession}>

View File

@@ -2,8 +2,8 @@ import { Button } from './Button';
import './Sidebar.css';
interface SidebarProps {
onModuleChange: (module: 'home' | 'verificador' | 'flashcards' | 'revisao-flashcards') => void;
activeModule: 'home' | 'verificador' | 'flashcards' | 'revisao-flashcards';
onModuleChange: (module: 'home' | 'verificador' | 'flashcards' | 'revisao-flashcards' | 'revisao-espacada') => void;
activeModule: 'home' | 'verificador' | 'flashcards' | 'revisao-flashcards' | 'revisao-espacada';
}
export function Sidebar({ onModuleChange, activeModule }: SidebarProps) {
@@ -34,6 +34,13 @@ export function Sidebar({ onModuleChange, activeModule }: SidebarProps) {
>
Revisao Flashcards
</Button>
<Button
variant={activeModule === 'revisao-espacada' ? 'primary' : 'secondary'}
onClick={() => onModuleChange('revisao-espacada')}
className="sidebar-btn"
>
Revisao Espacada
</Button>
</div>
</aside>
);

View File

@@ -0,0 +1,223 @@
.spaced-review-container {
width: 100%;
max-width: 980px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 1.2rem;
animation: slideUp 0.45s ease-out;
}
.spaced-review-title {
font-size: 2.35rem;
}
.spaced-review-error {
color: #ff9c96;
text-align: left;
background: rgba(255, 69, 58, 0.12);
border: 1px solid rgba(255, 69, 58, 0.4);
border-radius: 10px;
padding: 0.8rem 1rem;
}
.spaced-review-panel,
.spaced-review-session-panel {
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.14);
border-radius: 12px;
padding: 1.25rem;
text-align: left;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
box-shadow: 0 14px 36px rgba(0, 0, 0, 0.18);
}
.spaced-review-state {
color: rgba(255, 255, 255, 0.72);
font-size: 0.95rem;
}
.spaced-review-filters {
display: flex;
flex-wrap: wrap;
gap: 0.7rem;
margin-bottom: 1rem;
}
.spaced-review-filter {
display: flex;
align-items: center;
gap: 0.4rem;
border: 1px solid rgba(255, 255, 255, 0.14);
border-radius: 999px;
padding: 0.35rem 0.7rem;
background: rgba(255, 255, 255, 0.05);
font-size: 0.88rem;
}
.spaced-review-filter input[type="checkbox"],
.spaced-review-subsubject-item input[type="checkbox"] {
width: 15px;
height: 15px;
accent-color: var(--color-accent);
}
.spaced-review-subjects {
display: grid;
gap: 1rem;
}
.spaced-review-subject {
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 10px;
background: rgba(0, 0, 0, 0.18);
padding: 0.9rem;
}
.spaced-review-subject-header h3 {
margin: 0;
font-size: 1.03rem;
}
.spaced-review-subject-header p {
margin: 0.35rem 0 0;
font-size: 0.9rem;
color: rgba(255, 255, 255, 0.9);
}
.spaced-review-subject-header small {
display: block;
margin-top: 0.35rem;
color: rgba(255, 255, 255, 0.67);
font-size: 0.79rem;
}
.spaced-review-subsubject-list {
display: grid;
gap: 0.6rem;
margin-top: 0.85rem;
}
.spaced-review-subsubject-item {
display: flex;
align-items: flex-start;
gap: 0.7rem;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 10px;
padding: 0.6rem 0.7rem;
background: rgba(255, 255, 255, 0.03);
cursor: pointer;
}
.spaced-review-subsubject-texts {
display: flex;
flex-direction: column;
gap: 0.15rem;
}
.spaced-review-subsubject-texts strong {
font-size: 0.93rem;
}
.spaced-review-subsubject-texts span {
font-size: 0.82rem;
color: rgba(255, 255, 255, 0.78);
}
.spaced-review-subsubject-texts small {
font-size: 0.78rem;
color: rgba(255, 255, 255, 0.6);
}
.spaced-review-footer {
margin-top: 1rem;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
}
.spaced-review-footer p {
font-size: 0.93rem;
color: rgba(255, 255, 255, 0.78);
}
.spaced-review-progress {
display: flex;
align-items: center;
gap: 0.9rem;
margin-bottom: 0.8rem;
}
.spaced-review-progress span {
font-size: 0.9rem;
color: rgba(255, 255, 255, 0.8);
min-width: 60px;
}
.spaced-review-progress-bar {
width: 100%;
height: 8px;
border-radius: 999px;
background: rgba(0, 0, 0, 0.25);
overflow: hidden;
}
.spaced-review-progress-fill {
height: 100%;
background: linear-gradient(90deg, rgba(var(--color-accent-rgb), 0.8), rgba(var(--color-accent-rgb), 1));
border-radius: 999px;
transition: width 0.25s ease;
}
.spaced-review-card {
border: 1px solid rgba(255, 255, 255, 0.16);
border-radius: 10px;
padding: 1rem;
background: rgba(0, 0, 0, 0.2);
min-height: 260px;
}
.spaced-review-card header {
margin-bottom: 0.8rem;
}
.spaced-review-card small {
color: rgba(255, 255, 255, 0.62);
}
.spaced-review-card h3 {
margin: 0 0 0.4rem;
font-size: 0.98rem;
color: rgba(255, 255, 255, 0.9);
}
.spaced-review-card p {
margin: 0 0 1rem;
font-size: 1rem;
line-height: 1.55;
}
.spaced-review-card-meta {
display: flex;
gap: 1rem;
flex-wrap: wrap;
color: rgba(255, 255, 255, 0.75);
font-size: 0.84rem;
}
.spaced-review-session-actions {
margin-top: 1rem;
display: flex;
gap: 0.65rem;
flex-wrap: wrap;
}
@media (max-width: 760px) {
.spaced-review-session-actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}

View File

@@ -0,0 +1,422 @@
import { useEffect, useMemo, useState } from 'preact/hooks';
import {
MindforgeApiService,
type FlashcardCard,
type FlashcardRagCard,
type FlashcardRagDashboardResponse,
type FlashcardRagStatus,
} from '../services/MindforgeApiService';
import { Button } from './Button';
import './SpacedReviewComponent.css';
interface RagStatusOption {
status: FlashcardRagStatus;
label: string;
}
const STATUS_OPTIONS: RagStatusOption[] = [
{ status: 'Red', label: 'Vermelho' },
{ status: 'Amber', label: 'Amarelo' },
{ status: 'Green', label: 'Verde' },
{ status: 'Grey', label: 'Cinza' },
];
const STATUS_PRIORITY: Record<FlashcardRagStatus, number> = {
Red: 0,
Amber: 1,
Green: 2,
Grey: 3,
};
function buildGroupKey(subject: string, subSubject: string) {
return `${subject}::${subSubject}`;
}
function shuffleCards(cards: FlashcardCard[]) {
const shuffled = [...cards];
for (let i = shuffled.length - 1; i > 0; i--) {
const randomIndex = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[randomIndex]] = [shuffled[randomIndex], shuffled[i]];
}
return shuffled;
}
function formatPercentage(value: number) {
return `${value.toFixed(2).replace('.', ',')}%`;
}
function summaryText(activeCount: number, greenPercentage: number, attentionPercentage: number) {
if (activeCount === 0) {
return 'Sem revisoes avaliaveis';
}
return `Verde ${formatPercentage(greenPercentage)} | Atencao ${formatPercentage(attentionPercentage)}`;
}
function formatPerformance(rate: number) {
return `${Math.round(rate * 100)}%`;
}
function orderCardsForSession(cards: FlashcardCard[], ragByCardId: Map<number, FlashcardRagCard>) {
const buckets: FlashcardCard[][] = [[], [], [], []];
cards.forEach((card) => {
const ragCard = ragByCardId.get(card.id);
const status = ragCard?.ragStatus || 'Grey';
buckets[STATUS_PRIORITY[status]].push(card);
});
return buckets.flatMap((bucket) => shuffleCards(bucket));
}
export function SpacedReviewComponent() {
const [dashboard, setDashboard] = useState<FlashcardRagDashboardResponse | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [selectedStatuses, setSelectedStatuses] = useState<FlashcardRagStatus[]>(['Red', 'Amber']);
const [selectedGroupKeys, setSelectedGroupKeys] = useState<string[]>([]);
const [startingSession, setStartingSession] = useState(false);
const [sessionCards, setSessionCards] = useState<FlashcardCard[]>([]);
const [sessionSourceCards, setSessionSourceCards] = useState<FlashcardRagCard[]>([]);
const [currentIndex, setCurrentIndex] = useState(0);
const [showAnswer, setShowAnswer] = useState(false);
const [submittingAnswer, setSubmittingAnswer] = useState(false);
const loadDashboard = async (preserveSelection: boolean) => {
setLoading(true);
setError(null);
try {
const response = await MindforgeApiService.getFlashcardRagStatus();
setDashboard(response);
const allGroupKeys = response.subjects.flatMap((subjectGroup) =>
subjectGroup.subSubjects.map((subSubjectGroup) =>
buildGroupKey(subjectGroup.subject, subSubjectGroup.subSubject)),
);
setSelectedGroupKeys((current) => {
if (!preserveSelection || current.length === 0) {
return allGroupKeys;
}
const available = new Set(allGroupKeys);
const kept = current.filter((key) => available.has(key));
return kept.length > 0 ? kept : allGroupKeys;
});
} catch (err: any) {
setError(err?.message || 'Falha ao carregar status de revisao espacada.');
} finally {
setLoading(false);
}
};
useEffect(() => {
let cancelled = false;
async function runLoad() {
if (cancelled) {
return;
}
await loadDashboard(false);
}
runLoad();
return () => {
cancelled = true;
};
}, []);
const ragCards = useMemo(() => {
if (!dashboard) {
return [];
}
return dashboard.subjects.flatMap((subjectGroup) =>
subjectGroup.subSubjects.flatMap((subSubjectGroup) => subSubjectGroup.cards));
}, [dashboard]);
const selectedRagCards = useMemo(() => {
const selectedGroups = new Set(selectedGroupKeys);
const selectedStatusSet = new Set(selectedStatuses);
return ragCards.filter((card) =>
selectedGroups.has(buildGroupKey(card.subject, card.subSubject))
&& selectedStatusSet.has(card.ragStatus));
}, [ragCards, selectedGroupKeys, selectedStatuses]);
const sessionSourceById = useMemo(() => {
return new Map(sessionSourceCards.map((card) => [card.cardId, card]));
}, [sessionSourceCards]);
const currentCard = sessionCards[currentIndex];
const currentCardMetadata = currentCard ? sessionSourceById.get(currentCard.id) : undefined;
const progressPercent = sessionCards.length > 0
? ((currentIndex + 1) / sessionCards.length) * 100
: 0;
const toggleStatus = (status: FlashcardRagStatus) => {
if (selectedStatuses.includes(status)) {
setSelectedStatuses(selectedStatuses.filter((value) => value !== status));
return;
}
setSelectedStatuses([...selectedStatuses, status]);
};
const toggleGroup = (groupKey: string) => {
if (selectedGroupKeys.includes(groupKey)) {
setSelectedGroupKeys(selectedGroupKeys.filter((key) => key !== groupKey));
return;
}
setSelectedGroupKeys([...selectedGroupKeys, groupKey]);
};
const startSession = async () => {
if (selectedStatuses.length === 0) {
setError('Selecione ao menos um status para iniciar a revisao.');
return;
}
if (selectedGroupKeys.length === 0) {
setError('Selecione ao menos uma submateria para iniciar a revisao.');
return;
}
if (selectedRagCards.length === 0) {
setError('Nenhum card encontrado com os filtros selecionados.');
return;
}
setStartingSession(true);
setError(null);
try {
const ragByCardId = new Map(selectedRagCards.map((card) => [card.cardId, card]));
const libraryIds = Array.from(new Set(selectedRagCards.map((card) => card.libraryId)));
const response = await MindforgeApiService.createFlashcardReviewSession({ libraryIds });
const selectedCardIds = new Set(selectedRagCards.map((card) => card.cardId));
const filteredCards = response.cards.filter((card) => selectedCardIds.has(card.id));
const orderedCards = orderCardsForSession(filteredCards, ragByCardId);
if (orderedCards.length === 0) {
setError('Os filtros selecionados nao retornaram cards para revisar.');
return;
}
setSessionSourceCards(selectedRagCards);
setSessionCards(orderedCards);
setCurrentIndex(0);
setShowAnswer(false);
} catch (err: any) {
setError(err?.message || 'Falha ao iniciar revisao espacada.');
} finally {
setStartingSession(false);
}
};
const endSession = () => {
setSessionCards([]);
setSessionSourceCards([]);
setCurrentIndex(0);
setShowAnswer(false);
setSubmittingAnswer(false);
void loadDashboard(true);
};
const goToPrevious = () => {
if (currentIndex === 0) {
return;
}
setCurrentIndex(currentIndex - 1);
setShowAnswer(false);
};
const registerAnswer = async (correct: boolean) => {
if (!currentCard) {
return;
}
setSubmittingAnswer(true);
setError(null);
try {
await MindforgeApiService.recordFlashcardReviewAnswer({
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 (
<div className="spaced-review-container">
<h2 className="title spaced-review-title">Revisao espacada</h2>
<p className="subtitle">Acompanhe o status RAG dos cards por materia e submateria.</p>
{error && <div className="spaced-review-error">{error}</div>}
{sessionCards.length === 0 && (
<div className="spaced-review-panel">
{loading && <p className="spaced-review-state">Carregando painel de revisao...</p>}
{!loading && (!dashboard || dashboard.subjects.length === 0) && (
<p className="spaced-review-state">Nenhum flashcard encontrado para revisar.</p>
)}
{!loading && dashboard && dashboard.subjects.length > 0 && (
<>
<div className="spaced-review-filters">
{STATUS_OPTIONS.map((option) => (
<label key={option.status} className="spaced-review-filter">
<input
type="checkbox"
checked={selectedStatuses.includes(option.status)}
onChange={() => toggleStatus(option.status)}
/>
<span>{option.label}</span>
</label>
))}
</div>
<div className="spaced-review-subjects">
{dashboard.subjects.map((subjectGroup) => (
<section key={subjectGroup.subject} className="spaced-review-subject">
<header className="spaced-review-subject-header">
<h3>{subjectGroup.subject}</h3>
<p>{summaryText(
subjectGroup.summary.activeCount,
subjectGroup.summary.greenPercentage,
subjectGroup.summary.attentionPercentage,
)}</p>
<small>
Verde: {subjectGroup.summary.greenCount} | Amarelo: {subjectGroup.summary.amberCount}
{' '}
| Vermelho: {subjectGroup.summary.redCount} | Cinza: {subjectGroup.summary.greyCount}
</small>
</header>
<div className="spaced-review-subsubject-list">
{subjectGroup.subSubjects.map((subSubjectGroup) => {
const groupKey = buildGroupKey(subjectGroup.subject, subSubjectGroup.subSubject);
const checked = selectedGroupKeys.includes(groupKey);
return (
<label key={groupKey} className="spaced-review-subsubject-item">
<input
type="checkbox"
checked={checked}
onChange={() => toggleGroup(groupKey)}
/>
<div className="spaced-review-subsubject-texts">
<strong>{subSubjectGroup.subSubject}</strong>
<span>{summaryText(
subSubjectGroup.summary.activeCount,
subSubjectGroup.summary.greenPercentage,
subSubjectGroup.summary.attentionPercentage,
)}</span>
<small>
Cards: {subSubjectGroup.cards.length} | Cinza: {subSubjectGroup.summary.greyCount}
</small>
</div>
</label>
);
})}
</div>
</section>
))}
</div>
</>
)}
<div className="spaced-review-footer">
<p>
Cards selecionados: <strong>{selectedRagCards.length}</strong>
</p>
<Button
variant="primary"
disabled={startingSession || selectedRagCards.length === 0}
onClick={startSession}
>
{startingSession ? 'Iniciando...' : 'Iniciar Revisao Espacada'}
</Button>
</div>
</div>
)}
{sessionCards.length > 0 && currentCard && (
<div className="spaced-review-session-panel">
<div className="spaced-review-progress">
<span>{currentIndex + 1} / {sessionCards.length}</span>
<div className="spaced-review-progress-bar">
<div className="spaced-review-progress-fill" style={{ width: `${progressPercent}%` }} />
</div>
</div>
<article className="spaced-review-card">
<header>
<small>
{currentCardMetadata?.fileName || 'Arquivo'} - {currentCardMetadata?.subject || 'Geral'} - {currentCardMetadata?.subSubject || 'Geral'}
</small>
</header>
<h3>Frente</h3>
<p>{currentCard.front}</p>
{showAnswer && (
<>
<h3>Verso</h3>
<p>{currentCard.back}</p>
<div className="spaced-review-card-meta">
<span>Desempenho: {currentCardMetadata ? formatPerformance(currentCardMetadata.performanceRate) : '-'}</span>
<span>Status: {currentCardMetadata?.ragStatus || 'Grey'}</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>
);
}

View File

@@ -40,7 +40,10 @@ export interface FlashcardCard {
front: string;
back: string;
position: number;
correctCount: number;
incorrectCount: number;
createdAt: string;
lastReviewedAt?: string | null;
}
export interface FlashcardLibrarySummary {
@@ -70,6 +73,56 @@ export interface FlashcardReviewSessionResponse {
cards: FlashcardCard[];
}
export interface FlashcardReviewAnswerRequest {
cardId: number;
correct: boolean;
}
export type FlashcardRagStatus = 'Grey' | 'Red' | 'Amber' | 'Green';
export interface FlashcardRagCard {
cardId: number;
libraryId: number;
fileName: string;
subject: string;
subSubject: string;
front: string;
back: string;
correctCount: number;
incorrectCount: number;
totalAnswers: number;
performanceRate: number;
lastReviewedAt?: string | null;
ragStatus: FlashcardRagStatus;
}
export interface FlashcardRagSummary {
greenCount: number;
amberCount: number;
redCount: number;
greyCount: number;
activeCount: number;
greenPercentage: number;
attentionPercentage: number;
}
export interface FlashcardRagSubSubjectGroup {
subSubject: string;
summary: FlashcardRagSummary;
cards: FlashcardRagCard[];
}
export interface FlashcardRagSubjectGroup {
subject: string;
summary: FlashcardRagSummary;
subSubjects: FlashcardRagSubSubjectGroup[];
}
export interface FlashcardRagDashboardResponse {
generatedAt: string;
subjects: FlashcardRagSubjectGroup[];
}
async function throwIfNotOk(response: Response, fallback: string) {
if (response.ok) {
return;
@@ -140,6 +193,24 @@ export const MindforgeApiService = {
return response.json();
},
async recordFlashcardReviewAnswer(data: FlashcardReviewAnswerRequest): Promise<void> {
const response = await fetch(`${BASE_URL}/api/v1/flashcard/review-answer`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
await throwIfNotOk(response, `Erro ao registrar resposta da revisao: ${response.statusText}`);
},
async getFlashcardRagStatus(): Promise<FlashcardRagDashboardResponse> {
const response = await fetch(`${BASE_URL}/api/v1/flashcard/rag-status`);
await throwIfNotOk(response, `Erro ao buscar status RAG de revisao: ${response.statusText}`);
return response.json();
},
async getRepositoryInfo(): Promise<RepositoryInfo> {
const response = await fetch(`${BASE_URL}/api/v1/repository/info`);
await throwIfNotOk(response, `Erro ao buscar info do repositorio: ${response.statusText}`);