Skip to content

SDD-051 - NEMO ESSAY ​

Nguồn: SRC-1153 (chủ dự án 30.09.2026). Yêu cầu và danh sách tính năng: PRD-009. Migration: 0317_essay_program.sql. Mã: workers/api/src/modules/essay/, apps/learn/src/essay/, apps/mentors/src/EssayReview.tsx, apps/admin/src/pages/SpeakOrders.tsx.

1. Nguyên tắc thiết kế ​

  1. Chữ của learner chỉ learner ghi được. Mọi route ghi chữ đòi CHÍNH CHỦ (learners.user_id = người đăng nhập), không dùng canAccessLearner vì hàm ấy mở cho người nhà và mọi mentor/staff (SRC-037).
  2. AI chỉ hỏi, và điều đó được máy chặn, không chỉ được dặn trong prompt.
  3. Quá trình được ghi từ snapshot, không từ phím gõ.
  4. Nội dung thư viện sống trong repo (JSON), có cổng audit; D1 chỉ giữ thứ learner tạo ra.

2. Địa chỉ ​

Địa chỉTrangAi mở được
/essayHub: giới thiệu, 6 bước, lối vào, nút ghi danhmọi người đã đăng nhập
/essay/library, /essay/library/{id}Thư viện, trang đọcmọi người đã đăng nhập
/essay/notebookMy notebookmọi người đã đăng nhập (dữ liệu của chính mình)
/essay/registerĐăng ký, chuyển khoản, mã kích hoạtmọi người đã đăng nhập
/essay/stories, /essay/stories/{id}Story Bank, Reflection Ladderđã ghi danh
/essay/drafts, /essay/drafts/{prompt}Bản nháp, lịch sử, so sánh, nhận xét, coach, nhật kýđã ghi danh
/essay/drillsShow, don't tellđã ghi danh

Route essay là chương trình cấp cao nhất trong apps/learn/src/App.tsx (khuôn /ai-teen), nằm sau cổng đăng nhập. Hub chính của learn (Root) có mục "Programs" với thẻ NEMO ESSAY. Chưa ghi danh mà mở trang công cụ thì thấy lời mời đăng ký (API trả 403 NOT_ENROLLED).

