"use client"; import { useAudioState, AudioFeatures } from "@/lib/audio-state-context"; import { cn } from "@/utils/cn"; import { useMemo, useState } from "react"; import { X, AudioWaveform, Music, Zap, Heart, Footprints, Gauge, Smile, Frown, Coffee, Flame, PartyPopper, Guitar, Radio, } from "lucide-react"; interface VibeOverlayProps { className?: string; currentTrackFeatures?: AudioFeatures | null; variant?: "floating" | "inline"; onClose?: () => void; } // Extended features for detailed analysis interface ExtendedFeatures extends AudioFeatures { arousal?: number | null; instrumentalness?: number | null; acousticness?: number | null; // All 7 ML mood predictions moodHappy?: number | null; moodSad?: number | null; moodRelaxed?: number | null; moodAggressive?: number | null; moodParty?: number | null; moodAcoustic?: number | null; moodElectronic?: number | null; analysisMode?: string | null; } // Feature configuration with icons and descriptions const FEATURE_CONFIG = [ { key: "energy", label: "Energy", icon: Zap, min: 0, max: 1, description: "Intensity and power", lowLabel: "Calm", highLabel: "Intense", unit: null as string | null, }, { key: "valence", label: "Mood", icon: Heart, min: 0, max: 1, description: "Emotional positivity", lowLabel: "Melancholic", highLabel: "Happy", unit: null as string | null, }, { key: "danceability", label: "Groove", icon: Footprints, min: 0, max: 1, description: "Rhythm & movement", lowLabel: "Freeform", highLabel: "Danceable", unit: null as string | null, }, { key: "bpm", label: "Tempo", icon: Gauge, min: 60, max: 180, description: "Beats per minute", lowLabel: "Slow", highLabel: "Fast", unit: "BPM" as string | null, }, { key: "arousal", label: "Arousal", icon: AudioWaveform, min: 0, max: 1, description: "Excitement level", lowLabel: "Peaceful", highLabel: "Energetic", unit: null as string | null, }, ]; // ML Mood predictions (Enhanced mode only) const ML_MOOD_CONFIG = [ { key: "moodHappy", label: "Happy", icon: Smile, color: "text-yellow-400" }, { key: "moodSad", label: "Sad", icon: Frown, color: "text-blue-400" }, { key: "moodRelaxed", label: "Relaxed", icon: Coffee, color: "text-green-400", }, { key: "moodAggressive", label: "Aggressive", icon: Flame, color: "text-red-400", }, { key: "moodParty", label: "Party", icon: PartyPopper, color: "text-pink-400", }, { key: "moodAcoustic", label: "Acoustic", icon: Guitar, color: "text-amber-400", }, { key: "moodElectronic", label: "Electronic", icon: Radio, color: "text-purple-400", }, ]; 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))); } function getMatchColor(diff: number): string { if (diff < 0.15) return "text-green-400"; if (diff < 0.3) return "text-brand"; return "text-red-400"; } function getMatchBgColor(diff: number): string { if (diff < 0.15) return "bg-green-500/20"; if (diff < 0.3) return "bg-brand/20"; return "bg-red-500/20"; } export function VibeOverlay({ className, currentTrackFeatures, variant = "floating", onClose, }: VibeOverlayProps) { const { vibeMode, vibeSourceFeatures } = useAudioState(); const [isExpanded, setIsExpanded] = useState(true); // Calculate match scores for each feature const featureComparisons = useMemo(() => { if (!vibeSourceFeatures || !currentTrackFeatures) return null; return FEATURE_CONFIG.map((feature) => { const sourceVal = (vibeSourceFeatures as ExtendedFeatures)?.[ feature.key as keyof ExtendedFeatures ]; const currentVal = (currentTrackFeatures as ExtendedFeatures)?.[ feature.key as keyof ExtendedFeatures ]; const sourceNorm = normalizeValue( sourceVal as number, feature.min, feature.max ); const currentNorm = normalizeValue( currentVal as number, feature.min, feature.max ); const diff = Math.abs(sourceNorm - currentNorm); const match = Math.round((1 - diff) * 100); return { ...feature, sourceValue: sourceVal, currentValue: currentVal, sourceNorm, currentNorm, diff, match, hasData: sourceVal != null && currentVal != null, }; }).filter((f) => f.hasData); }, [vibeSourceFeatures, currentTrackFeatures]); // Overall match score const overallMatch = useMemo(() => { if (!featureComparisons || featureComparisons.length === 0) return null; const totalMatch = featureComparisons.reduce( (sum, f) => sum + f.match, 0 ); return Math.round(totalMatch / featureComparisons.length); }, [featureComparisons]); // Don't render if not in vibe mode if (!vibeMode) return null; const isFloating = variant === "floating"; return (
{/* Header */}
isFloating && setIsExpanded(!isExpanded)} >
Vibe Analysis
{overallMatch !== null && ( = 80 ? "text-green-400" : overallMatch >= 60 ? "text-brand" : "text-red-400" )} > {overallMatch}% )} {isFloating && onClose && ( )}
{/* Content */} {isExpanded && (
{/* What is this? */}

Comparing current track to your vibe source. {(vibeSourceFeatures as ExtendedFeatures) ?.analysisMode === "enhanced" ? " Using ML mood predictions for accurate matching." : " Using audio signal analysis for matching."}

{/* Feature Bars */}
{featureComparisons?.map((feature) => { const Icon = feature.icon; return (
{feature.label}
{feature.match}%
{/* Comparison Bar */}
{/* Source marker (yellow dashed) */}
{/* Current value bar */}
{/* Current marker */}
{/* Labels */}
{feature.lowLabel} {feature.unit && feature.currentValue && ( {Math.round( feature.currentValue as number )}{" "} {feature.unit} )} {feature.highLabel}
); })}
{/* ML Moods (Enhanced mode only) */} {(vibeSourceFeatures as ExtendedFeatures)?.analysisMode === "enhanced" && (
ML Mood Analysis
{ML_MOOD_CONFIG.map((mood) => { const sourceVal = ( vibeSourceFeatures as ExtendedFeatures )?.[mood.key as keyof ExtendedFeatures] as | number | null; const currentVal = ( currentTrackFeatures as ExtendedFeatures )?.[mood.key as keyof ExtendedFeatures] as | number | null; const hasData = sourceVal != null && currentVal != null; const diff = hasData ? Math.abs(sourceVal - currentVal) : 0; const match = hasData ? Math.round((1 - diff) * 100) : null; const Icon = mood.icon; if (!hasData) return null; return (
= 80 ? "bg-green-500/10" : match !== null && match >= 60 ? "bg-white/5" : "bg-red-500/10" )} title={`Source: ${Math.round( sourceVal * 100 )}% | Current: ${Math.round( currentVal * 100 )}%`} > {mood.label} {match !== null && ( = 80 ? "text-green-400" : match >= 60 ? "text-gray-300" : "text-red-400" )} > {match}% )}
); })}
)} {/* Legend */}
Source
Current
{/* Match Explanation */} {overallMatch !== null && (
= 80 ? "bg-green-500/10 text-green-400" : overallMatch >= 60 ? "bg-brand/10 text-brand" : "bg-red-500/10 text-red-400" )} > {overallMatch >= 80 && "Excellent match - very similar vibe"} {overallMatch >= 60 && overallMatch < 80 && "Good match - similar energy"} {overallMatch < 60 && "Different vibe - exploring variety"}
)}
)}
); } // Compact version for mobile overlay player - shows as album art replacement export function VibeComparisonArt({ currentTrackFeatures, className, }: { currentTrackFeatures?: AudioFeatures | null; className?: string; }) { const { vibeMode, vibeSourceFeatures } = useAudioState(); // Calculate feature comparisons const comparisons = useMemo(() => { if (!vibeSourceFeatures || !currentTrackFeatures) return null; return FEATURE_CONFIG.slice(0, 4) .map((feature) => { const sourceVal = (vibeSourceFeatures as ExtendedFeatures)?.[ feature.key as keyof ExtendedFeatures ]; const currentVal = (currentTrackFeatures as ExtendedFeatures)?.[ feature.key as keyof ExtendedFeatures ]; const sourceNorm = normalizeValue( sourceVal as number, feature.min, feature.max ); const currentNorm = normalizeValue( currentVal as number, feature.min, feature.max ); const diff = Math.abs(sourceNorm - currentNorm); return { ...feature, sourceNorm, currentNorm, diff, match: Math.round((1 - diff) * 100), hasData: sourceVal != null && currentVal != null, }; }) .filter((f) => f.hasData); }, [vibeSourceFeatures, currentTrackFeatures]); const overallMatch = useMemo(() => { if (!comparisons || comparisons.length === 0) return null; const totalMatch = comparisons.reduce((sum, f) => sum + f.match, 0); return Math.round(totalMatch / comparisons.length); }, [comparisons]); if (!vibeMode || !comparisons) return null; // Radar chart dimensions const size = 280; const center = size / 2; const maxRadius = 110; const getPolygonPoints = (values: number[]) => { const angleStep = (2 * Math.PI) / values.length; return values .map((value, i) => { const angle = angleStep * i - Math.PI / 2; const radius = value * maxRadius; const x = center + radius * Math.cos(angle); const y = center + radius * Math.sin(angle); return `${x},${y}`; }) .join(" "); }; const sourceValues = comparisons.map((c) => c.sourceNorm); const currentValues = comparisons.map((c) => c.currentNorm); return (
{/* Animated background glow */}
{/* Radar Chart */} {/* Background circles */} {[0.25, 0.5, 0.75, 1].map((scale) => ( ))} {/* Axis lines */} {comparisons.map((_, i) => { const angleStep = (2 * Math.PI) / comparisons.length; const angle = angleStep * i - Math.PI / 2; const x = center + maxRadius * Math.cos(angle); const y = center + maxRadius * Math.sin(angle); return ( ); })} {/* Source polygon (yellow, dashed) */} {/* Current polygon (white/gradient, solid) */} {/* Gradient definition */} {/* Feature labels */} {comparisons.map((feature, i) => { const angleStep = (2 * Math.PI) / comparisons.length; const angle = angleStep * i - Math.PI / 2; const labelRadius = maxRadius + 25; const x = center + labelRadius * Math.cos(angle); const y = center + labelRadius * Math.sin(angle); const Icon = feature.icon; return ( {/* Icon background */} = 70 ? "rgba(74, 222, 128, 0.5)" : "rgba(255,255,255,0.2)" } strokeWidth="1" /> {/* Feature label */} {feature.label} {/* Match percentage */} = 70 ? "fill-green-400" : feature.match >= 50 ? "fill-yellow-400" : "fill-red-400" )} > {feature.match}% ); })} {/* Center match score */} = 70 ? "#4ade80" : overallMatch && overallMatch >= 50 ? "#facc15" : "#f87171" } strokeWidth="2" /> = 70 ? "fill-green-400" : overallMatch && overallMatch >= 50 ? "fill-yellow-400" : "fill-red-400" )} > {overallMatch}% Match {/* Legend */}
Source Vibe
Current Track
); }