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 && ( +
+ {error} +
+ )} + {successMsg && ( +
+ {successMsg} +
+ )} +
- Step + | + | + ) : ( + + )} + | + + + + +
-
+
-
- {!isPlaying ? ( - - ) : ( - - )} - - -
{currentStep >= 0 && ( @@ -338,7 +451,7 @@ export default function MelodyComposer() { className="rounded-lg border" style={{ backgroundColor: "var(--bg-card)", borderColor: "var(--border-primary)" }} > -
+
@@ -378,7 +491,7 @@ export default function MelodyComposer() { color: "var(--text-secondary)", }} > - {noteIndex + 1} + {NOTE_LABELS[noteIndex]} {steps.map((stepValue, stepIndex) => { const enabled = Boolean(stepValue & (1 << noteIndex)); @@ -386,25 +499,39 @@ export default function MelodyComposer() { return ( ); @@ -444,6 +571,107 @@ export default function MelodyComposer() { + + {showDeployModal && ( +
e.target === e.currentTarget && closeDeployModal()} + > +
+
+
+

+ Deploy as Archetype +

+

+ Create a new archetype from this composer pattern. +

+
+ +
+ +
+ {deployError && ( +
+ {deployError} +
+ )} +
+ + setDeployName(e.target.value)} + className="w-full px-3 py-2 rounded-md text-sm border" + placeholder="e.g. Doksologia_3k" + disabled={deploying} + /> +
+
+ + setDeployPid(e.target.value)} + className="w-full px-3 py-2 rounded-md text-sm border" + placeholder="e.g. builtin_doksologia_3k" + disabled={deploying} + /> +
+
+ +
+ + +
+
+
+ )} ); }