import { useCallback, useMemo, useRef, useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import { Heart, Loader2, Play, Shuffle, ThumbsDown, Sparkles, Square } from 'lucide-react'; import { advanceVibe, endVibeSession, reportVibeEvent, startVibeSession, vibeErrorMessage } from '../services/vibeSession'; import { trackService } from '../services/trackService'; import { usePlaybackStore } from '../store/usePlaybackStore'; import { useVibeStore } from '../store/useVibeStore'; import { TrackRow } from '../components/TrackRow'; import { PageContainer } from '../components/PageContainer'; import type { Track } from '../types'; import { VibeTimeline } from '../components/VibeTimeline'; import { toast } from '../store/useToastStore'; const SEED_LIST_SIZE = 50; function sampleTracks(tracks: Track[], count: number): Track[] { const sampled = [...tracks]; for (let index = sampled.length - 1; index > 0; index--) { const pick = Math.floor(Math.random() * (index + 1)); [sampled[index], sampled[pick]] = [sampled[pick], sampled[index]]; } return sampled.slice(0, count); } export default function Vibe() { const { currentTrack } = usePlaybackStore(); const { activeSessionId, buffer, initialBatchStatus, planVersion, } = useVibeStore(); const [starting, setStarting] = useState(false); const [error, setError] = useState(null); const [empty, setEmpty] = useState(false); const startingRef = useRef(false); const { data: libraryTracks = [], isLoading: libraryLoading } = useQuery({ queryKey: ['library-seed'], // Fetch the eligible library once so Surprise me is not restricted to the // most-played 50 tracks. The backend excludes hidden/deleted tracks. queryFn: () => trackService.listTracks({ limit: 5000, sort_by: 'title', order: 'ASC' }), enabled: !activeSessionId, }); const seedTracks = useMemo(() => sampleTracks(libraryTracks, SEED_LIST_SIZE), [libraryTracks]); const startSession = useCallback( async (seed: Track) => { if (startingRef.current) return; startingRef.current = true; setStarting(true); setError(null); setEmpty(false); try { const result = await startVibeSession(seed); if (result.tracks.length === 0) { setEmpty(true); setError('Could not load recommendations for this vibe. Please try another seed.'); } } catch (startError) { setError(vibeErrorMessage(startError)); } finally { startingRef.current = false; setStarting(false); } }, [] ); const startFromCurrent = useCallback(() => { if (currentTrack) void startSession(currentTrack); }, [currentTrack, startSession]); const surpriseMe = useCallback(() => { if (libraryTracks.length === 0) return; const seed = libraryTracks[Math.floor(Math.random() * libraryTracks.length)]; void startSession(seed); }, [libraryTracks, startSession]); const handleKeep = useCallback(() => { if (currentTrack) { void reportVibeEvent('kept', currentTrack.id).catch((feedbackError) => setError(vibeErrorMessage(feedbackError))); toast.success(`Kept "${currentTrack.title}"`); } }, [currentTrack]); const handleDislike = useCallback(() => { if (currentTrack) { void advanceVibe('disliked').catch((feedbackError) => setError(vibeErrorMessage(feedbackError))); } }, [currentTrack]); const handleEnd = useCallback(() => { void endVibeSession().catch((endError) => setError(vibeErrorMessage(endError))); setEmpty(false); }, []); const upcoming = buffer; // ---- Start screen (no active session) ---- if (!activeSessionId) { return (
Rolling Vibe

Start a Vibe

An infinite, ever-rolling stream of recommendations seeded from a track you love.

Pick a seed below and playback starts immediately, with a rolling timeline of upcoming tracks. Keep what you love, Dislike & skip what you don't — the vibe adapts as you go.

{error && (
{error}
)}
{currentTrack && ( )}
{!libraryLoading && seedTracks.length > 0 && (

Or pick a seed track

    {seedTracks.map((track, index) => (
  • ))}
)}
); } // ---- Active session ---- return (

Vibing

{initialBatchStatus === 'loading' && }
{(empty || initialBatchStatus === 'exhausted' || initialBatchStatus === 'failed') && (
{initialBatchStatus === 'failed' ? 'Could not load recommendations for this Vibe. Try starting a different one.' : 'No recommendations came back for this seed yet. Try ending and starting a different vibe.'}
)} {error && (
{error}
)} {planVersion &&

Plan revision {planVersion}; upcoming tracks may change as you listen.

} {currentTrack && (
)}
); }