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
+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) + "…";
}