Added SpeedCalc and MelodyBuilder. Evaluation Pending
This commit is contained in:
152
frontend/src/melodies/builder/BuildOnTheFlyModal.jsx
Normal file
152
frontend/src/melodies/builder/BuildOnTheFlyModal.jsx
Normal 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}>×</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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user