diff --git a/app/src/components/SimulationDashboard/AnalyzerManager.tsx b/app/src/components/SimulationDashboard/AnalyzerManager.tsx new file mode 100644 index 0000000..55e8a3b --- /dev/null +++ b/app/src/components/SimulationDashboard/AnalyzerManager.tsx @@ -0,0 +1,170 @@ +import { useEffect } from "react"; +import { useSceneContext } from "../../modules/scene/sceneContext"; + +const AnalyzerManager: React.FC = () => { + const { analysisStore, simulationDashBoardStore } = useSceneContext(); + const { analysis, getAssetAnalysis, getSystemMetrics } = analysisStore(); + const { blocks, updateGraphData } = simulationDashBoardStore(); + + // Helper to resolve value from path like "efficiency.overallEffectiveness" + const resolvePath = (obj: any, path: string | string[]) => { + if (!obj || !path) return undefined; + const actualPath = Array.isArray(path) ? path[0] : path; + if (typeof actualPath !== "string") return undefined; + return actualPath.split(".").reduce((prev, curr) => (prev ? prev[curr] : undefined), obj); + }; + + // Helper to get formatted value + const getFormattedValue = (value: any): string => { + if (typeof value === "number") { + return value.toFixed(2); + } + return String(value ?? "-"); + }; + + useEffect(() => { + if (!analysis) return; + + blocks.forEach((block) => { + block.elements.forEach((element) => { + // 1. Handle Label-Value (Direct DOM Manipulation) + if (element.type === "label-value") { + if (!element.dataSource || !element.dataValue) return; + + let value: any = "-"; + // Ensure dataValue is treated as string if possible, or take first if array + const mixedDataValue = element.dataValue; + const dataValueStr = Array.isArray(mixedDataValue) ? mixedDataValue[0] : mixedDataValue; + + if (typeof dataValueStr !== "string") return; + + if (element.dataSource === "global") { + // Handle global metrics + // Expected format: "global.systemPerformance.overallOEE" -> we need last part relative to metrics + // But dataValue usually comes as "systemPerformance.overallOEE" if dataSource is global? + // Let's check ElementEditor: + // It passes "global.systemPerformance.overallOEE" as the ID. + // We need to parse this. + + const metrics = getSystemMetrics(); + if (metrics) { + // If the ID starts with global., strip it. + const path = dataValueStr.startsWith("global.") + ? dataValueStr.replace("global.", "") + : dataValueStr; + + value = resolvePath(metrics, path); + } + + } else { + // Handle Asset Specific + const assetAnalysis = getAssetAnalysis(element.dataSource); + if (assetAnalysis) { + value = resolvePath(assetAnalysis, dataValueStr); + } + } + + // Update DOM + const valueElement = document.querySelector(`[data-element-id="${element.elementUuid}"] .value-text`); + if (valueElement) { + valueElement.textContent = getFormattedValue(value); + } + } + + // 2. Handle Graph (Store Update) + // We shouldn't update store on every frame if data hasn't changed or simpler approach? + // For graphs, we usually push new data points. + // Assuming we want to visualize historical data or current state trends. + else if (element.type === "graph") { + + if (element.dataType === "single-machine") { + if (!element.dataSource || !element.dataValue) return; + // Single machine, multiple values potentially? + // element.dataValue is string[] for single-machine graph + + const assetId = element.dataSource as string; + const dataKeys = element.dataValue as string[]; // e.g. ["efficiency.overallEffectiveness"] + + // For now support getting the FIRST value as the main value to plot? + // Or if we track history? + // The requirement says: "update the dom ... to show the value". + // UseAnalysisStore has 'historicalData'. + // But usually graphs update by appending new points. + // Here we might just pull the latest historical data or current value. + + const assetAnalysis = getAssetAnalysis(assetId); + if (assetAnalysis) { + // If it's a historical plot, we might want to read from assetAnalysis.historicalData (if it exists on client) + // or just push current value to the graph element's local state. + // But here we are asked to manage values using AnalyzerManager. + + // Let's construct a data point. + // GraphDataPoint = { name: string, value: number, ... } + + // We use analysis.lastUpdateTime or similar as 'name' (X-axis) + const timeLabel = new Date().toLocaleTimeString(); + + // We need to construct a new GraphDataPoint + const newPoint: any = { name: timeLabel }; + + let hasValidData = false; + dataKeys.forEach((key, index) => { + const val = resolvePath(assetAnalysis, key); + if (typeof val === "number") { + newPoint["value"] = val; // Primary value + // If we have multiple lines, we might need value1, value2 etc, but Recharts usually takes keys. + // The ElementContent uses 'value' key hardcoded for single line? + // Let's check ElementContent.tsx... + // It renders + // So it only supports ONE series called 'value' currently for default graphs? + // Or we can map index to keys? + + // For now, let's assume single generic 'value' or update if strictly array. + + hasValidData = true; + } + }); + + if (hasValidData) { + // We need to update the element's graphData. + // This is a store update, triggers re-render. + // BE CAREFUL: varying this too fast will kill performance. + // Maybe throttle this? useAnalysisStore updates often? + + const currentGraphData = element.graphData || []; + const newGraphData = [...currentGraphData, newPoint].slice(-20); // Keep last 20 points + + updateGraphData(block.blockUuid, element.elementUuid, newGraphData); + } + } + + } else if (element.dataType === "multiple-machine") { + // Multiple machines, single common value + const assetIds = element.dataSource as string[]; + const commonValueKey = element.commonValue as string; + + if (assetIds && commonValueKey) { + // Bar chart comparing machines? + // Data point per machine. + const newGraphData = assetIds.map(assetId => { + const assetAnalysis = getAssetAnalysis(assetId); + const val = assetAnalysis ? resolvePath(assetAnalysis, commonValueKey) : 0; + return { + name: assetAnalysis?.assetName || assetId, + value: typeof val === "number" ? val : 0 + }; + }); + + updateGraphData(block.blockUuid, element.elementUuid, newGraphData); + } + } + } + }); + }); + + }, [analysis, blocks, getAssetAnalysis, getSystemMetrics, updateGraphData]); + + return null; +}; + +export default AnalyzerManager; diff --git a/app/src/components/SimulationDashboard/DashboardEditor.tsx b/app/src/components/SimulationDashboard/DashboardEditor.tsx index 69e7b26..d7ecbf9 100644 --- a/app/src/components/SimulationDashboard/DashboardEditor.tsx +++ b/app/src/components/SimulationDashboard/DashboardEditor.tsx @@ -5,9 +5,11 @@ import ControlPanel from "./ControlPanel"; import SwapModal from "./SwapModal"; import { Block } from "../../types/exportedTypes"; import DataModelPanel from "./components/models/DataModelPanel"; +import AnalyzerManager from "./AnalyzerManager"; import { useSceneContext } from "../../modules/scene/sceneContext"; import useModuleStore from "../../store/ui/useModuleStore"; +import { usePlayButtonStore } from "../../store/ui/usePlayButtonStore"; import { calculateMinBlockSize } from "./functions/block/calculateMinBlockSize"; import { handleElementDragStart, handleElementResizeStart, handleBlockResizeStart, handleSwapStart, handleSwapTarget, handleBlockClick, handleElementClick } from "./functions/eventHandlers"; import BlockGrid from "./components/block/BlockGrid"; @@ -25,6 +27,7 @@ const DashboardEditor: React.FC = () => { const { selectedVersion } = versionStore(); const { projectId } = useParams(); const { activeModule } = useModuleStore(); + const { isPlaying } = usePlayButtonStore(); const { blocks, selectedBlock, @@ -410,6 +413,8 @@ const DashboardEditor: React.FC = () => { /> )} + {activeModule === "simulation" && isPlaying && } + {/* BlockGrid */}
diff --git a/app/src/modules/simulation/analyzer/analyzer.tsx b/app/src/modules/simulation/analyzer/analyzer.tsx index 5e71e51..3225f35 100644 --- a/app/src/modules/simulation/analyzer/analyzer.tsx +++ b/app/src/modules/simulation/analyzer/analyzer.tsx @@ -426,10 +426,8 @@ function Analyzer() { // Update historical data const timestamp = new Date().toISOString(); - if (!historicalDataRef.current[vehicle.modelUuid]) { - historicalDataRef.current[vehicle.modelUuid] = []; - } - historicalDataRef.current[vehicle.modelUuid].push({ + const currentData = historicalDataRef.current[vehicle.modelUuid] || []; + historicalDataRef.current[vehicle.modelUuid] = [...currentData, { timestamp, phase: vehicle.currentPhase, load: vehicle.currentLoad, @@ -438,7 +436,7 @@ function Analyzer() { performance: performance.performanceRate, speed: vehicle.speed, tripsCompleted, - }); + }].slice(-100); return { assetId: vehicle.modelUuid, @@ -699,10 +697,8 @@ function Analyzer() { // Update historical data const timestamp = new Date().toISOString(); - if (!historicalDataRef.current[machine.modelUuid]) { - historicalDataRef.current[machine.modelUuid] = []; - } - historicalDataRef.current[machine.modelUuid].push({ + const currentData = historicalDataRef.current[machine.modelUuid] || []; + historicalDataRef.current[machine.modelUuid] = [...currentData, { timestamp, processTime: actualProcessTime, partsProcessed, @@ -710,7 +706,7 @@ function Analyzer() { state: machine.state, defectRate, performance: performance.performanceRate, - }); + }].slice(-100); return { assetId: machine.modelUuid, @@ -1340,9 +1336,9 @@ function Analyzer() { const averageResidenceTime = materialHistoryRef.current.length > 0 ? materialHistoryRef.current.reduce((sum, entry) => { - const residenceTime = new Date(entry.removedAt).getTime() - (entry.material.startTime || 0); - return sum + (residenceTime || 0); - }, 0) / materialHistoryRef.current.length + const residenceTime = new Date(entry.removedAt).getTime() - (entry.material.startTime || 0); + return sum + (residenceTime || 0); + }, 0) / materialHistoryRef.current.length : 0; // Bottleneck Identification