- 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
1068 lines
39 KiB
TypeScript
1068 lines
39 KiB
TypeScript
"use client";
|
||
|
||
import { useState, useEffect, useCallback } from 'react';
|
||
import { apiClient } from '@/lib/api-client';
|
||
import type { CycleCalculationRequest, CycleCalculationResponse, RefrigerantInfo } from '@/types/api';
|
||
|
||
export default function CycleCalculatorModern() {
|
||
const [refrigerants, setRefrigerants] = useState<RefrigerantInfo[]>([]);
|
||
const [selectedRefrigerant, setSelectedRefrigerant] = useState('R290');
|
||
const [cycleData, setCycleData] = useState<CycleCalculationResponse | null>(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 [massFlowRate, setMassFlowRate] = useState('1.0');
|
||
const [loading, setLoading] = useState(false);
|
||
const [error, setError] = useState<string | null>(null);
|
||
|
||
useEffect(() => {
|
||
apiClient.getRefrigerants()
|
||
.then(setRefrigerants)
|
||
.catch(err => {
|
||
console.error('Failed to load refrigerants:', err);
|
||
setError('Cannot connect to API on port 8001');
|
||
});
|
||
}, []);
|
||
|
||
const handleCalculate = useCallback(async () => {
|
||
if (!selectedRefrigerant) return;
|
||
setLoading(true);
|
||
setError(null);
|
||
|
||
try {
|
||
const req: CycleCalculationRequest = {
|
||
refrigerant: selectedRefrigerant,
|
||
superheat: parseFloat(superheating) || 0,
|
||
subcool: parseFloat(subcooling) || 0,
|
||
compressor_efficiency: 0.7,
|
||
mass_flow: parseFloat(massFlowRate) || 1.0,
|
||
} as CycleCalculationRequest;
|
||
|
||
if (inputType === 'temperature') {
|
||
req.evap_temperature = parseFloat(evaporatingValue) || 0;
|
||
req.cond_temperature = parseFloat(condensingValue) || 0;
|
||
} else {
|
||
req.evap_pressure = parseFloat(evaporatingValue) || 1;
|
||
req.cond_pressure = parseFloat(condensingValue) || 1;
|
||
}
|
||
|
||
const response = await apiClient.calculateCycle(req);
|
||
setCycleData(response);
|
||
|
||
// dispatch overlay event
|
||
try {
|
||
if (response && response.points && response.points.length > 0) {
|
||
const cyclePoints = response.points.map(p => ({ enthalpy: p.enthalpy || 0, pressure: p.pressure }));
|
||
const ev = new CustomEvent('cycle:calculated', { detail: { refrigerant: selectedRefrigerant, points: cyclePoints } });
|
||
window.dispatchEvent(ev);
|
||
}
|
||
} catch (_) {}
|
||
|
||
} catch (err: any) {
|
||
setError(err?.message || String(err));
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, [selectedRefrigerant, inputType, evaporatingValue, condensingValue, massFlowRate, superheating, subcooling]);
|
||
|
||
// debounce 200ms
|
||
useEffect(() => {
|
||
let mounted = true;
|
||
const id = setTimeout(() => { if (mounted) handleCalculate(); }, 200);
|
||
return () => { mounted = false; clearTimeout(id); };
|
||
}, [selectedRefrigerant, inputType, evaporatingValue, condensingValue, massFlowRate, superheating, subcooling, handleCalculate]);
|
||
|
||
// Auto-calculate on initial load
|
||
useEffect(() => {
|
||
if (refrigerants.length > 0 && selectedRefrigerant && !cycleData) {
|
||
handleCalculate();
|
||
}
|
||
}, [refrigerants.length, selectedRefrigerant]);
|
||
|
||
const coolingCapacity = cycleData?.performance?.cooling_capacity ?? 0;
|
||
const compressorPower = cycleData?.performance?.compressor_power ?? 0;
|
||
|
||
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' }}>
|
||
{/* SIDEBAR - exact copy from PHDiagramModern */}
|
||
<aside 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 PILL - exact copy */}
|
||
<div style={{
|
||
background: 'linear-gradient(135deg, rgba(37, 99, 235, 0.18) 0%, rgba(59, 130, 246, 0.08) 100%)',
|
||
padding: '1rem 1.25rem',
|
||
borderRadius: '12px',
|
||
border: '1px solid rgba(59, 130, 246, 0.26)',
|
||
cursor: 'pointer',
|
||
transition: 'all 0.3s'
|
||
}}>
|
||
<h2 style={{
|
||
color: '#ffffff',
|
||
fontSize: '1.3rem',
|
||
fontWeight: '700',
|
||
margin: '0',
|
||
letterSpacing: '0.3px'
|
||
}}>Cycle Calculator</h2>
|
||
<div style={{
|
||
marginTop: '0.25rem',
|
||
fontSize: '0.75rem',
|
||
color: 'rgba(255,255,255,0.7)',
|
||
fontWeight: '400'
|
||
}}>
|
||
{cycleData && `COP = ${cycleData.performance?.cop?.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>
|
||
)}
|
||
|
||
<div>
|
||
<label style={{ display: 'block', color: '#ffffff', fontSize: '0.9rem', fontWeight: '600', marginBottom: '0.6rem', letterSpacing: '0.2px' }}>
|
||
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',
|
||
marginTop: 8
|
||
}}
|
||
>
|
||
{refrigerants.filter(r => r.available).map(r => (
|
||
<option key={r.name} value={r.name} style={{ background: '#0f172a' }}>{r.name}{r.formula ? ` - ${r.formula}` : ''}</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
|
||
<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/Temperature - EXACT STYLE from PHDiagramModern */}
|
||
<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={inputType === 'pressure' ? '1' : '5'}
|
||
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,
|
||
WebkitAppearance: 'none',
|
||
MozAppearance: 'textfield'
|
||
}}
|
||
/>
|
||
<span style={{ fontSize: '0.75rem', color: '#94a3b8', fontWeight: '500' }}>
|
||
{inputType === 'pressure' ? 'bar' : '°C'}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Evaporating Pressure/Temperature - EXACT STYLE from PHDiagramModern */}
|
||
<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' : '-40'}
|
||
max={inputType === 'pressure' ? '15' : '40'}
|
||
step={inputType === 'pressure' ? '1' : '5'}
|
||
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,
|
||
WebkitAppearance: 'none',
|
||
MozAppearance: 'textfield'
|
||
}}
|
||
/>
|
||
<span style={{ fontSize: '0.75rem', color: '#94a3b8', fontWeight: '500' }}>
|
||
{inputType === 'pressure' ? 'bar' : '°C'}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Mass Flow Rate - EXACT STYLE from PHDiagramModern */}
|
||
<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(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={massFlowRate}
|
||
onChange={(e) => setMassFlowRate(e.target.value)}
|
||
step="0.1"
|
||
style={{
|
||
width: '50px',
|
||
background: 'transparent',
|
||
border: 'none',
|
||
color: '#ffffff',
|
||
fontSize: '1.05rem',
|
||
fontWeight: '700',
|
||
textAlign: 'right',
|
||
outline: 'none',
|
||
padding: 0,
|
||
margin: 0,
|
||
WebkitAppearance: 'none',
|
||
MozAppearance: 'textfield'
|
||
}}
|
||
/>
|
||
<span style={{ fontSize: '0.75rem', color: '#94a3b8', fontWeight: '500' }}>
|
||
kg/s
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Superheating - EXACT STYLE from PHDiagramModern */}
|
||
<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(34, 197, 94, 0.4) 0%, rgba(34, 197, 94, 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,
|
||
WebkitAppearance: 'none',
|
||
MozAppearance: 'textfield'
|
||
}}
|
||
/>
|
||
<span style={{ fontSize: '0.75rem', color: '#94a3b8', fontWeight: '500' }}>
|
||
°C
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Subcooling - EXACT STYLE from PHDiagramModern */}
|
||
<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,
|
||
WebkitAppearance: 'none',
|
||
MozAppearance: 'textfield'
|
||
}}
|
||
/>
|
||
<span style={{ fontSize: '0.75rem', color: '#94a3b8', fontWeight: '500' }}>
|
||
°C
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{ marginTop: 'auto' }}>
|
||
<div style={{ fontSize: 12, color: 'rgba(255,255,255,0.6)' }}>Auto-update enabled (debounced)</div>
|
||
</div>
|
||
</aside>
|
||
|
||
{/* MAIN AREA - EXACT COPY from PHDiagramModern structure */}
|
||
<main style={{
|
||
flex: 1,
|
||
padding: '2rem',
|
||
overflowY: 'auto',
|
||
background: '#f8fafc'
|
||
}}>
|
||
{cycleData && (
|
||
<div style={{ maxWidth: '1400px', margin: '0 auto' }}>
|
||
{/* Header section - EXACT COPY from PHDiagramModern */}
|
||
<div style={{
|
||
display: 'flex',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
marginBottom: '1.5rem'
|
||
}}>
|
||
<h3 style={{
|
||
color: '#1e293b',
|
||
fontSize: '1.5rem',
|
||
fontWeight: '700',
|
||
margin: 0
|
||
}}>
|
||
Thermodynamic Cycle {cycleData.refrigerant}
|
||
</h3>
|
||
</div>
|
||
|
||
{/* COP Info - EXACT COPY from PHDiagramModern */}
|
||
{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>
|
||
)}
|
||
|
||
{/* Performance Cards - ELEGANT WHITE DESIGN */}
|
||
<div style={{ marginBottom: '2.5rem' }}>
|
||
<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',
|
||
cursor: 'default'
|
||
}}>
|
||
<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'
|
||
}}>
|
||
{coolingCapacity.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'
|
||
}}>
|
||
{compressorPower.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>
|
||
|
||
{/* Cycle State Points - ELEGANT WHITE CARDS */}
|
||
<div style={{ marginBottom: '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>
|
||
Cycle State Points
|
||
</h3>
|
||
<div style={{
|
||
display: 'grid',
|
||
gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))',
|
||
gap: '1.25rem'
|
||
}}>
|
||
{cycleData.points.map((p, index) => {
|
||
// Define icons and colors for each point
|
||
const pointStyles = [
|
||
{ icon: '🌡️', color: '#3b82f6', bg: 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)', label: 'Point 1' },
|
||
{ icon: '🔥', color: '#ef4444', bg: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)', label: 'Point 2' },
|
||
{ icon: '💧', color: '#10b981', bg: 'linear-gradient(135deg, #10b981 0%, #059669 100%)', label: 'Point 3' },
|
||
{ icon: '❄️', color: '#06b6d4', bg: 'linear-gradient(135deg, #06b6d4 0%, #0891b2 100%)', label: 'Point 4' }
|
||
];
|
||
const style = pointStyles[index] || pointStyles[0];
|
||
|
||
return (
|
||
<div
|
||
key={p.point_id}
|
||
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',
|
||
position: 'relative',
|
||
overflow: 'hidden'
|
||
}}
|
||
>
|
||
{/* Decorative gradient bar */}
|
||
<div style={{
|
||
position: 'absolute',
|
||
top: 0,
|
||
left: 0,
|
||
right: 0,
|
||
height: '4px',
|
||
background: style.bg
|
||
}} />
|
||
|
||
{/* Header with icon */}
|
||
<div style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: '0.75rem',
|
||
marginBottom: '1rem',
|
||
paddingTop: '0.25rem'
|
||
}}>
|
||
<div style={{
|
||
width: '40px',
|
||
height: '40px',
|
||
borderRadius: '10px',
|
||
background: style.bg,
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
fontSize: '1.25rem',
|
||
boxShadow: `0 4px 12px ${style.color}40`
|
||
}}>
|
||
{style.icon}
|
||
</div>
|
||
<div style={{ flex: 1 }}>
|
||
<div style={{
|
||
fontSize: '0.875rem',
|
||
fontWeight: '700',
|
||
color: '#1e293b',
|
||
marginBottom: '0.125rem'
|
||
}}>
|
||
{p.description}
|
||
</div>
|
||
<div style={{
|
||
fontSize: '0.75rem',
|
||
color: '#94a3b8',
|
||
fontWeight: '500'
|
||
}}>
|
||
{style.label}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Properties Grid */}
|
||
<div style={{
|
||
display: 'grid',
|
||
gap: '0.75rem'
|
||
}}>
|
||
{/* Pressure */}
|
||
<div style={{
|
||
display: 'flex',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
padding: '0.75rem',
|
||
background: '#f8fafc',
|
||
borderRadius: '10px',
|
||
border: '1px solid #e2e8f0'
|
||
}}>
|
||
<span style={{
|
||
fontSize: '0.875rem',
|
||
fontWeight: '600',
|
||
color: '#64748b'
|
||
}}>
|
||
Pressure
|
||
</span>
|
||
<span style={{
|
||
fontSize: '1rem',
|
||
fontWeight: '700',
|
||
color: '#1e293b'
|
||
}}>
|
||
{p.pressure.toFixed(2)} <span style={{ fontSize: '0.875rem', color: '#94a3b8', fontWeight: '600' }}>bar</span>
|
||
</span>
|
||
</div>
|
||
|
||
{/* Temperature */}
|
||
<div style={{
|
||
display: 'flex',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
padding: '0.75rem',
|
||
background: '#f8fafc',
|
||
borderRadius: '10px',
|
||
border: '1px solid #e2e8f0'
|
||
}}>
|
||
<span style={{
|
||
fontSize: '0.875rem',
|
||
fontWeight: '600',
|
||
color: '#64748b'
|
||
}}>
|
||
Temperature
|
||
</span>
|
||
<span style={{
|
||
fontSize: '1rem',
|
||
fontWeight: '700',
|
||
color: '#1e293b'
|
||
}}>
|
||
{(p.temperature || 0).toFixed(1)} <span style={{ fontSize: '0.875rem', color: '#94a3b8', fontWeight: '600' }}>°C</span>
|
||
</span>
|
||
</div>
|
||
|
||
{/* Enthalpy */}
|
||
<div style={{
|
||
display: 'flex',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
padding: '0.75rem',
|
||
background: '#f8fafc',
|
||
borderRadius: '10px',
|
||
border: '1px solid #e2e8f0'
|
||
}}>
|
||
<span style={{
|
||
fontSize: '0.875rem',
|
||
fontWeight: '600',
|
||
color: '#64748b'
|
||
}}>
|
||
Enthalpy
|
||
</span>
|
||
<span style={{
|
||
fontSize: '1rem',
|
||
fontWeight: '700',
|
||
color: '#1e293b'
|
||
}}>
|
||
{(p.enthalpy || 0).toFixed(1)} <span style={{ fontSize: '0.875rem', color: '#94a3b8', fontWeight: '600' }}>kJ/kg</span>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{!cycleData && (
|
||
<div style={{
|
||
maxWidth: '1400px',
|
||
margin: '0 auto',
|
||
background: '#fff',
|
||
padding: '3rem',
|
||
borderRadius: 16,
|
||
boxShadow: '0 1px 3px rgba(0,0,0,0.1)',
|
||
textAlign: 'center'
|
||
}}>
|
||
<div style={{ fontSize: 14, color: '#94a3b8' }}>
|
||
{loading ? 'Calculating cycle...' : 'Adjust sliders — results will appear here (auto-calculated)'}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</main>
|
||
</div>
|
||
</>
|
||
);
|
||
}
|
||
|