Build standalone MelodyMuse SPA
Drop the Supabase backend and call the AI provider directly from the
browser. The endpoint URL, API key, and model name are stored in
localStorage and used for direct /chat/completions requests.
- src/lib/llm.ts: config persistence, direct fetch, JSON extraction,
shape validation, connection test
- src/lib/prompts.ts: full + partial-regeneration system prompts and
user-message builder
- src/lib/{api,supabase}.ts removed
- supabase/ directory removed
- @supabase/supabase-js dropped from package.json
- README updated to describe the standalone architecture and CORS caveats
- .gitignore: drop Supabase entries, exclude *.tsbuildinfo
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
import { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ChevronDown, ChevronUp, Loader2, Settings as SettingsIcon, Sparkles } from 'lucide-react';
|
||||
import { EqualizerIcon } from './EqualizerIcon';
|
||||
import type { Language, Vocals } from '../lib/types';
|
||||
|
||||
const LANGUAGES: { value: Language; label: string }[] = [
|
||||
{ value: 'English', label: 'English' },
|
||||
{ value: 'Deutsch', label: 'Deutsch' },
|
||||
{ value: 'Español', label: 'Español' },
|
||||
{ value: 'Français', label: 'Français' },
|
||||
{ value: 'Italiano', label: 'Italiano' },
|
||||
{ value: 'Português', label: 'Português' },
|
||||
{ value: 'Polski', label: 'Polski' },
|
||||
{ value: 'Other', label: 'Other' },
|
||||
];
|
||||
|
||||
export interface InputValues {
|
||||
idea: string;
|
||||
language: Language | string;
|
||||
customLanguage: string;
|
||||
mood: string;
|
||||
vocals: Vocals;
|
||||
}
|
||||
|
||||
interface InputPanelProps {
|
||||
values: InputValues;
|
||||
onChange: (next: InputValues) => void;
|
||||
onSubmit: () => void;
|
||||
loading: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function InputPanel({ values, onChange, onSubmit, loading, disabled }: InputPanelProps) {
|
||||
const [optionsOpen, setOptionsOpen] = useState(false);
|
||||
|
||||
const set = <K extends keyof InputValues>(key: K, value: InputValues[K]) =>
|
||||
onChange({ ...values, [key]: value });
|
||||
|
||||
const canSubmit = !loading && !disabled && values.idea.trim().length > 0;
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{/* Animated gradient backdrop behind the header */}
|
||||
<div className="absolute -top-10 -left-10 -right-10 h-64 animated-gradient-bg rounded-3xl -z-10 opacity-70 pointer-events-none" />
|
||||
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
<EqualizerIcon className="w-8 h-8 shrink-0" />
|
||||
<h1 className="text-3xl font-bold gradient-text leading-none">MelodyMuse</h1>
|
||||
</div>
|
||||
<p className="text-sm text-fg-muted mb-6 ml-11">
|
||||
Generate your Suno song assets with AI
|
||||
</p>
|
||||
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
if (canSubmit) onSubmit();
|
||||
}}
|
||||
className="space-y-4"
|
||||
>
|
||||
<div>
|
||||
<label htmlFor="idea" className="sr-only">
|
||||
Describe your music idea
|
||||
</label>
|
||||
<textarea
|
||||
id="idea"
|
||||
value={values.idea}
|
||||
onChange={(e) => set('idea', e.target.value)}
|
||||
rows={4}
|
||||
placeholder={`Describe your music idea...\ne.g. 'melancholic house beat for a rainy night'`}
|
||||
className="textarea text-base leading-relaxed"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOptionsOpen((v) => !v)}
|
||||
className="w-full flex items-center justify-between px-3 py-2 rounded-lg border border-border bg-bg-card hover:bg-bg-hover transition-colors text-sm"
|
||||
aria-expanded={optionsOpen}
|
||||
>
|
||||
<span className="font-medium text-fg">⚙ Options</span>
|
||||
{optionsOpen ? (
|
||||
<ChevronUp className="w-4 h-4 text-fg-muted" />
|
||||
) : (
|
||||
<ChevronDown className="w-4 h-4 text-fg-muted" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{optionsOpen && (
|
||||
<div className="space-y-4 p-4 rounded-lg border border-border bg-bg-card/50">
|
||||
<div>
|
||||
<label htmlFor="language" className="block text-xs uppercase tracking-wider text-fg-muted mb-1">
|
||||
Language
|
||||
</label>
|
||||
<select
|
||||
id="language"
|
||||
value={values.language}
|
||||
onChange={(e) => set('language', e.target.value as Language)}
|
||||
className="input"
|
||||
>
|
||||
{LANGUAGES.map((l) => (
|
||||
<option key={l.value} value={l.value}>
|
||||
{l.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{values.language === 'Other' && (
|
||||
<input
|
||||
type="text"
|
||||
className="input mt-2"
|
||||
placeholder="e.g. 日本語, Türkçe, Русский"
|
||||
value={values.customLanguage}
|
||||
onChange={(e) => set('customLanguage', e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="mood" className="block text-xs uppercase tracking-wider text-fg-muted mb-1">
|
||||
Mood
|
||||
</label>
|
||||
<input
|
||||
id="mood"
|
||||
type="text"
|
||||
placeholder="e.g. melancholic, euphoric, tense…"
|
||||
value={values.mood}
|
||||
onChange={(e) => set('mood', e.target.value)}
|
||||
className="input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className="block text-xs uppercase tracking-wider text-fg-muted mb-1">
|
||||
Vocals
|
||||
</span>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => set('vocals', 'vocals')}
|
||||
className={
|
||||
values.vocals === 'vocals'
|
||||
? 'px-3 py-2 rounded-lg font-medium bg-accent-primary text-white transition-colors'
|
||||
: 'chip justify-center py-2'
|
||||
}
|
||||
aria-pressed={values.vocals === 'vocals'}
|
||||
>
|
||||
🎤 Vocals
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => set('vocals', 'instrumental')}
|
||||
className={
|
||||
values.vocals === 'instrumental'
|
||||
? 'px-3 py-2 rounded-lg font-medium bg-accent-primary text-white transition-colors'
|
||||
: 'chip justify-center py-2'
|
||||
}
|
||||
aria-pressed={values.vocals === 'instrumental'}
|
||||
>
|
||||
🎹 Instrumental
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!canSubmit}
|
||||
className="w-full py-4 rounded-xl text-base font-semibold text-white shadow-lg shadow-violet-900/30 transition-all duration-150 disabled:opacity-50 disabled:cursor-not-allowed disabled:shadow-none flex items-center justify-center gap-2 gradient-bg hover:brightness-110 active:brightness-95"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
Generating…
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Sparkles className="w-5 h-5" />
|
||||
Generate Song Assets
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div className="text-center">
|
||||
<Link
|
||||
to="/settings"
|
||||
className="inline-flex items-center gap-1.5 text-xs text-fg-muted hover:text-fg transition-colors"
|
||||
>
|
||||
<SettingsIcon className="w-3.5 h-3.5" />
|
||||
Settings
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user