Add cancel, history, revert, and quality-of-life features
UX - Add elapsed-time counter and Cancel button to in-flight generations, wired through AbortController so partial responses are discarded cleanly. The cancel action is available from both the input panel and the results header. - Add a Recent generations panel below the input. The last 6 successful generations are saved to localStorage; one click reloads both the input fields and the generated assets. - Add a Revert button to every editable card. It appears the moment the current value diverges from the last generated value and restores the field with a single click. - Add an Empty state to the right panel with a friendly hint pointing at the Generate button and the Settings page. - Add a 'Try an example' button that fills the input with a random starter idea (idea + mood + vocals). - Add Cmd/Ctrl+Enter as a keyboard shortcut to generate. - Add a Footer with project info and a privacy reminder. - Add a 'Clear saved key' button to the Settings page so the user can remove the API key without overwriting it. Bug fix - Settings > Test Connection used to save the in-progress form values to localStorage before testing. It now uses the in-memory candidate config, so failed tests don't pollute the saved config. Code quality - Extract the duplicated useAutoHeight hook to src/lib/useAutoHeight.ts. - Extract a useElapsed hook for the loading timer. - Move InputValues into src/lib/types.ts (was duplicated in InputPanel.tsx) and add SECTION_LABELS, replacing the humanizeSection switch in HomePage. - Centralize the filename sanitization: HomePage now calls sanitizeFilename from zip.ts instead of duplicating the regex. - Add previewConfig / testConnectionWithConfig helpers to llm.ts to support in-memory connection tests.
This commit is contained in:
@@ -1,34 +1,38 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { ResultCard } from '../ResultCard';
|
||||
import { CopyButton } from '../CopyButton';
|
||||
import { NEGATIVE_VIDEO_PROMPT, type VideoPrompt, type VideoPromptType } from '../../lib/types';
|
||||
import { useState } from "react";
|
||||
import { RotateCcw } from "lucide-react";
|
||||
import { ResultCard } from "../ResultCard";
|
||||
import { CopyButton } from "../CopyButton";
|
||||
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'];
|
||||
const TABS: VideoPromptType[] = ["Abstract", "Cinematic", "Hybrid"];
|
||||
|
||||
export function VideoPromptsCard({
|
||||
prompts,
|
||||
originalPrompts,
|
||||
onChange,
|
||||
onRegenerate,
|
||||
regenerating,
|
||||
}: VideoPromptsCardProps) {
|
||||
const [tab, setTab] = useState<VideoPromptType>('Abstract');
|
||||
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 = useRef<HTMLTextAreaElement | null>(null);
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el || !current) return;
|
||||
el.style.height = 'auto';
|
||||
el.style.height = `${Math.max(120, el.scrollHeight)}px`;
|
||||
}, [current?.prompt, tab]);
|
||||
const ref = useAutoHeight(current?.prompt ?? "", 120);
|
||||
|
||||
return (
|
||||
<ResultCard
|
||||
@@ -37,7 +41,25 @@ export function VideoPromptsCard({
|
||||
title="Video Prompts"
|
||||
onRegenerate={onRegenerate}
|
||||
regenerating={regenerating}
|
||||
headerExtra={current ? <CopyButton value={current.prompt} label="Copy" /> : null}
|
||||
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"
|
||||
>
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
<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) => {
|
||||
@@ -49,8 +71,8 @@ export function VideoPromptsCard({
|
||||
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'
|
||||
? "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}
|
||||
@@ -78,12 +100,19 @@ export function VideoPromptsCard({
|
||||
</div>
|
||||
|
||||
<p className="text-sm italic text-fg-muted">
|
||||
→ Best for: <span className="not-italic text-fg">{current.tool_recommendation}</span>
|
||||
→ 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>
|
||||
<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>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user