Added SpeedCalc and MelodyBuilder. Evaluation Pending

This commit is contained in:
2026-02-22 13:17:54 +02:00
parent 8a8c665dfd
commit 8703c4fe26
27 changed files with 4075 additions and 3 deletions

View File

@@ -0,0 +1,152 @@
import { useState } from "react";
import api from "../../api/client";
const labelStyle = { color: "var(--text-secondary)" };
const mutedStyle = { color: "var(--text-muted)" };
const inputClass = "w-full px-3 py-2 rounded-md text-sm border";
function countSteps(stepsStr) {
if (!stepsStr || !stepsStr.trim()) return 0;
return stepsStr.trim().split(",").length;
}
export default function BuildOnTheFlyModal({ open, melodyId, defaultName, defaultPid, onClose, onSuccess }) {
const [name, setName] = useState(defaultName || "");
const [pid, setPid] = useState(defaultPid || "");
const [steps, setSteps] = useState("");
const [building, setBuilding] = useState(false);
const [error, setError] = useState("");
const [statusMsg, setStatusMsg] = useState("");
if (!open) return null;
const handleBuildAndUpload = async () => {
if (!name.trim()) { setError("Name is required."); return; }
if (!steps.trim()) { setError("Steps are required."); return; }
setBuilding(true);
setError("");
setStatusMsg("");
let builtId = null;
try {
// Step 1: Create the built melody record
setStatusMsg("Creating melody record...");
const created = await api.post("/builder/melodies", {
name: name.trim(),
pid: pid.trim(),
steps: steps.trim(),
});
builtId = created.id;
// Step 2: Build the binary
setStatusMsg("Building binary...");
const built = await api.post(`/builder/melodies/${builtId}/build-binary`);
// Step 3: Fetch the .bsm file and upload to Firebase Storage
setStatusMsg("Uploading to cloud storage...");
const token = localStorage.getItem("access_token");
const res = await fetch(`/api${built.binary_url}`, {
headers: token ? { Authorization: `Bearer ${token}` } : {},
});
if (!res.ok) throw new Error(`Failed to fetch binary: ${res.statusText}`);
const blob = await res.blob();
const file = new File([blob], `${name.trim()}.bsm`, { type: "application/octet-stream" });
await api.upload(`/melodies/${melodyId}/upload/binary`, file);
// Step 4: Assign to this melody
setStatusMsg("Linking to melody...");
await api.post(`/builder/melodies/${builtId}/assign?firestore_melody_id=${melodyId}`);
setStatusMsg("Done!");
onSuccess();
} catch (err) {
setError(err.message);
setStatusMsg("");
} finally {
setBuilding(false);
}
};
return (
<div
className="fixed inset-0 flex items-center justify-center z-50"
style={{ backgroundColor: "rgba(0,0,0,0.6)" }}
onClick={(e) => e.target === e.currentTarget && !building && onClose()}
>
<div
className="w-full max-w-xl 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)" }}>Build on the Fly</h2>
<p className="text-xs mt-0.5" style={mutedStyle}>Enter steps, build binary, and upload all in one step.</p>
</div>
{!building && (
<button onClick={onClose} className="text-xl leading-none" style={mutedStyle}>&times;</button>
)}
</div>
<div className="p-6 space-y-4">
{error && (
<div className="text-sm rounded-md p-3 border" style={{ backgroundColor: "var(--danger-bg)", borderColor: "var(--danger)", color: "var(--danger-text)" }}>
{error}
</div>
)}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1" style={labelStyle}>Name *</label>
<input type="text" value={name} onChange={(e) => setName(e.target.value)} placeholder="e.g. Doksologia_3k" className={inputClass} disabled={building} />
</div>
<div>
<label className="block text-sm font-medium mb-1" style={labelStyle}>PID</label>
<input type="text" value={pid} onChange={(e) => setPid(e.target.value)} placeholder="e.g. builtin_doksologia_3k" className={inputClass} disabled={building} />
</div>
</div>
<div>
<div className="flex items-center justify-between mb-1">
<label className="block text-sm font-medium" style={labelStyle}>Steps *</label>
<span className="text-xs" style={mutedStyle}>{countSteps(steps)} steps</span>
</div>
<textarea
value={steps}
onChange={(e) => setSteps(e.target.value)}
rows={5}
placeholder={"e.g. 1,2,2+1,1,2,3+1,0,3\n\n• Comma-separated values\n• Single bell: 1, 2, 3...\n• Multiple: 2+3+1\n• Silence: 0"}
className={inputClass}
style={{ fontFamily: "monospace", resize: "vertical" }}
disabled={building}
/>
</div>
{statusMsg && !error && (
<div className="text-sm rounded-md p-2 text-center" style={{ color: "var(--text-muted)", backgroundColor: "var(--bg-primary)" }}>
{statusMsg}
</div>
)}
</div>
<div className="flex items-center justify-end gap-3 px-6 py-4 border-t" style={{ borderColor: "var(--border-primary)" }}>
<button
onClick={onClose}
disabled={building}
className="px-4 py-2 text-sm rounded-md disabled:opacity-50 transition-colors"
style={{ backgroundColor: "var(--bg-card-hover)", color: "var(--text-primary)" }}
>
Cancel
</button>
<button
onClick={handleBuildAndUpload}
disabled={building}
className="px-4 py-2 text-sm rounded-md disabled:opacity-50 transition-colors font-medium"
style={{ backgroundColor: "var(--btn-primary)", color: "var(--text-white)" }}
>
{building ? "Building & Uploading..." : "Build & Upload"}
</button>
</div>
</div>
</div>
);
}