Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 24 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
<div align="center">

[![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue?logo=python&logoColor=white)](https://www.python.org/downloads/)
[![Next.js](https://img.shields.io/badge/next.js-16-000000?logo=nextdotjs&logoColor=white)](https://nextjs.org/)
[![PostgreSQL](https://img.shields.io/badge/postgres-16-336791?logo=postgresql&logoColor=white)](https://www.postgresql.org/)
[![Powered by GPT-4o](https://img.shields.io/badge/powered%20by-GPT--4o-412991?logo=openai&logoColor=white)](https://platform.openai.com/)
[![AI: Claude / OpenAI / OpenRouter](https://img.shields.io/badge/AI-Claude%20%2F%20OpenAI%20%2F%20OpenRouter-ffb000)]()
[![Self-hostable](https://img.shields.io/badge/self--hostable-brightgreen?logo=docker&logoColor=white)]()
[![Voice support](https://img.shields.io/badge/voice-WebRTC-blueviolet?logo=googlemeet&logoColor=white)]()

</div>

Expand All @@ -19,7 +19,7 @@ Most interview platforms are puzzle grinders. O(1) Prep puts you in a real inter
- **A back-and-forth, not a quiz** - the interviewer follows up, pushes back on your reasoning, and adds constraints mid-session just like a real one would
- **Feedback that actually tells you something** - you get written scores and specific critique on your approach, code quality, communication, and tradeoffs - not just pass/fail
- **Your own account** - sign in and your sessions, history, and code are saved to your account in the app's database; self-host it and the data stays on infrastructure you control
- **Practice out loud** - voice mode lets you talk through your solution the way you would in an actual interview
- **Bring your own engine** - runs on your local Claude Code CLI by default (no API key), or point it at OpenAI or OpenRouter (one key, hundreds of models)
- **150+ problems grounded in real engineering** - each one has a real-world scenario, not just "given an array..."

---
Expand All @@ -38,12 +38,14 @@ python3 -m venv venv && source venv/bin/activate
pip install -r requirements.txt
```

Copy `backend/.env.example` to `backend/.env` and set your values (database URL, a secret key, and your OpenAI key):
Copy `backend/.env.example` to `backend/.env` and set your values. By default the AI engine is your local **Claude Code CLI** — no API key, just run `claude` once to sign in. Set `AI_PROVIDER` to `openai` or `openrouter` to use those instead (see `.env.example` for their keys).

```
DATABASE_URL=postgresql+psycopg://o1prep:o1prep@localhost:5433/o1prep
SECRET_KEY=change-me
OPENAI_API_KEY=sk-your-key-here
AI_PROVIDER=claude # claude (default) | openai | openrouter
# OPENAI_API_KEY=sk-... # only when AI_PROVIDER=openai
# OPENROUTER_API_KEY=sk-or-... # only when AI_PROVIDER=openrouter
```

Create the schema, load the problems, and run — all from `backend/`:
Expand All @@ -64,20 +66,20 @@ API. In a second terminal:

```bash
cd frontend
npm install
npm run dev # http://localhost:3000
pnpm install
pnpm dev # http://localhost:3000
```

Open **http://localhost:3000**. The dev server proxies `/api/*` to Flask on
`127.0.0.1:5000` (see `frontend/next.config.ts`), so the session cookie stays
same-origin and auth works with no CORS setup. Point it elsewhere with
`API_ORIGIN=http://host:port npm run dev`.
`API_ORIGIN=http://host:port pnpm dev`.

> On macOS, the AirPlay Receiver squats on port 5000 over IPv6 — the proxy
> targets `127.0.0.1` (IPv4) specifically to avoid it. If you change the Flask
> port, update `API_ORIGIN`.

> **Prerequisites:** Python 3.11+, Node.js 20+, Docker (for PostgreSQL), a paid OpenAI API key, and a modern browser (Chrome/Firefox/Safari).
> **Prerequisites:** Python 3.11+, Node.js 20+ and [pnpm](https://pnpm.io), Docker (for PostgreSQL), an AI engine (the Claude Code CLI needs no key; OpenAI/OpenRouter need an API key), and a modern browser (Chrome/Firefox/Safari).

---

Expand Down Expand Up @@ -138,7 +140,7 @@ Read a problem fully and chat with an AI tutor before you practice - useful when

![Study mode with problem details and research chat](docs/screenshots/StudyMode.png)

Two resizable panels side by side. The left panel has the full problem - scenario, constraints, examples, key skills, and follow-up challenges. The right panel is a tutor chat where you can ask anything about the problem without being handed the solution.
Two panels side by side — drag the divider to resize the tutor chat, and its width is remembered. The left panel has the full problem - scenario, constraints, examples, key skills, and follow-up challenges. The right panel is a tutor chat where you can ask anything about the problem without being handed the solution.

Good things to ask the tutor:

Expand All @@ -154,7 +156,7 @@ When you're ready, go back and hit **Practice** to start the interview.

![Interview mode with chat, code editor, test results, and tutor sidebar](docs/screenshots/PracticeAndEvaluateResult.png)

Three panels: the interviewer chat on the left, your code editor in the center, and an optional tutor sidebar on the right. All dividers are draggable.
Three panels: the interviewer chat on the left, your code editor in the center, and an optional tutor sidebar on the right. On a narrow screen, tabs switch between the chat and the editor.

The interview runs like a real one - you'll clarify the problem, talk through your approach, then implement. The interviewer watches what you submit and responds to it. You can submit code multiple times; each submission gets reviewed.

Expand All @@ -178,27 +180,15 @@ The interviewer gives a structured debrief with a hire/no-hire rating, scores ac

## Voice Interviews

Practice talking through your solution the way you actually would in an interview.
> **Status: temporarily unavailable in the web client.** Voice mode is being reworked to route through a natural-sounding neural TTS provider — the browser's built-in speech synthesis sounded too robotic for a realistic interview. The pipeline (browser speech-to-text → your chosen LLM → text-to-speech) is in place behind the scenes and will return once a good TTS voice is wired in.

![Voice interview mode with live transcript banner](docs/screenshots/VoiceModeToEmulateRealInterviews.png)

Switch to **Voice** mode in the toggle before starting. Once the interview begins, allow microphone access and wait for the connection. Your speech is transcribed live, the interviewer responds through your speakers, and everything is saved to your history just like a text session.

You can still write and submit code while in voice mode - just click **Submit Code** as normal.

### Troubleshooting

**Mic access denied** - Go to browser site settings for `localhost`, allow microphone, then reload.

**No audio from interviewer** - Check system audio output. Audio routes through the browser.

**Connection drops** - End the session and start a new one. Your previous session is preserved in History.
The intent: switch to **Voice** mode before starting and talk through your solution out loud — your speech is transcribed live, the interviewer replies through your speakers, and the whole session is saved to your history just like a text one, all while you can still write and **Submit Code**.

---

## Code Editor & Execution

Multi-language editor (Python or JavaScript, picked from the language selector) with syntax highlighting, auto-closing brackets, line numbers, and smart indentation. Switching language loads that language's starter code and runs against its test suite. Your code is auto-saved every 2 seconds - you won't lose it if you navigate away.
A CodeMirror editor (Python, JavaScript, or TypeScript, picked from the language selector) with syntax highlighting, auto-closing brackets, line numbers, and smart indentation. Switching language loads that language's starter code and runs against its test suite. Your code is auto-saved as you type - you won't lose it if you navigate away.

| Button | What it does |
| --------- | ------------------------------------------------------------------ |
Expand Down Expand Up @@ -313,11 +303,11 @@ Each category shows a progress bar and lists the problems you've attempted with

## FAQ

**Do I need a paid OpenAI account?**
Yes. The interviewer uses GPT-4o, which requires a paid API key. Voice mode additionally uses the Realtime API.
**Do I need a paid AI account?**
Not necessarily. The default engine is your local **Claude Code CLI**, which runs on your existing Claude sign-in with no separate API key. If you prefer, set `AI_PROVIDER=openai` or `openrouter` and supply that key instead.

**How much does each interview cost?**
Roughly $0.10-$0.50 per text session depending on length. Voice sessions cost more due to Realtime API audio pricing.
On the default Claude CLI engine it draws on your Claude usage rather than a per-call charge. On OpenAI or OpenRouter it's pay-per-token — roughly $0.10-$0.50 per text session depending on length and the model you pick.

**Can I use other programming languages?**
Yes. Pick Python or JavaScript from the language selector in the editor; the code
Expand All @@ -326,6 +316,9 @@ it, or set `NODE_BIN`). TypeScript is also wired up (via `tsx`/`TS_CMD`).
Problems are translated per language, so the selector offers whichever languages
a given problem has been authored for.

**Can I pick a different model?**
Yes. The **Settings** (gear) menu lets you choose the model and reasoning effort for new interviews and tutor chats. On OpenRouter you can search its live catalog of hundreds of models; on Claude and OpenAI you pick from that provider's list.

**What happens if I close the browser mid-interview?**
The session is auto-saved every time you send a message or submit code. Open History to resume from where you left off.

Expand All @@ -336,7 +329,7 @@ Yes. Multiple sessions on the same problem are tracked separately. The status do
That's what makes it useful. Vague feedback doesn't help you improve. The debrief is written the way a hiring committee would actually talk about your performance.

**Where is my data stored?**
In the application's PostgreSQL database, scoped to your account. If you self-host, that database runs on infrastructure you control. Your messages and code are sent to OpenAI's API to power the interviewer.
In the application's PostgreSQL database, scoped to your account. If you self-host, that database runs on infrastructure you control. Your messages and code are sent to your configured AI provider (Claude, OpenAI, or OpenRouter) to power the interviewer.

**Can I add my own problems?**
Yes - see [CONTRIBUTING.md](CONTRIBUTING.md) for the full problem YAML format.
Expand All @@ -353,4 +346,4 @@ This project is a fork of [codingprep](https://github.com/amrutha97/codingprep)

## Contact

Questions or feedback? Reach out on [LinkedIn](https://github.com/shettydev).
Questions or feedback? Reach out on [LinkedIn](https://www.linkedin.com/in/shettydev/).
26 changes: 22 additions & 4 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ DATABASE_URL=postgresql+psycopg://o1prep:o1prep@localhost:5433/o1prep
# deployment, e.g.: python -c "import secrets; print(secrets.token_hex(32))"
SECRET_KEY=dev-secret-change-me

# AI engine for text interviews and the tutor:
# claude -> uses your local Claude Code CLI and existing login (no API key)
# openai -> uses the OpenAI API (requires OPENAI_API_KEY below)
# AI engine for text interviews, the tutor, and voice mode:
# claude -> uses your local Claude Code CLI and existing login (no API key)
# openai -> uses the OpenAI API (requires OPENAI_API_KEY below)
# openrouter -> uses openrouter.ai; one key unlocks many models (see below)
AI_PROVIDER=claude

# --- Claude engine (AI_PROVIDER=claude) ---
Expand All @@ -21,10 +22,27 @@ AI_PROVIDER=claude
# CLAUDE_BIN=claude
# CLAUDE_TIMEOUT=180

# --- OpenAI (AI_PROVIDER=openai, and always required for Voice mode) ---
# --- OpenAI (AI_PROVIDER=openai) ---
# Get a key at https://platform.openai.com/api-keys
OPENAI_API_KEY=your-api-key-here

# --- OpenRouter (AI_PROVIDER=openrouter) ---
# One key for many models (Anthropic, OpenAI, Google, Meta, ...).
# Get a key at https://openrouter.ai/keys and browse slugs at
# https://openrouter.ai/models. Voice mode's STT -> LLM -> TTS pipeline uses
# this same provider, so voice runs through OpenRouter when it's selected.
OPENROUTER_API_KEY=your-openrouter-key-here
# Optional overrides:
# OPENROUTER_MODEL=anthropic/claude-3.5-sonnet
# OPENROUTER_SITE_URL=https://your-deployment.example.com
# OPENROUTER_SITE_NAME=O(1) Prep

# --- Voice mode ---
# Speech-to-text and text-to-speech run in the browser (Web Speech API), so no
# extra keys are needed. The spoken interview is powered by AI_PROVIDER above.
# The legacy OpenAI Realtime path (routes/realtime.py) still requires
# OPENAI_API_KEY if you use it instead.

# --- Code execution (multi-language) ---
# Python runs via the app's interpreter. JavaScript needs Node.js and TypeScript
# needs a TS runner; override these only if they are not on PATH.
Expand Down
19 changes: 16 additions & 3 deletions backend/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@
# random value in any real deployment.
SECRET_KEY = os.environ.get("SECRET_KEY", "dev-secret-change-me")

# AI provider for text interviews and the tutor: 'claude' drives the locally
# installed Claude Code CLI using your existing login (no API key); 'openai'
# uses the OpenAI API. Voice mode always uses OpenAI Realtime regardless.
# AI provider for text interviews, the tutor, and voice mode:
# 'claude' -> local Claude Code CLI using your existing login (no API key)
# 'openai' -> the OpenAI API (OPENAI_API_KEY)
# 'openrouter' -> openrouter.ai, one key for many models (OPENROUTER_API_KEY)
# Voice mode reuses the same text-streaming path, so it runs on whichever
# provider is selected here (the STT -> LLM -> TTS pipeline calls this LLM).
AI_PROVIDER = os.environ.get("AI_PROVIDER", "claude").strip().lower()

# Claude Code CLI provider
Expand All @@ -38,6 +41,16 @@
REALTIME_MODEL = "gpt-4o-realtime-preview"
TRANSCRIPTION_MODEL = "gpt-4o-mini-transcribe"

# OpenRouter (AI_PROVIDER=openrouter). One key, many models by slug; browse them
# at https://openrouter.ai/models. SITE_URL/SITE_NAME are optional attribution
# headers OpenRouter uses for its dashboard and model rankings.
OPENROUTER_API_URL = "https://openrouter.ai/api/v1"
# Public catalog endpoint (no API key required) used to populate the model list.
OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models"
OPENROUTER_MODEL = os.environ.get("OPENROUTER_MODEL", "anthropic/claude-3.5-sonnet").strip()
OPENROUTER_SITE_URL = os.environ.get("OPENROUTER_SITE_URL", "").strip()
OPENROUTER_SITE_NAME = os.environ.get("OPENROUTER_SITE_NAME", "O(1) Prep").strip()

# Chat parameters
CHAT_TEMPERATURE = 0.7
CHAT_MAX_TOKENS = 4000
Expand Down
2 changes: 2 additions & 0 deletions backend/routes/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from routes.auth import bp as auth_bp
from routes.code import bp as code_bp
from routes.models import bp as models_bp
from routes.problems import bp as problems_bp
from routes.realtime import bp as realtime_bp
from routes.research import bp as research_bp
Expand All @@ -10,6 +11,7 @@
problems_bp,
sessions_bp,
code_bp,
models_bp,
realtime_bp,
research_bp,
]
27 changes: 27 additions & 0 deletions backend/routes/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""Model catalog search.

Backs the settings-modal model picker. The OpenRouter catalog is ~300+ models,
so the client never receives the whole list: it renders the curated 'popular
few' from /api/config and calls this endpoint (debounced) to search the rest.
Filtering happens server-side against the in-process catalog cache; results are
bounded by `limit`.
"""

from flask import Blueprint, jsonify, request
from flask_login import login_required

from services import ai

bp = Blueprint("models", __name__)


@bp.route("/api/models/search", methods=["GET"])
@login_required
def search():
query = request.args.get("q", "")
try:
limit = int(request.args.get("limit", 25))
except (TypeError, ValueError):
limit = 25
limit = max(1, min(limit, 50))
return jsonify({"models": ai.search_models(query, limit)})
13 changes: 11 additions & 2 deletions backend/services/ai.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@
import json

import config
from services.providers import claude_provider, openai_provider
from services.providers import claude_provider, openai_provider, openrouter_provider


def _provider():
if config.AI_PROVIDER == "claude":
return claude_provider
if config.AI_PROVIDER == "openrouter":
return openrouter_provider
return openai_provider


Expand All @@ -33,14 +35,21 @@ def config_info():
p = _provider()
return {
"provider": config.AI_PROVIDER,
"models": p.MODELS,
"models": p.models(),
"default_model": p.default_model(),
"supports_effort": p.SUPPORTS_EFFORT,
"efforts": p.EFFORTS,
"default_effort": p.default_effort(),
"supports_search": getattr(p, "SUPPORTS_SEARCH", False),
}


def search_models(query, limit=25):
"""Bounded, provider-aware model search for the settings UI. For OpenRouter
this filters the live catalog; other providers filter their static list."""
return _provider().search_models(query, limit)


def missing_client_message():
"""Human-readable reason the current provider isn't usable, for API errors."""
if config.AI_PROVIDER == "claude":
Expand Down
12 changes: 12 additions & 0 deletions backend/services/providers/claude_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,18 @@
]
EFFORTS = ["low", "medium", "high", "max"]
SUPPORTS_EFFORT = True
# Fixed three-model list, filtered client-side; no live search endpoint needed.
SUPPORTS_SEARCH = False


def models():
return MODELS


def search_models(query, limit=25):
q = (query or "").strip().lower()
hits = [m for m in MODELS if not q or q in m["id"].lower() or q in m["label"].lower()]
return hits[:limit]


def available():
Expand Down
13 changes: 13 additions & 0 deletions backend/services/providers/openai_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,19 @@
]
EFFORTS = []
SUPPORTS_EFFORT = False
# OpenAI's list is tiny and static, so the UI filters it client-side; no live
# search endpoint is needed (unlike OpenRouter's ~300-model catalog).
SUPPORTS_SEARCH = False


def models():
return MODELS


def search_models(query, limit=25):
q = (query or "").strip().lower()
hits = [m for m in MODELS if not q or q in m["id"].lower() or q in m["label"].lower()]
return hits[:limit]


def default_model():
Expand Down
Loading
Loading