# CÁCH THÊM THÍ NGHIỆM MỚI

Tài liệu hợp đồng kiến trúc, phiên bản 1.0. Đọc file này trước khi sửa dự án.

## 1. Nguyên tắc

Giữ nguyên index.html, app.js, CSS và các bài cũ nếu chúng đã đáp ứng yêu cầu.
Tạo một thư mục module mới và thêm một mục trong data/experiments.js.
Không đưa logic Vật lí của bài mới vào app.js hoặc chart.js.
Các core hiện tại phục vụ 2 thí nghiệm đầu; chỉ tách helper mới khi thực sự có nhu cầu tái sử dụng.

Trước khi viết code, ghi rõ: mô hình; biến/đơn vị; công thức; giả thiết/miền giá trị;
đại lượng độc lập và phụ thuộc; dữ liệu lí tưởng hay có mô hình sai số.
Chọn đúng lớp/chủ đề theo nội dung giảng dạy; các chủ đề mở rộng cần ghi rõ là mở rộng.

## 2. Tạo module

Ví dụ thêm bài định luật Ohm:
- Tạo experiments/lop-11/dinh-luat-ohm/index.js.
- Tham khảo module boyle cho bài tĩnh, roi-tu-do cho bài có animation.
- Sao chép cấu trúc mount/cleanup, sau đó thay toàn bộ mô hình, hướng dẫn, dụng cụ và dữ liệu cho đúng bài mới.
- Không sao chép nguyên kiến thức hoặc nhãn đơn vị của bài cũ.

Mỗi module dùng IIFE, giữ biến trạng thái trong mount:

```javascript
(() => {
  'use strict';
  const L = PhysicsLab;
  function solve(/* đầu vào có đơn vị rõ */) {
    // Hàm tính thuần, kiểm tra miền, trả đối tượng kết quả hữu hạn.
  }
  L.register('dinh-luat-ohm', {
    solve,
    mount(root, meta) {
      // L.createShell(root, meta, nội dung giáo dục);
      // Tạo dụng cụ, slider, notebook, chart và liên kết sự kiện.
      // Trạng thái chỉ thuộc lần mount này; không lưu toàn cục.
      return () => {
        // Hủy requestAnimationFrame/timer; gỡ listener trên document/window.
      };
    }
  });
})();
```

Đoạn trên là hợp đồng để viết module, không phải một thí nghiệm Ohm đã hoàn thành.
Hai module trong thư mục lop-10 và lop-12 là ví dụ hoàn chỉnh chạy được.

## 3. Khai báo metadata

Bài Ohm đã có mục status: 'planned' trong catalog. Khi bài hoàn tất, sửa đúng mục đó,
không thêm mục thứ hai trùng id:

```javascript
{
  id: 'dinh-luat-ohm',
  title: 'Định luật Ohm',
  grade: 11,
  topic: 'Điện học',
  lesson: 'Điện trở',
  type: 'Thí nghiệm',
  status: 'ready',
  art: 'circuit',
  description: 'Đo hiệu điện thế và cường độ dòng điện qua điện trở thuần.',
  entry: 'experiments/lop-11/dinh-luat-ohm/index.js',
  modes: ['simulation', 'practice']
}
```

Nếu bài hoàn toàn mới, thêm một object với id mới vào mảng PhysicsLab.catalog.
- id: duy nhất, chữ thường không dấu, dùng dấu gạch ngang; phải trùng L.register.
- grade: số 10, 11 hoặc 12.
- topic: dùng tên thống nhất để bộ lọc không sinh các nhóm trùng nghĩa.
- lesson: tên bài/chương phục vụ phân cấp.
- type: 'Thí nghiệm' hoặc 'Mô phỏng'.
- status: 'planned' chưa mở; 'ready' mở được.
- entry: đường dẫn tương đối tính từ index.html, không dùng đường dẫn ổ đĩa.
- art: fall/gas/spring/throw/circuit/magnet. Có thể bổ sung hình riêng ở components.js
  nếu cần; nếu không có art, hệ thống dùng hình mặc định và vẫn hoạt động.
