feat: Maestro cycling note animation - quarter, eighth, beamed eighths, sixteenth

The note icon now cycles through four musical note variants during
loading: quarter note → eighth note → beamed eighths → sixteenth note.
Each variant bounces on staff lines while cycling every 800ms.
Idle state shows a static eighth note. No more generic spinner.
This commit is contained in:
rin.kagamine committed 2026-05-31 10:39:43 -04:00
1 parent 98cc6b031c
commit 55f4a98b38
1 file changed
+48 -15
+48 -15
View File
@@ -1,4 +1,4 @@
import { useState } from 'react'
import { useState, useEffect, useRef } from 'react'
import type { PromptStructure } from './StructuredPromptEditor'
interface Props {
@@ -19,26 +19,39 @@ interface Props {
onStructuredResult?: (fields: PromptStructure, mode: 'generate' | 'complete') => void
}
function NoteIcon({ bouncing }: { bouncing: boolean }) {
// ── Note Variants ─────────────────────────────────────────────────────
const NOTES = [
// Quarter note (♩)
'M5.5 13 C4 13 3 12 3 11.5 C3 11 4 10.5 5.5 10.5 C7 10.5 8 11 8 11.5 C8 12 7 13 5.5 13 Z M7.8 11 L7.8 4',
// Eighth note (♪)
'M5.5 13 C4 13 3 12 3 11.5 C3 11 4 10.5 5.5 10.5 C7 10.5 8 11 8 11.5 C8 12 7 13 5.5 13 Z M7.8 11 L7.8 4 C10 5.5 9.5 7.5 7.8 8',
// Beamed eighths (♫)
'M3 13 C1.5 13 0.5 12 0.5 11.5 C0.5 11 1.5 10.5 3 10.5 C4.5 10.5 5.5 11 5.5 11.5 C5.5 12 4.5 13 3 13 Z M5.3 11 L5.3 4 M10 12.5 C8.5 12.5 7.5 11.5 7.5 11 C7.5 10.5 8.5 10 10 10 C11.5 10 12.5 10.5 12.5 11 C12.5 11.5 11.5 12.5 10 12.5 Z M12.3 11 L12.3 4 M5.3 4 L12.3 4',
// Sixteenth note (♬)
'M5.5 13 C4 13 3 12 3 11.5 C3 11 4 10.5 5.5 10.5 C7 10.5 8 11 8 11.5 C8 12 7 13 5.5 13 Z M7.8 11 L7.8 4 M7.8 7 C10 6 9.5 5.5 7.8 5 M7.8 9 C10 8 9.5 7.5 7.8 7',
]
const 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" />
</>
)
function NoteIcon({ noteIndex, bouncing }: { noteIndex: number; bouncing: boolean }) {
return (
<svg
width="14"
height="14"
viewBox="0 0 18 18"
viewBox="0 0 16 16"
className={bouncing ? '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" />
{STAFF_LINES}
<path d={NOTES[noteIndex]} fill="none" stroke="currentColor" strokeWidth="0.8" />
</svg>
)
}
@@ -52,6 +65,26 @@ export default function MaestroButton({
onStructuredResult,
}: Props) {
const [isMaestroing, setIsMaestroing] = useState(false)
const [noteIndex, setNoteIndex] = useState(1) // Start on eighth note (♪)
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
// Cycle through note variants while loading
useEffect(() => {
if (isMaestroing) {
intervalRef.current = setInterval(() => {
setNoteIndex(prev => (prev + 1) % NOTES.length)
}, 800) // Change note every 800ms (slightly slower than bounce)
} else {
if (intervalRef.current) {
clearInterval(intervalRef.current)
intervalRef.current = null
}
setNoteIndex(1) // Reset to eighth note when idle
}
return () => {
if (intervalRef.current) clearInterval(intervalRef.current)
}
}, [isMaestroing])
const handleMaestro = async () => {
setIsMaestroing(true)
@@ -109,7 +142,7 @@ export default function MaestroButton({
: "Let Maestro complete your prompt with musical details"
}
>
<NoteIcon bouncing={isMaestroing} />
<NoteIcon noteIndex={noteIndex} bouncing={isMaestroing} />
<span className="truncate">
{isMaestroing
? (mode === 'generate' ? 'Dreaming...' : 'Composing...')