diff --git a/frontend/src/melodies/MelodyComposer.jsx b/frontend/src/melodies/MelodyComposer.jsx index 2092a93..ed1fa13 100644 --- a/frontend/src/melodies/MelodyComposer.jsx +++ b/frontend/src/melodies/MelodyComposer.jsx @@ -1,6 +1,9 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import api from "../api/client"; const MAX_NOTES = 16; +const NOTE_LABELS = "ABCDEFGHIJKLMNOP"; function bellFrequency(bellNumber) { return 880 * Math.pow(Math.pow(2, 1 / 12), -2 * (bellNumber - 1)); @@ -50,6 +53,7 @@ function playStep(audioCtx, stepValue, noteDurationMs) { } export default function MelodyComposer() { + const navigate = useNavigate(); const [steps, setSteps] = useState(Array.from({ length: 16 }, () => 0)); const [noteCount, setNoteCount] = useState(8); const [stepDelayMs, setStepDelayMs] = useState(280); @@ -57,6 +61,13 @@ export default function MelodyComposer() { const [loopEnabled, setLoopEnabled] = useState(true); const [isPlaying, setIsPlaying] = useState(false); const [currentStep, setCurrentStep] = useState(-1); + const [error, setError] = useState(""); + const [successMsg, setSuccessMsg] = useState(""); + const [showDeployModal, setShowDeployModal] = useState(false); + const [deployName, setDeployName] = useState(""); + const [deployPid, setDeployPid] = useState(""); + const [deployError, setDeployError] = useState(""); + const [deploying, setDeploying] = useState(false); const audioCtxRef = useRef(null); const playbackRef = useRef(null); @@ -171,10 +182,74 @@ export default function MelodyComposer() { const handlePlay = () => { if (!stepsRef.current.length) return; + setError(""); setIsPlaying(true); scheduleStep(0); }; + const openDeployModal = () => { + setError(""); + setSuccessMsg(""); + setDeployError(""); + setShowDeployModal(true); + }; + + const closeDeployModal = () => { + if (deploying) return; + setDeployError(""); + setShowDeployModal(false); + }; + + const handleDeploy = async () => { + const name = deployName.trim(); + const pid = deployPid.trim(); + if (!name) { + setDeployError("Name is required."); + return; + } + if (!pid) { + setDeployError("PID is required."); + return; + } + + setDeploying(true); + setDeployError(""); + setSuccessMsg(""); + try { + const existing = await api.get("/builder/melodies"); + const list = existing.melodies || []; + const dupName = list.find((m) => m.name.toLowerCase() === name.toLowerCase()); + if (dupName) { + setDeployError(`An archetype with the name "${name}" already exists.`); + return; + } + const dupPid = list.find((m) => m.pid && m.pid.toLowerCase() === pid.toLowerCase()); + if (dupPid) { + setDeployError(`An archetype with the PID "${pid}" already exists.`); + return; + } + + const stepsStr = steps.map(stepToNotation).join(","); + const created = await api.post("/builder/melodies", { + name, + pid, + steps: stepsStr, + }); + + setSuccessMsg(`Archetype "${name}" deployed successfully.`); + setShowDeployModal(false); + setDeployName(""); + setDeployPid(""); + if (created?.id) { + navigate(`/melodies/archetypes/${created.id}`); + } + } catch (err) { + setDeployError(err.message); + } finally { + setDeploying(false); + } + }; + const activeBellsInCurrentStep = useMemo(() => { if (currentStep < 0 || !steps[currentStep]) return []; const active = []; @@ -195,6 +270,31 @@ export default function MelodyComposer() {
+ {error && ( +