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:
1 parent
98cc6b031c
commit
55f4a98b38
1 file changed
+51
-18
@@ -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 }) {
|
||||
return (
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 18 18"
|
||||
className={bouncing ? 'animate-note-bounce' : ''}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{/* Staff lines */}
|
||||
// ── 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" />
|
||||
{/* 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" />
|
||||
</>
|
||||
)
|
||||
|
||||
function NoteIcon({ noteIndex, bouncing }: { noteIndex: number; bouncing: boolean }) {
|
||||
return (
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 16 16"
|
||||
className={bouncing ? 'animate-note-bounce' : ''}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{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...')
|
||||
|
||||
Reference in new issue
Block a user