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:
2026-06-03 01:47:11 +02:00
parent d42410560f
commit b945417773
16 changed files with 991 additions and 282 deletions
+38 -25
View File
@@ -1,27 +1,27 @@
// Domain types shared across the app. These mirror the JSON shape produced by
// the `generate-song` edge function.
// the LLM module.
export type Language =
| 'English'
| 'Deutsch'
| 'Español'
| 'Français'
| 'Italiano'
| 'Português'
| 'Polski'
| 'Other';
| "English"
| "Deutsch"
| "Español"
| "Français"
| "Italiano"
| "Português"
| "Polski"
| "Other";
export type Vocals = 'vocals' | 'instrumental';
export type Vocals = "vocals" | "instrumental";
export type GenerationSection =
| 'all'
| 'lyrics'
| 'style'
| 'titles'
| 'video_prompts'
| 'youtube_description';
| "all"
| "lyrics"
| "style"
| "titles"
| "video_prompts"
| "youtube_description";
export type VideoPromptType = 'Abstract' | 'Cinematic' | 'Hybrid';
export type VideoPromptType = "Abstract" | "Cinematic" | "Hybrid";
export interface VideoPrompt {
type: VideoPromptType;
@@ -40,7 +40,7 @@ export interface SongAssets {
export interface GenerateRequest {
input: string;
language: string; // 'English' or free-text from "Other"
language: string; // 'English' or free-text from "Other"
mood?: string;
vocals: Vocals;
section: GenerationSection;
@@ -53,16 +53,29 @@ export interface ConfigDisplay {
api_key_set: boolean;
}
export interface SaveConfigPayload {
api_endpoint: string;
api_key: string; // empty string => keep existing
model_name: string;
}
export interface TestConnectionResult {
success: boolean;
message: string;
}
export interface InputValues {
idea: string;
language: Language | string;
customLanguage: string;
mood: string;
vocals: Vocals;
}
export const NEGATIVE_VIDEO_PROMPT =
'text, watermark, logo, flash, abrupt cuts, camera shake, faces, hands, distortion, blur';
"text, watermark, logo, flash, abrupt cuts, camera shake, faces, hands, distortion, blur";
// Human-readable labels for each regenerable section — used by toasts and the
// history panel. Keep in sync with `GenerationSection`.
export const SECTION_LABELS: Record<GenerationSection, string> = {
all: "everything",
titles: "titles",
lyrics: "lyrics",
style: "style",
video_prompts: "video prompts",
youtube_description: "description",
};