"use client"; import { useAudioState } from "@/lib/audio-state-context"; import { cn } from "@/utils/cn"; import { useMemo } from "react"; interface AudioFeatures { bpm?: number | null; energy?: number | null; valence?: number | null; danceability?: number | null; keyScale?: string | null; } interface VibeGraphProps { className?: string; currentTrackFeatures?: AudioFeatures | null; } // Feature labels and their normalization ranges const FEATURES = [ { key: "energy", label: "Energy", min: 0, max: 1 }, { key: "valence", label: "Mood", min: 0, max: 1 }, { key: "danceability", label: "Dance", min: 0, max: 1 }, { key: "bpm", label: "BPM", min: 60, max: 200 }, ] as const; function normalizeValue(value: number | null | undefined, min: number, max: number): number { if (value === null || value === undefined) return 0; return Math.max(0, Math.min(1, (value - min) / (max - min))); } export function VibeGraph({ className, currentTrackFeatures }: VibeGraphProps) { const { vibeMode, vibeSourceFeatures } = useAudioState(); // Calculate normalized values for both source and current track const { sourceValues, currentValues } = useMemo(() => { const source: number[] = []; const current: number[] = []; FEATURES.forEach((feature) => { const sourceVal = vibeSourceFeatures?.[feature.key as keyof AudioFeatures]; const currentVal = currentTrackFeatures?.[feature.key as keyof AudioFeatures]; source.push(normalizeValue(sourceVal as number, feature.min, feature.max)); current.push(normalizeValue(currentVal as number, feature.min, feature.max)); }); return { sourceValues: source, currentValues: current }; }, [vibeSourceFeatures, currentTrackFeatures]); // Don't render if not in vibe mode if (!vibeMode) return null; // SVG dimensions const size = 80; const center = size / 2; const maxRadius = 32; // Calculate polygon points for radar chart const getPolygonPoints = (values: number[]) => { const angleStep = (2 * Math.PI) / values.length; return values .map((value, i) => { const angle = angleStep * i - Math.PI / 2; // Start from top const radius = value * maxRadius; const x = center + radius * Math.cos(angle); const y = center + radius * Math.sin(angle); return `${x},${y}`; }) .join(" "); }; // Calculate label positions const getLabelPosition = (index: number) => { const angleStep = (2 * Math.PI) / FEATURES.length; const angle = angleStep * index - Math.PI / 2; const radius = maxRadius + 8; return { x: center + radius * Math.cos(angle), y: center + radius * Math.sin(angle), }; }; // Calculate match percentage const matchScore = useMemo(() => { if (!vibeSourceFeatures || !currentTrackFeatures) return null; let totalDiff = 0; let count = 0; FEATURES.forEach((feature, i) => { if (sourceValues[i] > 0 || currentValues[i] > 0) { totalDiff += Math.abs(sourceValues[i] - currentValues[i]); count++; } }); if (count === 0) return null; return Math.round((1 - totalDiff / count) * 100); }, [sourceValues, currentValues, vibeSourceFeatures, currentTrackFeatures]); return (