SRDsExercise AILinear Tool
DraftDT-4662Exercise AIFigma design ↗SRDLinearToolCanvasSpeakingScoring

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 — StudentTechnical 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):

FieldKiểuÝ nghĩa
idStringSelf id; FE tham chiếu là snapshotId.
ideaCardsList<LinearCard>Các node ý (đổi tên từ cards).
rootCardIdStringCard gốc (card câu hỏi).
totalCard, totalAnswerCard, totalDescriptionCard, totalCauseCard, totalEffectCardIntegerAggregate, recompute mỗi update.
fullTextStringText các non-QUESTION card nối ". " — luyện nói cả bài.
conceptualizedCardsList<LinearCard>Bản AI triển khai (mỗi card có conceptualizedText) — set khi coaching LIVE = GOOD.
fullConceptualizedTextStringconceptualizedText các non-QUESTION card nối ". ".
contextStringBối cảnh tổng thể bài nói (AI tổng hợp từ conceptualizedCards) — set khi LIVE = GOOD.
correctPercentage, speechCorrections, verdictChấm phát âm per-snapshot.
statusDocumentStatusEDITING (mặc định) → PUBLISHED (LIVE = GOOD).
originalSnapshotIdStringSnapshot nguồn nếu là clone; null cho bản gốc.
linearModeLinearModeWorking 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, lastModifiedAtZonedDateTimeTimestamps.

Helper: findCard(cardId), findSnapshot(snapshotId), getCurrentSnapshot().

Coaching (MENU/LIVE), clone/delete snapshot, conceptualizedCards/context, và coaching tally: xem Do-Test API — Linear Tool.

LinearCard:

FieldKiểuÝ nghĩa
cardIdStringĐịnh danh card.
typeLinearCardTypeQUESTION / ANSWER / DESCRIPTION / CAUSE / EFFECT.
textStringNội dung ý.
conceptualizedTextStringBản AI triển khai từ text (set trên conceptualizedCards khi LIVE = GOOD).
linksList<String>Cạnh có hướng: id các card mà card này trỏ tới (DAG, không chu trình).
orderintThứ tự đọc khi flatten thành script.
stateLinearCardStateEMPTY / FILLED / ERROR.
rootbooleanCard gốc — miễn orphan-cascade delete.
col, rowIntegerGợi ý layout (FE tự dàn khi null).
correctPercentage, speechCorrections, verdictChấm phát âm theo card (khi luyện nói riêng từng card).
enhanceCardEnhance (@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 = links nó 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ác total*fullText mỗ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”: set correctPercentage / speechCorrections / verdict lên LinearQuestionProgress, và chuyển câu sang ANSWERED.
  • Theo card (có cardId) — “đọc riêng từng card”: tìm card qua canvas.findCard(cardId), set scoring lên card đó; không đổi state câu. cardId không tồn tại → CARD_NOT_FOUND (EAID007).

verdict suy ra từ correctPercentage (>= 70GOOD, ngược lại NEED_IMPROVE).

⚠️ Lưu ý tích hợp: PATCH /{progressId}/canvas ghi đè canvas từ payload FE. Nếu payload không mang speechCorrections củ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:

FieldCách tính
totalAnsweredSố câu state ANSWERED.
totalSkippedSố câu state SKIPPED.
totalNotAnswerSố câu state NOT_ANSWER.
averageCorrectPercentageTrung 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.