Spaces:
Running
Running
| 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> | |
| ); | |
| } | |