'use client' import { useMemo, useState } from 'react' import { carnotCycleSimulator as sim, fridgeLoadFromModeLoad, modeLoadFromFridgeLoad, resolveCarnotPhysics, type CarnotMode, type CarnotQuantity, } from '@/lib/simulators/carnot-cycle' import { intentColor } from '@/lib/interactive-demo/intent-colors' import { useDarkMode } from '@/components/interactive-demo/demo-speak' import { SimHeading, SimOutputCard, SimSlider } from './sim-controls' import { cn } from '@/lib/utils' const SVG_W = 360 const SVG_H = 270 type TempUnit = 'K' | 'C' | 'F' const TEMP_UNITS: { id: TempUnit; label: string }[] = [ { id: 'K', label: 'K' }, { id: 'C', label: '°C' }, { id: 'F', label: '°F' }, ] const MODES: { id: CarnotMode; fr: string; en: string }[] = [ { id: 'fridge', fr: 'Frigo', en: 'Fridge' }, { id: 'heat_pump', fr: 'PAC', en: 'Heat pump' }, { id: 'engine', fr: 'Moteur', en: 'Engine' }, ] const QTY: { id: CarnotQuantity; fr: string; en: string }[] = [ { id: 'energy', fr: 'Énergie (kJ)', en: 'Energy (kJ)' }, { id: 'power', fr: 'Puissance (W)', en: 'Power (W)' }, ] function kelvinToDisplay(k: number, unit: TempUnit): number { if (unit === 'C') return k - 273.15 if (unit === 'F') return (k * 9) / 5 - 459.67 return k } function displayToKelvin(v: number, unit: TempUnit): number { if (unit === 'C') return v + 273.15 if (unit === 'F') return ((v + 459.67) * 5) / 9 return v } function formatTemp(k: number, unit: TempUnit): string { const v = kelvinToDisplay(k, unit) if (unit === 'K') return `${Math.round(v)} K` if (unit === 'C') { const r = Math.round(v * 10) / 10 return `${Number.isInteger(r) ? r : r.toFixed(1)} °C` } return `${Math.round(v)} °F` } function tempSliderMeta( param: { min: number; max: number; step: number }, unit: TempUnit ): { min: number; max: number; step: number; unitLabel: string } { if (unit === 'K') { return { min: param.min, max: param.max, step: param.step, unitLabel: 'K' } } if (unit === 'C') { return { min: Math.round((param.min - 273.15) * 10) / 10, max: Math.round((param.max - 273.15) * 10) / 10, step: 0.5, unitLabel: '°C', } } return { min: Math.round(((param.min * 9) / 5 - 459.67) * 10) / 10, max: Math.round(((param.max * 9) / 5 - 459.67) * 10) / 10, step: 1, unitLabel: '°F', } } function formatQty(v: number): string { if (!Number.isFinite(v)) return '—' if (Math.abs(v - Math.round(v)) < 0.05) return String(Math.round(v)) return v.toFixed(1) } function Segmented({ value, onChange, options, ariaLabel, }: { value: T onChange: (v: T) => void options: { id: T; label: string }[] ariaLabel: string }) { return (
{options.map((o) => ( ))}
) } /** Arrowhead + shaft, thickness ∝ |value|/maxRef. */ function FlowArrow({ x1, y1, x2, y2, value, maxRef, color, label, unit, labelSide = 'right', }: { x1: number y1: number x2: number y2: number value: number maxRef: number color: string label: string unit?: string labelSide?: 'left' | 'right' | 'above' | 'below' }) { const mag = Math.max(0, value) const t = 2.2 + (mag / Math.max(1, maxRef)) * 10 const dx = x2 - x1 const dy = y2 - y1 const len = Math.hypot(dx, dy) || 1 const ux = dx / len const uy = dy / len const headLen = Math.min(14, Math.max(9, 7 + t * 0.45)) const headHalf = Math.min(7, 3.2 + t * 0.35) const bx = x2 - ux * headLen const by = y2 - uy * headLen const px = -uy const py = ux const mx = (x1 + bx) / 2 const my = (y1 + by) / 2 const labelGap = 11 + t * 0.45 let lx = mx let ly = my let textAnchor: 'start' | 'middle' | 'end' = 'middle' if (labelSide === 'above') ly = my - labelGap else if (labelSide === 'below') ly = my + labelGap else if (labelSide === 'right') { lx = mx + labelGap textAnchor = 'start' } else { lx = mx - labelGap textAnchor = 'end' } return ( {label} {formatQty(value)} {unit ? ` ${unit}` : ''} ) } export function CarnotCycleView({ preset, disclaimer, lang, }: { preset?: Record title?: string disclaimer?: string lang: string }) { const fr = lang.startsWith('fr') const dark = useDarkMode() const [tempUnit, setTempUnit] = useState('K') const [mode, setMode] = useState('fridge') const [qty, setQty] = useState('energy') const [values, setValues] = useState>(() => { const env: Record = {} for (const p of sim.params) env[p.id] = preset?.[p.id] ?? p.defaultValue return env }) const phys = useMemo( () => resolveCarnotPhysics( values.t_cold, values.t_hot, modeLoadFromFridgeLoad(values.t_cold, values.t_hot, values.q_cold, mode), mode ), [values.t_cold, values.t_hot, values.q_cold, mode] ) const unitE = qty === 'energy' ? 'kJ' : 'W' const workName = qty === 'energy' ? fr ? 'Travail' : 'Work' : fr ? 'Puissance' : 'Power' const maxRef = Math.max(phys.qh || 0, phys.qc || 0, phys.w || 0, 1) const cHot = intentColor('warning', dark) const cCold = intentColor('cache', dark) const cWork = intentColor('compute', dark) const text = dark ? '#e4e4e7' : '#27272a' const cx = SVG_W / 2 - 20 const hotY = 28 const coldY = SVG_H - 42 const midY = SVG_H / 2 - 4 const r = 32 const qOffset = 42 const isEngine = mode === 'engine' const loadMeta = useMemo(() => { if (mode === 'fridge') { return { symbol: qty === 'energy' ? 'Q_c' : '\\dot{Q}_c', label: fr ? 'Chaleur extraite (froid)' : 'Heat extracted (cold)', hint: fr ? 'Charge utile du réfrigérateur' : 'Useful fridge cooling load', } } if (mode === 'heat_pump') { return { symbol: qty === 'energy' ? 'Q_h' : '\\dot{Q}_h', label: fr ? 'Chaleur fournie (chaud)' : 'Heat delivered (hot)', hint: fr ? 'Charge utile de la PAC' : 'Useful heat-pump output', } } return { symbol: qty === 'energy' ? 'Q_h' : '\\dot{Q}_h', label: fr ? 'Chaleur absorbée (chaud)' : 'Heat absorbed (hot)', hint: fr ? 'Entrée thermique du moteur' : 'Engine heat input', } }, [mode, qty, fr]) const modeLoad = modeLoadFromFridgeLoad( values.t_cold, values.t_hot, values.q_cold, mode ) const workSym = qty === 'energy' ? 'W' : 'P' const qLabel = (base: 'c' | 'h') => (qty === 'energy' ? `Q_${base}` : `Q̇_${base}`) const lawLine = !phys.ok ? fr ? 'Il faut T_h > T_c (températures absolues).' : 'Need T_h > T_c (absolute temperatures).' : fr ? `1ᵉʳ principe : ${qLabel('h')} = ${qLabel('c')} + ${workSym} → ${formatQty(phys.qh)} = ${formatQty(phys.qc)} + ${formatQty(phys.w)} ${unitE}` : `1st law: ${qLabel('h')} = ${qLabel('c')} + ${workSym} → ${formatQty(phys.qh)} = ${formatQty(phys.qc)} + ${formatQty(phys.w)} ${unitE}` return (
({ id: m.id, label: fr ? m.fr : m.en }))} /> ({ id: q.id, label: fr ? q.fr : q.en }))} /> ({ id: u.id, label: u.label }))} />
{fr ? 'Source chaude' : 'Hot'} · T_h = {formatTemp(values.t_hot, tempUnit)} {fr ? 'Source froide' : 'Cold'} · T_c ={' '} {formatTemp(values.t_cold, tempUnit)} {fr ? 'Machine' : 'Engine'} {phys.ok && !isEngine ? ( <> {/* Fridge / PAC: Qc↑ into machine, W→ into machine, Qh↑ to hot */} ) : null} {phys.ok && isEngine ? ( <> {/* Engine: Qh↓ from hot into machine, W→ out, Qc↓ to cold */} ) : null} {fr ? qty === 'energy' ? 'Épaisseur ∝ énergie (kJ) — W = travail (pas le watt)' : 'Épaisseur ∝ puissance — P et W (watt) = même unité ici' : qty === 'energy' ? 'Thickness ∝ energy (kJ) — W = work (not the watt)' : 'Thickness ∝ power — P uses watts'}

