A fast, clean Python mini‑drills trainer with a built‑in editor, Pyodide test runner, progress/streaks, and an OpenAI‑powered drill generator.
Repository: J0YY/drilly
- Overview
- Features
- Architecture
- Data model
- Test model
- Security model
- API surface
- OpenAI integration (Structured Outputs)
- Getting started
- Seeding and demo data
- Running and usage tips
- Timer mode
- Hints and pinned solutions
- Deployment notes
- Troubleshooting
- Roadmap
Drilly focuses on short Python drills (not full LeetCode problems). Each drill includes starter code, a reference solution, visible and hidden tests, forbidden patterns (e.g., no slicing), and a per‑run timeout. Users solve in a Monaco editor and run locally in the browser via Pyodide (WebAssembly Python) inside a Web Worker.
- Monaco editor with Python syntax, Cmd/Ctrl‑Enter to run
- Safe in‑browser execution via Pyodide Worker (no server RCE)
- Visible vs hidden tests with structured pass/fail output
- Forbidden pattern scan before execution (regex)
- Per‑run time and stdout/stderr capture
- Reveal Solution with side‑by‑side diff (normalized whitespace)
- Attempts persisted with pass/fail and time
- Home page progress: green highlights for drills solved in last 24h; progress bars for 24h completion and daily goal; streak progress toward 5‑day goal
- Hint button (short, code‑free hints via OpenAI; 30s cooldown)
- Pinned solutions: save your own passing code variants per drill
- Timer Mode: pick 5 drills and a countdown (adjustable minutes)
- Bulk generator:
/api/generateuses OpenAI Responses API with JSON schema to return valid drills
- Frontend: Next.js 14 App Router + TypeScript + Tailwind
- Editor: Monaco (
monaco-editor), dynamically imported client‑side - Runner: Pyodide v0.25 inside a Web Worker (
workers/pyodide.worker.ts) - DB: Prisma + SQLite (replaceable with Postgres later)
- APIs: Next Route Handlers in
app/api/* - Schema validation: Zod; JSON Schema for OpenAI Structured Outputs
- Compose Python
run_tests()from Test[] on the client - Web Worker loads Pyodide once (lazy); replaces
__import__/open()with guards - User code + test runner execute; stdout/stderr captured
- Results are returned
{ passed, results[], stdout, stderr, timeMs } - If all pass, an Attempt is saved; user may pin their solution
See prisma/schema.prisma.
model Drill {
id String @id @default(cuid())
slug String @unique
title String
day Int
topic String
type String // "function" | "script" | "class"
prompt String
starterCode String
solutionCode String
visibleTests String // JSON stringified
hiddenTests String // JSON stringified
forbiddenPatterns String @default("[]")
timeoutMs Int @default(2000)
createdAt DateTime @default(now())
Attempt Attempt[]
PinnedSolution PinnedSolution[]
}
model Attempt {
id String @id @default(cuid())
drillId String
code String
passed Boolean
statsMs Int
createdAt DateTime @default(now())
Drill Drill @relation(fields: [drillId], references: [id], onDelete: Cascade)
}
model PinnedSolution {
id String @id @default(cuid())
drillId String
code String
createdAt DateTime @default(now())
Drill Drill @relation(fields: [drillId], references: [id], onDelete: Cascade)
}
SQLite lacks JSON and string[] columns, so JSON fields are stringified.
type Test =
| { kind: "assert"; code: string; message?: string }
| { kind: "io"; input?: any; expected?: any; func?: string };
The test composer serializes values to valid Python literals, including True/False and None.
- Execution in isolated Web Worker + Pyodide
- Guarded
__import__bans dangerous modules (subprocess,os,pathlib,requests,socket,urllib,shutil) and replacesopen() - Forbidden pattern scan (regex) before execution per drill
- Timeout enforcement with worker termination + respawn
- Output truncation to avoid oversized messages
GET /api/drills?day=&topic=&q=— list drillsGET /api/drills/[slug]— drill detailPOST /api/drills— create a drill (validated by Zod)POST /api/generate— OpenAI Structured Outputs → drills[] preview (unpersisted)POST /api/attempts— persist attemptPOST /api/hint— return a short hint for prompt+codeGET /api/pinned?drillId=— list pinned solutionsPOST /api/pinned— { drillId, code } → create pinDELETE /api/pinned/[id]— remove a pinGET /api/export— export all drills/attemptsPOST /api/import— import drills/attempts
Located in app/api/generate/route.ts and lib/openai.ts. Uses the Responses API with response_format: { type: "json_schema" } and a strict drillArrayJsonSchema so outputs are guaranteed valid.
- Install
pnpm i- Environment
Create .env with:
DATABASE_URL="file:./dev.db"
OPENAI_API_KEY="sk-..." # required for /api/generate and /api/hint- DB + seed
pnpm prisma db push
pnpm tsx lib/seedWeek1to7.ts- Run
pnpm devlib/seedWeek1to7.ts seeds representative drills for Days 1–7 (strings/lists, dicts/sets, comps/functional, DS/algos, recursion/DP, parsing, mixed). Includes the “no slicing” reverse drill with forbidden pattern and hidden tests.
- Cmd/Ctrl‑Enter runs tests.
- Reveal Solution appears after your first run.
- Hidden tests display pass/fail only. After revealing, the diff normalizes whitespace/line endings and shows an “Identical to solution ✅” state when identical.
- Your editor text persists to localStorage per drill.
Go to /timer. Choose minutes with the slider/number input (5–60 by default; supports up to 180) and start a 5‑drill session.
- Click Hint for a short, code‑free nudge (30s cooldown)
- After a passing run, click Pin Solution to save your own variant; manage pins below the results
- Works on Vercel. Ensure
OPENAI_API_KEYandDATABASE_URLare set. - For Postgres, update
datasource dbin Prisma and run migrations.
- Worker or monaco issues in SSR: both are dynamically imported or initialized client‑side.
- Schema changed but dev server crashed: restart dev server so Prisma Client regenerates; in dev we also guard against stale client for new models.
- OpenAI Structured Outputs parsing: we access
.output_jsonor fallback to text; ensure your SDK version supports Responses API.
- Topic filters and “Unsolved only” chip on home
- Animated confetti on full pass; framer‑motion transitions
- User accounts and cloud sync of pins/attempts
- Per‑topic progress rings, achievements, and weekly packs
License: MIT (add a LICENSE if you need an explicit file).