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