"use client"; import { useState, useEffect, useCallback } from 'react'; import { apiClient } from '@/lib/api-client'; import type { CycleCalculationRequest, CycleCalculationResponse, RefrigerantInfo } from '@/types/api'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, LabelList } from 'recharts'; export default function PHDiagramModern() { const [refrigerants, setRefrigerants] = useState([]); const [selectedRefrigerant, setSelectedRefrigerant] = useState('R290'); const [cycleData, setCycleData] = useState(null); const [diagramData, setDiagramData] = useState(null); const [inputType, setInputType] = useState<'temperature' | 'pressure'>('pressure'); const [evaporatingValue, setEvaporatingValue] = useState('3'); const [condensingValue, setCondensingValue] = useState('12'); const [superheating, setSuperheating] = useState('0'); const [subcooling, setSubcooling] = useState('2'); const [compressorEfficiency, setCompressorEfficiency] = useState('0.7'); const [massFlowRate, setMassFlowRate] = useState('1.0'); const [loading, setLoading] = useState(false); const [lastMetadata, setLastMetadata] = useState(null); const [resolution, setResolution] = useState<'low' | 'medium' | 'high'>('medium'); const [debounceMs, setDebounceMs] = useState(400); const [error, setError] = useState(null); const [testPoints, setTestPoints] = useState | null>(null); useEffect(() => { apiClient.getRefrigerants() .then(setRefrigerants) .catch(err => { console.error('Failed to load refrigerants:', err); setError('Cannot connect to API on port 8001'); }); }, []); // small spinner component const Spinner = () => (
); const handleGenerate = useCallback(async () => { if (!selectedRefrigerant) return; setLoading(true); setError(null); try { const request: CycleCalculationRequest = { refrigerant: selectedRefrigerant, superheat: parseFloat(superheating), subcool: parseFloat(subcooling), compressor_efficiency: parseFloat(compressorEfficiency), mass_flow: parseFloat(massFlowRate) }; if (inputType === 'temperature') { request.evap_temperature = parseFloat(evaporatingValue); request.cond_temperature = parseFloat(condensingValue); } else { request.evap_pressure = parseFloat(evaporatingValue); request.cond_pressure = parseFloat(condensingValue); } const cycleResponse = await apiClient.calculateCycle(request); setCycleData(cycleResponse); if (cycleResponse.points && cycleResponse.points.length >= 4) { const cyclePoints = cycleResponse.points.map(point => ({ enthalpy: point.enthalpy || 0, pressure: point.pressure })); const pressures = cyclePoints.map(p => p.pressure); const diagramRequest = { refrigerant: selectedRefrigerant, pressure_range: { min: Math.min(...pressures) * 0.5, max: Math.max(...pressures) * 1.5 }, include_isotherms: true, cycle_points: cyclePoints, format: 'json' }; const diagramResponse = await apiClient.generateDiagram(diagramRequest); if (diagramResponse.data) { setDiagramData(diagramResponse.data); } setLastMetadata(diagramResponse.metadata || null); } } catch (err: any) { setError(err.message); } finally { setLoading(false); } }, [ selectedRefrigerant, inputType, evaporatingValue, condensingValue, superheating, subcooling, compressorEfficiency, massFlowRate, resolution ]); // Debounced auto-generate when inputs change (600ms) useEffect(() => { if (!selectedRefrigerant) return; // do not trigger if a manual generation is already in progress let mounted = true; const id = setTimeout(() => { if (mounted) { handleGenerate(); } }, debounceMs); return () => { mounted = false; clearTimeout(id); }; }, [ selectedRefrigerant, inputType, evaporatingValue, condensingValue, superheating, subcooling, compressorEfficiency, massFlowRate, debounceMs, resolution, ]); // Auto-generate only on initial load useEffect(() => { if (refrigerants.length > 0 && selectedRefrigerant && !cycleData) { handleGenerate(); } }, [refrigerants.length, selectedRefrigerant]); // Header controls: resolution + debounce + status const Controls = () => (
{loading && } {lastMetadata && (
Gen: {lastMetadata.generation_time_ms} ms
{lastMetadata.generation_time_ms === 0 && (
Cache hit
)}
)}
); // Listen for global cycle events dispatched by CycleCalculator (for dynamic overlay) useEffect(() => { const onCycle = (e: any) => { try { const detail = e.detail; if (!detail) return; if (detail.refrigerant && detail.refrigerant !== selectedRefrigerant) return; // ignore if not matching const cyclePoints = detail.points as Array<{enthalpy:number, pressure:number}>; if (!cyclePoints || cyclePoints.length === 0) return; // Update cycleData and request diagram data for these points (async () => { setLoading(true); setCycleData(prev => ({ ...(prev || {}), points: cyclePoints } as any)); const pressures = cyclePoints.map(p => p.pressure); const diagramRequest = { refrigerant: selectedRefrigerant, pressure_range: { min: Math.min(...pressures) * 0.5, max: Math.max(...pressures) * 1.5 }, include_isotherms: true, cycle_points: cyclePoints, format: 'json', resolution: resolution }; try { const diagramResponse = await apiClient.generateDiagram(diagramRequest); if (diagramResponse.data) setDiagramData(diagramResponse.data); setLastMetadata(diagramResponse.metadata || null); } catch (err) { console.error('Diagram generation (event) failed', err); } finally { setLoading(false); } })(); } catch (err) { // ignore } }; window.addEventListener('cycle:calculated', onCycle as EventListener); return () => window.removeEventListener('cycle:calculated', onCycle as EventListener); }, [selectedRefrigerant, resolution]); // Listen for global cycle events dispatched by CycleCalculator (for dynamic overlay) useEffect(() => { const onCycle = (e: any) => { try { const detail = e.detail; if (!detail) return; if (detail.refrigerant && detail.refrigerant !== selectedRefrigerant) return; // ignore if not matching const cyclePoints = detail.points as Array<{enthalpy:number, pressure:number}>; if (!cyclePoints || cyclePoints.length === 0) return; // Update cycleData and request diagram data for these points (async () => { setLoading(true); setCycleData(prev => ({ ...(prev || {}), points: cyclePoints } as any)); const pressures = cyclePoints.map(p => p.pressure); const diagramRequest = { refrigerant: selectedRefrigerant, pressure_range: { min: Math.min(...pressures) * 0.5, max: Math.max(...pressures) * 1.5 }, include_isotherms: true, cycle_points: cyclePoints, format: 'json' }; try { const diagramResponse = await apiClient.generateDiagram(diagramRequest); if (diagramResponse.data) setDiagramData(diagramResponse.data); } catch (err) { console.error('Diagram generation (event) failed', err); } finally { setLoading(false); } })(); } catch (err) { // ignore } }; window.addEventListener('cycle:calculated', onCycle as EventListener); return () => window.removeEventListener('cycle:calculated', onCycle as EventListener); }, [selectedRefrigerant]); const handleLoadTestData = () => { const input = document.createElement('input'); input.type = 'file'; input.accept = '.json,.csv'; input.onchange = async (e: any) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (event) => { try { const content = event.target?.result as string; let points: Array<{enthalpy: number, pressure: number}> = []; if (file.name.endsWith('.json')) { const data = JSON.parse(content); points = data.points || data; } else if (file.name.endsWith('.csv')) { const lines = content.split('\n').filter(l => l.trim()); const hasHeader = lines[0].toLowerCase().includes('pressure') || lines[0].toLowerCase().includes('enthalpy'); const dataLines = hasHeader ? lines.slice(1) : lines; points = dataLines.map(line => { const [pressure, enthalpy] = line.split(/[,;\t]/).map(v => parseFloat(v.trim())); return { pressure, enthalpy }; }).filter(p => !isNaN(p.pressure) && !isNaN(p.enthalpy)); } if (points.length > 0) { setTestPoints(points); alert(`✅ ${points.length} test points loaded successfully!`); } else { alert('❌ No valid points found in file'); } } catch (err) { alert('❌ Error reading file: ' + err); } }; reader.readAsText(file); }; input.click(); }; const handleClearTestData = () => { setTestPoints(null); }; return ( <>
{/* LEFT SIDEBAR */}
{/* Header */}

