FetalCLIP / frontend /src /components /SessionHistory.tsx
Numan Saeed
Add feedback system with correction logging
10ac650
import { useState, useEffect } from 'react';
import { History, Check, X, AlertTriangle, Download, ChevronDown, ChevronUp } from 'lucide-react';
import { getFeedback, exportFeedbackCSV, FeedbackEntry, FeedbackStats, getFeedbackStats } from '../lib/api';
interface SessionHistoryProps {
sessionId: string;
onSelectEntry?: (entry: FeedbackEntry) => void;
refreshTrigger?: number; // Increment to trigger refresh
}
export function SessionHistory({ sessionId, onSelectEntry, refreshTrigger }: SessionHistoryProps) {
const [entries, setEntries] = useState<FeedbackEntry[]>([]);
const [stats, setStats] = useState<FeedbackStats | null>(null);
const [isExpanded, setIsExpanded] = useState(false);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
loadData();
}, [sessionId, refreshTrigger]);
const loadData = async () => {
if (!sessionId) return;
setIsLoading(true);
try {
const [feedbackData, statsData] = await Promise.all([
getFeedback(sessionId),
getFeedbackStats(sessionId)
]);
setEntries(feedbackData);
setStats(statsData);
} catch (error) {
console.error('Failed to load session data:', error);
} finally {
setIsLoading(false);
}
};
const handleExport = async () => {
try {
await exportFeedbackCSV(sessionId);
} catch (error) {
console.error('Failed to export:', error);
}
};
if (entries.length === 0 && !isLoading) {
return null; // Don't show anything if no entries
}
return (
<div className="bg-surface-secondary rounded-lg overflow-hidden">
{/* Header - always visible */}
<button
onClick={() => setIsExpanded(!isExpanded)}
className="w-full flex items-center justify-between px-3 py-2.5 hover:bg-surface-secondary/80 transition-colors"
>
<div className="flex items-center gap-2">
<History className="w-4 h-4 text-accent-blue" />
<span className="text-sm font-medium text-text-primary">Session History</span>
{stats && (
<span className="text-xs text-text-muted">
({stats.total_feedback} reviewed)
</span>
)}
</div>
<div className="flex items-center gap-3">
{stats && stats.total_feedback > 0 && (
<div className="flex items-center gap-2 text-xs">
<span className="flex items-center gap-1 text-green-600">
<Check className="w-3 h-3" />
{stats.correct_count}
</span>
<span className="flex items-center gap-1 text-red-500">
<X className="w-3 h-3" />
{stats.incorrect_count}
</span>
<span className="text-text-muted">
({stats.accuracy}% acc)
</span>
</div>
)}
{isExpanded ? (
<ChevronUp className="w-4 h-4 text-text-muted" />
) : (
<ChevronDown className="w-4 h-4 text-text-muted" />
)}
</div>
</button>
{/* Expanded content */}
{isExpanded && (
<div className="border-t border-border">
{/* Export button */}
{entries.length > 0 && (
<div className="px-3 py-2 border-b border-border">
<button
onClick={handleExport}
className="flex items-center gap-1.5 px-2.5 py-1.5 text-xs bg-accent-blue/10 hover:bg-accent-blue/20 text-accent-blue rounded-md transition-colors"
>
<Download className="w-3.5 h-3.5" />
Export to CSV
</button>
</div>
)}
{/* Entry list */}
<div className="max-h-48 overflow-y-auto">
{isLoading ? (
<div className="px-3 py-4 text-center text-sm text-text-muted">
Loading...
</div>
) : entries.length === 0 ? (
<div className="px-3 py-4 text-center text-sm text-text-muted">
No feedback recorded yet
</div>
) : (
entries.map((entry) => (
<div
key={entry.id}
onClick={() => onSelectEntry?.(entry)}
className={`px-3 py-2 border-b border-border/50 last:border-b-0 hover:bg-surface cursor-pointer transition-colors`}
>
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-xs font-mono text-text-muted truncate max-w-[120px]">
{entry.filename}
</span>
<span className={`flex items-center gap-0.5 text-xs ${entry.is_correct ? 'text-green-600' : 'text-red-500'}`}>
{entry.is_correct ? (
<Check className="w-3 h-3" />
) : (
<X className="w-3 h-3" />
)}
</span>
</div>
<div className="text-xs text-text-primary mt-0.5">
{entry.is_correct ? (
entry.predicted_label
) : (
<span>
<span className="line-through text-text-muted">{entry.predicted_label}</span>
{' → '}
<span className="text-primary">{entry.correct_label}</span>
</span>
)}
</div>
</div>
<div className="flex flex-col items-end">
<span className={`text-xs font-medium ${
entry.predicted_confidence >= 70 ? 'text-green-600' :
entry.predicted_confidence >= 50 ? 'text-yellow-600' : 'text-red-500'
}`}>
{entry.predicted_confidence}%
</span>
{entry.predicted_confidence < 70 && (
<AlertTriangle className="w-3 h-3 text-yellow-600" />
)}
</div>
</div>
</div>
))
)}
</div>
</div>
)}
</div>
);
}