decidron-simulator / frontend /src /components /decidron /DecidronSimulator.js
NeonClary
Add settings menu for diagram layout and panel visibility
ebbd1a5
Raw
History Blame Contribute Delete
6.22 kB
import React, { useState, useEffect, useCallback } from 'react';
import { RotateCcw } from 'lucide-react';
import NetworkDiagram from './NetworkDiagram';
import SensorInputForm from './SensorInputForm';
import CommandBuilder from './CommandBuilder';
import ResultsPanel from './ResultsPanel';
import StatsTable from './StatsTable';
import HistoryTimeline from './HistoryTimeline';
import {
getNetwork, getCommands, getStats, getHistory,
submitSensorInput, runSimulation, createCommand, deleteCommand, resetSimulation,
} from '../../utils/api';
import '../../styles/decidron.css';
const DEFAULT_VALUE = '97';
export default function DecidronSimulator({ settings }) {
const [network, setNetwork] = useState(null);
const [commands, setCommands] = useState([]);
const [stats, setStats] = useState(null);
const [history, setHistory] = useState([]);
const [results, setResults] = useState([]);
const [pending, setPending] = useState([]);
const [selected, setSelected] = useState(null); // null = live network
const [running, setRunning] = useState(false);
const [error, setError] = useState(null);
// Live sensor-input form values, pre-populated so the user can press
// "Run Simulation" immediately and see the network react.
const [channel, setChannel] = useState('');
const [value, setValue] = useState(DEFAULT_VALUE);
const refreshAll = useCallback(async () => {
try {
const [net, cmds, st, hist] = await Promise.all([
getNetwork(), getCommands(), getStats(), getHistory(),
]);
setNetwork(net);
setCommands(cmds);
setStats(st);
setHistory(hist.snapshots);
} catch (err) {
setError(err.message);
}
}, []);
useEffect(() => { refreshAll(); }, [refreshAll]);
// Default the channel to the first sensor once the network loads.
useEffect(() => {
if (network && !channel) {
const firstSensor = network.nodes.find((n) => n.type === 'sensor');
if (firstSensor) setChannel(firstSensor.id);
}
}, [network, channel]);
const handleQueue = async () => {
if (!channel || value === '') return;
setError(null);
try {
await submitSensorInput(channel, value);
setPending((prev) => [...prev, { channel, value }]);
} catch (err) {
setError(err.message);
}
};
const handleRun = async () => {
// Run queued inputs if any; otherwise run the current form value
// inline so a single click works on first load.
const inputs = pending.length > 0 ? null : [{ channel, value }];
if (pending.length === 0 && (!channel || value === '')) return;
setRunning(true);
setError(null);
try {
const resp = await runSimulation(inputs);
setResults(resp.results);
setNetwork(resp.network);
setSelected(null);
setPending([]);
const [st, hist] = await Promise.all([getStats(), getHistory()]);
setStats(st);
setHistory(hist.snapshots);
} catch (err) {
setError(err.message);
} finally {
setRunning(false);
}
};
const handleCreate = async (cmd, replaceName = null) => {
await createCommand(cmd);
if (replaceName && replaceName !== cmd.name) {
await deleteCommand(replaceName);
}
setCommands(await getCommands());
};
const handleDelete = async (name) => {
setError(null);
try {
await deleteCommand(name);
setCommands(await getCommands());
} catch (err) {
setError(err.message);
}
};
const handleReset = async () => {
setError(null);
try {
await resetSimulation();
setResults([]);
setPending([]);
setSelected(null);
await refreshAll();
} catch (err) {
setError(err.message);
}
};
if (!network) {
return (
<div className="decidron-app">
{error ? <div className="decidron-error">{error}</div> : <p>Loading…</p>}
</div>
);
}
const sensors = network.nodes.filter((n) => n.type === 'sensor');
const shownDiagram = selected ? selected.diagram : network.diagram;
const shownVersion = selected ? selected.version : network.version;
const { panels, layoutMode } = settings;
return (
<div className="decidron-app">
{error && <div className="decidron-error">{error}</div>}
<div className="decidron-grid">
<div>
<div className="decidron-panel">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h2>Decidron Network (v{shownVersion}{selected ? ' — historical' : ' — live'})</h2>
<button type="button" className="decidron-btn secondary" onClick={handleReset}>
<RotateCcw size={13} style={{ marginRight: 4, verticalAlign: 'middle' }} />
Reset
</button>
</div>
<p className="panel-hint">
Sensors (blue) feed coupled Decidrons (purple) that drive
actuators (green). Commands can modify this topology.
</p>
<NetworkDiagram data={shownDiagram} layoutMode={layoutMode} />
</div>
<HistoryTimeline
snapshots={history}
selectedVersion={selected ? selected.version : null}
onSelect={(s) => setSelected(
s.version === network.version ? null : s
)}
/>
</div>
<div>
{panels.sensorInput && (
<SensorInputForm
sensors={sensors}
channel={channel}
value={value}
onChannelChange={setChannel}
onValueChange={setValue}
pending={pending}
onQueue={handleQueue}
onRun={handleRun}
running={running}
/>
)}
{panels.commands && (
<CommandBuilder
commands={commands}
sensors={sensors}
nodes={network.nodes}
onCreate={handleCreate}
onDelete={handleDelete}
/>
)}
{panels.results && <ResultsPanel results={results} />}
{panels.stats && <StatsTable stats={stats} />}
</div>
</div>
</div>
);
}