3. Nội dung trong repo ​

  • apps/learn/src/essay/content/essays/*.json: 12 bài mẫu. Mỗi bài có paragraphs là mảng các đoạn, mỗi đoạn là mảng cặp câu {en, vi} (căn một-một); highlights tham chiếu chỉ số câu phẳng toàn bài {from, to, skill, label, note_vi}; metadata type, themes, steps, word_count, word_limit, summary_vi.
  • drills.json: 18 câu "kể" kèm gợi ý tiếng Việt và 2 bản viết lại mẫu.
  • story-prompts.json: 14 câu hỏi Story Bank kèm tag gợi ý.
  • content.ts giữ bảng chín kỹ năng (màu, biểu tượng, giải thích), sáu bước, sáu bậc thang.
  • Cổng: scripts/audit-content.mjs khu essay: đúng 12 bài, 2 bài mỗi loại, word_count khớp đếm thật, độ dài (Personal Statement 550-650, bài phụ 150-350), mọi câu có đủ EN/VI, 8-15 highlight không chồng nhau và nằm trong bài, ít nhất 5 kỹ năng, không em dash / en dash; 15+ bài luyện đủ 2 bản mẫu.

4. Đơn hàng và ghi danh ​

Khuôn NEMO SPEAK (SRC-1130): POST /v1/learners/{id}/essay-orders tạo đơn 8.000.000đ, nội dung chuyển khoản ESY-XXXXXX (bảng chữ bỏ 0/O/1/I/L), gửi thư essay-order; bấm lại trả đúng đơn cũ. Admin GET /v1/admin/essay-orders, POST /v1/admin/essay-orders/{id}/paid sinh ESSAY-XXXXXX và gửi thư essay-activation. POST /v1/learners/{id}/essay-enroll nhập mã. Đơn và nhập mã dùng canAccessLearner để bố mẹ mua hộ; mã thuộc về đơn, không thuộc người đặt.

Vì sao bảng song song thay vì nới bảng SPEAK. speak_course_orders.course_id có CHECK ('speak-2','speak-3'); D1/SQLite không sửa CHECK nếu không dựng lại bảng, tức là chép lại một bảng đang giữ tiền thật. speak_course_enrollments còn mang first_session_index của lịch cuốn chiếu mà essay không có. Hai bảng cùng hình dạng rẻ và an toàn hơn; tab admin "Đơn SPEAK" (apps/admin/src/pages/SpeakOrders.tsx) gọi cả hai endpoint, gộp theo thời gian, và gửi "Đã nhận tiền" tới đúng endpoint theo course_id ('essay').

5. Bản nháp, phiên bản, so sánh, nhật ký ​

  • essay_drafts: một bản nháp mỗi (learner, đề); sáu đề ở modules/essay/catalog.ts (common-app 650 chữ, why-major 250, why-college 250, activities 150, community 300, challenge 350).
  • PUT .../essay/drafts/{prompt} kèm session_key (sinh mỗi lần mở trang). Snapshot theo phiên lưu: cùng session_key và phiên bản cuối còn trong 30 phút thì ghi đè phiên bản ấy; khác phiên hoặc quá 30 phút thì mở phiên bản mới; chữ không đổi thì không có phiên bản mới.
  • GET .../versions/{no}, GET .../diff?from&to: so sánh theo từ bằng LCS (modules/essay/diff.ts).
  • essay_process_log: draft_snapshot, ladder_answer, comment, ai_turn, drill, story. Chỉ ghi ở các lượt lưu; không có bảng phím gõ nào.
  • Giao diện: đếm chữ trực tiếp, đỏ khi vượt trần; lưu khi bấm Save hoặc rời ô chữ (chặn lượt lưu kép).

6. Mentor ​

  • Cổng mentor (apps/mentors) có tab Essay trong trang một learner (EssayReview.tsx).
  • GET /v1/mentor/essay/learners: learner đã ghi danh VÀ được giao cho mentor này (mentor_assignments active); admin thấy tất cả.
  • GET /v1/mentor/essay/learners/{id}/drafts: chỉ khi được giao.
  • POST /v1/mentor/essay/drafts/{draftId}/comments: kind = question | observation | rubric (rubric bắt buộc một trong chín kỹ năng), neo bằng anchor_start/anchor_end; máy chủ cắt anchor_text từ bản nháp hiện tại. Nhánh mentor chỉ INSERT vào essay_comments; không route nào cho mentor ghi essay_drafts, và route ghi của learner đòi chính chủ nên mentor, admin, bố mẹ gọi vào đều 401.
  • Learner thấy nhận xét ngay trong bản nháp: đoạn được neo tô vàng, bấm để xem.

7. AI coach chỉ hỏi ​

POST .../essay/coach {kind: draft|story, ref_id} (modules/essay/coach.ts):

  1. Prompt essay.coach-questions@1 (registry shared/prompts.ts, model llama70bFast qua AI Gateway) cấm viết văn, trả JSON 3-5 câu hỏi gắn chín kỹ năng.
  2. Lọc sau: loại câu không kết thúc bằng ?, dài quá 240 ký tự, có xuống dòng, có dấu hiệu viết lại ("here is", "you could write", "viết lại", "câu mẫu"...), có câu tiếng Anh ngôi thứ nhất kiểu văn bài luận, có trích dẫn từ 7 từ trở lên, hoặc có từ hai câu khẳng định đứng trước câu hỏi. Còn dưới 3 câu thì lượt ấy hỏng.
  3. Hỏng thì thử lại đúng một lần, rồi trả câu hỏi mẫu soạn sẵn (source: "template").
  4. Trần tiền: đọc sổ ngày ai:usage:<ngày> dùng chung với cron (shared/aiBudget.ts); chạm AI_DAILY_USD_CAP thì không gọi model, trả câu hỏi mẫu. Mỗi lượt gọi được cộng vào sổ.
  5. Trần lượt: RATE_LIMITS.essayAiCoach 8 lượt / learner / ngày (429).
  6. Mỗi lượt ghi ai_turn vào nhật ký, kèm nguồn và các câu hỏi.

8. Story Bank, Reflection Ladder, bài luyện, ghi chú ​

  • essay_stories: thẻ câu chuyện {prompt_id, title, body, themes, skills, essay_types}.
  • essay_ladder_rungs: sáu bậc (What happened? → ... → What does it reveal about me?). PUT .../ladder/{n} trả 409 RUNG_LOCKED nếu bậc n-1 chưa có hoặc dưới 80 ký tự; giao diện khoá bậc tương ứng.
  • essay_drill_attempts: mỗi lần lưu một lượt viết lại; giao diện hiện 2 bản mẫu sau khi lưu.
  • essay_marks: highlight + ghi chú riêng theo chỉ số câu; chỉ chính chủ đọc/sửa/xoá, không cần ghi danh.

9. Kiểm chứng ​

  • workers/api/src/modules/essay/routes.test.ts (D1 thật): đơn/mã cho khoá 'essay'; cổng ghi danh; ghi chú riêng tư (bố mẹ, người lạ, mentor, admin đều 401); mentor chỉ nhận xét và chỉ learner được giao, không sửa được chữ; coach (câu hỏi sạch, văn hộ bị lọc + thử lại + câu mẫu, trần tiền, trần lượt); snapshot + lịch sử + diff; bậc thang khoá; bài luyện.
  • apps/learn/e2e/essay.spec.ts và 11 địa chỉ /essay/** trong mobileAudit.spec.ts.
  • scripts/audit-content.mjs khu essay.

Trace ​

NguồnMục
SRC-1153 thư viện, highlight, song ngữ, sổ tay§3, §8
SRC-1153 đăng ký như SPEAK§4
SRC-1153 bản nháp, phiên bản, nhật ký§5
SRC-1153 mentor chỉ nhận xét§6
SRC-1153 AI chỉ hỏi§7
SRC-1153 Story Bank, Ladder, Show don't tell§8