LMS-BGN/docs/technical/design-notes-interaktif.md

7.1 KiB

Design Notes — Pemetaan sampleInteractiveExamData ke OpenAPI

Tanggal: 2025-11-12 Tujuan: Menyelaraskan struktur data kuis interaktif dari frontend (sampleInteractiveExamData) dengan kontrak backend (OpenAPI) untuk create/score/summary, agar diskusi skema dan wiring berikutnya berjalan mulus.

Ruang Lingkup & Asumsi

  • Frontend: sumber data contoh berada di src/app/exam-session/page.tsx (tipe soal: enhanced MCQ, video scenario, image hotspot, media gallery, puzzle, scenario).
  • Backend: spesifikasi saat ini di docs/api/openapi.yaml mendukung penilaian sederhana; perluasan diperlukan untuk tipe interaktif.
  • Kompatibilitas: halaman ringkasan (exam-summary) saat ini menampilkan userAnswer sebagai string; detail interaktif akan ditambahkan secara opsional.

Tipe Soal & Pemetaan Payload

  1. enhanced_mcq
  • Struktur frontend: { id, content, options[], correctAnswers[], hints?, explanation?, points?, confidenceEnabled? }
  • Score payload: { questionId, type: "enhanced_mcq", choiceIndex, confidence? }
  • Summary payload (kompatibel): userAnswer sebagai teks; details (opsional) { choiceIndex, confidence }
  • Catatan: confidence untuk analitik, tidak memengaruhi skor kecuali aturan penilaian menggunakannya.
  1. video_scenario
  • Struktur frontend: { id, title, steps: [{ stepId, prompt, options[] }], scoringRules? }
  • Score payload: { questionId, type: "video_scenario", scenarioAnswers: [{ stepId, selectedOptionId }] }
  • Summary payload: userAnswer diringkas; details (opsional) { steps: [{ stepId, selectedOptionId, isCorrect, earnedPoints }] }
  • Catatan: skor dihitung agregat per-step mengikuti scoringRules.
  1. image_hotspot
  • Struktur frontend: { id, imageUrl, hotspots: [{ hotspotId, label }], scoringRules? }
  • Score payload: { questionId, type: "image_hotspot", selectedHotspots: [hotspotId] }
  • Summary payload: userAnswer diringkas; details (opsional) { selectedHotspots, correctHotspots }
  • Catatan: titik hotspot adalah ID referensial (koordinat didefinisikan di konten soal).
  1. media_gallery
  • Struktur frontend: { id, items: [{ id, type: "video|image|document", title }], trackingEnabled: true, scoringRules? }
  • Score payload: { questionId, type: "media_gallery", viewedItems: [itemId] }
  • Summary payload: details (opsional) { viewedItems, requiredItems?, completionRate }
  • Catatan: biasanya non-graded; dapat diberi syarat minimal keterlihatan (mis. harus melihat document1).
  1. puzzle (matching)
  • Struktur frontend: { id, pairs: [{ pieceId, targetId }], points?, scoringRules? }
  • Score payload: { questionId, type: "puzzle", matches: [{ pieceId, targetId }] }
  • Summary payload: details (opsional) { matches, correctPairs, earnedPoints }
  • Catatan: penilaian bisa per-pasangan (parsial) atau all-or-nothing.
  1. scenario (single-step)
  • Struktur frontend: { id, prompt, options[], correctOptionId, points? }
  • Score payload: { questionId, type: "scenario", selectedOptionId }
  • Summary payload: details (opsional) { selectedOptionId, isCorrect }

