InnerVoice / frontend /components /CircularProgress.tsx
E5K7's picture
InnerVoice Production Upgrade: Multi-channel chat, premium UI components, Whisper-small accuracy upgrade, and full-screen layout polishing
b5e8a53
"use client";
import { motion } from "framer-motion";
interface CircularProgressProps {
score: number;
label: string;
size?: number;
strokeWidth?: number;
}
function getScoreColor(score: number): string {
if (score >= 70) return "#0070f3";
if (score >= 40) return "#00d4ff";
return "#666";
}
export default function CircularProgress({
score,
label,
size = 96,
strokeWidth = 8,
}: CircularProgressProps) {
const radius = (size - strokeWidth * 2) / 2;
const circumference = 2 * Math.PI * radius;
const progress = (score / 100) * circumference;
const color = getScoreColor(score);
const cx = size / 2;
const cy = size / 2;
return (
<div className="flex flex-col items-center gap-2">
<div className="relative" style={{ width: size, height: size }}>
<svg
width={size}
height={size}
className="-rotate-90"
style={{ transform: "rotate(-90deg)" }}
>
{/* Background track */}
<circle
cx={cx}
cy={cy}
r={radius}
fill="none"
stroke="rgba(255,255,255,0.05)"
strokeWidth={strokeWidth}
/>
{/* Progress arc */}
<motion.circle
cx={cx}
cy={cy}
r={radius}
fill="none"
stroke={color}
strokeWidth={strokeWidth}
strokeDasharray={`${progress} ${circumference}`}
strokeLinecap="round"
initial={{ strokeDasharray: `0 ${circumference}` }}
animate={{ strokeDasharray: `${progress} ${circumference}` }}
transition={{ duration: 1, ease: "easeOut" }}
style={{
filter: `drop-shadow(0 0 8px ${color}60)`,
}}
/>
</svg>
{/* Center score */}
<div className="absolute inset-0 flex flex-col items-center justify-center">
<motion.span
initial={{ opacity: 0, scale: 0.5 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.5, duration: 0.3 }}
className="text-xl font-bold text-white leading-none"
>
{score}
</motion.span>
</div>
</div>
<span className="text-xs font-medium text-vercel-fg/60 tracking-wide uppercase">{label}</span>
</div>
);
}