152 lines
7.1 KiB
Markdown
152 lines
7.1 KiB
Markdown
# 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`. |