refactor: rename project Lyria Studio → Kithara Studio
- App name, UI text, page titles → Kithara Studio - Python module: lyria_client.py → kithara_client.py - Tailwind color classes: lyria-* → kithara-* - CSS custom properties: --lyria-* → --kithara-* - Database: lyria.db → kithara.db - Package name: kithara-studio-frontend - localStorage key: kithara-saved-searches - Add project logo (lyre icon, black on transparent, invert via CSS) Preserved as Google's property: - LYRIA_API_KEY, LYRIA_BASE_URL, LYRIA_DEFAULT_MODEL env vars - LYRIA_MODELS dict and model IDs (lyria-3-clip-preview, lyria-3-pro-preview) - Google Lyria API references in error messages and docstrings
This commit is contained in:
1 parent
6c1e1b4509
commit
816f8fbd2f
20 files changed
+191
-193
No files matched your search
@@ -1,4 +1,4 @@
|
||||
# Lyria Studio
|
||||
# Kithara Studio
|
||||
|
||||
A webapp for generating, playing, and storing music clips using Google's Lyria music generation models.
|
||||
|
||||
@@ -23,7 +23,7 @@ npm run dev
|
||||
|
||||
## Architecture
|
||||
|
||||
- **Backend**: FastAPI (Python) — async Lyria API client, SQLite metadata, file storage
|
||||
- **Backend**: FastAPI (Python) — async API client for Google's Lyria models, SQLite metadata, file storage
|
||||
- **Frontend**: React + Vite + TypeScript + Tailwind — dark theme, audio player
|
||||
- **Storage**: SQLite for clip metadata, local filesystem for MP3 files
|
||||
|
||||
@@ -40,9 +40,9 @@ npm run dev
|
||||
|
||||
## Models
|
||||
|
||||
- **v0.1**: Lyria 3 Clip Preview (30s, REST)
|
||||
- **v0.2**: Lyria 3 Pro (structure-aware, ~3min)
|
||||
- **v0.3**: Lyria RealTime (WebSocket streaming)
|
||||
- **v0.1**: Google Lyria 3 Clip Preview (30s, REST)
|
||||
- **v0.2**: Google Lyria 3 Pro (structure-aware, ~3min)
|
||||
- **v0.3**: Google Lyria RealTime (WebSocket streaming)
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import uuid
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
DB_PATH = Path(__file__).parent.parent / "data" / "lyria.db"
|
||||
DB_PATH = Path(__file__).parent.parent / "data" / "kithara.db"
|
||||
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Timezone from TZ env var (default: America/New_York)
|
||||
|
||||
@@ -4,7 +4,7 @@ import os
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger("lyria_client")
|
||||
logger = logging.getLogger("kithara_client")
|
||||
|
||||
LYRIA_API_KEY = os.getenv("LYRIA_API_KEY", "")
|
||||
LYRIA_BASE_URL = "https://generativelanguage.googleapis.com/v1beta"
|
||||
@@ -50,11 +50,11 @@ async def generate_clip(prompt: str, model: str = "lyria-3-clip-preview", durati
|
||||
block_reason = data.get("blockReason", "")
|
||||
if block_reason:
|
||||
block_message = data.get("blockReasonMessage", "")
|
||||
logger.warning(f"Lyria API blocked: {block_reason} — {block_message}")
|
||||
logger.warning(f"Google Lyria API blocked: {block_reason} — {block_message}")
|
||||
return {"success": False, "error": f"Prompt was filtered. Try describing the sound using mood, instruments, and tempo instead of genre names. ({block_message or block_reason})"}
|
||||
|
||||
# Extract base64 audio from response
|
||||
# Lyria returns parts array: first part may be text (e.g. "<instrumental>"),
|
||||
# Google Lyria returns parts array: first part may be text (e.g. "<instrumental>"),
|
||||
# audio is in inlineData of a subsequent part.
|
||||
candidates = data.get("candidates", [])
|
||||
if not candidates:
|
||||
@@ -103,7 +103,7 @@ async def generate_clip(prompt: str, model: str = "lyria-3-clip-preview", durati
|
||||
error_detail = e.response.json().get("error", {}).get("message", str(e))
|
||||
except:
|
||||
error_detail = str(e)
|
||||
return {"success": False, "error": f"Lyria API error: {error_detail}"}
|
||||
return {"success": False, "error": f"Google Lyria API error: {error_detail}"}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": f"Client error: {str(e)}"}
|
||||
|
||||
+4
-4
@@ -6,18 +6,18 @@ import os
|
||||
import shutil
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables BEFORE importing lyria_client (it reads env at module level)
|
||||
# Load environment variables BEFORE importing kithara_client (it reads env at module level)
|
||||
load_dotenv(Path(__file__).parent.parent / ".env")
|
||||
|
||||
from models import GenerateRequest, GenerateResponse, ClipList, RenameRequest, MetadataRequest, GenerateArtworkRequest, LYRIA_MODELS, DEFAULT_MODEL
|
||||
from database import init_db, create_clip, get_clips, get_clip, delete_clip, update_clip_status, rename_clip, set_artwork, remove_artwork, delete_all_clips, set_analysis, update_metadata
|
||||
from lyria_client import generate_clip, save_clip_file
|
||||
from kithara_client import generate_clip, save_clip_file
|
||||
from waveform import generate_waveform_svg, get_waveform_path
|
||||
from analysis import analyze_clip
|
||||
from image_gen import generate_artwork, get_image_gen_config, get_available_models, SAFETY_LEVELS
|
||||
from text_gen import suggest_prompts, get_text_gen_config, get_available_models as get_text_models
|
||||
|
||||
app = FastAPI(title="Lyria Studio", version="0.1.0")
|
||||
app = FastAPI(title="Kithara Studio", version="0.1.0")
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
@@ -49,7 +49,7 @@ async def list_models():
|
||||
return {"models": LYRIA_MODELS, "default": DEFAULT_MODEL}
|
||||
|
||||
async def _generate_and_save(clip_id: str, prompt: str, model: str, duration: int):
|
||||
"""Background task: call Lyria API and save the result.
|
||||
"""Background task: call Google Lyria API and save the result.
|
||||
|
||||
DR-LYRIA-001: Pre-generates waveform SVG after clip is ready,
|
||||
so users never wait for on-demand generation.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Shared test fixtures for Lyria Studio backend."""
|
||||
"""Shared test fixtures for Kithara Studio backend."""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
@@ -39,7 +39,7 @@ async def app_client(tmp_path):
|
||||
waveforms_dir.mkdir()
|
||||
artwork_dir = data_dir / "artwork"
|
||||
artwork_dir.mkdir()
|
||||
test_db = data_dir / "lyria.db"
|
||||
test_db = data_dir / "kithara.db"
|
||||
|
||||
# Set env vars BEFORE import so module-level Path() calls use test dirs
|
||||
# (image_gen.py reads ARTWORK_DIR from env, main.py uses __file__-relative paths)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Tests for Lyria Studio API endpoints."""
|
||||
"""Tests for Kithara Studio API endpoints."""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
+2
-2
@@ -4,9 +4,9 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Lyria Studio</title>
|
||||
<title>Kithara Studio</title>
|
||||
</head>
|
||||
<body class="bg-lyria-dark text-lyria-text">
|
||||
<body class="bg-kithara-dark text-kithara-text">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
Generated
+2
-2
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "lyria-studio-frontend",
|
||||
"name": "kithara-studio-frontend",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "lyria-studio-frontend",
|
||||
"name": "kithara-studio-frontend",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"fuse.js": "^7.0.0",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "lyria-studio-frontend",
|
||||
"name": "kithara-studio-frontend",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
@@ -237,19 +237,17 @@ function App() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-lyria-dark">
|
||||
<div className="min-h-screen bg-kithara-dark">
|
||||
<header className="border-b border-white/10 px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded bg-lyria-accent flex items-center justify-center text-white font-bold text-sm">
|
||||
L
|
||||
</div>
|
||||
<h1 className="text-xl font-bold text-white">Lyria Studio</h1>
|
||||
<span className="text-xs text-lyria-muted bg-white/5 px-2 py-0.5 rounded">v0.1</span>
|
||||
<img src="/logo.png" alt="Kithara Studio" className="w-8 h-8 invert" />
|
||||
<h1 className="text-xl font-bold text-white">Kithara Studio</h1>
|
||||
<span className="text-xs text-kithara-muted bg-white/5 px-2 py-0.5 rounded">v0.1</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setSettingsOpen(true)}
|
||||
className="text-lyria-muted hover:text-white transition-colors p-1 rounded hover:bg-white/5"
|
||||
className="text-kithara-muted hover:text-white transition-colors p-1 rounded hover:bg-white/5"
|
||||
title="Settings"
|
||||
aria-label="Open settings"
|
||||
>
|
||||
|
||||
@@ -41,17 +41,17 @@ export default function ArtworkGenerationModal({ isOpen, onClose, onGenerate, tr
|
||||
/>
|
||||
|
||||
{/* Modal */}
|
||||
<div className="relative bg-lyria-surface border border-white/10 rounded-xl shadow-2xl w-full max-w-md mx-4 overflow-hidden">
|
||||
<div className="relative bg-kithara-surface border border-white/10 rounded-xl shadow-2xl w-full max-w-md mx-4 overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-white/10">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-white">Generate Artwork</h2>
|
||||
<p className="text-xs text-lyria-muted mt-0.5 truncate max-w-[200px]">{trackName}</p>
|
||||
<p className="text-xs text-kithara-muted mt-0.5 truncate max-w-[200px]">{trackName}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
disabled={loading}
|
||||
className="text-lyria-muted hover:text-white transition-colors disabled:opacity-50"
|
||||
className="text-kithara-muted hover:text-white transition-colors disabled:opacity-50"
|
||||
aria-label="Close"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -70,10 +70,10 @@ export default function ArtworkGenerationModal({ isOpen, onClose, onGenerate, tr
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
placeholder="e.g., A cyberpunk cityscape at night with neon lights reflecting on wet streets..."
|
||||
className="w-full h-32 bg-black/30 border border-white/10 rounded-lg px-4 py-3 text-sm text-white placeholder:text-lyria-muted/40 focus:outline-none focus:border-lyria-accent resize-none"
|
||||
className="w-full h-32 bg-black/30 border border-white/10 rounded-lg px-4 py-3 text-sm text-white placeholder:text-kithara-muted/40 focus:outline-none focus:border-kithara-accent resize-none"
|
||||
disabled={loading}
|
||||
/>
|
||||
<p className="text-xs text-lyria-muted/60 mt-2">
|
||||
<p className="text-xs text-kithara-muted/60 mt-2">
|
||||
Uses Gemini Image model. Be descriptive for best results.
|
||||
</p>
|
||||
</div>
|
||||
@@ -82,7 +82,7 @@ export default function ArtworkGenerationModal({ isOpen, onClose, onGenerate, tr
|
||||
<button
|
||||
onClick={handleGenerate}
|
||||
disabled={!prompt.trim() || loading}
|
||||
className="w-full py-3 text-sm font-medium rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed bg-lyria-accent text-black hover:bg-lyria-accent/90"
|
||||
className="w-full py-3 text-sm font-medium rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed bg-kithara-accent text-black hover:bg-kithara-accent/90"
|
||||
>
|
||||
{loading ? (
|
||||
<span className="flex items-center justify-center gap-2">
|
||||
|
||||
@@ -98,7 +98,7 @@ export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, o
|
||||
case 'ready': return 'bg-green-500/20 text-green-400'
|
||||
case 'generating': return 'bg-yellow-500/20 text-yellow-400'
|
||||
case 'failed': return 'bg-red-500/20 text-red-400'
|
||||
default: return 'bg-white/10 text-lyria-muted'
|
||||
default: return 'bg-white/10 text-kithara-muted'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,13 +119,13 @@ export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, o
|
||||
onClick={onClose}
|
||||
/>
|
||||
{/* Panel */}
|
||||
<div className="fixed right-0 top-0 bottom-0 w-full max-w-md bg-lyria-panel border-l border-white/10 z-50 overflow-y-auto">
|
||||
<div className="fixed right-0 top-0 bottom-0 w-full max-w-md bg-kithara-panel border-l border-white/10 z-50 overflow-y-auto">
|
||||
{/* Header */}
|
||||
<div className="sticky top-0 bg-lyria-panel border-b border-white/10 px-5 py-4 flex items-center justify-between">
|
||||
<div className="sticky top-0 bg-kithara-panel border-b border-white/10 px-5 py-4 flex items-center justify-between">
|
||||
<h2 className="text-sm font-semibold text-white uppercase tracking-wider">Track Details</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-lyria-muted hover:text-white transition-colors"
|
||||
className="text-kithara-muted hover:text-white transition-colors"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
@@ -137,17 +137,17 @@ export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, o
|
||||
{/* Album Artwork */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label className="text-xs text-lyria-muted uppercase tracking-wider">Album Art</label>
|
||||
<label className="text-xs text-kithara-muted uppercase tracking-wider">Album Art</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => onGenerateArtwork?.(clip)}
|
||||
className="text-xs text-lyria-accent hover:text-lyria-accent/80 transition-colors"
|
||||
className="text-xs text-kithara-accent hover:text-kithara-accent/80 transition-colors"
|
||||
>
|
||||
Generate
|
||||
</button>
|
||||
<button
|
||||
onClick={() => artworkInputRef.current?.click()}
|
||||
className="text-xs text-lyria-accent hover:text-lyria-accent/80 transition-colors"
|
||||
className="text-xs text-kithara-accent hover:text-kithara-accent/80 transition-colors"
|
||||
>
|
||||
{clip.artwork_filename ? 'Change' : 'Upload'}
|
||||
</button>
|
||||
@@ -191,7 +191,7 @@ export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, o
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="relative w-full aspect-square bg-white/5 rounded-lg flex flex-col items-center justify-center text-lyria-accent/20 cursor-pointer group hover:bg-white/10 transition-colors"
|
||||
className="relative w-full aspect-square bg-white/5 rounded-lg flex flex-col items-center justify-center text-kithara-accent/20 cursor-pointer group hover:bg-white/10 transition-colors"
|
||||
onClick={() => {
|
||||
if (currentClipId === clip.id && isPlaying) {
|
||||
onTogglePlay?.()
|
||||
@@ -203,7 +203,7 @@ export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, o
|
||||
<svg className="w-14 h-14" viewBox="0 0 24 24">
|
||||
<text x="12" y="19" textAnchor="middle" fontSize="20" fill="currentColor">𝄞</text>
|
||||
</svg>
|
||||
<span className="text-xs mt-1 text-lyria-muted/40">No album art</span>
|
||||
<span className="text-xs mt-1 text-kithara-muted/40">No album art</span>
|
||||
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
|
||||
{currentClipId === clip.id && isPlaying ? (
|
||||
<svg className="w-12 h-12 text-white" fill="currentColor" viewBox="0 0 24 24">
|
||||
@@ -246,11 +246,11 @@ export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, o
|
||||
{/* Title — inline editable */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<label className="text-xs text-lyria-muted uppercase tracking-wider">Title</label>
|
||||
<label className="text-xs text-kithara-muted uppercase tracking-wider">Title</label>
|
||||
{!isEditingTitle && (
|
||||
<button
|
||||
onClick={startEditing}
|
||||
className="text-xs text-lyria-accent hover:text-lyria-accent/80 transition-colors"
|
||||
className="text-xs text-kithara-accent hover:text-kithara-accent/80 transition-colors"
|
||||
>
|
||||
Rename
|
||||
</button>
|
||||
@@ -264,7 +264,7 @@ export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, o
|
||||
onChange={(e) => setEditTitle(e.target.value)}
|
||||
onBlur={saveTitle}
|
||||
onKeyDown={handleTitleKeyDown}
|
||||
className="w-full bg-white/5 border border-lyria-accent/50 rounded-md px-3 py-2 text-white text-lg font-medium outline-none focus:border-lyria-accent focus:ring-1 focus:ring-lyria-accent/30 transition-all"
|
||||
className="w-full bg-white/5 border border-kithara-accent/50 rounded-md px-3 py-2 text-white text-lg font-medium outline-none focus:border-kithara-accent focus:ring-1 focus:ring-kithara-accent/30 transition-all"
|
||||
placeholder="Track title..."
|
||||
/>
|
||||
) : (
|
||||
@@ -272,14 +272,14 @@ export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, o
|
||||
onClick={startEditing}
|
||||
title="Click to rename"
|
||||
>
|
||||
{clip.title || <span className="text-lyria-muted italic">Untitled</span>}
|
||||
{clip.title || <span className="text-kithara-muted italic">Untitled</span>}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Prompt */}
|
||||
<div>
|
||||
<label className="text-xs text-lyria-muted uppercase tracking-wider mb-1 block">Prompt</label>
|
||||
<label className="text-xs text-kithara-muted uppercase tracking-wider mb-1 block">Prompt</label>
|
||||
<p className="text-sm text-white/90 whitespace-pre-wrap break-words bg-white/5 rounded-md p-3">
|
||||
{clip.prompt}
|
||||
</p>
|
||||
@@ -288,7 +288,7 @@ export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, o
|
||||
{/* Structured Prompt Breakdown */}
|
||||
{clip.prompt_structure && (
|
||||
<div>
|
||||
<label className="text-xs text-lyria-muted uppercase tracking-wider mb-1 block">Structured Prompt</label>
|
||||
<label className="text-xs text-kithara-muted uppercase tracking-wider mb-1 block">Structured Prompt</label>
|
||||
<div className="space-y-2">
|
||||
{[
|
||||
{ key: 'style', label: 'Style' },
|
||||
@@ -304,7 +304,7 @@ export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, o
|
||||
if (!val) return null
|
||||
return (
|
||||
<div key={key} className="bg-white/5 rounded-md p-2">
|
||||
<span className="text-xs text-lyria-accent font-medium">{label}: </span>
|
||||
<span className="text-xs text-kithara-accent font-medium">{label}: </span>
|
||||
<span className="text-sm text-white/90">{val}</span>
|
||||
</div>
|
||||
)
|
||||
@@ -315,7 +315,7 @@ export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, o
|
||||
|
||||
{/* Status */}
|
||||
<div>
|
||||
<label className="text-xs text-lyria-muted uppercase tracking-wider mb-1 block">Status</label>
|
||||
<label className="text-xs text-kithara-muted uppercase tracking-wider mb-1 block">Status</label>
|
||||
<span className={`text-xs px-2 py-1 rounded ${statusColor(clip.status)}`}>
|
||||
{statusLabel(clip.status)}
|
||||
</span>
|
||||
@@ -324,7 +324,7 @@ export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, o
|
||||
{/* Error message */}
|
||||
{clip.status === 'failed' && clip.error_message && (
|
||||
<div>
|
||||
<label className="text-xs text-lyria-muted uppercase tracking-wider mb-1 block">Error</label>
|
||||
<label className="text-xs text-kithara-muted uppercase tracking-wider mb-1 block">Error</label>
|
||||
<p className="text-sm text-red-400/90 bg-red-500/10 rounded-md p-3 break-words">
|
||||
{clip.error_message}
|
||||
</p>
|
||||
@@ -334,16 +334,16 @@ export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, o
|
||||
{/* Metadata grid */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="text-xs text-lyria-muted uppercase tracking-wider block">Model</label>
|
||||
<label className="text-xs text-kithara-muted uppercase tracking-wider block">Model</label>
|
||||
<p className="text-sm text-white">{modelName(clip.model)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-lyria-muted uppercase tracking-wider block">Duration</label>
|
||||
<label className="text-xs text-kithara-muted uppercase tracking-wider block">Duration</label>
|
||||
<p className="text-sm text-white">{clip.duration_seconds}s</p>
|
||||
</div>
|
||||
{clip.file_size_bytes && (
|
||||
<div>
|
||||
<label className="text-xs text-lyria-muted uppercase tracking-wider block">File Size</label>
|
||||
<label className="text-xs text-kithara-muted uppercase tracking-wider block">File Size</label>
|
||||
<p className="text-sm text-white">{(clip.file_size_bytes / 1024).toFixed(0)} KB</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -351,7 +351,7 @@ export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, o
|
||||
const c = getBPMColor(clip.bpm)
|
||||
return c ? (
|
||||
<div>
|
||||
<label className="text-xs text-lyria-muted uppercase tracking-wider block">BPM</label>
|
||||
<label className="text-xs text-kithara-muted uppercase tracking-wider block">BPM</label>
|
||||
<span
|
||||
className="text-sm px-2 py-0.5 rounded font-medium inline-block"
|
||||
style={{ backgroundColor: c.bg, color: c.text, border: `1px solid ${c.border}` }}
|
||||
@@ -365,7 +365,7 @@ export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, o
|
||||
const c = getKeyColor(clip.key)
|
||||
return c ? (
|
||||
<div>
|
||||
<label className="text-xs text-lyria-muted uppercase tracking-wider block">Key</label>
|
||||
<label className="text-xs text-kithara-muted uppercase tracking-wider block">Key</label>
|
||||
<span
|
||||
className="text-sm px-2 py-0.5 rounded font-medium inline-block"
|
||||
style={{ backgroundColor: c.bg, color: c.text, border: `1px solid ${c.border}` }}
|
||||
@@ -376,7 +376,7 @@ export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, o
|
||||
) : null
|
||||
})()}
|
||||
<div>
|
||||
<label className="text-xs text-lyria-muted uppercase tracking-wider block">Created</label>
|
||||
<label className="text-xs text-kithara-muted uppercase tracking-wider block">Created</label>
|
||||
<p className="text-sm text-white">{formatDate(clip.created_at)}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -405,8 +405,8 @@ export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, o
|
||||
|
||||
{/* ID (for reference) */}
|
||||
<div>
|
||||
<label className="text-xs text-lyria-muted uppercase tracking-wider mb-1 block">Clip ID</label>
|
||||
<p className="text-xs text-lyria-muted/60 font-mono break-all">{clip.id}</p>
|
||||
<label className="text-xs text-kithara-muted uppercase tracking-wider mb-1 block">Clip ID</label>
|
||||
<p className="text-xs text-kithara-muted/60 font-mono break-all">{clip.id}</p>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
@@ -414,7 +414,7 @@ export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, o
|
||||
<a
|
||||
href={`/api/clips/${clip.id}/download`}
|
||||
download
|
||||
className="w-full py-2 text-sm text-lyria-accent bg-lyria-accent/10 hover:bg-lyria-accent/20 rounded-md transition-colors flex items-center justify-center gap-2"
|
||||
className="w-full py-2 text-sm text-kithara-accent bg-kithara-accent/10 hover:bg-kithara-accent/20 rounded-md transition-colors flex items-center justify-center gap-2"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||
@@ -466,7 +466,7 @@ function MetadataField({ label, value, placeholder, onSave }: {
|
||||
if (editing) {
|
||||
return (
|
||||
<div>
|
||||
<label className="text-xs text-lyria-muted uppercase tracking-wider block mb-1">{label}</label>
|
||||
<label className="text-xs text-kithara-muted uppercase tracking-wider block mb-1">{label}</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
ref={inputRef}
|
||||
@@ -479,7 +479,7 @@ function MetadataField({ label, value, placeholder, onSave }: {
|
||||
}}
|
||||
onBlur={save}
|
||||
placeholder={placeholder}
|
||||
className="flex-1 bg-black/30 border border-lyria-accent rounded-md px-3 py-1.5 text-sm text-white placeholder:text-lyria-muted/40 focus:outline-none"
|
||||
className="flex-1 bg-black/30 border border-kithara-accent rounded-md px-3 py-1.5 text-sm text-white placeholder:text-kithara-muted/40 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -491,13 +491,13 @@ function MetadataField({ label, value, placeholder, onSave }: {
|
||||
className="group cursor-pointer"
|
||||
onClick={() => setEditing(true)}
|
||||
>
|
||||
<label className="text-xs text-lyria-muted uppercase tracking-wider block mb-1">{label}</label>
|
||||
<label className="text-xs text-kithara-muted uppercase tracking-wider block mb-1">{label}</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`text-sm ${value ? 'text-white' : 'text-lyria-muted/40 italic'}`}>
|
||||
<span className={`text-sm ${value ? 'text-white' : 'text-kithara-muted/40 italic'}`}>
|
||||
{value || placeholder}
|
||||
</span>
|
||||
<svg
|
||||
className="w-3 h-3 text-lyria-muted/40 group-hover:text-lyria-accent transition-colors"
|
||||
className="w-3 h-3 text-kithara-muted/40 group-hover:text-kithara-accent transition-colors"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
|
||||
@@ -71,7 +71,7 @@ function HighlightText({ text, query }: { text: string; query: string }) {
|
||||
<>
|
||||
{parts.map((part, i) =>
|
||||
part.toLowerCase() === query.toLowerCase() ? (
|
||||
<span key={i} className="bg-lyria-accent/30 text-white rounded px-0.5">{part}</span>
|
||||
<span key={i} className="bg-kithara-accent/30 text-white rounded px-0.5">{part}</span>
|
||||
) : (
|
||||
<span key={i}>{part}</span>
|
||||
)
|
||||
@@ -104,7 +104,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
|
||||
// Saved searches
|
||||
const [savedSearches, setSavedSearches] = useState<SavedSearch[]>(() => {
|
||||
try {
|
||||
const raw = localStorage.getItem('lyria-saved-searches')
|
||||
const raw = localStorage.getItem('kithara-saved-searches')
|
||||
return raw ? JSON.parse(raw) : []
|
||||
} catch {
|
||||
return []
|
||||
@@ -195,7 +195,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
|
||||
}
|
||||
const updated = [...savedSearches, newSearch]
|
||||
setSavedSearches(updated)
|
||||
localStorage.setItem('lyria-saved-searches', JSON.stringify(updated))
|
||||
localStorage.setItem('kithara-saved-searches', JSON.stringify(updated))
|
||||
}, [search, filters, savedSearches])
|
||||
|
||||
const loadSearch = useCallback((saved: SavedSearch) => {
|
||||
@@ -207,7 +207,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
|
||||
const deleteSearch = useCallback((id: string) => {
|
||||
const updated = savedSearches.filter(s => s.id !== id)
|
||||
setSavedSearches(updated)
|
||||
localStorage.setItem('lyria-saved-searches', JSON.stringify(updated))
|
||||
localStorage.setItem('kithara-saved-searches', JSON.stringify(updated))
|
||||
}, [savedSearches])
|
||||
|
||||
const clearFilters = () => {
|
||||
@@ -261,14 +261,14 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
|
||||
|
||||
if (clips.length === 0) {
|
||||
return (
|
||||
<div className="bg-lyria-panel rounded-lg p-8 border border-white/5 text-center">
|
||||
<p className="text-lyria-muted text-sm">No clips yet. Generate your first one!</p>
|
||||
<div className="bg-kithara-panel rounded-lg p-8 border border-white/5 text-center">
|
||||
<p className="text-kithara-muted text-sm">No clips yet. Generate your first one!</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-lyria-panel rounded-lg border border-white/5">
|
||||
<div className="bg-kithara-panel rounded-lg border border-white/5">
|
||||
{/* Header: title, search, view toggle */}
|
||||
<div className="px-5 py-4 border-b border-white/5">
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -279,7 +279,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
|
||||
{/* Search bar */}
|
||||
<div className="relative flex-1 max-w-xs">
|
||||
<svg
|
||||
className="absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-lyria-muted"
|
||||
className="absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-kithara-muted"
|
||||
fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
@@ -289,12 +289,12 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
|
||||
placeholder="Search tracks, artists, albums..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="w-full bg-white/5 border border-white/10 rounded-md pl-8 pr-3 py-1.5 text-sm text-white placeholder-lyria-muted focus:outline-none focus:border-lyria-accent/50 focus:ring-1 focus:ring-lyria-accent/30 transition-colors"
|
||||
className="w-full bg-white/5 border border-white/10 rounded-md pl-8 pr-3 py-1.5 text-sm text-white placeholder-kithara-muted focus:outline-none focus:border-kithara-accent/50 focus:ring-1 focus:ring-kithara-accent/30 transition-colors"
|
||||
/>
|
||||
{search && (
|
||||
<button
|
||||
onClick={() => setSearch('')}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-lyria-muted hover:text-white transition-colors"
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-kithara-muted hover:text-white transition-colors"
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
@@ -308,8 +308,8 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
|
||||
onClick={() => setShowFilters(!showFilters)}
|
||||
className={`p-1.5 rounded transition-colors ${
|
||||
showFilters || hasActiveFilters
|
||||
? 'bg-lyria-accent/20 text-lyria-accent border border-lyria-accent/30'
|
||||
: 'text-lyria-muted hover:text-white border border-white/10'
|
||||
? 'bg-kithara-accent/20 text-kithara-accent border border-kithara-accent/30'
|
||||
: 'text-kithara-muted hover:text-white border border-white/10'
|
||||
}`}
|
||||
title="Filters"
|
||||
>
|
||||
@@ -321,7 +321,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
|
||||
{/* Saved searches button */}
|
||||
<button
|
||||
onClick={() => setShowSaved(!showSaved)}
|
||||
className="p-1.5 rounded transition-colors text-lyria-muted hover:text-white border border-white/10"
|
||||
className="p-1.5 rounded transition-colors text-kithara-muted hover:text-white border border-white/10"
|
||||
title="Saved searches"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -333,7 +333,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
|
||||
<div className="flex bg-white/5 rounded-md border border-white/10 overflow-hidden">
|
||||
<button
|
||||
onClick={() => setView('ribbons')}
|
||||
className={`p-1.5 transition-colors ${view === 'ribbons' ? 'bg-lyria-accent text-white' : 'text-lyria-muted hover:text-white'}`}
|
||||
className={`p-1.5 transition-colors ${view === 'ribbons' ? 'bg-kithara-accent text-white' : 'text-kithara-muted hover:text-white'}`}
|
||||
title="List view"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -342,7 +342,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setView('tiles')}
|
||||
className={`p-1.5 transition-colors ${view === 'tiles' ? 'bg-lyria-accent text-white' : 'text-lyria-muted hover:text-white'}`}
|
||||
className={`p-1.5 transition-colors ${view === 'tiles' ? 'bg-kithara-accent text-white' : 'text-kithara-muted hover:text-white'}`}
|
||||
title="Tile view"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -356,33 +356,33 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
|
||||
{hasActiveFilters && (
|
||||
<div className="flex items-center gap-2 flex-wrap mt-2">
|
||||
{filters.model !== 'all' && (
|
||||
<span className="text-xs px-2 py-1 rounded bg-lyria-accent/10 text-lyria-accent border border-lyria-accent/20">
|
||||
<span className="text-xs px-2 py-1 rounded bg-kithara-accent/10 text-kithara-accent border border-kithara-accent/20">
|
||||
Model: {filters.model}
|
||||
</span>
|
||||
)}
|
||||
{filters.status !== 'all' && (
|
||||
<span className="text-xs px-2 py-1 rounded bg-lyria-accent/10 text-lyria-accent border border-lyria-accent/20">
|
||||
<span className="text-xs px-2 py-1 rounded bg-kithara-accent/10 text-kithara-accent border border-kithara-accent/20">
|
||||
Status: {filters.status}
|
||||
</span>
|
||||
)}
|
||||
{filters.hasArtwork !== 'all' && (
|
||||
<span className="text-xs px-2 py-1 rounded bg-lyria-accent/10 text-lyria-accent border border-lyria-accent/20">
|
||||
<span className="text-xs px-2 py-1 rounded bg-kithara-accent/10 text-kithara-accent border border-kithara-accent/20">
|
||||
Artwork: {filters.hasArtwork}
|
||||
</span>
|
||||
)}
|
||||
{(filters.bpmMin || filters.bpmMax) && (
|
||||
<span className="text-xs px-2 py-1 rounded bg-lyria-accent/10 text-lyria-accent border border-lyria-accent/20">
|
||||
<span className="text-xs px-2 py-1 rounded bg-kithara-accent/10 text-kithara-accent border border-kithara-accent/20">
|
||||
BPM: {filters.bpmMin || '0'} - {filters.bpmMax || '∞'}
|
||||
</span>
|
||||
)}
|
||||
{(filters.dateFrom || filters.dateTo) && (
|
||||
<span className="text-xs px-2 py-1 rounded bg-lyria-accent/10 text-lyria-accent border border-lyria-accent/20">
|
||||
<span className="text-xs px-2 py-1 rounded bg-kithara-accent/10 text-kithara-accent border border-kithara-accent/20">
|
||||
Date: {filters.dateFrom || '...'} → {filters.dateTo || '...'}
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
onClick={clearFilters}
|
||||
className="text-xs text-lyria-muted hover:text-white transition-colors"
|
||||
className="text-xs text-kithara-muted hover:text-white transition-colors"
|
||||
>
|
||||
Clear all
|
||||
</button>
|
||||
@@ -391,7 +391,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
|
||||
|
||||
{/* Results count */}
|
||||
{(search || hasActiveFilters) && (
|
||||
<p className="text-xs text-lyria-muted mt-1.5">
|
||||
<p className="text-xs text-kithara-muted mt-1.5">
|
||||
Showing {filtered.length} of {clips.length} clips
|
||||
</p>
|
||||
)}
|
||||
@@ -402,7 +402,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
|
||||
<div className="px-5 py-4 border-b border-white/5 bg-white/5">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
<div>
|
||||
<label className="block text-xs text-lyria-muted mb-1">Model</label>
|
||||
<label className="block text-xs text-kithara-muted mb-1">Model</label>
|
||||
<select
|
||||
value={filters.model}
|
||||
onChange={(e) => setFilters(f => ({ ...f, model: e.target.value }))}
|
||||
@@ -414,7 +414,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-lyria-muted mb-1">Status</label>
|
||||
<label className="block text-xs text-kithara-muted mb-1">Status</label>
|
||||
<select
|
||||
value={filters.status}
|
||||
onChange={(e) => setFilters(f => ({ ...f, status: e.target.value }))}
|
||||
@@ -427,7 +427,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-lyria-muted mb-1">Artwork</label>
|
||||
<label className="block text-xs text-kithara-muted mb-1">Artwork</label>
|
||||
<select
|
||||
value={filters.hasArtwork}
|
||||
onChange={(e) => setFilters(f => ({ ...f, hasArtwork: e.target.value }))}
|
||||
@@ -439,29 +439,29 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-lyria-muted mb-1">BPM Range</label>
|
||||
<label className="block text-xs text-kithara-muted mb-1">BPM Range</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="number"
|
||||
value={filters.bpmMin}
|
||||
onChange={(e) => setFilters(f => ({ ...f, bpmMin: e.target.value }))}
|
||||
placeholder="Min"
|
||||
className="w-full bg-black/30 border border-white/10 rounded-md px-2 py-1.5 text-sm text-white placeholder:text-lyria-muted/30"
|
||||
className="w-full bg-black/30 border border-white/10 rounded-md px-2 py-1.5 text-sm text-white placeholder:text-kithara-muted/30"
|
||||
/>
|
||||
<span className="text-lyria-muted">-</span>
|
||||
<span className="text-kithara-muted">-</span>
|
||||
<input
|
||||
type="number"
|
||||
value={filters.bpmMax}
|
||||
onChange={(e) => setFilters(f => ({ ...f, bpmMax: e.target.value }))}
|
||||
placeholder="Max"
|
||||
className="w-full bg-black/30 border border-white/10 rounded-md px-2 py-1.5 text-sm text-white placeholder:text-lyria-muted/30"
|
||||
className="w-full bg-black/30 border border-white/10 rounded-md px-2 py-1.5 text-sm text-white placeholder:text-kithara-muted/30"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3 mt-3">
|
||||
<div>
|
||||
<label className="block text-xs text-lyria-muted mb-1">Date From</label>
|
||||
<label className="block text-xs text-kithara-muted mb-1">Date From</label>
|
||||
<input
|
||||
type="date"
|
||||
value={filters.dateFrom}
|
||||
@@ -470,7 +470,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-lyria-muted mb-1">Date To</label>
|
||||
<label className="block text-xs text-kithara-muted mb-1">Date To</label>
|
||||
<input
|
||||
type="date"
|
||||
value={filters.dateTo}
|
||||
@@ -482,13 +482,13 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
|
||||
<div className="flex items-center justify-between mt-3">
|
||||
<button
|
||||
onClick={clearFilters}
|
||||
className="text-xs text-lyria-muted hover:text-white transition-colors"
|
||||
className="text-xs text-kithara-muted hover:text-white transition-colors"
|
||||
>
|
||||
Reset all filters
|
||||
</button>
|
||||
<button
|
||||
onClick={saveSearch}
|
||||
className="text-xs text-lyria-accent hover:text-lyria-accent/80 transition-colors"
|
||||
className="text-xs text-kithara-accent hover:text-kithara-accent/80 transition-colors"
|
||||
>
|
||||
Save this search 💾
|
||||
</button>
|
||||
@@ -499,19 +499,19 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
|
||||
{/* Saved searches dropdown */}
|
||||
{showSaved && savedSearches.length > 0 && (
|
||||
<div className="px-5 py-3 border-b border-white/5 bg-white/5 space-y-2">
|
||||
<p className="text-xs text-lyria-muted uppercase tracking-wider">Saved Searches</p>
|
||||
<p className="text-xs text-kithara-muted uppercase tracking-wider">Saved Searches</p>
|
||||
{savedSearches.map(saved => (
|
||||
<div key={saved.id} className="flex items-center justify-between group">
|
||||
<button
|
||||
onClick={() => loadSearch(saved)}
|
||||
className="text-sm text-white hover:text-lyria-accent transition-colors"
|
||||
className="text-sm text-white hover:text-kithara-accent transition-colors"
|
||||
>
|
||||
{saved.name}
|
||||
{saved.query && <span className="text-lyria-muted ml-1">"{saved.query}"</span>}
|
||||
{saved.query && <span className="text-kithara-muted ml-1">"{saved.query}"</span>}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => deleteSearch(saved.id)}
|
||||
className="text-lyria-muted hover:text-red-400 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
className="text-kithara-muted hover:text-red-400 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
@@ -525,7 +525,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
|
||||
{/* Clip list */}
|
||||
{filtered.length === 0 ? (
|
||||
<div className="px-5 py-8 text-center">
|
||||
<p className="text-lyria-muted text-sm">No clips match your search</p>
|
||||
<p className="text-kithara-muted text-sm">No clips match your search</p>
|
||||
</div>
|
||||
) : view === 'ribbons' ? (
|
||||
/* ── RIBBON VIEW ── */
|
||||
@@ -546,7 +546,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
|
||||
className="w-10 h-10 rounded object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-10 h-10 rounded bg-white/5 flex items-center justify-center text-lyria-accent/30">
|
||||
<div className="w-10 h-10 rounded bg-white/5 flex items-center justify-center text-kithara-accent/30">
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24">
|
||||
<text x="12" y="19" textAnchor="middle" fontSize="20" fill="currentColor">𝄞</text>
|
||||
</svg>
|
||||
@@ -556,7 +556,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
|
||||
>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onPlay(clip) }}
|
||||
className="text-white hover:text-lyria-accent transition-colors"
|
||||
className="text-white hover:text-kithara-accent transition-colors"
|
||||
title="Play"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
@@ -579,7 +579,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
|
||||
}}
|
||||
onBlur={commitRename}
|
||||
placeholder="Enter a name..."
|
||||
className="w-full bg-white/5 border border-lyria-accent/50 rounded px-2 py-1 text-sm text-white placeholder-lyria-muted focus:outline-none focus:ring-1 focus:ring-lyria-accent/50"
|
||||
className="w-full bg-white/5 border border-kithara-accent/50 rounded px-2 py-1 text-sm text-white placeholder-kithara-muted focus:outline-none focus:ring-1 focus:ring-kithara-accent/50"
|
||||
autoFocus
|
||||
/>
|
||||
) : clip.title ? (
|
||||
@@ -587,7 +587,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
|
||||
<p className="text-sm font-medium text-white truncate">
|
||||
<HighlightText text={clip.title} query={debouncedSearch} />
|
||||
</p>
|
||||
<p className="text-xs text-lyria-muted/60 truncate">
|
||||
<p className="text-xs text-kithara-muted/60 truncate">
|
||||
<HighlightText text={clip.prompt} query={debouncedSearch} />
|
||||
</p>
|
||||
</div>
|
||||
@@ -606,9 +606,9 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
|
||||
}`}>
|
||||
{clip.status}
|
||||
</span>
|
||||
<span className="text-xs text-lyria-muted">{clip.duration_seconds}s</span>
|
||||
<span className="text-xs text-lyria-muted">{modelName(clip.model)}</span>
|
||||
<span className="text-xs text-lyria-muted">{formatDate(clip.created_at)}</span>
|
||||
<span className="text-xs text-kithara-muted">{clip.duration_seconds}s</span>
|
||||
<span className="text-xs text-kithara-muted">{modelName(clip.model)}</span>
|
||||
<span className="text-xs text-kithara-muted">{formatDate(clip.created_at)}</span>
|
||||
{clip.bpm && (() => {
|
||||
const c = getBPMColor(clip.bpm)
|
||||
return c ? (
|
||||
@@ -645,7 +645,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
|
||||
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); startRename(clip) }}
|
||||
className="text-lyria-muted/60 hover:text-white transition-colors shrink-0"
|
||||
className="text-kithara-muted/60 hover:text-white transition-colors shrink-0"
|
||||
title="Rename clip"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -658,7 +658,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
|
||||
setUploadingId(clip.id)
|
||||
setTimeout(() => artworkInputRef.current?.click(), 0)
|
||||
}}
|
||||
className="text-lyria-muted/60 hover:text-white transition-colors shrink-0"
|
||||
className="text-kithara-muted/60 hover:text-white transition-colors shrink-0"
|
||||
title="Upload album art"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -670,7 +670,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
|
||||
e.stopPropagation()
|
||||
window.open(`/api/clips/${clip.id}/download`, '_blank')
|
||||
}}
|
||||
className="text-lyria-muted hover:text-lyria-accent transition-colors shrink-0"
|
||||
className="text-kithara-muted hover:text-kithara-accent transition-colors shrink-0"
|
||||
title="Download MP3"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -679,7 +679,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onDelete(clip.id) }}
|
||||
className="text-lyria-muted hover:text-red-400 transition-colors shrink-0"
|
||||
className="text-kithara-muted hover:text-red-400 transition-colors shrink-0"
|
||||
title="Delete clip"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -695,8 +695,8 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
|
||||
{paged.map((clip) => (
|
||||
<div
|
||||
key={clip.id}
|
||||
className={`bg-lyria-dark rounded-lg border border-white/5 p-3 hover:border-lyria-accent/30 transition-colors flex flex-col cursor-pointer ${
|
||||
currentClipId === clip.id ? 'border-lyria-accent/50' : ''
|
||||
className={`bg-kithara-dark rounded-lg border border-white/5 p-3 hover:border-kithara-accent/30 transition-colors flex flex-col cursor-pointer ${
|
||||
currentClipId === clip.id ? 'border-kithara-accent/50' : ''
|
||||
}`}
|
||||
onClick={() => onSelect(clip)}
|
||||
>
|
||||
@@ -708,7 +708,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
|
||||
className="w-full aspect-square rounded-md object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full aspect-square bg-white/5 rounded-md flex items-center justify-center text-lyria-accent/30">
|
||||
<div className="w-full aspect-square bg-white/5 rounded-md flex items-center justify-center text-kithara-accent/30">
|
||||
<svg className="w-10 h-10" viewBox="0 0 24 24">
|
||||
<text x="12" y="19" textAnchor="middle" fontSize="20" fill="currentColor">𝄞</text>
|
||||
</svg>
|
||||
@@ -717,7 +717,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
|
||||
<div className="absolute inset-0 rounded-md bg-black/60 flex items-center justify-center gap-2 opacity-0 group-hover/art:opacity-100 transition-opacity">
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onPlay(clip) }}
|
||||
className="text-white hover:text-lyria-accent transition-colors"
|
||||
className="text-white hover:text-kithara-accent transition-colors"
|
||||
title="Play"
|
||||
>
|
||||
<svg className="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
|
||||
@@ -730,7 +730,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
|
||||
setUploadingId(clip.id)
|
||||
setTimeout(() => artworkInputRef.current?.click(), 0)
|
||||
}}
|
||||
className="text-white/80 hover:text-lyria-accent transition-colors"
|
||||
className="text-white/80 hover:text-kithara-accent transition-colors"
|
||||
title="Upload album art"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -752,7 +752,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
|
||||
}}
|
||||
onBlur={commitRename}
|
||||
placeholder="Enter a name..."
|
||||
className="w-full bg-white/5 border border-lyria-accent/50 rounded px-2 py-1 text-xs text-white placeholder-lyria-muted focus:outline-none focus:ring-1 focus:ring-lyria-accent/50 mb-1"
|
||||
className="w-full bg-white/5 border border-kithara-accent/50 rounded px-2 py-1 text-xs text-white placeholder-kithara-muted focus:outline-none focus:ring-1 focus:ring-kithara-accent/50 mb-1"
|
||||
autoFocus
|
||||
/>
|
||||
) : (
|
||||
@@ -771,8 +771,8 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
|
||||
}`}>
|
||||
{clip.status}
|
||||
</span>
|
||||
<span className="text-[10px] text-lyria-muted">{clip.duration_seconds}s</span>
|
||||
<span className="text-[10px] text-lyria-muted">{modelName(clip.model)}</span>
|
||||
<span className="text-[10px] text-kithara-muted">{clip.duration_seconds}s</span>
|
||||
<span className="text-[10px] text-kithara-muted">{modelName(clip.model)}</span>
|
||||
{clip.bpm && (() => {
|
||||
const c = getBPMColor(clip.bpm)
|
||||
return c ? (
|
||||
@@ -810,7 +810,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
|
||||
<div className="flex items-center gap-1 mt-2 pt-1.5 border-t border-white/5">
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); startRename(clip) }}
|
||||
className="text-lyria-muted/60 hover:text-white transition-colors text-xs"
|
||||
className="text-kithara-muted/60 hover:text-white transition-colors text-xs"
|
||||
title="Rename"
|
||||
>
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -822,7 +822,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
|
||||
e.stopPropagation()
|
||||
window.open(`/api/clips/${clip.id}/download`, '_blank')
|
||||
}}
|
||||
className="text-lyria-muted hover:text-lyria-accent transition-colors text-xs"
|
||||
className="text-kithara-muted hover:text-kithara-accent transition-colors text-xs"
|
||||
title="Download MP3"
|
||||
>
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -831,7 +831,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onDelete(clip.id) }}
|
||||
className="text-lyria-muted hover:text-red-400 transition-colors text-xs"
|
||||
className="text-kithara-muted hover:text-red-400 transition-colors text-xs"
|
||||
title="Delete"
|
||||
>
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -847,7 +847,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="px-5 py-3 border-t border-white/5 flex items-center justify-between">
|
||||
<p className="text-xs text-lyria-muted">
|
||||
<p className="text-xs text-kithara-muted">
|
||||
Page {page} of {totalPages} ({filtered.length} clip{filtered.length !== 1 ? 's' : ''})
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -107,7 +107,7 @@ export default function Generator({ onGenerate }: Props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-lyria-panel rounded-lg p-5 border border-white/5">
|
||||
<div className="bg-kithara-panel rounded-lg p-5 border border-white/5">
|
||||
<h2 className="text-sm font-semibold text-white mb-4 uppercase tracking-wider">
|
||||
Generate
|
||||
</h2>
|
||||
@@ -116,13 +116,13 @@ export default function Generator({ onGenerate }: Props) {
|
||||
{/* Model & Duration */}
|
||||
<div className="flex gap-4">
|
||||
<div className="flex-1">
|
||||
<label className="block text-xs text-lyria-muted mb-1.5">
|
||||
<label className="block text-xs text-kithara-muted mb-1.5">
|
||||
Model
|
||||
</label>
|
||||
<select
|
||||
value={model}
|
||||
onChange={(e) => handleModelChange(e.target.value)}
|
||||
className="w-full bg-lyria-dark border border-white/10 rounded-md px-3 py-2 text-sm text-white focus:outline-none focus:border-lyria-accent"
|
||||
className="w-full bg-kithara-dark border border-white/10 rounded-md px-3 py-2 text-sm text-white focus:outline-none focus:border-kithara-accent"
|
||||
>
|
||||
{Object.entries(models).map(([key, info]) => (
|
||||
<option key={key} value={key}>
|
||||
@@ -133,7 +133,7 @@ export default function Generator({ onGenerate }: Props) {
|
||||
</div>
|
||||
|
||||
<div className="w-28">
|
||||
<label className="block text-xs text-lyria-muted mb-1.5">
|
||||
<label className="block text-xs text-kithara-muted mb-1.5">
|
||||
Duration ({duration}s)
|
||||
</label>
|
||||
<input
|
||||
@@ -142,12 +142,12 @@ export default function Generator({ onGenerate }: Props) {
|
||||
max={maxDuration}
|
||||
value={duration}
|
||||
onChange={(e) => setDuration(Number(e.target.value))}
|
||||
className="w-full mt-2 accent-lyria-accent"
|
||||
className="w-full mt-2 accent-kithara-accent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between text-xs text-lyria-muted">
|
||||
<div className="flex items-center justify-between text-xs text-kithara-muted">
|
||||
<span>{currentModelInfo?.name || 'Clip Preview'}</span>
|
||||
<span>{duration}s / {maxDuration}s max</span>
|
||||
</div>
|
||||
@@ -160,8 +160,8 @@ export default function Generator({ onGenerate }: Props) {
|
||||
onClick={() => setPromptMode('basic')}
|
||||
className={`flex-1 py-1.5 text-xs font-medium rounded-md transition-colors ${
|
||||
promptMode === 'basic'
|
||||
? 'bg-lyria-accent text-black'
|
||||
: 'text-lyria-muted hover:text-white'
|
||||
? 'bg-kithara-accent text-black'
|
||||
: 'text-kithara-muted hover:text-white'
|
||||
}`}
|
||||
>
|
||||
Basic
|
||||
@@ -171,8 +171,8 @@ export default function Generator({ onGenerate }: Props) {
|
||||
onClick={() => setPromptMode('advanced')}
|
||||
className={`flex-1 py-1.5 text-xs font-medium rounded-md transition-colors ${
|
||||
promptMode === 'advanced'
|
||||
? 'bg-lyria-accent text-black'
|
||||
: 'text-lyria-muted hover:text-white'
|
||||
? 'bg-kithara-accent text-black'
|
||||
: 'text-kithara-muted hover:text-white'
|
||||
}`}
|
||||
>
|
||||
Advanced
|
||||
@@ -184,12 +184,12 @@ export default function Generator({ onGenerate }: Props) {
|
||||
{(promptMode === 'basic' || !isPro) && (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
<label className="text-xs text-lyria-muted">Prompt</label>
|
||||
<label className="text-xs text-kithara-muted">Prompt</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSuggestionsOpen(true)}
|
||||
disabled={!prompt.trim()}
|
||||
className="text-xs text-lyria-accent hover:text-lyria-accent/80 transition-colors disabled:opacity-40"
|
||||
className="text-xs text-kithara-accent hover:text-kithara-accent/80 transition-colors disabled:opacity-40"
|
||||
>
|
||||
Get Suggestions ✨
|
||||
</button>
|
||||
@@ -198,7 +198,7 @@ export default function Generator({ onGenerate }: Props) {
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
placeholder="Dark distorted kickdrum with atmospheric synth pads, 180 BPM..."
|
||||
className="w-full h-24 bg-lyria-dark border border-white/10 rounded-md px-3 py-2 text-sm text-white placeholder:text-lyria-muted/50 focus:outline-none focus:border-lyria-accent resize-none"
|
||||
className="w-full h-24 bg-kithara-dark border border-white/10 rounded-md px-3 py-2 text-sm text-white placeholder:text-kithara-muted/50 focus:outline-none focus:border-kithara-accent resize-none"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -207,12 +207,12 @@ export default function Generator({ onGenerate }: Props) {
|
||||
{promptMode === 'advanced' && isPro && (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
<label className="text-xs text-lyria-muted">Structured Prompt</label>
|
||||
<label className="text-xs text-kithara-muted">Structured Prompt</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSuggestionsOpen(true)}
|
||||
disabled={Object.values(structuredPrompt).every(v => !v?.trim())}
|
||||
className="text-xs text-lyria-accent hover:text-lyria-accent/80 transition-colors disabled:opacity-40"
|
||||
className="text-xs text-kithara-accent hover:text-kithara-accent/80 transition-colors disabled:opacity-40"
|
||||
>
|
||||
Get Suggestions ✨
|
||||
</button>
|
||||
@@ -231,7 +231,7 @@ export default function Generator({ onGenerate }: Props) {
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading || (promptMode === 'basic' ? !prompt.trim() : Object.values(structuredPrompt).every(v => !v?.trim()))}
|
||||
className="w-full py-2.5 bg-lyria-accent text-white text-sm font-medium rounded-md hover:bg-lyria-accent/90 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
||||
className="w-full py-2.5 bg-kithara-accent text-white text-sm font-medium rounded-md hover:bg-kithara-accent/90 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
{isLoading ? 'Generating...' : 'Generate'}
|
||||
</button>
|
||||
|
||||
@@ -167,12 +167,12 @@ export default function Player({ clip, isPlaying, onTogglePlay, onStop, onClose
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="fixed bottom-0 left-0 right-0 bg-lyria-panel border-t border-white/10 px-4 py-3 z-40">
|
||||
<div className="fixed bottom-0 left-0 right-0 bg-kithara-panel border-t border-white/10 px-4 py-3 z-40">
|
||||
<div className="max-w-6xl mx-auto flex items-center gap-3">
|
||||
{/* Play/Pause */}
|
||||
<button
|
||||
onClick={onTogglePlay}
|
||||
className="w-9 h-9 rounded-full bg-lyria-accent flex items-center justify-center text-white hover:bg-lyria-accent/90 transition-colors shrink-0"
|
||||
className="w-9 h-9 rounded-full bg-kithara-accent flex items-center justify-center text-white hover:bg-kithara-accent/90 transition-colors shrink-0"
|
||||
title={isPlaying ? 'Pause' : 'Play'}
|
||||
>
|
||||
{isPlaying ? (
|
||||
@@ -189,7 +189,7 @@ export default function Player({ clip, isPlaying, onTogglePlay, onStop, onClose
|
||||
{/* Stop */}
|
||||
<button
|
||||
onClick={handleStop}
|
||||
className="w-7 h-7 rounded flex items-center justify-center text-lyria-muted hover:text-white hover:bg-white/5 transition-colors shrink-0"
|
||||
className="w-7 h-7 rounded flex items-center justify-center text-kithara-muted hover:text-white hover:bg-white/5 transition-colors shrink-0"
|
||||
title="Stop"
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 24 24">
|
||||
@@ -200,7 +200,7 @@ export default function Player({ clip, isPlaying, onTogglePlay, onStop, onClose
|
||||
{/* Restart — ISO skip backwards to beginning */}
|
||||
<button
|
||||
onClick={handleRestart}
|
||||
className="w-7 h-7 rounded flex items-center justify-center text-lyria-muted hover:text-white hover:bg-white/5 transition-colors shrink-0"
|
||||
className="w-7 h-7 rounded flex items-center justify-center text-kithara-muted hover:text-white hover:bg-white/5 transition-colors shrink-0"
|
||||
title="Skip to beginning"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
|
||||
@@ -213,8 +213,8 @@ export default function Player({ clip, isPlaying, onTogglePlay, onStop, onClose
|
||||
onClick={toggleRepeat}
|
||||
className={`w-7 h-7 rounded flex items-center justify-center transition-colors shrink-0 ${
|
||||
isRepeating
|
||||
? 'text-lyria-accent bg-lyria-accent/10 shadow-[0_0_8px_rgba(255,107,53,0.4)]'
|
||||
: 'text-lyria-muted hover:text-white hover:bg-white/5'
|
||||
? 'text-kithara-accent bg-kithara-accent/10 shadow-[0_0_8px_rgba(255,107,53,0.4)]'
|
||||
: 'text-kithara-muted hover:text-white hover:bg-white/5'
|
||||
}`}
|
||||
title={isRepeating ? 'Repeat on' : 'Repeat off'}
|
||||
>
|
||||
@@ -225,7 +225,7 @@ export default function Player({ clip, isPlaying, onTogglePlay, onStop, onClose
|
||||
|
||||
{/* Time + Scrubber */}
|
||||
<div className="flex-1 min-w-0 flex items-center gap-3">
|
||||
<span className="text-xs text-lyria-muted tabular-nums shrink-0 w-10 text-right">
|
||||
<span className="text-xs text-kithara-muted tabular-nums shrink-0 w-10 text-right">
|
||||
{formatTime(currentTime)}
|
||||
</span>
|
||||
|
||||
@@ -237,7 +237,7 @@ export default function Player({ clip, isPlaying, onTogglePlay, onStop, onClose
|
||||
onMouseUp={handleProgressMouseUp}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-y-0 left-0 bg-lyria-accent rounded-full transition-all duration-75"
|
||||
className="absolute inset-y-0 left-0 bg-kithara-accent rounded-full transition-all duration-75"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
{/* Scrubber handle */}
|
||||
@@ -247,7 +247,7 @@ export default function Player({ clip, isPlaying, onTogglePlay, onStop, onClose
|
||||
/>
|
||||
</div>
|
||||
|
||||
<span className="text-xs text-lyria-muted tabular-nums shrink-0 w-10">
|
||||
<span className="text-xs text-kithara-muted tabular-nums shrink-0 w-10">
|
||||
{formatTime(duration)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -256,7 +256,7 @@ export default function Player({ clip, isPlaying, onTogglePlay, onStop, onClose
|
||||
<div className="hidden sm:flex items-center gap-2.5 min-w-0 max-w-[220px] shrink-0">
|
||||
<button
|
||||
onClick={() => window.open(`/api/clips/${clip.id}/download`, '_blank')}
|
||||
className="text-lyria-muted/60 hover:text-lyria-accent transition-colors shrink-0 mr-2"
|
||||
className="text-kithara-muted/60 hover:text-kithara-accent transition-colors shrink-0 mr-2"
|
||||
title="Download MP3"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -274,7 +274,7 @@ export default function Player({ clip, isPlaying, onTogglePlay, onStop, onClose
|
||||
target.style.display = 'none'
|
||||
const parent = target.parentElement
|
||||
if (parent) {
|
||||
parent.innerHTML = `<svg class='w-4 h-4 text-lyria-accent/40' viewBox='0 0 24 24'><text x='12' y='19' text-anchor='middle' font-size='16' fill='currentColor'>𝄞</text></svg>`
|
||||
parent.innerHTML = `<svg class='w-4 h-4 text-kithara-accent/40' viewBox='0 0 24 24'><text x='12' y='19' text-anchor='middle' font-size='16' fill='currentColor'>𝄞</text></svg>`
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -296,7 +296,7 @@ export default function Player({ clip, isPlaying, onTogglePlay, onStop, onClose
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<button
|
||||
onClick={toggleMute}
|
||||
className="text-lyria-muted hover:text-white transition-colors"
|
||||
className="text-kithara-muted hover:text-white transition-colors"
|
||||
title={isMuted ? 'Unmute' : 'Mute'}
|
||||
>
|
||||
{isMuted || volume === 0 ? (
|
||||
@@ -332,7 +332,7 @@ export default function Player({ clip, isPlaying, onTogglePlay, onStop, onClose
|
||||
{/* Close */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-lyria-muted hover:text-white transition-colors shrink-0"
|
||||
className="text-kithara-muted hover:text-white transition-colors shrink-0"
|
||||
title="Close player"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
||||
@@ -164,7 +164,7 @@ export default function SettingsModal({ isOpen, onClose, onClearWaveforms, onDel
|
||||
<span className="text-sm font-medium text-white">{title}</span>
|
||||
</div>
|
||||
<svg
|
||||
className={`w-4 h-4 text-lyria-muted transition-transform ${gen.expanded ? 'rotate-180' : ''}`}
|
||||
className={`w-4 h-4 text-kithara-muted transition-transform ${gen.expanded ? 'rotate-180' : ''}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -177,9 +177,9 @@ export default function SettingsModal({ isOpen, onClose, onClearWaveforms, onDel
|
||||
<div className="p-4 space-y-4">
|
||||
{/* Model Selection */}
|
||||
<div>
|
||||
<label className="block text-xs text-lyria-muted mb-1.5">Model</label>
|
||||
<label className="block text-xs text-kithara-muted mb-1.5">Model</label>
|
||||
<select
|
||||
className="w-full bg-black/30 border border-white/10 rounded-md px-3 py-2 text-sm text-white focus:outline-none focus:border-lyria-accent"
|
||||
className="w-full bg-black/30 border border-white/10 rounded-md px-3 py-2 text-sm text-white focus:outline-none focus:border-kithara-accent"
|
||||
value={gen.getValue('model')}
|
||||
onChange={(e) => gen.update('model', e.target.value)}
|
||||
>
|
||||
@@ -194,7 +194,7 @@ export default function SettingsModal({ isOpen, onClose, onClearWaveforms, onDel
|
||||
{/* API Key Status */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-2 h-2 rounded-full ${gen.config?.has_api_key ? 'bg-green-400' : 'bg-red-400'}`} />
|
||||
<span className="text-xs text-lyria-muted">
|
||||
<span className="text-xs text-kithara-muted">
|
||||
{gen.config?.has_api_key ? 'API key configured' : 'No API key — set in .env'}
|
||||
</span>
|
||||
</div>
|
||||
@@ -209,9 +209,9 @@ export default function SettingsModal({ isOpen, onClose, onClearWaveforms, onDel
|
||||
{ key: 'safety_dangerous', label: 'Dangerous Content' },
|
||||
].map(({ key, label }) => (
|
||||
<div key={key}>
|
||||
<label className="block text-xs text-lyria-muted mb-1">{label}</label>
|
||||
<label className="block text-xs text-kithara-muted mb-1">{label}</label>
|
||||
<select
|
||||
className="w-full bg-black/30 border border-white/10 rounded-md px-3 py-1.5 text-sm text-white focus:outline-none focus:border-lyria-accent"
|
||||
className="w-full bg-black/30 border border-white/10 rounded-md px-3 py-1.5 text-sm text-white focus:outline-none focus:border-kithara-accent"
|
||||
value={gen.getValue(key as keyof GenConfig)}
|
||||
onChange={(e) => gen.update(key, e.target.value)}
|
||||
>
|
||||
@@ -228,7 +228,7 @@ export default function SettingsModal({ isOpen, onClose, onClearWaveforms, onDel
|
||||
<button
|
||||
onClick={gen.save}
|
||||
disabled={!gen.hasChanges || gen.saving}
|
||||
className="px-4 py-2 text-sm font-medium rounded-md transition-colors disabled:opacity-50 bg-lyria-accent text-black hover:bg-lyria-accent/90"
|
||||
className="px-4 py-2 text-sm font-medium rounded-md transition-colors disabled:opacity-50 bg-kithara-accent text-black hover:bg-kithara-accent/90"
|
||||
>
|
||||
{gen.saving ? 'Saving...' : 'Save Changes'}
|
||||
</button>
|
||||
@@ -238,7 +238,7 @@ export default function SettingsModal({ isOpen, onClose, onClearWaveforms, onDel
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-lyria-muted/60">
|
||||
<p className="text-xs text-kithara-muted/60">
|
||||
Changes apply immediately but reset on server restart. Set .env for persistence.
|
||||
</p>
|
||||
</div>
|
||||
@@ -255,13 +255,13 @@ export default function SettingsModal({ isOpen, onClose, onClearWaveforms, onDel
|
||||
/>
|
||||
|
||||
{/* Modal */}
|
||||
<div className="relative bg-lyria-surface border border-white/10 rounded-xl shadow-2xl w-full max-w-md mx-4 overflow-hidden max-h-[80vh] flex flex-col">
|
||||
<div className="relative bg-kithara-surface border border-white/10 rounded-xl shadow-2xl w-full max-w-md mx-4 overflow-hidden max-h-[80vh] flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-white/10 shrink-0">
|
||||
<h2 className="text-lg font-semibold text-white">Settings</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-lyria-muted hover:text-white transition-colors"
|
||||
className="text-kithara-muted hover:text-white transition-colors"
|
||||
aria-label="Close settings"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -274,14 +274,14 @@ export default function SettingsModal({ isOpen, onClose, onClearWaveforms, onDel
|
||||
<div className="px-6 py-5 space-y-6 overflow-y-auto">
|
||||
{/* Image Generation */}
|
||||
{renderGenSection('Image Generation', (
|
||||
<svg className="w-4 h-4 text-lyria-accent" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg className="w-4 h-4 text-kithara-accent" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
), imageGen)}
|
||||
|
||||
{/* Text Generation */}
|
||||
{renderGenSection('Text Generation', (
|
||||
<svg className="w-4 h-4 text-lyria-accent" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg className="w-4 h-4 text-kithara-accent" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" />
|
||||
</svg>
|
||||
), textGen)}
|
||||
@@ -314,7 +314,7 @@ export default function SettingsModal({ isOpen, onClose, onClearWaveforms, onDel
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm text-white font-medium">Clear Waveform Cache</p>
|
||||
<p className="text-xs text-lyria-muted mt-0.5">Delete all cached SVGs. They regenerate on next view.</p>
|
||||
<p className="text-xs text-kithara-muted mt-0.5">Delete all cached SVGs. They regenerate on next view.</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleClearWaveforms}
|
||||
@@ -331,7 +331,7 @@ export default function SettingsModal({ isOpen, onClose, onClearWaveforms, onDel
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm text-white font-medium">Delete All Tracks</p>
|
||||
<p className="text-xs text-lyria-muted mt-0.5">
|
||||
<p className="text-xs text-kithara-muted mt-0.5">
|
||||
Remove {clipCount} clip{clipCount !== 1 ? 's' : ''}. Cannot be undone.
|
||||
</p>
|
||||
{confirmDelete && (
|
||||
|
||||
@@ -51,12 +51,12 @@ export default function StructuredPromptEditor({ value, onChange }: Props) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-lyria-muted">
|
||||
<span className="text-xs text-kithara-muted">
|
||||
{filledCount} of {FIELDS.length} fields filled
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setExpanded(new Set(FIELDS.map(f => f.key)))}
|
||||
className="text-xs text-lyria-accent hover:text-lyria-accent/80 transition-colors"
|
||||
className="text-xs text-kithara-accent hover:text-kithara-accent/80 transition-colors"
|
||||
>
|
||||
Expand All
|
||||
</button>
|
||||
@@ -75,11 +75,11 @@ export default function StructuredPromptEditor({ value, onChange }: Props) {
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`w-1.5 h-1.5 rounded-full ${hasValue ? 'bg-lyria-accent' : 'bg-lyria-muted/30'}`} />
|
||||
<span className={hasValue ? 'text-white' : 'text-lyria-muted'}>{label}</span>
|
||||
<span className={`w-1.5 h-1.5 rounded-full ${hasValue ? 'bg-kithara-accent' : 'bg-kithara-muted/30'}`} />
|
||||
<span className={hasValue ? 'text-white' : 'text-kithara-muted'}>{label}</span>
|
||||
</div>
|
||||
<svg
|
||||
className={`w-3.5 h-3.5 text-lyria-muted transition-transform ${isExpanded ? 'rotate-180' : ''}`}
|
||||
className={`w-3.5 h-3.5 text-kithara-muted transition-transform ${isExpanded ? 'rotate-180' : ''}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -94,7 +94,7 @@ export default function StructuredPromptEditor({ value, onChange }: Props) {
|
||||
value={value[key as keyof PromptStructure] || ''}
|
||||
onChange={(e) => updateField(key, e.target.value)}
|
||||
placeholder={placeholder}
|
||||
className="w-full h-20 bg-black/20 border border-white/5 rounded-md px-3 py-2 text-sm text-white placeholder:text-lyria-muted/30 focus:outline-none focus:border-lyria-accent/50 resize-none"
|
||||
className="w-full h-20 bg-black/20 border border-white/5 rounded-md px-3 py-2 text-sm text-white placeholder:text-kithara-muted/30 focus:outline-none focus:border-kithara-accent/50 resize-none"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -53,16 +53,16 @@ export default function SuggestionsModal({ isOpen, onClose, onSelect, userInput
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
<div className="relative bg-lyria-surface border border-white/10 rounded-xl shadow-2xl w-full max-w-lg mx-4 overflow-hidden max-h-[80vh] flex flex-col">
|
||||
<div className="relative bg-kithara-surface border border-white/10 rounded-xl shadow-2xl w-full max-w-lg mx-4 overflow-hidden max-h-[80vh] flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-white/10 shrink-0">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-white">Prompt Suggestions</h2>
|
||||
<p className="text-xs text-lyria-muted mt-0.5 truncate max-w-[280px]">{userInput}</p>
|
||||
<p className="text-xs text-kithara-muted mt-0.5 truncate max-w-[280px]">{userInput}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-lyria-muted hover:text-white transition-colors"
|
||||
className="text-kithara-muted hover:text-white transition-colors"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
@@ -74,7 +74,7 @@ export default function SuggestionsModal({ isOpen, onClose, onSelect, userInput
|
||||
<div className="px-6 py-5 overflow-y-auto">
|
||||
{loading && (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<svg className="animate-spin w-6 h-6 text-lyria-accent" fill="none" viewBox="0 0 24 24">
|
||||
<svg className="animate-spin w-6 h-6 text-kithara-accent" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
||||
</svg>
|
||||
@@ -86,7 +86,7 @@ export default function SuggestionsModal({ isOpen, onClose, onSelect, userInput
|
||||
<p className="text-sm text-red-400">{error}</p>
|
||||
<button
|
||||
onClick={fetchSuggestions}
|
||||
className="mt-2 text-xs text-lyria-accent hover:text-lyria-accent/80"
|
||||
className="mt-2 text-xs text-kithara-accent hover:text-kithara-accent/80"
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
@@ -95,10 +95,10 @@ export default function SuggestionsModal({ isOpen, onClose, onSelect, userInput
|
||||
|
||||
{!loading && !error && suggestions.length === 0 && (
|
||||
<div className="text-center py-8">
|
||||
<p className="text-sm text-lyria-muted">No suggestions yet. Click below to generate.</p>
|
||||
<p className="text-sm text-kithara-muted">No suggestions yet. Click below to generate.</p>
|
||||
<button
|
||||
onClick={fetchSuggestions}
|
||||
className="mt-3 px-4 py-2 text-sm bg-lyria-accent text-black rounded-md hover:bg-lyria-accent/90"
|
||||
className="mt-3 px-4 py-2 text-sm bg-kithara-accent text-black rounded-md hover:bg-kithara-accent/90"
|
||||
>
|
||||
Get Suggestions
|
||||
</button>
|
||||
@@ -113,13 +113,13 @@ export default function SuggestionsModal({ isOpen, onClose, onSelect, userInput
|
||||
onSelect(suggestion)
|
||||
onClose()
|
||||
}}
|
||||
className="w-full text-left p-4 bg-white/5 hover:bg-white/10 border border-white/5 hover:border-lyria-accent/30 rounded-lg transition-all group"
|
||||
className="w-full text-left p-4 bg-white/5 hover:bg-white/10 border border-white/5 hover:border-kithara-accent/30 rounded-lg transition-all group"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="text-lyria-accent font-mono text-sm shrink-0">{i + 1}.</span>
|
||||
<span className="text-kithara-accent font-mono text-sm shrink-0">{i + 1}.</span>
|
||||
<p className="text-sm text-white/90 leading-relaxed">{suggestion}</p>
|
||||
</div>
|
||||
<div className="mt-2 text-xs text-lyria-accent opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<div className="mt-2 text-xs text-kithara-accent opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
Click to use this prompt →
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -7,12 +7,12 @@ export default {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
'lyria-dark': '#0a0a0f',
|
||||
'lyria-panel': '#12121a',
|
||||
'lyria-surface': '#1a1a24',
|
||||
'lyria-accent': '#ff6b35',
|
||||
'lyria-text': '#e0e0e0',
|
||||
'lyria-muted': '#6b6b7b',
|
||||
'kithara-dark': '#0a0a0f',
|
||||
'kithara-panel': '#12121a',
|
||||
'kithara-surface': '#1a1a24',
|
||||
'kithara-accent': '#ff6b35',
|
||||
'kithara-text': '#e0e0e0',
|
||||
'kithara-muted': '#6b6b7b',
|
||||
},
|
||||
keyframes: {
|
||||
'marquee-scroll': {
|
||||
|
||||
Reference in new issue
Block a user