Skip to content

SDD-048 - Program Grammar ​

Nguồn: SRC-1104. Yêu cầu và mười quyết định: PRD-008.

1. Route ​

learn.nemo12.com/grammar/** là một mạch riêng trong app learn, cùng kiểu với sân IELTS: App.tsx nhận /grammar thành view grammarProgram rồi giao cả thanh địa chỉ cho grammar/GrammarApp.tsx (lazy load). Mạch tự giữ router của nó bằng history.pushState + popstate:

Địa chỉTrang
/grammarDashboard ba ô + danh sách 5 level
/grammar/<level>Giới thiệu level (tiếng Việt) + 6 mảng, mỗi mảng một thẻ danh sách bài
/grammar/<level>/<lesson>Năm khung Video, Rule, Examples, Common mistakes, Practice ("Coming soon"); bài có chủ đề IELTS tương ứng thì có link "Luyện ngay bài này" sang /ielts/grammar/<topic>

Id bài chỉ cần duy nhất trong một level (test gác). Slug tiếng Anh theo luật SRC-604.

2. Dữ liệu ​

Vòng này không có bảng D1 nào: danh mục là packages/grammar-catalog/catalog.json (level → units → lessons, trường ielts_topic nối sang 36 chủ đề cũ). Test grammar.test.ts gác ba điều: đúng 5 level, mỗi level 4-6 mảng; id bài không trùng trong level; tập ielts_topic bằng đúng tập chủ đề trong ielts/grammar/content, để một chủ đề mới thêm vào /ielts/grammar mà quên xếp vào program sẽ làm test đỏ.

Khi làm nội dung (vòng sau): nội dung bài theo khuôn JSON của Grammar hiện tại; tiến độ và phút học ghi vào learning_events (sổ giờ học duy nhất), không mở sổ thứ hai.

3. Giao diện và quyền ​

Dùng lại nguyên khối của sân IELTS: AuthProvider + SignInGate (bắt đăng nhập), NavBar/PageBody của ielts/Chrome.tsx, CardHint, HelpButton, AvatarMenu không ảnh. Không có component giao diện mới nào ngoài bố cục trang, đúng luật bộ ba Motion + shadcn + Tailwind. Chip trên thanh chỉ có phút học hôm nay (đọc ielts-effort, sổ chung cả bốn kỹ năng). Chân trang ký "NEMO GRAMMAR".

Membership (quyết định 7) chưa chặn gì ở vòng này vì chưa có nội dung để chặn; khi có nội dung thì dùng cùng cổng gói của IELTS.

4. Nội dung bài (SRC-1105) ​

lessonContent(level, lesson) trong GrammarApp.tsx chọn nguồn: bài có ielts_topic lấy chủ đề trong ielts/grammar/content (mã tiến độ là chính id chủ đề, nên tiến độ chung với /ielts/grammar); bài còn lại lấy grammar/content/<level>/<lesson>.json, mã tiến độ g-<level>-<lesson> (khớp regex [a-z0-9-]{1,60} của POST /ielts-grammar, không cần đổi API). Trang bài dùng lại TaskCard, Fold, useGrammarDone của ielts/Grammar.tsx. Từ SRC-1373 (15.10.2026) nội dung không còn trong bundle: bảng D1 grammar_content, learn tải theo level qua GET /v1/public/grammar/levels/{level} (apps/grammar/src/content.ts), lessonContent(data, level, lesson) đọc từ dữ liệu đã tải; file seed ở content/grammar/<level>/<lesson>.json, sinh scripts/seed-grammar.sql bằng scripts/gen-grammar-seed.mjs (PRD-008 §19). Test gác: mọi bài Starter, Elementary (SRC-1107) , Intermediate (SRC-1111) , Upper-intermediate (SRC-1112) và Advanced (SRC-1114) có nội dung đủ khuôn và đáp án nằm trong lựa chọn.

Chủ dự án 28.09.2026: Grammar, IELTS, SAT và Speak together mỗi khu có một nhóm Zalo riêng; link gửi sau. Bảng link ở packages/learn-shell/src/zaloGroups.tsx (ZALO_GROUPS), nút "Zalo" nằm trên thanh trên của cả bốn khu. Khu được chọn theo đường dẫn (zaloGroupFor): /ielts/speaking/pair là nhóm Speaking dù nằm dưới /ielts. Link còn trống thì nút không hiện, để không có nút dẫn tới đâu cả; có link thì chỉ cần điền vào bảng.

Kỳ miễn phí 14 ngày riêng (SRC-1123, 28.09.2026) ​

Chủ dự án: "NEMO GRAMMAR, free riêng 14 ngày". Bảng learner_grammar_membership (migration 0311), route GET /v1/learners/{id}/grammar-membership, chip "Miễn phí: còn N ngày" trên thanh trên /grammar. Kỳ này độc lập với kỳ IELTS: mở khu này không tạo hay tiêu kỳ bên kia. Chi tiết ở SDD-038 §136. Hiện kỳ chỉ để hiển thị, chưa khoá nội dung khi hết hạn, giống IELTS.

6. Set bài tập và cổng chất lượng (SRC-1174, 01.10.2026) ​

Tiêu chuẩn ở PRD-008 §14. Khuôn dữ liệu, giữ tương thích với file cũ:

Trường trong file lessonNghĩa
tasksSet Quick, 5 câu level 1-5 như hiện nay
sets.practiceSet Practice, 10 câu, cùng khuôn Task
sets.masterySet Mastery, 20 câu, cùng khuôn Task

Unit review nằm ở content/grammar/<level>/_units/<unit>.json (dòng kind=unit_review trong D1), gồm { "unit": "<id>", "tasks": [...] }; mỗi task thêm trường lesson để biết nó thuộc lesson nào. Tiến độ ghi theo mã g-<level>-<lesson> như cũ, set Practice và Mastery là level 6-15 và 16-35.

API POST /v1/learners/{id}/ielts-grammar nay nhận level 1-35 (trước là 1-5) và nhận mã gu-<level>-<unit> (danh sách sinh bởi scripts/gen-grammar-topics.mjs). Phút học: câu Quick vẫn cộng 120 giây; câu của Practice, Mastery và Unit review cộng 30 giây (GRAMMAR_SET_TASK_SECONDS). Lý do: một câu của set dài là một câu, không phải một bậc của cả bài; giữ 120 giây thì 35 câu thành 70 phút học cho một bài, con số không ai tin. ✍️ Quyết định của phiên, chờ chủ dự án duyệt.

Giao diện: bài có sets thì thay nút "Start practice" bằng danh sách ba set (tên, số câu, đã đúng bao nhiêu); bấm một set thì chạy từng câu, làm tiếp từ câu đầu chưa đúng. Trang level thêm dòng "Unit review" ở cuối thẻ mỗi unit khi có file; trang ôn ở /grammar/<level>/units/<unit>. Cột tiến độ ở trang level nay đếm cả bài (§11, SRC-1440), không còn n/5 chỉ đếm Quick.

Hai công cụ:

  • npm run grammar:coverage --workspace @nemo12/learn (apps/grammar/scripts/grammar-coverage.ts) in số lesson và unit đạt chuẩn theo từng level. Chỉ báo cáo, không chặn.
  • grammar.test.ts gác bốn điều máy đo được của PRD §14c cho mọi set mới (Practice, Mastery, Unit review). Năm câu Quick cũ được miễn cho tới khi lesson ấy được nâng, để cổng không đỏ ngay từ hôm nay; lesson đã có sets thì Quick của nó cũng phải đạt.

7. Giao diện tiếng Anh, căn giữa (SRC-1173, 01.10.2026) ​

Nhãn và nút của GrammarApp.tsx viết tiếng Anh. TaskCard (dùng chung với /ielts/grammar) nhận cờ english; không có cờ thì giữ nhãn tiếng Việt cho sân IELTS. helpFor trả hướng dẫn tiếng Anh riêng cho /grammar/** (trước đây rơi vào hướng dẫn mặc định của IELTS). Chip kỳ miễn phí ở §"Kỳ miễn phí" nay ghi "Free trial: N days left". Bố cục: mỗi trang bọc trong mx-auto max-w-3xl (trang level max-w-4xl); ô luyện bọc w-full để không co lại trong hàng căn giữa. e2e grammarProgram.spec.ts đo vị trí và độ rộng.

8. Buổi Live thứ Hai (SRC-1307, 09.10.2026) ​

Yêu cầu: PRD-008 §15. Cùng khuôn lịch Speak together (SRC-1088, SDD-052):

  • Lịch tính từ mốc, không lưu. workers/api/src/modules/grammarLive/schedule.ts: buổi số 0 là thứ Hai 12.10.2026 20:00 giờ VN (13:00Z), mỗi tuần một buổi 90 phút, mã buổi YYYY-MM-DD-2000. sessionById chỉ nhận mã đúng lịch, nên mã bịa không vào được bảng đăng ký.
  • Hai bảng (migration 0340): grammar_live_signups (session_id, learner_id) nhớ ai bấm tham gia; grammar_live_recordings (session_id, title, url) giữ link bản ghi, nạp bằng workflow seed-data sau mỗi buổi (chưa có màn admin).
  • API workers/api/src/modules/grammarLive/routes.ts: GET /v1/learners/{id}/grammar-live trả 6 buổi sắp tới và 12 buổi đã xong; POST / DELETE .../grammar-live/{sessionId} tham gia và huỷ. zoom_url chỉ có khi learner đã tham gia và buổi chưa xong; chưa đặt GRAMMAR_ZOOM_URL thì zoom_pending: true. Không có hạn chót tham gia, vì bấm tham gia là đường duy nhất tới link. Bản ghi hiện cho mọi learner.
  • Giao diện apps/grammar/src/GrammarApp.tsx: LiveCard trên trang chủ, LivePage ở /grammar/live, HowItWorks với nút "Ask a mentor on Zalo" (ẩn khi link nhóm Zalo grammar trong zaloGroups.tsx còn trống).
  • Trang công khai apps/web/src/site/grammarProgram.ts: GRAMMAR_LIVE, GRAMMAR_WEEK. Test trong grammarLive/routes.test.ts gác ngày, giờ, thứ của trang công khai khớp schedule.ts.

9. Phản hồi luyện tập (SRC-1308, 09.10.2026) ​

Yêu cầu: PRD-008 §16. TaskCard (nay ở apps/grammar/src/practice.tsx, SRC-1337) giữ ba trạng thái none | wrong | right và số lần sai; MAX_TRIES = 3. Câu đúng mới gọi markDone và recordGrammarTask như trước, nên phút học và tiến độ không đổi nghĩa. Câu lộ đáp án sau 3 lần sai thì không ghi gì. label="" ẩn nhãn trong thẻ khi trang đã có thanh tiến độ: SetRunner vẽ Progress của shadcn cùng chữ "n / N". e2e grammarProgram.spec.ts đi đủ ba nhánh.

10. Gỡ /ielts/grammar (SRC-1337, 11.10.2026) ​

Yêu cầu: PRD-008 §18. Trường ielts_topic bỏ khỏi danh mục; mọi bài có file riêng và mã tiến độ g-<level>-<bài>. Phần dùng chung (kiểu dữ liệu, kho tiến độ, TaskCard, Fold) chuyển từ ielts/Grammar.tsx (đã xoá) sang apps/grammar/src/practice.tsx. Router IELTS giữ một route grammarMoved để chuyển /ielts/grammar* sang /grammar. scripts/gen-grammar-topics.mjs chỉ còn đọc thư mục của Program Grammar, nên API không còn nhận mã chủ đề IELTS cũ. Endpoint /v1/learners/{id}/ielts-grammar giữ tên (đổi tên là việc riêng).

11. Tiến độ bài đếm cả bốn set (SRC-1440, 18.10.2026) ​

Chủ dự án thấy "Used to" hiện 5/5 có dấu tích và Unit 2 là 1/3, trong khi mới làm 5 câu Quick khởi động. Lý do: cột n/5, dấu tích, số bài xong của unit, level và trang chủ đều chỉ đếm Quick (level 1-5), dù một bài có bốn set.

  • Tiến độ một bài = số câu đã làm đúng trên cả bài: Quick 5 (level 1-5), Practice 10 (6-15), Mastery 20 (16-35) dưới mã g-<level>-<bài>, Fix it 3 (1-3) dưới gf-<level>-<bài>: 38 câu. Trang level hiện phần trăm (aria-label "n of 38 questions right"); dấu tích và "bài xong" chỉ khi đủ 38. Câu sai ba lần rồi lộ đáp án không được tính, nên làm lại set (chạy tiếp từ câu đầu chưa đúng) là cách lên 100%.
  • lessonProgress và LESSON_SHAPE ở GrammarApp.tsx đếm thẳng từ tiến độ, không cần tải nội dung, nên trang chủ (chưa tải level nào) đếm đúng như trang level. Đổi lại, mọi bài phải đúng khuôn 5/10/20/3: cổng "lesson shape" trong src/grammar.test.ts chặn bài lệch khuôn. Unit review vẫn đếm riêng (mã gu-).

Chặn tái phát: src/grammar.test.ts (khuôn bài, Quick xong là 5/38), ca e2e SRC-1440 trong e2e/grammarProgram.spec.ts.

12. "Fix it" đổi tên thành "Fix common mistakes" (SRC-1441, 18.10.2026) ​

Chủ dự án hỏi vì sao "Fix it" luôn là 3 dù không sai câu nào. Set ấy không lấy từ câu learner làm sai: nó là 3 câu sai mẫu trong mục Common mistakes của bài, giống nhau cho mọi người. Tên "Fix it" và lời nhắc ở trang unit review ("Got one wrong? ... do its Fix it set") khiến nó đọc như "sửa lỗi của bạn". Nay set tên Fix common mistakes, dòng phụ "The lesson's 3 typical wrong sentences: write them right", và lời nhắc ở unit review chỉ sang set Practice. Mã tiến độ gf- giữ nguyên nên tiến độ đã có không mất.

13. Fill in the blanks ở mỗi unit (SRC-1442, 18.10.2026) ​

Chủ dự án: "Trong mỗi Unit, cần thêm dạng bài tập điền từ. 10 câu, cần điền 2 từ, và có 2 từ gợi ý."

  • Mỗi file content/grammar/<level>/_units/<unit>.json có thêm mảng fill: 10 câu gap, mỗi câu một chỗ ___, hint là đúng 2 từ (hiện dưới câu thành hai thẻ "Use: not · like"), answer đúng 2 từ, accept cho dạng viết khác (ví dụ "do not like"). Đáp án không được chỉ chép lại hai gợi ý theo thứ tự: learner phải đổi thì, dạng, phủ định hay trật tự. Mỗi lesson của unit có ít nhất 3 câu. Lời giải tiếng Việt thật, cùng luật chống câu đệm như mọi set.
  • Không cần migration: thân unit nằm nguyên khối trong grammar_content.body_json, API trả nguyên văn, và schema UnitReview của PUT admin là passthrough. Nạp bằng scripts/seed-grammar.sql như mọi nội dung grammar.
  • Trang unit (/grammar/<level>/units/<unit>) có hai set: Unit review và Fill in the blanks; ?set=fill mở thẳng set mới. Trang level có thêm dòng "Fill in the blanks n/10" dưới dòng Unit review. Tiến độ ghi dưới mã gn-<level>-<unit>, level 1-10.
  • Cổng: fillProblems trong src/standard.ts và ca "fill in the blanks" trong src/grammar.test.ts (mọi unit phải đạt, bộ đo phải bắt được câu hỏng). 710 câu soạn bằng agent, mỗi agent một level.