chowdary1's picture
V2 Full Sync: 4-mode architecture, compiler, RAG, neon theme, round detection fix, camera toggle
7fff83a
Raw History Blame Contribute Delete
8.87 kB
/**
* 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 (
<div className="confidence-tooltip">
<div className="tooltip-time">
{formatTime(label)}
</div>
<div className="tooltip-value" style={{ color: level.color }}>
{value.toFixed(1)}%
</div>
<div className="tooltip-level" style={{ color: level.color }}>
{level.label}
</div>
</div>
);
};
/**
* 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 (
<div className={`confidence-graph-empty ${className}`}>
<div className="empty-icon">📊</div>
<p>No confidence data available yet</p>
<span className="empty-hint">
Video analysis will generate confidence metrics
</span>
</div>
);
}
return (
<div className={`confidence-graph ${className}`}>
{/* Header with summary */}
<div className="confidence-graph-header">
<div className="header-left">
<h3>Confidence Timeline</h3>
<span className="header-subtitle">
{processedData.length} seconds analyzed
</span>
</div>
<div className="header-right">
<div
className="average-badge"
style={{
backgroundColor: `${confidenceLevel.color}20`,
borderColor: confidenceLevel.color,
}}
>
<span className="average-value" style={{ color: confidenceLevel.color }}>
{avgConfidence.toFixed(1)}%
</span>
<span className="average-label">{confidenceLevel.label}</span>
</div>
</div>
</div>
{/* Chart */}
<div className="confidence-graph-chart" style={{ height }}>
<ResponsiveContainer width="100%" height="100%">
<LineChart
data={processedData}
margin={{ top: 20, right: 30, left: 10, bottom: 10 }}
>
{/* Gradient definition */}
<defs>
<linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={confidenceLevel.color} stopOpacity={0.4} />
<stop offset="100%" stopColor={confidenceLevel.color} stopOpacity={0.05} />
</linearGradient>
</defs>
{/* Grid */}
{showGrid && (
<CartesianGrid
strokeDasharray="3 3"
stroke="var(--grid-color, rgba(128,128,128,0.2))"
vertical={false}
/>
)}
{/* Axes */}
<XAxis
dataKey="time"
tickFormatter={formatTime}
stroke="var(--axis-color, #888)"
tick={{ fill: 'var(--text-secondary, #888)', fontSize: 12 }}
axisLine={{ stroke: 'var(--axis-color, rgba(128,128,128,0.3))' }}
interval="preserveStartEnd"
/>
<YAxis
domain={[0, 100]}
stroke="var(--axis-color, #888)"
tick={{ fill: 'var(--text-secondary, #888)', fontSize: 12 }}
axisLine={{ stroke: 'var(--axis-color, rgba(128,128,128,0.3))' }}
ticks={[0, 25, 50, 75, 100]}
tickFormatter={(v) => `${v}%`}
/>
{/* Average line */}
{showAverage && (
<ReferenceLine
y={avgConfidence}
stroke={confidenceLevel.color}
strokeDasharray="5 5"
strokeOpacity={0.7}
label={{
value: `Avg: ${avgConfidence.toFixed(0)}%`,
position: 'right',
fill: confidenceLevel.color,
fontSize: 11,
}}
/>
)}
{/* Tooltip */}
<Tooltip content={<CustomTooltip />} />
{/* Area fill */}
<Area
type="monotone"
dataKey="confidence"
stroke="transparent"
fill={`url(#${gradientId})`}
/>
{/* Main line */}
<Line
type="monotone"
dataKey="confidence"
stroke={confidenceLevel.color}
strokeWidth={2.5}
dot={false}
activeDot={{
r: 6,
fill: confidenceLevel.color,
stroke: '#fff',
strokeWidth: 2,
}}
/>
</LineChart>
</ResponsiveContainer>
</div>
{/* Summary stats */}
{summary && Object.keys(summary).length > 0 && (
<div className="confidence-graph-stats">
<div className="stat">
<span className="stat-label">Eye Contact</span>
<span className="stat-value">{(summary.average_eye_contact || 0).toFixed(0)}%</span>
</div>
<div className="stat">
<span className="stat-label">High Conf. Time</span>
<span className="stat-value">{(summary.high_confidence_percentage || 0).toFixed(0)}%</span>
</div>
<div className="stat">
<span className="stat-label">Stability</span>
<span className="stat-value">{((summary.stability || 0) * 100).toFixed(0)}%</span>
</div>
<div className="stat">
<span className="stat-label">Drops</span>
<span className="stat-value">{summary.drops || 0}</span>
</div>
</div>
)}
{/* Primary issue indicator */}
{summary?.primary_issue && (
<div className="confidence-issue">
<span className="issue-icon">⚠️</span>
<span className="issue-text">
Primary area for improvement: {formatIssue(summary.primary_issue)}
</span>
</div>
)}
</div>
);
}
/**
* 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, ' ');
}