UI enhancements, i18n, history drawer, and requested features

This commit is contained in:
2026-06-04 00:01:32 +02:00
parent c3e167bde9
commit 3a2c738bd0
14 changed files with 598 additions and 288 deletions
+27 -87
View File
@@ -12,35 +12,21 @@ import {
} from "@fortawesome/free-solid-svg-icons";
import { useToast } from "../lib/toast";
import { getServerStatus, type ServerStatus } from "../lib/llm";
import { clearHistory } from "../lib/history";
import { useI18n } from "../lib/i18n";
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, hasDraft: false, hasTheme: false };
}
let historyEntries = 0;
try {
const raw = window.localStorage.getItem(HISTORY_KEY);
if (raw) {
const parsed = JSON.parse(raw);
if (Array.isArray(parsed)) historyEntries = parsed.length;
}
} catch {
/* ignore */
return { hasDraft: false };
}
const hasDraft = window.localStorage.getItem(DRAFT_KEY) !== null;
const hasTheme = window.localStorage.getItem(THEME_KEY) !== null;
return { historyEntries, hasDraft, hasTheme };
return { hasDraft };
}
export function SettingsPage() {
@@ -52,9 +38,7 @@ export function SettingsPage() {
const [statusError, setStatusError] = useState<string | null>(null);
const [checking, setChecking] = useState(true);
const [summary, setSummary] = useState<LocalDataSummary>({
historyEntries: 0,
hasDraft: false,
hasTheme: false,
});
const check = async (signal?: AbortSignal) => {
@@ -64,7 +48,7 @@ export function SettingsPage() {
const s = await getServerStatus(signal);
setStatus(s);
} catch (err) {
setStatusError(err instanceof Error ? err.message : "Server unreachable");
setStatusError(err instanceof Error ? err.message : t.unreachable);
setStatus(null);
} finally {
setChecking(false);
@@ -78,40 +62,18 @@ export function SettingsPage() {
return () => ctrl.abort();
}, []);
const onClearHistory = () => {
if (summary.historyEntries === 0) return;
if (
!window.confirm(
`Delete all ${summary.historyEntries} recent generation${
summary.historyEntries === 1 ? "" : "s"
} from this browser? This cannot be undone.`,
)
)
return;
window.localStorage.removeItem(HISTORY_KEY);
setSummary((s) => ({ ...s, historyEntries: 0 }));
toast.info("Recent generations cleared");
const onClearHistory = async () => {
if (!window.confirm(t.clearHistoryConfirm)) return;
await clearHistory();
toast.info(t.clearHistory);
};
const onClearDraft = () => {
if (!summary.hasDraft) return;
if (!window.confirm(t.clearDraftConfirm)) 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, the in-progress draft, and the theme preference.",
)
)
return;
window.localStorage.removeItem(HISTORY_KEY);
window.localStorage.removeItem(DRAFT_KEY);
window.localStorage.removeItem(THEME_KEY);
setSummary({ historyEntries: 0, hasDraft: false, hasTheme: false });
toast.info("Local data cleared");
toast.info(t.clearDraft);
};
return (
@@ -135,7 +97,7 @@ export function SettingsPage() {
<section>
<h2 className="text-sm font-semibold text-fg mb-3 flex items-center gap-2">
<FontAwesomeIcon icon={faServer} className="w-4 h-4 text-accent-primary" />
Server status
{t.serverStatus}
</h2>
<div className="space-y-3">
@@ -145,23 +107,23 @@ export function SettingsPage() {
<div className="min-w-0">
<p className="text-sm font-medium text-fg">
{checking
? "Checking…"
? t.checking
: status
? "Connected"
? t.connected
: statusError
? "Unreachable"
? t.unreachable
: "Unknown"}
</p>
{status && (
<p className="text-xs text-fg-muted truncate">
Model: <span className="font-mono">{status.model}</span>
<p className="text-xs text-fg-muted truncate mt-1">
{t.model}: <span className="font-mono">{status.model}</span>
<br />
Endpoint:{" "}
{t.endpoint}:{" "}
<span className="font-mono">{status.endpoint}</span>
</p>
)}
{statusError && !checking && (
<p className="text-xs text-rose-400 break-words">
<p className="text-xs text-rose-400 break-words mt-1">
{statusError}
</p>
)}
@@ -174,11 +136,11 @@ export function SettingsPage() {
className="btn-ghost"
>
{checking ? (
<FontAwesomeIcon icon={faCircleNotch} spin className="w-4 h-4" />
<FontAwesomeIcon icon={faCircleNotch} spin className="w-4 h-4" />
) : (
<FontAwesomeIcon icon={faPlug} className="w-4 h-4" />
)}
<span>Re-check</span>
<span>{t.recheck}</span>
</button>
</div>
</div>
@@ -187,36 +149,14 @@ export function SettingsPage() {
<hr className="border-border" />
<section>
<h2 className="text-sm font-semibold text-fg mb-3">Local data</h2>
<p className="text-xs text-fg-muted mb-3">
MelodyMuse keeps some data in this browser. None of it leaves your
device except for the generation requests themselves.
</p>
<h2 className="text-sm font-semibold text-fg mb-3">{t.localData}</h2>
<ul className="text-xs text-fg-muted space-y-1 mb-4">
<li>
<code className="font-mono text-accent-primary">melodymuse-history</code> {" "}
{summary.historyEntries} recent generation
{summary.historyEntries === 1 ? "" : "s"}
</li>
<li>
<code className="font-mono text-accent-primary">melodymuse-draft</code> {" "}
{summary.hasDraft ? "saved (in-progress input)" : "empty"}
</li>
<li>
<code className="font-mono text-accent-primary">melodymuse-theme</code> {" "}
{summary.hasTheme ? "set" : "using default"}
{summary.hasDraft ? t.draftSaved : t.draftEmpty}
</li>
</ul>
<div className="flex flex-col gap-2">
<button
type="button"
onClick={onClearHistory}
disabled={summary.historyEntries === 0}
className="btn-secondary"
>
<FontAwesomeIcon icon={faTrashAlt} className="w-4 h-4" />
Clear recent generations
</button>
<button
type="button"
onClick={onClearDraft}
@@ -224,15 +164,15 @@ export function SettingsPage() {
className="btn-secondary"
>
<FontAwesomeIcon icon={faTrashAlt} className="w-4 h-4" />
Clear in-progress draft
{t.clearDraft}
</button>
<button
type="button"
onClick={onClearAll}
className="btn-secondary text-rose-400 hover:text-rose-300"
onClick={onClearHistory}
className="btn-secondary text-rose-400 hover:text-rose-300 border-rose-400/20"
>
<FontAwesomeIcon icon={faTrashAlt} className="w-4 h-4" />
Clear all local data
{t.clearHistory}
</button>
</div>
</section>
@@ -242,7 +182,7 @@ export function SettingsPage() {
to="/"
className="text-xs font-semibold text-fg-muted hover:text-accent-primary transition-colors flex items-center justify-center gap-2"
>
<FontAwesomeIcon icon={faArrowLeft} /> Back to generator
<FontAwesomeIcon icon={faArrowLeft} /> {t.backToGenerator}
</Link>
</div>
</div>