log p-h Diagram

{cycleData && `COP (Heat Pump) = ${cycleData.performance?.cop.toFixed(2) || '-'} / COP (Refrigerator) = ${(cycleData.performance?.cop ? (cycleData.performance.cop - 1).toFixed(2) : '-')}`}
{error && (
{error}
)} {/* Select Refrigerant */}
{/* Define State Cycle */}
{/* Condensing Pressure */}
setCondensingValue(e.target.value)} style={{ flex: 1, height: '5px', background: 'linear-gradient(90deg, rgba(236, 72, 153, 0.4) 0%, rgba(236, 72, 153, 0.9) 100%)', borderRadius: '5px', outline: 'none', cursor: 'pointer', appearance: 'none' }} />
setCondensingValue(e.target.value)} step="1" style={{ width: '50px', background: 'transparent', border: 'none', color: '#ffffff', fontSize: '1.05rem', fontWeight: '700', textAlign: 'right', outline: 'none', padding: 0, margin: 0 }} /> {inputType === 'pressure' ? 'bar' : '°C'}
{/* Evaporating Pressure */}
setEvaporatingValue(e.target.value)} style={{ flex: 1, height: '5px', background: 'linear-gradient(90deg, rgba(59, 130, 246, 0.4) 0%, rgba(59, 130, 246, 0.9) 100%)', borderRadius: '5px', outline: 'none', cursor: 'pointer', appearance: 'none' }} />
setEvaporatingValue(e.target.value)} step="1" style={{ width: '50px', background: 'transparent', border: 'none', color: '#ffffff', fontSize: '1.05rem', fontWeight: '700', textAlign: 'right', outline: 'none', padding: 0, margin: 0 }} /> {inputType === 'pressure' ? 'bar' : '°C'}
{/* Superheating */}
setSuperheating(e.target.value)} style={{ flex: 1, height: '5px', background: 'linear-gradient(90deg, rgba(168, 85, 247, 0.4) 0%, rgba(168, 85, 247, 0.9) 100%)', borderRadius: '5px', outline: 'none', cursor: 'pointer', appearance: 'none' }} />
setSuperheating(e.target.value)} step="1" style={{ width: '50px', background: 'transparent', border: 'none', color: '#ffffff', fontSize: '1.05rem', fontWeight: '700', textAlign: 'right', outline: 'none', padding: 0, margin: 0 }} /> °C
{/* Subcooling */}
setSubcooling(e.target.value)} style={{ flex: 1, height: '5px', background: 'linear-gradient(90deg, rgba(249, 115, 22, 0.4) 0%, rgba(249, 115, 22, 0.9) 100%)', borderRadius: '5px', outline: 'none', cursor: 'pointer', appearance: 'none' }} />
setSubcooling(e.target.value)} step="1" style={{ width: '50px', background: 'transparent', border: 'none', color: '#ffffff', fontSize: '1.05rem', fontWeight: '700', textAlign: 'right', outline: 'none', padding: 0, margin: 0 }} /> °C
{/* Compressor Efficiency */}
setCompressorEfficiency(e.target.value)} style={{ flex: 1, height: '5px', background: 'linear-gradient(90deg, rgba(34, 197, 94, 0.4) 0%, rgba(34, 197, 94, 0.9) 100%)', borderRadius: '5px', outline: 'none', cursor: 'pointer' }} />
{compressorEfficiency} -
{/* Mass Flow Rate */}
setMassFlowRate(e.target.value)} style={{ flex: 1, height: '5px', background: 'linear-gradient(90deg, rgba(251, 146, 60, 0.4) 0%, rgba(251, 146, 60, 0.9) 100%)', borderRadius: '5px', outline: 'none', cursor: 'pointer' }} />
{massFlowRate} kg/s
{/* Calculate Cycle Button removed - auto-update enabled */} {/* Load Test Data Button */}
{testPoints && ( )}
{/* Copyright */}
CC
BY
{/* RIGHT CONTENT AREA */}
{diagramData && cycleData && cycleData.points && cycleData.points.length > 0 ? (
{/* Title with COP */}

log(p)-h chart {cycleData.refrigerant}

{cycleData.performance && (
COP (Heat Pump) = {cycleData.performance.cop.toFixed(2)} / COP (Refrigerator) = {(cycleData.performance.cop - 1).toFixed(2)}
)} {/* Legend */}
Saturation
Isotherms
Cycle
{testPoints && testPoints.length > 0 && (
Test Data ({testPoints.length})
)}
{/* Chart */} {/* Gradient for labels */} p.enthalpy || 0)) - 50, Math.max(...cycleData.points.map(p => p.enthalpy || 0)) + 100 ]} label={{ value: 'Specific Enthalpy [kJ/kg]', position: 'insideBottom', offset: -15, fill: '#64748b', fontSize: 13, fontWeight: 600 }} stroke="#94a3b8" tick={{ fill: '#64748b', fontSize: 11 }} tickFormatter={(value) => Math.round(value).toString()} /> p.pressure)) * 0.7, Math.max(...cycleData.points.map(p => p.pressure)) * 1.3 ]} label={{ value: 'Pressure [bar]', angle: -90, position: 'insideLeft', offset: 10, fill: '#64748b', fontSize: 13, fontWeight: 600 }} stroke="#94a3b8" tick={{ fill: '#64748b', fontSize: 11 }} tickFormatter={(value) => value.toFixed(value < 1 ? 1 : 0)} /> `h = ${Math.round(Number(value))} kJ/kg`} /> {/* Saturation Curve - Black Lines */} {diagramData.saturation_curve && Array.isArray(diagramData.saturation_curve) && diagramData.saturation_curve.length > 0 && ( <> ({ enthalpy: p.enthalpy, pressure: p.pressure }))} type="monotone" dataKey="pressure" stroke="#000000" strokeWidth={2.5} dot={false} name="Liquid Saturation" connectNulls legendType="none" /> ({ enthalpy: p.enthalpy, pressure: p.pressure }))} type="monotone" dataKey="pressure" stroke="#000000" strokeWidth={2.5} dot={false} name="Vapor Saturation" connectNulls legendType="none" /> )} {/* Isotherms - Purple/Magenta Dashed */} {diagramData.isotherms && diagramData.isotherms.map((isotherm: any, idx: number) => { const hue = 280 + (idx * 15); const color = `hsl(${hue % 360}, 65%, 55%)`; const tempLabel = `${Math.round(isotherm.temperature)}°C`; return ( ({ enthalpy: p.enthalpy, pressure: p.pressure, temperature: isotherm.temperature }))} type="monotone" dataKey="pressure" stroke={color} strokeWidth={0.8} strokeDasharray="4 3" dot={false} name={tempLabel} connectNulls legendType="none" > { const { x, y, index } = props; if (index !== isotherm.points.length - 1) return null; return ( {tempLabel} ); }} /> ); })} {/* Thermodynamic Cycle - Cyan */} ({ enthalpy: p.enthalpy || 0, pressure: p.pressure, point: p.point_id, temperature: p.temperature })), { enthalpy: cycleData.points[0].enthalpy || 0, pressure: cycleData.points[0].pressure, point: '1', temperature: cycleData.points[0].temperature } ]} type="linear" dataKey="pressure" stroke="#00CED1" strokeWidth={3} dot={{ fill: '#00CED1', r: 6, strokeWidth: 2, stroke: '#ffffff' }} activeDot={{ r: 8 }} name="Cycle" connectNulls legendType="none" > { const { x, y, index, value } = props; if (index >= cycleData.points.length) return null; const point = cycleData.points[index]; const temp = (point.temperature || 0).toFixed(1); const pressure = point.pressure.toFixed(2); // Position labels FAR OUTSIDE the cycle area // Point 0 (evaporator outlet - bottom left) -> FAR RIGHT // Point 1 (compressor outlet - top left) -> FAR LEFT // Point 2 (condenser outlet - top right) -> FAR RIGHT // Point 3 (expansion valve - bottom right) -> FAR RIGHT let offsetX = 0; let offsetY = 0; if (index === 0) { // Bottom left point -> push to RIGHT and DOWN offsetX = 90; offsetY = 25; } else if (index === 1) { // Top left point -> push to LEFT and UP offsetX = -90; offsetY = -25; } else if (index === 2) { // Top right point -> push to RIGHT and UP offsetX = 90; offsetY = -25; } else if (index === 3) { // Bottom right point -> push to RIGHT and DOWN offsetX = 90; offsetY = 25; } return ( {/* Main label box */} {/* Temperature */} {temp}°C {/* Pressure */} {pressure} bar ); }} /> {/* Test Points - Orange/Red */} {testPoints && testPoints.length > 0 && ( ({ enthalpy: p.enthalpy, pressure: p.pressure }))} type="monotone" dataKey="pressure" stroke="#ff6b35" strokeWidth={0} dot={{ fill: '#ff6b35', r: 5, strokeWidth: 2, stroke: '#ffffff' }} activeDot={{ r: 7 }} name="Test Data" connectNulls={false} /> )} {/* Performance Section Below Chart - ELEGANT WHITE DESIGN */} {cycleData.performance && (

Performance Metrics

{/* COP Card */}
🎯
COP
Coefficient
{cycleData.performance.cop.toFixed(2)}
{/* Cooling Capacity Card */}
❄️
Cooling
Capacity
{(cycleData.performance.cooling_capacity || 0).toFixed(1)} kW
{/* Compressor Power Card */}
⚙️
Power
Compressor
{(cycleData.performance.compressor_power || 0).toFixed(1)} kW
{/* Compression Ratio Card */}
📊
Ratio
Compression
{cycleData.performance.compression_ratio.toFixed(2)}
)}
) : (
Adjust the sliders to update the P-h diagram automatically
)}
); }