Proposal Perluasan OpenAPI

  1. ExamSessionCreateRequest
  • Tambah (opsional) isInteractive: boolean dan clientContext: { device, appVersion, locale }.
  • Idempotensi create (opsional): idempotencyKey.
  1. ExamScoreRequest
  • answers[] menjadi union berdasarkan type:
{
  "questionId": "string",
  "type": "enhanced_mcq | video_scenario | image_hotspot | media_gallery | puzzle | scenario",
  "choiceIndex": 0,
  "confidence": 4,
  "scenarioAnswers": [{ "stepId": "string", "selectedOptionId": "string" }],
  "selectedHotspots": ["string"],
  "viewedItems": ["string"],
  "matches": [{ "pieceId": "string", "targetId": "string" }],
  "selectedOptionId": "string"
}
  • Wajib: questionId, type. Field lain kondisional sesuai tipe.
  • Tetap dukung bentuk sederhana untuk MCQ sebagai compatibility path.
  1. ExamSummaryResponse.answers[]
  • Tambah type dan details (opsional) sebagai union detail per tipe:
{
  "questionId": "string",
  "question": "string",
  "userAnswer": "string",
  "correctAnswer": "string",
  "isCorrect": true,
  "type": "...",
  "details": {
    "enhanced_mcq": { "choiceIndex": 0, "confidence": 4 },
    "video_scenario": { "steps": [{ "stepId": "s1", "selectedOptionId": "opt4", "isCorrect": true, "earnedPoints": 10 }] },
    "image_hotspot": { "selectedHotspots": ["hotspot1"], "correctHotspots": ["hotspot1","hotspot2"] },
    "media_gallery": { "viewedItems": ["video1","image1"], "requiredItems": ["document1"], "completionRate": 0.8 },
    "puzzle": { "matches": [{ "pieceId": "ccp", "targetId": "def1" }], "correctPairs": 4, "earnedPoints": 10 },
    "scenario": { "selectedOptionId": "option2", "isCorrect": true }
  }
}

Contoh Payload Ringkas per Tipe

  • enhanced_mcq:
{ "questionId": "1", "type": "enhanced_mcq", "choiceIndex": 0, "confidence": 4 }
  • video_scenario:
{ "questionId": "video-scenario-1", "type": "video_scenario", "scenarioAnswers": [ { "stepId": "step1", "selectedOptionId": "opt4" }, { "stepId": "step2", "selectedOptionId": "opt4" } ] }
  • image_hotspot:
{ "questionId": "hotspot-scenario-1", "type": "image_hotspot", "selectedHotspots": ["hotspot1","hotspot2","hotspot4"] }
  • media_gallery:
{ "questionId": "media-gallery-1", "type": "media_gallery", "viewedItems": ["video1","image1","video2","image2","document1","image3"] }
  • puzzle:
{ "questionId": "3", "type": "puzzle", "matches": [ { "pieceId": "ccp", "targetId": "def1" }, { "pieceId": "haccp", "targetId": "def2" } ] }
  • scenario:
{ "questionId": "6", "type": "scenario", "selectedOptionId": "option2" }

Kompatibilitas & Migrasi

  • Backward: userAnswer tetap string untuk semua tipe; details opsional.
  • Forward: UI interaktif dapat menampilkan rincian dari details jika tersedia.
  • Penilaian: jika backend belum mendukung tipe interaktif, treat sebagai MCQ atau non-graded dengan zero-points.

Rencana Wiring Frontend (Ringkas)

  • Create: panggil POST /api/exam-session dengan examId, userId (opsional isInteractive, clientContext). Simpan sessionId.
  • Score: bentuk answers[] union dari state ExamContext; kirim idempotencyKey unik.
  • Summary: ambil GET /api/exam-session/{sessionId}/summary; render sesuai compatibility + details bila tersedia.

Catatan Idempotensi

  • idempotencyKey pada score menjamin hasil konsisten untuk payload yang sama.
  • Backend menyimpan fingerprint jawaban untuk kunci yang sama dalam rentang waktu tertentu.

Referensi

  • OpenAPI: docs/api/openapi.yaml
  • Retrospektif: docs/retrospectives/epic-2-retrospective-2025-11-12.md
  • Frontend contoh: src/app/exam-session/page.tsx, src/app/exam-summary/page.tsx

Diagram Pemetaan (Sederhana)

flowchart LR
  UI[UI Interaktif] --> EC[ExamContext State]
  EC --> MP[Mapper: build answers[]]
  MP --> ASR[ExamScoreRequest.answers[]]
  ASR --> API[POST /api/exam-session/{sessionId}/score]
  • Sumber: ExamContext menyimpan jawaban interaktif per questionId.
  • Mapper: membentuk union answers[] berdasarkan tipe soal.
  • Payload: dikirim sebagai ExamScoreRequest (mode batch) dengan idempotencyKey.
  • Hasil: backend menghitung skor dan menyediakan ringkasan melalui ExamSummaryResponse.