Spaces:
Build error
Build error
| "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... Verse 1: Walking through the city lights 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> | |
| ); | |
| } | |