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,109 @@
|
||||
import { RefreshCw } from "lucide-react";
|
||||
import type { SongAssets } from "../lib/types";
|
||||
import { SkeletonCard } from "./SkeletonCard";
|
||||
import { TitlesCard } from "./cards/TitlesCard";
|
||||
import { LyricsCard } from "./cards/LyricsCard";
|
||||
import { StyleCard } from "./cards/StyleCard";
|
||||
import { VideoPromptsCard } from "./cards/VideoPromptsCard";
|
||||
import { YouTubeCard } from "./cards/YouTubeCard";
|
||||
|
||||
export type SectionKey =
|
||||
| "titles"
|
||||
| "lyrics"
|
||||
| "style"
|
||||
| "video_prompts"
|
||||
| "youtube_description";
|
||||
|
||||
export type RegeneratingMap = Partial<Record<SectionKey | "all", boolean>>;
|
||||
|
||||
interface ResultsPanelProps {
|
||||
assets: SongAssets | null;
|
||||
loading: boolean; // true during the initial "Generate all" call
|
||||
selectedTitleIndex: number;
|
||||
onSelectTitle: (i: number) => void;
|
||||
onChange: <K extends keyof SongAssets>(key: K, value: SongAssets[K]) => void;
|
||||
onChangeVideoPrompt: (index: number, value: string) => void;
|
||||
onRegenerateAll: () => void;
|
||||
onRegenerateSection: (section: SectionKey) => void;
|
||||
regenerating: RegeneratingMap;
|
||||
}
|
||||
|
||||
export function ResultsPanel({
|
||||
assets,
|
||||
loading,
|
||||
selectedTitleIndex,
|
||||
onSelectTitle,
|
||||
onChange,
|
||||
onChangeVideoPrompt,
|
||||
onRegenerateAll,
|
||||
onRegenerateSection,
|
||||
regenerating,
|
||||
}: ResultsPanelProps) {
|
||||
const showSkeletons = loading && !assets;
|
||||
const regenAll = Boolean(regenerating.all);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="sticky top-0 z-30 -mx-4 sm:-mx-6 lg:-mx-8 px-4 sm:px-6 lg:px-8 py-3 bg-bg/80 backdrop-blur border-b border-border mb-6 flex items-center justify-between gap-3">
|
||||
<h2 className="text-lg font-semibold text-fg">Results</h2>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRegenerateAll}
|
||||
disabled={loading || regenAll}
|
||||
className="btn-ghost"
|
||||
>
|
||||
<RefreshCw
|
||||
className={`w-4 h-4 ${regenAll || loading ? "animate-spin" : ""}`}
|
||||
/>
|
||||
<span>Regenerate All</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showSkeletons ? (
|
||||
<div className="space-y-4">
|
||||
<SkeletonCard height="h-16" rows={1} />
|
||||
<SkeletonCard height="h-40" rows={4} />
|
||||
<SkeletonCard height="h-24" rows={3} />
|
||||
<SkeletonCard height="h-32" rows={4} />
|
||||
<SkeletonCard height="h-56" rows={3} />
|
||||
</div>
|
||||
) : assets ? (
|
||||
<div className="space-y-4">
|
||||
<TitlesCard
|
||||
titles={assets.titles}
|
||||
selectedIndex={selectedTitleIndex}
|
||||
onSelect={onSelectTitle}
|
||||
onRegenerate={() => onRegenerateSection("titles")}
|
||||
regenerating={Boolean(regenerating.titles)}
|
||||
/>
|
||||
<LyricsCard
|
||||
lyrics={assets.lyrics}
|
||||
onChange={(v) => onChange("lyrics", v)}
|
||||
onRegenerate={() => onRegenerateSection("lyrics")}
|
||||
regenerating={Boolean(regenerating.lyrics)}
|
||||
/>
|
||||
<StyleCard
|
||||
style={assets.style}
|
||||
negativeStyle={assets.negative_style}
|
||||
onStyleChange={(v) => onChange("style", v)}
|
||||
onNegativeChange={(v) => onChange("negative_style", v)}
|
||||
onRegenerate={() => onRegenerateSection("style")}
|
||||
regenerating={Boolean(regenerating.style)}
|
||||
/>
|
||||
<VideoPromptsCard
|
||||
prompts={assets.video_prompts}
|
||||
onChange={onChangeVideoPrompt}
|
||||
onRegenerate={() => onRegenerateSection("video_prompts")}
|
||||
regenerating={Boolean(regenerating.video_prompts)}
|
||||
/>
|
||||
<YouTubeCard
|
||||
description={assets.youtube_description}
|
||||
onChange={(v) => onChange("youtube_description", v)}
|
||||
onRegenerate={() => onRegenerateSection("youtube_description")}
|
||||
regenerating={Boolean(regenerating.youtube_description)}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user