Polish: draft autosave, history refresh, outdated footer, more

Bug fixes / UX

- Footer: the privacy line still claimed 'generation runs in your
  browser'. Now correctly says the generation runs on the server and
  the key never reaches the browser.

- History panel: when the panel was open while a new generation
  completed, the new entry didn't appear until the user closed and
  re-opened the panel. The history module now dispatches a custom
  'melodymuse-history-updated' event on every write; the panel
  listens and re-reads. removeFromHistory/clearHistory emit too.
  writeAll is also now try/catch'd against quota errors.

- Auto-save the input form: the input is persisted to localStorage
  ('melodymuse-draft', debounced 400ms) so a refresh doesn't wipe
  what the user was typing. The initial state hydrates from the
  draft on mount.

- Cmd/Ctrl+Enter now works in the Music style field as well as
  the idea field — shared onKeyDown handler, propagated through the
  StyleField sub-component.

- Full regen ('Generate' / 'Regenerate All') now clears the previous
  assets before kicking off the new request, so the skeleton state
  shows during the round-trip instead of stale cards with a spinner
  on top. Per-section regen still leaves the other cards intact.

- Settings → Local data: the in-progress draft is now listed
  alongside history and theme, and has its own 'Clear in-progress
  draft' button. The 'Clear all local data' button clears it too.
This commit is contained in:
2026-06-03 08:59:31 +02:00
parent 1f82825849
commit 3096669a13
6 changed files with 137 additions and 34 deletions
+2 -2
View File
@@ -7,8 +7,8 @@ export function Footer() {
Generate Suno song assets with AI
</p>
<p>
All generation runs in your browser your API key never leaves this
device.
Generation runs on the MelodyMuse server your API key never reaches
the browser.
</p>
</div>
</footer>
+20 -9
View File
@@ -1,13 +1,14 @@
import { useEffect, useState } from 'react';
import { Clock, History, Trash2, X } from 'lucide-react';
import { useEffect, useState } from "react";
import { Clock, History, Trash2, X } from "lucide-react";
import {
clearHistory,
formatRelative,
HISTORY_UPDATED_EVENT,
loadHistory,
removeFromHistory,
type HistoryEntry,
} from '../lib/history';
import type { InputValues } from '../lib/types';
} from "../lib/history";
import type { InputValues } from "../lib/types";
interface HistoryPanelProps {
onLoad: (entry: HistoryEntry) => void;
@@ -17,13 +18,23 @@ export function HistoryPanel({ onLoad }: HistoryPanelProps) {
const [open, setOpen] = useState(false);
const [entries, setEntries] = useState<HistoryEntry[]>([]);
// Refresh the list whenever the panel is opened, so newly-saved generations
// appear without needing a full page refresh.
// Refresh the list whenever the panel is opened.
useEffect(() => {
if (!open) return;
setEntries(loadHistory());
}, [open]);
// And whenever the history is updated from elsewhere in the app (after a
// successful generation, or a delete/clear from a different panel).
useEffect(() => {
const handler = () => {
if (!open) return;
setEntries(loadHistory());
};
window.addEventListener(HISTORY_UPDATED_EVENT, handler);
return () => window.removeEventListener(HISTORY_UPDATED_EVENT, handler);
}, [open]);
if (entries.length === 0 && !open) {
return null;
}
@@ -43,7 +54,7 @@ export function HistoryPanel({ onLoad }: HistoryPanelProps) {
<span className="text-xs text-fg-muted">({entries.length})</span>
)}
</span>
<span className="text-xs text-fg-muted">{open ? 'Hide' : 'Show'}</span>
<span className="text-xs text-fg-muted">{open ? "Hide" : "Show"}</span>
</button>
{open && (
@@ -130,6 +141,6 @@ function HistoryRow({
function summarizeInput(v: InputValues): string {
const idea = v.idea.trim();
const max = 90;
if (idea.length <= max) return idea || '(no idea text)';
return idea.slice(0, max - 1) + '…';
if (idea.length <= max) return idea || "(no idea text)";
return idea.slice(0, max - 1) + "…";
}
+11 -1
View File
@@ -228,6 +228,7 @@ export function InputPanel({
loading={styleLoading}
onChange={(v) => set("style_hint", v)}
onRandom={handleStyleRandom}
onCmdEnter={onKeyDown}
/>
<div>
@@ -324,7 +325,15 @@ interface StyleFieldProps {
onRandom: (mode: "normal" | "crazy") => void;
}
function StyleField({ value, loading, onChange, onRandom }: StyleFieldProps) {
function StyleField({
value,
loading,
onChange,
onRandom,
onCmdEnter,
}: StyleFieldProps & {
onCmdEnter: (e: KeyboardEvent<HTMLTextAreaElement>) => void;
}) {
return (
<div>
<label
@@ -340,6 +349,7 @@ function StyleField({ value, loading, onChange, onRandom }: StyleFieldProps) {
placeholder="e.g. dark synthwave, analog pads, 110 BPM"
value={value}
onChange={(e) => onChange(e.target.value)}
onKeyDown={onCmdEnter}
className="textarea text-sm"
/>
<div className="mt-2 grid grid-cols-2 gap-2">