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ế
- 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ùngcanAccessLearnervì hàm ấy mở cho người nhà và mọi mentor/staff (SRC-037). - AI chỉ hỏi, và điều đó được máy chặn, không chỉ được dặn trong prompt.
- Quá trình được ghi từ snapshot, không từ phím gõ.
- 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ỉ | Trang | Ai mở được |
|---|---|---|
/essay | Hub: giới thiệu, 6 bước, lối vào, nút ghi danh | mọi người đã đăng nhập |
/essay/library, /essay/library/{id} | Thư viện, trang đọc | mọi người đã đăng nhập |
/essay/notebook | My notebook | mọ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ạt | mọ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/drills | Show, 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óparagraphslà 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);highlightstham chiếu chỉ số câu phẳng toàn bài{from, to, skill, label, note_vi}; metadatatype,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.tsgiữ 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.mjskhuessay: đúng 12 bài, 2 bài mỗi loại,word_countkhớ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-app650 chữ,why-major250,why-college250,activities150,community300,challenge350).PUT .../essay/drafts/{prompt}kèmsession_key(sinh mỗi lần mở trang). Snapshot theo phiên lưu: cùngsession_keyvà 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_assignmentsactive); 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ằnganchor_start/anchor_end; máy chủ cắtanchor_texttừ bản nháp hiện tại. Nhánh mentor chỉ INSERT vàoessay_comments; không route nào cho mentor ghiessay_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):
- Prompt
essay.coach-questions@1(registryshared/prompts.ts, modelllama70bFastqua AI Gateway) cấm viết văn, trả JSON 3-5 câu hỏi gắn chín kỹ năng. - 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. - 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"). - Trần tiền: đọc sổ ngày
ai:usage:<ngày>dùng chung với cron (shared/aiBudget.ts); chạmAI_DAILY_USD_CAPthì không gọi model, trả câu hỏi mẫu. Mỗi lượt gọi được cộng vào sổ. - Trần lượt:
RATE_LIMITS.essayAiCoach8 lượt / learner / ngày (429). - Mỗi lượt ghi
ai_turnvà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ả 409RUNG_LOCKEDnế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.tsvà 11 địa chỉ/essay/**trongmobileAudit.spec.ts.scripts/audit-content.mjskhuessay.
Trace
| Nguồn | Mụ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 |