Ajoute le pipeline PageSpec (validation, rendu, publication /p/{slug}),
les démos TipTap /demo, et le simulateur Carnot (modes frigo/PAC/moteur,
énergie kJ vs puissance W, unités K/°C/°F) avec correctifs d’équations KaTeX.
Co-authored-by: Cursor <cursoragent@cursor.com>
673 lines
20 KiB
TypeScript
673 lines
20 KiB
TypeScript
'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<T extends string>({
|
|
value,
|
|
onChange,
|
|
options,
|
|
ariaLabel,
|
|
}: {
|
|
value: T
|
|
onChange: (v: T) => void
|
|
options: { id: T; label: string }[]
|
|
ariaLabel: string
|
|
}) {
|
|
return (
|
|
<div
|
|
className="inline-flex max-w-full flex-wrap rounded-lg border border-border/60 bg-background/80 p-0.5"
|
|
role="group"
|
|
aria-label={ariaLabel}
|
|
>
|
|
{options.map((o) => (
|
|
<button
|
|
key={o.id}
|
|
type="button"
|
|
onClick={() => onChange(o.id)}
|
|
className={cn(
|
|
'rounded-md px-2 py-1 text-[11px] font-semibold transition-colors',
|
|
value === o.id
|
|
? 'bg-foreground text-background'
|
|
: 'text-muted-foreground hover:text-foreground'
|
|
)}
|
|
aria-pressed={value === o.id}
|
|
>
|
|
{o.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
/** 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 (
|
|
<g>
|
|
<line
|
|
x1={x1}
|
|
y1={y1}
|
|
x2={bx}
|
|
y2={by}
|
|
stroke={color}
|
|
strokeWidth={t}
|
|
strokeLinecap="round"
|
|
opacity={0.9}
|
|
/>
|
|
<polygon
|
|
points={`${x2},${y2} ${bx + px * headHalf},${by + py * headHalf} ${bx - px * headHalf},${by - py * headHalf}`}
|
|
fill={color}
|
|
opacity={0.95}
|
|
/>
|
|
<text
|
|
x={lx}
|
|
y={ly}
|
|
fontSize={11}
|
|
fill={color}
|
|
fontWeight={600}
|
|
dominantBaseline="middle"
|
|
textAnchor={textAnchor}
|
|
>
|
|
{label} {formatQty(value)}
|
|
{unit ? ` ${unit}` : ''}
|
|
</text>
|
|
</g>
|
|
)
|
|
}
|
|
|
|
export function CarnotCycleView({
|
|
preset,
|
|
disclaimer,
|
|
lang,
|
|
}: {
|
|
preset?: Record<string, number>
|
|
title?: string
|
|
disclaimer?: string
|
|
lang: string
|
|
}) {
|
|
const fr = lang.startsWith('fr')
|
|
const dark = useDarkMode()
|
|
const [tempUnit, setTempUnit] = useState<TempUnit>('K')
|
|
const [mode, setMode] = useState<CarnotMode>('fridge')
|
|
const [qty, setQty] = useState<CarnotQuantity>('energy')
|
|
const [values, setValues] = useState<Record<string, number>>(() => {
|
|
const env: Record<string, number> = {}
|
|
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 (
|
|
<div>
|
|
<div className="mb-3 flex flex-wrap items-center gap-2">
|
|
<Segmented
|
|
value={mode}
|
|
onChange={setMode}
|
|
ariaLabel={fr ? 'Mode machine' : 'Machine mode'}
|
|
options={MODES.map((m) => ({ id: m.id, label: fr ? m.fr : m.en }))}
|
|
/>
|
|
<Segmented
|
|
value={qty}
|
|
onChange={setQty}
|
|
ariaLabel={fr ? 'Énergie ou puissance' : 'Energy or power'}
|
|
options={QTY.map((q) => ({ id: q.id, label: fr ? q.fr : q.en }))}
|
|
/>
|
|
<Segmented
|
|
value={tempUnit}
|
|
onChange={setTempUnit}
|
|
ariaLabel={fr ? 'Unité de température' : 'Temperature unit'}
|
|
options={TEMP_UNITS.map((u) => ({ id: u.id, label: u.label }))}
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid gap-5 md:grid-cols-[minmax(0,1.1fr)_minmax(0,1fr)]">
|
|
<div>
|
|
<svg
|
|
viewBox={`0 0 ${SVG_W} ${SVG_H}`}
|
|
className="mx-auto w-full max-w-md"
|
|
role="img"
|
|
aria-label={fr ? sim.title.fr : sim.title.en}
|
|
>
|
|
<rect
|
|
x={cx - 105}
|
|
y={hotY - 18}
|
|
width={210}
|
|
height={34}
|
|
rx={10}
|
|
fill={`${cHot}14`}
|
|
stroke={cHot}
|
|
strokeWidth={1.5}
|
|
/>
|
|
<text
|
|
x={cx}
|
|
y={hotY}
|
|
textAnchor="middle"
|
|
fontSize={11.5}
|
|
fontWeight={600}
|
|
fill={cHot}
|
|
dominantBaseline="middle"
|
|
>
|
|
{fr ? 'Source chaude' : 'Hot'} · T_h = {formatTemp(values.t_hot, tempUnit)}
|
|
</text>
|
|
|
|
<rect
|
|
x={cx - 105}
|
|
y={coldY - 18}
|
|
width={210}
|
|
height={34}
|
|
rx={10}
|
|
fill={`${cCold}14`}
|
|
stroke={cCold}
|
|
strokeWidth={1.5}
|
|
/>
|
|
<text
|
|
x={cx}
|
|
y={coldY}
|
|
textAnchor="middle"
|
|
fontSize={11.5}
|
|
fontWeight={600}
|
|
fill={cCold}
|
|
dominantBaseline="middle"
|
|
>
|
|
{fr ? 'Source froide' : 'Cold'} · T_c ={' '}
|
|
{formatTemp(values.t_cold, tempUnit)}
|
|
</text>
|
|
|
|
<circle
|
|
cx={cx}
|
|
cy={midY}
|
|
r={r}
|
|
fill={dark ? '#141820' : '#ffffff'}
|
|
stroke={text}
|
|
strokeWidth={1.5}
|
|
/>
|
|
<text
|
|
x={cx}
|
|
y={midY}
|
|
textAnchor="middle"
|
|
fontSize={11}
|
|
fontWeight={600}
|
|
fill={text}
|
|
dominantBaseline="middle"
|
|
>
|
|
{fr ? 'Machine' : 'Engine'}
|
|
</text>
|
|
|
|
{phys.ok && !isEngine ? (
|
|
<>
|
|
{/* Fridge / PAC: Qc↑ into machine, W→ into machine, Qh↑ to hot */}
|
|
<FlowArrow
|
|
x1={cx - qOffset}
|
|
y1={coldY - 22}
|
|
x2={cx - qOffset}
|
|
y2={midY + r + 2}
|
|
value={phys.qc}
|
|
maxRef={maxRef}
|
|
color={cCold}
|
|
label={qLabel('c')}
|
|
unit={unitE}
|
|
labelSide="left"
|
|
/>
|
|
<FlowArrow
|
|
x1={cx + qOffset}
|
|
y1={midY - r - 2}
|
|
x2={cx + qOffset}
|
|
y2={hotY + 22}
|
|
value={phys.qh}
|
|
maxRef={maxRef}
|
|
color={cHot}
|
|
label={qLabel('h')}
|
|
unit={unitE}
|
|
/>
|
|
<FlowArrow
|
|
x1={cx + r + 78}
|
|
y1={midY}
|
|
x2={cx + r + 4}
|
|
y2={midY}
|
|
value={phys.w}
|
|
maxRef={maxRef}
|
|
color={cWork}
|
|
label={workSym}
|
|
unit={unitE}
|
|
labelSide="above"
|
|
/>
|
|
</>
|
|
) : null}
|
|
|
|
{phys.ok && isEngine ? (
|
|
<>
|
|
{/* Engine: Qh↓ from hot into machine, W→ out, Qc↓ to cold */}
|
|
<FlowArrow
|
|
x1={cx + qOffset}
|
|
y1={hotY + 22}
|
|
x2={cx + qOffset}
|
|
y2={midY - r - 2}
|
|
value={phys.qh}
|
|
maxRef={maxRef}
|
|
color={cHot}
|
|
label={qLabel('h')}
|
|
unit={unitE}
|
|
/>
|
|
<FlowArrow
|
|
x1={cx - qOffset}
|
|
y1={midY + r + 2}
|
|
x2={cx - qOffset}
|
|
y2={coldY - 22}
|
|
value={phys.qc}
|
|
maxRef={maxRef}
|
|
color={cCold}
|
|
label={qLabel('c')}
|
|
unit={unitE}
|
|
labelSide="left"
|
|
/>
|
|
<FlowArrow
|
|
x1={cx + r + 4}
|
|
y1={midY}
|
|
x2={cx + r + 78}
|
|
y2={midY}
|
|
value={phys.w}
|
|
maxRef={maxRef}
|
|
color={cWork}
|
|
label={workSym}
|
|
unit={unitE}
|
|
labelSide="above"
|
|
/>
|
|
</>
|
|
) : null}
|
|
|
|
<text x={10} y={SVG_H - 10} fontSize={10} fill={dark ? '#8b8b93' : '#6b7280'}>
|
|
{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'}
|
|
</text>
|
|
</svg>
|
|
<p className="mt-2 text-center text-[11px] text-muted-foreground">{lawLine}</p>
|
|
{phys.ok && phys.entropyOk ? (
|
|
<p className="mt-1 text-center text-[10px] text-muted-foreground/80">
|
|
{fr
|
|
? '2ᵉ principe (réversible) : Q_c/T_c = Q_h/T_h'
|
|
: '2nd law (reversible): Q_c/T_c = Q_h/T_h'}
|
|
</p>
|
|
) : null}
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
<div>
|
|
<SimHeading>{fr ? 'Paramètres' : 'Parameters'}</SimHeading>
|
|
<div className="space-y-2">
|
|
{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 (
|
|
<SimSlider
|
|
key={`${p.id}-${tempUnit}`}
|
|
symbol={p.symbol}
|
|
label={fr ? p.label.fr : p.label.en}
|
|
unit={meta.unitLabel}
|
|
min={meta.min}
|
|
max={meta.max}
|
|
step={meta.step}
|
|
value={
|
|
tempUnit === 'K'
|
|
? Math.round(displayVal)
|
|
: Math.round(displayVal * 10) / 10
|
|
}
|
|
intent={p.intent}
|
|
onChange={(v) => {
|
|
const k = displayToKelvin(v, tempUnit)
|
|
const clamped = Math.min(p.max, Math.max(p.min, k))
|
|
setValues((s) => ({ ...s, [p.id]: clamped }))
|
|
}}
|
|
/>
|
|
)
|
|
})}
|
|
|
|
<SimSlider
|
|
key={`load-${mode}-${qty}`}
|
|
symbol={loadMeta.symbol}
|
|
label={loadMeta.label}
|
|
unit={unitE}
|
|
min={10}
|
|
max={500}
|
|
step={5}
|
|
value={Math.round(modeLoad * 10) / 10}
|
|
intent="flow"
|
|
onChange={(v) => {
|
|
const fridgeQc = fridgeLoadFromModeLoad(
|
|
values.t_cold,
|
|
values.t_hot,
|
|
v,
|
|
mode
|
|
)
|
|
setValues((s) => ({
|
|
...s,
|
|
q_cold: Math.min(500, Math.max(10, fridgeQc)),
|
|
}))
|
|
}}
|
|
/>
|
|
<p className="px-1 text-[10px] text-muted-foreground">{loadMeta.hint}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<SimHeading>
|
|
{fr ? 'Résultats (limites de Carnot)' : 'Results (Carnot limits)'}
|
|
</SimHeading>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{mode === 'fridge' || mode === 'heat_pump' ? (
|
|
<>
|
|
<SimOutputCard
|
|
symbol={String.raw`\mathrm{COP}_{R}`}
|
|
label={fr ? 'COP frigo' : 'Fridge COP'}
|
|
value={phys.copR}
|
|
intent="output"
|
|
digits={2}
|
|
/>
|
|
<SimOutputCard
|
|
symbol={String.raw`\mathrm{COP}_{HP}`}
|
|
label={fr ? 'COP PAC' : 'Heat-pump COP'}
|
|
value={phys.copHP}
|
|
intent="output"
|
|
digits={2}
|
|
/>
|
|
</>
|
|
) : (
|
|
<SimOutputCard
|
|
symbol={String.raw`\eta`}
|
|
label={fr ? 'Rendement moteur' : 'Engine efficiency'}
|
|
value={phys.eta * 100}
|
|
unit="%"
|
|
intent="highlight"
|
|
digits={1}
|
|
/>
|
|
)}
|
|
<SimOutputCard
|
|
symbol={workSym}
|
|
label={
|
|
isEngine
|
|
? fr
|
|
? `${workName} fourni`
|
|
: `${workName} out`
|
|
: fr
|
|
? `${workName} minimale`
|
|
: `Minimum ${workName.toLowerCase()}`
|
|
}
|
|
value={phys.w}
|
|
unit={unitE}
|
|
intent="compute"
|
|
digits={1}
|
|
/>
|
|
<SimOutputCard
|
|
symbol={
|
|
qty === 'energy' ? String.raw`Q_h` : String.raw`\dot{Q}_h`
|
|
}
|
|
label={
|
|
isEngine
|
|
? fr
|
|
? 'Chaleur absorbée'
|
|
: 'Heat absorbed'
|
|
: fr
|
|
? 'Chaleur côté chaud'
|
|
: 'Hot-side heat'
|
|
}
|
|
value={phys.qh}
|
|
unit={unitE}
|
|
intent="flow"
|
|
digits={1}
|
|
/>
|
|
<SimOutputCard
|
|
symbol={
|
|
qty === 'energy' ? String.raw`Q_c` : String.raw`\dot{Q}_c`
|
|
}
|
|
label={
|
|
isEngine
|
|
? fr
|
|
? 'Chaleur rejetée (froid)'
|
|
: 'Heat rejected (cold)'
|
|
: fr
|
|
? 'Chaleur côté froid'
|
|
: 'Cold-side heat'
|
|
}
|
|
value={phys.qc}
|
|
unit={unitE}
|
|
intent="cache"
|
|
digits={1}
|
|
/>
|
|
</div>
|
|
<p className="mt-2 text-[10px] leading-relaxed text-muted-foreground">
|
|
{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.'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{disclaimer ? (
|
|
<p className="mt-3 text-xs italic text-muted-foreground">{disclaimer}</p>
|
|
) : null}
|
|
</div>
|
|
)
|
|
}
|