125 lines
4.0 KiB
TypeScript
125 lines
4.0 KiB
TypeScript
import { useState } from "react";
|
|
import { ResultCard } from "../ResultCard";
|
|
import { CopyButton } from "../CopyButton";
|
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
import { faVideo, faUndo } from "@fortawesome/free-solid-svg-icons";
|
|
import { useAutoHeight } from "../../lib/useAutoHeight";
|
|
import {
|
|
NEGATIVE_VIDEO_PROMPT,
|
|
type VideoPrompt,
|
|
type VideoPromptType,
|
|
} from "../../lib/types";
|
|
|
|
interface VideoPromptsCardProps {
|
|
prompts: VideoPrompt[];
|
|
originalPrompts: VideoPrompt[];
|
|
onChange: (index: number, next: string) => void;
|
|
onRegenerate: () => void;
|
|
regenerating: boolean;
|
|
}
|
|
|
|
const TABS: VideoPromptType[] = ["Abstract", "Cinematic", "Hybrid"];
|
|
|
|
export function VideoPromptsCard({
|
|
prompts,
|
|
originalPrompts,
|
|
onChange,
|
|
onRegenerate,
|
|
regenerating,
|
|
}: VideoPromptsCardProps) {
|
|
const [tab, setTab] = useState<VideoPromptType>("Abstract");
|
|
const idx = prompts.findIndex((p) => p.type === tab);
|
|
const current = idx >= 0 ? prompts[idx] : undefined;
|
|
const originalCurrent = originalPrompts.find((p) => p.type === tab);
|
|
const dirty = current?.prompt !== originalCurrent?.prompt;
|
|
|
|
const ref = useAutoHeight(current?.prompt ?? "", 120);
|
|
|
|
return (
|
|
<ResultCard
|
|
index={3}
|
|
icon={<FontAwesomeIcon icon={faVideo} className="text-rose-400" />}
|
|
title="Video Prompts"
|
|
onRegenerate={onRegenerate}
|
|
regenerating={regenerating}
|
|
headerExtra={
|
|
current ? (
|
|
<>
|
|
{dirty && originalCurrent && (
|
|
<button
|
|
type="button"
|
|
onClick={() => onChange(idx, originalCurrent.prompt)}
|
|
className="btn-ghost"
|
|
aria-label="Revert video prompt to last generated version"
|
|
title="Revert to last generated"
|
|
>
|
|
<FontAwesomeIcon icon={faUndo} className="w-3.5 h-3.5" />
|
|
<span>Revert</span>
|
|
</button>
|
|
)}
|
|
<CopyButton value={current.prompt} label="Copy" />
|
|
</>
|
|
) : null
|
|
}
|
|
>
|
|
<div className="flex items-center gap-1 mb-4 p-1 rounded-lg bg-bg-hover/40 border border-border w-fit">
|
|
{TABS.map((t) => {
|
|
const isActive = t === tab;
|
|
return (
|
|
<button
|
|
key={t}
|
|
type="button"
|
|
onClick={() => setTab(t)}
|
|
className={
|
|
isActive
|
|
? "px-3 py-1.5 rounded-md text-sm font-medium bg-accent-primary text-white transition-colors"
|
|
: "px-3 py-1.5 rounded-md text-sm font-medium text-fg-muted hover:text-fg hover:bg-bg-hover transition-colors"
|
|
}
|
|
role="tab"
|
|
aria-selected={isActive}
|
|
>
|
|
{t}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{current ? (
|
|
<div className="space-y-3">
|
|
<div>
|
|
<div className="flex items-center justify-between mb-2">
|
|
<label className="text-sm font-medium text-fg">Main prompt</label>
|
|
</div>
|
|
<textarea
|
|
ref={ref}
|
|
value={current.prompt}
|
|
onChange={(e) => onChange(idx, e.target.value)}
|
|
rows={5}
|
|
className="textarea text-sm leading-relaxed scrollbar-thin"
|
|
style={{ minHeight: 120 }}
|
|
/>
|
|
</div>
|
|
|
|
<p className="text-sm italic text-fg-muted">
|
|
→ Best for:{" "}
|
|
<span className="not-italic text-fg">
|
|
{current.tool_recommendation}
|
|
</span>
|
|
</p>
|
|
|
|
<div className="mt-1 p-3 rounded-lg bg-bg-hover/40 border border-border">
|
|
<p className="text-[11px] uppercase tracking-wider text-fg-muted mb-1">
|
|
Negative
|
|
</p>
|
|
<p className="text-xs text-fg-muted leading-relaxed">
|
|
{NEGATIVE_VIDEO_PROMPT}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<p className="text-sm text-fg-muted">No prompt for this tab yet.</p>
|
|
)}
|
|
</ResultCard>
|
|
);
|
|
}
|