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:
2026-06-03 01:25:22 +02:00
parent f2401d0c57
commit d42410560f
37 changed files with 5443 additions and 376 deletions
+198
View File
@@ -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>
);
}