Spaces:
Running
Running
| 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> | |
| ); | |
| } | |