NeonClary
Make simulator runnable on first click + editable commands
7f237a3
Raw
History Blame Contribute Delete
6.28 kB
import React, { useState } from 'react';
import { Plus, Pencil, Trash2, X } from 'lucide-react';
const MATCH_OPS = ['contains', 'equals', 'gt', 'gte', 'lt', 'lte', 'startswith', 'endswith', 'regex'];
function parseMatch(match) {
if (typeof match === 'string' && match.includes(':')) {
const [op, ...rest] = match.split(':');
return { op: MATCH_OPS.includes(op) ? op : 'contains', value: rest.join(':') };
}
return { op: 'contains', value: match || '' };
}
const BLANK = { name: '', sensor: '', op: 'gte', matchValue: '', output: '', actuator: '' };
export default function CommandBuilder({ commands, sensors, nodes, onCreate, onDelete }) {
const [form, setForm] = useState(BLANK);
const [editing, setEditing] = useState(null); // original name being edited
const [preservedOps, setPreservedOps] = useState([]); // keep network_ops we don't edit in the UI
const [error, setError] = useState(null);
const actuators = nodes.filter((n) => n.type === 'actuator');
const set = (k) => (e) => setForm((f) => ({ ...f, [k]: e.target.value }));
const resetForm = () => {
setForm(BLANK);
setEditing(null);
setPreservedOps([]);
};
const startEdit = (c) => {
const { op, value } = parseMatch(c.match);
setForm({
name: c.name,
sensor: c.sensor,
op,
matchValue: value,
output: c.output,
actuator: c.actuator || '',
});
setPreservedOps(c.network_ops || []);
setEditing(c.name);
setError(null);
};
const submit = async (e) => {
e.preventDefault();
setError(null);
if (!form.name || !form.sensor || form.matchValue === '' || !form.output) {
setError('Name, sensor, match value, and output are required.');
return;
}
try {
await onCreate({
name: form.name,
sensor: form.sensor,
match: `${form.op}:${form.matchValue}`,
output: form.output,
actuator: form.actuator || null,
network_ops: preservedOps,
}, editing);
resetForm();
} catch (err) {
setError(err.message);
}
};
return (
<div className="decidron-panel">
<h2>Processing Commands</h2>
<p className="panel-hint">
Sample commands are loaded below — edit or delete them, or add your
own. A command is IF (sensor matches) THEN (emit output, optionally
drive an actuator). Commands fire when you run the simulation.
</p>
{error && <div className="decidron-error">{error}</div>}
<form onSubmit={submit}>
<div className="decidron-row">
<div className="decidron-field" style={{ flex: 1 }}>
<label>Name</label>
<input value={form.name} onChange={set('name')} placeholder="HighTempAlert" />
</div>
<div className="decidron-field" style={{ flex: 1 }}>
<label>Sensor</label>
<select value={form.sensor} onChange={set('sensor')}>
<option value="">Select…</option>
{sensors.map((s) => <option key={s.id} value={s.id}>{s.label}</option>)}
</select>
</div>
</div>
<div className="decidron-row">
<div className="decidron-field" style={{ width: 120 }}>
<label>Match</label>
<select value={form.op} onChange={set('op')}>
{MATCH_OPS.map((o) => <option key={o} value={o}>{o}</option>)}
</select>
</div>
<div className="decidron-field" style={{ flex: 1 }}>
<label>Value</label>
<input value={form.matchValue} onChange={set('matchValue')} placeholder="85" />
</div>
<div className="decidron-field" style={{ flex: 1 }}>
<label>Actuator (optional)</label>
<select value={form.actuator} onChange={set('actuator')}>
<option value="">None</option>
{actuators.map((a) => <option key={a.id} value={a.id}>{a.label}</option>)}
</select>
</div>
</div>
<div className="decidron-field">
<label>Output</label>
<input value={form.output} onChange={set('output')} placeholder="Trigger failsafe review" />
</div>
{editing && preservedOps.length > 0 && (
<p className="panel-hint">
This command has {preservedOps.length} network op(s); they are
preserved when you save.
</p>
)}
<div className="decidron-row">
<button type="submit" className="decidron-btn">
{editing
? <><Pencil size={14} style={{ marginRight: 4, verticalAlign: 'middle' }} />Update Command</>
: <><Plus size={14} style={{ marginRight: 4, verticalAlign: 'middle' }} />Add Command</>}
</button>
{editing && (
<button type="button" className="decidron-btn secondary" onClick={resetForm}>
<X size={14} style={{ marginRight: 4, verticalAlign: 'middle' }} />
Cancel
</button>
)}
</div>
</form>
<div style={{ marginTop: '0.75rem' }}>
{commands.map((c) => (
<div className="decidron-cmd" key={c.name}>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '0.5rem' }}>
<span>
<strong>{c.name}</strong>: IF <code>{c.sensor}</code> <code>{c.match}</code> THEN {c.output}
{c.actuator && <> → drives <code>{c.actuator}</code></>}
{c.network_ops && c.network_ops.length > 0 && <> <span className="decidron-chip">{c.network_ops.length} network op(s)</span></>}
</span>
<span style={{ whiteSpace: 'nowrap' }}>
<button type="button" className="decidron-btn secondary" title="Edit" onClick={() => startEdit(c)} style={{ padding: '0.2rem 0.4rem', marginLeft: 4 }}>
<Pencil size={13} />
</button>
<button type="button" className="decidron-btn secondary" title="Delete" onClick={() => onDelete(c.name)} style={{ padding: '0.2rem 0.4rem', marginLeft: 4 }}>
<Trash2 size={13} />
</button>
</span>
</div>
</div>
))}
</div>
</div>
);
}