From 81aefe07009fd62f2fbf757391ce48454ce104d8 Mon Sep 17 00:00:00 2001 From: orfelorfel23 Date: Wed, 3 Jun 2026 10:49:58 +0200 Subject: [PATCH] Modernize the UI and fix the equalizer sizing bug MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug fix - EqualizerIcon: the SVG was rendering at the wrong size (filling its parent) because it had no explicit width/height attributes. Now it accepts a size prop with a 24px default and the SVG element carries the size as width/height attributes, with the className on top. Can't accidentally balloon. Design - Stripped the light-mode CSS — the app is dark-only now, and the theme toggle was a no-op (clicking it just toggled a class that had no matching rules). ThemeToggle.tsx and lib/theme.ts are gone; initTheme() call removed from main.tsx. - Reworked the input form: • brand row is now small + refined (icon + wordmark, no huge gradient title) • Options is a single subtle collapsible card with a chevron and a count badge showing how many options are non-default • Vocals is now a segmented pill (single rounded container, two flat segments) instead of two full-width grid buttons • Generate button is slightly smaller (py-3.5, text-sm) and uses a softer shadow (shadow-violet-900/20) - Empty state in the results panel: dashed-border card, soft gradient blur behind the music icon, cleaner copy. - Results header: smaller heading, the 'Regenerate All' and 'Cancel' buttons are now tiny ghost-style pills in the top-right. - Sticky ZIP bar: softer shadow, more refined copy. - Result cards: smaller icon, single-line title, the Regenerate button is now an inline pill that fits next to Revert + Copy. - Home page header now has a tiny equalizer icon + 'MelodyMuse' wordmark on the left (was just text). Settings page header has the back arrow on the left and the title, no theme toggle. - Global CSS: added modern focus-visible ring, custom selection color, subtle scrollbar thumb, slightly tweaked the gradient text to a softer violet-300 → cyan-300 instead of the loud violet-400 → fuchsia-400 → cyan-400. --- .env.example | 98 +++++++++++-- src/components/EqualizerIcon.tsx | 81 ++++++++-- src/components/InputPanel.tsx | 245 ++++++++++++++++--------------- src/components/ResultCard.tsx | 32 ++-- src/components/ResultsPanel.tsx | 47 +++--- src/components/StickyZipBar.tsx | 23 +-- src/components/ThemeToggle.tsx | 28 ---- src/index.css | 215 +++++++++++++-------------- src/lib/theme.ts | 36 ----- src/main.tsx | 16 +- src/pages/HomePage.tsx | 12 +- src/pages/SettingsPage.tsx | 6 +- 12 files changed, 460 insertions(+), 379 deletions(-) delete mode 100644 src/components/ThemeToggle.tsx delete mode 100644 src/lib/theme.ts diff --git a/.env.example b/.env.example index 8950b5f..c646f26 100644 --- a/.env.example +++ b/.env.example @@ -1,34 +1,106 @@ # ───────────────────────────────────────────────────────────────────────────── # MelodyMuse — server environment # -# The Node server (`server.mjs`) reads these from `process.env`. Pass them in -# however you run the server: +# The Node server (`server.mjs`) reads these from `process.env`. It refuses to +# start without `LLM_ENDPOINT` and `LLM_API_KEY`. Pass them in however you run +# the server: # # • Bare-metal: LLM_ENDPOINT=… LLM_API_KEY=… node server.mjs # • Docker: -e LLM_ENDPOINT=… -e LLM_API_KEY=… # • docker-compose: put them in a `.env` file next to compose.yml # • Jannik-Cloud: AGE-encrypted `.env` generated by `generate-env.sh` # -# `LLM_ENDPOINT` and `LLM_API_KEY` are required — the server refuses to start -# without them. The rest have sensible defaults. +# Three things you set: +# +# LLM_ENDPOINT — base URL of an OpenAI-compatible /chat/completions route +# LLM_API_KEY — provider secret (never sent to the browser) +# LLM_MODEL — model name to send in each request +# +# Below the active config you'll find worked examples for several providers — +# copy one of them into the active block above to switch. # ───────────────────────────────────────────────────────────────────────────── -# OpenAI-compatible base URL, e.g. https://api.minimax.chat/v1 -LLM_ENDPOINT=https://api.minimax.chat/v1 +# ─── Active provider (uncomment ONE of the examples below to switch) ────────── -# Your provider's secret key +LLM_ENDPOINT=https://api.minimax.io/v1 LLM_API_KEY= - -# Model name (default: MiniMax-M3) LLM_MODEL=MiniMax-M3 -# Listen port (default: 3000) -PORT=3000 +# ─── Server config ──────────────────────────────────────────────────────────── -# CORS origin. "*" allows any origin (fine for a single-user personal app). -# Set to a specific origin like "https://melodymuse.orfel.de" to lock it down. +PORT=3000 CORS_ORIGIN=* + +# ╔═══════════════════════════════════════════════════════════════════════════╗ +# ║ PROVIDER EXAMPLES — copy one block up to switch providers ║ +# ╚═══════════════════════════════════════════════════════════════════════════╝ + +# ─── MiniMax (the default) ─────────────────────────────────────────────────── +# https://api.minimax.io/v1 +# Sign in at https://MiniMax.io to get a key. +# +# LLM_ENDPOINT=https://api.minimax.io/v1 +# LLM_API_KEY=eyXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +# LLM_MODEL=MiniMax-M3 + + +# ─── OpenAI ────────────────────────────────────────────────────────────────── +# https://platform.openai.com/ +# Keys look like: sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +# Common models: gpt-4o, gpt-4o-mini, gpt-4-turbo, o1-mini, o1, … +# +# LLM_ENDPOINT=https://api.openai.com/v1 +# LLM_API_KEY=sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +# LLM_MODEL=gpt-4o-mini + + +# ─── OpenRouter (Claude, Llama, Mistral, Gemini, …) ──────────────────────── +# https://openrouter.ai/ +# One OpenAI-compatible endpoint that fronts hundreds of models. Easiest way +# to use Anthropic Claude, Meta Llama, Google Gemini, etc. without writing a +# custom adapter. +# +# Keys look like: sk-or-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +# Model names are namespaced: anthropic/claude-3.5-sonnet, openai/gpt-4o, +# meta-llama/llama-3.1-405b-instruct, google/gemini-2.0-flash-exp, … +# +# LLM_ENDPOINT=https://openrouter.ai/api/v1 +# LLM_API_KEY=sk-or-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +# LLM_MODEL=anthropic/claude-3.5-sonnet + + +# ─── Poe ───────────────────────────────────────────────────────────────────── +# https://poe.com/ +# Poe exposes an OpenAI-compatible API for the bots you've created in their +# UI. Create a bot there first (free), then use its exact name as the model. +# +# LLM_ENDPOINT=https://api.poe.com/v1 +# LLM_API_KEY=sk-poe-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +# LLM_MODEL=Claude-3.5-Sonnet + + +# ─── Anthropic Claude (DIRECT — needs a server adapter) ───────────────────── +# https://console.anthropic.com/ +# Anthropic's native API is NOT OpenAI-compatible — it lives at +# POST /v1/messages with a different body shape (system message is a +# top-level field, not a message) and a different response shape. +# +# MelodyMuse's server does not currently talk to Anthropic's native API +# directly. Two options: +# +# 1. Use OpenRouter above — same model, OpenAI-compatible, zero code +# changes. +# 2. Add a small adapter to server.mjs that translates the request +# to Anthropic's Messages API format. Roughly 30 lines of code. +# +# If you do add the adapter, the endpoint would be: +# +# LLM_ENDPOINT=https://api.anthropic.com +# LLM_API_KEY=sk-ant-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +# LLM_MODEL=claude-3-5-sonnet-latest + + # ───────────────────────────────────────────────────────────────────────────── # Vite dev environment (only used by `npm run dev`, not by the server) # ───────────────────────────────────────────────────────────────────────────── diff --git a/src/components/EqualizerIcon.tsx b/src/components/EqualizerIcon.tsx index 3c2bcfe..aa87434 100644 --- a/src/components/EqualizerIcon.tsx +++ b/src/components/EqualizerIcon.tsx @@ -1,35 +1,92 @@ -// Animated equalizer/waveform icon for the MelodyMuse header. -export function EqualizerIcon({ className = 'w-7 h-7' }: { className?: string }) { +// Equalizer/waveform mark for the MelodyMuse header. The SVG has explicit +// width/height attributes (in addition to the Tailwind class) so it can't +// accidentally balloon to fill its parent if the CSS fails to load. + +interface EqualizerIconProps { + className?: string; + size?: number; +} + +export function EqualizerIcon({ className, size = 24 }: EqualizerIconProps) { return ( diff --git a/src/components/InputPanel.tsx b/src/components/InputPanel.tsx index 0dca226..c9cd33e 100644 --- a/src/components/InputPanel.tsx +++ b/src/components/InputPanel.tsx @@ -1,8 +1,7 @@ import { useState, type FormEvent, type KeyboardEvent } from "react"; import { Link } from "react-router-dom"; import { - ChevronDown, - ChevronUp, + ChevronRight, Loader2, Settings as SettingsIcon, Sparkles, @@ -29,8 +28,6 @@ const LANGUAGES: { value: Language; label: string }[] = [ { value: "Other", label: "Other" }, ]; -// A handful of starter ideas shown as quick-fill chips. Kept short and varied -// so the user can see the kind of input the model expects. const EXAMPLE_IDEAS: { text: string; mood: string; vocals: Vocals }[] = [ { text: "melancholic lo-fi house beat for a rainy sunday morning", @@ -71,10 +68,10 @@ export function InputPanel({ disabled, cancelButton, }: InputPanelProps) { - const [optionsOpen, setOptionsOpen] = useState(false); const [styleLoading, setStyleLoading] = useState( null, ); + const [optionsOpen, setOptionsOpen] = useState(false); const elapsed = useElapsed(loading); const toast = useToast(); @@ -88,8 +85,7 @@ export function InputPanel({ if (canSubmit) onSubmit(); }; - // Submit on Cmd/Ctrl+Enter, even from inside the textarea. - const onKeyDown = (e: KeyboardEvent) => { + const onCmdEnter = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key === "Enter") { e.preventDefault(); submit(); @@ -107,7 +103,7 @@ export function InputPanel({ }; const handleStyleRandom = async (mode: "normal" | "crazy") => { - if (styleLoading) return; // already running, don't fire two in parallel + if (styleLoading) return; const ctrl = new AbortController(); setStyleLoading(mode); try { @@ -126,77 +122,81 @@ export function InputPanel({ }; return ( -
- {/* Animated gradient backdrop behind the header */} -
- -
- -

- MelodyMuse -

-
-

- Generate your Suno song assets with AI -

- -
+ + {/* Brand */} +
+
-
- - - Music idea - - -
-