- modes: ['simulation','practice']; 'assessment' là chế độ dành cho tương lai, hiện chưa được triển khai.
Không cần thêm script module vào index.html: app tự tải từ entry.

## 4. Các API dùng chung

### L.createShell(root, meta, config)
config gồm objectives (mảng chuỗi), knowledge (HTML tin cậy do lập trình viên viết),
assumptions (chuỗi), tasks (mảng chuỗi), conclusion (chuỗi).
Không đưa HTML người dùng hoặc nội dung lấy từ nguồn không tin cậy vào knowledge.
Các trường còn lại được escape để tránh chèn mã HTML.

Trả về:
- get(name): vùng DOM như stage, metrics, controls, actions, notebook, chart-options,
  chart, chart-hint, plot, reset, knowledge, conclusion.
- mode: simulation hoặc practice.
- onMode(fn): gọi khi người dùng chuyển chế độ. Module phải tự cập nhật đồ thị,
  ẩn đường lí thuyết trong thực hành và xử lý animation nếu cần.
- message(text, error=false): thông báo tiếng Việt tại khu vực thí nghiệm.
- clearNote(): xóa ghi chép của bài.
Shell tự ẩn kiến thức/kết luận trong thực hành; không tự biết dữ liệu của từng bài.

### L.slider(host, config)
config: id, label, min, max, step, value, unit, digits, help, onInput(value).
Trả về input, set(value), disable(boolean).
ID của các điều khiển phải rõ nghĩa; input range dùng được bằng chạm và bàn phím.
Nếu thêm kéo thả quan trọng, dùng Pointer Events và có slider/nút thay thế.

### L.createNotebook(host, config)
config: columns [{key,label,digits}], filename, onChange(rows), limit (mặc định 60).
Trả về add(row) -> boolean, clear(), rows (bản sao mảng).
Mỗi key trong columns phải là giá trị số hữu hạn. Cột label phải kèm đơn vị.
Không thay đổi trực tiếp dữ liệu trong rows; dùng add/clear hoặc nút xóa của component.
onChange có thể được gọi ngay lúc tạo component: tránh truy cập biến chưa khởi tạo.
CSV giữ giá trị số đầy đủ; bảng hiển thị theo digits.

### L.drawChart(host, config)
config: points [{x,y}], theory [{x,y}], xLabel, yLabel, join, empty.
Vẽ SVG co giãn, tự xác định miền có chứa gốc, vẽ điểm/đường, chú thích và title từng điểm.
Truy cập số liệu trên điện thoại qua bảng đo, không phụ thuộc hover.
points/theory chỉ dùng số hữu hạn. Đường theory đi theo thứ tự mảng được truyền vào.
Nếu nối điểm đo, core sắp theo x; không dùng chế độ nối này cho đường có tính trễ/chu trình.
Với chu trình nhiệt cần truyền đường có thứ tự riêng hoặc mở rộng API có kiểm thử.

### L.clamp / L.format / L.storage
clamp(value,min,max,fallback) chặn số ngoài miền; không hữu hạn dùng fallback.
format(value,digits) hiển thị kiểu vi-VN.
storage.get(key,fallback), storage.set(key,value) dùng tiền tố vlab:;
set trả false nếu trình duyệt chặn lưu dữ liệu.
Không lưu dữ liệu cá nhân nhạy cảm vào localStorage.

## 5. Vòng đời và hiệu năng

- Mỗi lần mở bài tạo state mới, render lần đầu; module script chỉ đăng ký một lần.
- mount phải trả cleanup, kể cả khi chỉ trả một hàm rỗng cho bài tĩnh.
- Hủy animation/timer khi rời bài, khi chuyển ứng dụng nên tạm dừng.
- Không dùng setInterval vô hạn để vẽ lại bài tĩnh.
- Dùng requestAnimationFrame cho bài động và giới hạn tốc độ tăng thời gian khi tab bị treo.
- Không tự chạy animation khi mở trang. Tôn trọng prefers-reduced-motion;
  cho phép xem từng bước bằng slider hoặc nút.
