UI enhancements, i18n, history drawer, and requested features
This commit is contained in:
+27
-87
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user