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 (
-