Files
MelodyMuse/src/components/ResultsPanel.tsx
T

158 lines
5.9 KiB
TypeScript

import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faMusic, faSync } from "@fortawesome/free-solid-svg-icons";
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;
originalAssets: SongAssets | null;
loading: boolean;
selectedTitleIndex: number;
onSelectTitle: (i: number) => void;
onTitleEdit?: (customTitle: string) => void;
onChange: <K extends keyof SongAssets>(key: K, value: SongAssets[K]) => void;
onChangeVideoPrompt: (index: number, value: string) => void;
onRegenerateAll: () => void;
onRegenerateSection: (section: SectionKey) => void;
onCancel?: () => void;
anyRegenerating: boolean;
regenerating: RegeneratingMap;
}
export function ResultsPanel({
assets,
originalAssets,
loading,
selectedTitleIndex,
onSelectTitle,
onTitleEdit,
onChange,
onChangeVideoPrompt,
onRegenerateAll,
onRegenerateSection,
onCancel,
anyRegenerating,
regenerating,
}: ResultsPanelProps) {
const showSkeletons = loading && !assets;
return (
<div>
<div className="sticky top-0 z-30 -mx-4 px-4 py-2.5 bg-bg/60 backdrop-blur-md border-b border-border mb-5 flex items-center justify-between gap-3">
<h2 className="text-sm font-bold text-fg">Results</h2>
<div className="flex items-center gap-1">
{anyRegenerating && onCancel && (
<button
type="button"
onClick={onCancel}
className="px-2.5 py-1.5 rounded-lg text-xs font-semibold text-fg-muted hover:text-fg hover:bg-bg-hover transition-colors"
>
Cancel
</button>
)}
{assets && (
<button
type="button"
onClick={onRegenerateAll}
disabled={anyRegenerating}
className="inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs font-semibold text-fg-muted hover:text-fg hover:bg-bg-hover transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<FontAwesomeIcon icon={faSync} spin={regenerating.all} className="w-3 h-3" />
Regenerate all
</button>
)}
</div>
</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 && originalAssets ? (
<div className="space-y-4 pb-4">
<TitlesCard
titles={assets.titles}
selectedIndex={selectedTitleIndex}
onSelect={onSelectTitle}
onTitleEdit={onTitleEdit}
onRegenerate={() => onRegenerateSection("titles")}
regenerating={Boolean(regenerating.titles)}
/>
<LyricsCard
lyrics={assets.lyrics}
originalLyrics={originalAssets.lyrics}
onChange={(v) => onChange("lyrics", v)}
onRegenerate={() => onRegenerateSection("lyrics")}
regenerating={Boolean(regenerating.lyrics)}
/>
<StyleCard
style={assets.style}
negativeStyle={assets.negative_style}
originalStyle={originalAssets.style}
originalNegativeStyle={originalAssets.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}
originalPrompts={originalAssets.video_prompts}
onChange={onChangeVideoPrompt}
onRegenerate={() => onRegenerateSection("video_prompts")}
regenerating={Boolean(regenerating.video_prompts)}
/>
<YouTubeCard
description={assets.youtube_description}
originalDescription={originalAssets.youtube_description}
onChange={(v) => onChange("youtube_description", v)}
onRegenerate={() => onRegenerateSection("youtube_description")}
regenerating={Boolean(regenerating.youtube_description)}
/>
</div>
) : (
<EmptyState />
)}
</div>
);
}
function EmptyState() {
return (
<div className="flex flex-col items-center justify-center text-center px-6 py-20 rounded-2xl border border-dashed border-border backdrop-blur-sm bg-bg-card/30">
<div className="relative w-16 h-16 mb-5">
<div className="absolute inset-0 rounded-full bg-gradient-to-br from-accent-primary/20 to-accent-warm/20 blur-xl" />
<div className="relative w-full h-full rounded-full bg-bg-card border border-border flex items-center justify-center shadow-lg">
<FontAwesomeIcon icon={faMusic} className="w-7 h-7 text-accent-primary" />
</div>
</div>
<h3 className="text-base font-bold text-fg mb-1.5">Your song will appear here</h3>
<p className="text-sm text-fg-muted max-w-sm leading-relaxed">
Describe a music idea on the left and press{" "}
<kbd className="px-1.5 py-0.5 rounded-lg bg-bg-hover border border-border text-[11px] font-mono text-fg">
Generate
</kbd>{" "}
to create titles, lyrics, style, video prompts, and a YouTube description.
</p>
</div>
);
}