- Add Frontend Dockerfile with Next.js standalone build - Add docker-compose.yml for production deployment - Add docker-compose.dev.yml for development with hot-reload - Configure Frontend next.config.js with standalone output - Add .dockerignore files for both backend and frontend - Add comprehensive README-DOCKER.md documentation - Update .gitignore to exclude node_modules and build artifacts - Remove obsolete component files (CycleCalculator.tsx, PHDiagram.tsx) - Backend and Frontend communicate via Docker network - Healthchecks configured for both services - Environment variables configured for API URL
1499 lines
56 KiB
TypeScript
1499 lines
56 KiB
TypeScript
"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<RefrigerantInfo[]>([]);
|
||
const [selectedRefrigerant, setSelectedRefrigerant] = useState('R290');
|
||
const [cycleData, setCycleData] = useState<CycleCalculationResponse | null>(null);
|
||
const [diagramData, setDiagramData] = useState<any>(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<any | null>(null);
|
||
const [resolution, setResolution] = useState<'low' | 'medium' | 'high'>('medium');
|
||
const [debounceMs, setDebounceMs] = useState<number>(400);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [testPoints, setTestPoints] = useState<Array<{enthalpy: number, pressure: number}> | 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 = () => (
|
||
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
|
||
<div style={{ width: 14, height: 14, borderRadius: 7, border: '2px solid rgba(0,0,0,0.1)', borderTopColor: '#3b82f6', animation: 'spin 1s linear infinite' }} />
|
||
<style>{`@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }`}</style>
|
||
</div>
|
||
);
|
||
|
||
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 = () => (
|
||
<div style={{ display: 'flex', gap: '0.75rem', alignItems: 'center' }}>
|
||
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||
<label style={{ color: '#64748b', fontSize: 12 }}>Resolution</label>
|
||
<select value={resolution} onChange={(e) => setResolution(e.target.value as any)} style={{ padding: '6px', borderRadius: 8 }}>
|
||
<option value="low">Low</option>
|
||
<option value="medium">Medium</option>
|
||
<option value="high">High</option>
|
||
</select>
|
||
</div>
|
||
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||
<label style={{ color: '#64748b', fontSize: 12 }}>Debounce</label>
|
||
<select value={debounceMs} onChange={(e) => setDebounceMs(parseInt(e.target.value))} style={{ padding: '6px', borderRadius: 8 }}>
|
||
<option value={400}>400ms</option>
|
||
<option value={600}>600ms</option>
|
||
<option value={800}>800ms</option>
|
||
</select>
|
||
</div>
|
||
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||
{loading && <Spinner />}
|
||
{lastMetadata && (
|
||
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||
<div style={{ color: '#64748b', fontSize: 12 }}>Gen: {lastMetadata.generation_time_ms} ms</div>
|
||
{lastMetadata.generation_time_ms === 0 && (
|
||
<div style={{ background: '#10b981', color: '#fff', padding: '4px 8px', borderRadius: 8, fontSize: 12 }}>Cache hit</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
|
||
// 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 (
|
||
<>
|
||
<style jsx>{`
|
||
input[type="number"]::-webkit-outer-spin-button,
|
||
input[type="number"]::-webkit-inner-spin-button {
|
||
-webkit-appearance: none;
|
||
margin: 0;
|
||
}
|
||
input[type="number"] {
|
||
-moz-appearance: textfield;
|
||
}
|
||
`}</style>
|
||
|
||
<div style={{
|
||
minHeight: '100vh',
|
||
background: 'linear-gradient(135deg, #3a5a77 0%, #4a2c5e 100%)',
|
||
display: 'flex',
|
||
fontFamily: '"Inter", system-ui, -apple-system, sans-serif'
|
||
}}>
|
||
{/* LEFT SIDEBAR */}
|
||
<div style={{
|
||
width: '320px',
|
||
background: 'rgba(42, 74, 95, 0.95)',
|
||
backdropFilter: 'blur(10px)',
|
||
padding: '2rem 1.5rem',
|
||
boxShadow: '4px 0 24px rgba(0,0,0,0.4)',
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
gap: '1.5rem',
|
||
overflowY: 'auto',
|
||
borderRight: '1px solid rgba(255,255,255,0.1)'
|
||
}}>
|
||
{/* Header */}
|
||
<div style={{
|
||
background: 'linear-gradient(135deg, rgba(56, 178, 172, 0.25) 0%, rgba(72, 187, 178, 0.15) 100%)',
|
||
padding: '1rem 1.25rem',
|
||
borderRadius: '12px',
|
||
border: '1px solid rgba(56, 178, 172, 0.4)',
|
||
cursor: 'pointer',
|
||
transition: 'all 0.3s'
|
||
}}>
|
||
<h2 style={{
|
||
color: '#ffffff',
|
||
fontSize: '1.3rem',
|
||
fontWeight: '700',
|
||
margin: '0',
|
||
letterSpacing: '0.3px'
|
||
}}>log p-h Diagram</h2>
|
||
<div style={{
|
||
marginTop: '0.25rem',
|
||
fontSize: '0.7rem',
|
||
color: 'rgba(255,255,255,0.6)',
|
||
fontWeight: '400'
|
||
}}>
|
||
{cycleData && `COP (Heat Pump) = ${cycleData.performance?.cop.toFixed(2) || '-'} / COP (Refrigerator) = ${(cycleData.performance?.cop ? (cycleData.performance.cop - 1).toFixed(2) : '-')}`}
|
||
</div>
|
||
</div>
|
||
|
||
{error && (
|
||
<div style={{
|
||
padding: '0.75rem 1rem',
|
||
background: 'rgba(239, 68, 68, 0.2)',
|
||
border: '1px solid rgba(239, 68, 68, 0.4)',
|
||
borderRadius: '10px',
|
||
color: '#fca5a5',
|
||
fontSize: '0.85rem',
|
||
lineHeight: '1.4'
|
||
}}>
|
||
{error}
|
||
</div>
|
||
)}
|
||
|
||
{/* Select Refrigerant */}
|
||
<div>
|
||
<label style={{
|
||
display: 'block',
|
||
color: '#ffffff',
|
||
fontSize: '0.9rem',
|
||
fontWeight: '600',
|
||
marginBottom: '0.6rem',
|
||
letterSpacing: '0.2px'
|
||
}}>
|
||
Select Refrigerant
|
||
</label>
|
||
<select
|
||
value={selectedRefrigerant}
|
||
onChange={(e) => setSelectedRefrigerant(e.target.value)}
|
||
style={{
|
||
width: '100%',
|
||
padding: '0.8rem 1rem',
|
||
background: 'rgba(15, 23, 42, 0.9)',
|
||
border: '1.5px solid rgba(148, 163, 184, 0.4)',
|
||
borderRadius: '10px',
|
||
color: '#ffffff',
|
||
fontSize: '0.95rem',
|
||
fontWeight: '600',
|
||
cursor: 'pointer',
|
||
outline: 'none',
|
||
transition: 'all 0.2s'
|
||
}}
|
||
>
|
||
{refrigerants.filter(r => r.available).map(ref => (
|
||
<option key={ref.name} value={ref.name} style={{ background: '#0f172a', color: '#ffffff' }}>
|
||
{ref.name}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
|
||
{/* Define State Cycle */}
|
||
<div>
|
||
<label style={{
|
||
display: 'block',
|
||
color: '#ffffff',
|
||
fontSize: '0.9rem',
|
||
fontWeight: '600',
|
||
marginBottom: '0.6rem',
|
||
letterSpacing: '0.2px'
|
||
}}>
|
||
Define State Cycle
|
||
</label>
|
||
<select
|
||
value={inputType}
|
||
onChange={(e) => setInputType(e.target.value as 'temperature' | 'pressure')}
|
||
style={{
|
||
width: '100%',
|
||
padding: '0.8rem 1rem',
|
||
background: 'rgba(15, 23, 42, 0.9)',
|
||
border: '1.5px solid rgba(148, 163, 184, 0.4)',
|
||
borderRadius: '10px',
|
||
color: '#ffffff',
|
||
fontSize: '0.95rem',
|
||
fontWeight: '600',
|
||
cursor: 'pointer',
|
||
outline: 'none'
|
||
}}
|
||
>
|
||
<option value="pressure" style={{ background: '#0f172a' }}>Pressure levels (Evap./Cond.)</option>
|
||
<option value="temperature" style={{ background: '#0f172a' }}>Temperature levels (Evap./Cond.)</option>
|
||
</select>
|
||
</div>
|
||
|
||
{/* Condensing Pressure */}
|
||
<div>
|
||
<label style={{
|
||
display: 'block',
|
||
color: '#ffffff',
|
||
fontSize: '0.9rem',
|
||
fontWeight: '600',
|
||
marginBottom: '0.8rem',
|
||
letterSpacing: '0.2px'
|
||
}}>
|
||
{inputType === 'pressure' ? 'Condensing Pressure' : 'Condensing Temperature'}
|
||
</label>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
|
||
<input
|
||
type="range"
|
||
min={inputType === 'pressure' ? '5' : '20'}
|
||
max={inputType === 'pressure' ? '30' : '70'}
|
||
step="1"
|
||
value={condensingValue}
|
||
onChange={(e) => 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'
|
||
}}
|
||
/>
|
||
<div style={{
|
||
minWidth: '90px',
|
||
padding: '0.6rem 0.9rem',
|
||
background: 'rgba(30, 58, 80, 0.95)',
|
||
border: '1.5px solid rgba(148, 163, 184, 0.35)',
|
||
borderRadius: '10px',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
gap: '0.3rem'
|
||
}}>
|
||
<input
|
||
type="number"
|
||
value={condensingValue}
|
||
onChange={(e) => 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
|
||
}}
|
||
/>
|
||
<span style={{ fontSize: '0.75rem', color: '#94a3b8', fontWeight: '500' }}>
|
||
{inputType === 'pressure' ? 'bar' : '°C'}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Evaporating Pressure */}
|
||
<div>
|
||
<label style={{
|
||
display: 'block',
|
||
color: '#ffffff',
|
||
fontSize: '0.9rem',
|
||
fontWeight: '600',
|
||
marginBottom: '0.8rem',
|
||
letterSpacing: '0.2px'
|
||
}}>
|
||
{inputType === 'pressure' ? 'Evaporating Pressure' : 'Evaporating Temperature'}
|
||
</label>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
|
||
<input
|
||
type="range"
|
||
min={inputType === 'pressure' ? '1' : '-20'}
|
||
max={inputType === 'pressure' ? '15' : '20'}
|
||
step="1"
|
||
value={evaporatingValue}
|
||
onChange={(e) => 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'
|
||
}}
|
||
/>
|
||
<div style={{
|
||
minWidth: '90px',
|
||
padding: '0.6rem 0.9rem',
|
||
background: 'rgba(30, 58, 80, 0.95)',
|
||
border: '1.5px solid rgba(148, 163, 184, 0.35)',
|
||
borderRadius: '10px',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
gap: '0.3rem'
|
||
}}>
|
||
<input
|
||
type="number"
|
||
value={evaporatingValue}
|
||
onChange={(e) => 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
|
||
}}
|
||
/>
|
||
<span style={{ fontSize: '0.75rem', color: '#94a3b8', fontWeight: '500' }}>
|
||
{inputType === 'pressure' ? 'bar' : '°C'}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Superheating */}
|
||
<div>
|
||
<label style={{
|
||
display: 'block',
|
||
color: '#ffffff',
|
||
fontSize: '0.9rem',
|
||
fontWeight: '600',
|
||
marginBottom: '0.8rem',
|
||
letterSpacing: '0.2px'
|
||
}}>
|
||
Superheating
|
||
</label>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
|
||
<input
|
||
type="range"
|
||
min="0"
|
||
max="20"
|
||
step="1"
|
||
value={superheating}
|
||
onChange={(e) => 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'
|
||
}}
|
||
/>
|
||
<div style={{
|
||
minWidth: '90px',
|
||
padding: '0.6rem 0.9rem',
|
||
background: 'rgba(30, 58, 80, 0.95)',
|
||
border: '1.5px solid rgba(148, 163, 184, 0.35)',
|
||
borderRadius: '10px',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
gap: '0.3rem'
|
||
}}>
|
||
<input
|
||
type="number"
|
||
value={superheating}
|
||
onChange={(e) => 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
|
||
}}
|
||
/>
|
||
<span style={{ fontSize: '0.75rem', color: '#94a3b8', fontWeight: '500' }}>
|
||
°C
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Subcooling */}
|
||
<div>
|
||
<label style={{
|
||
display: 'block',
|
||
color: '#ffffff',
|
||
fontSize: '0.9rem',
|
||
fontWeight: '600',
|
||
marginBottom: '0.8rem',
|
||
letterSpacing: '0.2px'
|
||
}}>
|
||
Subcooling
|
||
</label>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
|
||
<input
|
||
type="range"
|
||
min="0"
|
||
max="20"
|
||
step="1"
|
||
value={subcooling}
|
||
onChange={(e) => 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'
|
||
}}
|
||
/>
|
||
<div style={{
|
||
minWidth: '90px',
|
||
padding: '0.6rem 0.9rem',
|
||
background: 'rgba(30, 58, 80, 0.95)',
|
||
border: '1.5px solid rgba(148, 163, 184, 0.35)',
|
||
borderRadius: '10px',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
gap: '0.3rem'
|
||
}}>
|
||
<input
|
||
type="number"
|
||
value={subcooling}
|
||
onChange={(e) => 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
|
||
}}
|
||
/>
|
||
<span style={{ fontSize: '0.75rem', color: '#94a3b8', fontWeight: '500' }}>
|
||
°C
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Compressor Efficiency */}
|
||
<div>
|
||
<label style={{
|
||
display: 'block',
|
||
color: '#ffffff',
|
||
fontSize: '0.9rem',
|
||
fontWeight: '600',
|
||
marginBottom: '0.8rem',
|
||
letterSpacing: '0.2px'
|
||
}}>
|
||
Compressor Efficiency
|
||
</label>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
|
||
<input
|
||
type="range"
|
||
min="0.5"
|
||
max="1"
|
||
step="0.05"
|
||
value={compressorEfficiency}
|
||
onChange={(e) => 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'
|
||
}}
|
||
/>
|
||
<div style={{
|
||
minWidth: '75px',
|
||
padding: '0.6rem 0.9rem',
|
||
background: 'rgba(30, 58, 80, 0.95)',
|
||
border: '1.5px solid rgba(148, 163, 184, 0.35)',
|
||
borderRadius: '10px',
|
||
color: '#ffffff',
|
||
fontSize: '1.05rem',
|
||
fontWeight: '700',
|
||
textAlign: 'center'
|
||
}}>
|
||
{compressorEfficiency} <span style={{ fontSize: '0.75rem', color: '#94a3b8', fontWeight: '500' }}>-</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Mass Flow Rate */}
|
||
<div>
|
||
<label style={{
|
||
display: 'block',
|
||
color: '#ffffff',
|
||
fontSize: '0.9rem',
|
||
fontWeight: '600',
|
||
marginBottom: '0.8rem',
|
||
letterSpacing: '0.2px'
|
||
}}>
|
||
Mass Flow Rate
|
||
</label>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
|
||
<input
|
||
type="range"
|
||
min="0.1"
|
||
max="5"
|
||
step="0.1"
|
||
value={massFlowRate}
|
||
onChange={(e) => 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'
|
||
}}
|
||
/>
|
||
<div style={{
|
||
minWidth: '75px',
|
||
padding: '0.6rem 0.9rem',
|
||
background: 'rgba(30, 58, 80, 0.95)',
|
||
border: '1.5px solid rgba(148, 163, 184, 0.35)',
|
||
borderRadius: '10px',
|
||
color: '#ffffff',
|
||
fontSize: '1.05rem',
|
||
fontWeight: '700',
|
||
textAlign: 'center'
|
||
}}>
|
||
{massFlowRate} <span style={{ fontSize: '0.75rem', color: '#94a3b8', fontWeight: '500' }}>kg/s</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Calculate Cycle Button removed - auto-update enabled */}
|
||
|
||
{/* Load Test Data Button */}
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
|
||
<button
|
||
onClick={handleLoadTestData}
|
||
style={{
|
||
width: '100%',
|
||
padding: '1rem 1.5rem',
|
||
background: 'linear-gradient(135deg, rgba(56, 178, 172, 0.3) 0%, rgba(44, 122, 123, 0.4) 100%)',
|
||
border: '1.5px solid rgba(56, 178, 172, 0.5)',
|
||
borderRadius: '12px',
|
||
color: '#ffffff',
|
||
fontSize: '0.95rem',
|
||
fontWeight: '700',
|
||
cursor: 'pointer',
|
||
transition: 'all 0.3s',
|
||
letterSpacing: '0.5px',
|
||
marginTop: '0.5rem',
|
||
textTransform: 'uppercase'
|
||
}}
|
||
>
|
||
📁 Load Test Data
|
||
</button>
|
||
|
||
{testPoints && (
|
||
<button
|
||
onClick={handleClearTestData}
|
||
style={{
|
||
width: '100%',
|
||
padding: '0.75rem 1rem',
|
||
background: 'rgba(239, 68, 68, 0.2)',
|
||
border: '1px solid rgba(239, 68, 68, 0.5)',
|
||
borderRadius: '8px',
|
||
color: '#fca5a5',
|
||
fontSize: '0.85rem',
|
||
fontWeight: '600',
|
||
cursor: 'pointer',
|
||
transition: 'all 0.3s'
|
||
}}
|
||
>
|
||
🗑️ Clear Test Data ({testPoints.length} points)
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
{/* Copyright */}
|
||
<div style={{
|
||
marginTop: 'auto',
|
||
paddingTop: '1.5rem',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: '0.5rem',
|
||
fontSize: '0.75rem',
|
||
color: 'rgba(255,255,255,0.5)'
|
||
}}>
|
||
<div style={{
|
||
display: 'inline-flex',
|
||
padding: '0.3rem 0.5rem',
|
||
background: 'rgba(255, 255, 255, 0.15)',
|
||
borderRadius: '6px',
|
||
gap: '0.25rem',
|
||
alignItems: 'center'
|
||
}}>
|
||
<span style={{ fontWeight: '700' }}>CC</span>
|
||
</div>
|
||
<span style={{ fontWeight: '500' }}>BY</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* RIGHT CONTENT AREA */}
|
||
<div style={{
|
||
flex: 1,
|
||
padding: '2.5rem',
|
||
overflowY: 'auto',
|
||
background: '#f8fafc'
|
||
}}>
|
||
{diagramData && cycleData && cycleData.points && cycleData.points.length > 0 ? (
|
||
<div style={{
|
||
background: '#ffffff',
|
||
borderRadius: '16px',
|
||
padding: '2rem',
|
||
boxShadow: '0 4px 24px rgba(0,0,0,0.08)',
|
||
border: '1px solid rgba(0,0,0,0.05)'
|
||
}}>
|
||
{/* Title with COP */}
|
||
<div style={{ marginBottom: '1.5rem', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||
<h3 style={{
|
||
color: '#1e293b',
|
||
fontSize: '1.5rem',
|
||
fontWeight: '700',
|
||
margin: 0
|
||
}}>
|
||
log(p)-h chart {cycleData.refrigerant}
|
||
</h3>
|
||
<button style={{
|
||
padding: '0.6rem 1rem',
|
||
background: '#ec4899',
|
||
border: 'none',
|
||
borderRadius: '50px',
|
||
color: '#ffffff',
|
||
fontSize: '1.2rem',
|
||
fontWeight: '600',
|
||
cursor: 'pointer',
|
||
boxShadow: '0 2px 8px rgba(236, 72, 153, 0.3)'
|
||
}}>
|
||
⚙
|
||
</button>
|
||
</div>
|
||
|
||
{cycleData.performance && (
|
||
<div style={{
|
||
fontSize: '0.85rem',
|
||
color: '#64748b',
|
||
marginBottom: '1.5rem',
|
||
fontWeight: '500'
|
||
}}>
|
||
COP (Heat Pump) = {cycleData.performance.cop.toFixed(2)} / COP (Refrigerator) = {(cycleData.performance.cop - 1).toFixed(2)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Legend */}
|
||
<div style={{
|
||
display: 'flex',
|
||
gap: '1.5rem',
|
||
flexWrap: 'wrap',
|
||
marginBottom: '1rem',
|
||
padding: '0.75rem 1rem',
|
||
background: 'rgba(241, 245, 249, 0.8)',
|
||
borderRadius: '10px',
|
||
fontSize: '0.8rem',
|
||
fontWeight: '600'
|
||
}}>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||
<div style={{ width: '20px', height: '3px', background: '#000000', borderRadius: '2px' }}></div>
|
||
<span style={{ color: '#1e293b' }}>Saturation</span>
|
||
</div>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||
<div style={{ width: '20px', height: '2px', background: '#c026d3', borderRadius: '2px', borderTop: '2px dashed #c026d3' }}></div>
|
||
<span style={{ color: '#1e293b' }}>Isotherms</span>
|
||
</div>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||
<div style={{ width: '20px', height: '3px', background: '#00CED1', borderRadius: '2px' }}></div>
|
||
<span style={{ color: '#1e293b' }}>Cycle</span>
|
||
</div>
|
||
{testPoints && testPoints.length > 0 && (
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||
<div style={{ width: '10px', height: '10px', background: '#ff6b35', borderRadius: '50%', border: '2px solid #ffffff' }}></div>
|
||
<span style={{ color: '#1e293b' }}>Test Data ({testPoints.length})</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Chart */}
|
||
<ResponsiveContainer width="100%" height={650}>
|
||
<LineChart margin={{ top: 20, right: 120, left: 70, bottom: 70 }}>
|
||
<defs>
|
||
{/* Gradient for labels */}
|
||
<linearGradient id="labelGradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
||
<stop offset="0%" stopColor="#ffffff" stopOpacity="1" />
|
||
<stop offset="100%" stopColor="#f0f9ff" stopOpacity="1" />
|
||
</linearGradient>
|
||
</defs>
|
||
<CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" />
|
||
<XAxis
|
||
type="number"
|
||
dataKey="enthalpy"
|
||
name="Enthalpy"
|
||
domain={[
|
||
Math.min(...cycleData.points.map(p => 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()}
|
||
/>
|
||
<YAxis
|
||
type="number"
|
||
dataKey="pressure"
|
||
name="Pressure"
|
||
scale="log"
|
||
domain={[
|
||
Math.min(...cycleData.points.map(p => 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)}
|
||
/>
|
||
<Tooltip
|
||
contentStyle={{ backgroundColor: '#ffffff', border: '1px solid #e2e8f0', borderRadius: '8px', color: '#1e293b', boxShadow: '0 4px 12px rgba(0,0,0,0.1)' }}
|
||
labelFormatter={(value) => `h = ${Math.round(Number(value))} kJ/kg`}
|
||
/>
|
||
|
||
{/* Saturation Curve - Black Lines */}
|
||
{diagramData.saturation_curve && Array.isArray(diagramData.saturation_curve) && diagramData.saturation_curve.length > 0 && (
|
||
<>
|
||
<Line
|
||
key="saturation-liquid"
|
||
data={diagramData.saturation_curve.slice(0, Math.floor(diagramData.saturation_curve.length / 2)).map((p: any) => ({ enthalpy: p.enthalpy, pressure: p.pressure }))}
|
||
type="monotone"
|
||
dataKey="pressure"
|
||
stroke="#000000"
|
||
strokeWidth={2.5}
|
||
dot={false}
|
||
name="Liquid Saturation"
|
||
connectNulls
|
||
legendType="none"
|
||
/>
|
||
<Line
|
||
key="saturation-vapor"
|
||
data={diagramData.saturation_curve.slice(Math.floor(diagramData.saturation_curve.length / 2)).map((p: any) => ({ 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 (
|
||
<Line
|
||
key={`isotherm-${idx}`}
|
||
data={isotherm.points.map((p: any) => ({
|
||
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"
|
||
>
|
||
<LabelList
|
||
dataKey="pressure"
|
||
position="right"
|
||
content={(props: any) => {
|
||
const { x, y, index } = props;
|
||
if (index !== isotherm.points.length - 1) return null;
|
||
return (
|
||
<text
|
||
x={Number(x) + 10}
|
||
y={Number(y)}
|
||
fill={color}
|
||
fontSize={10}
|
||
fontWeight="600"
|
||
style={{ textShadow: '0 0 4px #fff' }}
|
||
>
|
||
{tempLabel}
|
||
</text>
|
||
);
|
||
}}
|
||
/>
|
||
</Line>
|
||
);
|
||
})}
|
||
|
||
{/* Thermodynamic Cycle - Cyan */}
|
||
<Line
|
||
data={[
|
||
...cycleData.points.map(p => ({
|
||
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"
|
||
>
|
||
<LabelList
|
||
content={(props: any) => {
|
||
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 (
|
||
<g transform={`translate(${offsetX}, ${offsetY})`}>
|
||
{/* Main label box */}
|
||
<rect
|
||
x={Number(x) - 50}
|
||
y={Number(y) - 28}
|
||
width="100"
|
||
height="50"
|
||
fill="white"
|
||
stroke="#00CED1"
|
||
strokeWidth="2.5"
|
||
rx="6"
|
||
opacity="0.98"
|
||
filter="drop-shadow(0 2px 8px rgba(0,0,0,0.15))"
|
||
/>
|
||
{/* Temperature */}
|
||
<text
|
||
x={Number(x)}
|
||
y={Number(y) - 8}
|
||
textAnchor="middle"
|
||
fill="#0f172a"
|
||
fontSize="15"
|
||
fontWeight="800"
|
||
>
|
||
{temp}°C
|
||
</text>
|
||
{/* Pressure */}
|
||
<text
|
||
x={Number(x)}
|
||
y={Number(y) + 10}
|
||
textAnchor="middle"
|
||
fill="#475569"
|
||
fontSize="13"
|
||
fontWeight="700"
|
||
>
|
||
{pressure} bar
|
||
</text>
|
||
</g>
|
||
);
|
||
}}
|
||
/>
|
||
</Line>
|
||
|
||
{/* Test Points - Orange/Red */}
|
||
{testPoints && testPoints.length > 0 && (
|
||
<Line
|
||
data={testPoints.map(p => ({
|
||
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}
|
||
/>
|
||
)}
|
||
</LineChart>
|
||
</ResponsiveContainer>
|
||
|
||
{/* Performance Section Below Chart - ELEGANT WHITE DESIGN */}
|
||
{cycleData.performance && (
|
||
<div style={{ marginTop: '2rem' }}>
|
||
<h3 style={{
|
||
color: '#1e293b',
|
||
fontSize: '1.5rem',
|
||
fontWeight: '700',
|
||
marginBottom: '1.5rem',
|
||
marginTop: 0,
|
||
letterSpacing: '-0.025em',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: '0.5rem'
|
||
}}>
|
||
<span style={{ fontSize: '1.75rem' }}>⚡</span>
|
||
Performance Metrics
|
||
</h3>
|
||
<div style={{
|
||
display: 'grid',
|
||
gridTemplateColumns: 'repeat(auto-fit, minmax(240px, 1fr))',
|
||
gap: '1.25rem'
|
||
}}>
|
||
{/* COP Card */}
|
||
<div style={{
|
||
background: '#ffffff',
|
||
padding: '1.5rem',
|
||
borderRadius: '16px',
|
||
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
|
||
border: '1px solid rgba(0, 0, 0, 0.05)',
|
||
transition: 'all 0.3s ease'
|
||
}}>
|
||
<div style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: '0.75rem',
|
||
marginBottom: '1rem'
|
||
}}>
|
||
<div style={{
|
||
width: '48px',
|
||
height: '48px',
|
||
borderRadius: '12px',
|
||
background: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
fontSize: '1.5rem',
|
||
boxShadow: '0 4px 12px rgba(16, 185, 129, 0.3)'
|
||
}}>
|
||
🎯
|
||
</div>
|
||
<div>
|
||
<div style={{
|
||
color: '#64748b',
|
||
fontSize: '0.75rem',
|
||
fontWeight: '600',
|
||
textTransform: 'uppercase',
|
||
letterSpacing: '0.05em'
|
||
}}>
|
||
COP
|
||
</div>
|
||
<div style={{
|
||
fontSize: '0.875rem',
|
||
color: '#94a3b8',
|
||
fontWeight: '500'
|
||
}}>
|
||
Coefficient
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div style={{
|
||
fontSize: '2.25rem',
|
||
fontWeight: '800',
|
||
color: '#10b981',
|
||
lineHeight: 1,
|
||
letterSpacing: '-0.025em'
|
||
}}>
|
||
{cycleData.performance.cop.toFixed(2)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Cooling Capacity Card */}
|
||
<div style={{
|
||
background: '#ffffff',
|
||
padding: '1.5rem',
|
||
borderRadius: '16px',
|
||
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
|
||
border: '1px solid rgba(0, 0, 0, 0.05)',
|
||
transition: 'all 0.3s ease'
|
||
}}>
|
||
<div style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: '0.75rem',
|
||
marginBottom: '1rem'
|
||
}}>
|
||
<div style={{
|
||
width: '48px',
|
||
height: '48px',
|
||
borderRadius: '12px',
|
||
background: 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
fontSize: '1.5rem',
|
||
boxShadow: '0 4px 12px rgba(59, 130, 246, 0.3)'
|
||
}}>
|
||
❄️
|
||
</div>
|
||
<div>
|
||
<div style={{
|
||
color: '#64748b',
|
||
fontSize: '0.75rem',
|
||
fontWeight: '600',
|
||
textTransform: 'uppercase',
|
||
letterSpacing: '0.05em'
|
||
}}>
|
||
Cooling
|
||
</div>
|
||
<div style={{
|
||
fontSize: '0.875rem',
|
||
color: '#94a3b8',
|
||
fontWeight: '500'
|
||
}}>
|
||
Capacity
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div style={{
|
||
fontSize: '2.25rem',
|
||
fontWeight: '800',
|
||
color: '#3b82f6',
|
||
lineHeight: 1,
|
||
letterSpacing: '-0.025em'
|
||
}}>
|
||
{(cycleData.performance.cooling_capacity || 0).toFixed(1)}
|
||
<span style={{
|
||
fontSize: '1rem',
|
||
color: '#94a3b8',
|
||
fontWeight: '600',
|
||
marginLeft: '0.5rem'
|
||
}}>
|
||
kW
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Compressor Power Card */}
|
||
<div style={{
|
||
background: '#ffffff',
|
||
padding: '1.5rem',
|
||
borderRadius: '16px',
|
||
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
|
||
border: '1px solid rgba(0, 0, 0, 0.05)',
|
||
transition: 'all 0.3s ease'
|
||
}}>
|
||
<div style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: '0.75rem',
|
||
marginBottom: '1rem'
|
||
}}>
|
||
<div style={{
|
||
width: '48px',
|
||
height: '48px',
|
||
borderRadius: '12px',
|
||
background: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
fontSize: '1.5rem',
|
||
boxShadow: '0 4px 12px rgba(239, 68, 68, 0.3)'
|
||
}}>
|
||
⚙️
|
||
</div>
|
||
<div>
|
||
<div style={{
|
||
color: '#64748b',
|
||
fontSize: '0.75rem',
|
||
fontWeight: '600',
|
||
textTransform: 'uppercase',
|
||
letterSpacing: '0.05em'
|
||
}}>
|
||
Power
|
||
</div>
|
||
<div style={{
|
||
fontSize: '0.875rem',
|
||
color: '#94a3b8',
|
||
fontWeight: '500'
|
||
}}>
|
||
Compressor
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div style={{
|
||
fontSize: '2.25rem',
|
||
fontWeight: '800',
|
||
color: '#ef4444',
|
||
lineHeight: 1,
|
||
letterSpacing: '-0.025em'
|
||
}}>
|
||
{(cycleData.performance.compressor_power || 0).toFixed(1)}
|
||
<span style={{
|
||
fontSize: '1rem',
|
||
color: '#94a3b8',
|
||
fontWeight: '600',
|
||
marginLeft: '0.5rem'
|
||
}}>
|
||
kW
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Compression Ratio Card */}
|
||
<div style={{
|
||
background: '#ffffff',
|
||
padding: '1.5rem',
|
||
borderRadius: '16px',
|
||
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
|
||
border: '1px solid rgba(0, 0, 0, 0.05)',
|
||
transition: 'all 0.3s ease'
|
||
}}>
|
||
<div style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: '0.75rem',
|
||
marginBottom: '1rem'
|
||
}}>
|
||
<div style={{
|
||
width: '48px',
|
||
height: '48px',
|
||
borderRadius: '12px',
|
||
background: 'linear-gradient(135deg, #06b6d4 0%, #0891b2 100%)',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
fontSize: '1.5rem',
|
||
boxShadow: '0 4px 12px rgba(6, 182, 212, 0.3)'
|
||
}}>
|
||
📊
|
||
</div>
|
||
<div>
|
||
<div style={{
|
||
color: '#64748b',
|
||
fontSize: '0.75rem',
|
||
fontWeight: '600',
|
||
textTransform: 'uppercase',
|
||
letterSpacing: '0.05em'
|
||
}}>
|
||
Ratio
|
||
</div>
|
||
<div style={{
|
||
fontSize: '0.875rem',
|
||
color: '#94a3b8',
|
||
fontWeight: '500'
|
||
}}>
|
||
Compression
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div style={{
|
||
fontSize: '2.25rem',
|
||
fontWeight: '800',
|
||
color: '#06b6d4',
|
||
lineHeight: 1,
|
||
letterSpacing: '-0.025em'
|
||
}}>
|
||
{cycleData.performance.compression_ratio.toFixed(2)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
) : (
|
||
<div style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
minHeight: '600px',
|
||
background: '#ffffff',
|
||
borderRadius: '16px',
|
||
border: '2px dashed #cbd5e1',
|
||
color: '#94a3b8',
|
||
fontSize: '1.1rem',
|
||
fontWeight: '600'
|
||
}}>
|
||
Adjust the sliders to update the P-h diagram automatically
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</>
|
||
);
|
||
}
|