Compare commits

...

2 Commits

Author SHA1 Message Date
Dwindi Ramadhana
ab86c254d1 docs: Add REACT_Migration_Plan.md with feasibility assessment
- Fixed broken Markdown formatting (removed excessive backslashes)
- Added Section 11: Feasibility Assessment
  - Current state summary (8 items already in place)
  - Identified gaps (6 items)
  - Detailed gap analysis for session timer, monorepo, nested routes
  - Feasibility score: 7/10
  - Recommended execution order (Phase 0-4)
  - Summary with challenges and strengths
2026-06-17 21:09:40 +07:00
Dwindi Ramadhana
792f9b7483 refactor: redesign admin permalinks to RESTful paths
- Replace fragile /admin/ai-playground and /admin/ai-generation routes
  with item-scoped /admin/questions/{id}/generate endpoints
- Add /admin/tryouts/{id}/questions route using Tryout primary key
  instead of composite query params (?tryout_id=X&website_id=Y)
- Fix variant detail and review-bulk endpoints to use scoped paths
- Update all internal links (dashboard, hierarchy, exams) to new routes
- Remove obsolete ai_playground_view/submit/save functions
2026-06-16 23:54:24 +07:00
17 changed files with 4999 additions and 741 deletions

700
ADMIN_UI_REDESIGN_PLAN.md Normal file
View File

@@ -0,0 +1,700 @@
# Admin UI Redesign Plan
> **Document Type:** UI/UX Improvement Plan
> **Current System:** IRT Bank Soal Admin
> **Date:** 2026-06-15
> **Status:** Draft for Review
---
## Executive Summary
The current admin interface is built from a **developer/system perspective** rather than a **human/admin perspective**. This plan outlines a complete redesign to make the admin dashboard intuitive, workflow-oriented, and human-readable.
### Current Problems
| Problem | Impact |
|---------|--------|
| Navigation uses technical terms | Admins don't understand menu labels |
| Multiple unrelated features in one view | Confusing, overwhelming |
| Data displayed in database terminology | Hard to interpret scores |
| No clear workflow guidance | Admin doesn't know what to do first |
| No contextual help | Unclear what each feature does |
| Mixed concern pages | AI + Questions + Calibration all on one page |
---
## Current State Analysis
### Current Navigation (System POV)
```
├── Dashboard (raw counts)
├── Websites (technical list)
├── Tryout Import (system term)
├── Data Hierarchy (developer term)
├── Basis Items (technical term)
├── Calibration Status (technical term)
├── Item Statistics (technical term)
├── Session Overview (technical term)
├── AI Playground (slang)
└── Password Info (unrelated)
```
### Current Issues
1. **Naming Problems:**
- "Basis Items" → Should be "Question Templates" or "Original Questions"
- "Data Hierarchy" → Should be "Data Overview" or "Website Structure"
- "Tryout Import" → Should be "Import Questions"
- "Calibration Status" → Should be "Question Quality" or "Difficulty Analysis"
- "AI Playground" → Should be "Generate AI Questions"
- "Session Overview" → Should be "Student Attempts"
2. **Dashboard Issues:**
- Shows raw database counts (Tryouts, Items, Sessions)
- No meaningful KPIs or actionable insights
- No visual indicators of system health
3. **Page Organization:**
- Too many technical terms on each page
- Tables show raw data without explanation
- No breadcrumbs or context
---
## Proposed Redesign
### New Navigation Structure (Human POV)
```
🎯 Dashboard (Home)
├── System Health Summary
├── Quick Actions
└── Recent Activity
📋 Questions Bank
├── All Questions (list + search)
├── Question Templates (basis items)
├── Import Questions (from Excel/JSON)
└── Question Quality (calibration status)
🤖 AI Generation
├── Generate New Questions
├── Review Generated Questions
└── Generation History
📊 Exams (Tryouts)
├── All Exams (list)
├── Exam Settings (scoring mode)
├── Student Attempts
└── Normalization Settings
📈 Reports
├── Student Performance Report
├── Item Analysis Report
├── Exam Comparison Report
└── Scheduled Reports
⚙️ Settings
├── Websites Management
├── Account Settings
└── System Info
```
### Navigation Mapping Table
| Current Menu | New Menu | Reason |
|-------------|----------|--------|
| Dashboard | 🎯 Dashboard | Home base |
| Websites | ⚙️ Settings > Websites | Configuration |
| Tryout Import | 📋 Questions > Import Questions | Workflow step |
| Data Hierarchy | ⚙️ Settings | Admin settings |
| Basis Items | 📋 Questions > Question Templates | Content management |
| Calibration Status | 📋 Questions > Question Quality | Quality assurance |
| Item Statistics | 📈 Reports > Item Analysis | Reporting |
| Session Overview | 📊 Exams > Student Attempts | Workflow |
| AI Playground | 🤖 AI Generation | Dedicated feature |
| Password Info | ⚙️ Settings > Account | Configuration |
---
## Detailed Page Redesigns
### 1. Dashboard (Home) — `GET /admin/dashboard`
**Current State:**
```python
# Shows raw counts
body = f"""
<p>Signed in as <strong>{admin}</strong>.</p>
<div class="grid">
<div class="stat">Tryouts<strong>{tryouts}</strong></div>
<div class="stat">Items<strong>{items}</strong></div>
<div class="stat">Sessions<strong>{sessions}</strong></div>
<div class="stat">Completed Sessions<strong>{completed}</strong></div>
</div>
<p><a href="/admin/ai-playground">Open AI Playground</a></p>
"""
```
**Proposed State:**
```
┌─────────────────────────────────────────────────────────────────┐
│ Good Morning, Admin! 👋 │
│ Last login: Today at 9:00 AM │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 📊 System Overview │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 5 Exams │ │ 450 │ │ 1,234 │ │ 89% │ │
│ │ Active │ │ Questions│ │ Students │ │ Avg Score│ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ ⚠️ Attention Needed │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ ⚡ 23 questions need calibration (do this first!) │ │
│ │ 📝 5 AI-generated questions pending review │ │
│ │ 📥 2 exam exports ready for download │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 🚀 Quick Actions │
│ [Import Questions] [Generate AI] [View Reports] [Add Exam] │
│ │
│ 📈 Recent Activity │
│ • 12 students completed "UTBK 2024" in last hour │
│ • 3 new questions generated via AI │
│ • Calibration completed for "SIMAK UI" (95% ready) │
│ │
└─────────────────────────────────────────────────────────────────┘
```
**Key Changes:**
- Human-readable greeting with time
- Meaningful metrics (not raw counts)
- Actionable alerts with urgency indicators
- Quick action buttons with clear labels
- Recent activity feed
---
### 2. Questions Bank — Questions List (`/admin/questions`)
**Current State:** Table with raw database fields
**Proposed State:**
```
┌─────────────────────────────────────────────────────────────────┐
│ 📋 Question Bank │
├─────────────────────────────────────────────────────────────────┤
│ │
│ [Search: "matematika" ] [Filter ▼] [🔍] │
│ │
│ Showing 450 questions across 5 exams │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ ☐ │ Q1 │ Berapakah hasil dari 2 + 2? │ │
│ │ │ │ ▸ Easy (p=0.85) | Used 234x | SIMAK UI │ │
│ ├────┼─────┼──────────────────────────────────────────────┤ │
│ │ ☐ │ Q5 │ Hitung integral dari x² dx... │ │
│ │ │ │ ▸ Medium (p=0.45) | Used 89x | UTBK 2024 │ │
│ ├────┼─────┼──────────────────────────────────────────────┤ │
│ │ ☐ │ Q12 │ Jelaskan teori evolusi... │ │
│ │ │ │ ▸ Hard (p=0.22) | Used 45x | ONM 2024 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ [Delete Selected] [Export Selected] [Edit Selected] │
│ │
│ 📄 Page 1 of 23 [<] [1] [2] [3] ... [>] │
│ │
└─────────────────────────────────────────────────────────────────┘
```
**Key Changes:**
- Question preview in list (not just ID)
- Human-readable difficulty (Easy/Medium/Hard)
- Usage count (how many times used)
- Which exam it belongs to
- Visual indicators for difficulty colors
---
### 3. Question Templates — (`/admin/templates`)
**Current State:** "Basis Items" - confusing technical term
**Proposed State:**
```
┌─────────────────────────────────────────────────────────────────┐
│ 📝 Question Templates │
│ (Original questions used to generate AI variants) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Templates are your "master questions" that AI uses to │
│ create different versions with varying difficulty levels. │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 📝 Template #45: "Berapakah hasil dari 2 + 2?" │ │
│ │ AI Generated Variants: 12 (3 easy, 6 medium, 3 hard) │ │
│ │ [View All Variants] [Generate More] [Edit] │ │
│ ├─────────────────────────────────────────────────────────┤ │
│ │ 📝 Template #89: "Hitung integral dari x² dx..." │ │
│ │ AI Generated Variants: 8 (2 easy, 4 medium, 2 hard) │ │
│ │ [View All Variants] [Generate More] [Edit] │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ [+ Create New Template] │
│ │
└─────────────────────────────────────────────────────────────────┘
```
**Key Changes:**
- Clear explanation of what templates are
- Visual representation of variants
- Easy action buttons
- "Create New Template" prominent
---
### 4. AI Generation — (`/admin/ai-generation`)
**Current State:** "AI Playground" - informal, confusing tabs
**Proposed State:**
```
┌─────────────────────────────────────────────────────────────────┐
│ 🤖 AI Question Generator │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Generate new question variants using AI. │
│ Select a template question and specify difficulty level. │
│ │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ 📝 Select Template │ │ 🎯 Target Difficulty │ │
│ │ [Dropdown: Questions]│ │ ○ Easy (p > 0.70) │ │
│ └──────────────────────┘ │ ● Medium (p ≈ 0.50) │ │
│ │ ○ Hard (p < 0.30) │ │
│ ┌──────────────────────┐ └──────────────────────┘ │
│ │ 📝 How many variants?│ │
│ │ [1] [3] [5] [10] │ ┌──────────────────────┐ │
│ └──────────────────────┘ │ 💬 Additional Notes │ │
│ │ [Optional context...] │ │
│ └──────────────────────┘ │
│ │
│ [🚀 Generate Questions] │
│ │
├─────────────────────────────────────────────────────────────────┤
│ 📋 Generated Questions (Pending Review) │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 🔄 Generating... 2 of 5 questions completed │ │
│ │ [████████░░] 60% │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ✅ Generated & Ready for Review: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ ✓ Variant #123: "Berapakah hasil dari 3 + 4?" (Easy) │ │
│ │ [Preview] [Approve] [Regenerate] [Reject] │ │
│ ├─────────────────────────────────────────────────────────┤ │
│ │ ✓ Variant #124: "Hitung hasil dari 5 + 6..." (Easy) │ │
│ │ [Preview] [Approve] [Regenerate] [Reject] │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
```
**Key Changes:**
- Clear, labeled sections
- Radio buttons for difficulty (not dropdown)
- Progress indicator during generation
- Clear action buttons (Approve/Reject/Regenerate)
- Explanation of what each option means
---
### 5. Question Quality (Calibration) — (`/admin/question-quality`)
**Current State:** "Calibration Status" - technical IRT terminology
**Proposed State:**
```
┌─────────────────────────────────────────────────────────────────┐
│ 📊 Question Quality Dashboard │
│ (Shows how well each question is "calibrated" for testing) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 📖 What is Question Quality? │
│ Questions become "calibrated" after many students answer them. │
│ Well-calibrated questions give accurate student scores. │
│ │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Overall Quality: ████████░░ 78% │
│ (78 out of 100 questions are ready for adaptive testing) │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 📋 By Exam │ │
│ │ │ │
│ │ UTBK 2024 ████████████ 95% ✓ Ready │ │
│ │ SIMAK UI █████████░░░ 72% ⚠️ Partial │ │
│ │ ONM 2024 ██████░░░░░░ 45% ❌ Needs more data│ │
│ │ PASIAD Selection ████████████ 100% ✓ Excellent │ │
│ │ │ │
│ │ [Run Calibration for All Exams] │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Questions Needing Attention: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ ⚠️ Q45 - "Hitung integral..." only answered 12 times │ │
│ │ Need at least 100 answers to calibrate properly. │ │
│ │ Current estimate: p=0.42 (might change) │ │
│ ├─────────────────────────────────────────────────────────┤ │
│ │ ❌ Q78 - "Teori relativitas..." has conflicting answers │ │
│ │ Check if correct answer is correct in database. │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
```
**Key Changes:**
- Clear explanation of what calibration means
- Progress bars for visual understanding
- Status indicators (✓ Ready, ⚠️ Partial, ❌ Needs data)
- Specific recommendations for action
- User-friendly difficulty explanation
---
### 6. Student Attempts — (`/admin/student-attempts`)
**Current State:** "Session Overview" - raw database table
**Proposed State:**
```
┌─────────────────────────────────────────────────────────────────┐
│ 📊 Student Attempts │
│ (See how students performed on each exam) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Filter: [Select Exam ▼] [Status ▼] [Date Range ▼] [Search] │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 📋 UTBK 2024 Results │ │
│ │ │ │
│ │ Participants: 1,234 students │ │
│ │ Average Score (NM): 672 / 1000 │ │
│ │ Average Score (NN): 505 / 1000 │ │
│ │ Completion Rate: 98% (1,209 completed) │ │
│ │ │ │
│ │ Score Distribution: │ │
│ │ ▁▂▃▇█▇▃▂▁ (bell curve centered around 500) │ │
│ │ 200 300 400 500 600 700 800 900 1000 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Recent Attempts: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 👤 John Doe (john@example.com) │ │
│ │ Exam: UTBK 2024 | Completed: Today, 2:30 PM │ │
│ │ Score: NM=720 (85th percentile) | NN=645 │ │
│ │ Correct: 28/30 | Time: 45 minutes │ │
│ ├─────────────────────────────────────────────────────────┤ │
│ │ 👤 Jane Smith (jane@example.com) │ │
│ │ Exam: SIMAK UI | Completed: Today, 1:15 PM │ │
│ │ Score: NM=580 (45th percentile) | NN=485 │ │
│ │ Correct: 22/30 | Time: 52 minutes │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ [Export All Results] [View Detailed Report] [Schedule Report] │
│ │
└─────────────────────────────────────────────────────────────────┘
```
**Key Changes:**
- Grouped by exam with summary stats
- Human-readable student info
- Percentile ranking
- Score distribution visualization
- Clear action buttons
---
### 7. Reports — (`/admin/reports`)
**Proposed State:**
```
┌─────────────────────────────────────────────────────────────────┐
│ 📈 Reports │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Generate detailed analysis reports for exams and students. │
│ │
│ ┌─────────────────────────┐ ┌─────────────────────────┐ │
│ │ 📊 Student Performance │ │ 📋 Item Analysis │ │
│ │ │ │ │ │
│ │ See individual student │ │ Analyze question │ │
│ │ scores, rankings, and │ │ difficulty, validity, │ │
│ │ detailed breakdowns. │ │ and discrimination. │ │
│ │ │ │ │ │
│ │ [Generate Report] │ │ [Generate Report] │ │
│ └─────────────────────────┘ └─────────────────────────┘ │
│ │
│ ┌─────────────────────────┐ ┌─────────────────────────┐ │
│ │ 📈 Exam Comparison │ │ 📅 Scheduled Reports │ │
│ │ │ │ │ │
│ │ Compare scores across │ │ Set up automatic │ │
│ │ different exams or │ │ weekly/monthly reports │ │
│ │ time periods. │ │ delivery. │ │
│ │ │ │ │ │
│ │ [Generate Report] │ │ [Manage Schedules] │ │
│ └─────────────────────────┘ └─────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
```
**Key Changes:**
- Card-based layout with icons
- Clear description of each report type
- Visual cards instead of dropdowns
- Scheduled reports as first-class feature
---
## Implementation Phases
### Phase 1: Navigation Redesign (Foundation)
**Files to modify:**
- `app/admin_web.py` - Update `ADMIN_NAV_ITEMS`
- Create new route handlers
**Steps:**
1. Rename navigation items with human labels
2. Create new route structure
3. Implement breadcrumb system
4. Add help tooltips
**New Navigation Structure:**
```python
ADMIN_NAV_ITEMS = (
("Dashboard", "/admin/dashboard", ("/admin/dashboard",)),
("Questions", "/admin/questions", ("/admin/questions", "/admin/templates")),
("AI Generator", "/admin/ai-generation", ("/admin/ai-generation",)),
("Exams", "/admin/exams", ("/admin/exams", "/admin/student-attempts")),
("Reports", "/admin/reports", ("/admin/reports",)),
("Settings", "/admin/settings", ("/admin/settings",)),
)
```
---
### Phase 2: Dashboard Overhaul
**New Dashboard Components:**
1. Greeting with user name and time
2. System health cards (with meaningful metrics)
3. Action alerts section
4. Quick action buttons
5. Recent activity feed
**Files to modify:**
- `dashboard_view()` function
- `_render_admin_page()` for dashboard-specific layout
---
### Phase 3: Questions Section
**New Pages:**
1. `/admin/questions` - List all questions with search/filter
2. `/admin/questions/{id}` - Question detail view
3. `/admin/templates` - Question templates (formerly basis items)
4. `/admin/questions/import` - Import wizard
**Key UI Components:**
- Question preview cards
- Difficulty badges (Easy/Medium/Hard)
- Color-coded indicators
- Inline search
---
### Phase 4: AI Generation Section
**New Pages:**
1. `/admin/ai-generation` - Main generation interface
2. `/admin/ai-generation/review` - Review pending variants
3. `/admin/ai-generation/history` - Generation history
**Key UI Components:**
- Template selector with preview
- Difficulty radio buttons
- Generation progress bar
- Batch approve/reject actions
---
### Phase 5: Exams Section
**New Pages:**
1. `/admin/exams` - List all exams
2. `/admin/exams/{id}/settings` - Exam configuration
3. `/admin/student-attempts` - Student attempts list
4. `/admin/normalization` - Normalization settings
**Key UI Components:**
- Exam cards with status indicators
- Student attempt cards
- Score distribution visualization
---
### Phase 6: Reports Section
**New Pages:**
1. `/admin/reports` - Report dashboard
2. `/admin/reports/student-performance` - Student report
3. `/admin/reports/item-analysis` - Item report
4. `/admin/reports/exam-comparison` - Comparison report
5. `/admin/reports/scheduled` - Scheduled reports
**Key UI Components:**
- Report type cards
- Export format options
- Schedule configuration
---
## Technical Implementation Notes
### CSS Class Naming Convention
```css
/* Old: System POV */
.stat { }
.grid { }
.table-wrap { }
/* New: Human POV */
.dashboard-hero { }
.metric-card { }
.question-list { }
.difficulty-badge { }
.difficulty-easy { background: #dcfce7; }
.difficulty-medium { background: #fef3c7; }
.difficulty-hard { background: #fee2e2; }
```
### Helper Functions to Create
```python
# In admin_web.py
def _render_question_card(item: Item) -> str:
"""Render a human-readable question card."""
difficulty = _human_difficulty(item.ctt_p)
difficulty_color = _difficulty_color(item.ctt_p)
return f"""
<div class="question-card">
<div class="difficulty-badge {difficulty_color}">{difficulty}</div>
<div class="question-stem">{escape(item.stem[:100])}...</div>
<div class="question-meta">
Used {item.calibration_sample_size}x |
{item.tryout_id}
</div>
</div>
"""
def _human_difficulty(p_value: float | None) -> str:
"""Convert p-value to human-readable difficulty."""
if p_value is None:
return "Unknown"
if p_value > 0.70:
return "Easy"
elif p_value >= 0.30:
return "Medium"
else:
return "Hard"
def _difficulty_color(p_value: float | None) -> str:
"""Get color class for difficulty badge."""
if p_value is None:
return "difficulty-unknown"
if p_value > 0.70:
return "difficulty-easy"
elif p_value >= 0.30:
return "difficulty-medium"
else:
return "difficulty-hard"
```
### Responsive Design
```css
/* Mobile-friendly layout */
@media (max-width: 768px) {
.admin-layout {
flex-direction: column;
}
.sidebar-nav {
display: flex;
overflow-x: auto;
padding: 8px;
}
.metric-cards {
grid-template-columns: repeat(2, 1fr);
}
}
```
---
## Success Metrics
| Metric | Target |
|--------|--------|
| Time to complete common task | Reduce by 50% |
| Admin confusion score | < 2/5 |
| Support tickets about UI | Reduce by 80% |
| Feature discovery rate | > 90% can find features |
---
## Appendix: Terminology Mapping
| System Term | Human Term |
|------------|------------|
| Tryout | Exam / Test |
| Item | Question |
| Basis Item | Question Template / Original Question |
| Session | Student Attempt |
| Calibration | Question Quality / Difficulty Analysis |
| IRT | Adaptive Scoring |
| CTT | Standard Scoring |
| Bobot | Weight / Point Value |
| NM | Raw Score |
| NN | Normalized Score |
| p-value | Difficulty Score |
| Theta | Student Ability Score |
---
## Files to Modify
| File | Changes |
|------|---------|
| `app/admin_web.py` | Complete UI rewrite |
| `app/admin.py` | May need minor updates |
| `requirements.txt` | Add any new frontend deps (if needed) |
---
## Next Steps
1. [ ] Review and approve this plan
2. [ ] Prioritize phases (suggest starting with Phase 1 & 2)
3. [ ] Create mockups/wireframes for key pages
4. [ ] Implement Phase 1: Navigation & Dashboard
5. [ ] User testing with admin users
6. [ ] Iterate based on feedback

