5 SDD batches archived: - Batch 1: UI Polish (10 features, 14 tasks) - Batch 2: Study System (8 features, 23 tasks) - Batch 3: Infrastructure (5 features, 22 tasks) - Batch 4: AI Advanced (5 features, 30 tasks) — RAG with @xenova/transformers - Batch 5: Core Features (5 features, 19 tasks) 37 bugs fixed from comprehensive code review (11 CRITICAL, 12 HIGH, 14 MEDIUM/LOW): - SSE streaming now works (event.token check) - API keys no longer exposed via GET /api/models - FTS5 injection sanitized - DB backup/restore with admin auth - Buddy mode wired (buddy_meta column) - Exam auto-submit stale closure fixed - CSS variables aligned with design tokens - Progress data corruption fixed - WebSocket protocol auto-detection - Tests infrastructure completed (vitest + node:test)
134 lines
3.5 KiB
JavaScript
134 lines
3.5 KiB
JavaScript
import { useState, useCallback, useEffect, useRef } from 'react';
|
|
import { getProgress, updateProgress, resetProgressTopic } from '../lib/api';
|
|
|
|
function calcPercentage(row) {
|
|
if (!row || row.exercises_done === 0) return 0;
|
|
return Math.round((row.exercises_correct / row.exercises_done) * 100);
|
|
}
|
|
|
|
export default function useProgress() {
|
|
const [progress, setProgress] = useState([]);
|
|
const [error, setError] = useState(null);
|
|
const wsRef = useRef(null);
|
|
const reconnectDelayRef = useRef(1000);
|
|
const reconnectTimerRef = useRef(null);
|
|
|
|
const refresh = useCallback(async () => {
|
|
try {
|
|
const rows = await getProgress();
|
|
setProgress(
|
|
rows.map((r) => ({
|
|
...r,
|
|
percentage: calcPercentage(r),
|
|
}))
|
|
);
|
|
setError(null);
|
|
} catch (err) {
|
|
console.error('[useProgress] refresh error:', err.message);
|
|
setError(err.message);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
refresh();
|
|
|
|
const connect = () => {
|
|
const wsUrl = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws`;
|
|
const ws = new WebSocket(wsUrl);
|
|
wsRef.current = ws;
|
|
|
|
ws.onopen = () => {
|
|
reconnectDelayRef.current = 1000;
|
|
};
|
|
|
|
ws.onmessage = (event) => {
|
|
try {
|
|
const msg = JSON.parse(event.data);
|
|
if (msg.type === 'progress_update' && msg.data) {
|
|
setProgress((prev) => {
|
|
const row = msg.data;
|
|
const enriched = { ...row, percentage: calcPercentage(row) };
|
|
const idx = prev.findIndex((p) => p.topic === row.topic);
|
|
if (idx >= 0) {
|
|
const next = [...prev];
|
|
next[idx] = enriched;
|
|
return next;
|
|
}
|
|
return [...prev, enriched];
|
|
});
|
|
}
|
|
} catch (e) {
|
|
// ignore malformed messages
|
|
}
|
|
};
|
|
|
|
ws.onclose = () => {
|
|
wsRef.current = null;
|
|
const delay = Math.min(reconnectDelayRef.current, 30000);
|
|
reconnectDelayRef.current *= 2;
|
|
reconnectTimerRef.current = setTimeout(connect, delay);
|
|
};
|
|
|
|
ws.onerror = () => {
|
|
ws.close();
|
|
};
|
|
};
|
|
|
|
connect();
|
|
|
|
return () => {
|
|
if (wsRef.current) {
|
|
wsRef.current.onclose = null;
|
|
wsRef.current.close();
|
|
wsRef.current = null;
|
|
}
|
|
if (reconnectTimerRef.current) {
|
|
clearTimeout(reconnectTimerRef.current);
|
|
reconnectTimerRef.current = null;
|
|
}
|
|
};
|
|
}, [refresh]);
|
|
|
|
const updateExercise = useCallback(
|
|
async (topic, correct) => {
|
|
try {
|
|
const row = await updateProgress(topic, correct);
|
|
setProgress((prev) => {
|
|
const idx = prev.findIndex((p) => p.topic === topic);
|
|
const enriched = { ...row, percentage: calcPercentage(row) };
|
|
if (idx >= 0) {
|
|
const next = [...prev];
|
|
next[idx] = enriched;
|
|
return next;
|
|
}
|
|
return [...prev, enriched];
|
|
});
|
|
setError(null);
|
|
} catch (err) {
|
|
console.error('[useProgress] update error:', err.message);
|
|
setError(err.message);
|
|
}
|
|
},
|
|
[]
|
|
);
|
|
|
|
const resetTopic = useCallback(async (topic) => {
|
|
try {
|
|
await resetProgressTopic(topic);
|
|
setProgress((prev) => prev.filter((p) => p.topic !== topic));
|
|
setError(null);
|
|
} catch (err) {
|
|
console.error('[useProgress] reset error:', err.message);
|
|
setError(err.message);
|
|
}
|
|
}, []);
|
|
|
|
return {
|
|
progress,
|
|
error,
|
|
refresh,
|
|
updateExercise,
|
|
resetTopic,
|
|
};
|
|
}
|