/**
* 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, ' ');
}