An AI-powered competitive programming IDE that teaches algorithmic thinking — not by handing over answers, but by walking you through your own code.
Most AI coding tools fail learners the same way: paste your broken code, get back a clean solution. You copy it, it passes, you learned nothing. Repeat indefinitely.
Competitive programming is especially vulnerable to this. The value isn't in the answer — it's in discovering why your approach breaks on a specific edge case, why your O(n²) loop times out at n = 10⁵, and which algorithmic technique unlocks the problem. A tool that just solves it robs you of exactly that.
A three-panel IDE where the AI adapts to your code and your approach — it never replaces them.
The tutor classifies your submission into one of four buckets before saying anything:
| Bucket | Response |
|---|---|
| Correct + optimal | Confirms it. One optional polish note. |
| Correct but too slow | Points at the bottleneck with exact complexity math. Nudges the optimization. |
| Right approach, buggy | Finds the bug in your code. Never swaps the approach. |
| Approach can't work | Names the constraint or edge case that kills it, then the technique. No code. |
Two hard rules baked into the system prompt: the AI may only call something wrong if it can produce a concrete failing input — otherwise it hedges. And it never outputs code, ever.
User loads problem
└─> problem + hints + tags from Supabase (zero AI)
User reveals Hint 1/2/3
└─> stored hint from DB (zero AI, anonymous allowed)
Tutor chat panel
├─ most turns: choice chips → stored hint (zero AI)
├─ Quick Review (Tier 1, Flash-Lite): bucket classification, obvious bugs
└─ Deep Analysis (Tier 2, 3.5 Flash): multi-turn reasoning, counterexamples
└─> T2 exhausted → auto-fall to T1
└─> T1 exhausted → Zero AI mode (stored hints only)
Model IDs in use (app/api/review/route.ts):
- Quick Review (Tier 1):
gemini-3.1-flash-lite-preview - Deep Analysis (Tier 2):
gemini-3.5-flash
Why two AI tiers? A weaker model misclassifying a correct solution as wrong destroys user trust. For reasoning-heavy turns where trust matters, quality is non-negotiable. For "any obvious bugs?" — speed and availability win. The split optimises both.
The fallback chain is automatic. Users always see why their tier changed and how many calls remain. This turns a real API quota constraint into honest, visible UX.
- Three-panel layout — problem statement, Monaco editor (C++/Python), tutor chat
- Progressive hint system — three curated hints per problem, stored in DB, revealed on demand
- Adaptive AI feedback — classify-then-respond, never rewrite; counterexample-or-hedge rule
- Quota management — per-user daily caps (20 Quick / 5 Deep), global project backstops, live badge, automatic fallback chain
- Performance dashboard — 0–100 score (rating-weighted, hint/AI-penalized, rolling 30 days), streak calendar, strong/weak topic analysis, difficulty histogram
- Unseen problem paste — paste a Codeforces or AtCoder problem (or whole-contest) URL; the app parses it via a per-source adapter, generates hints live, and caches embeddings. Live-contest problems are blocked with an honest "no external help during contests" message; finished contest URLs expand to a pickable problem list. (LeetCode/CodeChef are a future adapter — see below.)
- Similar problems — pgvector cosine similarity with tag/difficulty-weighted ranking
- Per-message model selector — pick Quick or Deep per message; Deep auto-disables when its quota is spent
- Save & restore code — download your solution (File System Access API, with a Downloads fallback); a per-session
sessionStoragecache restores your code across reloads and in-app navigation - Manual stopwatch — a display-only practice timer in the editor toolbar (heartbeat stays the source of truth for tracked time)
- Dark / light theme — system-aware toggle
- Auth tiers — anonymous: browse + editor + all stored hints; signed-in: all AI features + profile
| Layer | Choice | Why |
|---|---|---|
| Framework | Next.js 16.2 (App Router, Turbopack) | Server Components + streaming |
| Database | Supabase (Postgres + pgvector) | Free tier, RLS, vector search |
| AI | Google Gemini API (@google/generative-ai) |
Free tier: Flash-Lite 500 RPD, 3.5 Flash 20 RPD |
| Quota store | Upstash Redis | Serverless-friendly, TTL-per-midnight |
| Editor | Monaco via @monaco-editor/react |
Full IDE experience in-browser |
| Styling | Tailwind v4 | No config file, @plugin directives |
| Auth | @supabase/ssr |
Google OAuth + email magic links |
| Parsing | cheerio |
Server-side HTML parse of Codeforces & AtCoder problem pages (per-source adapters in lib/parsers/) |
| Math rendering | katex + remark-math / rehype-katex |
LaTeX in problem statements |
| Charts | recharts |
Profile dashboard visualisations |
2000+ curated problems from open-r1/codeforces (ODC-By 4.0), filtered to ratings 1000–2500 across core algorithm tags. Each problem has three progressive hints generated offline via Flash-Lite (thinking_budget=0) and stored as JSONB — no AI cost at read time.
Dataset attribution: Problems sourced from open-r1/codeforces (ODC-By 4.0).
The free Gemini API tier is per-project, shared across all users. Every architectural decision flows from this constraint. The numbers below live in lib/quota.ts.
Per-user daily caps (Redis, expires at UTC midnight):
Quick Review — 20 calls/user (Flash-Lite, 500 RPD project-wide)
Deep Analysis — 5 calls/user (3.5 Flash, 20 RPD project-wide)
Global project backstops:
Quick Review — 480/day (20 under the 500 RPD hard cap)
Deep Analysis — 18/day (2 under the 20 RPD hard cap)
Fallback order: Deep → Quick → Zero AI (stored hints + chips only)
Response cache: SHA-256(problem_id + normalized_code + intent + tier), TTL 24h
Cache hits return instantly and don't increment any counter. Identical code sent twice costs nothing.
The codebase went through dedicated security audit passes. Highlights:
- Service-role / anon-key boundary (enforced). The RLS-bypassing service-role client is confined to trusted server-only paths (
/api/review,/api/progress/*,/api/unseen/parse, profile queries) and is never imported into a page component. Every user-facing read goes through the RLS-respecting anon SSR client (@supabase/ssr). - Auth-gated paid endpoints. Every Gemini / embedding call sits behind
requireUser()plus a quota check; over-quota returns429before any external request fires. - Open-redirect-safe auth. The OAuth callback
?next=param accepts only same-site relative paths —//evil.comand/\evil.comfall back to/. - Cache-poisoning-safe AI cache. The Redis response-cache key folds in a fingerprint of the prompt-determining fields, so a forged problem statement can't poison the cached answer served to other users on a real problem id.
- Security headers.
X-Frame-Options: DENY,X-Content-Type-Options: nosniff,Referrer-Policy: strict-origin-when-cross-origin, and a restrictivePermissions-Policyon every route (next.config.ts). - XSS-safe rendering. All problem and AI markdown renders through
react-markdowndefaults — norehype-raw, nodangerouslySetInnerHTMLon user/AI content; KaTeX runs withtrust: false. - SSRF-safe parsing. The unseen-problem parser restricts user-supplied URLs to an allowlist of judge hosts (
codeforces.com,atcoder.jp) and specific path shapes; any other host or non-http(s) scheme is rejected before any fetch. Live-status timing reads hit only fixed, server-controlled API URLs. - Boot-time env validation.
lib/env.tsfails fast with a named error at module load if a required secret is missing, rather than failing cryptically mid-request. - No committed secrets.
.env*is git-ignored; the service-role key is server-only and never exposed to the browser.
This project runs on a $0 budget. Some decisions are worth documenting:
- 3.5 Flash = 20 RPD project-wide. At 5 deep analyses per user that's ~4 users before the project ceiling is hit. Fine for a prototype; the fallback chain means the app never breaks, it just steps down.
- No code execution (deferred). Piston public API became auth-gated in Feb 2026 and won't issue keys to portfolio projects. Judge0 via RapidAPI is $0.0017/submission — rejected. Options: self-host Piston on Fly.io, Pyodide in-browser (Python only), or wait for a free provider. The
app/api/execute/route.ts+RunPanel.tsxscaffolding is in place for when one lands. - Non-streaming JSON → SSE streaming. The simpler approach shipped first;
ReadableStream+TransformStreamadded once the core was stable. - Embeddings. The
tagscolumn satisfies "frame the nudge in the right paradigm" at zero cost. Embeddings earn their keep for the unseen-problem paste and similar-problem recommender — not for per-problem feedback. - LeetCode / CodeChef parsers (deferred). The parser is built around per-source adapters (
lib/parsers/), so new judges are drop-in. Codeforces and AtCoder ship today because both serve problems as static, server-rendered HTML over stdin/stdout. LeetCode and CodeChef are deferred: both are JS SPAs served from Cloudflare-protected GraphQL/JSON APIs that reject datacenter/Vercel IPs (so a server-sidefetchis unreliable), and LeetCode's function-signature I/O doesn't match this stdin/stdout editor. They land once a reliable fetch path is chosen.
git clone https://github.com/your-username/ai-tutor
cd ai-tutor
npm installCreate .env.local in the project root and fill in:
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
SUPABASE_SERVICE_ROLE_KEY=
GEMINI_API_KEY=
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=Set up the database, then seed:
# Provision the schema in your Supabase project (Postgres + pgvector):
# - enable the `vector` extension
# - create the problems / hints / progress / embeddings tables and RLS policies
# Schema is managed in the Supabase dashboard (not checked into this repo).
# Then seed 300 problems (requires GEMINI_API_KEY, ~40 min, idempotent)
python scripts/seed.py --limit 300
# Optional: generate 768-d embeddings for the remaining problems
python scripts/embed.py
# Optional: patch sample I/O on seeded problems
python scripts/patch_sample_io.pynpm run devDeployed on Vercel (Hobby). The six env vars above are set in the Vercel project dashboard (Production scope); lib/env.ts validates them at cold start and fails fast with a named error if one is missing.
Supabase keep-alive. Supabase Free pauses a project after ~7 days of inactivity. app/api/health/route.ts runs a one-row DB read and a Vercel cron (vercel.json) pings it every ~3 days (0 12 */3 * *), keeping the database warm. The route is force-dynamic so the cron always hits the DB instead of a cached response.
app/
page.tsx # Homepage — problem list + dashboard banner
layout.tsx # Root layout
globals.css # Tailwind v4 entry
problems/
seen/[id]/ # Seeded problem view (page.tsx + ProblemView.tsx)
unseen/[id]/ # Parsed unseen problem view (page.tsx + UnseenProblemView.tsx)
profile/page.tsx # Performance dashboard
auth/page.tsx # Sign-in card
auth/error/page.tsx # Auth error page
auth/callback/route.ts # OAuth / magic-link callback (open-redirect-safe)
api/
review/route.ts # AI feedback dispatcher (SSE, quota, cache)
quota/route.ts # Badge state on mount
progress/ # open / hint-revealed / mark-solved / heartbeat
unseen/parse/route.ts # URL → adapter parse → hints → embed → DB (CF/AtCoder, live-contest block)
similar/route.ts # pgvector similarity search
execute/route.ts # Code execution (deferred — no provider)
health/route.ts # DB-ping keep-alive (Vercel cron)
components/ # Flat — no subfolders
TutorChat.tsx # Chat panel — chips, stored hints, SSE consumer
CodeEditor.tsx # Monaco editor
RunPanel.tsx # Sample I/O runner (deferred)
ProblemStatement.tsx # KaTeX-rendered statement
HintPanel.tsx # Progressive hint reveal
SimilarProblems.tsx # Similarity widget
Stopwatch.tsx # Display-only timer
FilterForm.tsx # Problem-list filters
UnseenProblemInput.tsx # Codeforces URL paste box
AuthButton.tsx / Footer.tsx # Shell chrome
ThemeToggle.tsx # Dark / light theme switch
StreakCalender.tsx # Profile: streak calendar
TopicStrength.tsx # Profile: strong/weak topics
DifficultyHistogram.tsx # Profile: difficulty distribution
RecentActivity.tsx # Profile: recent solves
AIUsageToday.tsx # Profile: live quota usage
lib/
quota.ts # Per-user + global counters, fallback chain
prompt.ts # buildSystemPrompt, postProcessResponse, sanitizeGeminiHistory
scoring.ts # 0–100 performance score formula
profile-queries.ts # Parallel profile data fetching
progress.ts # Progress read/write helpers
topic-categories.ts # Tag → topic-bucket mapping
env.ts # Boot-time env validation
downloadCode.ts # Save-to-file (File System Access API + fallback)
useSessionCode.ts # Per-session sessionStorage code cache
types.ts # Shared types
parsers/ # Multi-source problem parser
index.ts # resolveAdapter() + SSRF host allowlist
types.ts # SourceAdapter / ParsedProblem / UrlMatch
http.ts # guarded fetch + cached live-status JSON
codeforces.ts # CF adapter (HTML parse + contest.list live check)
atcoder.ts # AtCoder adapter (HTML parse + kenkoooo live check)
supabase.ts / supabase-server.ts / supabase-browser.ts # Supabase clients
scripts/
seed.py # Offline hint generation (Flash-Lite, thinking_budget=0)
embed.py # 768-d embeddings via gemini-embedding-001
patch_sample_io.py # Populate sample_io from HF dataset cache
docs/ # Build plans / design notes
base = difficulty_rating / 3250
raw_contribution = base − 0.04·hints − (0.015·quick_calls + 0.05·deep_calls)
problem_points = raw_contribution × 16
overall_score = clamp( Σ problem_points [last 30 days], 0, 100 )
Score can decrease. Heavy hint and AI use make raw_contribution negative. A clean solve of a 2400-rated problem contributes ~11.8 points; the same solve with all 3 hints and 3 deep-analysis calls contributes ~0.6. ~10 clean medium solves approach 100.
MIT. Problems are sourced from open-r1/codeforces under ODC-By 4.0 — attribution required.