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:
@@ -13,21 +13,21 @@ import { ThemeToggle } from "../components/ThemeToggle";
|
||||
import { useToast } from "../lib/toast";
|
||||
import { getServerStatus, type ServerStatus } from "../lib/llm";
|
||||
|
||||
const STORAGE_KEY = "melodymuse-config";
|
||||
const HISTORY_KEY = "melodymuse-history";
|
||||
const DRAFT_KEY = "melodymuse-draft";
|
||||
const THEME_KEY = "melodymuse-theme";
|
||||
|
||||
interface LocalDataSummary {
|
||||
historyEntries: number;
|
||||
hasDraft: boolean;
|
||||
hasTheme: boolean;
|
||||
}
|
||||
|
||||
function readLocalDataSummary(): LocalDataSummary {
|
||||
if (typeof window === "undefined") {
|
||||
return { historyEntries: 0, hasTheme: false };
|
||||
return { historyEntries: 0, hasDraft: false, hasTheme: false };
|
||||
}
|
||||
let historyEntries = 0;
|
||||
let hasTheme = false;
|
||||
try {
|
||||
const raw = window.localStorage.getItem(HISTORY_KEY);
|
||||
if (raw) {
|
||||
@@ -37,8 +37,9 @@ function readLocalDataSummary(): LocalDataSummary {
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
hasTheme = window.localStorage.getItem(THEME_KEY) !== null;
|
||||
return { historyEntries, hasTheme };
|
||||
const hasDraft = window.localStorage.getItem(DRAFT_KEY) !== null;
|
||||
const hasTheme = window.localStorage.getItem(THEME_KEY) !== null;
|
||||
return { historyEntries, hasDraft, hasTheme };
|
||||
}
|
||||
|
||||
export function SettingsPage() {
|
||||
@@ -50,6 +51,7 @@ export function SettingsPage() {
|
||||
const [checking, setChecking] = useState(true);
|
||||
const [summary, setSummary] = useState<LocalDataSummary>({
|
||||
historyEntries: 0,
|
||||
hasDraft: false,
|
||||
hasTheme: false,
|
||||
});
|
||||
|
||||
@@ -89,17 +91,24 @@ export function SettingsPage() {
|
||||
toast.info("Recent generations cleared");
|
||||
};
|
||||
|
||||
const onClearDraft = () => {
|
||||
if (!summary.hasDraft) return;
|
||||
window.localStorage.removeItem(DRAFT_KEY);
|
||||
setSummary((s) => ({ ...s, hasDraft: false }));
|
||||
toast.info("In-progress draft cleared");
|
||||
};
|
||||
|
||||
const onClearAll = () => {
|
||||
if (
|
||||
!window.confirm(
|
||||
"Clear all MelodyMuse data from this browser? This will remove the recent generations and the theme preference. The API key on the server is NOT affected.",
|
||||
"Clear all MelodyMuse data from this browser? This will remove the recent generations, the in-progress draft, and the theme preference. The API key on the server is NOT affected.",
|
||||
)
|
||||
)
|
||||
return;
|
||||
window.localStorage.removeItem(STORAGE_KEY);
|
||||
window.localStorage.removeItem(HISTORY_KEY);
|
||||
window.localStorage.removeItem(DRAFT_KEY);
|
||||
window.localStorage.removeItem(THEME_KEY);
|
||||
setSummary({ historyEntries: 0, hasTheme: false });
|
||||
setSummary({ historyEntries: 0, hasDraft: false, hasTheme: false });
|
||||
toast.info("Local data cleared");
|
||||
};
|
||||
|
||||
@@ -196,6 +205,10 @@ export function SettingsPage() {
|
||||
{summary.historyEntries} recent generation
|
||||
{summary.historyEntries === 1 ? "" : "s"}
|
||||
</li>
|
||||
<li>
|
||||
<code className="font-mono">melodymuse-draft</code> —{" "}
|
||||
{summary.hasDraft ? "saved (in-progress input)" : "empty"}
|
||||
</li>
|
||||
<li>
|
||||
<code className="font-mono">melodymuse-theme</code> —{" "}
|
||||
{summary.hasTheme ? "set" : "using default"}
|
||||
@@ -211,6 +224,15 @@ export function SettingsPage() {
|
||||
<Trash2 className="w-4 h-4" />
|
||||
Clear recent generations
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClearDraft}
|
||||
disabled={!summary.hasDraft}
|
||||
className="btn-secondary"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
Clear in-progress draft
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClearAll}
|
||||
|
||||
Reference in New Issue
Block a user