Avery Felts facaabd4eb Remove artificial 30-second delay from generation loading bar
Loading bar now only displays during actual beat generation instead of
forcing a fixed 30-second wait time.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 23:32:45 -07:00

363 lines
13 KiB
TypeScript

'use client';
import { useState } from 'react';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Slider } from '@/components/ui/slider';
import { Label } from '@/components/ui/label';
import { LayerBox } from './LayerBox';
import { Visualizer } from './Visualizer';
import { useAudioEngine } from '@/hooks/useAudioEngine';
import {
Play,
Pause,
Shuffle,
Drum,
Cloud,
Guitar,
Waves,
Piano,
} from 'lucide-react';
import { Genre, GENRE_CONFIG, INSTRUMENT_OPTIONS } from '@/types/audio';
// Custom Trumpet icon component
function TrumpetIcon({ className }: { className?: string }) {
return (
<svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M3 12h4l3-3v6l-3-3" />
<path d="M10 12h8" />
<circle cx="20" cy="12" r="2" />
<path d="M7 9V6" />
<path d="M7 15v3" />
</svg>
);
}
export function LofiGenerator() {
const {
state,
currentStep,
togglePlayback,
generateNewBeat,
setMasterVolume,
setLayerVolume,
toggleMute,
setGenre,
setDuration,
setInstrument,
setBpm,
setSwing,
} = useAudioEngine();
const [isGenerating, setIsGenerating] = useState(false);
const [generationProgress, setGenerationProgress] = useState(0);
const genres: { value: Genre; label: string }[] = [
{ value: 'hiphop', label: 'Hip Hop' },
{ value: 'classical', label: 'Classical' },
{ value: 'trap', label: 'Trap' },
{ value: 'pop', label: 'Pop' },
];
const handleGenerateBeat = async () => {
setIsGenerating(true);
setGenerationProgress(0);
// Animate progress while generating
let currentProgress = 0;
const progressInterval = setInterval(() => {
const jump = Math.random() > 0.5 ? Math.random() * 15 : Math.random() * 8;
currentProgress = Math.min(currentProgress + jump, 90);
setGenerationProgress(currentProgress);
}, 50);
// Generate the beat
await generateNewBeat();
clearInterval(progressInterval);
setGenerationProgress(100);
setTimeout(() => {
setIsGenerating(false);
setGenerationProgress(0);
}, 300);
};
return (
<div className="min-h-screen flex flex-col items-center p-4 pt-8">
{/* Modern Header */}
<header className="w-full max-w-2xl mb-8">
<div className="flex items-center justify-center gap-3 mb-2">
<div className="h-10 w-10 rounded-xl bg-gradient-to-br from-lofi-orange via-lofi-pink to-lofi-purple flex items-center justify-center">
<Waves className="h-5 w-5 text-white" />
</div>
<h1 className="text-3xl font-bold tracking-tight bg-gradient-to-r from-lofi-orange via-lofi-pink to-lofi-purple bg-clip-text text-transparent">
Beat Generator
</h1>
</div>
<p className="text-center text-sm text-muted-foreground">
Create custom beats across multiple genres
</p>
</header>
<Card className="w-full max-w-2xl bg-card/80 backdrop-blur-sm border-border/50">
<CardContent className="p-6 space-y-6">
{/* Top Controls: Duration & Genre */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="text-xs text-muted-foreground uppercase tracking-wider">
Duration
</Label>
<div className="flex items-center gap-2">
<Slider
value={[state.duration]}
onValueChange={([v]) => setDuration(v)}
min={1}
max={3}
step={1}
className="flex-1"
/>
<span className="text-sm font-mono w-12 text-right">
{state.duration} min
</span>
</div>
</div>
<div className="space-y-2">
<Label className="text-xs text-muted-foreground uppercase tracking-wider">
Genre
</Label>
<Select value={state.genre} onValueChange={(v) => setGenre(v as Genre)}>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
{genres.map((g) => (
<SelectItem key={g.value} value={g.value}>
{g.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* Transport Controls */}
<div className="flex items-center justify-center gap-4">
<Button
size="lg"
onClick={togglePlayback}
disabled={isGenerating}
className="h-14 w-14 rounded-full bg-gradient-to-br from-lofi-orange to-lofi-pink hover:opacity-90"
>
{state.isPlaying ? (
<Pause className="h-6 w-6" />
) : (
<Play className="h-6 w-6 ml-1" />
)}
</Button>
<Button
variant="secondary"
size="lg"
onClick={handleGenerateBeat}
disabled={isGenerating}
className="gap-2"
>
<Shuffle className="h-4 w-4" />
Generate
</Button>
</div>
{/* Generation Progress Bar */}
{isGenerating && (
<div className="space-y-2">
<div className="flex items-center justify-between text-xs text-muted-foreground">
<span>Generating beat with song structure...</span>
<span>{Math.round(generationProgress)}%</span>
</div>
<div className="h-2 bg-secondary rounded-full overflow-hidden">
<div
className="h-full bg-gradient-to-r from-lofi-orange via-lofi-pink to-lofi-purple transition-all duration-100 ease-out"
style={{ width: `${generationProgress}%` }}
/>
</div>
<p className="text-xs text-muted-foreground/60 text-center">
Creating intro, verse, bridge, chorus, and outro...
</p>
</div>
)}
{/* Current Section Indicator */}
{state.isPlaying && !isGenerating && (
<div className="flex items-center justify-center gap-2">
<span className="text-xs text-muted-foreground uppercase tracking-wider">Now Playing:</span>
<span className="px-3 py-1 rounded-full text-xs font-medium bg-gradient-to-r from-lofi-orange/20 via-lofi-pink/20 to-lofi-purple/20 border border-lofi-pink/30 text-lofi-pink capitalize">
{state.currentSection}
</span>
</div>
)}
{/* Visualizer */}
<Visualizer currentStep={currentStep} isPlaying={state.isPlaying} />
{/* Master Controls */}
<div className="grid grid-cols-3 gap-4">
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-xs text-muted-foreground">Master</Label>
<span className="text-xs text-muted-foreground font-mono">
{Math.round(state.volumes.master * 100)}%
</span>
</div>
<Slider
value={[state.volumes.master]}
onValueChange={([v]) => setMasterVolume(v)}
min={0}
max={1}
step={0.01}
/>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-xs text-muted-foreground">BPM</Label>
<span className="text-xs text-muted-foreground font-mono">
{state.bpm}
</span>
</div>
<Slider
value={[state.bpm]}
onValueChange={([v]) => setBpm(v)}
min={60}
max={180}
step={1}
/>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-xs text-muted-foreground">Swing</Label>
<span className="text-xs text-muted-foreground font-mono">
{Math.round(state.swing * 100)}%
</span>
</div>
<Slider
value={[state.swing]}
onValueChange={([v]) => setSwing(v)}
min={0}
max={0.5}
step={0.01}
/>
</div>
</div>
{/* Instrument Layers */}
<div className="space-y-3">
<h3 className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
Instruments
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<LayerBox
title="Drums"
icon={<Drum className="h-4 w-4" />}
volume={state.volumes.drums}
muted={state.muted.drums}
instrument={state.instruments.drums}
instrumentOptions={INSTRUMENT_OPTIONS.drums}
onVolumeChange={(v) => setLayerVolume('drums', v)}
onToggleMute={() => toggleMute('drums')}
onInstrumentChange={(v) => setInstrument('drums', v)}
accentColor="grey"
/>
<LayerBox
title="Bass"
icon={<Guitar className="h-4 w-4" />}
volume={state.volumes.bass}
muted={state.muted.bass}
instrument={state.instruments.bass}
instrumentOptions={INSTRUMENT_OPTIONS.bass}
onVolumeChange={(v) => setLayerVolume('bass', v)}
onToggleMute={() => toggleMute('bass')}
onInstrumentChange={(v) => setInstrument('bass', v)}
accentColor="red"
/>
<LayerBox
title="Piano"
icon={<Piano className="h-4 w-4" />}
volume={state.volumes.piano}
muted={state.muted.piano}
instrument={state.instruments.piano}
instrumentOptions={INSTRUMENT_OPTIONS.piano}
onVolumeChange={(v) => setLayerVolume('piano', v)}
onToggleMute={() => toggleMute('piano')}
onInstrumentChange={(v) => setInstrument('piano', v)}
accentColor="green"
/>
<LayerBox
title="Brass"
icon={<TrumpetIcon className="h-4 w-4" />}
volume={state.volumes.brass}
muted={state.muted.brass}
instrument={state.instruments.brass}
instrumentOptions={INSTRUMENT_OPTIONS.brass}
onVolumeChange={(v) => setLayerVolume('brass', v)}
onToggleMute={() => toggleMute('brass')}
onInstrumentChange={(v) => setInstrument('brass', v)}
accentColor="babyblue"
/>
<LayerBox
title="Chords"
icon={<Waves className="h-4 w-4" />}
volume={state.volumes.chords}
muted={state.muted.chords}
instrument={state.instruments.chords}
instrumentOptions={INSTRUMENT_OPTIONS.chords}
onVolumeChange={(v) => setLayerVolume('chords', v)}
onToggleMute={() => toggleMute('chords')}
onInstrumentChange={(v) => setInstrument('chords', v)}
accentColor="pink"
/>
<LayerBox
title="Ambient"
icon={<Cloud className="h-4 w-4" />}
volume={state.volumes.ambient}
muted={state.muted.ambient}
instrument={state.instruments.ambient}
instrumentOptions={INSTRUMENT_OPTIONS.ambient}
onVolumeChange={(v) => setLayerVolume('ambient', v)}
onToggleMute={() => toggleMute('ambient')}
onInstrumentChange={(v) => setInstrument('ambient', v)}
accentColor="purple"
/>
</div>
</div>
{/* Footer */}
<p className="text-center text-xs text-muted-foreground/60 pt-2">
Click play to initialize audio engine Genre: {GENRE_CONFIG[state.genre].name}
</p>
</CardContent>
</Card>
</div>
);
}