Replaced generic spinning character with a custom SVG eighth note bouncing against four staff lines. Added note-bounce keyframe to Tailwind config (0.6s ease-in-out infinite). More on-brand for a music tool than a generic spinner.
124 lines
4.1 KiB
TypeScript
124 lines
4.1 KiB
TypeScript
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
|
|
}
|
|
|
|
function BouncingNote() {
|
|
return (
|
|
<svg
|
|
width="18"
|
|
height="18"
|
|
viewBox="0 0 18 18"
|
|
className="animate-note-bounce"
|
|
aria-hidden="true"
|
|
>
|
|
{/* Staff lines */}
|
|
<line x1="2" y1="5" x2="16" y2="5" stroke="currentColor" strokeWidth="0.7" opacity="0.4" />
|
|
<line x1="2" y1="8" x2="16" y2="8" stroke="currentColor" strokeWidth="0.7" opacity="0.4" />
|
|
<line x1="2" y1="11" x2="16" y2="11" stroke="currentColor" strokeWidth="0.7" opacity="0.4" />
|
|
<line x1="2" y1="14" x2="16" y2="14" stroke="currentColor" strokeWidth="0.7" opacity="0.4" />
|
|
{/* Note head */}
|
|
<ellipse cx="5.5" cy="13" rx="2.5" ry="2" fill="currentColor" transform="rotate(-15 5.5 13)" />
|
|
{/* Stem */}
|
|
<line x1="7.8" y1="12.5" x2="7.8" y2="4" stroke="currentColor" strokeWidth="1.2" />
|
|
{/* Flag */}
|
|
<path d="M7.8 4 C10 5.5, 9.5 7.5, 7.8 8" fill="none" stroke="currentColor" strokeWidth="1" />
|
|
</svg>
|
|
)
|
|
}
|
|
|
|
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 min-w-[140px] border border-kithara-accent/30 transition-colors duration-200 ${
|
|
isMaestroing
|
|
? 'bg-kithara-accent/20 text-kithara-accent'
|
|
: 'bg-kithara-accent/10 text-kithara-accent hover:bg-kithara-accent/25 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 ? (
|
|
<>
|
|
<BouncingNote />
|
|
<span className="truncate">
|
|
{mode === 'generate' ? 'Dreaming...' : 'Composing...'}
|
|
</span>
|
|
</>
|
|
) : (
|
|
<span className="truncate">{label}</span>
|
|
)}
|
|
</button>
|
|
)
|
|
} |