From 4f89aa6219a00b28250ab63a13f9503821591a18 Mon Sep 17 00:00:00 2001 From: Ricardo Miranda Date: Wed, 16 Sep 2026 11:56:30 +0200 Subject: [PATCH 1/3] Add guided tour and analysis features to Process Studio; enhance Unit Inspector with tips --- .../components/studio/FuelCellAnalysis.jsx | 75 +++++++++++ .../src/components/studio/ProcessCanvas.jsx | 2 +- .../src/components/studio/ProcessStudio.jsx | 38 +++++- frontend/src/components/studio/StudioTour.jsx | 117 ++++++++++++++++++ .../components/studio/UnitAnalysisModal.jsx | 92 ++++++++++++++ .../src/components/studio/UnitInspector.jsx | 65 +++++++++- 6 files changed, 381 insertions(+), 8 deletions(-) create mode 100644 frontend/src/components/studio/FuelCellAnalysis.jsx create mode 100644 frontend/src/components/studio/StudioTour.jsx create mode 100644 frontend/src/components/studio/UnitAnalysisModal.jsx diff --git a/frontend/src/components/studio/FuelCellAnalysis.jsx b/frontend/src/components/studio/FuelCellAnalysis.jsx new file mode 100644 index 0000000..aebc02f --- /dev/null +++ b/frontend/src/components/studio/FuelCellAnalysis.jsx @@ -0,0 +1,75 @@ +/** + * FuelCellAnalysis.jsx + * ───────────────────────────────────────────────────────────────────────────── + * Compact analysis for a fuel-cell Unit (no dedicated panel existed in the old + * simulator). Shows the efficiency-vs-load characteristic (fuel cells are most + * efficient at part load) and power output vs load, from the composed + * technology's nominal efficiency and capacity. + */ + +import React from 'react'; +import ReactECharts from 'echarts-for-react'; + +const HHV = 39.4; // kWh/kg H2 + +export default function FuelCellAnalysis({ model }) { + const effNom = Number(model?.efficiency_pct ?? model?.nominal_efficiency_pct ?? 58); + const ratedKw = Number(model?.capacity_kw ?? 1000); + const minLoad = 10; + + // Fuel cells peak at low/mid load and decline toward full load. + const loads = Array.from({ length: 10 }, (_, i) => (i + 1) * 10); + const eff = (x) => { + if (x < minLoad) return null; + const f = 1.14 - 0.26 * (x / 100); // ~+14% at low load → ~−12% at full load + return Math.max(20, Math.min(99, effNom * f)); + }; + const effSeries = loads.map(eff); + const powerSeries = loads.map((x) => Math.round(ratedKw * (x / 100))); + const peakEff = Math.max(...effSeries.filter((v) => v != null)); + const dailyH2 = +((ratedKw / (HHV * effNom / 100)) * 24 / 1000).toFixed(2); // t/day at nominal + + const option = { + animation: false, + color: ['#4d4b9e', '#6f7dd6'], + grid: { top: 30, bottom: 42, left: 52, right: 56 }, + tooltip: { trigger: 'axis' }, + legend: { data: ['Efficiency (%)', 'Power (kW)'], bottom: 0, textStyle: { fontSize: 11 } }, + xAxis: { type: 'category', data: loads.map((x) => `${x}%`), name: 'Load', axisLabel: { fontSize: 10 } }, + yAxis: [ + { type: 'value', name: 'η (%)', min: 0, max: 100, axisLabel: { fontSize: 10 }, nameTextStyle: { fontSize: 10 } }, + { type: 'value', name: 'kW', axisLabel: { fontSize: 10 }, nameTextStyle: { fontSize: 10 }, splitLine: { show: false } }, + ], + series: [ + { name: 'Efficiency (%)', type: 'line', smooth: true, symbol: 'circle', data: effSeries, yAxisIndex: 0, areaStyle: { opacity: 0.08 } }, + { name: 'Power (kW)', type: 'line', smooth: true, symbol: 'none', lineStyle: { type: 'dashed' }, data: powerSeries, yAxisIndex: 1 }, + ], + }; + + return ( +
+
+ = 1000 ? `${(ratedKw / 1000).toFixed(1)} MW` : `${ratedKw} kW`} /> + + + +
+
+
+ bolt +

Part-Load Efficiency & Power

+
+ +
+
+ ); +} + +function Stat({ label, value }) { + return ( +
+

{label}

+

{value}

+
+ ); +} diff --git a/frontend/src/components/studio/ProcessCanvas.jsx b/frontend/src/components/studio/ProcessCanvas.jsx index 76f74e0..c03bd7d 100644 --- a/frontend/src/components/studio/ProcessCanvas.jsx +++ b/frontend/src/components/studio/ProcessCanvas.jsx @@ -210,7 +210,7 @@ const ProcessCanvas = forwardRef(function ProcessCanvas({ initialProcess, onDirt }), [nodes, edges, setNodes, markDirty]); return ( -
+
{error && (
(s || 'untitled').toLowerCase().trim().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '') || 'untitled'; @@ -46,6 +61,7 @@ export default function ProcessStudio() { const [dirty, setDirty] = useState(false); const [toast, setToast] = useState(null); const [sim, setSim] = useState({ status: 'idle', result: null, error: null }); // idle|running|done|error + const [tourOpen, setTourOpen] = useState(false); const canvasRef = useRef(null); const flash = (msg) => { setToast(msg); setTimeout(() => setToast(null), 2500); }; @@ -76,6 +92,12 @@ export default function ProcessStudio() { setDirty(false); setSim({ status: 'idle', result: null, error: null }); setMode('build'); + // First time in the builder → show the guided tour (once). A short delay + // lets the Build layout mount so the tour can measure its targets. + if (!localStorage.getItem(TOUR_SEEN_KEY)) { + localStorage.setItem(TOUR_SEEN_KEY, '1'); + setTimeout(() => setTourOpen(true), 500); + } }, []); const newProcess = useCallback(() => { @@ -129,6 +151,10 @@ export default function ProcessStudio() { ) : (
{dirty && ● unsaved} + - -
); } @@ -333,7 +363,7 @@ function BuildView({ meta, setMeta, initialProcess, canvasKey, canvasRef, onDirt
{/* Palette */} -