/** * ConfidenceGraph Component * * Displays interview confidence over time as a line chart. * Data is from computer vision analysis - NOT LLM sentiment. * * Features: * - Smooth line chart with gradient fill * - Tooltips with timestamp and score * - Confidence level indicator * - Works in light/dark mode */ import React, { useMemo } from 'react'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Area, ReferenceLine, } from 'recharts'; import './ConfidenceGraph.css'; // Confidence level thresholds const CONFIDENCE_LEVELS = { very_low: { min: 0, max: 20, color: '#EF4444', label: 'Very Low' }, low: { min: 21, max: 40, color: '#F97316', label: 'Low' }, moderate: { min: 41, max: 60, color: '#EAB308', label: 'Moderate' }, high: { min: 61, max: 80, color: '#22C55E', label: 'High' }, very_high: { min: 81, max: 100, color: '#10B981', label: 'Very High' }, }; /** * Custom tooltip for the graph */ const CustomTooltip = ({ active, payload, label }) => { if (!active || !payload || !payload.length) return null; const value = payload[0].value; const level = getConfidenceLevel(value); return (
{formatTime(label)}
{value.toFixed(1)}%
{level.label}
); }; /** * Get confidence level from score */ function getConfidenceLevel(score) { if (score <= 20) return CONFIDENCE_LEVELS.very_low; if (score <= 40) return CONFIDENCE_LEVELS.low; if (score <= 60) return CONFIDENCE_LEVELS.moderate; if (score <= 80) return CONFIDENCE_LEVELS.high; return CONFIDENCE_LEVELS.very_high; } /** * Format seconds to mm:ss */ function formatTime(seconds) { const mins = Math.floor(seconds / 60); const secs = seconds % 60; return `${mins}:${secs.toString().padStart(2, '0')}`; } /** * Generate gradient ID based on data */ function getGradientId(avgConfidence) { const level = getConfidenceLevel(avgConfidence); return `confidenceGradient-${level.label.toLowerCase().replace(' ', '')}`; } /** * ConfidenceGraph Component */ export default function ConfidenceGraph({ graphData = [], summary = {}, showGrid = true, showAverage = true, height = 300, className = '', }) { // Memoize processed data const processedData = useMemo(() => { if (!graphData || graphData.length === 0) return []; return graphData.map((point) => ({ time: point.time, confidence: point.confidence, formattedTime: formatTime(point.time), })); }, [graphData]); // Calculate derived values const avgConfidence = summary?.average_confidence || (processedData.length > 0 ? processedData.reduce((sum, p) => sum + p.confidence, 0) / processedData.length : 0); const confidenceLevel = getConfidenceLevel(avgConfidence); const gradientId = getGradientId(avgConfidence); // Empty state if (processedData.length === 0) { return (
📊

No confidence data available yet

Video analysis will generate confidence metrics
); } return (
{/* Header with summary */}

Confidence Timeline

{processedData.length} seconds analyzed
{avgConfidence.toFixed(1)}% {confidenceLevel.label}
{/* Chart */}
{/* Gradient definition */} {/* Grid */} {showGrid && ( )} {/* Axes */} `${v}%`} /> {/* Average line */} {showAverage && ( )} {/* Tooltip */} } /> {/* Area fill */} {/* Main line */}
{/* Summary stats */} {summary && Object.keys(summary).length > 0 && (
Eye Contact {(summary.average_eye_contact || 0).toFixed(0)}%
High Conf. Time {(summary.high_confidence_percentage || 0).toFixed(0)}%
Stability {((summary.stability || 0) * 100).toFixed(0)}%
Drops {summary.drops || 0}
)} {/* Primary issue indicator */} {summary?.primary_issue && (
⚠️ Primary area for improvement: {formatIssue(summary.primary_issue)}
)}
); } /** * Format issue ID to readable text */ function formatIssue(issue) { const issueMap = { low_eye_contact: 'Eye contact', excessive_head_movement: 'Head movement', poor_face_visibility: 'Face visibility', looking_away: 'Looking away from camera', }; return issueMap[issue] || issue.replace(/_/g, ' '); }