# 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. 2) 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`. 3) 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). 4) 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`). 5) 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. 6) 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`. 2) ExamScoreRequest - `answers[]` menjadi union berdasarkan `type`: ```json { "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. 3) ExamSummaryResponse.answers[] - Tambah `type` dan `details` (opsional) sebagai union detail per tipe: ```json { "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: ```json { "questionId": "1", "type": "enhanced_mcq", "choiceIndex": 0, "confidence": 4 } ``` - video_scenario: ```json { "questionId": "video-scenario-1", "type": "video_scenario", "scenarioAnswers": [ { "stepId": "step1", "selectedOptionId": "opt4" }, { "stepId": "step2", "selectedOptionId": "opt4" } ] } ``` - image_hotspot: ```json { "questionId": "hotspot-scenario-1", "type": "image_hotspot", "selectedHotspots": ["hotspot1","hotspot2","hotspot4"] } ``` - media_gallery: ```json { "questionId": "media-gallery-1", "type": "media_gallery", "viewedItems": ["video1","image1","video2","image2","document1","image3"] } ``` - puzzle: ```json { "questionId": "3", "type": "puzzle", "matches": [ { "pieceId": "ccp", "targetId": "def1" }, { "pieceId": "haccp", "targetId": "def2" } ] } ``` - scenario: ```json { "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) ```mermaid 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`.