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:
rin.kagamine committed 2026-05-31 09:45:22 -04:00
1 parent 6c1e1b4509
commit 816f8fbd2f
20 files changed
+191 -193

No files matched your search

+5 -5
View File
@@ -1,4 +1,4 @@
# Lyria Studio # Kithara Studio
A webapp for generating, playing, and storing music clips using Google's Lyria music generation models. A webapp for generating, playing, and storing music clips using Google's Lyria music generation models.
@@ -23,7 +23,7 @@ npm run dev
## Architecture ## 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 - **Frontend**: React + Vite + TypeScript + Tailwind — dark theme, audio player
- **Storage**: SQLite for clip metadata, local filesystem for MP3 files - **Storage**: SQLite for clip metadata, local filesystem for MP3 files
@@ -40,9 +40,9 @@ npm run dev
## Models ## Models
- **v0.1**: Lyria 3 Clip Preview (30s, REST) - **v0.1**: Google Lyria 3 Clip Preview (30s, REST)
- **v0.2**: Lyria 3 Pro (structure-aware, ~3min) - **v0.2**: Google Lyria 3 Pro (structure-aware, ~3min)
- **v0.3**: Lyria RealTime (WebSocket streaming) - **v0.3**: Google Lyria RealTime (WebSocket streaming)
## License ## License
+1 -1
View File
@@ -4,7 +4,7 @@ import uuid
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path 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) DB_PATH.parent.mkdir(parents=True, exist_ok=True)
# Timezone from TZ env var (default: America/New_York) # Timezone from TZ env var (default: America/New_York)
@@ -4,7 +4,7 @@ import os
import logging import logging
from pathlib import Path from pathlib import Path
logger = logging.getLogger("lyria_client") logger = logging.getLogger("kithara_client")
LYRIA_API_KEY = os.getenv("LYRIA_API_KEY", "") LYRIA_API_KEY = os.getenv("LYRIA_API_KEY", "")
LYRIA_BASE_URL = "https://generativelanguage.googleapis.com/v1beta" 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", "") block_reason = data.get("blockReason", "")
if block_reason: if block_reason:
block_message = data.get("blockReasonMessage", "") 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})"} 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 # 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. # audio is in inlineData of a subsequent part.
candidates = data.get("candidates", []) candidates = data.get("candidates", [])
if not 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)) error_detail = e.response.json().get("error", {}).get("message", str(e))
except: except:
error_detail = str(e) 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: except Exception as e:
return {"success": False, "error": f"Client error: {str(e)}"} return {"success": False, "error": f"Client error: {str(e)}"}
+4 -4
View File
@@ -6,18 +6,18 @@ import os
import shutil import shutil
from dotenv import load_dotenv 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") load_dotenv(Path(__file__).parent.parent / ".env")
from models import GenerateRequest, GenerateResponse, ClipList, RenameRequest, MetadataRequest, GenerateArtworkRequest, LYRIA_MODELS, DEFAULT_MODEL 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 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 waveform import generate_waveform_svg, get_waveform_path
from analysis import analyze_clip from analysis import analyze_clip
from image_gen import generate_artwork, get_image_gen_config, get_available_models, SAFETY_LEVELS 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 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( app.add_middleware(
CORSMiddleware, CORSMiddleware,
@@ -49,7 +49,7 @@ async def list_models():
return {"models": LYRIA_MODELS, "default": DEFAULT_MODEL} return {"models": LYRIA_MODELS, "default": DEFAULT_MODEL}
async def _generate_and_save(clip_id: str, prompt: str, model: str, duration: int): 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, DR-LYRIA-001: Pre-generates waveform SVG after clip is ready,
so users never wait for on-demand generation. so users never wait for on-demand generation.
+2 -2
View File
@@ -1,4 +1,4 @@
"""Shared test fixtures for Lyria Studio backend.""" """Shared test fixtures for Kithara Studio backend."""
import asyncio import asyncio
import os import os
@@ -39,7 +39,7 @@ async def app_client(tmp_path):
waveforms_dir.mkdir() waveforms_dir.mkdir()
artwork_dir = data_dir / "artwork" artwork_dir = data_dir / "artwork"
artwork_dir.mkdir() 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 # 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) # (image_gen.py reads ARTWORK_DIR from env, main.py uses __file__-relative paths)
+1 -1
View File
@@ -1,4 +1,4 @@
"""Tests for Lyria Studio API endpoints.""" """Tests for Kithara Studio API endpoints."""
import subprocess import subprocess
import sys import sys
+2 -2
View File
@@ -4,9 +4,9 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lyria Studio</title> <title>Kithara Studio</title>
</head> </head>
<body class="bg-lyria-dark text-lyria-text"> <body class="bg-kithara-dark text-kithara-text">
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
</body> </body>
+2 -2
View File
@@ -1,11 +1,11 @@
{ {
"name": "lyria-studio-frontend", "name": "kithara-studio-frontend",
"version": "0.1.0", "version": "0.1.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "lyria-studio-frontend", "name": "kithara-studio-frontend",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"fuse.js": "^7.0.0", "fuse.js": "^7.0.0",
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"name": "lyria-studio-frontend", "name": "kithara-studio-frontend",
"private": true, "private": true,
"version": "0.1.0", "version": "0.1.0",
"type": "module", "type": "module",
Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

+5 -7
View File
@@ -237,19 +237,17 @@ function App() {
} }
return ( 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"> <header className="border-b border-white/10 px-6 py-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-3"> <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"> <img src="/logo.png" alt="Kithara Studio" className="w-8 h-8 invert" />
L <h1 className="text-xl font-bold text-white">Kithara Studio</h1>
</div> <span className="text-xs text-kithara-muted bg-white/5 px-2 py-0.5 rounded">v0.1</span>
<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>
</div> </div>
<button <button
onClick={() => setSettingsOpen(true)} 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" title="Settings"
aria-label="Open settings" aria-label="Open settings"
> >
@@ -41,17 +41,17 @@ export default function ArtworkGenerationModal({ isOpen, onClose, onGenerate, tr
/> />
{/* Modal */} {/* 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 */} {/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-white/10"> <div className="flex items-center justify-between px-6 py-4 border-b border-white/10">
<div> <div>
<h2 className="text-lg font-semibold text-white">Generate Artwork</h2> <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> </div>
<button <button
onClick={handleClose} onClick={handleClose}
disabled={loading} 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" aria-label="Close"
> >
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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} value={prompt}
onChange={(e) => setPrompt(e.target.value)} onChange={(e) => setPrompt(e.target.value)}
placeholder="e.g., A cyberpunk cityscape at night with neon lights reflecting on wet streets..." 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} 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. Uses Gemini Image model. Be descriptive for best results.
</p> </p>
</div> </div>
@@ -82,7 +82,7 @@ export default function ArtworkGenerationModal({ isOpen, onClose, onGenerate, tr
<button <button
onClick={handleGenerate} onClick={handleGenerate}
disabled={!prompt.trim() || loading} 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 ? ( {loading ? (
<span className="flex items-center justify-center gap-2"> <span className="flex items-center justify-center gap-2">
+32 -32
View File
@@ -98,7 +98,7 @@ export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, o
case 'ready': return 'bg-green-500/20 text-green-400' case 'ready': return 'bg-green-500/20 text-green-400'
case 'generating': return 'bg-yellow-500/20 text-yellow-400' case 'generating': return 'bg-yellow-500/20 text-yellow-400'
case 'failed': return 'bg-red-500/20 text-red-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} onClick={onClose}
/> />
{/* Panel */} {/* 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 */} {/* 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> <h2 className="text-sm font-semibold text-white uppercase tracking-wider">Track Details</h2>
<button <button
onClick={onClose} 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"> <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" /> <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 */} {/* Album Artwork */}
<div> <div>
<div className="flex items-center justify-between mb-2"> <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"> <div className="flex items-center gap-2">
<button <button
onClick={() => onGenerateArtwork?.(clip)} 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 Generate
</button> </button>
<button <button
onClick={() => artworkInputRef.current?.click()} 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'} {clip.artwork_filename ? 'Change' : 'Upload'}
</button> </button>
@@ -191,7 +191,7 @@ export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, o
</div> </div>
) : ( ) : (
<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={() => { onClick={() => {
if (currentClipId === clip.id && isPlaying) { if (currentClipId === clip.id && isPlaying) {
onTogglePlay?.() 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"> <svg className="w-14 h-14" viewBox="0 0 24 24">
<text x="12" y="19" textAnchor="middle" fontSize="20" fill="currentColor">𝄞</text> <text x="12" y="19" textAnchor="middle" fontSize="20" fill="currentColor">𝄞</text>
</svg> </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"> <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 ? ( {currentClipId === clip.id && isPlaying ? (
<svg className="w-12 h-12 text-white" fill="currentColor" viewBox="0 0 24 24"> <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 */} {/* Title — inline editable */}
<div> <div>
<div className="flex items-center justify-between mb-1"> <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 && ( {!isEditingTitle && (
<button <button
onClick={startEditing} 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 Rename
</button> </button>
@@ -264,7 +264,7 @@ export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, o
onChange={(e) => setEditTitle(e.target.value)} onChange={(e) => setEditTitle(e.target.value)}
onBlur={saveTitle} onBlur={saveTitle}
onKeyDown={handleTitleKeyDown} 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..." placeholder="Track title..."
/> />
) : ( ) : (
@@ -272,14 +272,14 @@ export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, o
onClick={startEditing} onClick={startEditing}
title="Click to rename" 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> </p>
)} )}
</div> </div>
{/* Prompt */} {/* Prompt */}
<div> <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"> <p className="text-sm text-white/90 whitespace-pre-wrap break-words bg-white/5 rounded-md p-3">
{clip.prompt} {clip.prompt}
</p> </p>
@@ -288,7 +288,7 @@ export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, o
{/* Structured Prompt Breakdown */} {/* Structured Prompt Breakdown */}
{clip.prompt_structure && ( {clip.prompt_structure && (
<div> <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"> <div className="space-y-2">
{[ {[
{ key: 'style', label: 'Style' }, { key: 'style', label: 'Style' },
@@ -304,7 +304,7 @@ export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, o
if (!val) return null if (!val) return null
return ( return (
<div key={key} className="bg-white/5 rounded-md p-2"> <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> <span className="text-sm text-white/90">{val}</span>
</div> </div>
) )
@@ -315,7 +315,7 @@ export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, o
{/* Status */} {/* Status */}
<div> <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)}`}> <span className={`text-xs px-2 py-1 rounded ${statusColor(clip.status)}`}>
{statusLabel(clip.status)} {statusLabel(clip.status)}
</span> </span>
@@ -324,7 +324,7 @@ export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, o
{/* Error message */} {/* Error message */}
{clip.status === 'failed' && clip.error_message && ( {clip.status === 'failed' && clip.error_message && (
<div> <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"> <p className="text-sm text-red-400/90 bg-red-500/10 rounded-md p-3 break-words">
{clip.error_message} {clip.error_message}
</p> </p>
@@ -334,16 +334,16 @@ export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, o
{/* Metadata grid */} {/* Metadata grid */}
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<div> <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> <p className="text-sm text-white">{modelName(clip.model)}</p>
</div> </div>
<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> <p className="text-sm text-white">{clip.duration_seconds}s</p>
</div> </div>
{clip.file_size_bytes && ( {clip.file_size_bytes && (
<div> <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> <p className="text-sm text-white">{(clip.file_size_bytes / 1024).toFixed(0)} KB</p>
</div> </div>
)} )}
@@ -351,7 +351,7 @@ export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, o
const c = getBPMColor(clip.bpm) const c = getBPMColor(clip.bpm)
return c ? ( return c ? (
<div> <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 <span
className="text-sm px-2 py-0.5 rounded font-medium inline-block" 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}` }} 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) const c = getKeyColor(clip.key)
return c ? ( return c ? (
<div> <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 <span
className="text-sm px-2 py-0.5 rounded font-medium inline-block" 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}` }} 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 ) : null
})()} })()}
<div> <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> <p className="text-sm text-white">{formatDate(clip.created_at)}</p>
</div> </div>
</div> </div>
@@ -405,8 +405,8 @@ export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, o
{/* ID (for reference) */} {/* ID (for reference) */}
<div> <div>
<label className="text-xs text-lyria-muted uppercase tracking-wider mb-1 block">Clip ID</label> <label className="text-xs text-kithara-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> <p className="text-xs text-kithara-muted/60 font-mono break-all">{clip.id}</p>
</div> </div>
{/* Actions */} {/* Actions */}
@@ -414,7 +414,7 @@ export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, o
<a <a
href={`/api/clips/${clip.id}/download`} href={`/api/clips/${clip.id}/download`}
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"> <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" /> <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) { if (editing) {
return ( return (
<div> <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"> <div className="flex items-center gap-2">
<input <input
ref={inputRef} ref={inputRef}
@@ -479,7 +479,7 @@ function MetadataField({ label, value, placeholder, onSave }: {
}} }}
onBlur={save} onBlur={save}
placeholder={placeholder} 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>
</div> </div>
@@ -491,13 +491,13 @@ function MetadataField({ label, value, placeholder, onSave }: {
className="group cursor-pointer" className="group cursor-pointer"
onClick={() => setEditing(true)} 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"> <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} {value || placeholder}
</span> </span>
<svg <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" fill="none"
stroke="currentColor" stroke="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"
+61 -61
View File
@@ -71,7 +71,7 @@ function HighlightText({ text, query }: { text: string; query: string }) {
<> <>
{parts.map((part, i) => {parts.map((part, i) =>
part.toLowerCase() === query.toLowerCase() ? ( 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> <span key={i}>{part}</span>
) )
@@ -104,7 +104,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
// Saved searches // Saved searches
const [savedSearches, setSavedSearches] = useState<SavedSearch[]>(() => { const [savedSearches, setSavedSearches] = useState<SavedSearch[]>(() => {
try { try {
const raw = localStorage.getItem('lyria-saved-searches') const raw = localStorage.getItem('kithara-saved-searches')
return raw ? JSON.parse(raw) : [] return raw ? JSON.parse(raw) : []
} catch { } catch {
return [] return []
@@ -195,7 +195,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
} }
const updated = [...savedSearches, newSearch] const updated = [...savedSearches, newSearch]
setSavedSearches(updated) setSavedSearches(updated)
localStorage.setItem('lyria-saved-searches', JSON.stringify(updated)) localStorage.setItem('kithara-saved-searches', JSON.stringify(updated))
}, [search, filters, savedSearches]) }, [search, filters, savedSearches])
const loadSearch = useCallback((saved: SavedSearch) => { 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 deleteSearch = useCallback((id: string) => {
const updated = savedSearches.filter(s => s.id !== id) const updated = savedSearches.filter(s => s.id !== id)
setSavedSearches(updated) setSavedSearches(updated)
localStorage.setItem('lyria-saved-searches', JSON.stringify(updated)) localStorage.setItem('kithara-saved-searches', JSON.stringify(updated))
}, [savedSearches]) }, [savedSearches])
const clearFilters = () => { const clearFilters = () => {
@@ -261,14 +261,14 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
if (clips.length === 0) { if (clips.length === 0) {
return ( return (
<div className="bg-lyria-panel rounded-lg p-8 border border-white/5 text-center"> <div className="bg-kithara-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> <p className="text-kithara-muted text-sm">No clips yet. Generate your first one!</p>
</div> </div>
) )
} }
return ( 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 */} {/* Header: title, search, view toggle */}
<div className="px-5 py-4 border-b border-white/5"> <div className="px-5 py-4 border-b border-white/5">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@@ -279,7 +279,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
{/* Search bar */} {/* Search bar */}
<div className="relative flex-1 max-w-xs"> <div className="relative flex-1 max-w-xs">
<svg <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" 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" /> <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..." placeholder="Search tracks, artists, albums..."
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} 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 && ( {search && (
<button <button
onClick={() => setSearch('')} 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"> <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" /> <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)} onClick={() => setShowFilters(!showFilters)}
className={`p-1.5 rounded transition-colors ${ className={`p-1.5 rounded transition-colors ${
showFilters || hasActiveFilters showFilters || hasActiveFilters
? 'bg-lyria-accent/20 text-lyria-accent border border-lyria-accent/30' ? 'bg-kithara-accent/20 text-kithara-accent border border-kithara-accent/30'
: 'text-lyria-muted hover:text-white border border-white/10' : 'text-kithara-muted hover:text-white border border-white/10'
}`} }`}
title="Filters" title="Filters"
> >
@@ -321,7 +321,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
{/* Saved searches button */} {/* Saved searches button */}
<button <button
onClick={() => setShowSaved(!showSaved)} 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" title="Saved searches"
> >
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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"> <div className="flex bg-white/5 rounded-md border border-white/10 overflow-hidden">
<button <button
onClick={() => setView('ribbons')} 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" title="List view"
> >
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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>
<button <button
onClick={() => setView('tiles')} 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" title="Tile view"
> >
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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 && ( {hasActiveFilters && (
<div className="flex items-center gap-2 flex-wrap mt-2"> <div className="flex items-center gap-2 flex-wrap mt-2">
{filters.model !== 'all' && ( {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} Model: {filters.model}
</span> </span>
)} )}
{filters.status !== 'all' && ( {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} Status: {filters.status}
</span> </span>
)} )}
{filters.hasArtwork !== 'all' && ( {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} Artwork: {filters.hasArtwork}
</span> </span>
)} )}
{(filters.bpmMin || filters.bpmMax) && ( {(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 || '∞'} BPM: {filters.bpmMin || '0'} - {filters.bpmMax || '∞'}
</span> </span>
)} )}
{(filters.dateFrom || filters.dateTo) && ( {(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 || '...'} Date: {filters.dateFrom || '...'} → {filters.dateTo || '...'}
</span> </span>
)} )}
<button <button
onClick={clearFilters} 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 Clear all
</button> </button>
@@ -391,7 +391,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
{/* Results count */} {/* Results count */}
{(search || hasActiveFilters) && ( {(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 Showing {filtered.length} of {clips.length} clips
</p> </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="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 className="grid grid-cols-2 md:grid-cols-4 gap-3">
<div> <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 <select
value={filters.model} value={filters.model}
onChange={(e) => setFilters(f => ({ ...f, model: e.target.value }))} onChange={(e) => setFilters(f => ({ ...f, model: e.target.value }))}
@@ -414,7 +414,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
</select> </select>
</div> </div>
<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 <select
value={filters.status} value={filters.status}
onChange={(e) => setFilters(f => ({ ...f, status: e.target.value }))} onChange={(e) => setFilters(f => ({ ...f, status: e.target.value }))}
@@ -427,7 +427,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
</select> </select>
</div> </div>
<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 <select
value={filters.hasArtwork} value={filters.hasArtwork}
onChange={(e) => setFilters(f => ({ ...f, hasArtwork: e.target.value }))} onChange={(e) => setFilters(f => ({ ...f, hasArtwork: e.target.value }))}
@@ -439,29 +439,29 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
</select> </select>
</div> </div>
<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"> <div className="flex items-center gap-2">
<input <input
type="number" type="number"
value={filters.bpmMin} value={filters.bpmMin}
onChange={(e) => setFilters(f => ({ ...f, bpmMin: e.target.value }))} onChange={(e) => setFilters(f => ({ ...f, bpmMin: e.target.value }))}
placeholder="Min" 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 <input
type="number" type="number"
value={filters.bpmMax} value={filters.bpmMax}
onChange={(e) => setFilters(f => ({ ...f, bpmMax: e.target.value }))} onChange={(e) => setFilters(f => ({ ...f, bpmMax: e.target.value }))}
placeholder="Max" 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>
</div> </div>
<div className="grid grid-cols-2 gap-3 mt-3"> <div className="grid grid-cols-2 gap-3 mt-3">
<div> <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 <input
type="date" type="date"
value={filters.dateFrom} value={filters.dateFrom}
@@ -470,7 +470,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
/> />
</div> </div>
<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 <input
type="date" type="date"
value={filters.dateTo} 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"> <div className="flex items-center justify-between mt-3">
<button <button
onClick={clearFilters} 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 Reset all filters
</button> </button>
<button <button
onClick={saveSearch} 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 💾 Save this search 💾
</button> </button>
@@ -499,19 +499,19 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
{/* Saved searches dropdown */} {/* Saved searches dropdown */}
{showSaved && savedSearches.length > 0 && ( {showSaved && savedSearches.length > 0 && (
<div className="px-5 py-3 border-b border-white/5 bg-white/5 space-y-2"> <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 => ( {savedSearches.map(saved => (
<div key={saved.id} className="flex items-center justify-between group"> <div key={saved.id} className="flex items-center justify-between group">
<button <button
onClick={() => loadSearch(saved)} 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.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>
<button <button
onClick={() => deleteSearch(saved.id)} 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"> <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" /> <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 */} {/* Clip list */}
{filtered.length === 0 ? ( {filtered.length === 0 ? (
<div className="px-5 py-8 text-center"> <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> </div>
) : view === 'ribbons' ? ( ) : view === 'ribbons' ? (
/* ── RIBBON VIEW ── */ /* ── RIBBON VIEW ── */
@@ -546,7 +546,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
className="w-10 h-10 rounded object-cover" 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"> <svg className="w-5 h-5" viewBox="0 0 24 24">
<text x="12" y="19" textAnchor="middle" fontSize="20" fill="currentColor">𝄞</text> <text x="12" y="19" textAnchor="middle" fontSize="20" fill="currentColor">𝄞</text>
</svg> </svg>
@@ -556,7 +556,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
> >
<button <button
onClick={(e) => { e.stopPropagation(); onPlay(clip) }} 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" title="Play"
> >
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"> <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} onBlur={commitRename}
placeholder="Enter a name..." 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 autoFocus
/> />
) : clip.title ? ( ) : 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"> <p className="text-sm font-medium text-white truncate">
<HighlightText text={clip.title} query={debouncedSearch} /> <HighlightText text={clip.title} query={debouncedSearch} />
</p> </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} /> <HighlightText text={clip.prompt} query={debouncedSearch} />
</p> </p>
</div> </div>
@@ -606,9 +606,9 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
}`}> }`}>
{clip.status} {clip.status}
</span> </span>
<span className="text-xs text-lyria-muted">{clip.duration_seconds}s</span> <span className="text-xs text-kithara-muted">{clip.duration_seconds}s</span>
<span className="text-xs text-lyria-muted">{modelName(clip.model)}</span> <span className="text-xs text-kithara-muted">{modelName(clip.model)}</span>
<span className="text-xs text-lyria-muted">{formatDate(clip.created_at)}</span> <span className="text-xs text-kithara-muted">{formatDate(clip.created_at)}</span>
{clip.bpm && (() => { {clip.bpm && (() => {
const c = getBPMColor(clip.bpm) const c = getBPMColor(clip.bpm)
return c ? ( return c ? (
@@ -645,7 +645,7 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
<button <button
onClick={(e) => { e.stopPropagation(); startRename(clip) }} 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" title="Rename clip"
> >
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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) setUploadingId(clip.id)
setTimeout(() => artworkInputRef.current?.click(), 0) 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" title="Upload album art"
> >
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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() e.stopPropagation()
window.open(`/api/clips/${clip.id}/download`, '_blank') 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" title="Download MP3"
> >
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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>
<button <button
onClick={(e) => { e.stopPropagation(); onDelete(clip.id) }} 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" title="Delete clip"
> >
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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) => ( {paged.map((clip) => (
<div <div
key={clip.id} 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 ${ 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-lyria-accent/50' : '' currentClipId === clip.id ? 'border-kithara-accent/50' : ''
}`} }`}
onClick={() => onSelect(clip)} 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" 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"> <svg className="w-10 h-10" viewBox="0 0 24 24">
<text x="12" y="19" textAnchor="middle" fontSize="20" fill="currentColor">𝄞</text> <text x="12" y="19" textAnchor="middle" fontSize="20" fill="currentColor">𝄞</text>
</svg> </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"> <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 <button
onClick={(e) => { e.stopPropagation(); onPlay(clip) }} 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" title="Play"
> >
<svg className="w-8 h-8" fill="currentColor" viewBox="0 0 24 24"> <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) setUploadingId(clip.id)
setTimeout(() => artworkInputRef.current?.click(), 0) 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" title="Upload album art"
> >
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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} onBlur={commitRename}
placeholder="Enter a name..." 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 autoFocus
/> />
) : ( ) : (
@@ -771,8 +771,8 @@ export default function Gallery({ clips, onPlay, onDelete, onRename, onSelect, o
}`}> }`}>
{clip.status} {clip.status}
</span> </span>
<span className="text-[10px] text-lyria-muted">{clip.duration_seconds}s</span> <span className="text-[10px] text-kithara-muted">{clip.duration_seconds}s</span>
<span className="text-[10px] text-lyria-muted">{modelName(clip.model)}</span> <span className="text-[10px] text-kithara-muted">{modelName(clip.model)}</span>
{clip.bpm && (() => { {clip.bpm && (() => {
const c = getBPMColor(clip.bpm) const c = getBPMColor(clip.bpm)
return c ? ( 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"> <div className="flex items-center gap-1 mt-2 pt-1.5 border-t border-white/5">
<button <button
onClick={(e) => { e.stopPropagation(); startRename(clip) }} 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" title="Rename"
> >
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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() e.stopPropagation()
window.open(`/api/clips/${clip.id}/download`, '_blank') 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" title="Download MP3"
> >
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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>
<button <button
onClick={(e) => { e.stopPropagation(); onDelete(clip.id) }} 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" title="Delete"
> >
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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 */} {/* Pagination */}
{totalPages > 1 && ( {totalPages > 1 && (
<div className="px-5 py-3 border-t border-white/5 flex items-center justify-between"> <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' : ''}) Page {page} of {totalPages} ({filtered.length} clip{filtered.length !== 1 ? 's' : ''})
</p> </p>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
+16 -16
View File
@@ -107,7 +107,7 @@ export default function Generator({ onGenerate }: Props) {
} }
return ( 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"> <h2 className="text-sm font-semibold text-white mb-4 uppercase tracking-wider">
Generate Generate
</h2> </h2>
@@ -116,13 +116,13 @@ export default function Generator({ onGenerate }: Props) {
{/* Model & Duration */} {/* Model & Duration */}
<div className="flex gap-4"> <div className="flex gap-4">
<div className="flex-1"> <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 Model
</label> </label>
<select <select
value={model} value={model}
onChange={(e) => handleModelChange(e.target.value)} 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]) => ( {Object.entries(models).map(([key, info]) => (
<option key={key} value={key}> <option key={key} value={key}>
@@ -133,7 +133,7 @@ export default function Generator({ onGenerate }: Props) {
</div> </div>
<div className="w-28"> <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) Duration ({duration}s)
</label> </label>
<input <input
@@ -142,12 +142,12 @@ export default function Generator({ onGenerate }: Props) {
max={maxDuration} max={maxDuration}
value={duration} value={duration}
onChange={(e) => setDuration(Number(e.target.value))} 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> </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>{currentModelInfo?.name || 'Clip Preview'}</span>
<span>{duration}s / {maxDuration}s max</span> <span>{duration}s / {maxDuration}s max</span>
</div> </div>
@@ -160,8 +160,8 @@ export default function Generator({ onGenerate }: Props) {
onClick={() => setPromptMode('basic')} onClick={() => setPromptMode('basic')}
className={`flex-1 py-1.5 text-xs font-medium rounded-md transition-colors ${ className={`flex-1 py-1.5 text-xs font-medium rounded-md transition-colors ${
promptMode === 'basic' promptMode === 'basic'
? 'bg-lyria-accent text-black' ? 'bg-kithara-accent text-black'
: 'text-lyria-muted hover:text-white' : 'text-kithara-muted hover:text-white'
}`} }`}
> >
Basic Basic
@@ -171,8 +171,8 @@ export default function Generator({ onGenerate }: Props) {
onClick={() => setPromptMode('advanced')} onClick={() => setPromptMode('advanced')}
className={`flex-1 py-1.5 text-xs font-medium rounded-md transition-colors ${ className={`flex-1 py-1.5 text-xs font-medium rounded-md transition-colors ${
promptMode === 'advanced' promptMode === 'advanced'
? 'bg-lyria-accent text-black' ? 'bg-kithara-accent text-black'
: 'text-lyria-muted hover:text-white' : 'text-kithara-muted hover:text-white'
}`} }`}
> >
Advanced Advanced
@@ -184,12 +184,12 @@ export default function Generator({ onGenerate }: Props) {
{(promptMode === 'basic' || !isPro) && ( {(promptMode === 'basic' || !isPro) && (
<div> <div>
<div className="flex items-center justify-between mb-1.5"> <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 <button
type="button" type="button"
onClick={() => setSuggestionsOpen(true)} onClick={() => setSuggestionsOpen(true)}
disabled={!prompt.trim()} 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 ✨ Get Suggestions ✨
</button> </button>
@@ -198,7 +198,7 @@ export default function Generator({ onGenerate }: Props) {
value={prompt} value={prompt}
onChange={(e) => setPrompt(e.target.value)} onChange={(e) => setPrompt(e.target.value)}
placeholder="Dark distorted kickdrum with atmospheric synth pads, 180 BPM..." 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> </div>
)} )}
@@ -207,12 +207,12 @@ export default function Generator({ onGenerate }: Props) {
{promptMode === 'advanced' && isPro && ( {promptMode === 'advanced' && isPro && (
<div> <div>
<div className="flex items-center justify-between mb-1.5"> <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 <button
type="button" type="button"
onClick={() => setSuggestionsOpen(true)} onClick={() => setSuggestionsOpen(true)}
disabled={Object.values(structuredPrompt).every(v => !v?.trim())} 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 ✨ Get Suggestions ✨
</button> </button>
@@ -231,7 +231,7 @@ export default function Generator({ onGenerate }: Props) {
<button <button
type="submit" type="submit"
disabled={isLoading || (promptMode === 'basic' ? !prompt.trim() : Object.values(structuredPrompt).every(v => !v?.trim()))} 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'} {isLoading ? 'Generating...' : 'Generate'}
</button> </button>
+13 -13
View File
@@ -167,12 +167,12 @@ export default function Player({ clip, isPlaying, onTogglePlay, onStop, onClose
return ( 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"> <div className="max-w-6xl mx-auto flex items-center gap-3">
{/* Play/Pause */} {/* Play/Pause */}
<button <button
onClick={onTogglePlay} 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'} title={isPlaying ? 'Pause' : 'Play'}
> >
{isPlaying ? ( {isPlaying ? (
@@ -189,7 +189,7 @@ export default function Player({ clip, isPlaying, onTogglePlay, onStop, onClose
{/* Stop */} {/* Stop */}
<button <button
onClick={handleStop} 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" title="Stop"
> >
<svg className="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 24 24"> <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 */} {/* Restart — ISO skip backwards to beginning */}
<button <button
onClick={handleRestart} 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" title="Skip to beginning"
> >
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"> <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} onClick={toggleRepeat}
className={`w-7 h-7 rounded flex items-center justify-center transition-colors shrink-0 ${ className={`w-7 h-7 rounded flex items-center justify-center transition-colors shrink-0 ${
isRepeating isRepeating
? 'text-lyria-accent bg-lyria-accent/10 shadow-[0_0_8px_rgba(255,107,53,0.4)]' ? 'text-kithara-accent bg-kithara-accent/10 shadow-[0_0_8px_rgba(255,107,53,0.4)]'
: 'text-lyria-muted hover:text-white hover:bg-white/5' : 'text-kithara-muted hover:text-white hover:bg-white/5'
}`} }`}
title={isRepeating ? 'Repeat on' : 'Repeat off'} title={isRepeating ? 'Repeat on' : 'Repeat off'}
> >
@@ -225,7 +225,7 @@ export default function Player({ clip, isPlaying, onTogglePlay, onStop, onClose
{/* Time + Scrubber */} {/* Time + Scrubber */}
<div className="flex-1 min-w-0 flex items-center gap-3"> <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)} {formatTime(currentTime)}
</span> </span>
@@ -237,7 +237,7 @@ export default function Player({ clip, isPlaying, onTogglePlay, onStop, onClose
onMouseUp={handleProgressMouseUp} onMouseUp={handleProgressMouseUp}
> >
<div <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}%` }} style={{ width: `${progress}%` }}
/> />
{/* Scrubber handle */} {/* Scrubber handle */}
@@ -247,7 +247,7 @@ export default function Player({ clip, isPlaying, onTogglePlay, onStop, onClose
/> />
</div> </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)} {formatTime(duration)}
</span> </span>
</div> </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"> <div className="hidden sm:flex items-center gap-2.5 min-w-0 max-w-[220px] shrink-0">
<button <button
onClick={() => window.open(`/api/clips/${clip.id}/download`, '_blank')} 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" title="Download MP3"
> >
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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' target.style.display = 'none'
const parent = target.parentElement const parent = target.parentElement
if (parent) { 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"> <div className="flex items-center gap-2 shrink-0">
<button <button
onClick={toggleMute} onClick={toggleMute}
className="text-lyria-muted hover:text-white transition-colors" className="text-kithara-muted hover:text-white transition-colors"
title={isMuted ? 'Unmute' : 'Mute'} title={isMuted ? 'Unmute' : 'Mute'}
> >
{isMuted || volume === 0 ? ( {isMuted || volume === 0 ? (
@@ -332,7 +332,7 @@ export default function Player({ clip, isPlaying, onTogglePlay, onStop, onClose
{/* Close */} {/* Close */}
<button <button
onClick={onClose} 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" title="Close player"
> >
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+14 -14
View File
@@ -164,7 +164,7 @@ export default function SettingsModal({ isOpen, onClose, onClearWaveforms, onDel
<span className="text-sm font-medium text-white">{title}</span> <span className="text-sm font-medium text-white">{title}</span>
</div> </div>
<svg <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" fill="none"
stroke="currentColor" stroke="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -177,9 +177,9 @@ export default function SettingsModal({ isOpen, onClose, onClearWaveforms, onDel
<div className="p-4 space-y-4"> <div className="p-4 space-y-4">
{/* Model Selection */} {/* Model Selection */}
<div> <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 <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')} value={gen.getValue('model')}
onChange={(e) => gen.update('model', e.target.value)} onChange={(e) => gen.update('model', e.target.value)}
> >
@@ -194,7 +194,7 @@ export default function SettingsModal({ isOpen, onClose, onClearWaveforms, onDel
{/* API Key Status */} {/* API Key Status */}
<div className="flex items-center gap-2"> <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'}`} /> <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'} {gen.config?.has_api_key ? 'API key configured' : 'No API key — set in .env'}
</span> </span>
</div> </div>
@@ -209,9 +209,9 @@ export default function SettingsModal({ isOpen, onClose, onClearWaveforms, onDel
{ key: 'safety_dangerous', label: 'Dangerous Content' }, { key: 'safety_dangerous', label: 'Dangerous Content' },
].map(({ key, label }) => ( ].map(({ key, label }) => (
<div key={key}> <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 <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)} value={gen.getValue(key as keyof GenConfig)}
onChange={(e) => gen.update(key, e.target.value)} onChange={(e) => gen.update(key, e.target.value)}
> >
@@ -228,7 +228,7 @@ export default function SettingsModal({ isOpen, onClose, onClearWaveforms, onDel
<button <button
onClick={gen.save} onClick={gen.save}
disabled={!gen.hasChanges || gen.saving} 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'} {gen.saving ? 'Saving...' : 'Save Changes'}
</button> </button>
@@ -238,7 +238,7 @@ export default function SettingsModal({ isOpen, onClose, onClearWaveforms, onDel
</span> </span>
)} )}
</div> </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. Changes apply immediately but reset on server restart. Set .env for persistence.
</p> </p>
</div> </div>
@@ -255,13 +255,13 @@ export default function SettingsModal({ isOpen, onClose, onClearWaveforms, onDel
/> />
{/* Modal */} {/* 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 */} {/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-white/10 shrink-0"> <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> <h2 className="text-lg font-semibold text-white">Settings</h2>
<button <button
onClick={onClose} onClick={onClose}
className="text-lyria-muted hover:text-white transition-colors" className="text-kithara-muted hover:text-white transition-colors"
aria-label="Close settings" aria-label="Close settings"
> >
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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"> <div className="px-6 py-5 space-y-6 overflow-y-auto">
{/* Image Generation */} {/* Image Generation */}
{renderGenSection('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" /> <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> </svg>
), imageGen)} ), imageGen)}
{/* Text Generation */} {/* Text Generation */}
{renderGenSection('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" /> <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> </svg>
), textGen)} ), textGen)}
@@ -314,7 +314,7 @@ export default function SettingsModal({ isOpen, onClose, onClearWaveforms, onDel
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm text-white font-medium">Clear Waveform Cache</p> <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> </div>
<button <button
onClick={handleClearWaveforms} onClick={handleClearWaveforms}
@@ -331,7 +331,7 @@ export default function SettingsModal({ isOpen, onClose, onClearWaveforms, onDel
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm text-white font-medium">Delete All Tracks</p> <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. Remove {clipCount} clip{clipCount !== 1 ? 's' : ''}. Cannot be undone.
</p> </p>
{confirmDelete && ( {confirmDelete && (
@@ -51,12 +51,12 @@ export default function StructuredPromptEditor({ value, onChange }: Props) {
return ( return (
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center justify-between"> <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 {filledCount} of {FIELDS.length} fields filled
</span> </span>
<button <button
onClick={() => setExpanded(new Set(FIELDS.map(f => f.key)))} 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 Expand All
</button> </button>
@@ -75,11 +75,11 @@ export default function StructuredPromptEditor({ value, onChange }: Props) {
}`} }`}
> >
<div className="flex items-center gap-2"> <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={`w-1.5 h-1.5 rounded-full ${hasValue ? 'bg-kithara-accent' : 'bg-kithara-muted/30'}`} />
<span className={hasValue ? 'text-white' : 'text-lyria-muted'}>{label}</span> <span className={hasValue ? 'text-white' : 'text-kithara-muted'}>{label}</span>
</div> </div>
<svg <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" fill="none"
stroke="currentColor" stroke="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -94,7 +94,7 @@ export default function StructuredPromptEditor({ value, onChange }: Props) {
value={value[key as keyof PromptStructure] || ''} value={value[key as keyof PromptStructure] || ''}
onChange={(e) => updateField(key, e.target.value)} onChange={(e) => updateField(key, e.target.value)}
placeholder={placeholder} 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> </div>
)} )}
+10 -10
View File
@@ -53,16 +53,16 @@ export default function SuggestionsModal({ isOpen, onClose, onSelect, userInput
onClick={onClose} 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 */} {/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-white/10 shrink-0"> <div className="flex items-center justify-between px-6 py-4 border-b border-white/10 shrink-0">
<div> <div>
<h2 className="text-lg font-semibold text-white">Prompt Suggestions</h2> <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> </div>
<button <button
onClick={onClose} 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"> <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" /> <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"> <div className="px-6 py-5 overflow-y-auto">
{loading && ( {loading && (
<div className="flex items-center justify-center py-8"> <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" /> <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" /> <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> </svg>
@@ -86,7 +86,7 @@ export default function SuggestionsModal({ isOpen, onClose, onSelect, userInput
<p className="text-sm text-red-400">{error}</p> <p className="text-sm text-red-400">{error}</p>
<button <button
onClick={fetchSuggestions} 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 Try again
</button> </button>
@@ -95,10 +95,10 @@ export default function SuggestionsModal({ isOpen, onClose, onSelect, userInput
{!loading && !error && suggestions.length === 0 && ( {!loading && !error && suggestions.length === 0 && (
<div className="text-center py-8"> <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 <button
onClick={fetchSuggestions} 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 Get Suggestions
</button> </button>
@@ -113,13 +113,13 @@ export default function SuggestionsModal({ isOpen, onClose, onSelect, userInput
onSelect(suggestion) onSelect(suggestion)
onClose() 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"> <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> <p className="text-sm text-white/90 leading-relaxed">{suggestion}</p>
</div> </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 → Click to use this prompt →
</div> </div>
</button> </button>
+6 -6
View File
@@ -7,12 +7,12 @@ export default {
theme: { theme: {
extend: { extend: {
colors: { colors: {
'lyria-dark': '#0a0a0f', 'kithara-dark': '#0a0a0f',
'lyria-panel': '#12121a', 'kithara-panel': '#12121a',
'lyria-surface': '#1a1a24', 'kithara-surface': '#1a1a24',
'lyria-accent': '#ff6b35', 'kithara-accent': '#ff6b35',
'lyria-text': '#e0e0e0', 'kithara-text': '#e0e0e0',
'lyria-muted': '#6b6b7b', 'kithara-muted': '#6b6b7b',
}, },
keyframes: { keyframes: {
'marquee-scroll': { 'marquee-scroll': {