158 lines
5.9 KiB
TypeScript
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>
|
|
);
|
|
}
|