Optimize frontend rendering, CSS, and backend history caching

This commit is contained in:
2026-07-04 23:14:47 +02:00
parent b888d3d3fb
commit d157d34fca
5 changed files with 88 additions and 48 deletions
+7 -2
View File
@@ -82,17 +82,22 @@ console.log(`[startup] Listening on: http://0.0.0.0:${PORT}`);
// History helpers
// ──────────────────────────────────────────────────────────────────────────────
let historyCache = null;
async function readHistory() {
if (historyCache !== null) return historyCache;
try {
const raw = await readFile(HISTORY_FILE, 'utf-8');
const parsed = JSON.parse(raw);
return Array.isArray(parsed) ? parsed : [];
historyCache = Array.isArray(parsed) ? parsed : [];
} catch {
return [];
historyCache = [];
}
return historyCache;
}
async function writeHistory(entries) {
historyCache = entries;
await writeFile(HISTORY_FILE, JSON.stringify(entries, null, 2), 'utf-8');
}
+11 -7
View File
@@ -1,20 +1,24 @@
import { Suspense, lazy } from 'react';
import { Route, Routes } from 'react-router-dom';
import { HomePage } from './pages/HomePage';
import { SettingsPage } from './pages/SettingsPage';
import { ToastProvider } from './lib/toast';
import { I18nProvider } from './lib/i18n';
import { ThemeProvider } from './lib/theme';
const HomePage = lazy(() => import('./pages/HomePage').then(module => ({ default: module.HomePage })));
const SettingsPage = lazy(() => import('./pages/SettingsPage').then(module => ({ default: module.SettingsPage })));
export default function App() {
return (
<ThemeProvider>
<I18nProvider>
<ToastProvider>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/settings" element={<SettingsPage />} />
<Route path="*" element={<HomePage />} />
</Routes>
<Suspense fallback={<div className="h-screen w-screen flex items-center justify-center bg-bg"><div className="w-8 h-8 rounded-full border-4 border-accent-primary border-t-transparent animate-spin" /></div>}>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/settings" element={<SettingsPage />} />
<Route path="*" element={<HomePage />} />
</Routes>
</Suspense>
</ToastProvider>
</I18nProvider>
</ThemeProvider>
+35 -13
View File
@@ -1,4 +1,4 @@
import { useState, type FormEvent, type KeyboardEvent } from "react";
import { useState, useEffect, type FormEvent, type KeyboardEvent } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faCircleNotch,
@@ -51,7 +51,8 @@ const CRAZY_PAIRS: { idea: string; style: string; mood: string; vocals: Vocals }
];
interface InputPanelProps {
values: InputValues;
initialValues: InputValues;
forcedUpdate?: { count: number; values: InputValues };
onChange: (next: InputValues) => void;
onSubmit: () => void;
loading: boolean;
@@ -60,19 +61,32 @@ interface InputPanelProps {
}
export function InputPanel({
values,
initialValues,
forcedUpdate,
onChange,
onSubmit,
loading,
disabled,
cancelButton,
}: InputPanelProps) {
const [values, setValues] = useState<InputValues>(initialValues);
const [styleLoading, setStyleLoading] = useState<null | "normal" | "crazy">(null);
const elapsed = useElapsed(loading);
const { t } = useI18n();
const set = <K extends keyof InputValues>(key: K, value: InputValues[K]) =>
onChange({ ...values, [key]: value });
// Handle external updates (e.g. from loading history)
useEffect(() => {
if (forcedUpdate && forcedUpdate.count > 0) {
setValues(forcedUpdate.values);
onChange(forcedUpdate.values);
}
}, [forcedUpdate, onChange]);
const set = <K extends keyof InputValues>(key: K, value: InputValues[K]) => {
const next = { ...values, [key]: value };
setValues(next);
onChange(next);
};
const canSubmit = !loading && !disabled && values.idea.trim().length > 0;
@@ -99,22 +113,26 @@ export function InputPanel({
setStyleLoading("normal");
try {
const style = await randomStyle("normal", ctrl.signal, ideaToUse);
onChange({
const nextValues = {
...values,
idea: ideaToUse,
style_hint: style,
mood: hasIdea ? values.mood : ex.mood,
vocals: hasIdea ? values.vocals : ex.vocals
});
};
setValues(nextValues);
onChange(nextValues);
} catch (err) {
if (err instanceof DOMException && err.name === "AbortError") return;
onChange({
const nextValues = {
...values,
idea: ideaToUse,
style_hint: hasIdea ? values.style_hint : ex.style,
mood: hasIdea ? values.mood : ex.mood,
vocals: hasIdea ? values.vocals : ex.vocals
});
};
setValues(nextValues);
onChange(nextValues);
} finally {
setStyleLoading((curr) => (curr === "normal" ? null : curr));
}
@@ -131,22 +149,26 @@ export function InputPanel({
setStyleLoading("crazy");
try {
const style = await randomStyle("crazy", ctrl.signal, ideaToUse);
onChange({
const nextValues = {
...values,
idea: ideaToUse,
style_hint: style,
mood: hasIdea ? values.mood : ex.mood,
vocals: hasIdea ? values.vocals : ex.vocals
});
};
setValues(nextValues);
onChange(nextValues);
} catch (err) {
if (err instanceof DOMException && err.name === "AbortError") return;
onChange({
const nextValues = {
...values,
idea: ideaToUse,
style_hint: hasIdea ? values.style_hint : ex.style,
mood: hasIdea ? values.mood : ex.mood,
vocals: hasIdea ? values.vocals : ex.vocals
});
};
setValues(nextValues);
onChange(nextValues);
} finally {
setStyleLoading((curr) => (curr === "crazy" ? null : curr));
}
+3
View File
@@ -192,18 +192,21 @@
position: fixed; top: -20%; left: -15%; width: 70vw; height: 70vw;
background: radial-gradient(circle, var(--accent-primary) 0%, transparent 65%);
opacity: 0.18; filter: blur(130px); z-index: -1; pointer-events: none;
will-change: transform;
animation: blob-drift-1 18s ease-in-out infinite;
}
.liquid-blob-2 {
position: fixed; bottom: -20%; right: -15%; width: 65vw; height: 65vw;
background: radial-gradient(circle, var(--accent-secondary) 0%, transparent 65%);
opacity: 0.18; filter: blur(130px); z-index: -1; pointer-events: none;
will-change: transform;
animation: blob-drift-2 22s ease-in-out infinite;
}
.liquid-blob-3 {
position: fixed; top: 30%; right: 20%; width: 40vw; height: 40vw;
background: radial-gradient(circle, var(--accent-warm) 0%, transparent 65%);
opacity: 0.10; filter: blur(100px); z-index: -1; pointer-events: none;
will-change: transform;
animation: blob-drift-3 26s ease-in-out infinite;
}
+32 -26
View File
@@ -26,7 +26,6 @@ import { useI18n } from "../lib/i18n";
import { useTheme } from "../lib/theme";
const DRAFT_KEY = "melodymuse-draft";
const DRAFT_DEBOUNCE_MS = 400;
const DEFAULT_INPUT: InputValues = {
idea: "",
@@ -103,9 +102,8 @@ export function HomePage() {
const { t, lang, setLang } = useI18n();
const { theme, toggleTheme } = useTheme();
const [input, setInput] = useState<InputValues>(
() => loadDraft() ?? DEFAULT_INPUT,
);
const inputRef = useRef<InputValues>(loadDraft() ?? DEFAULT_INPUT);
const [forcedUpdate, setForcedUpdate] = useState<{ count: number; values: InputValues }>();
const [assets, setAssets] = useState<SongAssets | null>(null);
const [originalAssets, setOriginalAssets] = useState<SongAssets | null>(null);
const [loading, setLoading] = useState(false);
@@ -119,14 +117,14 @@ export function HomePage() {
const abortRef = useRef<AbortController | null>(null);
const anyRegenerating = isAnyBusy(loading, regenerating);
// Save draft
// Save draft periodically
useEffect(() => {
const id = window.setTimeout(() => {
try { window.localStorage.setItem(DRAFT_KEY, JSON.stringify(input)); }
const id = window.setInterval(() => {
try { window.localStorage.setItem(DRAFT_KEY, JSON.stringify(inputRef.current)); }
catch { /* ignore */ }
}, DRAFT_DEBOUNCE_MS);
return () => window.clearTimeout(id);
}, [input]);
}, 1000);
return () => window.clearInterval(id);
}, []);
// Server health check
useEffect(() => {
@@ -168,16 +166,19 @@ export function HomePage() {
}, [anyRegenerating, cancel]);
const buildRequest = useCallback(
(section: string, extra: Record<string, unknown> = {}): GenerateCall => ({
input: input.idea.trim(),
language: resolveLanguage(input),
...(input.mood.trim() ? { mood: input.mood.trim() } : {}),
...(input.style_hint.trim() ? { style_hint: input.style_hint.trim() } : {}),
vocals: input.vocals,
section,
...extra,
} as unknown as GenerateCall),
[input],
(section: string, extra: Record<string, unknown> = {}): GenerateCall => {
const input = inputRef.current;
return {
input: input.idea.trim(),
language: resolveLanguage(input),
...(input.mood.trim() ? { mood: input.mood.trim() } : {}),
...(input.style_hint.trim() ? { style_hint: input.style_hint.trim() } : {}),
vocals: input.vocals,
section,
...extra,
} as unknown as GenerateCall;
},
[],
);
const runGeneration = useCallback(
@@ -212,6 +213,7 @@ export function HomePage() {
);
const handleGenerate = useCallback(async () => {
const input = inputRef.current;
if (!input.idea.trim()) return;
setAssets(null);
setOriginalAssets(null);
@@ -228,9 +230,10 @@ export function HomePage() {
setLoading,
t.assetsGenerated,
);
}, [input, buildRequest, runGeneration, t]);
}, [buildRequest, runGeneration, t]);
const handleRegenerateAll = useCallback(async () => {
const input = inputRef.current;
if (!input.idea.trim()) return;
setAssets(null);
setOriginalAssets(null);
@@ -250,10 +253,11 @@ export function HomePage() {
},
t.regeneratedAll,
);
}, [input, buildRequest, runGeneration, t]);
}, [buildRequest, runGeneration, t]);
const handleRegenerateSection = useCallback(
async (section: SectionKey, selectedText?: string) => {
const input = inputRef.current;
if (!input.idea.trim() || !assets) return;
const isPartialLyrics = section === "lyrics" && !!selectedText;
@@ -299,7 +303,7 @@ export function HomePage() {
`${t.regeneratedSection} ${SECTION_LABELS[section]}`,
);
},
[input, assets, buildRequest, runGeneration, t],
[assets, buildRequest, runGeneration, t],
);
// Execute chained regeneration
@@ -345,7 +349,8 @@ export function HomePage() {
const handleLoadHistory = useCallback(
(entry: HistoryEntry) => {
if (anyRegenerating) cancel();
setInput(entry.input);
inputRef.current = entry.input;
setForcedUpdate((prev) => ({ count: (prev?.count ?? 0) + 1, values: entry.input }));
setAssets(entry.assets);
setOriginalAssets(entry.assets);
setSelectedTitleIndex(0);
@@ -486,8 +491,9 @@ export function HomePage() {
<div className="w-full md:w-[340px] xl:w-[380px] shrink-0 flex flex-col">
<div className="card p-4 flex flex-col flex-none md:flex-1 min-h-0">
<InputPanel
values={input}
onChange={setInput}
initialValues={inputRef.current}
forcedUpdate={forcedUpdate}
onChange={(next) => { inputRef.current = next; }}
onSubmit={handleGenerate}
loading={loading}
disabled={anyRegenerating}