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:
59
memento-note/lib/simulators/README.md
Normal file
59
memento-note/lib/simulators/README.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Simulateurs interactifs (plugins)
|
||||
|
||||
Bibliothèque de simulateurs pédagogiques pour les **pages interactives** (`/p/{slug}`).
|
||||
L'IA ne code jamais une simulation : elle **choisit** un simulateur de cette liste et le
|
||||
**configure** (`preset` = valeurs de la note), ou utilise `generic-formula` si aucun ne
|
||||
correspond au sujet.
|
||||
|
||||
## Liste des simulateurs disponibles
|
||||
|
||||
| `simId` | Famille | Sujet | Interaction |
|
||||
|---|---|---|---|
|
||||
| `carnot-cycle` | `sim` | Machine frigorifique / PAC / moteur de Carnot (2ᵉ principe) | Curseurs T_c, T_h, Q_c → COP, η, W min, Q_h en direct |
|
||||
| `carnot-cycle-anim` | `anim` | Cycle de Carnot animé (piston + diagramme P–V live, 5 étapes) | Play / Pause / Step / Reset / vitesse + narration |
|
||||
| `generic-formula` | `sim` | (générique) toute relation chiffrée y = f(paramètres) | Curseurs définis par l'IA, expressions sûres (aucun `eval`) |
|
||||
|
||||
Deux familles de plugins : `sim` (manipulation de paramètres, calcul en direct) et
|
||||
`anim` (scène animée codée en dur, pilotée par le player Play/Step — narration par
|
||||
battements). Les deux sont choisis par l'IA via le même bloc `{ "type": "sim" }`.
|
||||
|
||||
## Comment l'IA les utilise
|
||||
|
||||
1. Le prompt de génération de section injecte `catalogForPrompt(lang)` (registry.ts) —
|
||||
id + résumé + mots-clés + bornes de chaque simulateur.
|
||||
2. Si le contenu de la note correspond (`keywords`), l'IA émet
|
||||
`{ "type": "sim", "sim": { "simId": "carnot-cycle", "preset": { "t_hot": 300 } } }`.
|
||||
3. `validateInteractivePage` vérifie : `simId` connu, preset dans les bornes,
|
||||
`compute(preset)` fini — sinon rejet (renvoyé au LLM ou fallback).
|
||||
|
||||
## Ajouter un plugin (5 étapes)
|
||||
|
||||
Pour un **`sim`** (curseurs) :
|
||||
1. **`lib/simulators/<id>.ts`** — implémenter `SimulatorPlugin` (`family: 'sim'`) :
|
||||
`id`, `title`/`summary` fr+en (le summary sert au matching IA), `keywords`,
|
||||
`params` (curseurs : bornes, pas, défaut, unité, intent), `outputs` (symbole KaTeX,
|
||||
unité), et `compute(env)` **pur et déterministe**.
|
||||
2. **`lib/simulators/index.ts`** — ajouter au `REGISTRY`.
|
||||
3. **`components/simulators/<id>-view.tsx`** — composant sur mesure
|
||||
(props : `preset`, `title`, `disclaimer`, `lang`). Réutiliser
|
||||
`SimSlider` / `SimOutputCard` / `SimHeading` / `SimKaTeX` de `sim-controls.tsx`.
|
||||
4. **`components/simulators/index.ts`** — enregistrer dans `SIMULATOR_VIEWS`.
|
||||
5. Vérifier : `compute` fini aux valeurs par défaut, `npx tsc --noEmit`, tester sur
|
||||
`/dev/interactive-page`.
|
||||
|
||||
Pour une **`anim`** (scène animée Play/Step) :
|
||||
1. **`lib/simulators/<id>.ts`** — implémenter `AnimPlugin` (`family: 'anim'`) :
|
||||
`id`, `title`/`summary` fr+en, `keywords`, `disclaimer?`, `beats` (narration
|
||||
fr+en par étape — KaTeX inline `$…$` dans `speak`).
|
||||
2. **`lib/simulators/index.ts`** — ajouter au `REGISTRY`.
|
||||
3. **`components/simulators/<id>-anim-view.tsx`** — scène SVG/React pilotée par
|
||||
`step` (props : `step`, `lang`). Transitions CSS sur `transform`/`opacity`
|
||||
uniquement. Le chrome (Play/Pause/Step/Reset/vitesse + panneau de narration +
|
||||
clavier Espace/←/→/R) est fourni par `AnimPlayerShell` — ne pas le réécrire.
|
||||
4. **`components/simulators/index.ts`** — enregistrer dans `ANIM_VIEWS`.
|
||||
5. Vérifier : chaque `step` rend un état cohérent (y compris état final sans JS),
|
||||
`npx tsc --noEmit`, tester sur `/dev/interactive-page`.
|
||||
|
||||
Règles : pas de couleurs en dur (intents / tokens `--pp-*`), labels fr+en dans le
|
||||
plugin, tout calcul côté `compute` ou scène codée (jamais de logique LLM), SSR =
|
||||
état lisible sans JS.
|
||||
76
memento-note/lib/simulators/carnot-cycle-anim.ts
Normal file
76
memento-note/lib/simulators/carnot-cycle-anim.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import type { AnimPlugin } from './types'
|
||||
|
||||
/**
|
||||
* Carnot cycle — animated piston + live P-V diagram, 5 beats.
|
||||
* Scene rendered by components/simulators/carnot-cycle-anim-view.tsx.
|
||||
*/
|
||||
export const carnotCycleAnim: AnimPlugin = {
|
||||
family: 'anim',
|
||||
id: 'carnot-cycle-anim',
|
||||
title: {
|
||||
fr: 'Le cycle de Carnot en mouvement',
|
||||
en: 'The Carnot cycle in motion',
|
||||
},
|
||||
summary: {
|
||||
fr: 'Animation du cycle de Carnot : piston, détentes/compressions isothermes et adiabatiques, diagramme P-V tracé en direct, échanges Q et W. 2ᵉ principe, machine thermique, réfrigérateur.',
|
||||
en: 'Animated Carnot cycle: piston, isothermal and adiabatic expansion/compression, live P-V diagram, Q and W exchanges. Second law, heat engine, refrigerator.',
|
||||
},
|
||||
keywords: [
|
||||
'carnot',
|
||||
'thermodynamique',
|
||||
'thermodynamics',
|
||||
'cycle',
|
||||
'piston',
|
||||
'isotherme',
|
||||
'adiabatique',
|
||||
'isothermal',
|
||||
'adiabatic',
|
||||
'entropie',
|
||||
'entropy',
|
||||
'machine thermique',
|
||||
'heat engine',
|
||||
'deuxième principe',
|
||||
'second law',
|
||||
],
|
||||
disclaimer: {
|
||||
fr: 'Schéma pédagogique — grandeurs illustratives, gaz parfait.',
|
||||
en: 'Teaching schematic — illustrative values, ideal gas.',
|
||||
},
|
||||
beats: [
|
||||
{
|
||||
id: 'b1',
|
||||
speak: {
|
||||
fr: '**Détente isotherme** à $T_h$ : le gaz pousse le piston, la chaleur $Q_h$ entre depuis la source chaude.',
|
||||
en: '**Isothermal expansion** at $T_h$: the gas pushes the piston, heat $Q_h$ flows in from the hot reservoir.',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'b2',
|
||||
speak: {
|
||||
fr: '**Détente adiabatique** : isolé, le gaz continue de se détendre et se refroidit de $T_h$ à $T_c$.',
|
||||
en: '**Adiabatic expansion**: insulated, the gas keeps expanding and cools from $T_h$ down to $T_c$.',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'b3',
|
||||
speak: {
|
||||
fr: '**Compression isotherme** à $T_c$ : on travaille sur le gaz, la chaleur $Q_c$ sort vers la source froide.',
|
||||
en: '**Isothermal compression** at $T_c$: work is done on the gas, heat $Q_c$ flows out to the cold reservoir.',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'b4',
|
||||
speak: {
|
||||
fr: '**Compression adiabatique** : le gaz remonte de $T_c$ à $T_h$ — le cycle se referme.',
|
||||
en: '**Adiabatic compression**: the gas warms back from $T_c$ to $T_h$ — the cycle closes.',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'b5',
|
||||
speak: {
|
||||
fr: "Le **travail net** $W = Q_h - Q_c$ est l'aire du cycle sur le diagramme $P$–$V$. Le rendement $\eta = 1 - T_c/T_h$ est le maximum permis par le 2ᵉ principe.",
|
||||
en: '**Net work** $W = Q_h - Q_c$ is the cycle area on the $P$–$V$ diagram. Efficiency $\eta = 1 - T_c/T_h$ is the second-law maximum.',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
254
memento-note/lib/simulators/carnot-cycle.ts
Normal file
254
memento-note/lib/simulators/carnot-cycle.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
import type { SimulatorPlugin } from './types'
|
||||
|
||||
/**
|
||||
* Ideal Carnot machine between two reservoirs (2nd law).
|
||||
*
|
||||
* Physics (absolute temperatures only):
|
||||
* - Refrigerator: COP_R = Tc/(Th−Tc), W_min = Qc/COP_R, Qh = Qc+W
|
||||
* - Heat pump: COP_HP = Th/(Th−Tc) = COP_R+1, W_min = Qh/COP_HP
|
||||
* - Engine: η = 1−Tc/Th, W_out = η·Qh, Qc = Qh−W
|
||||
*
|
||||
* `q_cold` is the primary load magnitude (kJ or W — same number; unit is UI-only).
|
||||
* For fridge it is Qc; the view remaps for PAC / engine. Temperatures always Kelvin.
|
||||
*
|
||||
* Refs: COP_R Carnot = Tc/(Th−Tc); 1st law Qh=Qc+W; energy↔power interchangeable
|
||||
* if all rates use the same time basis (see standard thermo textbooks / Carnot fridge calculators).
|
||||
*/
|
||||
export const carnotCycleSimulator: SimulatorPlugin = {
|
||||
family: 'sim',
|
||||
id: 'carnot-cycle',
|
||||
title: {
|
||||
fr: 'Machine de Carnot (frigo / PAC / moteur)',
|
||||
en: 'Carnot machine (fridge / heat pump / engine)',
|
||||
},
|
||||
summary: {
|
||||
fr: 'Limites de Carnot entre deux sources : COP frigo Tc/(Th−Tc), COP pompe à chaleur Th/(Th−Tc), rendement moteur η=1−Tc/Th, travail ou puissance minimal(e). 1er et 2e principes.',
|
||||
en: 'Carnot limits between two reservoirs: fridge COP Tc/(Th−Tc), heat-pump COP Th/(Th−Tc), engine η=1−Tc/Th, minimum work or power. 1st and 2nd laws.',
|
||||
},
|
||||
keywords: [
|
||||
'carnot',
|
||||
'thermodynamique',
|
||||
'thermodynamics',
|
||||
'cop',
|
||||
'réfrigérateur',
|
||||
'frigo',
|
||||
'refrigerator',
|
||||
'pompe à chaleur',
|
||||
'heat pump',
|
||||
'moteur',
|
||||
'engine',
|
||||
'rendement',
|
||||
'efficiency',
|
||||
'watt',
|
||||
'puissance',
|
||||
'power',
|
||||
'deuxième principe',
|
||||
'second law',
|
||||
],
|
||||
params: [
|
||||
{
|
||||
id: 't_cold',
|
||||
symbol: 'T_c',
|
||||
label: { fr: 'Source froide', en: 'Cold reservoir' },
|
||||
min: 200,
|
||||
max: 320,
|
||||
step: 1,
|
||||
defaultValue: 260,
|
||||
unit: 'K',
|
||||
intent: 'cache',
|
||||
},
|
||||
{
|
||||
id: 't_hot',
|
||||
symbol: 'T_h',
|
||||
label: { fr: 'Source chaude', en: 'Hot reservoir' },
|
||||
min: 273,
|
||||
max: 400,
|
||||
step: 1,
|
||||
defaultValue: 300,
|
||||
unit: 'K',
|
||||
intent: 'warning',
|
||||
},
|
||||
{
|
||||
id: 'q_cold',
|
||||
symbol: 'Q_c',
|
||||
label: { fr: 'Charge (Qc frigo)', en: 'Load (fridge Qc)' },
|
||||
min: 10,
|
||||
max: 500,
|
||||
step: 5,
|
||||
defaultValue: 100,
|
||||
unit: 'kJ',
|
||||
intent: 'flow',
|
||||
},
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
id: 'cop_fridge',
|
||||
symbol: '\\mathrm{COP}_{R}',
|
||||
label: { fr: 'COP réfrigérateur', en: 'Fridge COP' },
|
||||
intent: 'output',
|
||||
digits: 2,
|
||||
},
|
||||
{
|
||||
id: 'cop_hp',
|
||||
symbol: '\\mathrm{COP}_{HP}',
|
||||
label: { fr: 'COP pompe à chaleur', en: 'Heat-pump COP' },
|
||||
intent: 'output',
|
||||
digits: 2,
|
||||
},
|
||||
{
|
||||
id: 'eta',
|
||||
symbol: '\\eta',
|
||||
label: { fr: 'Rendement moteur', en: 'Engine efficiency' },
|
||||
unit: '%',
|
||||
intent: 'highlight',
|
||||
digits: 1,
|
||||
},
|
||||
{
|
||||
id: 'w_min',
|
||||
symbol: 'W',
|
||||
label: { fr: 'Travail (énergie)', en: 'Work (energy)' },
|
||||
unit: 'kJ',
|
||||
intent: 'compute',
|
||||
digits: 1,
|
||||
},
|
||||
{
|
||||
id: 'q_hot',
|
||||
symbol: 'Q_h',
|
||||
label: { fr: 'Chaleur côté chaud', en: 'Hot-side heat' },
|
||||
unit: 'kJ',
|
||||
intent: 'flow',
|
||||
digits: 1,
|
||||
},
|
||||
],
|
||||
compute(env) {
|
||||
const tc = env.t_cold
|
||||
const th = env.t_hot
|
||||
const qc = env.q_cold
|
||||
if (!(th > tc) || !(qc > 0)) {
|
||||
return {
|
||||
cop_fridge: NaN,
|
||||
cop_hp: NaN,
|
||||
eta: NaN,
|
||||
w_min: NaN,
|
||||
q_hot: NaN,
|
||||
}
|
||||
}
|
||||
const copFridge = tc / (th - tc)
|
||||
const copHp = th / (th - tc)
|
||||
const eta = 1 - tc / th
|
||||
const wMin = qc / copFridge
|
||||
return {
|
||||
cop_fridge: copFridge,
|
||||
cop_hp: copHp,
|
||||
eta: eta * 100,
|
||||
w_min: wMin,
|
||||
q_hot: qc + wMin,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
/** Operating mode for the interactive view (UI-only; physics shared). */
|
||||
export type CarnotMode = 'fridge' | 'heat_pump' | 'engine'
|
||||
|
||||
/** Energy (kJ) vs power (W) — same ratios; only unit labels change. */
|
||||
export type CarnotQuantity = 'energy' | 'power'
|
||||
|
||||
export type CarnotPhysics = {
|
||||
ok: boolean
|
||||
tc: number
|
||||
th: number
|
||||
copR: number
|
||||
copHP: number
|
||||
eta: number
|
||||
/** Heat exchanged with cold reservoir (magnitude > 0). */
|
||||
qc: number
|
||||
/** Heat exchanged with hot reservoir (magnitude > 0). */
|
||||
qh: number
|
||||
/** Work magnitude > 0 (input for fridge/PAC, output for engine). */
|
||||
w: number
|
||||
/** Reversible check: Qc/Tc ≈ Qh/Th */
|
||||
entropyOk: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve magnitudes for the selected mode.
|
||||
* `load` is the primary useful quantity:
|
||||
* - fridge: Qc extracted from cold
|
||||
* - heat_pump: Qh delivered to hot
|
||||
* - engine: Qh absorbed from hot
|
||||
*/
|
||||
export function resolveCarnotPhysics(
|
||||
tc: number,
|
||||
th: number,
|
||||
load: number,
|
||||
mode: CarnotMode
|
||||
): CarnotPhysics {
|
||||
if (!(th > tc) || !(load > 0) || !Number.isFinite(tc) || !Number.isFinite(th)) {
|
||||
return {
|
||||
ok: false,
|
||||
tc,
|
||||
th,
|
||||
copR: NaN,
|
||||
copHP: NaN,
|
||||
eta: NaN,
|
||||
qc: NaN,
|
||||
qh: NaN,
|
||||
w: NaN,
|
||||
entropyOk: false,
|
||||
}
|
||||
}
|
||||
const copR = tc / (th - tc)
|
||||
const copHP = th / (th - tc)
|
||||
const eta = 1 - tc / th
|
||||
|
||||
let qc: number
|
||||
let qh: number
|
||||
let w: number
|
||||
if (mode === 'fridge') {
|
||||
qc = load
|
||||
w = qc / copR
|
||||
qh = qc + w
|
||||
} else if (mode === 'heat_pump') {
|
||||
qh = load
|
||||
w = qh / copHP
|
||||
qc = qh - w
|
||||
} else {
|
||||
qh = load
|
||||
w = eta * qh
|
||||
qc = qh - w
|
||||
}
|
||||
|
||||
const ratioC = qc / tc
|
||||
const ratioH = qh / th
|
||||
const entropyOk =
|
||||
Number.isFinite(ratioC) &&
|
||||
Number.isFinite(ratioH) &&
|
||||
Math.abs(ratioC - ratioH) / Math.max(ratioC, ratioH, 1e-9) < 1e-6
|
||||
|
||||
return { ok: true, tc, th, copR, copHP, eta, qc, qh, w, entropyOk }
|
||||
}
|
||||
|
||||
/** Convert fridge-stored Qc load ↔ display load for other modes (fixture-compatible). */
|
||||
export function fridgeLoadFromModeLoad(
|
||||
tc: number,
|
||||
th: number,
|
||||
modeLoad: number,
|
||||
mode: CarnotMode
|
||||
): number {
|
||||
if (!(th > tc) || !(modeLoad > 0)) return modeLoad
|
||||
if (mode === 'fridge') return modeLoad
|
||||
if (mode === 'heat_pump') return modeLoad * (tc / th) // Qc = Qh · Tc/Th
|
||||
return modeLoad * (tc / th) // engine: Qc = Qh · (1−η) = Qh · Tc/Th
|
||||
}
|
||||
|
||||
export function modeLoadFromFridgeLoad(
|
||||
tc: number,
|
||||
th: number,
|
||||
fridgeQc: number,
|
||||
mode: CarnotMode
|
||||
): number {
|
||||
if (!(th > tc) || !(fridgeQc > 0)) return fridgeQc
|
||||
if (mode === 'fridge') return fridgeQc
|
||||
if (mode === 'heat_pump') return fridgeQc * (th / tc) // Qh = Qc · Th/Tc
|
||||
return fridgeQc * (th / tc) // engine Qh = Qc / (1−η) = Qc · Th/Tc
|
||||
}
|
||||
65
memento-note/lib/simulators/index.ts
Normal file
65
memento-note/lib/simulators/index.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import type { IntentId } from '@/lib/interactive-demo/types'
|
||||
import { carnotCycleSimulator } from './carnot-cycle'
|
||||
import { carnotCycleAnim } from './carnot-cycle-anim'
|
||||
import { tsDiagramAnim } from './ts-diagram'
|
||||
import type { AnimPlugin, AnyPlugin, SimulatorPlugin } from './types'
|
||||
|
||||
export const GENERIC_SIM_ID = 'generic-formula' as const
|
||||
|
||||
const REGISTRY: Record<string, AnyPlugin> = {
|
||||
[carnotCycleSimulator.id]: carnotCycleSimulator,
|
||||
[carnotCycleAnim.id]: carnotCycleAnim,
|
||||
[tsDiagramAnim.id]: tsDiagramAnim,
|
||||
}
|
||||
|
||||
export function getPlugin(id: string): AnyPlugin | null {
|
||||
return REGISTRY[id] ?? null
|
||||
}
|
||||
|
||||
export function getSimulator(id: string): SimulatorPlugin | null {
|
||||
const p = REGISTRY[id]
|
||||
return p?.family === 'sim' ? p : null
|
||||
}
|
||||
|
||||
export function getAnimPlugin(id: string): AnimPlugin | null {
|
||||
const p = REGISTRY[id]
|
||||
return p?.family === 'anim' ? p : null
|
||||
}
|
||||
|
||||
export function isCatalogSimId(id: string): boolean {
|
||||
return id !== GENERIC_SIM_ID && id in REGISTRY
|
||||
}
|
||||
|
||||
export function listSimulators(): AnyPlugin[] {
|
||||
return Object.values(REGISTRY)
|
||||
}
|
||||
|
||||
/**
|
||||
* Compact catalog injected into the section-generation prompt so the LLM
|
||||
* can pick a curated plugin (sim or anim) and bind note values into a preset.
|
||||
*/
|
||||
export function catalogForPrompt(lang: string): string {
|
||||
const fr = lang.startsWith('fr')
|
||||
const catalog = listSimulators().map((plugin) => ({
|
||||
simId: plugin.id,
|
||||
family: plugin.family,
|
||||
summary: fr ? plugin.summary.fr : plugin.summary.en,
|
||||
keywords: plugin.keywords.slice(0, 10),
|
||||
...(plugin.family === 'sim'
|
||||
? {
|
||||
params: plugin.params.map((p) => ({
|
||||
id: p.id,
|
||||
symbol: p.symbol,
|
||||
range: [p.min, p.max],
|
||||
default: p.defaultValue,
|
||||
unit: p.unit,
|
||||
})),
|
||||
outputs: plugin.outputs.map((o) => o.symbol),
|
||||
}
|
||||
: {}),
|
||||
}))
|
||||
return JSON.stringify(catalog, null, 1)
|
||||
}
|
||||
|
||||
export type { SimulatorPlugin, AnimPlugin, AnyPlugin, AnimBeat, SimI18n, SimParamDef, SimOutputDef } from './types'
|
||||
export type { IntentId }
|
||||
74
memento-note/lib/simulators/ts-diagram.ts
Normal file
74
memento-note/lib/simulators/ts-diagram.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import type { AnimPlugin } from './types'
|
||||
|
||||
/**
|
||||
* T–s diagram of the Carnot cycle — the canonical 2nd-law diagram:
|
||||
* isotherms are horizontal, adiabatics vertical, heat = area.
|
||||
* Same 5 phases as carnot-cycle-anim (piston) so both tell one story.
|
||||
*/
|
||||
export const tsDiagramAnim: AnimPlugin = {
|
||||
family: 'anim',
|
||||
id: 'ts-diagram',
|
||||
title: {
|
||||
fr: 'Le cycle de Carnot sur le diagramme T–s',
|
||||
en: 'The Carnot cycle on the T–s diagram',
|
||||
},
|
||||
summary: {
|
||||
fr: 'Diagramme température–entropie (T–s) du cycle de Carnot : isothermes horizontales, adiabatiques verticales, aires = chaleurs Q_h et Q_c, aire du cycle = travail W. 2ᵉ principe, entropie, rendement.',
|
||||
en: 'Temperature–entropy (T–s) diagram of the Carnot cycle: horizontal isotherms, vertical adiabatics, areas = heats Q_h and Q_c, cycle area = work W. Second law, entropy, efficiency.',
|
||||
},
|
||||
keywords: [
|
||||
'carnot',
|
||||
'entropie',
|
||||
'entropy',
|
||||
'diagramme t-s',
|
||||
't-s diagram',
|
||||
'thermodynamique',
|
||||
'thermodynamics',
|
||||
'deuxième principe',
|
||||
'second law',
|
||||
'rendement',
|
||||
'efficiency',
|
||||
'cycle',
|
||||
],
|
||||
disclaimer: {
|
||||
fr: 'Diagramme pédagogique — grandeurs illustratives.',
|
||||
en: 'Teaching diagram — illustrative values.',
|
||||
},
|
||||
beats: [
|
||||
{
|
||||
id: 'b1',
|
||||
speak: {
|
||||
fr: '**Détente isotherme** à $T_h$ : l’entropie croît, la chaleur $Q_h = T_h \\Delta s$ est l’aire sous l’isotherme.',
|
||||
en: '**Isothermal expansion** at $T_h$: entropy grows, heat $Q_h = T_h \\Delta s$ is the area under the isotherm.',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'b2',
|
||||
speak: {
|
||||
fr: '**Détente adiabatique** : verticale — l’entropie est constante, la température chute de $T_h$ à $T_c$.',
|
||||
en: '**Adiabatic expansion**: vertical line — entropy is constant, temperature drops from $T_h$ to $T_c$.',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'b3',
|
||||
speak: {
|
||||
fr: '**Compression isotherme** à $T_c$ : l’entropie décroît, la chaleur $Q_c = T_c \\Delta s$ est rejetée — l’aire bleue.',
|
||||
en: '**Isothermal compression** at $T_c$: entropy decreases, heat $Q_c = T_c \\Delta s$ is rejected — the blue area.',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'b4',
|
||||
speak: {
|
||||
fr: '**Compression adiabatique** : remontée verticale de $T_c$ à $T_h$ — le rectangle se referme.',
|
||||
en: '**Adiabatic compression**: vertical climb from $T_c$ back to $T_h$ — the rectangle closes.',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'b5',
|
||||
speak: {
|
||||
fr: 'Le **travail net** $W = (T_h - T_c)\\Delta s$ est l’aire du rectangle. Rapport des aires = rendement $\\eta = 1 - T_c/T_h$ — le maximum du 2ᵉ principe.',
|
||||
en: '**Net work** $W = (T_h - T_c)\\Delta s$ is the rectangle area. Area ratio = efficiency $\\eta = 1 - T_c/T_h$ — the second-law maximum.',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
72
memento-note/lib/simulators/types.ts
Normal file
72
memento-note/lib/simulators/types.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import type { IntentId } from '@/lib/interactive-demo/types'
|
||||
|
||||
/** Bilingual label — page content follows the note's language, not the UI locale. */
|
||||
export type SimI18n = { fr: string; en: string }
|
||||
|
||||
export type SimParamDef = {
|
||||
id: string
|
||||
/** KaTeX symbol (without $…$), e.g. "T_c" */
|
||||
symbol: string
|
||||
label: SimI18n
|
||||
min: number
|
||||
max: number
|
||||
step: number
|
||||
defaultValue: number
|
||||
unit?: string
|
||||
intent?: IntentId
|
||||
}
|
||||
|
||||
export type SimOutputDef = {
|
||||
id: string
|
||||
/** KaTeX symbol (without $…$), e.g. "\\mathrm{COP}" */
|
||||
symbol: string
|
||||
label: SimI18n
|
||||
unit?: string
|
||||
intent?: IntentId
|
||||
/** Fraction digits for display (default 2). */
|
||||
digits?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* A curated interactive simulator plugin (catalog).
|
||||
* The AI never writes simulation code — it picks a plugin and a preset.
|
||||
* `compute` must be pure and deterministic; it runs client-side on every
|
||||
* slider move and once server-side at validation time.
|
||||
*/
|
||||
export type SimulatorPlugin = {
|
||||
family: 'sim'
|
||||
id: string
|
||||
title: SimI18n
|
||||
/** Shown to the LLM for content matching. */
|
||||
summary: SimI18n
|
||||
keywords: string[]
|
||||
params: SimParamDef[]
|
||||
outputs: SimOutputDef[]
|
||||
compute(env: Record<string, number>): Record<string, number>
|
||||
}
|
||||
|
||||
// ── Animated pedagogical scenes (Play/Step narration over a coded scene) ────
|
||||
|
||||
export type AnimBeat = {
|
||||
id: string
|
||||
speak: SimI18n
|
||||
}
|
||||
|
||||
/**
|
||||
* A curated ANIMATION plugin: a hand-coded parametric scene (like the demos
|
||||
* on distill.pub / the Kimi AttnRes page) driven beat-by-beat by the shared
|
||||
* player chrome. The view component receives `stepIndex` and renders the
|
||||
* scene state — transitions are CSS (transform/opacity) only.
|
||||
*/
|
||||
export type AnimPlugin = {
|
||||
family: 'anim'
|
||||
id: string
|
||||
title: SimI18n
|
||||
summary: SimI18n
|
||||
keywords: string[]
|
||||
disclaimer?: SimI18n
|
||||
/** Narration beats; step N of the player = beat N. */
|
||||
beats: AnimBeat[]
|
||||
}
|
||||
|
||||
export type AnyPlugin = SimulatorPlugin | AnimPlugin
|
||||
Reference in New Issue
Block a user