Files
muzick/frontend/src/components/VibeAura.tsx
T
kami bfe22745bc feat(discovery): acquire recommendations that keep their names
Acquisition ran yt-dlp without --embed-metadata, so every download
arrived untagged. The scanner then stored the video id as the title and
"Unknown Artist" as the artist, the vetted-candidate tag check rejected
the mismatch, and all 18 acquired tracks were hidden and retired.

- Pass --embed-metadata so downloads carry real tags.
- Let a scan take fallback title/artist from the candidate, for sources
  that still ship untagged files.
- Install Deno alongside yt-dlp: YouTube guards some formats with a JS
  challenge yt-dlp must execute, and no other runtime is enabled.
- Dedupe candidates by artist and title. The (source, external_id) key
  misses the same song reaching us under two Deezer release ids.

Also carries the in-flight discovery work this builds on: the
Recommendations page replacing Discover, the discovery source service,
and the acquisition spec tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 18:43:58 +04:00

115 lines
4.8 KiB
TypeScript

import type { CSSProperties } from 'react';
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 }) {
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 = (
<div className="grid h-16 w-16 place-items-center rounded-full">
<div className="vibe-aura-halo" />
<div className="vibe-aura-orbit vibe-aura-orbit-one" />
<div className="vibe-aura-orbit vibe-aura-orbit-two" />
<div className="vibe-aura-core" style={{ opacity: 'var(--vibe-core-opacity, 1)' }}>
<span className="vibe-aura-spark vibe-aura-spark-one" />
<span className="vibe-aura-spark vibe-aura-spark-two" />
<span className="vibe-aura-heart" />
</div>
{!ambient && (
<div className="pointer-events-none absolute right-0 top-[calc(100%+0.4rem)] z-10 w-44 rounded-md border border-border bg-surface1 px-3 py-2 text-xs leading-relaxed text-muted opacity-0 shadow-xl transition-opacity group-hover:opacity-100 group-focus:opacity-100">
<span className="block font-medium text-text">Your Vibe is {energyLabel}</span>
<span>Leaning {discoveryLabel}; it shifts as you listen.</span>
</div>
)}
</div>
);
if (ambient) {
// Displacing clean gradients by animated fractal noise is what separates a
// plasma from a blurred blob. baseFrequency is animated in SMIL rather than
// CSS because no CSS property reaches inside an SVG filter primitive.
return (
<div
className="vibe-aura-blob pointer-events-none absolute z-0 h-[380px] w-[420px] -translate-x-1/2 -translate-y-1/2 opacity-85 mix-blend-screen sm:h-[560px] sm:w-[660px] sm:opacity-95"
style={style}
role="img"
aria-label={`Current Vibe: ${energyLabel} energy and ${discoveryLabel} discovery`}
>
<svg aria-hidden className="absolute h-0 w-0">
<filter id="vibe-plasma" x="-30%" y="-30%" width="160%" height="160%">
<feTurbulence
type="fractalNoise"
baseFrequency="0.009 0.014"
numOctaves={3}
seed={7}
result="noise"
>
<animate
attributeName="baseFrequency"
dur={`${(18 - energy * 8).toFixed(1)}s`}
values="0.009 0.014; 0.021 0.007; 0.009 0.014"
repeatCount="indefinite"
/>
</feTurbulence>
<feDisplacementMap
in="SourceGraphic"
in2="noise"
scale={String(Math.round(46 + energy * 70))}
xChannelSelector="R"
yChannelSelector="G"
/>
</filter>
</svg>
<div className="vibe-aura-stack">
<div className="vibe-aura-layer vibe-aura-rays-layer" />
<div className="vibe-aura-layer vibe-aura-swirl-layer" />
<div className="vibe-aura-layer vibe-aura-core-layer" />
</div>
</div>
);
}
return (
<div
className="group relative grid h-16 w-16 shrink-0 place-items-center rounded-full focus:outline-none"
style={style}
role="img"
tabIndex={0}
aria-label={`Current Vibe: ${energyLabel} energy and ${discoveryLabel} discovery`}
title={`Current Vibe: ${energyLabel} energy, ${discoveryLabel} discovery`}
>
{aura}
</div>
);
}