Skip to content

Repository files navigation

Drilly — Python Mini‑Drills (Next.js 14 + Pyodide)

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

Contents

  • 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

Overview

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.

Features

  • 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/generate uses OpenAI Responses API with JSON schema to return valid drills

Architecture

  • 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

Execution flow

  1. Compose Python run_tests() from Test[] on the client
  2. Web Worker loads Pyodide once (lazy); replaces __import__/open() with guards
  3. User code + test runner execute; stdout/stderr captured
  4. Results are returned { passed, results[], stdout, stderr, timeMs }
  5. If all pass, an Attempt is saved; user may pin their solution

Data model (Prisma)

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.

Test model (TypeScript)

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.

Security model

  • Execution in isolated Web Worker + Pyodide
  • Guarded __import__ bans dangerous modules (subprocess, os, pathlib, requests, socket, urllib, shutil) and replaces open()
  • Forbidden pattern scan (regex) before execution per drill
  • Timeout enforcement with worker termination + respawn
  • Output truncation to avoid oversized messages

API surface

  • GET /api/drills?day=&topic=&q= — list drills
  • GET /api/drills/[slug] — drill detail
  • POST /api/drills — create a drill (validated by Zod)
  • POST /api/generate — OpenAI Structured Outputs → drills[] preview (unpersisted)
  • POST /api/attempts — persist attempt
  • POST /api/hint — return a short hint for prompt+code
  • GET /api/pinned?drillId= — list pinned solutions
  • POST /api/pinned — { drillId, code } → create pin
  • DELETE /api/pinned/[id] — remove a pin
  • GET /api/export — export all drills/attempts
  • POST /api/import — import drills/attempts

OpenAI integration

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.

Getting started

  1. Install
pnpm i
  1. Environment

Create .env with:

DATABASE_URL="file:./dev.db"
OPENAI_API_KEY="sk-..." # required for /api/generate and /api/hint
  1. DB + seed
pnpm prisma db push
pnpm tsx lib/seedWeek1to7.ts
  1. Run
pnpm dev

Open http://localhost:3000/

Seeding & demo

lib/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.

Running & usage tips

  • 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.

Timer mode

Go to /timer. Choose minutes with the slider/number input (5–60 by default; supports up to 180) and start a 5‑drill session.

Hints & pinned solutions

  • 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

Deployment notes

  • Works on Vercel. Ensure OPENAI_API_KEY and DATABASE_URL are set.
  • For Postgres, update datasource db in Prisma and run migrations.

Troubleshooting

  • 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_json or fallback to text; ensure your SDK version supports Responses API.

Roadmap

  • 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).

About

Python mini‑drills trainer for leetcode (made while procrastinating leetcode myself)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages