CODEX - Improvements to the page

This commit is contained in:
2026-02-22 20:36:15 +02:00
parent fcc513a842
commit e11b89a1b7

View File

@@ -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() {
</p>
</div>
{error && (
<div
className="text-sm rounded-md p-3 border"
style={{
backgroundColor: "var(--danger-bg)",
borderColor: "var(--danger)",
color: "var(--danger-text)",
}}
>
{error}
</div>
)}
{successMsg && (
<div
className="text-sm rounded-md p-3 border"
style={{
backgroundColor: "var(--success-bg)",
borderColor: "var(--success)",
color: "var(--success-text)",
}}
>
{successMsg}
</div>
)}
<section
className="rounded-lg border p-4"
style={{ backgroundColor: "var(--bg-card)", borderColor: "var(--border-primary)" }}
@@ -216,6 +316,7 @@ export default function MelodyComposer() {
>
- Step
</button>
<span className="mx-1 text-sm" style={{ color: "var(--border-primary)" }}>|</span>
<button
type="button"
onClick={addNote}
@@ -234,6 +335,7 @@ export default function MelodyComposer() {
>
- Note
</button>
<span className="mx-1 text-sm" style={{ color: "var(--border-primary)" }}>|</span>
<button
type="button"
onClick={clearAll}
@@ -253,48 +355,17 @@ export default function MelodyComposer() {
className="rounded-lg border p-4"
style={{ backgroundColor: "var(--bg-card)", borderColor: "var(--border-primary)" }}
>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
<div>
<div className="flex items-center justify-between">
<label className="text-sm font-medium" style={{ color: "var(--text-secondary)" }}>
Step Delay
</label>
<span className="text-sm font-semibold" style={{ color: "var(--accent)" }}>
{stepDelayMs} ms
</span>
</div>
<div className="grid grid-cols-1 xl:grid-cols-12 gap-4 items-end">
<div className="xl:col-span-3">
<div className="flex items-center gap-2">
<label className="inline-flex items-center gap-2 text-sm" style={{ color: "var(--text-secondary)" }}>
<input
type="range"
min="40"
max="2000"
step="10"
value={stepDelayMs}
onChange={(e) => setStepDelayMs(Number(e.target.value))}
className="w-full mt-2"
type="checkbox"
checked={loopEnabled}
onChange={(e) => setLoopEnabled(e.target.checked)}
/>
</div>
<div>
<div className="flex items-center justify-between">
<label className="text-sm font-medium" style={{ color: "var(--text-secondary)" }}>
Note Duration
Loop
</label>
<span className="text-sm font-semibold" style={{ color: "var(--accent)" }}>
{noteDurationMs} ms
</span>
</div>
<input
type="range"
min="20"
max="1200"
step="10"
value={noteDurationMs}
onChange={(e) => setNoteDurationMs(Number(e.target.value))}
className="w-full mt-2"
/>
</div>
<div className="flex flex-wrap items-center gap-2 lg:justify-end">
{!isPlaying ? (
<button
type="button"
@@ -314,18 +385,60 @@ export default function MelodyComposer() {
Stop
</button>
)}
<label className="inline-flex items-center gap-2 text-sm" style={{ color: "var(--text-secondary)" }}>
<input
type="checkbox"
checked={loopEnabled}
onChange={(e) => setLoopEnabled(e.target.checked)}
/>
Loop
</label>
<span className="mx-1 text-sm" style={{ color: "var(--border-primary)" }}>|</span>
<button
type="button"
onClick={openDeployModal}
className="px-4 py-2 rounded-md text-sm"
style={{ backgroundColor: "var(--text-link)", color: "var(--text-white)" }}
>
Deploy as Archetype
</button>
</div>
</div>
<div className="xl:col-span-5">
<div className="flex items-center justify-between">
<label className="text-sm font-medium" style={{ color: "var(--text-secondary)" }}>
Step Delay
</label>
<span className="text-sm font-semibold" style={{ color: "var(--accent)" }}>
{stepDelayMs} ms
</span>
</div>
<input
type="range"
min="40"
max="2000"
step="10"
value={stepDelayMs}
onChange={(e) => setStepDelayMs(Number(e.target.value))}
className="w-full mt-2"
/>
</div>
<div className="xl:col-span-4">
<div className="flex items-center justify-between">
<label className="text-sm font-medium" style={{ color: "var(--text-secondary)" }}>
Note Duration
</label>
<span className="text-sm font-semibold" style={{ color: "var(--accent)" }}>
{noteDurationMs} ms
</span>
</div>
<input
type="range"
min="20"
max="500"
step="10"
value={noteDurationMs}
onChange={(e) => setNoteDurationMs(Number(e.target.value))}
className="w-full mt-2"
/>
</div>
</div>
{currentStep >= 0 && (
<p className="text-xs mt-3" style={{ color: "var(--text-muted)" }}>
Playing step {currentStep + 1}/{steps.length}
@@ -338,7 +451,7 @@ export default function MelodyComposer() {
className="rounded-lg border"
style={{ backgroundColor: "var(--bg-card)", borderColor: "var(--border-primary)" }}
>
<div className="overflow-auto" style={{ maxHeight: "60vh" }}>
<div className="overflow-x-auto">
<table className="min-w-max border-separate border-spacing-0">
<thead>
<tr>
@@ -378,7 +491,7 @@ export default function MelodyComposer() {
color: "var(--text-secondary)",
}}
>
{noteIndex + 1}
{NOTE_LABELS[noteIndex]}
</th>
{steps.map((stepValue, stepIndex) => {
const enabled = Boolean(stepValue & (1 << noteIndex));
@@ -386,25 +499,39 @@ export default function MelodyComposer() {
return (
<td
key={`${noteIndex}-${stepIndex}`}
className="border-b border-r p-1"
className="border-b border-r"
style={{
borderColor: "var(--border-primary)",
backgroundColor: isCurrent ? "rgba(116,184,22,0.08)" : "transparent",
width: "44px",
height: "44px",
}}
>
<button
type="button"
aria-label={`Toggle note ${noteIndex + 1} on step ${stepIndex + 1}`}
aria-pressed={enabled}
onClick={() => toggleCell(noteIndex, stepIndex)}
className="w-8 h-8 rounded-md border transition-colors"
className="w-full h-full flex items-center justify-center transition-colors"
style={{
borderColor: enabled ? "var(--accent)" : "var(--border-primary)",
backgroundColor: enabled ? "var(--accent)" : "var(--bg-primary)",
color: enabled ? "var(--bg-primary)" : "var(--text-muted)",
fontSize: "11px",
backgroundColor: "transparent",
border: "none",
outline: "none",
}}
>
{enabled ? "ON" : ""}
<span
aria-hidden="true"
style={{
width: "64%",
height: "64%",
borderRadius: "9999px",
backgroundColor: "var(--btn-primary)",
opacity: enabled ? 1 : 0,
transform: enabled ? "scale(1)" : "scale(0.4)",
boxShadow: enabled ? "0 0 12px 4px rgba(116, 184, 22, 0.55)" : "none",
transition: "opacity 140ms ease, transform 140ms ease, box-shadow 180ms ease",
}}
/>
</button>
</td>
);
@@ -444,6 +571,107 @@ export default function MelodyComposer() {
</div>
</div>
</section>
{showDeployModal && (
<div
className="fixed inset-0 flex items-center justify-center z-50"
style={{ backgroundColor: "rgba(0,0,0,0.7)" }}
onClick={(e) => e.target === e.currentTarget && closeDeployModal()}
>
<div
className="w-full max-w-md rounded-lg border shadow-xl"
style={{ backgroundColor: "var(--bg-card)", borderColor: "var(--border-primary)" }}
>
<div
className="flex items-center justify-between px-6 py-4 border-b"
style={{ borderColor: "var(--border-primary)" }}
>
<div>
<h2 className="text-lg font-semibold" style={{ color: "var(--text-heading)" }}>
Deploy as Archetype
</h2>
<p className="text-xs mt-0.5" style={{ color: "var(--text-muted)" }}>
Create a new archetype from this composer pattern.
</p>
</div>
<button
type="button"
onClick={closeDeployModal}
className="text-xl leading-none"
style={{ color: "var(--text-muted)" }}
disabled={deploying}
>
&times;
</button>
</div>
<div className="px-6 py-5 space-y-4">
{deployError && (
<div
className="text-sm rounded-md p-3 border"
style={{
backgroundColor: "var(--danger-bg)",
borderColor: "var(--danger)",
color: "var(--danger-text)",
}}
>
{deployError}
</div>
)}
<div>
<label className="block text-sm font-medium mb-1" style={{ color: "var(--text-secondary)" }}>
Name *
</label>
<input
type="text"
value={deployName}
onChange={(e) => setDeployName(e.target.value)}
className="w-full px-3 py-2 rounded-md text-sm border"
placeholder="e.g. Doksologia_3k"
disabled={deploying}
/>
</div>
<div>
<label className="block text-sm font-medium mb-1" style={{ color: "var(--text-secondary)" }}>
PID *
</label>
<input
type="text"
value={deployPid}
onChange={(e) => setDeployPid(e.target.value)}
className="w-full px-3 py-2 rounded-md text-sm border"
placeholder="e.g. builtin_doksologia_3k"
disabled={deploying}
/>
</div>
</div>
<div
className="flex justify-end gap-2 px-6 py-4 border-t"
style={{ borderColor: "var(--border-primary)" }}
>
<button
type="button"
onClick={closeDeployModal}
className="px-4 py-2 text-sm rounded-md transition-colors"
style={{ backgroundColor: "var(--bg-card-hover)", color: "var(--text-primary)" }}
disabled={deploying}
>
Cancel
</button>
<button
type="button"
onClick={handleDeploy}
className="px-4 py-2 text-sm rounded-md transition-colors disabled:opacity-50"
style={{ backgroundColor: "var(--btn-primary)", color: "var(--text-white)" }}
disabled={deploying}
>
{deploying ? "Deploying..." : "Deploy"}
</button>
</div>
</div>
</div>
)}
</div>
);
}