memlnaut-nisps/manifold/src/dock/BackendAdvanced.tsx
monkey-w1n5t0n 19b7f7eee8 feat(manifold): add convertible React front-end + simplify console chrome
Adds the Manifold app (manifold/) — the convertible-mode React front-end on
the real NISPS ML+audio engine, served live at meml.lnfinitemonkeys.org/next/.

Console chrome trimmed per UI pass:
- drop mode label + subtitle from the top-left overlay (keep MEMLNaut wordmark)
- remove the composite split preset/ratio readout (top-centre)
- remove the OUTPUT corner tag above the bars
- remove the A/B compare toggle from the verdict cluster
- remove the follow button + input/noise readout (bottom-left)
- remove AltitudeNav focus switcher (bottom-right)
2026-06-28 03:28:45 +02:00

282 lines
9.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* BackendAdvanced — the FULL-depth advanced backend modal bodies (dock-spec §4).
* One editor per backend. All backends share the §3.1 baseline (rendered as
* OutputControlRow elsewhere); these add the backend-specific fields.
*
* The backend transport (backends-spec workstream E) is now LIVE: editing these
* fields writes the shared MFParam store, which the BackendManager reads to send
* real Web MIDI CC / OSC-over-WS. This modal is the full-depth duplicate of the
* inline config in OutputsBackendConfig; both write the same store.
*/
import type { MFParam } from '../console/model';
import type { BackendId } from './output-state';
import { defaultMidiSpec, defaultOscSpec } from './output-state';
function num(s: string, fallback: number): number {
const v = parseFloat(s);
return Number.isFinite(v) ? v : fallback;
}
const cellInput: React.CSSProperties = {
width: '100%',
background: 'var(--bg-1)',
border: '1px solid var(--line)',
borderRadius: 'var(--r-1)',
color: 'var(--fg)',
fontFamily: 'var(--font-mono)',
fontSize: 'var(--fs-xs)',
padding: '3px 6px',
};
function Th({ children }: { children: React.ReactNode }) {
return (
<th
style={{
textAlign: 'left',
fontSize: 9,
textTransform: 'uppercase',
letterSpacing: '0.06em',
color: 'var(--fg-dim)',
padding: '4px 6px',
borderBottom: '1px solid var(--line)',
}}
>
{children}
</th>
);
}
export interface BackendAdvancedProps {
backend: BackendId;
params: MFParam[];
setParam: (i: number, patch: Partial<MFParam>) => void;
}
export function BackendAdvanced({ backend, params, setParam }: BackendAdvancedProps) {
switch (backend) {
case 'midi':
return <MidiCcEditor params={params} setParam={setParam} />;
case 'osc':
return <OscPathEditor params={params} setParam={setParam} />;
case 'vcv':
case 'cvgate':
return <VcvChannelEditor params={params} setParam={setParam} />;
default:
return <SynthGroupNote params={params} />;
}
}
// ---- MIDI (dock-spec §4.1) -------------------------------------------------
function MidiCcEditor({
params,
setParam,
}: {
params: MFParam[];
setParam: (i: number, patch: Partial<MFParam>) => void;
}) {
return (
<div>
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: '0 0 8px' }}>
{params.length} CCs · live Web MIDI out (backends-spec §2.3). Editing the CC map here sends in
real time once a MIDI port is selected in the Outputs panel.
</p>
<div style={{ maxHeight: 360, overflow: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr>
<Th>Name</Th>
<Th>CC#</Th>
<Th>Ch</Th>
<Th>State</Th>
</tr>
</thead>
<tbody>
{params.map((p, i) => {
const m = p.midi ?? defaultMidiSpec(i);
return (
<tr key={i}>
<td style={{ padding: '3px 6px' }}>
<input
style={cellInput}
value={m.name}
onChange={(e) => setParam(i, { midi: { ...m, name: e.target.value } })}
/>
</td>
<td style={{ padding: '3px 6px', width: 70 }}>
<input
type="number"
min={0}
max={127}
style={cellInput}
value={m.cc}
onChange={(e) =>
setParam(i, {
midi: { ...m, cc: Math.max(0, Math.min(127, num(e.target.value, m.cc))) },
})
}
/>
</td>
<td style={{ padding: '3px 6px', width: 60 }}>
<input
type="number"
min={1}
max={16}
style={cellInput}
value={m.channel}
onChange={(e) =>
setParam(i, {
midi: {
...m,
channel: Math.max(1, Math.min(16, num(e.target.value, m.channel))),
},
})
}
/>
</td>
<td style={{ padding: '3px 6px', fontSize: 'var(--fs-xs)', color: 'var(--fg-mute)' }}>
{p.status}
{p.muted ? ' · muted' : ''}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
);
}
// ---- OSC (dock-spec §4.2) --------------------------------------------------
function OscPathEditor({
params,
setParam,
}: {
params: MFParam[];
setParam: (i: number, patch: Partial<MFParam>) => void;
}) {
return (
<div>
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: '0 0 8px' }}>
Live OSC over the WebSocket bridge (backends-spec §2.4). Set the bridge URL + per-output paths in
the Outputs panel; emits only while the bridge process is connected.
</p>
<div style={{ maxHeight: 360, overflow: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr>
<Th>OSC path</Th>
<Th>range min</Th>
<Th>range max</Th>
</tr>
</thead>
<tbody>
{params.map((p, i) => {
const o = p.osc ?? defaultOscSpec(p.name);
return (
<tr key={i}>
<td style={{ padding: '3px 6px' }}>
<input
style={cellInput}
value={o.path}
onChange={(e) => setParam(i, { osc: { ...o, path: e.target.value } })}
/>
</td>
<td style={{ padding: '3px 6px', width: 90 }}>
<input
type="number"
style={cellInput}
value={o.rangeMin}
onChange={(e) => setParam(i, { osc: { ...o, rangeMin: num(e.target.value, o.rangeMin) } })}
/>
</td>
<td style={{ padding: '3px 6px', width: 90 }}>
<input
type="number"
style={cellInput}
value={o.rangeMax}
onChange={(e) => setParam(i, { osc: { ...o, rangeMax: num(e.target.value, o.rangeMax) } })}
/>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
);
}
// ---- VCV / CV (dock-spec §4.3) ---------------------------------------------
function VcvChannelEditor({
params,
setParam,
}: {
params: MFParam[];
setParam: (i: number, patch: Partial<MFParam>) => void;
}) {
return (
<div>
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: '0 0 8px' }}>
VCV adds nothing beyond the baseline (min/max = range, fixed = freeze) plus per-channel polarity.
{/* TODO(backends-spec §2.6): the VCV browser↔module bridge transport is not yet wired here. */}
</p>
<div style={{ maxHeight: 360, overflow: 'auto', display: 'flex', flexDirection: 'column', gap: 4 }}>
{params.map((p, i) => {
const bipolar = p.vcv?.bipolar ?? false;
return (
<div
key={i}
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
background: 'var(--bg-2)',
border: '1px solid var(--line)',
borderRadius: 'var(--r-1)',
padding: '4px 8px',
}}
>
<span style={{ flex: 1, fontSize: 'var(--fs-xs)', color: 'var(--fg)' }}>{p.name}</span>
<span style={{ fontSize: 9, color: 'var(--fg-dim)' }}>
{p.min.toFixed(2)}{p.max.toFixed(2)} · {p.status === 'fixed' ? 'frozen' : 'live'}
</span>
<button
type="button"
onClick={() => setParam(i, { vcv: { bipolar: !bipolar } })}
style={{
fontSize: 9,
fontFamily: 'var(--font-mono)',
padding: '2px 8px',
cursor: 'pointer',
borderRadius: 'var(--r-pill)',
border: `1px solid ${bipolar ? 'var(--danger)' : 'var(--line)'}`,
background: 'transparent',
color: bipolar ? 'var(--danger)' : 'var(--fg-mute)',
}}
>
{bipolar ? '±5 V' : '010 V'}
</button>
</div>
);
})}
</div>
</div>
);
}
function SynthGroupNote({ params }: { params: MFParam[] }) {
const groups = Array.from(new Set(params.map((p) => p.group)));
return (
<div>
<p style={{ fontSize: 'var(--fs-xs)', color: 'var(--fg-dim)', margin: '0 0 8px', lineHeight: 1.6 }}>
The synth backend's advanced surface is the group-override matrix — see the Powerful Synth Engine
drawer's full depth (dock-spec §4.4 / §5). Groups: {groups.join(' · ')}.
</p>
</div>
);
}