18
Dockerfile Normal file
View File

@@ -0,0 +1,18 @@
FROM python:3.11-slim
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
postgresql-client \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements and install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Run migrations and start the app
CMD ["sh", "-c", "alembic upgrade head && uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload"]

612
PROJECT_UNDERSTANDING.md Normal file
View File

@@ -0,0 +1,612 @@
# Project Understanding: IRT-Powered Adaptive Question Bank System
> **Project Name:** IRT Bank Soal
> **Version:** 1.0.0
> **Last Updated:** 2026-06-15
> **Repository:** https://git.backoffice.biz.id/dwindown/yellow-bank-soal
---
## Table of Contents
1. [Executive Summary](#executive-summary)
2. [Project Purpose](#project-purpose)
3. [Tech Stack](#tech-stack)
4. [Project Structure](#project-structure)
5. [Core Concepts](#core-concepts)
6. [Data Models](#data-models)
7. [API Endpoints](#api-endpoints)
8. [Key Services](#key-services)
9. [Scoring Formulas](#scoring-formulas)
10. [Configuration](#configuration)
11. [Workflows](#workflows)
12. [Deployment](#deployment)
---
## Executive Summary
This is a **FastAPI-based backend system** for managing adaptive assessment/tryout exams with sophisticated scoring capabilities. The system supports both **Classical Test Theory (CTT)** and **Item Response Theory (IRT)** scoring methods, with multi-website support for WordPress integration.
### Key Features
| Feature | Description |
|---------|-------------|
| **CTT Scoring** | Classical Test Theory with exact Excel formula compatibility |
| **IRT Support** | Item Response Theory (1PL Rasch model) for adaptive testing |
| **Multi-Site** | Single backend serving multiple WordPress sites |
| **AI Generation** | Automatic question variant generation via OpenRouter |
| **Excel Import/Export** | Bulk import/export questions from Excel files |
| **Adaptive Testing** | Computer Adaptive Testing (CAT) with theta estimation |
| **Normalization** | Static, dynamic, or hybrid score normalization |
---
## Project Purpose
The system replaces traditional fixed-difficulty exams with an **adaptive question bank** that:
1. **Measures student ability accurately** using IRT theta estimation
2. **Provides comparable scores** across different exam sessions via normalization
3. **Generates new questions** using AI when needed
4. **Integrates with WordPress** LMS platforms for student access
5. **Reduces exam fraud** by delivering different question variants to each student
---
## Tech Stack
### Core Technologies
```
Framework: FastAPI >= 0.104.1
Server: Uvicorn >= 0.24.0
Database: PostgreSQL + SQLAlchemy 2.0 (async)
ORM: SQLAlchemy >= 2.0.23
Driver: asyncpg >= 0.29.0
Migrations: Alembic >= 1.13.0
Validation: Pydantic >= 2.5.0
```
### Data Processing
```
Excel: openpyxl >= 3.1.2, pandas >= 2.1.4
Math/Science: numpy >= 1.26.2, scipy >= 1.11.4
```
### External Integrations
```
AI: OpenAI >= 1.6.1 (OpenRouter API)
Task Queue: Celery >= 5.3.6, Redis >= 5.0.1
Admin Panel: FastAPI-Admin >= 1.0.0
```
---
## Project Structure
```
yellow-bank-soal/
├── app/
│ ├── __init__.py
│ ├── main.py # FastAPI app entry point
│ ├── admin.py # FastAPI Admin configuration
│ ├── admin_web.py # Admin web interface
│ ├── database.py # Database configuration & session
│ │
│ ├── api/
│ │ └── v1/
│ │ ├── __init__.py
│ │ └── session.py # Adaptive session endpoints
│ │
│ ├── core/
│ │ ├── __init__.py
│ │ ├── auth.py # Authentication & authorization
│ │ ├── config.py # Settings from environment
│ │ └── rate_limit.py # Rate limiting
│ │
│ ├── models/
│ │ ├── __init__.py
│ │ ├── ai_generation_run.py
│ │ ├── item.py # Question items
│ │ ├── report_schedule.py
│ │ ├── session.py # Student tryout sessions
│ │ ├── tryout.py # Tryout configurations
│ │ ├── tryout_import_snapshot.py
│ │ ├── tryout_snapshot_question.py
│ │ ├── tryout_stats.py # Normalization statistics
│ │ ├── user.py
│ │ ├── user_answer.py # Student responses
│ │ └── website.py
│ │
│ ├── routers/
│ │ ├── __init__.py
│ │ ├── admin.py # Admin-only endpoints
│ │ ├── ai.py # AI generation endpoints
│ │ ├── import_export.py # Excel import/export
│ │ ├── reports.py # Report generation
│ │ ├── sessions.py # Session management
│ │ ├── tryouts.py # Tryout configuration
│ │ └── wordpress.py # WordPress integration
│ │
│ ├── schemas/ # Pydantic request/response models
│ │ ├── __init__.py
│ │ ├── ai.py
│ │ ├── report.py
│ │ ├── session.py
│ │ ├── tryout.py
│ │ └── wordpress.py
│ │
│ └── services/
│ ├── __init__.py
│ ├── ai_generation.py # OpenRouter integration
│ ├── cat_selection.py # Computer Adaptive Testing
│ ├── config_management.py
│ ├── ctt_scoring.py # CTT scoring engine
│ ├── excel_import.py # Excel parsing
│ ├── irt_calibration.py # IRT calibration
│ ├── normalization.py
│ ├── reporting.py
│ ├── tryout_json_import.py
│ └── wordpress_auth.py
├── alembic/ # Database migrations
│ ├── env.py
│ ├── script.py.mako
│ └── versions/
├── tests/ # Unit & integration tests
│ ├── test_auth_scope.py
│ ├── test_auth_tokens.py
│ ├── test_model_mappings.py
│ ├── test_normalization.py
│ ├── test_operational_hardening.py
│ ├── test_route_wiring.py
│ ├── test_security_regressions.py
│ └── test_tryout_json_import.py
├── requirements.txt
├── alembic.ini
├── irt_1pl_mle.py # Standalone IRT MLE script
├── PRD.md # Product Requirements Document
├── project-brief.md # Technical specification
└── handoff.md # Project handoff context
```
---
## Core Concepts
### 1. Tryout (Exam)
A **Tryout** represents a complete exam/test with configurable behavior:
```python
scoring_mode: "ctt" | "irt" | "hybrid"
selection_mode: "fixed" | "adaptive" | "hybrid"
normalization_mode: "static" | "dynamic" | "hybrid"
```
### 2. Item (Question)
An **Item** represents a single question with:
- **Content**: stem (question text), options (A/B/C/D), correct_answer
- **CTT Parameters**: p-value (difficulty), bobot (weight)
- **IRT Parameters**: b (difficulty), se (standard error)
- **Metadata**: slot position, difficulty level, AI generation info
### 3. Session (Student Attempt)
A **Session** tracks a student's attempt:
- Links student (`wp_user_id`) to a Tryout
- Records all answers via `UserAnswer` records
- Stores computed scores: NM, NN, theta
### 4. Website (Multi-Tenant)
The system supports **multiple WordPress websites** from a single backend:
- Each website has isolated data
- Authenticated via `X-Website-ID` header
- WordPress JWT tokens for authentication
---
## Data Models
### Entity Relationship Diagram
```mermaid
erDiagram
Website ||--o{ Tryout : "hosts"
Website ||--o{ User : "contains"
Website ||--o{ Session : "serves"
Website ||--o{ Item : "contains"
Tryout ||--o{ Item : "contains"
Tryout ||--o{ Session : "has"
Tryout ||--o{ TryoutStats : "tracks"
Session ||--o{ UserAnswer : "contains"
Session ||--o{ User : "belongs to"
Item ||--o{ UserAnswer : "answered by"
Item ||--o{ Item : "has variants"
AIGenerationRun ||--o{ Item : "generates"
```
### Model Summary
| Model | Purpose | Key Fields |
|-------|---------|------------|
| `Website` | Multi-tenant isolation | domain, wordpress_url |
| `User` | WordPress user mapping | wp_user_id, website_id |
| `Tryout` | Exam configuration | scoring_mode, selection_mode, normalization_mode |
| `Item` | Question | stem, options, ctt_p, ctt_bobot, irt_b, irt_se |
| `Session` | Student attempt | session_id, NM, NN, theta |
| `UserAnswer` | Single response | response, is_correct, bobot_earned |
| `TryoutStats` | Normalization data | participant_count, rataan, sb |
| `AIGenerationRun` | AI generation batch | model, status, items_generated |
---
## API Endpoints
### Public API (via `/api/v1`)
| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/tryout/{tryout_id}/config` | Get tryout configuration |
| `PUT` | `/tryout/{tryout_id}/normalization` | Update normalization settings |
| `GET` | `/tryout/` | List tryouts for website |
| `GET` | `/tryout/{tryout_id}/calibration-status` | Get IRT calibration status |
| `POST` | `/tryout/{tryout_id}/calibrate` | Trigger IRT calibration |
| `POST` | `/session/` | Create new session |
| `GET` | `/session/{session_id}` | Get session details |
| `POST` | `/session/{session_id}/complete` | Submit answers, calculate scores |
### Admin API (requires admin role)
| Method | Endpoint | Description |
|--------|----------|-------------|
| `POST` | `/ai/generate` | Generate AI questions |
| `POST` | `/import/excel` | Import questions from Excel |
| `GET` | `/export/excel/{tryout_id}` | Export questions to Excel |
| `GET` | `/reports/*` | Generate various reports |
### Adaptive Session API (via `/api/v1/session`)
| Method | Endpoint | Description |
|--------|----------|-------------|
| `POST` | `/adaptive/start` | Start adaptive session |
| `POST` | `/adaptive/respond` | Submit answer, get next item |
| `POST` | `/adaptive/complete` | Complete adaptive session |
---
## Key Services
### 1. CTT Scoring Engine (`ctt_scoring.py`)
Implements Classical Test Theory scoring with exact Excel formulas.
**Key Functions:**
- `calculate_ctt_p()` - Difficulty: p = Σ Benar / Total Peserta
- `calculate_ctt_bobot()` - Weight: Bobot = 1 - p
- `calculate_ctt_nm()` - Raw Score: NM = (Total_Bobot / Total_Bobot_Max) × 1000
- `calculate_ctt_nn()` - Normalized: NN = 500 + 100 × ((NM - Rataan) / SB)
- `categorize_difficulty()` - Categorize by p-value
- `update_tryout_stats()` - Incrementally update normalization stats
### 2. IRT Calibration (`irt_calibration.py`)
Implements Item Response Theory (1PL Rasch model) for adaptive testing.
**Key Functions:**
- `estimate_theta_mle()` - MLE theta estimation for students
- `estimate_b()` - IRT difficulty calibration for items
- `calibrate_item()` - Calibrate single item from response data
- `calibrate_all()` - Batch calibrate all items in tryout
- `calculate_fisher_information()` - Fisher information for item selection
**Parameters:**
- θ (theta): Student ability [-3, +3]
- b: Item difficulty [-3, +3]
- Probability: P(θ) = 1 / (1 + exp(-(θ - b)))
### 3. AI Generation (`ai_generation.py`)
Generates question variants using OpenRouter API.
**Key Functions:**
- `generate_question()` - Generate single question via OpenRouter
- `generate_questions_batch()` - Generate multiple questions
- `save_ai_question()` - Save generated question to database
- `check_cache_reuse()` - Check for reusable similar questions
**Models Supported:**
- Qwen 2.5 32B (balanced)
- Mistral Small (low cost)
- Llama 3.3 70B (premium)
### 4. Excel Import/Export (`excel_import.py`)
Bulk import/export questions from Excel files.
**Key Functions:**
- `parse_excel_import()` - Parse Excel file to items
- `bulk_insert_items()` - Insert parsed items to database
- `export_questions_to_excel()` - Export tryout to Excel
### 5. CAT Selection (`cat_selection.py`)
Computer Adaptive Testing item selection algorithm.
**Key Functions:**
- `select_next_item()` - Select next item based on theta estimate
- `calculate_theta_update()` - Update theta after response
- `check_termination()` - Check if test should end
---
## Scoring Formulas
### CTT (Classical Test Theory)
Based on exact client Excel formulas:
```python
# STEP 1: Tingkat Kesukaran (p-value)
p = Σ Benar / Total Peserta
# STEP 2: Bobot (Weight)
Bobot = 1 - p
# STEP 3: Total Benar per Siswa
Total_Benar = count of correct answers
# STEP 4: Total Bobot Earned per Siswa
Total_Bobot_Siswa = Σ Bobot for each correct answer
# STEP 5: Nilai Mentah (Raw Score)
NM = (Total_Bobot_Siswa / Total_Bobot_Max) × 1000
# STEP 6: Nilai Nasional (Normalized Score)
NN = 500 + 100 × ((NM - Rataan) / SB)
```
### IRT (Item Response Theory)
1PL Rasch Model:
```python
# Probability of correct response
P(θ, b) = 1 / (1 + exp(-(θ - b)))
# Log-likelihood for MLE
LL = Σ [u_i × log(P) + (1-u_i) × log(1-P)]
# Theta estimation via MLE
θ_mle = argmax_θ LL(θ)
```
### Difficulty Categories (CTT Standard)
| p-value | Category | Description |
|---------|----------|-------------|
| p < 0.30 | Sulit | Difficult |
| 0.30 ≤ p ≤ 0.70 | Sedang | Medium |
| p > 0.70 | Mudah | Easy |
---
## Configuration
### Environment Variables
```bash
# Database
DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/irt_bank_soal
# FastAPI
SECRET_KEY=your-secret-key-here
ENVIRONMENT=development # development, staging, production
ENABLE_ADMIN=true
ADMIN_USERNAME=admin
ADMIN_PASSWORD=your-password
# OpenRouter (AI)
OPENROUTER_API_KEY=sk-or-v1-xxx
OPENROUTER_MODEL_QWEN=qwen/qwen2.5-32b-instruct
OPENROUTER_MODEL_CHEAP=mistralai/mistral-small-2603
OPENROUTER_MODEL_LLAMA=meta-llama/llama-3.3-70b-instruct
# Redis/Celery
REDIS_URL=redis://localhost:6379/0
CELERY_BROKER_URL=redis://localhost:6379/0
# CORS
ALLOWED_ORIGINS=http://localhost:3000,https://yourdomain.com
```
### Tryout Configuration Options
```python
# Scoring Mode
scoring_mode = "ctt" # Classical Test Theory
scoring_mode = "irt" # Item Response Theory
scoring_mode = "hybrid" # Both (IRT for calibration, CTT for scoring)
# Selection Mode
selection_mode = "fixed" # Fixed order questions
selection_mode = "adaptive" # Computer Adaptive Testing
selection_mode = "hybrid" # Start fixed, switch to adaptive
# Normalization Mode
normalization_mode = "static" # Use hardcoded rataan/sb
normalization_mode = "dynamic" # Calculate from participant data
normalization_mode = "hybrid" # Dynamic when sufficient data
```
---
## Workflows
### 1. Student Taking a Tryout
```mermaid
sequenceDiagram
participant S as Student
participant API as FastAPI
participant WP as WordPress
S->>API: POST /session/ (start session)
API-->>S: session_id
loop For each question
S->>API: GET /session/{id}/next-item
API-->>S: Question data
S->>API: POST /session/{id}/answer
API-->>S: Next question or completion
end
S->>API: POST /session/{id}/complete
API-->>S: NM, NN scores
```
### 2. Admin Importing Questions
```mermaid
flowchart TD
A[Upload Excel File] --> B[Parse Excel]
B --> C{Validate Structure}
C -->|Invalid| D[Return Error]
C -->|Valid| E[Calculate CTT p & bobot]
E --> F[Bulk Insert Items]
F --> G[Commit to Database]
G --> H[Return Import Summary]
```
### 3. AI Question Generation
```mermaid
flowchart TD
A[Request Generation] --> B{Check Cache}
B -->|Found similar| C[Return Cached]
B -->|Not found| D[Call OpenRouter API]
D --> E{Parse Response}
E -->|Parse Error| F[Return Error]
E -->|Success| G[Save to Database]
G --> H[Return Generated Item]
```
### 4. IRT Calibration
```mermaid
flowchart TD
A[Collect Responses] --> B{Enough Data?}
B -->|No| C[Wait for more]
B -->|Yes| D[For each Item]
D --> E[Get Response Matrix]
E --> F[Estimate b via MLE]
F --> G[Calculate Standard Error]
G --> H[Update Item]
H --> D
D --> I[Mark Items Calibrated]
```
---
## Deployment
### Requirements
- Python 3.10+
- PostgreSQL 14+
- Redis 6+ (for Celery)
- Nginx (reverse proxy)
- aaPanel with Python Manager (recommended)
### Running the Application
```bash
# Install dependencies
pip install -r requirements.txt
# Run migrations
alembic upgrade head
# Start server
uvicorn app.main:app --host 0.0.0.0 --port 8000
# Or with reload (development)
uvicorn app.main:app --reload
```
### Running Tests
```bash
pytest tests/ -v
```
### API Documentation
- Swagger UI: `http://localhost:8000/docs`
- ReDoc: `http://localhost:8000/redoc`
- OpenAPI JSON: `http://localhost:8000/openapi.json`
---
## Security Considerations
### Authentication
- WordPress JWT tokens for user authentication
- `X-Website-ID` header for multi-tenant isolation
- Admin routes protected by admin role check
### Production Hardening
1. **SECRET_KEY** must be set to a strong, unique value
2. **ADMIN_PASSWORD** must not be the default
3. **CORS** origins should be explicitly configured
4. **Database** connections should use SSL in production
5. **Rate limiting** enabled for AI generation endpoints
---
## Glossary
| Term | Definition |
|------|------------|
| **Tryout** | An exam/test assessment |
| **Item** | A single question in a tryout |
| **Session** | A student's attempt at a tryout |
| **CTT** | Classical Test Theory - traditional scoring |
| **IRT** | Item Response Theory - modern adaptive scoring |
| **NM** | Nilai Mentah - raw score [0-1000] |
| **NN** | Nilai Nasional - normalized score [0-1000] |
| **θ (theta)** | IRT ability estimate [-3 to +3] |
| **b** | IRT item difficulty [-3 to +3] |
| **p-value** | CTT proportion correct [0 to 1] |
| **Bobot** | CTT weight (1 - p) |
| **Rataan** | Mean (Indonesian) |
| **SB** | Simpangan Baku - Standard Deviation |
| **CAT** | Computer Adaptive Testing |
| **MLE** | Maximum Likelihood Estimation |
---
## References
- [PRD.md](./PRD.md) - Complete Product Requirements Document
- [project-brief.md](./project-brief.md) - Original technical specification
- [FastAPI Documentation](https://fastapi.tiangolo.com/)
- [SQLAlchemy 2.0](https://docs.sqlalchemy.org/en/20/)
- [Item Response Theory](https://en.wikipedia.org/wiki/Item_response_theory)

393
REACT_Migration_Plan.md Normal file
View File

@@ -0,0 +1,393 @@
# **Frontend Migration Plan: React + Tailwind + shadcn/ui**
**Project:** IRT Bank Soal
**Target Architecture:** Decoupled (FastAPI Backend + React SPA Frontend)
**Date Prepared:** 2026-06-17
## **1. Executive Summary**
This document outlines the strategic plan to migrate the user interface of the **IRT Bank Soal** project from a server-rendered application (FastAPI-Admin & Jinja/Web) to a modern Single Page Application (SPA) architecture using **React, Tailwind CSS, and shadcn/ui components**. This migration aims to significantly enhance UI responsiveness—especially for the Computer Adaptive Testing (CAT) feature—and streamline the development of future interactive features.
## **2. Target Frontend Tech Stack**
| Category | Primary Technology | Rationale |
| :---- | :---- | :---- |
| **Framework** | Vite + React (TypeScript) | Fast build times, responsive Hot Module Replacement (HMR), industry standard for SPAs. |
| **Styling** | Tailwind CSS | Utility-first styling approach, accelerating the UI slicing process. |
| **UI Components** | shadcn/ui | Accessible headless components (Radix UI) that are copy-pasteable and fully customizable via Tailwind. |
| **Data Fetching** | TanStack Query (React Query) | Server-state management, caching, automatic retry logic for unstable connections, and elegant loading/error state handling. |
| **State Management** | Zustand / React Context | Lightweight client-state storage (e.g., WordPress JWT tokens, X-Website-ID, UI themes). Employs persist middleware for crash/reload recovery. |
| **Routing** | React Router DOM | Seamless navigation using **BrowserRouter** for clean URLs. Enables robust **Nested Routing** required for the new hierarchical admin structure. |
| **Form Handling** | React Hook Form + Zod | Strict input validation and type safety (aligning perfectly with Pydantic models in the backend). |
## **3. Repository Restructuring (Monorepo Approach)**
It is highly recommended to use a single repository (monorepo) structure with separate folders for the frontend and backend to simplify version control.
```
yellow-bank-soal/
├── backend/ # Existing FastAPI application folder (app/, alembic/, etc.)
│ ├── Dockerfile # Patched Backend Dockerfile
│ ├── app/
│ ├── requirements.txt
│ └── ...
├── frontend/ # New React application
│ ├── Dockerfile # New Frontend Dockerfile (Multi-stage Nginx)
│ ├── .env.example # VITE_API_BASE_URL references
│ ├── src/
│ ├── package.json
│ └── ...
└── docker-compose.yml # Orchestrates ALL services (API, UI, Redis, Celery)
```
## **4. Migration Phases**
The migration will be divided into 4 sequential phases to minimize disruption to the existing system.
### **Phase 1: Backend Preparation (API Readiness)**
Focus on preparing FastAPI to securely communicate with an external React application.
1. **CORS Configuration:** Update `ALLOWED_ORIGINS` in `app/core/config.py` to permit frontend origins (e.g., `http://localhost:5173` for development and the target production domain).
2. **Endpoint Audit & Restructuring:** Ensure all administrative functionalities are exposed via RESTful API endpoints. **Crucially, restructure the API endpoints to match the new UI hierarchy** (e.g., `GET /api/v1/admin/tryouts/{id}/questions` instead of a standalone `/questions` endpoint).
3. **Data Scrubbing:** Strictly ensure that endpoints like `/api/v1/session/{id}/next-item` omit sensitive fields such as `correct_answer`, `ctt_p`, and `irt_b` from the response payload to prevent client-side cheating.
### **Phase 2: Frontend Scaffolding & Design System**
Focus on project initialization and establishing the UI foundation.
1. **Vite Initialization:** Run `npm create vite@latest frontend -- --template react-ts`.
2. **Environment Variables Setup:** Define `VITE_API_BASE_URL` in frontend `.env` so Axios knows exactly where to route the API requests across different environments.
3. **Tailwind & shadcn Setup:**
* Install Tailwind CSS and configure `tailwind.config.js`.
* Run `npx shadcn-ui@latest init` to set up base CSS variables and utility functions.
4. **Core Components (shadcn):** Install frequently used foundational components:
`npx shadcn-ui@latest add button card input label dialog alert table tabs progress radio-group toast collapsible accordion badge`
5. **API Client Setup:** Configure an Axios instance to universally append:
* The Authorization Bearer Token (from WordPress).
* The `X-Website-ID` header for multi-tenant isolation.
### **Phase 3: Student Portal Construction (Core Business Flow)**
Focus on the primary user interaction: executing the adaptive tryout.
1. **Tryout Listing:** A view displaying available tryouts for the user based on their X-Website-ID.
2. **Exam Dashboard (Session) & Asynchronous Forms:**
* **UI:** Utilize shadcn's Card for the question area, RadioGroup for options, and a Progress bar for tracking exam status.
* **No-Reload Submissions (AJAX):** Completely replace legacy PHP `$_POST` form actions. Forms will use `e.preventDefault()` to stop browser reloads. Submissions to `/adaptive/respond` will be handled asynchronously in the background via TanStack Query.
* **Instant Feedback (Toast):** Upon submitting an answer, the UI will instantly display a non-intrusive shadcn Toast notification and smoothly render the next question.
3. **State Recovery & Timer Security (Crucial):**
* **Anti-Refresh:** Use Zustand's persist middleware to save the current session ID and active question ID into localStorage. If the user accidentally hits F5 or closes the tab, the React app can instantly resume the exam state.
* **Server-Synced Timer:** Do not rely on the client's `Date.now()`. Fetch the exam's exact server-side end time (`expires_at`) from the FastAPI backend and calculate the countdown on the frontend based on that fixed timestamp.
4. **Result Page:** A summary view to display the Raw Score (NM) and Normalized Score (NN) upon exam completion.
### **Phase 4: Admin Panel Construction (Hierarchy-Driven Redesign)**
Based on the `ADMIN_TRYOUT_RESTRUCTURE_PLAN`, the admin UI will shift from a scattered menu to a deeply nested, Tryout-centric navigation leveraging React Router DOM.
1. **Tree-Based Root Navigation (`/admin/tryouts`):**
* Replace standard data tables with an interactive Tree/Collapsible layout grouped by Websites.
* Implement **Stat Cards** inline for each Tryout (showing NM, NN averages, and Calibration Progress).
* Add a global `[+ Import Tryout]` button/modal directly in the header of this tree view.
2. **Nested Tryout Workspaces:**
* Utilize React Router's nested routing to build drill-down pages maintaining the parent Tryout context:
* `/admin/tryout/:id/attempts` (filtered DataTable of sessions)
* `/admin/tryout/:id/normalization` (settings form to update NM/NN targets)
* `/admin/tryout/:id/questions` (filtered DataTable of basis questions)
3. **Question AI Workspace (`/admin/tryout/:id/questions/:questionId/workspace`):**
* Build a dedicated tabbed interface using shadcn Tabs (Generate, Review, Batch).
* Provide seamless integration with the OpenRouter AI generation API endpoints.
## **5. UI/UX Design Guidelines (Human-Centric Approach)**
To resolve the "developer-centric" nature of the legacy system, the frontend must adopt a "Human POV" ensuring the dashboard is intuitive, workflow-oriented, and actionable.
### **5.1. Dashboard Layout Re-imagination**
Shift away from displaying raw database counts. The new Home Dashboard (`/admin/dashboard`) must include:
* **Personalized Greeting:** E.g., "Good Morning, Admin! Last login..."
* **Actionable System Overview:** Display meaningful KPIs (Active Tryouts, Average Scores, Completion Rates).
* **Attention Needed (Alerts):** A dedicated section highlighting urgent tasks (e.g., "23 questions need calibration", "5 AI questions pending review").
* **Quick Actions:** Prominent buttons for daily workflows (`[Import Tryout]`, `[Generate AI]`).
### **5.2. Visual Indicators & Color Coding**
Use Tailwind CSS classes to create consistent, semantic color coding across the application.
* **Difficulty Badges:**
* **Easy (p > 0.70):** Green (`bg-green-100 text-green-800`)
* **Medium (0.30 ≤ p ≤ 0.70):** Yellow (`bg-yellow-100 text-yellow-800`)
* **Hard (p < 0.30):** Red (`bg-red-100 text-red-800`)
* **Calibration Status:**
* **Ready (≥90%):** Green Checkmark icon / Progress Bar
* **Partial (50-89%):** Yellow Warning icon / Progress Bar
* **Needs Data (<50%):** Red Cross icon / Progress Bar
### **5.3. Terminology Mapping (System to UI)**
While the FastAPI backend retains technical database names, the React frontend must translate these terms into human-readable labels:
| System Term (Backend) | UI Label (Frontend) | Context |
| :---- | :---- | :---- |
| Session | Student Attempt | Table headers, Navigation |
| Calibration | Question Quality | Dashboards, Menus |
| IRT | Adaptive Scoring | Tryout Settings |
| CTT | Standard Scoring | Tryout Settings |
| NM (Nilai Mentah) | Raw Score | Reports, Attempt Lists |
| NN (Nilai Nasional) | Normalized Score | Reports, Attempt Lists |
| p-value | Difficulty Score | Question Data Grids |
## **6. Key shadcn/ui Component Mapping**
| Feature Requirement | Recommended shadcn/ui Component | Usage / Context |
| :---- | :---- | :---- |
| Tryouts Hierarchy Map | Collapsible, Accordion | Creates the nested tree structure for Websites -> Tryouts list. |
| Tryout Stat Cards | Card, Badge, Progress | Displays quick metrics (Participants, NM avg, Calibration progress) in the tree. |
| Visual Indicators | Badge | Colored badges for difficulty levels and calibration status. |
| Nested Navigation | Tabs | Switches between "Generate", "Review", and "Batch" in the Question Workspace. |
| Question & Options View | Card, RadioGroup | Wraps the question stem and manages A/B/C/D selections. |
| Form Feedback & Notices | Toast | Displays non-blocking success/error messages asynchronously without page reloads. |
| Alerts / Errors | Alert | Displays prominent API error messages (e.g., lost internet connection). |
| Submit Confirmation | AlertDialog | Prompts the user before calling `/session/complete` to prevent accidental submissions. |
| Item/Student Roster | DataTable (Table) | Renders data grids for Attempts and Questions with server-side sort/filter capabilities. |
| Tryout Settings | Switch, Form | Configures CTT/IRT parameters and Normalization targets. |
## **7. Security Checklist**
* [ ] **Client-Side Authorization:** Implement Route Guards via React Router to prevent unauthorized access to active exam sessions and admin pages.
* [ ] **Sensitive Data Protection:** Ensure the client-state manager (Zustand) never stores data the student shouldn't see (e.g., b values, p-values, or correct answers).
* [ ] **HTML Sanitization:** If the question text (stem) contains rich HTML (from an editor), process it through a library like dompurify before rendering it via `dangerouslySetInnerHTML` to prevent XSS attacks.
## **8. References**
* [Vite Documentation](https://vitejs.dev/)
* [Tailwind CSS Documentation](https://tailwindcss.com/)
* [shadcn/ui Documentation](https://ui.shadcn.com/)
* [TanStack Query Documentation](https://tanstack.com/query/latest)
* [React Router Documentation](https://reactrouter.com/)
## **9. Deployment & Routing Strategy**
Since the React application operates as a standalone frontend and relies on WordPress exclusively via API for data integration, the URL routing must be handled cleanly without interference from WordPress.
### **Chosen Strategy: BrowserRouter (HTML5 History API)**
* **URL Format:** `https://app.domain.com/session/123` (Clean, SEO-friendly URLs)
* **Rationale:** As a standalone deployment, there are no conflicts with WordPress's internal rewrite rules or `.htaccess`. BrowserRouter provides the standard React routing experience.
* **Server Requirement (Crucial):** To prevent 404 Not Found errors when users manually refresh a page (e.g., hitting F5 on `/session/123`), the web server hosting the built React files must be configured to redirect all missing paths back to `index.html`.
## **10. Docker & Containerization Strategy**
To support the decoupled architecture and the existing AI features, the deployment process will utilize Docker Compose to orchestrate the backend, frontend, Redis, and Celery workers.
### **10.1. Backend Dockerfile Patch (`backend/Dockerfile`)**
The existing Dockerfile is well-structured but needs a minor patch for production. The `--reload` flag must be removed as it consumes excessive resources.
```dockerfile
# ... existing setup ...
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# PATCH: Removed the '--reload' flag for production readiness
CMD ["sh", "-c", "alembic upgrade head && uvicorn app.main:app --host 0.0.0.0 --port 8000"]
```
### **10.2. Frontend Dockerfile (`frontend/Dockerfile`)**
A new multi-stage Dockerfile is required for the React application. Stage 1 compiles the application using Node.js, and Stage 2 serves the static files using a lightweight Nginx server configured for BrowserRouter.
```dockerfile
# Stage 1: Build the React application
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
# Using ARG to inject API URL during the build phase
ARG VITE_API_BASE_URL
ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
RUN npm run build
# Stage 2: Serve with Nginx
FROM nginx:alpine
# Copy the built assets from Stage 1
COPY --from=builder /app/dist /usr/share/nginx/html
# Add a custom Nginx configuration to support BrowserRouter (catch-all rule)
RUN echo 'server { \
listen 80; \
location / { \
root /usr/share/nginx/html; \
index index.html index.htm; \
try_files $uri $uri/ /index.html; \
} \
}' > /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
```
### **10.3. Full Orchestration (`docker-compose.yml`)**
Place this at the root of the monorepo to run the complete ecosystem. **This includes Redis and Celery which are required for the OpenRouter AI generation feature outlined in the PRD.**
```yaml
version: '3.8'
services:
# 1. FastAPI Backend
backend:
build:
context: ./backend
ports:
- "8000:8000"
env_file:
- ./backend/.env
depends_on:
- redis
restart: unless-stopped
# 2. Redis Message Broker (Required by Celery)
redis:
image: redis:7-alpine
ports:
- "6379:6379"
restart: unless-stopped
# 3. Celery Worker (For AI Generation Background Tasks)
celery_worker:
build:
context: ./backend
command: ["celery", "-A", "app.core.celery_app", "worker", "--loglevel=info"]
env_file:
- ./backend/.env
depends_on:
- backend
- redis
restart: unless-stopped
# 4. React Frontend SPA
frontend:
build:
context: ./frontend
args:
# Inject the backend API URL into the React build
VITE_API_BASE_URL: "https://api.yourdomain.com/api/v1"
ports:
- "80:80"
depends_on:
- backend
restart: unless-stopped
```
## **11. Feasibility Assessment**
*Assessed: 2026-06-17*
### **11.1 Current State Summary**
| Item | Status | Notes |
| :---- | :---- | :---- |
| FastAPI Backend | ✅ Complete | Well-structured with routers, models, services |
| CORS Configuration | ✅ Configured | `ALLOWED_ORIGINS` in `app/core/config.py` - just needs to add Vite dev server port (`localhost:5173`) |
| Session API Endpoints | ✅ Complete | `GET /api/v1/session/{id}/next_item`, `POST /session/{id}/submit_answer`, `POST /session/{id}/complete` |
| Tryout API Endpoints | ✅ Complete | `GET /tryout`, `GET /tryout/{id}/config`, `PUT /tryout/{id}/normalization` |
| Admin Endpoints | ✅ Complete | Calibration, AI toggle, normalization reset |
| Redis/Celery Setup | ✅ In docker-compose.dev.yml | Used for AI generation |
| WordPress Auth Integration | ✅ In place | `X-Website-ID` header support via `app/core/auth.py` |
| Data Scrubbing (Security) | ✅ Done | Session endpoints already omit sensitive fields (`correct_answer`, `ctt_p`, `irt_b`) |
### **11.2 Identified Gaps**
| Item | Status | Action Required |
| :---- | :---- | :---- |
| No `frontend/` folder | ❌ Missing | Create React app from scratch |
| No root `docker-compose.yml` | ❌ Missing | Currently only `docker-compose.dev.yml` exists |
| Backend Dockerfile location | ⚠️ Inconsistent | Current `Dockerfile` is at root, needs to be moved to `backend/` |
| `expires_at` in session | ⚠️ Not found | Session model may not have server-side end time for timer sync |
| Nested admin routes | ⚠️ Flat structure | Current admin routes are flat, need hierarchical restructuring |
| Monorepo structure | ⚠️ Not set up | Root currently IS the backend, needs folder restructuring |
### **11.3 Detailed Gap Analysis**
#### **Gap 1: Session Timer Implementation**
The plan mentions fetching `expires_at` from the backend for server-synced timers. However, the Session model (`app/models/session.py`) does not have an explicit `expires_at` field. Only `start_time` exists.
**Action needed:** Add `expires_at` field to Session model and update session creation endpoint.
#### **Gap 2: Monorepo Structure**
Current repository layout:
```
yellow-bank-soal/ # Root = backend
├── app/ # FastAPI app
├── Dockerfile # Backend Dockerfile at root
├── docker-compose.dev.yml # Dev setup
```
Plan requires:
```
yellow-bank-soal/ # Root = monorepo
├── backend/ # Move current root to backend/
├── frontend/ # New React app
└── docker-compose.yml # New orchestration
```
**Action needed:** Significant file reorganization - move existing files to `backend/` subfolder.
#### **Gap 3: Nested Admin Routes**
Current admin endpoints are flat:
- `/api/v1/admin/{tryout_id}/calibrate`
- `/api/v1/tryout/{tryout_id}/config`
Plan requires nested structure:
- `/api/v1/admin/tryouts/{id}/questions` ❌ (doesn't exist)
- `/api/v1/admin/tryout/{id}/attempts` ❌ (doesn't exist)
**Action needed:** Create new nested router structure in `app/routers/admin/`.
### **11.4 Feasibility Score: 7/10**
| Category | Score | Notes |
| :---- | :---- | :---- |
| Backend API Readiness | 8/10 | Core endpoints exist, minor gaps in session expiration |
| Infrastructure | 6/10 | Needs restructuring for monorepo |
| Auth Integration | 9/10 | WordPress JWT + X-Website-ID already in place |
| Docker Setup | 5/10 | Need new docker-compose.yml + frontend Dockerfile |
| Data Security | 9/10 | Already scrubbing sensitive fields |
### **11.5 Recommended Execution Order**
1. **Phase 0: Repository Restructuring** (High Impact)
- Move current root contents → `backend/`
- Create `frontend/` with Vite scaffold
- Update Dockerfile references
- Create root `docker-compose.yml`
2. **Phase 1: Backend Additions**
- Add `expires_at` to Session model
- Create nested admin endpoints (`/admin/tryout/:id/questions`, etc.)
- Update CORS for `localhost:5173`
3. **Phase 2-4: Frontend Build** (Follow original plan)
### **11.6 Summary**
The migration plan is **doable** but requires significant upfront work on repository restructuring and a few backend additions. The core FastAPI infrastructure is solid, and the auth/scoring logic is already well-implemented.
**Main challenges:**
1. **Monorepo migration** - moving existing code to `backend/` subfolder
2. **Session expiration tracking** - adding server-side timer (`expires_at`)
3. **Nested admin routes** - restructuring some API endpoints
**Strengths:**
- Complete session/tryout API already exists
- Data scrubbing already implemented
- WordPress integration already in place
- Redis/Celery for AI already configured

View File

@@ -84,7 +84,7 @@ path_separator = os
# database URL. This is consumed by the user-maintained env.py script only. # database URL. This is consumed by the user-maintained env.py script only.
# other means of configuring database URLs may be customized within the env.py # other means of configuring database URLs may be customized within the env.py
# file. # file.
sqlalchemy.url = postgresql+asyncpg://postgres:postgres@localhost:5432/irt_bank_soal sqlalchemy.url = driver://user:pass@localhost/dbname
[post_write_hooks] [post_write_hooks]

File diff suppressed because it is too large Load Diff

112
app/admin_web_icons.py Normal file
View File

@@ -0,0 +1,112 @@
"""
Icon constants using inline SVG (Heroicons style).
These replace emoji usage in the admin UI for consistent, professional icons.
"""
# Navigation icons
ICON_DASHBOARD = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="nav-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6A2.25 2.25 0 0 1 6 3.75h2.25A2.25 2.25 0 0 1 10.5 6v2.25a2.25 2.25 0 0 1-2.25 2.25H6a2.25 2.25 0 0 1-2.25-2.25V6ZM3.75 15.75A2.25 2.25 0 0 1 6 13.5h2.25a2.25 2.25 0 0 1 2.25 2.25V18a2.25 2.25 0 0 1-2.25 2.25H6A2.25 2.25 0 0 1 3.75 18v-2.25ZM13.5 6a2.25 2.25 0 0 1 2.25-2.25H18A2.25 2.25 0 0 1 20.25 6v2.25A2.25 2.25 0 0 1 18 10.5h-2.25a2.25 2.25 0 0 1-2.25-2.25V6ZM13.5 15.75a2.25 2.25 0 0 1 2.25-2.25H18a2.25 2.25 0 0 1 2.25 2.25V18A2.25 2.25 0 0 1 18 20.25h-2.25A2.25 2.25 0 0 1 13.5 18v-2.25Z" /></svg>"""
ICON_QUESTIONS = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="nav-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 5.25h.008v.008H12v-.008Z" /></svg>"""
ICON_IMPORT = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="nav-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5m-13.5-9L12 3m0 0 4.5 4.5M12 3v13.5" /></svg>"""
ICON_AI = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="nav-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M9.813 15.904 9 18.75l-.813-2.846a4.5 4.5 0 0 0-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 0 0 3.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 0 0 3.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 0 0-3.09 3.09ZM18.259 8.715 18 9.75l-.259-1.035a3.375 3.375 0 0 0-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 0 0 2.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 0 0 2.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 0 0-2.456 2.456ZM16.894 20.567 16.5 21.75l-.394-1.183a2.25 2.25 0 0 0-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 0 0 1.423-1.423l.394-1.183.394 1.183a2.25 2.25 0 0 0 1.423 1.423l1.183.394-1.183.394a2.25 2.25 0 0 0-1.423 1.423Z" /></svg>"""
ICON_EXAMS = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="nav-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" /></svg>"""
ICON_REPORTS = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="nav-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 0 1 3 19.875v-6.75ZM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V8.625ZM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V4.125Z" /></svg>"""
ICON_SETTINGS = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="nav-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" /></svg>"""
ICON_LOGOUT = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="nav-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0 0 13.5 3h-6a2.25 2.25 0 0 0-2.25 2.25v13.5A2.25 2.25 0 0 0 7.5 21h6a2.25 2.25 0 0 0 2.25-2.25V15M12 9l-3 3m0 0 3 3m-3-3h12.75" /></svg>"""
# Page icons
ICON_TARGET = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="page-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M12 18v-5.25m0 0a6.01 6.01 0 0 0 1.5-.189m-1.5.189a6.01 6.01 0 0 1-1.5-.189m3.75 7.478a12.06 12.06 0 0 1-4.5 0m3.75 2.383a14.406 14.406 0 0 1-3 0M14.25 18v-.192c0-.983.658-1.727 1.5-1.727s1.5.744 1.5 1.727V18m-4.5 0h.008v.008H14.25v-.008Z" /></svg>"""
ICON_USERS = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="page-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z" /></svg>"""
ICON_CALIBRATION = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="page-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M10.5 6a7.5 7.5 0 1 0 0 15 7.5 7.5 0 0 0 0-15ZM21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" /><path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h6" /></svg>"""
ICON_STUDENTS = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="page-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M4.26 10.147a60.438 60.438 0 0 0-.491 6.347A48.62 48.62 0 0 1 12 20.904a48.62 48.62 0 0 1 8.232-4.41 60.46 60.46 0 0 0-.491-6.347m-15.482 0a50.636 50.636 0 0 0-2.658-.813A59.906 59.906 0 0 1 12 3.493a59.903 59.903 0 0 1 10.399 5.84c-.896.248-1.783.52-2.658.814m-15.482 0A50.717 50.717 0 0 1 12 13.489a50.702 50.702 0 0 1 7.74-3.342M6.75 15a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm0 0v-3.675A55.378 55.378 0 0 1 12 8.443m-7.007 11.55A5.981 5.981 0 0 0 6.75 15.75v-1.5" /></svg>"""
ICON_DOWNLOAD = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="page-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" /></svg>"""
ICON_UPLOAD = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="page-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5m-13.5-9L12 3m0 0 4.5 4.5M12 3v13.5" /></svg>"""
ICON_SEARCH = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="page-icon"><path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" /></svg>"""
ICON_CHECK = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="page-icon"><path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" /></svg>"""
ICON_WARNING = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="page-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" /></svg>"""
ICON_INFO = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="page-icon"><path stroke-linecap="round" stroke-linejoin="round" d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" /></svg>"""
ICON_LIGHTBULB = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="page-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M12 18v-5.25m0 0a6.01 6.01 0 0 0 1.5-.189m-1.5.189a6.01 6.01 0 0 1-1.5-.189m3.75 7.478a12.06 12.06 0 0 1-4.5 0m3.75 2.383a14.406 14.406 0 0 1-3 0M14.25 18v-.192c0-.983.658-1.727 1.5-1.727s1.5.744 1.5 1.727V18m-4.5 0h.008v.008H14.25v-.008Z" /></svg>"""
ICON_TREND_UP = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="page-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 18 9 11.25l4.306 4.306a11.95 11.95 0 0 1 5.814-5.518l2.74-1.22m0 0-5.94-2.281m5.94 2.28-2.28 5.941" /></svg>"""
ICON_TREND_DOWN = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="page-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 6 9 12.75l4.286-4.286a11.948 11.948 0 0 1 4.306 6.43l.776 2.898m0 0 3.182-5.511m-3.182 5.51-5.511-3.181" /></svg>"""
# Huge icons for replacing emojis (24x24 with larger visual weight)
ICON_HUGE_TARGET = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="huge-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M12 18v-5.25m0 0a8.01 8.01 0 0 0 1.5-.189m-1.5.189a8.01 8.01 0 0 1-1.5-.189m3.75 7.478a10.56 10.56 0 0 1-4.5 0m3.75 2.383a13.406 13.406 0 0 1-3 0M14.25 18v-.192c0-.983.658-1.727 1.5-1.727s1.5.744 1.5 1.727V18m-4.5 0h.008v.008H14.25v-.008Z" /></svg>"""
ICON_HUGE_USER = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="huge-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" /></svg>"""
ICON_HUGE_CHECK = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="huge-icon"><path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" /></svg>"""
ICON_HUGE_CLOCK = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="huge-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" /></svg>"""
ICON_HUGE_ROCKET = """<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none" /><g fill="none" stroke="currentColor" stroke-width="1.5"><path stroke-linejoin="round" d="m11.801 6.49l1.486-1.486c1.673-1.673 3.862-2.367 6.18-2.48c.902-.044 1.352-.066 1.714.295c.361.362.34.812.295 1.714c-.113 2.318-.807 4.507-2.48 6.18L17.511 12.2c-1.224 1.223-1.572 1.571-1.315 2.898c.254 1.014.499 1.995-.238 2.732c-.894.895-1.71.895-2.604 0l-7.183-7.183c-.895-.894-.895-1.71 0-2.604c.737-.737 1.718-.492 2.732-.238c1.327.257 1.675-.091 2.898-1.315Z" /><path stroke-linecap="round" d="m2.5 21.5l5-5m1 5l2-2m-8-4l2-2" /><path stroke-linecap="round" stroke-linejoin="round" d="M17.125 7H17m.25 0a.25.25 0 1 1-.5 0a.25.25 0 0 1 .5 0" /></g></svg>"""
ICON_HUGE_CHART = """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="huge-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 0 1 3 19.875v-6.75ZM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V8.625ZM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V4.125Z" /></svg>"""
# Emoji to SVG icon mapping for replacement
EMOJI_TO_ICON = {
# Navigation & main icons
"🏠": ICON_DASHBOARD,
"📝": ICON_QUESTIONS,
"📥": ICON_IMPORT,
"🤖": ICON_AI,
"📋": ICON_EXAMS,
"📊": ICON_REPORTS,
"⚙️": ICON_SETTINGS,
"🚪": ICON_LOGOUT,
"🎯": ICON_HUGE_TARGET,
"👤": ICON_HUGE_USER,
"👥": ICON_USERS,
"⚠️": ICON_WARNING,
"": ICON_INFO,
"🚀": ICON_HUGE_ROCKET,
"": ICON_HUGE_CHECK,
"": """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="huge-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" /></svg>""",
"": ICON_HUGE_CLOCK,
"📈": ICON_TREND_UP,
"📉": ICON_TREND_DOWN,
"💡": ICON_LIGHTBULB,
"👋": '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" style="display:inline;width:28px;height:28px;margin-bottom:-4px;"><path stroke-linecap="round" stroke-linejoin="round" d="M15.042 21.672 13.684 16.6m0 0-2.51 2.225.569-9.47 5.227 7.917-3.286-.672ZM12 2.25V4.5m5.834.166-1.591 1.591M20.25 10.5H18M7.757 14.743l-1.59 1.59M6 10.5H3.75m4.007-4.243-1.59-1.59" /></svg>',
"📊": ICON_REPORTS,
"🚀": ICON_HUGE_ROCKET,
"📈": ICON_TREND_UP,
# Additional icons from UI
"🌐": """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="huge-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M12 21a9.004 9.004 0 0 0 8.716-6.747M12 21a9.004 9.004 0 0 1-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 0 1 7.843 4.582M12 3a8.997 8.997 0 0 0-7.843 4.582m15.686 0A11.953 11.953 0 0 1 12 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0 1 21 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0 1 12 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 0 1 3 12c0-1.605.42-3.113 1.157-4.418" /></svg>""",
"🔍": ICON_SEARCH,
"📁": """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="huge-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12.75V12A2.25 2.25 0 0 1 4.5 9.75h15A2.25 2.25 0 0 1 21.75 12v.75m-8.69-6.44-2.12-2.12a1.5 1.5 0 0 0-1.061-.44H4.5A2.25 2.25 0 0 0 2.25 6v12a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18V9a2.25 2.25 0 0 0-2.25-2.25h-5.379a1.5 1.5 0 0 1-1.06-.44Z" /></svg>""",
"🔐": """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="huge-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H6.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z" /></svg>""",
"": """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="huge-icon"><path stroke-linecap="round" stroke-linejoin="round" d="m3.75 13.5 10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75Z" /></svg>""",
"💾": """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="huge-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0v3.75C20.25 20.653 16.556 22.5 12 22.5s-8.25-1.847-8.25-4.125v-3.75m-16.5 0v3.75" /></svg>""",
"🔄": """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="huge-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" /></svg>""",
"🔘": """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="nav-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M8.25 6.75h12M8.25 12h12m-12 5.25h12M3.75 6.75h.007v.008H3.75V6.75Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0ZM3.75 12h.007v.008H3.75V12Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm-.375 5.25h.007v.008H3.75v-.008Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" /></svg>""",
"📍": """<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="nav-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M15 10.5a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" /><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1 1 15 0Z" /></svg>""",
}
# Navigation icon mapping
NAV_ICONS_SVG = {
"Dashboard": ICON_DASHBOARD,
"Questions": ICON_QUESTIONS,
"Import Questions": ICON_IMPORT,
"AI Generator": ICON_AI,
"Exams": ICON_EXAMS,
"Reports": ICON_REPORTS,
"Settings": ICON_SETTINGS,
"Logout": ICON_LOGOUT,
}

View File

@@ -107,6 +107,7 @@ REQUIREMENTS:
4. Only ONE correct answer 4. Only ONE correct answer
5. Include a clear explanation of why the correct answer is correct 5. Include a clear explanation of why the correct answer is correct
6. Make the question noticeably {level_desc} - not just a minor variation 6. Make the question noticeably {level_desc} - not just a minor variation
7. Follow and preserve any HTML formatting (e.g., <p>, <br>, <b>) present in the basis question
OUTPUT FORMAT: OUTPUT FORMAT:
Return ONLY a valid JSON object with this exact structure (no markdown, no code blocks): Return ONLY a valid JSON object with this exact structure (no markdown, no code blocks):

37
docker-compose.dev.yml Normal file
View File

@@ -0,0 +1,37 @@
services:
postgres:
image: postgres:15-alpine
environment:
POSTGRES_USER: irt_user
POSTGRES_PASSWORD: dev_password
POSTGRES_DB: irt_bank_soal
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
user: "70:70" # postgres user
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
app:
build: .
ports:
- "8080:8000"
environment:
DATABASE_URL: postgresql+asyncpg://irt_user:dev_password@postgres:5432/irt_bank_soal
REDIS_URL: redis://redis:6379
depends_on:
- postgres
- redis
volumes:
- .:/app
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
volumes:
postgres_data:
redis_data:

View File

@@ -0,0 +1,626 @@
# Alur Aplikasi IRT-Powered Question Bank
Dokumen ini menjelaskan alur lengkap aplikasi dari input data hingga menghasilkan next-question berbasis IRT.
---
## 1. Arsitektur Sistem
### 1.1 Teknologi Stack
```
Framework: FastAPI >= 0.104.1
Database: PostgreSQL + SQLAlchemy 2.0 (async)
AI: OpenAI (OpenRouter API)
Admin Panel: FastAPI-Admin
Math: numpy, scipy
Excel: openpyxl, pandas
```
### 1.2 Entity Relationship
```mermaid
erDiagram
Website ||--o{ Tryout : "hosts"
Website ||--o{ User : "contains"
Website ||--o{ Session : "serves"
Website ||--o{ Item : "contains"
Tryout ||--o{ Item : "contains"
Tryout ||--o{ Session : "has"
Session ||--o{ UserAnswer : "contains"
Item ||--o{ Item : "has variants"
Item ||--o{ UserAnswer : "answered by"
AIGenerationRun ||--o{ Item : "generates"
```
---
## 2. Konsep Inti
### 2.1 Tryout (Exam)
**Tryout** merepresentasikan 1 ujian lengkap dengan konfigurasi:
| Field | Opsi | Default | Deskripsi |
|-------|------|---------|-----------|
| `scoring_mode` | `ctt`, `irt`, `hybrid` | `ctt` | Metode kalkulasi score |
| `selection_mode` | `fixed`, `adaptive`, `hybrid` | `fixed` | Strategi pemilihan soal |
| `normalization_mode` | `static`, `dynamic`, `hybrid` | `static` | Metode normalisasi |
### 2.2 Item (Soal)
**Item** merepresentasikan 1 soal dengan parameter:
| Field | Deskripsi |
|-------|-----------|
| `stem` | Teks pertanyaan |
| `options` | Pilihan jawaban (A/B/C/D/E) |
| `correct_answer` | Kunci jawaban |
| `slot` | Posisi nomor soal (1, 2, 3...) |
| `level` | Kategori kesulitan (mudah/sedang/sulit) |
| `parent_item_id` | ID soal original (jika ini variant) |
| `calibrated` | Status IRT calibration |
| `irt_b` | Item difficulty parameter |
| `irt_se` | Standard error |
| `ctt_p` | P-value (tingkat kesukaran CTT) |
| `ctt_bobot` | Bobot soal = 1 - p |
### 2.3 Session (Percobaan Siswa)
**Session** melacak aktivitas siswa:
| Field | Deskripsi |
|-------|-----------|
| `session_id` | Identifier unik |
| `wp_user_id` | ID user dari WordPress |
| `tryout_id` | Tryout yang diambil |
| `theta` | Kemampuan estimasi IRT |
| `theta_se` | Standard error theta |
| `NM` | Nilai Mentah (raw score) |
| `NN` | Nilai Nasional (normalized) |
| `is_completed` | Status selesai |
### 2.4 Website (Multi-Tenant)
Sistem mendukung multiple WordPress websites dari 1 backend:
- Isolasi data per website
- Auth via `X-Website-ID` header
- WordPress JWT tokens
---
## 3. Alur Input Data
### 3.1 Sumber Data Masuk
| Sumber | Format | Endpoint | Fungsi |
|--------|--------|----------|--------|
| Admin Import | Excel (.xlsx) | `POST /import/excel` | Bulk import dari file Excel |
| JSON Import | JSON | `tryout_json_import.py` | Import dari JSON (LMS external) |
| AI Generation | API Request | `POST /ai/generate` | Generate variant soal baru |
### 3.2 Flow Import JSON
```
┌─────────────────────────────────────────────────────────────┐
│ ADMIN: Import Tryout JSON │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. Upload JSON file │
│ └─> File berisi 1 tryout lengkap (misal: "TO 2024") │
│ └─> Terdiri dari N soal (slot 1, 2, 3, ...) │
│ │
│ 2. Parse JSON │
│ └─> Extract setiap soal → Item record │
│ └─> Generate unique item_id │
│ │
│ 3. Simpan ke Database │
│ └─> Item.calibrated = False (belum ada IRT params) │
│ └─> Item.ctt_p = NULL (belum ada response data) │
│ │
└─────────────────────────────────────────────────────────────┘
```
### 3.3 Flow AI Generate Variants
```
┌─────────────────────────────────────────────────────────────┐
│ ADMIN: Generate AI Variants │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. Pilih Item Original │
│ └─> Ambil 1 soal dari imported tryout │
│ │
│ 2. Request ke OpenRouter API │
│ └─> Kirim prompt dengan soal original │
│ └─> Minta generate variant dengan level berbeda │
│ │
│ 3. Simpan Variant │
│ └─> variant.item_id = unique_id │
│ └─> variant.parent_item_id = original.id │
│ └─> variant.slot = original.slot (nomor sama) │
│ │
│ 4. Result │
│ └─> Slot 1: 1 original + 1 variant = 2 soal │
│ └─> Slot 2: 1 original + 1 variant = 2 soal │
│ └─> Total: 2N soal (N slot × 2 variant) │
│ │
└─────────────────────────────────────────────────────────────┘
```
### 3.4 Contoh Struktur Data Setelah Import + Generate
```
Tryout: "TO-2024"
├── Slot 1
│ ├── Item #1 (original, calibrated=True, irt_b=0.5)
│ └── Item #2 (variant, calibrated=True, irt_b=-0.3)
├── Slot 2
│ ├── Item #3 (original, calibrated=True, irt_b=0.8)
│ └── Item #4 (variant, calibrated=True, irt_b=0.2)
└── ...
```
---
## 4. Pemrosesan Scoring
### 4.1 CTT (Classical Test Theory)
#### Step-by-Step Formula:
```python
# STEP 1: Tingkat Kesukaran (p-value)
p = Σ Benar / Total Peserta
# Contoh: 70 siswa menjawab benar dari 100 siswa → p = 0.70
# STEP 2: Bobot (Weight)
bobot = 1 - p
# Contoh: bobot = 1 - 0.70 = 0.30
# STEP 3: Total Benar per Siswa
total_benar = count(correct answers)
# STEP 4: Total Bobot Earned per Siswa
total_bobot_siswa = Σ bobot for each correct answer
# Contoh: Jawab benar 3 soal dengan bobot [0.3, 0.5, 0.2] = 1.0
# STEP 5: Nilai Mentah (Raw Score)
NM = (Total_Bobot_Siswa / Total_Bobot_Max) × 1000
# Contoh: NM = (1.0 / 2.5) × 1000 = 400
# STEP 6: Nilai Nasional (Normalized Score)
NN = 500 + 100 × ((NM - Rataan) / SB)
# Contoh: NN = 500 + 100 × ((400 - 450) / 80) = 437.5
```
#### Kategori Kesulitan (CTT Standard):
| p-value | Kategori | Arti |
|---------|----------|------|
| p < 0.30 | Sulit | Hanya <30% siswa menjawab benar |
| 0.30 ≤ p ≤ 0.70 | Sedang | 30-70% siswa menjawab benar |
| p > 0.70 | Mudah | >70% siswa menjawab benar |
### 4.2 IRT (Item Response Theory) - 1PL Rasch Model
#### Formula Inti:
```python
# Probability of correct response
P(θ, b) = 1 / (1 + exp(-(θ - b)))
# Di mana:
# - θ (theta) = kemampuan siswa [-3, +3]
# - b = difficulty soal [-3, +3]
# Contoh:
# - Siswa dengan θ = 0.5 menghadapi soal dengan b = 0.5
# - P(0.5, 0.5) = 1 / (1 + exp(0)) = 0.5 (50% kemungkinan benar)
```
#### Interpretasi Theta:
| Theta | Kemampuan | Persentase Benar (jika b=0) |
|-------|-----------|------------------------------|
| -3.0 | Sangat Lemah | ~5% |
| -1.5 | Lemah | ~18% |
| 0.0 | Rata-rata | ~50% |
| +1.5 | Cerdas | ~82% |
| +3.0 | Sangat Cerdas | ~95% |
#### Theta Estimation via MLE:
```python
# Log-likelihood
LL = Σ [u_i × log(P) + (1-u_i) × log(1-P)]
# u_i = 1 jika benar, 0 jika salah
# Theta estimation = maximize LL
θ_mle = argmax_θ LL(θ)
```
### 4.3 Kombinasi Scoring Mode
| Konfigurasi | Arti |
|-------------|------|
| `scoring_mode="ctt"` | Score akhir = NM, NN |
| `scoring_mode="irt"` | Score akhir = theta × 200 + 500 |
| `scoring_mode="hybrid"` | CTT score + IRT theta keduanya di-track |
---
## 5. IRT Calibration
### 5.1 Apa Itu Calibration?
**IRT Calibration** adalah proses mengestimasi parameter `b` (difficulty) untuk setiap soal berdasarkan response data dari siswa.
### 5.2 Kapan Item Became Calibrated?
```
┌─────────────────────────────────────────────────────────────┐
│ SYARAT ITEM CALIBRATED │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. Minimum Response Sample │
│ └─> Ada cukup response data (default: 100 siswa) │
│ │
│ 2. IRT b Parameter │
│ └─> Sudah diestimasi via MLE │
│ │
│ 3. IRT SE (Standard Error) │
│ └─> Sudah dihitung │
│ │
│ 4. Item.calibrated = True │
│ │
└─────────────────────────────────────────────────────────────┘
```
### 5.3 Flow IRT Calibration
```mermaid
flowchart TD
A[Collect Response Data] --> B{Have Min Sample?}
B -->|No| C[Wait for more students]
C --> A
B -->|Yes| D[For each Item]
D --> E[Build Response Matrix]
E --> F[Estimate b via MLE]
F --> G[Calculate Standard Error]
G --> H[Update Item.irt_b]
H --> I[Item.calibrated = True]
I --> D
D --> J[Calibration Complete]
```
### 5.4 Trigger Calibration
Calibration bisa dipicu via:
1. **API Endpoint:**
```
POST /tryout/{tryout_id}/calibrate
```
2. **Admin Panel:**
- Buka `/admin` → Tryouts → Pilih tryout → Trigger calibration
3. **Background Job (jika configured):**
- Setelah enough responses terkumpul
---
## 6. Item Selection Modes
### 6.1 Fixed Selection
**Fixed** = Soal disajikan berurutan berdasarkan slot.
```python
# Flow:
1. Siswa mulai session
2. Ambil item dengan slot=1 (urutan terendah)
3. Setelah dijawab, ambil slot=2
4. Lanjutkan sampai selesai
```
**Karakteristik:**
- Predictable, urutan soal tetap
- Tidak butuh IRT calibration
- Semua siswa dapat soal sama di posisi sama
### 6.2 Adaptive Selection (CAT)
**Adaptive** = Soal dipilih berdasarkan kemampuan siswa saat ini (theta).
```python
# Flow:
1. Siswa mulai session (θ = 0.0, default)
2. Pilih item dengan b ≈ θ
3. Siswa jawab → update θ
4. Pilih item baru dengan b ≈ θ baru
5. Ulangi sampai terminate condition
```
**Karakteristik:**
- Personalized, setiap siswa beda soal
- Butuh item calibrated
- Item selection pakai Fisher Information
#### Fisher Information Formula:
```python
# Information at current theta
I(θ) = P(θ) × (1 - P(θ))
# Di mana P(θ) = 1 / (1 + exp(-(θ - b)))
# Item dengan MAX information dipilih
# Maximum information = item paling informatif untuk theta saat ini
```
### 6.3 Hybrid Selection
**Hybrid** = Gabungan fixed + adaptive.
```python
# Flow:
1. Slot 1-N: Fixed selection (sequential)
2. Setelah slot N: Switch ke adaptive selection
3. Theta sudah ter-update dari fixed portion
4. Adaptive portion pakai theta untuk pilih soal
```
**Parameter:**
- `hybrid_transition_slot` = Slot dimana switch ke adaptive
### 6.4 Perbandingan Selection Modes
| Mode | Butuh Calibration | Personalisasi | Predictable |
|------|-------------------|---------------|-------------|
| Fixed | Tidak | Tidak | Ya |
| Adaptive | Ya | Ya | Tidak |
| Hybrid | Parsial | Parsial | Parsial |
---
## 7. Student Session Flow
### 7.1 Full Student Flow
```mermaid
sequenceDiagram
participant S as Student
participant API as FastAPI
participant DB as Database
S->>API: POST /session/ (start session)
API->>DB: Create session, θ=0.0
DB-->>API: session_id
API-->>S: session_id
loop For each question (adaptive/fixed/hybrid)
S->>API: GET /session/{id}/next-item
API->>DB: Query next item based on selection_mode
DB-->>API: Item data
API-->>S: Question
S->>API: POST /session/{id}/answer
API->>API: Update θ (if adaptive)
API->>DB: Save UserAnswer
DB-->>API: Saved
API-->>S: Ack + next question
end
S->>API: POST /session/{id}/complete
API->>API: Calculate NM, NN, final theta
API->>DB: Update session
DB-->>API: Updated
API-->>S: Final scores
```
### 7.2 Next-Item Selection Berdasarkan Mode
```
┌─────────────────────────────────────────────────────────────┐
│ SELECTION MODE = FIXED │
├─────────────────────────────────────────────────────────────┤
│ │
│ SELECT * FROM items │
│ WHERE tryout_id = ? │
│ AND item.id NOT IN (answered_items) │
│ ORDER BY slot ASC │
│ LIMIT 1 │
│ │
│ Result: Item dengan slot terkecil yang belum dijawab │
│ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ SELECTION MODE = ADAPTIVE │
├─────────────────────────────────────────────────────────────┤
│ │
│ current_theta = session.theta -- e.g., 0.5 │
│ │
│ SELECT * FROM items │
│ WHERE tryout_id = ? │
│ AND calibrated = TRUE │
│ AND item.id NOT IN (answered_items) │
│ ORDER BY ABS(irt_b - current_theta) ASC -- terdekat │
│ LIMIT 1 │
│ │
│ Result: Item dengan b ≈ θ │
│ │
└─────────────────────────────────────────────────────────────┘
```
---
## 8. Konfigurasi Tryout
### 8.1 Semua Opsi Konfigurasi
```python
# Scoring
scoring_mode = "ctt" # ctt, irt, hybrid
scoring_mode = "irt" #
scoring_mode = "hybrid" #
# Selection
selection_mode = "fixed" # Sequential
selection_mode = "adaptive" # CAT based on theta
selection_mode = "hybrid" # Fixed until transition slot
# Normalization
normalization_mode = "static" # Use static_rataan, static_sb
normalization_mode = "dynamic" # Calculate from participant data
normalization_mode = "hybrid" # Dynamic when min_sample reached
# IRT Settings
min_calibration_sample = 100 # Min responses for calibration
theta_estimation_method = "mle" # mle, map, eap
fallback_to_ctt_on_error = True # Fallback if IRT fails
# Hybrid Settings
hybrid_transition_slot = 10 # Switch to adaptive at slot 10
# AI Settings
ai_generation_enabled = True # Allow AI generated items
```
### 8.2 Cara Mengubah Konfigurasi
#### Via Database:
```sql
UPDATE tryouts
SET
scoring_mode = 'hybrid',
selection_mode = 'adaptive',
normalization_mode = 'dynamic'
WHERE tryout_id = 'your-tryout-id';
```
#### Via Admin Panel:
1. Buka `/admin`
2. Pilih menu **Tryouts**
3. Edit tryout yang diinginkan
4. Ubah field-field sesuai kebutuhan
5. Save
---
## 9. Ringkasan Alur End-to-End
### 9.1 Admin Flow (Sekali / Periodik)
```
┌─────────────────────────────────────────────────────────────┐
│ 1. IMPORT TRYOUT JSON │
│ Input: File JSON (1 tryout = 1 exam) │
│ Output: N items dalam database │
│ │
│ 2. AI GENERATE VARIANTS │
│ Input: Item original │
│ Output: Item variant (same slot, different content) │
│ Result: 2N items (N slot × 2 variant) │
│ │
│ 3. COLLECT RESPONSE DATA │
│ Input: Student answers │
│ Output: UserAnswer records │
│ │
│ 4. IRT CALIBRATION │
│ Input: Response data (min 100 students) │
│ Output: Item.irt_b, Item.irt_se, Item.calibrated=True │
│ │
│ 5. CONFIGURE TRYOUT │
│ Input: Set selection_mode = 'adaptive' │
│ Output: Tryout siap untuk adaptive testing │
│ │
└─────────────────────────────────────────────────────────────┘
```
### 9.2 Student Flow (Setiap Ujian)
```
┌─────────────────────────────────────────────────────────────┐
│ 1. START SESSION │
│ Input: tryout_id │
│ Output: session_id, theta=0.0 │
│ │
│ 2. ANSWER LOOP │
│ For each question: │
│ - Get next item (based on selection_mode) │
│ - Submit answer │
│ - If adaptive: update theta │
│ │
│ 3. COMPLETE SESSION │
│ Input: All answers │
│ Output: NM, NN, theta, completion status │
│ │
└─────────────────────────────────────────────────────────────┘
```
### 9.3 Konsep Kunci
| Konsep | Penjelasan |
|--------|------------|
| **Tryout** | 1 exam yang di-import dari JSON |
| **Item** | 1 soal (original atau variant) |
| **Slot** | Posisi nomor soal (1, 2, 3...) |
| **Variant** | Soal berbeda di slot yang sama |
| **Calibrated** | Item sudah punya irt_b (siap untuk adaptive) |
| **Theta** | Estimasi kemampuan siswa dalam IRT scale |
---
## 10. FAQ
### Q: Kenapa default scoring_mode = "ctt"?
A: CTT lebih simpel, tidak butuh IRT calibration. Cocok untuk awal sebelum cukup data.
### Q: Kenapa default selection_mode = "fixed"?
A: Fixed selection tidak butuh item calibrated. Bisa jalan langsung setelah import.
### Q: Bagaimana switch ke adaptive?
A:
1. Pastikan item sudah calibrated (`calibrated = True`)
2. Ubah `selection_mode = 'adaptive'` di tryout
3. Student baru akan dapat adaptive selection
### Q: Adaptive butuh berapa banyak data?
A: Default `min_calibration_sample = 100`. Artinya minimal 100 siswa harus sudah menjawab sebelum calibration bisa jalan.
### Q: CTT dan Fixed itu sama?
A: Tidak. Mereka orthogonal:
- **scoring_mode** = bagaimana menghitung score akhir
- **selection_mode** = bagaimana memilih soal berikutnya
### Q: Aplikasi ini membuat exam?
A: Tidak. Aplikasi ini adalah **question bank**. Exam sudah di-import dari JSON. Aplikasi "mengembangbiakkan" soal dengan membuat variants.
---
## 11. Referensi Code
| File | Fungsi |
|------|--------|
| `app/services/ctt_scoring.py` | CTT scoring calculations |
| `app/services/irt_calibration.py` | IRT calibration, theta estimation |
| `app/services/cat_selection.py` | Item selection (fixed/adaptive/hybrid) |
| `app/services/ai_generation.py` | OpenRouter AI integration |
| `app/services/excel_import.py` | Excel import/export |
| `app/routers/sessions.py` | Session management API |
| `app/models/tryout.py` | Tryout model definition |
| `app/models/item.py` | Item model definition |
| `app/models/session.py` | Session model definition |
---
*Document version: 1.0*
*Last updated: 2026-06-15*

0
error.html Normal file
View File

19
patch_css.py Normal file
View File

@@ -0,0 +1,19 @@
import re
with open("app/admin_web.py", "r") as f:
content = f.read()
# Fix activity feed CSS
content = content.replace(
".activity-feed li:last-child {{ border-bottom: none; }}",
".activity-feed li:last-child {{ border-bottom: none; }}\n .activity-feed li svg, .activity-feed li svg.nav-icon, .activity-feed li svg.huge-icon {{ width: 20px; height: 20px; flex-shrink: 0; }}"
)
# Fix alert CSS
content = content.replace(
".alert-warning {{ background: #fef3c7; border: 1px solid #f59e0b; color: #92400e; }}",
".alert svg, .alert svg.huge-icon, .alert svg.page-icon {{ width: 24px; height: 24px; flex-shrink: 0; margin-right: 4px; margin-bottom: -4px; }}\n .alert-warning {{ background: #fef3c7; border: 1px solid #f59e0b; color: #92400e; }}"
)
with open("app/admin_web.py", "w") as f:
f.write(content)

17
patch_icons.py Normal file
View File

@@ -0,0 +1,17 @@
import re
with open("app/admin_web_icons.py", "r") as f:
content = f.read()
new_mappings = """ "📈": ICON_TREND_UP,
"📉": ICON_TREND_DOWN,
"💡": ICON_LIGHTBULB,
"👋": '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" style="display:inline;width:28px;height:28px;margin-bottom:-4px;"><path stroke-linecap="round" stroke-linejoin="round" d="M15.042 21.672 13.684 16.6m0 0-2.51 2.225.569-9.47 5.227 7.917-3.286-.672ZM12 2.25V4.5m5.834.166-1.591 1.591M20.25 10.5H18M7.757 14.743l-1.59 1.59M6 10.5H3.75m4.007-4.243-1.59-1.59" /></svg>',
"📊": ICON_REPORTS,
"🚀": ICON_HUGE_ROCKET,
"📈": ICON_TREND_UP,"""
content = content.replace(' "📈": ICON_TREND_UP,\n "📉": ICON_TREND_DOWN,', new_mappings)
with open("app/admin_web_icons.py", "w") as f:
f.write(content)

View File

@@ -38,3 +38,6 @@ fastapi-admin>=1.0.0
# Utilities # Utilities
python-dotenv>=1.0.0 python-dotenv>=1.0.0
# Async support
greenlet>=2.0.0

66
run_local.sh Executable file
View File

@@ -0,0 +1,66 @@
#!/bin/bash
# Run local development server
set -e
echo "🚀 Starting IRT Bank Soal Local Dev Server"
echo "=========================================="
# Check if Docker is available
if ! command -v docker &> /dev/null; then
echo "❌ Docker not found. Please install Docker first."
exit 1
fi
# Check if docker-compose is available
if ! command -v docker-compose &> /dev/null && ! docker compose version &> /dev/null; then
echo "❌ Docker Compose not found. Please install Docker Compose first."
exit 1
fi
# Use docker compose command (Docker Desktop includes it as a plugin)
DOCKER_COMPOSE="docker compose"
# Start databases
echo "📦 Starting PostgreSQL and Redis..."
$DOCKER_COMPOSE -f docker-compose.dev.yml up -d postgres redis
# Wait for PostgreSQL to be ready
echo "⏳ Waiting for PostgreSQL..."
for i in {1..60}; do
if docker exec yellow-bank-soal-postgres-1 pg_isready -U irt_user -d irt_bank_soal &> /dev/null 2>&1; then
echo "✅ PostgreSQL is ready!"
break
fi
if [ $i -eq 60 ]; then
echo "❌ PostgreSQL failed to start"
docker logs yellow-bank-soal-postgres-1
exit 1
fi
sleep 1
done
# Check if venv exists, create if not
if [ ! -d "venv" ]; then
echo "📦 Creating Python virtual environment..."
python3 -m venv venv
fi
# Activate venv and install dependencies
echo "📦 Installing dependencies..."
source venv/bin/activate
pip install -r requirements.txt -q
# Run migrations
echo "🔄 Running database migrations..."
alembic upgrade head
# Start the dev server
echo ""
echo "🎉 Starting FastAPI dev server..."
echo " Admin UI: http://localhost:8000/admin"
echo " API Docs: http://localhost:8000/docs"
echo " Login: admin / admin123"
echo ""
echo "=========================================="
exec uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload

8
test_error.py Normal file
View File

@@ -0,0 +1,8 @@
import asyncio
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
response = client.get("/admin/hierarchy")
print(response.status_code)
print(response.text)

9
test_fetch.py Normal file
View File

@@ -0,0 +1,9 @@
import asyncio
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
client.post("/admin/login", data={"username": "admin", "password": "password"})
response = client.get("/admin/hierarchy")
print(response.status_code)
print(response.text)