feat: page interactive, démos Play/Step et simulateur Carnot
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>
This commit is contained in:
672
memento-note/components/simulators/carnot-cycle-view.tsx
Normal file
672
memento-note/components/simulators/carnot-cycle-view.tsx
Normal file
@@ -0,0 +1,672 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user