feat: Maestro Mode - context-aware prompt generation
- Backend: POST /api/text-gen/maestro endpoint with dual modes
- Empty prompt → generate from scratch (temp 1.0)
- Non-empty prompt → complete/extend (temp 0.85)
- structured=true → returns JSON fields for Advanced/Pro mode
- Three system prompts: MAESTRO_GENERATE, MAESTRO_COMPLETE, MAESTRO_STRUCTURED
- Trailing sentence cleanup for plain text mode
- JSON fallback parsing for structured mode
- Frontend: MaestroButton component
- Context-aware labels: ♪ Maestro Mode (empty) / ♪ Keep Composing (has content)
- Loading states: animating ♩ with Dreaming.../Composing...
- structured prop for Pro mode → populates structured fields
- onStructuredResult callback merges fields into StructuredPrompt
- UX: Replaced 'Get Suggestions ✨' with Maestro Mode
- Suggestions required text in the box; Maestro works with or without
- Single button, context-aware behavior, cleaner UI
- Advanced/Pro mode gets structured fields from Maestro
- Removed SuggestionsModal dependency
- Rename: 'Compose for Me' → 'Keep Composing'
This commit is contained in:
1 parent
816f8fbd2f
commit
e0ce9e4b6c
4 files changed
+304
-33
No files matched your search
+21
-1
@@ -15,7 +15,7 @@ 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, maestro_prompt, get_text_gen_config, get_available_models as get_text_models
|
||||||
|
|
||||||
app = FastAPI(title="Kithara Studio", version="0.1.0")
|
app = FastAPI(title="Kithara Studio", version="0.1.0")
|
||||||
|
|
||||||
@@ -447,6 +447,26 @@ async def get_suggestions(request: dict):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=f"Suggestion generation failed: {str(e)}")
|
raise HTTPException(status_code=500, detail=f"Suggestion generation failed: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/text-gen/maestro")
|
||||||
|
async def maestro(request: dict):
|
||||||
|
"""Generate or complete a music prompt using Maestro Mode.
|
||||||
|
|
||||||
|
If hint is empty/missing: generates a creative prompt from scratch ("Dream for Me").
|
||||||
|
If hint is provided: completes the user's existing prompt ("Keep Composing").
|
||||||
|
If structured=true: returns JSON fields for Advanced/Pro mode instead of plain text.
|
||||||
|
"""
|
||||||
|
hint = request.get("hint", "").strip()
|
||||||
|
model = request.get("model") # Optional text model override
|
||||||
|
structured = request.get("structured", False) # Return structured fields for Pro mode
|
||||||
|
|
||||||
|
try:
|
||||||
|
config = get_text_gen_config()
|
||||||
|
result = await maestro_prompt(hint=hint, model=model, config=config, structured=structured)
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Maestro generation failed: {str(e)}")
|
||||||
|
|
||||||
@app.patch("/api/clips/{clip_id}")
|
@app.patch("/api/clips/{clip_id}")
|
||||||
async def rename_clip_endpoint(clip_id: str, request: RenameRequest):
|
async def rename_clip_endpoint(clip_id: str, request: RenameRequest):
|
||||||
clip = await get_clip(clip_id)
|
clip = await get_clip(clip_id)
|
||||||
|
|||||||
+152
-1
@@ -1,6 +1,8 @@
|
|||||||
"""Text generation client using Google Gemini models for prompt suggestions."""
|
"""Text generation client using Google Gemini models for prompt suggestions."""
|
||||||
|
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from google import genai
|
from google import genai
|
||||||
@@ -93,7 +95,6 @@ Format your response as a numbered list (1., 2., 3.) with just the prompts, no e
|
|||||||
suggestions = []
|
suggestions = []
|
||||||
|
|
||||||
# Split by numbered lines (1., 2., 3.)
|
# Split by numbered lines (1., 2., 3.)
|
||||||
import re
|
|
||||||
# Find all text after numbers
|
# Find all text after numbers
|
||||||
matches = re.findall(r'(?:^|\n)\d+\.\s*(.*?)(?=\n\d+\.|$)', text, re.DOTALL)
|
matches = re.findall(r'(?:^|\n)\d+\.\s*(.*?)(?=\n\d+\.|$)', text, re.DOTALL)
|
||||||
if matches:
|
if matches:
|
||||||
@@ -106,6 +107,156 @@ Format your response as a numbered list (1., 2., 3.) with just the prompts, no e
|
|||||||
|
|
||||||
return suggestions[:3] # Ensure max 3
|
return suggestions[:3] # Ensure max 3
|
||||||
|
|
||||||
|
# ── Maestro Mode ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
MAESTRO_GENERATE_PROMPT = """You are Maestro, an expert music prompt writer for AI music generation models (specifically Google Lyria).
|
||||||
|
|
||||||
|
Your task: Write a SINGLE compelling, detailed music generation prompt from scratch. The user has given you no starting point — be creative and bold.
|
||||||
|
|
||||||
|
Pick any genre, style, or mood that inspires you. The prompt should be:
|
||||||
|
- A single paragraph, vivid and evocative
|
||||||
|
- Rich in musical detail (genre, instrumentation, mood, tempo, structure, production style)
|
||||||
|
- Written in terminology that music producers use
|
||||||
|
- Between 2-4 sentences long
|
||||||
|
- Something that would produce an INTERESTING piece of music — surprise the user
|
||||||
|
|
||||||
|
Vary your choices! Don't default to the same genre or mood every time. Draw from the full spectrum of music: electronic, orchestral, jazz, world, experimental, ambient, industrial, folk, hip-hop, and beyond.
|
||||||
|
|
||||||
|
Output ONLY the prompt text. No numbers, no labels, no commentary."""
|
||||||
|
|
||||||
|
|
||||||
|
MAESTRO_COMPLETE_PROMPT = """You are Maestro, an expert music prompt writer for AI music generation models (specifically Google Lyria).
|
||||||
|
|
||||||
|
The user has started writing a prompt and wants you to complete it. Here is their prompt so far:
|
||||||
|
|
||||||
|
"{user_input}"
|
||||||
|
|
||||||
|
Complete this prompt into a detailed, evocative music generation prompt. Your completion should:
|
||||||
|
- Build on the user's existing idea naturally
|
||||||
|
- Add specific details about genre, instrumentation, mood, tempo, structure, or production style
|
||||||
|
- Result in a single paragraph that's ready to use as-is
|
||||||
|
- Be between 2-4 sentences total (including what the user already wrote)
|
||||||
|
|
||||||
|
Do NOT repeat what the user wrote — continue from where they left off and blend it into a complete, polished prompt.
|
||||||
|
|
||||||
|
Output ONLY the completed prompt text. No numbers, no labels, no commentary."""
|
||||||
|
|
||||||
|
|
||||||
|
MAESTRO_STRUCTURED_PROMPT = """You are Maestro, an expert music prompt writer for AI music generation models (specifically Google Lyria).
|
||||||
|
|
||||||
|
{context_instruction}
|
||||||
|
|
||||||
|
Generate a detailed music generation prompt, filling in as many of these fields as appropriate:
|
||||||
|
|
||||||
|
- style: Genre and style (e.g., "Industrial techno", "Cinematic orchestral", "Lo-fi hip hop")
|
||||||
|
- timbre: Sound texture and tone quality (e.g., "Warm analog synths with gritty distortion layers")
|
||||||
|
- structure: Song structure (e.g., "Intro-Build-Drop-Breakdown-Drop-Outro")
|
||||||
|
- mood: Emotional quality and energy (e.g., "Dark and aggressive, euphoric climax")
|
||||||
|
- tempo: BPM and rhythmic feel (e.g., "180 BPM, driving four-on-the-floor")
|
||||||
|
- instrumentation: Specific instruments and sounds (e.g., "Distorted 909 kick, reverbed vocal chops, sub-bass")
|
||||||
|
- effects: Production effects and processing (e.g., "Heavy sidechain compression, bitcrushed melodies")
|
||||||
|
- additional: Any extra details, references, or instructions
|
||||||
|
|
||||||
|
You MUST output valid JSON with those exact field names. Only include fields where you have specific, meaningful content. Leave fields as empty strings if you don't have a good detail for them. Do NOT include any fields beyond these 8.
|
||||||
|
|
||||||
|
Be creative and varied! Draw from the full spectrum of music — electronic, orchestral, jazz, world, experimental, ambient, industrial, folk, hip-hop, and beyond.
|
||||||
|
|
||||||
|
Output ONLY the JSON object. No markdown, no commentary, no explanation."""
|
||||||
|
|
||||||
|
|
||||||
|
async def maestro_prompt(hint: str = "", model: str = None, config: dict = None, structured: bool = False) -> dict:
|
||||||
|
"""Generate or complete a music prompt using Maestro Mode.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
hint: User's existing text. Empty = generate from scratch, non-empty = complete.
|
||||||
|
model: Text model to use. Defaults to configured model.
|
||||||
|
config: Optional config override.
|
||||||
|
structured: If True, return structured JSON fields for Advanced/Pro mode.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Plain: dict with "prompt" (text) and "mode" ("generate" | "complete")
|
||||||
|
Structured: dict with "fields" (JSON) and "mode" ("generate" | "complete")
|
||||||
|
"""
|
||||||
|
cfg = config or get_text_gen_config()
|
||||||
|
client = genai.Client(api_key=cfg["api_key"])
|
||||||
|
safety = build_safety_settings(cfg)
|
||||||
|
|
||||||
|
# Auto-select mode based on input
|
||||||
|
mode = "generate" if not hint.strip() else "complete"
|
||||||
|
|
||||||
|
if structured:
|
||||||
|
# Structured mode: return JSON fields for Advanced/Pro prompt
|
||||||
|
context_instruction = (
|
||||||
|
"The user wants you to generate a creative music prompt from scratch. Surprise them with something interesting."
|
||||||
|
if mode == "generate"
|
||||||
|
else f"The user has started describing music they want. Here's what they have so far:\n\n\"{hint.strip()}\"\n\nBuild on their idea, filling in the structured fields with rich, specific details."
|
||||||
|
)
|
||||||
|
system = MAESTRO_STRUCTURED_PROMPT.format(context_instruction=context_instruction)
|
||||||
|
user_content = "Generate structured music prompt fields."
|
||||||
|
temperature = 0.95 if mode == "generate" else 0.8
|
||||||
|
max_tokens = 1536
|
||||||
|
|
||||||
|
response = client.models.generate_content(
|
||||||
|
model=model or cfg["model"],
|
||||||
|
contents=user_content,
|
||||||
|
config=types.GenerateContentConfig(
|
||||||
|
system_instruction=system,
|
||||||
|
safety_settings=safety,
|
||||||
|
temperature=temperature,
|
||||||
|
max_output_tokens=max_tokens,
|
||||||
|
response_mime_type="application/json",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
fields = json.loads(response.text.strip())
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
# Fallback: try to extract JSON from markdown code blocks
|
||||||
|
match = re.search(r'```(?:json)?\s*([\s\S]*?)```', response.text)
|
||||||
|
if match:
|
||||||
|
fields = json.loads(match.group(1).strip())
|
||||||
|
else:
|
||||||
|
# Last resort: put everything in additional
|
||||||
|
fields = {"additional": response.text.strip()}
|
||||||
|
|
||||||
|
return {"fields": fields, "mode": mode}
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Plain text mode: original behavior for Basic prompt
|
||||||
|
if mode == "generate":
|
||||||
|
system = MAESTRO_GENERATE_PROMPT
|
||||||
|
user_content = "Write me a music generation prompt. Surprise me."
|
||||||
|
temperature = 1.0
|
||||||
|
max_tokens = 1024
|
||||||
|
else:
|
||||||
|
system = MAESTRO_COMPLETE_PROMPT.format(user_input=hint.strip())
|
||||||
|
user_content = "Complete this prompt into a finished, ready-to-use music generation prompt. End with a complete sentence."
|
||||||
|
temperature = 0.85
|
||||||
|
max_tokens = 1536
|
||||||
|
|
||||||
|
response = client.models.generate_content(
|
||||||
|
model=model or cfg["model"],
|
||||||
|
contents=user_content,
|
||||||
|
config=types.GenerateContentConfig(
|
||||||
|
system_instruction=system,
|
||||||
|
safety_settings=safety,
|
||||||
|
temperature=temperature,
|
||||||
|
max_output_tokens=max_tokens,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
text = response.text.strip()
|
||||||
|
# Remove any leading numbering or labels the model might add
|
||||||
|
text = re.sub(r'^\d+\.\s*', '', text)
|
||||||
|
# Clean up trailing incomplete sentences (cut at last period)
|
||||||
|
if text and not text[-1] in '.!?':
|
||||||
|
last_period = max(text.rfind('.'), text.rfind('!'), text.rfind('?'))
|
||||||
|
if last_period > len(text) // 2: # Only cut if we're past halfway
|
||||||
|
text = text[:last_period + 1]
|
||||||
|
|
||||||
|
return {"prompt": text, "mode": mode}
|
||||||
|
|
||||||
|
|
||||||
# ── Available Models ───────────────────────────────────────────────────────
|
# ── Available Models ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
AVAILABLE_TEXT_MODELS = [
|
AVAILABLE_TEXT_MODELS = [
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import StructuredPromptEditor, { PromptStructure } from './StructuredPromptEditor'
|
import StructuredPromptEditor, { PromptStructure } from './StructuredPromptEditor'
|
||||||
import SuggestionsModal from './SuggestionsModal'
|
import MaestroButton from './MaestroButton'
|
||||||
|
|
||||||
interface ModelInfo {
|
interface ModelInfo {
|
||||||
name: string
|
name: string
|
||||||
@@ -24,7 +24,7 @@ export default function Generator({ onGenerate }: Props) {
|
|||||||
const [models, setModels] = useState<Record<string, ModelInfo>>({})
|
const [models, setModels] = useState<Record<string, ModelInfo>>({})
|
||||||
const [promptMode, setPromptMode] = useState<PromptMode>('basic')
|
const [promptMode, setPromptMode] = useState<PromptMode>('basic')
|
||||||
const [structuredPrompt, setStructuredPrompt] = useState<PromptStructure>({})
|
const [structuredPrompt, setStructuredPrompt] = useState<PromptStructure>({})
|
||||||
const [suggestionsOpen, setSuggestionsOpen] = useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('/api/models')
|
fetch('/api/models')
|
||||||
@@ -185,14 +185,16 @@ export default function Generator({ onGenerate }: Props) {
|
|||||||
<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-kithara-muted">Prompt</label>
|
<label className="text-xs text-kithara-muted">Prompt</label>
|
||||||
<button
|
<MaestroButton
|
||||||
type="button"
|
prompt={prompt}
|
||||||
onClick={() => setSuggestionsOpen(true)}
|
onResult={(result, mode) => {
|
||||||
disabled={!prompt.trim()}
|
setPrompt(result)
|
||||||
className="text-xs text-kithara-accent hover:text-kithara-accent/80 transition-colors disabled:opacity-40"
|
if (mode === 'generate') {
|
||||||
>
|
setError('')
|
||||||
Get Suggestions ✨
|
}
|
||||||
</button>
|
}}
|
||||||
|
onError={(msg) => setError(msg)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
value={prompt}
|
value={prompt}
|
||||||
@@ -208,14 +210,26 @@ export default function Generator({ onGenerate }: Props) {
|
|||||||
<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-kithara-muted">Structured Prompt</label>
|
<label className="text-xs text-kithara-muted">Structured Prompt</label>
|
||||||
<button
|
<MaestroButton
|
||||||
type="button"
|
prompt={Object.values(structuredPrompt).filter(Boolean).join(' ')}
|
||||||
onClick={() => setSuggestionsOpen(true)}
|
structured={isPro}
|
||||||
disabled={Object.values(structuredPrompt).every(v => !v?.trim())}
|
onResult={(result, mode) => {
|
||||||
className="text-xs text-kithara-accent hover:text-kithara-accent/80 transition-colors disabled:opacity-40"
|
// Fallback: plain text mode for non-Pro, put result in 'additional'
|
||||||
>
|
setStructuredPrompt(prev => ({ ...prev, additional: result }))
|
||||||
Get Suggestions ✨
|
if (mode === 'generate') setError('')
|
||||||
</button>
|
}}
|
||||||
|
onStructuredResult={(fields, mode) => {
|
||||||
|
// Structured mode: merge fields into structuredPrompt, preserving user edits
|
||||||
|
setStructuredPrompt(prev => ({
|
||||||
|
...prev,
|
||||||
|
...Object.fromEntries(
|
||||||
|
Object.entries(fields).filter(([_, v]) => v?.trim())
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
if (mode === 'generate') setError('')
|
||||||
|
}}
|
||||||
|
onError={(msg) => setError(msg)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<StructuredPromptEditor
|
<StructuredPromptEditor
|
||||||
value={structuredPrompt}
|
value={structuredPrompt}
|
||||||
@@ -237,19 +251,7 @@ export default function Generator({ onGenerate }: Props) {
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<SuggestionsModal
|
|
||||||
isOpen={suggestionsOpen}
|
|
||||||
onClose={() => setSuggestionsOpen(false)}
|
|
||||||
onSelect={(suggestion) => {
|
|
||||||
if (promptMode === 'basic' || !isPro) {
|
|
||||||
setPrompt(suggestion)
|
|
||||||
} else {
|
|
||||||
// For advanced mode, put it in the 'style' field or additional
|
|
||||||
setStructuredPrompt(prev => ({ ...prev, additional: suggestion }))
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
userInput={promptMode === 'basic' || !isPro ? prompt : Object.values(structuredPrompt).filter(Boolean).join(' ')}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import type { PromptStructure } from './StructuredPromptEditor'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** Current prompt text. Empty = generate mode, non-empty = complete mode. */
|
||||||
|
prompt: string
|
||||||
|
/** Called with the Maestro result — the generated or completed prompt. */
|
||||||
|
onResult: (result: string, mode: 'generate' | 'complete') => void
|
||||||
|
/** Called when Maestro encounters an error. */
|
||||||
|
onError: (message: string) => void
|
||||||
|
/** Optional CSS class for the button container. */
|
||||||
|
className?: string
|
||||||
|
/**
|
||||||
|
* If true, send structured=true and return structured fields.
|
||||||
|
* The onStructuredResult callback will be called instead of onResult.
|
||||||
|
*/
|
||||||
|
structured?: boolean
|
||||||
|
/** Called with structured fields when structured mode returns. */
|
||||||
|
onStructuredResult?: (fields: PromptStructure, mode: 'generate' | 'complete') => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MaestroButton({
|
||||||
|
prompt,
|
||||||
|
onResult,
|
||||||
|
onError,
|
||||||
|
className = '',
|
||||||
|
structured = false,
|
||||||
|
onStructuredResult,
|
||||||
|
}: Props) {
|
||||||
|
const [isMaestroing, setIsMaestroing] = useState(false)
|
||||||
|
|
||||||
|
const handleMaestro = async () => {
|
||||||
|
setIsMaestroing(true)
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/text-gen/maestro', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
hint: prompt.trim(),
|
||||||
|
structured,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
try {
|
||||||
|
const errorData = await res.json()
|
||||||
|
throw new Error(errorData.detail || 'Maestro generation failed')
|
||||||
|
} catch (parseErr) {
|
||||||
|
if (parseErr instanceof Error && parseErr.message !== 'Maestro generation failed') throw parseErr
|
||||||
|
throw new Error('Maestro generation failed')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json()
|
||||||
|
|
||||||
|
if (structured && data.fields && onStructuredResult) {
|
||||||
|
onStructuredResult(data.fields as PromptStructure, data.mode)
|
||||||
|
} else {
|
||||||
|
onResult(data.prompt, data.mode)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
onError(err instanceof Error ? err.message : 'Maestro generation failed')
|
||||||
|
} finally {
|
||||||
|
setIsMaestroing(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mode = prompt.trim() ? 'complete' : 'generate'
|
||||||
|
const label = mode === 'generate'
|
||||||
|
? '♪ Maestro Mode'
|
||||||
|
: '♪ Keep Composing'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleMaestro}
|
||||||
|
disabled={isMaestroing}
|
||||||
|
className={`${className} flex items-center justify-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md transition-all duration-200 ${
|
||||||
|
isMaestroing
|
||||||
|
? 'bg-kithara-accent/20 text-kithara-accent animate-pulse'
|
||||||
|
: 'bg-kithara-accent/10 text-kithara-accent hover:bg-kithara-accent/25 border border-kithara-accent/30 hover:border-kithara-accent/50'
|
||||||
|
}`}
|
||||||
|
title={mode === 'generate'
|
||||||
|
? "Let Maestro write a creative prompt for you"
|
||||||
|
: "Let Maestro complete your prompt with musical details"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isMaestroing ? (
|
||||||
|
<>
|
||||||
|
<span className="inline-block animate-spin">♩</span>
|
||||||
|
{mode === 'generate' ? 'Dreaming...' : 'Composing...'}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
label
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in new issue
Block a user