import type { CSSProperties } from 'react'; import { useMediaQuery } from '../hooks/useMediaQuery'; export interface VibeProfile { energy?: number; noveltyHunger?: number; explorationCoefficient?: number; discoveryRadius?: number; sessionGoal?: { type?: string; progress?: number; target?: number }; } const clamp = (value: number | undefined, fallback: number) => Math.min(1, Math.max(0, typeof value === 'number' ? value : fallback)); function describeProfile(energy: number, novelty: number) { const energyLabel = energy < 0.36 ? 'settled' : energy > 0.68 ? 'charged' : 'flowing'; const discoveryLabel = novelty < 0.36 ? 'familiar' : novelty > 0.64 ? 'adventurous' : 'balanced'; return { energyLabel, discoveryLabel }; } /** An ambient representation of the live recommendation profile. */ export function VibeAura({ profile, ambient = false }: { profile: VibeProfile; ambient?: boolean }) { // The displacement pass is desktop-only; see the comment in the ambient // branch below. Same breakpoint the layout already uses for this element. const plasma = useMediaQuery('(min-width: 640px)'); const energy = clamp(profile.energy, 0.5); const novelty = clamp(profile.noveltyHunger ?? profile.explorationCoefficient, 0.3); const { energyLabel, discoveryLabel } = describeProfile(energy, novelty); // Hue is discovery alone: ember red when the Vibe stays familiar, gold when it // reaches. Energy is deliberately absent — it already drives four motion // channels below, and mixing it in here cancelled half the novelty swing. // Warm range only; the old 205 base put a teal-blue glow in a warm brown room. const hue = Math.round(15 + novelty * 40); const style = { '--vibe-hue': String(hue), '--vibe-pulse': `${(4.8 - energy * 2.3).toFixed(2)}s`, '--vibe-orbit': `${(11 - energy * 4).toFixed(2)}s`, '--vibe-scale': String((0.9 + energy * 0.16).toFixed(2)), '--vibe-core-opacity': ambient ? '0.38' : '1', } as CSSProperties; const aura = (