"use client"; import { useAudioState, AudioFeatures } from "@/lib/audio-state-context"; import { cn } from "@/utils/cn"; import { useMemo, useState, useEffect, useRef, memo } from "react"; import { X, Maximize2, Minimize2 } from "lucide-react"; import { motion, AnimatePresence } from "framer-motion"; import { ResponsiveContainer, RadarChart, PolarGrid, PolarAngleAxis, Radar, } from "recharts"; // Extended feature interface for all enhanced vibe data interface ExtendedFeatures extends AudioFeatures { danceabilityMl?: number | null; } // Feature configurations for radar chart const RADAR_FEATURES = [ { key: "energy", label: "Energy", min: 0, max: 1 }, { key: "valence", label: "Mood", min: 0, max: 1 }, { key: "arousal", label: "Arousal", min: 0, max: 1 }, { key: "danceability", label: "Dance", min: 0, max: 1 }, { key: "bpm", label: "Tempo", min: 60, max: 200 }, { key: "moodHappy", label: "Happy", min: 0, max: 1 }, { key: "moodSad", label: "Sad", min: 0, max: 1 }, { key: "moodRelaxed", label: "Relaxed", min: 0, max: 1 }, { key: "moodAggressive", label: "Aggressive", min: 0, max: 1 }, { key: "moodParty", label: "Party", min: 0, max: 1 }, { key: "moodAcoustic", label: "Acoustic", min: 0, max: 1 }, { key: "moodElectronic", label: "Electronic", min: 0, max: 1 }, ]; const ML_MOODS = [ { key: "moodHappy", label: "Happy", color: "#ecb200" }, { key: "moodSad", label: "Sad", color: "#5c8dd6" }, { key: "moodRelaxed", label: "Relaxed", color: "#1db954" }, { key: "moodAggressive", label: "Aggressive", color: "#e35656" }, { key: "moodParty", label: "Party", color: "#e056a0" }, { key: "moodAcoustic", label: "Acoustic", color: "#d4a656" }, { key: "moodElectronic", label: "Electronic", color: "#a056e0" }, ]; interface EnhancedVibeOverlayProps { className?: string; currentTrackFeatures?: ExtendedFeatures | null; variant?: "floating" | "inline"; onClose?: () => void; } // Helper to normalize values function normalizeValue( value: number | null | undefined, min: number, max: number ): number { if (value == null) return 0; return Math.max(0, Math.min(1, (value - min) / (max - min))); } // Helper to get feature value safely function getFeatureValue( features: ExtendedFeatures | null | undefined, key: string ): number | null { if (!features) return null; const value = (features as Record)[key]; if (typeof value === "number") return value; return null; } // Audio waveform visualization component - slower, smoother animation const AudioWaveform = memo(function AudioWaveform({ energy, bpm, }: { energy: number; bpm: number; }) { const canvasRef = useRef(null); const animationRef = useRef(null); const timeRef = useRef(0); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext("2d"); if (!ctx) return; const resize = () => { canvas.width = canvas.offsetWidth * 2; canvas.height = canvas.offsetHeight * 2; ctx.scale(2, 2); }; resize(); const animate = () => { const width = canvas.offsetWidth; const height = canvas.offsetHeight; ctx.clearRect(0, 0, width, height); // Calculate wave parameters based on audio features - SLOWER const baseAmplitude = height * 0.35 * Math.max(0.3, energy); const frequency = (bpm / 120) * 0.015; // Reduced frequency const speed = (bpm / 120) * 0.015; // Slower speed (was 0.05) // Draw multiple layered waves with #ecb200 accent for (let layer = 0; layer < 3; layer++) { const layerOffset = layer * 0.4; const layerAmplitude = baseAmplitude * (1 - layer * 0.2); const alpha = 0.5 - layer * 0.12; ctx.beginPath(); ctx.moveTo(0, height / 2); for (let x = 0; x <= width; x += 2) { const y = height / 2 + Math.sin( (x * frequency + timeRef.current * speed + layerOffset) * Math.PI ) * layerAmplitude + Math.sin( (x * frequency * 1.5 + timeRef.current * speed * 0.8) * Math.PI ) * (layerAmplitude * 0.25); ctx.lineTo(x, y); } ctx.strokeStyle = "#ecb200"; ctx.globalAlpha = alpha; ctx.lineWidth = 2 - layer * 0.4; ctx.stroke(); } // Draw glow effect ctx.globalAlpha = 0.15; ctx.filter = "blur(10px)"; ctx.beginPath(); ctx.moveTo(0, height / 2); for (let x = 0; x <= width; x += 2) { const y = height / 2 + Math.sin((x * frequency + timeRef.current * speed) * Math.PI) * baseAmplitude; ctx.lineTo(x, y); } ctx.strokeStyle = "#ecb200"; ctx.lineWidth = 8; ctx.stroke(); ctx.filter = "none"; ctx.globalAlpha = 1; timeRef.current += 1; animationRef.current = requestAnimationFrame(animate); }; animate(); return () => { if (animationRef.current) { cancelAnimationFrame(animationRef.current); } }; }, [energy, bpm]); return ( ); }); export function EnhancedVibeOverlay({ className, currentTrackFeatures, variant = "floating", onClose, }: EnhancedVibeOverlayProps) { const { vibeMode, vibeSourceFeatures } = useAudioState(); const [isExpanded, setIsExpanded] = useState(true); const [viewMode, setViewMode] = useState<"full" | "minimal">("full"); // Use vibeSourceFeatures as the source const sourceFeatures = vibeSourceFeatures as ExtendedFeatures | null; // Current features should be from the actual current track, NOT fallback to source const currentFeatures = currentTrackFeatures as ExtendedFeatures | null; // For display, use currentFeatures if available, otherwise show source (for first track) const displayFeatures = currentFeatures || sourceFeatures; // Prepare radar chart data const radarData = useMemo(() => { return RADAR_FEATURES.map((feature) => { const sourceVal = getFeatureValue(sourceFeatures, feature.key); // For current, use displayFeatures (falls back to source if current is null) const currentVal = getFeatureValue(displayFeatures, feature.key); return { feature: feature.label, source: normalizeValue(sourceVal, feature.min, feature.max) * 100, current: normalizeValue(currentVal, feature.min, feature.max) * 100, fullMark: 100, }; }); }, [sourceFeatures, displayFeatures]); // Calculate overall match score using cosine similarity (same as backend) const matchScore = useMemo(() => { if (!sourceFeatures || !currentFeatures) return null; // Build feature vectors for cosine similarity const sourceVector: number[] = []; const currentVector: number[] = []; RADAR_FEATURES.forEach((feature) => { const sourceVal = getFeatureValue(sourceFeatures, feature.key); const currentVal = getFeatureValue(currentFeatures, feature.key); // Use 0.5 as default for missing values (neutral) const sourceNorm = normalizeValue(sourceVal ?? (feature.min + feature.max) / 2, feature.min, feature.max); const currentNorm = normalizeValue(currentVal ?? (feature.min + feature.max) / 2, feature.min, feature.max); // Weight ML mood features higher (1.3x) like backend does const weight = feature.key.startsWith("mood") ? 1.3 : 1.0; sourceVector.push(sourceNorm * weight); currentVector.push(currentNorm * weight); }); // Calculate cosine similarity let dotProduct = 0; let magSource = 0; let magCurrent = 0; for (let i = 0; i < sourceVector.length; i++) { dotProduct += sourceVector[i] * currentVector[i]; magSource += sourceVector[i] * sourceVector[i]; magCurrent += currentVector[i] * currentVector[i]; } const magnitude = Math.sqrt(magSource) * Math.sqrt(magCurrent); if (magnitude === 0) return null; const similarity = dotProduct / magnitude; return Math.round(similarity * 100); }, [sourceFeatures, currentFeatures]); // Get audio features for waveform - use current if available, fallback to source const energy = getFeatureValue(currentFeatures, "energy") ?? getFeatureValue(sourceFeatures, "energy") ?? 0.5; const bpm = getFeatureValue(currentFeatures, "bpm") ?? getFeatureValue(sourceFeatures, "bpm") ?? 120; // Don't render if not in vibe mode if (!vibeMode) return null; const isFloating = variant === "floating"; return ( {/* Header */}
isFloating && setIsExpanded(!isExpanded)} >
Vibe Analysis {matchScore !== null && ( = 80 ? "bg-[#1db954]/20 text-[#1db954]" : matchScore >= 60 ? "bg-[#ecb200]/20 text-[#ecb200]" : "bg-[#e35656]/20 text-[#e35656]" )} > {matchScore}% Match )}
{isFloating && onClose && ( )}
{/* Content */} {isExpanded && ( {/* Waveform visualization */}
{viewMode === "full" ? (
{/* Radar Chart */}
{/* Source track (dashed yellow) */} {/* Current track (solid white) */}
{/* Feature cards */}
Analysis Mode
{sourceFeatures?.analysisMode || "Standard"}
Tempo
{bpm ? `${Math.round(bpm)} BPM` : "N/A"}
{/* ML Mood Spectrum - shows current track's moods */}
Mood Spectrum {!currentFeatures && "(Source Track)"}
{ML_MOODS.map((mood) => { const value = getFeatureValue( displayFeatures, mood.key ); // Show the bar even if value is 0, only hide if null/undefined const percentage = value != null ? Math.round(value * 100) : 0; const hasValue = value != null; return (
{mood.label} {hasValue ? `${percentage}%` : "—"}
); })}
) : ( /* Minimal view - just radar */
)} {/* Legend */}
Source
Current
)} ); }