Phase 6: take a test (weighted sampling + question flow)

- internal/sampling: ComputeWeight (Laplace-smoothed error rate + recency
  multiplier, floor 0.15) and SelectWeighted (A-Res reservoir algorithm).
  10k-run statistical test verifies weak questions appear >3x more often
  than mastered, and mastered questions still appear (floor exercised).
- GET/POST /test/new: source filter with live available-count JS update,
  n-questions input, weighted vs uniform mode radio.
- GET /test/{id}/q/{n}: deterministic answer shuffle per (test_id,
  question_id), progress bar, mobile-friendly large tap targets.
- POST /test/{id}/q/{n}: records answer + upserts stat; advances to next
  question or finishes test and redirects to results stub.
- GET /test/{id}/results: stub (Phase 7 will add full review).
- Ownership enforced: all test routes 404 for wrong user.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jānis Kacēns
2026-05-11 13:56:44 +03:00
parent 177b4e8fd8
commit 968479ff51
8 changed files with 628 additions and 0 deletions
+41
View File
@@ -0,0 +1,41 @@
{{define "content"}}
<div class="mb-5">
<div class="flex items-center justify-between mb-1">
<span class="text-xs font-medium text-gray-500">Question {{.N}} of {{.Total}}</span>
<span class="text-xs text-gray-400">{{.ProgressPct}}% done</span>
</div>
<div class="h-2 bg-gray-200 rounded-full overflow-hidden">
<div class="h-full bg-blue-500 rounded-full transition-all"
style="width: {{.ProgressPct}}%"></div>
</div>
</div>
<div class="bg-white border border-gray-200 rounded-lg p-5 mb-5 shadow-sm">
<p class="text-gray-800 leading-relaxed text-base">{{.Question.Text}}</p>
{{if .Question.Source}}
<p class="text-xs text-gray-400 mt-3">{{.Question.Source}}</p>
{{end}}
</div>
<form method="post" action="/test/{{.TestID}}/q/{{.N}}">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<div class="space-y-3 mb-6">
{{range .Answers}}
<label class="flex items-start gap-4 p-4 bg-white border-2 border-gray-200 rounded-xl
cursor-pointer hover:border-blue-400 hover:bg-blue-50 transition-all
has-[:checked]:border-blue-500 has-[:checked]:bg-blue-50">
<input type="radio" name="answer_id" value="{{.ID}}"
class="mt-0.5 text-blue-600 focus:ring-blue-500 flex-shrink-0 w-4 h-4">
<span class="text-sm text-gray-800 leading-relaxed">{{.Text}}</span>
</label>
{{end}}
</div>
<button type="submit"
class="w-full bg-blue-600 hover:bg-blue-700 text-white py-3 px-4 rounded-xl
text-sm font-semibold shadow-sm">
{{if lt .N .Total}}Next question{{else}}Finish test{{end}}
</button>
</form>
{{end}}