{lawLine}

{phys.ok && phys.entropyOk ? (

{fr ? '2ᵉ principe (réversible) : Q_c/T_c = Q_h/T_h' : '2nd law (reversible): Q_c/T_c = Q_h/T_h'}

) : null}
{fr ? 'Paramètres' : 'Parameters'}
{sim.params .filter((p) => p.id === 't_cold' || p.id === 't_hot') .map((p) => { const meta = tempSliderMeta(p, tempUnit) const displayVal = kelvinToDisplay(values[p.id], tempUnit) return ( { const k = displayToKelvin(v, tempUnit) const clamped = Math.min(p.max, Math.max(p.min, k)) setValues((s) => ({ ...s, [p.id]: clamped })) }} /> ) })} { const fridgeQc = fridgeLoadFromModeLoad( values.t_cold, values.t_hot, v, mode ) setValues((s) => ({ ...s, q_cold: Math.min(500, Math.max(10, fridgeQc)), })) }} />

{loadMeta.hint}

{fr ? 'Résultats (limites de Carnot)' : 'Results (Carnot limits)'}
{mode === 'fridge' || mode === 'heat_pump' ? ( <> ) : ( )}

{qty === 'energy' ? fr ? 'W = travail (énergie en kJ), pas le watt. Passe en « Puissance (W) » pour raisonner en watts.' : 'W = work (energy in kJ), not the watt. Switch to “Power (W)” to use watts.' : fr ? 'Mode puissance : P, Q̇_c et Q̇_h sont en watts (W). Les COP / η restent sans unité.' : 'Power mode: P, Q̇_c and Q̇_h are in watts (W). COP / η stay dimensionless.'}

{disclaimer ? (

{disclaimer}

) : null}
) }