Exercise AI — Linear Tool
LINEAR_TOOL là dạng bài logic nặng nhất: học viên dựng sơ đồ ý phân nhánh (canvas), AI sinh câu, rồi
luyện nói từng card hoặc cả bài. Phần ý đồ sản phẩm: Linear Note — Student
và Technical Proposal.
Mô hình canvas (snapshot model)
LinearQuestionProgress giữ snapshots: List<LinearCanvasSnapshot> + currentSnapshotId (không còn 1 canvas duy nhất). Canvas được flatten thẳng vào snapshot (bỏ lớp LinearCanvas). Mỗi LinearCanvasSnapshot (value snapshot trong progress):
| Field | Kiểu | Ý nghĩa |
|---|---|---|
id | String | Self id; FE tham chiếu là snapshotId. |
ideaCards | List<LinearCard> | Các node ý (đổi tên từ cards). |
rootCardId | String | Card gốc (card câu hỏi). |
totalCard, totalAnswerCard, totalDescriptionCard, totalCauseCard, totalEffectCard | Integer | Aggregate, recompute mỗi update. |
fullText | String | Text các non-QUESTION card nối ". " — luyện nói cả bài. |
conceptualizedCards | List<LinearCard> | Bản AI triển khai (mỗi card có conceptualizedText) — set khi coaching LIVE = GOOD. |
fullConceptualizedText | String | conceptualizedText các non-QUESTION card nối ". ". |
context | String | Bối cảnh tổng thể bài nói (AI tổng hợp từ conceptualizedCards) — set khi LIVE = GOOD. |
correctPercentage, speechCorrections, verdict | Chấm phát âm per-snapshot. | |
status | DocumentStatus | EDITING (mặc định) → PUBLISHED (LIVE = GOOD). |
originalSnapshotId | String | Snapshot nguồn nếu là clone; null cho bản gốc. |
linearMode | LinearMode | Working mode: IDEA_IMPLEMENTATION (mặc định, xây dựng ý) / SPEAKING_PRACTICE (luyện nói). Toggle via PATCH /progress/{progressId}/apply-speaking-practice. |
createdAt, lastModifiedAt | ZonedDateTime | Timestamps. |
Helper: findCard(cardId), findSnapshot(snapshotId), getCurrentSnapshot().
Coaching (MENU/LIVE), clone/delete snapshot, conceptualizedCards/context, và coaching tally: xem Do-Test API — Linear Tool.
LinearCard:
| Field | Kiểu | Ý nghĩa |
|---|---|---|
cardId | String | Định danh card. |
type | LinearCardType | QUESTION / ANSWER / DESCRIPTION / CAUSE / EFFECT. |
text | String | Nội dung ý. |
conceptualizedText | String | Bản AI triển khai từ text (set trên conceptualizedCards khi LIVE = GOOD). |
links | List<String> | Cạnh có hướng: id các card mà card này trỏ tới (DAG, không chu trình). |
order | int | Thứ tự đọc khi flatten thành script. |
state | LinearCardState | EMPTY / FILLED / ERROR. |
root | boolean | Card gốc — miễn orphan-cascade delete. |
col, row | Integer | Gợi ý layout (FE tự dàn khi null). |
correctPercentage, speechCorrections, verdict | Chấm phát âm theo card (khi luyện nói riêng từng card). | |
enhance | CardEnhance (@Transient) | Verdict coaching LIVE per-card (situationType, why) — trả FE, không persist. |
Luật sơ đồ (LinearCanvasMutator)
- Đồ thị có hướng (DAG): quan hệ card =
linksnó trỏ tới; giữ acyclic. - Trục ngang (ý chính): tối đa 4 ý; quá giới hạn → khóa nút thêm.
- Nhánh dọc: đào sâu một ý thành ý con.
- Xóa orphan-cascade: xóa card cha → mất toàn bộ nhánh con; card sống chừng nào còn ≥1 link.
flatten: DFS từ root, nối text theo thứ tự đọc → script nói.recomputeAggregates: tính lại cáctotal*vàfullTextmỗi lần lưu canvas.
fullText loại bỏ card QUESTION
fullText nối text các card theo order, bỏ qua card type QUESTION (card câu hỏi gốc không phải nội
dung học viên nói) — chỉ gồm ANSWER / DESCRIPTION / CAUSE / EFFECT có text.
Chấm phát âm — 2 mức
Cùng endpoint PATCH /{progressId}/speech-correction, phân nhánh theo cardId:
- Cả câu (không
cardId) — “Luyện nói cả bài”: setcorrectPercentage/speechCorrections/verdictlênLinearQuestionProgress, và chuyển câu sangANSWERED. - Theo card (có
cardId) — “đọc riêng từng card”: tìm card quacanvas.findCard(cardId), set scoring lên card đó; không đổi state câu.cardIdkhông tồn tại →CARD_NOT_FOUND(EAID007).
verdict suy ra từ correctPercentage (>= 70 → GOOD, ngược lại NEED_IMPROVE).
⚠️ Lưu ý tích hợp:
PATCH /{progressId}/canvasghi đè canvas từ payload FE. Nếu payload không mangspeechCorrectionscủa card, lưu canvas sau khi đã chấm card sẽ xóa điểm card. FE cần giữ lại scoring khi gửi canvas nếu hai luồng đan xen.
Tổng kết khi submit
POST /{progressId}/submit tính tổng kết per-type qua content mapper factory
(computeSubmitSummary, dispatch theo subtype của content — không instanceof). Với LinearProgressContent,
ghi lên content:
| Field | Cách tính |
|---|---|
totalAnswered | Số câu state ANSWERED. |
totalSkipped | Số câu state SKIPPED. |
totalNotAnswer | Số câu state NOT_ANSWER. |
averageCorrectPercentage | Trung bình correctPercentage của các câu ANSWERED. |
Các dạng khác (turn-based, guided) kế thừa computeSubmitSummary mặc định (no-op) tới khi cần tổng kết riêng.
Submit set status = COMPLETED + completedAt, trả DocumentIdOnly.