Guided Journey · Khi nào dùng, giải phẫu và luật nội dung
Một phần của Guided Journey pattern (SRC-1253). Khi nào dùng mẫu Guided Journey, giải phẫu từng phần, luật nội dung, tương tác và a11y, hiện thực, ví dụ, nên và không nên.
1. Khi nào dùng
Dùng khi learner cần tự trả lời một câu hỏi lớn ("Mình có kịp không?", "Mình nên bắt đầu từ đâu?") và câu trả lời ghép được từ 3 đến 9 câu nhỏ, mỗi câu một quyết định.
Nhịp của mẫu là Show, Understand, Explore: cho thấy câu hỏi, giúp hiểu câu trả lời, rồi mở đường đi tiếp. Đây là một công cụ để learner tự khám phá mình, không phải một trang nói về Nemo. Chữ trên trang nói về learner; Nemo chỉ xuất hiện như người tính hộ.
Không dùng khi:
- nội dung là thông tin tra cứu một chiều (bảng giá, lịch, điều khoản): dùng trang thường;
- chỉ có một câu hỏi: dùng một form hoặc một card;
- các bước không phụ thuộc nhau và learner nên thấy tất cả cùng lúc: dùng lưới card;
- trình tự dài hơn khoảng 9 bước: tách thành nhiều journey (như hub learn chia 6 journey thành 3 tầng);
- là luồng làm bài thi có tính giờ: dùng khung học §6b của DS-001.
2. Giải phẫu
Trên learn (mọi trang AI Teen và mọi trang learn.nemo12.com/ielts), "site nav" và thanh (b) là JourneyShell của §9 (SRC-1263): NavBar mang các JOURNEY, thanh tiến độ ngay dưới nêu TÊN từng bước của journey đang đứng (mỗi bước một pill số + tên, SRC-1272), và ở learn /ielts thêm Status Card 3-5 dòng ở góc trên phải vùng nội dung (AI Teen không có, SRC-1272). Sơ đồ dưới là một trang câu hỏi trên www; thanh tròn-và-mũi-tên dưới đây là bản www.
+---------------------------------------------------------------+
| site nav (learn: NavBar = journeys, xem §9) |
+---------------------------------------------------------------+
| (b) STICKY PROGRESS (1)-->(2)-->(3)-->(4)..>(5)..>(6) |
| filled = done, ring = current, grey = later |
+---------------------------------------------------------------+
| (a) HERO |
| DISCOVER YOUR IELTS PATH <- label (small caps) |
| Mình cần gì để đạt IELTS? <- h1 = a question |
| |
| (c) NUMBERED QUESTION LIST |
| (1) +-----------------------------------------------+ |
| | | Mình cần IELTS để làm gì? v [ Sửa ] | view |
| | | Xét tuyển đại học Việt Nam <- own answer | |
| | |-----------------------------------------------| divider |
| | | (Xét tuyển...) (Ứng tuyển du học) ... | |
| | +-----------------------------------------------+ |
| (2) +-----------------------------------------------+ |
| | | Mình cần band bao nhiêu? | current |
| | | Chọn một con số, Nemo dùng nó... <- hint | |
| | |-----------------------------------------------| |
| | | [-] 6.5 [+] [Xác nhận] | (d) |
| | | +---------------- reveal ---------------+ | |
| | | | computed answer + source link | | |
| | +-----------------------------------------------+ |
| (3) +-----------------------------------------------+ |
| | Mình đang ở band mấy? (dim, locked) | later |
| +-----------------------------------------------+ |
| |
| (g) AHA PORTRAIT |
| +===============================================+ |
| | Lộ trình IELTS của bạn . YOUR IELTS PATH | |
| | Mục tiêu Band 6.5 | Hiện tại Band 5.0 | |
| | Khoảng cách 1.5 band | Thời gian ... | |
| | [ ONE primary CTA ] | |
| +===============================================+ |
+---------------------------------------------------------------+(a) Hero
Hai dòng, căn giữa: một nhãn nhỏ chữ hoa (tên tiếng Anh của journey, ví dụ Discover Your IELTS Path) và một h1 là một câu hỏi của learner. Không dòng giới thiệu (chủ dự án 06.10.2026, SRC-1260: các câu hỏi tự nói learner sẽ làm gì), không ảnh hero.
(b) Thanh tiến độ dính (sticky progress)
- Các nút tròn đánh số
1..N, màuaccent(cam): đặc = đã xong (bg-accent text-white), viền = đang làm (border-2 border-accent text-accent), xám = chưa tới (border border-border text-muted-foreground). - Giữa hai nút là icon
ArrowRight(lucide); mũi tên chuyển sangtext-accentkhi bước trước nó đã xong. - Dính ngay dưới thanh nav của site:
position: sticky,topđo bằng chiều cao củaheader.n12-nav-deepkhi trang đã chạy, đo lại khi resize; nềnbg-background/95 backdrop-blurvà một đườngborder-b. - Ở 375px chỉ hiện nhãn của card đang mở (khi trang truyền
activeId; không thì của bước hiện tại), các bước khác chỉ còn vòng số; chữ tối thiểu 12px. - Nhấn card đang mở (
activeId, SRC-1266): nút của card đang mở có vòngring-2 ring-accent, đậm đủ; mọi nút khác mờopacity-50(nút đã xong vẫn đặc, chỉ mờ đi),aria-labelcủa nút đang mở thêm ", đang mở". Done/current vẫn do câu trả lời quyết định; cái được nhấn là card đang mở. - Bấm một nút: card ấy thành card đang mở (nếu bước đã mở được) rồi cuộn tới nó. Nút không nhảy anchor gốc của trình duyệt (
preventDefault, cập nhật#step-idbằnghistory.replaceState). - Bù thanh dính khi cuộn (SRC-1266):
scrollToCard(id)đo lúc chạy chiều caoheader.n12-nav-deep- thanh tiến độ (
[data-journey-bar]) (stickyOffset), rồi cuộn tớicardScrollTop(top, scrollY, offset)= mép trên card nằm 12px dưới đáy thanh, ở 1280 lẫn 375. Gọi sau ~60ms để card cũ đã gập xong. Mở trang bằng/ielts#targethay/ielts/consider#pricecũng đi đường này (cuộn tức thì).scroll-mt-36trên card là phương án dự phòng khi chưa có JS. Mỗi nút là một liên kết#step-idcóaria-current="step"cho bước hiện tại vàaria-labelđầy đủ ("2. Mục tiêu · TARGET, đã xong"), để trạng thái không chỉ nói bằng màu. Cả thanh lànav aria-label="Tiến độ".
- thanh tiến độ (
- Nguồn: component dùng chung
JourneyProgressBar(+ProgressNodes) ởapps/web/src/site/JourneyProgressBar.tsx(SRC-1256), nhậnsteps(id, nhãn ngắn),markOf(done/current/later) vàonNode; trang gọi tự định nghĩa done/current. Dùng ở hai trang:/ielts(done = đã trả lời, PR #455, gỡ nhầm ở #456, khôi phục ở #464) và/ielts/consider(done = learner đã tự mở thẻ ít nhất một lần, lưu trongsessionStorage; current = thẻ đầu tiên chưa mở; nhãn 1-2 chữ như "Lên điểm", "Học phí"). Đừng bỏ thanh này với lý do "danh sách số đã báo vị trí": chủ dự án giữ nó có chủ ý.
(c) Danh sách câu hỏi đánh số
Component NumberedQuestion (apps/web/src/site/NumberedQuestionList.tsx):
- vòng số
size-9bên trái, đường nối dọc mảnh (w-px bg-border) xuống mục kế, trừ mục cuối; - một card bo góc (
rounded-card border border-border bg-card) có header là nút disclosure: tiêu đề là câu hỏi bằng giọng nội tâm của learner, dưới nó đúng một dòng tóm tắt, bên phải là chevron xoay 180 độ khi mở; - dòng tóm tắt là gợi ý trước khi trả lời (Nemo sẽ cho bạn biết gì) và là câu trả lời / kết quả của chính learner sau khi trả lời (
stepSummary); - một divider (
border-t border-border,data-part="card-divider") tách hai dòng đầu (tiêu đề + tóm tắt) khỏi phần còn lại của card khi card mở (SRC-1260;/ielts/considercũng có); - đúng MỘT card đang mở (SRC-1266): card đang mở (do bấm card, bấm nút trên thanh, hoặc đi tiếp sau khi trả lời) mở và đậm đủ; mọi card khác gập lại và mờ (
dim,opacity-50). Mở card khác thì card ấy thành card đang mở. Bấm vào chính card đang mở thì gập nó, lúc ấy không card nào mờ. Card chưa tới bị mờ và khoá (disabled). Chế độ sửa chỉ có ở card đang mở: mở card khác là "Huỷ"; - ô Sửa cố định (
actions,data-part="card-actions"): góc trên bên phải của header, ngang hàng tiêu đề, ngay sau chevron và nằm ngoài nút disclosure (không lồng nút trong nút). Ô có bề rộng giữ sẵn (w-[7.75rem]) đủ cho "Lưu" + "Huỷ", nên đổi "Sửa" thành "Lưu" + "Huỷ" không xê dịch chữ; - vòng số theo card đang mở (
focus): vòng số của card đang mở sáng lên (ring-2 ring-accent), các vòng số khác mờ (opacity-50). Không truyềnfocusthì vòng số giữ nguyên. Luật này thay luật scroll focus của #474 (SRC-1260): cuộn trang một mình không còn đổi chỗ nhấn; card đang mở quyết định cả vòng số bên trái lẫn nút trên thanh.
Chế độ của một card trả lời (SRC-1260)
| Chế độ | Khi nào | Input | Nút |
|---|---|---|---|
locked | bước trước chưa xong | không hiện | không |
answer | card đang mở, chưa trả lời | bật, chọn vào bản nháp | "Xác nhận" cuối card (tắt khi chưa đủ) |
closed | không phải card đang mở (mờ) | không hiện | chevron |
view | card đang mở, đã trả lời | hiện giá trị đã chọn nhưng tắt (chỉ xem) + Reveal | "Sửa" trong ô cố định |
edit | sau khi bấm "Sửa" | bật, chọn vào bản nháp | "Lưu" + "Huỷ" đúng chỗ "Sửa" |
- Chọn chưa phải lưu: mọi input (kể cả chip ở câu 1) chỉ đổi bản nháp; "Xác nhận" / "Lưu" mới ghi vào visitor state và tính lại các bước sau. "Huỷ" bỏ bản nháp, giá trị đã lưu hiện lại. Gập card đang sửa cũng là "Huỷ". Mỗi lúc chỉ một card ở chế độ
edit. - Bước tính toán không có input (§f) không có ô Sửa; nó có đúng một nút "Đã hiểu" để gập lại và cuộn tới bước kế. Bước có input phụ (ví dụ số giờ mỗi tuần ở
timeline) theo bảng trên như mọi bước.
(d) Hé lộ dần: Answer, Reveal, Answer
Mỗi bước: learner chọn, ngay dưới đó hiện một khối Reveal (bg-muted/60, rounded-2xl, motion opacity 0 -> 1, y 8 -> 0) chứa điều Nemo vừa tính ra từ câu trả lời ấy. Sau đó card đóng lại, bước kế mở ra và được cuộn vào tầm nhìn (block: "nearest", cuộn mượt trừ khi reduced-motion).
(e) Trí nhớ giữa các bước
- Bước sau nhắc lại câu trả lời trước (khoảng cách tính từ band mục tiêu và band hiện tại), không bao giờ hỏi lại.
- Câu trả lời lưu trong visitor state (www: sổ
n12.visitor.v2, entryielts, fielddiscover, bản bố mẹ fieldparentDiscover, §2h; luật ở visitor-state), được kiểm lại mỗi lần đọc. Quay lại trang là thấy đúng chỗ mình đã dừng. - Sửa một câu trước thì mọi bước phía sau tính lại từ cùng một hàm thuần; không có số nào lưu cứng.
(f) Bước tính toán không cần nhập
Một số bước chỉ có Reveal (ví dụ feasibility: "Mình có kịp không?"). Bước ấy mở cùng lúc với bước cung cấp dữ liệu cuối cùng cho nó và được tính là xong khi đủ dữ liệu.
(g) Kết thúc "Aha"
Khi mọi bước xong, hiện một card chân dung (Card viền border-2 border-accent): tiêu đề "của bạn", một lưới dl gom mọi câu trả lời và kết quả, và một CTA chính duy nhất (buttonVariants({ variant: "accent" })). Thêm một dòng nhỏ nói dữ liệu lưu ở đâu và cách sửa.
(h) Tham số giọng: một journey, nhiều người hỏi (SRC-1273)
Khi cùng một chuỗi câu hỏi có hai người đọc (learner tự hỏi, bố mẹ hỏi về con), không chép component. Component journey nhận một prop persona và mọi câu khác nhau theo người đọc nằm trong một bảng giọng; phần còn lại dùng chung tuyệt đối.
| Đổi theo giọng (bảng giọng) | Dùng chung (không bao giờ tách) |
|---|---|
| nhãn, h1 (câu hỏi trang của đường dẫn ấy), câu hỏi và gợi ý từng bước | danh sách bước và thứ tự |
| chữ trong Reveal ("bạn" / "con", "Bạn tự chấm" / "Bố mẹ tự đánh giá") | thanh tiến độ, một card đang mở, chế độ xem/sửa, ô Sửa |
| tiêu đề chân dung và CTA chính duy nhất (nhãn + đích) | bù thanh dính khi cuộn, anchor, prerender |
| model thuần: kiểm câu trả lời, migration, mọi phép tính và nguồn |
- Giọng mặc định là bản đang chạy, và bản ấy phải render y hệt từng byte sau khi thêm tham số (test so với bản trước, hoặc snapshot).
- Mỗi giọng lưu riêng. Câu trả lời lưu theo persona (www: cùng mục visitor state, mỗi persona một trường), để hai người dùng chung một máy không ghi đè nhau. Bố mẹ ước của con và con tự chấm là hai câu trả lời khác nhau, không phải một.
- Tự đánh giá ghi đúng người đánh giá: "bạn tự chấm" cho learner, "Bố mẹ tự đánh giá" cho bố mẹ.
- Dữ liệu từ tài khoản (learn) nói về tài khoản trên máy này, không gán cho "con" hay "bạn" khi không biết ai đang đăng nhập.
- Hiện thực:
IeltsDiscover(persona, bảngVOICE),PARENT_STEP_COPY+stepCopy(persona)trongieltsDiscoverModel.ts;App.tsxgắnkeytheo persona để đi qua lại giữa hai trang không mang trạng thái giao diện sang. Luật lưu: visitor-state.
3. Luật nội dung
- Giọng nội tâm của learner (học sinh 16-17 tuổi): "Mình cần band bao nhiêu?", không phải "Khám phá lộ trình đột phá cùng Nemo". Không ngôn ngữ marketing. Trang cho bố mẹ thì là giọng bố mẹ hỏi về con ("Con cần band bao nhiêu?"), cùng luật không marketing (§2h).
- Một quyết định mỗi card. Hai câu hỏi thì hai card.
- Tóm tắt một dòng. Dài hơn một dòng là dấu hiệu card đang chứa hai quyết định.
- Trạng thái rỗng thật thà. Thiếu dữ liệu thì nói thiếu và chỉ đường lấy nó ("Chờ bài chẩn đoán"); không bao giờ bịa số, không đánh dấu "xong" một bước thiếu dữ liệu.
- Số tính ra phải có nguồn, kèm liên kết (Cambridge PDF, nhịp học NEMO), và một câu nói rõ đó là mức tham chiếu, không phải lời hứa. Tự chấm của learner luôn ghi là "bạn tự chấm", không phải phép đo.
- Ngôn ngữ theo bề mặt (bảng ngôn ngữ trong CLAUDE.md): www nói tiếng Việt, tên tiếng Anh chỉ là nhãn đứng sau (SRC-829); learn
/ielts/**có giao diện tiếng Anh, chữ hướng dẫn của Nemo bằng tiếng Việt (SRC-834, SRC-953). - Không em dash; ngày luôn DD.MM.YYYY (hàm
ddmmyyyytrongapps/learn/src/ielts/journeys/model.ts).
4. Tương tác và a11y
- Lựa chọn là radio group (
RadioChips:role="radiogroup",role="radio",aria-checked, rovingtabIndex); phím mũi tên di chuyển và chọn, Tab rời nhóm. Số band dùng nút-/+vàoutput aria-live="polite". - Header card là
buttoncóaria-expanded+aria-controls, nên Tab/Enter/Space chạy không cần code thêm. - Thứ tự Tab trong một card: header, ô Sửa ("Sửa", hoặc "Huỷ" rồi "Lưu"), rồi input và nút cuối card. Bấm "Sửa" thì focus nhảy vào input đầu tiên đang bật; "Lưu" / "Huỷ" đưa focus về "Sửa". Nút trong ô Sửa và nút cuối card cao tối thiểu 44px (
min-h-11,min-h-12). - Không còn listener cuộn: chỗ nhấn (vòng số, nút trên thanh) đi theo card đang mở, không theo vị trí cuộn (SRC-1266 thay luật scroll focus của SRC-1260). Chuyển opacity có
motion-reduce:transition-none. - Motion qua preset
m/AppearcủaMotion.tsx;MotionConfig reducedMotion="user"tự tắt chuyển động, cuộn trang cũng kiểmprefers-reduced-motion. - Mọi câu hỏi được prerender cho SEO: bản HTML tĩnh chứa hero và tiêu đề của mọi bước (card đóng vẫn mang câu hỏi), bước 1 mở, vì lúc prerender visitor state là
null. - Mỗi bước một anchor (
li id="target",scroll-mt-24);/ielts#targetcuộn tới card ấy sau khi hydrate. - Vùng chạm tối thiểu 44px (
min-h-11,min-h-12,size-12); thiết kế cho 375px trước, rồi mớism:trở lên (DS-001 §4c).
5. Hiện thực
Stack: motion + shadcn/ui + Tailwind v4 (DS-001 §0); không thêm thư viện.
| File | Dùng lại gì |
|---|---|
apps/web/src/site/NumberedQuestionList.tsx | NumberedQuestion: vòng số, đường nối, card disclosure |
apps/web/src/site/IeltsDiscover.tsx | RadioChips, BandPicker, Reveal, StepCard, Portrait, luồng advance, prop persona + bảng VOICE (§2h) |
apps/web/src/site/ieltsDiscoverModel.ts | khuôn model thuần: STEPS, STEP_COPY, currentStep, stepDone, stepOpen, stepSummary |
apps/web/src/site/IeltsLearnerJourney.tsx + ieltsLearnerJourneyData.ts | biến thể chỉ đọc (/ielts/consider): 9 card, chữ ở file data, khối dữ liệu sống từ API |
apps/learn/src/ielts/journeys/NumberedStep.tsx | bản learn của cùng hình (token theme sâu của learn): link, disclosure, hoặc active mang CTA |
apps/learn/src/ielts/journeys/model.ts + JourneyPages.tsx | hub 6 journey / 3 tầng (TIERS, JOURNEYS), trạng thái từ dữ liệu API có sẵn |
Token / class: accent (cam, trạng thái xong và CTA), border-border, bg-card, rounded-card, text-ocean-900 (tiêu đề), text-muted-foreground (tóm tắt, chưa tới), bg-muted/60 (Reveal), focus-visible:outline-accent.
Thêm một journey mới:
- File data: danh sách bước +
STEP_COPY(câu hỏi giọng learner, gợi ý một dòng, nhãn EN). - Model thuần (không React, không fetch, không đồng hồ): đọc/kiểm câu trả lời,
currentStep,stepDone,stepSummary, các phép tính có nguồn ghi ngay trong comment đầu file. - Component trang: hero (a) + thanh tiến độ (b) + một
NumberedQuestionmỗi bước (c) +Portrait(g). - Lưu câu trả lời vào visitor state (www) hoặc API của learner (learn), có version.
- Test, chép từ:
apps/web/src/site/ieltsDiscover.test.tsx(thứ tự bước, nhắc lại câu trả lời, sửa thì tính lại, prerender đủ câu hỏi);apps/web/src/site/IeltsLearnerJourney.test.tsx(card disclosure, dữ liệu sống hỏng vẫn còn chữ tĩnh);apps/learn/src/ielts/journeys/model.test.ts(thiếu dữ liệu thì không "xong", không bịa số).
6. Ví dụ đang chạy
- nemo12.com/ielts: Discover Your IELTS Path, 6 bước có nhập + chân dung.
- nemo12.com/parents/ielts: cùng màn ấy, giọng bố mẹ (§2h), CTA "Cho con học thử miễn phí".
- nemo12.com/ielts/consider: 9 câu hỏi chỉ đọc, "Học ở NEMO thì sao?".
- learn.nemo12.com/ielts: hub 6 journey trong 3 tầng (sau đăng nhập).
7. Nên và không nên
| Nên | Không nên |
|---|---|
| h1 là một câu hỏi của learner | h1 là tên sản phẩm |
| một quyết định, một dòng tóm tắt mỗi card | gộp hai câu hỏi vào một card |
| hiện câu trả lời của learner trên card đã xong | hỏi lại điều đã biết |
| nói thiếu dữ liệu và chỉ cách lấy | điền số mẫu hay số đoán |
| ghi nguồn cho mọi con số tính ra | đưa ước tính như lời hứa |
| một CTA chính ở chân dung | nhiều nút cùng độ nổi |
| thử ở 375px trước | để thanh tiến độ tràn ngang ở điện thoại |