- Gắn listener vào root/vùng con sẽ được giải phóng khi root thay nội dung;
  listener trên window/document phải gỡ trong cleanup.
- Nếu module thêm tài nguyên phụ, dùng đường dẫn từ gốc dự án hoặc từ entry đã được xác định;
  không giả định URL tương đối trong JS tính từ thư mục của script.

## 6. Kiểm tra trước khi chuyển status thành ready

1. So sánh ít nhất một ca chuẩn tính tay và các biên của đầu vào.
2. Không xuất hiện NaN/Infinity hoặc đại lượng vượt giả thiết.
3. Chuyển simulation/practice; trong practice không lộ sẵn kết luận/đường lí thuyết.
4. Ghi nhiều số đo, xóa một dòng, xóa cả bảng, xuất CSV, đặt lại.
5. Đổi điều kiện mô hình: không trộn dữ liệu khác điều kiện vào một đường lí thuyết.
6. Kiểm tra đồ thị đúng tên trục, đơn vị, miền và điểm đo.
7. Dùng bàn phím, thao tác chạm, màn hình 320/390/768/1440 px; không tràn ngang trang.
8. Mở bằng file://, máy chủ HTTP và đường dẫn con /ten-thu-muc/.
9. Thử đường dẫn #experiment/id trực tiếp, bấm Back, mở lại bài và kiểm tra cleanup.
10. Kiểm tra Console; xác nhận các bài cũ vẫn mở và hoạt động.

## 7. Tài khoản, giao bài và kiểm tra sau này

Tách lưu kết quả thành service, để module trả dữ liệu đo và điều kiện theo cấu trúc
{schemaVersion, experimentId, mode, parameters, measurements, notes}.
Có thể thêm adapter local/server mà không viết lại bộ giải Vật lí.
Chế độ assessment cần thiết kế nhiệm vụ, đáp án, độ dung sai và giao diện chấm riêng;
chỉ bật sau khi triển khai. Chấm quan trọng phải kiểm chứng phía máy chủ, không tin
mã client. Phiên bản hiện tại không có đăng nhập hay chấm điểm giả lập.
PWA/service worker thêm ở lõi với version cache; không sửa công thức từng module.

## 8. Câu lệnh bàn giao cho AI

“Thêm thí nghiệm [tên bài] vào Phòng thí nghiệm ảo Vật lí THPT hiện tại.
Đọc docs/CACH_THEM_THI_NGHIEM_MOI.md và data/experiments.js trước.
Giữ kiến trúc IIFE + PhysicsLab.register + mount/cleanup; ưu tiên chỉ thêm thư mục
module và sửa metadata. Trình bày mô hình Vật lí trước khi code. Hoàn thiện mô phỏng,
thực hành, đo đạc, bảng, đồ thị và reset. Kiểm tra desktop/mobile và hồi quy hai bài
đã có. Liệt kê đúng file mới/file sửa và trả lại bản ZIP đầy đủ có thể deploy.”

Gửi kèm bản ZIP mới nhất mỗi lần phát triển ở cuộc trò chuyện khác.

## Bổ sung v1.1

Module chuyen-dong-thang là ví dụ hoàn chỉnh có nhân vật kéo được và ba đồ thị đồng thời.
CSS riêng nằm ở experiments/lop-10/chuyen-dong-thang/style.css, được module tự tải.
Các helper đồ thị có trục giá trị âm/dương hiện nằm trong module; chỉ tách ra core
khi thêm bài khác thực sự cần tái sử dụng. Không đổi kiến trúc mount/cleanup.

## Bổ sung v1.2

con-lac-lo-xo là ví dụ có hai cách bố trí trong cùng module, đồ thị đa đường và hai
bảng số liệu. Bảng trạng thái xóa khi đổi điều kiện, bảng đo chu kì giữ điều kiện từng
loạt để so sánh. Khi thêm bộ giải khác, vẫn giữ đơn vị/giả thiết rõ và cleanup đúng.
