import React, { useState, useEffect, useRef } from 'react'; function Records() { const [filter, setFilter] = useState('all'); const [sessions, setSessions] = useState([]); const [loading, setLoading] = useState(false); const [detailState, setDetailState] = useState({ open: false, loading: false, error: '', session: null }); const chartRef = useRef(null); // Format a session duration. const formatDuration = (seconds) => { const safeSeconds = Math.max(0, Number(seconds) || 0); const mins = Math.floor(safeSeconds / 60); const secs = safeSeconds % 60; return `${mins}m ${secs}s`; }; // Format a session timestamp for table display. const formatDate = (dateString) => { const date = new Date(dateString); return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); }; const formatDateTime = (dateString) => { if (!dateString) return 'Not available'; const date = new Date(dateString); return date.toLocaleString('en-US', { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit' }); }; const parseMetadata = (detectionData) => { if (!detectionData) return {}; if (typeof detectionData === 'object') return detectionData; try { return JSON.parse(detectionData); } catch (_) { return {}; } }; const averageOf = (values) => { const valid = values.filter((value) => Number.isFinite(value)); if (valid.length === 0) return null; return valid.reduce((sum, value) => sum + value, 0) / valid.length; }; const buildTimelineSegments = (events, maxSegments = 48) => { if (!events.length) return []; const segmentSize = Math.ceil(events.length / maxSegments); const segments = []; for (let i = 0; i < events.length; i += segmentSize) { const slice = events.slice(i, i + segmentSize); const focusedCount = slice.filter((event) => event.isFocused).length; const focusRatio = focusedCount / slice.length; const confidence = averageOf(slice.map((event) => event.confidence)); let tone = 'distracted'; if (focusRatio >= 0.75) tone = 'focused'; else if (focusRatio >= 0.35) tone = 'mixed'; segments.push({ tone, focusRatio, confidence, count: slice.length }); } return segments; }; const buildDetailView = (session) => { if (!session) return null; const parsedEvents = (session.events || []).map((event) => { const metadata = parseMetadata(event.detection_data); return { ...event, metadata, isFocused: Boolean(event.is_focused), confidence: Number(event.confidence) || 0 }; }); const focusRatio = session.total_frames ? session.focused_frames / session.total_frames : parsedEvents.length ? parsedEvents.filter((event) => event.isFocused).length / parsedEvents.length : 0; const modelCounts = parsedEvents.reduce((counts, event) => { const model = event.metadata?.model; if (model) counts[model] = (counts[model] || 0) + 1; return counts; }, {}); const dominantModel = Object.entries(modelCounts).sort((a, b) => b[1] - a[1])[0]?.[0] || 'Unavailable'; const avgConfidence = averageOf(parsedEvents.map((event) => event.confidence)); const avgFaceScore = averageOf(parsedEvents.map((event) => Number(event.metadata?.s_face))); const avgEyeScore = averageOf(parsedEvents.map((event) => Number(event.metadata?.s_eye))); const avgMar = averageOf(parsedEvents.map((event) => Number(event.metadata?.mar))); const startTime = session.start_time ? new Date(session.start_time) : null; const timeline = buildTimelineSegments(parsedEvents); const recentEvents = parsedEvents.slice(-10).reverse(); return { parsedEvents, focusRatio, dominantModel, avgConfidence, avgFaceScore, avgEyeScore, avgMar, timeline, recentEvents, formatOffset(timestamp) { if (!startTime || !timestamp) return '--'; const offsetSeconds = Math.max(0, Math.round((new Date(timestamp) - startTime) / 1000)); const mins = Math.floor(offsetSeconds / 60); const secs = offsetSeconds % 60; return mins > 0 ? `${mins}m ${secs}s` : `${secs}s`; } }; }; const getScoreTone = (score) => { if (score >= 0.8) return 'excellent'; if (score >= 0.6) return 'good'; if (score >= 0.4) return 'fair'; return 'low'; }; const closeDetails = () => { setDetailState({ open: false, loading: false, error: '', session: null }); }; // Load session rows for the selected filter. const loadSessions = async (filterType) => { setLoading(true); try { const response = await fetch(`/api/sessions?filter=${filterType}&limit=50`); const data = await response.json(); setSessions(data); drawChart(data); } catch (error) { console.error('Failed to load sessions:', error); } finally { setLoading(false); } }; // Draw the session score chart. const drawChart = (data) => { const canvas = chartRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); const width = canvas.width = canvas.offsetWidth; const height = canvas.height = 300; // Clear the canvas before each redraw. ctx.clearRect(0, 0, width, height); if (data.length === 0) { ctx.fillStyle = '#999'; ctx.font = '16px Nunito'; ctx.textAlign = 'center'; ctx.fillText('No data available', width / 2, height / 2); return; } // Use at most the latest 20 sessions in the chart. const displayData = data.slice(0, 20).reverse(); const padding = 50; const chartWidth = width - padding * 2; const chartHeight = height - padding * 2; const barWidth = chartWidth / displayData.length; // Use a normalized max score for chart scaling. const maxScore = 1.0; // Draw the chart axes. ctx.strokeStyle = '#E0E0E0'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(padding, padding); ctx.lineTo(padding, height - padding); ctx.lineTo(width - padding, height - padding); ctx.stroke(); // Draw Y-axis labels. ctx.fillStyle = '#666'; ctx.font = '12px Nunito'; ctx.textAlign = 'right'; for (let i = 0; i <= 4; i++) { const y = height - padding - (chartHeight * i / 4); const value = (maxScore * i / 4 * 100).toFixed(0); ctx.fillText(value + '%', padding - 10, y + 4); // Draw horizontal grid lines. ctx.strokeStyle = '#F0F0F0'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(padding, y); ctx.lineTo(width - padding, y); ctx.stroke(); } // Draw the bar chart. displayData.forEach((session, index) => { const barHeight = (session.focus_score / maxScore) * chartHeight; const x = padding + index * barWidth + barWidth * 0.1; const y = height - padding - barHeight; const barActualWidth = barWidth * 0.8; // Map each score to a blue-toned color band. const score = session.focus_score; let color; if (score >= 0.8) color = '#4A90E2'; else if (score >= 0.6) color = '#5DADE2'; else if (score >= 0.4) color = '#85C1E9'; else color = '#AED6F1'; ctx.fillStyle = color; ctx.fillRect(x, y, barActualWidth, barHeight); // Draw a matching outline around each bar. ctx.strokeStyle = color; ctx.lineWidth = 1; ctx.strokeRect(x, y, barActualWidth, barHeight); }); // Draw the chart title. ctx.textAlign = 'left'; ctx.font = 'bold 14px Nunito'; ctx.fillStyle = '#4A90E2'; ctx.fillText('Focus Score by Session', padding, 30); }; // Initial load. useEffect(() => { loadSessions(filter); }, [filter]); useEffect(() => { if (!detailState.open) return undefined; const previousOverflow = document.body.style.overflow; document.body.style.overflow = 'hidden'; const handleKeyDown = (event) => { if (event.key === 'Escape') { closeDetails(); } }; window.addEventListener('keydown', handleKeyDown); return () => { document.body.style.overflow = previousOverflow; window.removeEventListener('keydown', handleKeyDown); }; }, [detailState.open]); // Filter button handler. const handleFilterClick = (filterType) => { setFilter(filterType); }; // Open the detail modal for one session. const handleViewDetails = async (sessionId) => { setDetailState({ open: true, loading: true, error: '', session: null }); try { const response = await fetch(`/api/sessions/${sessionId}`); if (!response.ok) { throw new Error('Failed to load session details.'); } const data = await response.json(); setDetailState({ open: true, loading: false, error: '', session: data }); } catch (error) { setDetailState({ open: true, loading: false, error: error.message || 'Failed to load session details.', session: null }); } }; const detailView = buildDetailView(detailState.session); return (

My Records

Recent Sessions

{loading ? (
Loading sessions...
) : sessions.length === 0 ? (
No sessions found for this period.
) : ( {sessions.map((session, index) => ( ))}
Date Duration Focus Score Action
{formatDate(session.start_time)} {formatDuration(session.duration_seconds)} = 0.8 ? '#28a745' : session.focus_score >= 0.6 ? '#ffc107' : session.focus_score >= 0.4 ? '#fd7e14' : '#dc3545', fontWeight: '600', fontSize: '13px' }} > {(session.focus_score * 100).toFixed(1)}%
)}
{detailState.open ? (
event.stopPropagation()}>
Session Detail

{detailState.session ? formatDateTime(detailState.session.start_time) : 'Loading session'}

Review score, capture quality, and a condensed event timeline for this session.

{detailState.loading ? (
Loading session details...
) : detailState.error ? (
{detailState.error}
) : detailState.session && detailView ? ( <>
Focus Score {(detailState.session.focus_score * 100).toFixed(1)}%
Duration {formatDuration(detailState.session.duration_seconds)}
Frames Analysed {detailState.session.total_frames}
Focused Frames {(detailView.focusRatio * 100).toFixed(1)}%

Session Info

Started {formatDateTime(detailState.session.start_time)}
Ended {formatDateTime(detailState.session.end_time)}
Dominant Model {detailView.dominantModel}
Event Samples {detailView.parsedEvents.length}

Signal Quality

Avg Confidence {detailView.avgConfidence !== null ? `${(detailView.avgConfidence * 100).toFixed(1)}%` : '--'}
Avg Face Score {detailView.avgFaceScore !== null ? detailView.avgFaceScore.toFixed(3) : '--'}
Avg Eye Score {detailView.avgEyeScore !== null ? detailView.avgEyeScore.toFixed(3) : '--'}
Avg MAR {detailView.avgMar !== null ? detailView.avgMar.toFixed(3) : '--'}

Focus Timeline

{detailView.parsedEvents.length} events condensed
{detailView.timeline.length > 0 ? ( <>
{detailView.timeline.map((segment, index) => (
))}
Focused Mixed Distracted
) : (
No event timeline was recorded for this session.
)}

Recent Events

Last {detailView.recentEvents.length} samples
{detailView.recentEvents.length > 0 ? (
{detailView.recentEvents.map((event) => (
{detailView.formatOffset(event.timestamp)}
{event.isFocused ? 'Focused' : 'Distracted'}
{event.metadata?.model || 'model n/a'} ยท confidence {(event.confidence * 100).toFixed(1)}%
{event.isFocused ? 'OK' : 'Alert'}
))}
) : (
No individual event samples are available.
)}
) : null}
) : null}
); } export default Records;