AudioForge / frontend /src /components /generation-form.tsx
OnyxlMunkey's picture
c618549
"use client";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { Sparkles, Loader2 } from "lucide-react";
import { generationsApi, type GenerationRequest } from "@/lib/api";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Slider } from "@/components/ui/slider";
import { useToast } from "@/hooks/use-toast";
import { PromptSuggestions } from "@/components/prompt-suggestions";
const generationSchema = z.object({
prompt: z.string().min(1, "Prompt is required").max(1000),
lyrics: z.string().optional(),
duration: z.number().min(5).max(300).optional(),
style: z.string().optional(),
voice_preset: z.string().optional(),
vocal_volume: z.number().min(0).max(1).optional(),
instrumental_volume: z.number().min(0).max(1).optional(),
});
type GenerationFormData = z.infer<typeof generationSchema>;
export function GenerationForm() {
const [isExpanded, setIsExpanded] = useState(false);
const [showSuggestions, setShowSuggestions] = useState(true);
const { toast } = useToast();
const queryClient = useQueryClient();
const {
register,
handleSubmit,
setValue,
watch,
formState: { errors },
} = useForm<GenerationFormData>({
resolver: zodResolver(generationSchema),
defaultValues: {
duration: 30,
vocal_volume: 0.7,
instrumental_volume: 0.8,
},
});
const successMessages = [
"🎵 Your masterpiece is being forged!",
"🎸 The AI musicians are tuning up!",
"🎹 Composing your sonic masterpiece!",
"🎺 The orchestra is assembling!",
"🎼 Your music is coming to life!",
];
const mutation = useMutation({
mutationFn: (data: GenerationRequest) => generationsApi.create(data),
onSuccess: () => {
const randomMessage = successMessages[Math.floor(Math.random() * successMessages.length)];
toast({
title: randomMessage,
description: "Watch the magic happen in your creations list below.",
});
queryClient.invalidateQueries({ queryKey: ["generations"] });
setIsExpanded(false);
setShowSuggestions(false);
},
onError: (error: Error) => {
toast({
title: "😔 Oops! Something went wrong",
description: error.message || "Failed to start generation. Please try again.",
variant: "destructive",
});
},
});
const onSubmit = (data: GenerationFormData) => {
mutation.mutate(data);
};
const handleSelectPrompt = (prompt: string) => {
setValue("prompt", prompt);
setShowSuggestions(false);
};
const promptValue = watch("prompt");
const vocalVolume = watch("vocal_volume") ?? 0.7;
const instrumentalVolume = watch("instrumental_volume") ?? 0.8;
return (
<div className="bg-card border rounded-lg p-6 shadow-lg hover:shadow-xl transition-all duration-300 group">
<div className="mb-4 flex items-center gap-2">
<div className="w-1 h-8 bg-gradient-to-b from-primary to-purple-500 rounded-full" />
<h2 className="font-display text-2xl font-bold">Compose Something New</h2>
</div>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
<div>
<Label htmlFor="prompt" className="text-lg font-semibold flex items-center gap-2">
<span className="text-2xl">🎼</span>
Describe your music
</Label>
<Textarea
id="prompt"
{...register("prompt")}
placeholder="Try: 'A dreamy lo-fi hip-hop beat with vinyl crackle and soft piano melodies' or 'Epic orchestral soundtrack with soaring strings and thunderous percussion'"
className="mt-2 min-h-[120px] focus:ring-2 focus:ring-primary/50 transition-all"
/>
{errors.prompt && (
<p className="text-sm text-destructive mt-1 animate-fade-in">
{errors.prompt.message}
</p>
)}
<p className="text-xs text-muted-foreground mt-2">
💡 Tip: Be specific about instruments, mood, tempo, and style for best results
</p>
</div>
{showSuggestions && !promptValue && (
<PromptSuggestions onSelectPrompt={handleSelectPrompt} />
)}
<div>
<Label htmlFor="lyrics" className="text-base flex items-center gap-2">
<span className="text-xl">🎤</span>
Lyrics (optional)
</Label>
<Textarea
id="lyrics"
{...register("lyrics")}
placeholder="Add your lyrics here and we'll bring them to life with AI vocals...&#10;&#10;Verse 1:&#10;Walking through the city lights&#10;Everything feels so right..."
className="mt-2 min-h-[100px] focus:ring-2 focus:ring-purple-500/50 transition-all"
/>
<p className="text-xs text-muted-foreground mt-2">
✨ Pro tip: Structure your lyrics with verses, chorus, and bridge for better results
</p>
</div>
{isExpanded && (
<div className="space-y-4 pt-4 border-t">
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="duration">Duration (seconds)</Label>
<Input
id="duration"
type="number"
{...register("duration", { valueAsNumber: true })}
min={5}
max={300}
className="mt-1"
/>
</div>
<div>
<Label htmlFor="style">Style</Label>
<Select
onValueChange={(value) => setValue("style", value === "auto" ? undefined : value)}
defaultValue="auto"
>
<SelectTrigger id="style" className="mt-1">
<SelectValue placeholder="Auto-detect" />
</SelectTrigger>
<SelectContent>
<SelectItem value="auto">Auto-detect</SelectItem>
<SelectItem value="rock">Rock</SelectItem>
<SelectItem value="pop">Pop</SelectItem>
<SelectItem value="jazz">Jazz</SelectItem>
<SelectItem value="electronic">Electronic</SelectItem>
<SelectItem value="hip-hop">Hip-Hop</SelectItem>
<SelectItem value="classical">Classical</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{watch("lyrics") && (
<div className="space-y-2">
<Label>Vocal Volume: {Math.round(vocalVolume * 100)}%</Label>
<Slider
value={[vocalVolume]}
onValueChange={([value]) => setValue("vocal_volume", value)}
min={0}
max={1}
step={0.1}
/>
</div>
)}
<div className="space-y-2">
<Label>
Instrumental Volume: {Math.round(instrumentalVolume * 100)}%
</Label>
<Slider
value={[instrumentalVolume]}
onValueChange={([value]) =>
setValue("instrumental_volume", value)
}
min={0}
max={1}
step={0.1}
/>
</div>
</div>
)}
<div className="flex gap-3">
<Button
type="submit"
disabled={mutation.isPending}
className="flex-1 relative overflow-hidden group/btn"
size="lg"
>
<span className="absolute inset-0 bg-gradient-to-r from-primary via-purple-500 to-primary opacity-0 group-hover/btn:opacity-100 transition-opacity duration-500" />
<span className="relative flex items-center justify-center">
{mutation.isPending ? (
<>
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
<span className="animate-pulse">Forging your masterpiece...</span>
</>
) : (
<>
<Sparkles className="mr-2 h-5 w-5 group-hover/btn:animate-bounce-subtle" />
Generate Music
</>
)}
</span>
</Button>
<Button
type="button"
variant="outline"
onClick={() => setIsExpanded(!isExpanded)}
className="hover:bg-secondary/80 transition-all"
>
{isExpanded ? "Less" : "More"} Options
</Button>
</div>
</form>
</div>
);
}