|
|
import React, { useMemo, useState } from "react"; |
|
|
import { motion } from "framer-motion"; |
|
|
import { Play, Pause, UploadCloud, Sparkles, Music, Settings2, Gauge, Share2, Wand2 } from "lucide-react"; |
|
|
import { ResponsiveContainer, RadialBarChart, RadialBar, PolarAngleAxis } from "recharts"; |
|
|
import { |
|
|
Card, |
|
|
CardContent, |
|
|
CardDescription, |
|
|
CardFooter, |
|
|
CardHeader, |
|
|
CardTitle, |
|
|
} from "@/components/ui/card"; |
|
|
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 { Slider } from "@/components/ui/slider"; |
|
|
import { Switch } from "@/components/ui/switch"; |
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; |
|
|
import { Separator } from "@/components/ui/separator"; |
|
|
|
|
|
|
|
|
const Stat = ({ label, value, hint }: { label: string; value: string; hint?: string }) => ( |
|
|
<div className="flex flex-col"> |
|
|
<span className="text-xs text-muted-foreground">{label}</span> |
|
|
<span className="font-semibold">{value}</span> |
|
|
{hint ? <span className="text-[11px] text-muted-foreground">{hint}</span> : null} |
|
|
</div> |
|
|
); |
|
|
|
|
|
const ScoreGauge = ({ score = 72, label = "Algorithm Fit" }: { score?: number; label?: string }) => { |
|
|
const data = useMemo(() => [{ name: label, value: score, fill: "hsl(var(--primary))" }], [score, label]); |
|
|
return ( |
|
|
<div className="h-40 w-40"> |
|
|
<ResponsiveContainer> |
|
|
<RadialBarChart innerRadius="70%" outerRadius="100%" data={data} startAngle={220} endAngle={-40}> |
|
|
<PolarAngleAxis type="number" domain={[0, 100]} tick={false} /> |
|
|
<RadialBar dataKey="value" cornerRadius={20} /> |
|
|
</RadialBarChart> |
|
|
</ResponsiveContainer> |
|
|
<div className="-mt-24 text-center"> |
|
|
<div className="text-3xl font-bold">{score}</div> |
|
|
<div className="text-xs text-muted-foreground">{label}</div> |
|
|
</div> |
|
|
</div> |
|
|
); |
|
|
}; |
|
|
|
|
|
export default function PromptComposer() { |
|
|
const [prompt, setPrompt] = useState("Upbeat 115 BPM house track with soulful vocals; short 5s intro; bright mix; optimized for workout playlists."); |
|
|
const [title, setTitle] = useState("Moving On (Pop‑Punk Revival)"); |
|
|
const [tempo, setTempo] = useState(115); |
|
|
const [lufs, setLufs] = useState(-14); |
|
|
const [autoMaster, setAutoMaster] = useState(true); |
|
|
const [generating, setGenerating] = useState(false); |
|
|
const [playing, setPlaying] = useState(false); |
|
|
|
|
|
const score = useMemo(() => { |
|
|
|
|
|
let s = 60 + Math.min(20, Math.max(0, 120 - Math.abs(120 - tempo)) / 2); |
|
|
s += autoMaster ? 8 : 0; |
|
|
s += lufs >= -14 && lufs <= -13 ? 6 : 0; |
|
|
return Math.min(99, Math.round(s)); |
|
|
}, [tempo, lufs, autoMaster]); |
|
|
|
|
|
const recs = useMemo(() => { |
|
|
const list: string[] = []; |
|
|
if (prompt.toLowerCase().includes("intro") === false) list.push("Add a clear hook within first 10s to reduce early skips."); |
|
|
if (lufs < -16) list.push("Raise loudness toward −14 LUFS (Spotify norm)." ); |
|
|
if (tempo < 100) list.push("Consider +10–15 BPM for workout playlists."); |
|
|
list.push("Tag with 3–5 micro‑genres your audience actually searches."); |
|
|
list.push("Add call‑to‑action in description (save/share)." ); |
|
|
return list; |
|
|
}, [prompt, lufs, tempo]); |
|
|
|
|
|
const handleGenerate = async () => { |
|
|
setGenerating(true); |
|
|
await new Promise(r => setTimeout(r, 1200)); |
|
|
setGenerating(false); |
|
|
}; |
|
|
|
|
|
return ( |
|
|
<div className="mx-auto max-w-6xl p-6 space-y-6"> |
|
|
<div className="flex items-center gap-3"> |
|
|
<motion.div initial={{ opacity: 0, y: -6 }} animate={{ opacity: 1, y: 0 }}> |
|
|
<Sparkles className="h-6 w-6 text-primary" /> |
|
|
</motion.div> |
|
|
<h1 className="text-2xl md:text-3xl font-semibold">Prompt Composer — AI‑Aware Music Creation</h1> |
|
|
</div> |
|
|
|
|
|
<Card className="shadow-sm"> |
|
|
<CardHeader> |
|
|
<CardTitle className="flex items-center gap-2"><Wand2 className="h-5 w-5"/> Compose from Prompt</CardTitle> |
|
|
<CardDescription>Describe your track. We’ll structure it for generation and platform algorithms.</CardDescription> |
|
|
</CardHeader> |
|
|
<CardContent className="space-y-4"> |
|
|
<div className="grid md:grid-cols-2 gap-4"> |
|
|
<div className="space-y-3"> |
|
|
<Label htmlFor="prompt">Music prompt</Label> |
|
|
<Textarea id="prompt" value={prompt} onChange={e => setPrompt(e.target.value)} className="min-h-[110px]" /> |
|
|
<div className="grid grid-cols-3 gap-3"> |
|
|
<div> |
|
|
<Label>Tempo (BPM)</Label> |
|
|
<div className="flex items-center gap-3"> |
|
|
<Slider value={[tempo]} min={70} max={200} step={1} onValueChange={(v) => setTempo(v[0])} /> |
|
|
<div className="w-10 text-right text-sm">{tempo}</div> |
|
|
</div> |
|
|
</div> |
|
|
<div> |
|
|
<Label>Target loudness (LUFS)</Label> |
|
|
<div className="flex items-center gap-3"> |
|
|
<Slider value={[lufs]} min={-20} max={-10} step={0.5} onValueChange={(v) => setLufs(v[0])} /> |
|
|
<div className="w-12 text-right text-sm">{lufs}</div> |
|
|
</div> |
|
|
</div> |
|
|
<div className="flex items-center gap-3 pt-6"> |
|
|
<Switch checked={autoMaster} onCheckedChange={setAutoMaster} id="am" /> |
|
|
<Label htmlFor="am">Auto‑master</Label> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
<div className="space-y-3"> |
|
|
<Label>Working title</Label> |
|
|
<Input value={title} onChange={e => setTitle(e.target.value)} /> |
|
|
<div className="grid grid-cols-2 gap-3"> |
|
|
<Stat label="Hook ETA" value="0:08" hint="Aim < 0:15" /> |
|
|
<Stat label="Energy curve" value="Rising → Chorus" /> |
|
|
<Stat label="Key (est.)" value="A minor" /> |
|
|
<Stat label="Mood" value="Upbeat • Confident" /> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</CardContent> |
|
|
<CardFooter className="flex items-center justify-between"> |
|
|
<Button size="sm" variant="secondary" className="gap-2" onClick={() => setPrompt(p => p + (p.endsWith(".") ? "" : ".") + " Short 8s intro; keep vocals upfront; chorus by 0:20.")}>Quick improve</Button> |
|
|
<Button className="gap-2" onClick={handleGenerate} disabled={generating}> |
|
|
<Sparkles className="h-4 w-4"/> |
|
|
{generating ? "Generating…" : "Generate mock"} |
|
|
</Button> |
|
|
</CardFooter> |
|
|
</Card> |
|
|
|
|
|
<div className="grid md:grid-cols-3 gap-4"> |
|
|
<Card> |
|
|
<CardHeader className="pb-2"><CardTitle className="flex items-center gap-2"><Gauge className="h-5 w-5"/> Algorithm Fit</CardTitle></CardHeader> |
|
|
<CardContent className="flex items-center justify-center"> |
|
|
<ScoreGauge score={score} /> |
|
|
</CardContent> |
|
|
<CardFooter className="text-xs text-muted-foreground">Based on tempo proximity, mastering target and intro/hook heuristics.</CardFooter> |
|
|
</Card> |
|
|
|
|
|
<Card> |
|
|
<CardHeader className="pb-2"><CardTitle className="flex items-center gap-2"><Settings2 className="h-5 w-5"/> Metadata Optimizer</CardTitle></CardHeader> |
|
|
<CardContent className="space-y-3 text-sm"> |
|
|
<div> |
|
|
<Label>SoundCloud</Label> |
|
|
<ul className="list-disc pl-5 mt-1 space-y-1"> |
|
|
<li>Genre: <b>house</b></li> |
|
|
<li>Tags: soulful, workout, vocal house, 115bpm</li> |
|
|
<li>Desc: “Hook by 0:08 • Bright mix • Save & share if it moves you.”</li> |
|
|
</ul> |
|
|
</div> |
|
|
<Separator /> |
|
|
<div> |
|
|
<Label>Spotify</Label> |
|
|
<ul className="list-disc pl-5 mt-1 space-y-1"> |
|
|
<li>Playlists fit: <b>Cardio</b>, <b>Dance Rising</b>, <b>mint Fresh</b></li> |
|
|
<li>Keywords: upbeat, energetic, vocal, gym</li> |
|
|
<li>Loudness target: −14 LUFS (true‑peak ≤ −1 dB)</li> |
|
|
</ul> |
|
|
</div> |
|
|
</CardContent> |
|
|
</Card> |
|
|
|
|
|
<Card> |
|
|
<CardHeader className="pb-2"><CardTitle className="flex items-center gap-2"><Music className="h-5 w-5"/> Prescriptive Advice</CardTitle></CardHeader> |
|
|
<CardContent> |
|
|
<ul className="list-disc pl-5 text-sm space-y-1"> |
|
|
{recs.map((r, i) => ( |
|
|
<li key={i}>{r}</li> |
|
|
))} |
|
|
</ul> |
|
|
</CardContent> |
|
|
</Card> |
|
|
</div> |
|
|
|
|
|
<Card> |
|
|
<CardHeader> |
|
|
<CardTitle className="flex items-center gap-2"><Share2 className="h-5 w-5"/> Export & Upload (Mock)</CardTitle> |
|
|
<CardDescription>Preview how you'd ship this to platforms. Buttons are disabled in the mock.</CardDescription> |
|
|
</CardHeader> |
|
|
<CardContent className="grid md:grid-cols-3 gap-4"> |
|
|
<Card className="border-dashed"> |
|
|
<CardHeader> |
|
|
<CardTitle className="text-base">Audio</CardTitle> |
|
|
<CardDescription>2:57 • 115 BPM • A min</CardDescription> |
|
|
</CardHeader> |
|
|
<CardContent> |
|
|
<div className="flex items-center gap-3"> |
|
|
<Button variant="secondary" size="icon" className="rounded-full" onClick={() => setPlaying(p=>!p)}> |
|
|
{playing ? <Pause className="h-4 w-4"/> : <Play className="h-4 w-4"/>} |
|
|
</Button> |
|
|
<div className="text-sm text-muted-foreground">(Mock preview)</div> |
|
|
</div> |
|
|
</CardContent> |
|
|
<CardFooter> |
|
|
<Button size="sm" variant="outline" disabled className="gap-2"><UploadCloud className="h-4 w-4"/> Upload WAV</Button> |
|
|
</CardFooter> |
|
|
</Card> |
|
|
|
|
|
<Card className="border-dashed"> |
|
|
<CardHeader> |
|
|
<CardTitle className="text-base">SoundCloud Package</CardTitle> |
|
|
</CardHeader> |
|
|
<CardContent className="text-sm space-y-2"> |
|
|
<div>Title: <b>{title}</b></div> |
|
|
<div>Genre: <b>house</b></div> |
|
|
<div>Tags: soulful; workout; vocal‑house; 115bpm</div> |
|
|
</CardContent> |
|
|
<CardFooter> |
|
|
<Button size="sm" variant="outline" disabled className="gap-2"><UploadCloud className="h-4 w-4"/> Upload to SC</Button> |
|
|
</CardFooter> |
|
|
</Card> |
|
|
|
|
|
<Card className="border-dashed"> |
|
|
<CardHeader> |
|
|
<CardTitle className="text-base">Spotify Package</CardTitle> |
|
|
</CardHeader> |
|
|
<CardContent className="text-sm space-y-2"> |
|
|
<div>Title: <b>{title}</b></div> |
|
|
<div>Playlist pitch: cardio • dance rising • gym anthems</div> |
|
|
<div>Master target: −14 LUFS / −1 dBTP</div> |
|
|
</CardContent> |
|
|
<CardFooter> |
|
|
<Button size="sm" variant="outline" disabled className="gap-2"><UploadCloud className="h-4 w-4"/> Upload to Spotify</Button> |
|
|
</CardFooter> |
|
|
</Card> |
|
|
</CardContent> |
|
|
</Card> |
|
|
|
|
|
<div className="text-xs text-muted-foreground text-center"> |
|
|
SIMULATED — No external calls. Replace with Suno/Udio + platform SDKs in a real build. |
|
|
</div> |
|
|
</div> |
|
|
); |
|
|
} |