Initial commit — AI-powered coding tutor (Professor)

Next.js 16, React 19, Monaco editor, Anthropic SDK, multi-provider AI,
Wandbox Python execution, iframe HTML preview, SQLite auth + session persistence.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Aodhan Collins
2026-03-04 21:48:34 +00:00
commit f644937604
56 changed files with 14012 additions and 0 deletions

198
CLAUDE.md Normal file
View File

@@ -0,0 +1,198 @@
# Professor — LLM Project Reference
> This file is loaded automatically by Claude Code at session start. It is not end-user documentation.
> For planned features and build order see [ROADMAP.md](ROADMAP.md).
## What the app does
AI-powered coding tutor. User picks a topic → Claude generates a task → user writes code in Monaco editor → runs it → gets AI feedback via chat.
---
## Tech Stack
| Layer | Choice |
|---|---|
| Framework | Next.js 16 (App Router, server components minimally used) |
| UI | React 19, Tailwind CSS v4 |
| Editor | `@monaco-editor/react` (dynamic import, SSR disabled) |
| AI | Anthropic SDK (primary) + raw SSE fetch for OpenAI-compatible providers |
| Code execution | Wandbox API (Python); iframe srcdoc (HTML/CSS) |
| DB | SQLite via Drizzle ORM + better-sqlite3 |
| Auth | Custom JWT (jose) + bcryptjs, httpOnly cookie |
---
## Directory Map
```
app/
api/
ai/route.ts # Streaming AI endpoint (task gen, review, chat)
execute/route.ts # Code execution proxy → Wandbox
models/route.ts # Fetch available models from provider
auth/
register/route.ts # POST: create account, set JWT cookie
login/route.ts # POST: verify password, set JWT cookie
logout/route.ts # POST: clear cookie
me/route.ts # GET: verify cookie → return user
session/route.ts # GET/PUT: load/save session for logged-in user
layout.tsx # Root layout (metadata, fonts)
page.tsx # Renders <AppShell />
globals.css
components/
AppShell.tsx # ← START HERE. Top-level orchestrator. Owns all state,
# auth, session sync, and renders everything.
TopicSelector.tsx # Full-screen topic picker (shown during 'selecting' phase)
ProviderSettings.tsx # Modal: AI provider/model/key config
AuthModal.tsx # Modal: Sign In / Create Account
editor/
EditorPane.tsx # Left pane: wraps toolbar + editor + output
EditorToolbar.tsx # Toolbar: run, submit, auth status, saved indicator
CodeEditor.tsx # Monaco editor wrapper
OutputPanel.tsx # Execution result display
HtmlPreview.tsx # iframe srcdoc preview for HTML topics
chat/
ChatPane.tsx # Right pane: wraps task card + message list + input
MessageList.tsx # Renders all messages
MessageBubble.tsx # Single message (markdown rendered)
ChatInput.tsx # Text input for follow-up chat
TaskCard.tsx # Displays the AI-generated task description
hooks/
useAppState.ts # Central useReducer — AppState + AppAction + reducer
useAI.ts # Streaming fetch: generateTask / reviewCode / sendMessage
useCodeExecution.ts # Calls /api/execute, dispatches EXECUTE_START/DONE
lib/
topics.ts # 10 static topics (TOPICS array + TOPIC_BY_ID map)
providers.ts # PROVIDERS, PROVIDER_MAP, load/saveProviderConfig
prompts.ts # 3 system prompt builders (task / review / chat)
pistonClient.ts # executePython() → Wandbox API (15s timeout)
localSession.ts # localStorage save/load/clear for soft session
auth.ts # hashPassword, verifyPassword, signToken, verifyToken,
# setAuthCookie, clearAuthCookie, getAuthUser
db/
schema.ts # Drizzle table definitions: users, saved_sessions
index.ts # Drizzle singleton (globalThis pattern for hot reload)
types/
index.ts # ALL shared types — read this first when confused about types
drizzle.config.ts # Drizzle migration config (SQLite, professor.db)
```
---
## State Machine
```
AppPhase: selecting → loading_task → ready ↔ executing
reviewing
```
`AppState` (in `hooks/useAppState.ts`):
- `phase` — current app phase
- `topic` — selected `Topic | null`
- `task` — AI-generated `Task | null`
- `code` — current editor content
- `messages``Message[]` (task card, reviews, chat)
- `executionResult` — last run output
- `streamingContent` — partial AI response being streamed
- `isStreaming` — whether AI stream is active
- `error` — error string | null
Key actions: `SELECT_TOPIC`, `RESTORE_SESSION`, `CODE_CHANGE`, `EXECUTE_DONE`, `STREAM_DONE`, `RESET`
---
## Data Flow
```
AppShell
├── useAppState() → [state, dispatch]
├── useAI() → generateTask / reviewCode / sendMessage
│ └── POST /api/ai → streams text back → dispatches STREAM_CHUNK / STREAM_DONE
├── EditorPane
│ ├── EditorToolbar (auth status, saved indicator, run/submit buttons)
│ ├── CodeEditor (Monaco, dispatches CODE_CHANGE)
│ └── useCodeExecution → POST /api/execute → dispatches EXECUTE_DONE
└── ChatPane
├── TaskCard (renders state.task)
└── MessageList (renders state.messages)
```
---
## Session Persistence
**Soft (always on):** localStorage key `professor_session` stores `{ topicId, task, code, messages, executionResult }`. Saved on every state change, restored on mount via `RESTORE_SESSION` action.
**Hard (account required):** Same data synced to `saved_sessions` table in SQLite, debounced 1s. Auth via httpOnly JWT cookie (`professor_auth`). On login, DB session overrides localStorage.
Restore sequence in `AppShell.tsx`:
1. Mount → restore from localStorage immediately
2. Mount → `GET /api/auth/me` → if logged in, `GET /api/session` → dispatch `RESTORE_SESSION` (overrides local)
---
## AI Provider System
Configured at runtime via `ProviderSettings` modal. Config stored in `localStorage` under `professor_provider_config`.
Supported providers (`lib/providers.ts`): `anthropic`, `openrouter`, `lmstudio`, `ollama`
`/api/ai/route.ts` branches on `providerConfig.provider`:
- Anthropic → uses `@anthropic-ai/sdk` with streaming
- Others → raw SSE fetch to OpenAI-compatible endpoint
---
## Key Patterns & Conventions
- **All types in one file:** `types/index.ts`. Check here before creating new types.
- **No external state library** — everything is `useReducer` in `AppShell` via `useAppState`.
- **Prop drilling** (intentional, no Context) — `AppShell → EditorPane → EditorToolbar` and `AppShell → ChatPane`.
- **Modal pattern:** Fixed overlay div, `onClick` on backdrop closes. See `ProviderSettings.tsx` or `AuthModal.tsx` as reference.
- **localStorage pattern:** Always guard with `if (typeof window === 'undefined') return`. See `lib/providers.ts`.
- **API routes are all public** except `/api/session` (requires JWT cookie).
- **Streaming:** AI responses stream as plain text (`text/plain`). Client accumulates via `STREAM_CHUNK` and finalises with `STREAM_DONE`.
- **Monaco:** Always `next/dynamic({ ssr: false })`.
- **HTML topics** skip `/api/execute` entirely — rendered client-side in an iframe.
---
## Environment Variables
```
ANTHROPIC_API_KEY=... # Fallback key for Anthropic provider (required)
JWT_SECRET=... # 32+ char secret for JWT signing (required)
```
---
## Common Tasks — Where to Start
| Task | File(s) to read first |
|---|---|
| Add a new topic | `lib/topics.ts` |
| Change AI prompt behaviour | `lib/prompts.ts` |
| Add a new app phase / state | `types/index.ts``hooks/useAppState.ts` |
| Add a new modal | Copy `components/AuthModal.tsx` or `components/ProviderSettings.tsx` |
| Add a new API route | Mirror existing routes in `app/api/` |
| Change execution behaviour | `hooks/useCodeExecution.ts` + `app/api/execute/route.ts` |
| Modify session data saved | `lib/localSession.ts` + `app/api/session/route.ts` + `db/schema.ts` |
| Add a new provider | `lib/providers.ts` + `types/index.ts` (`ProviderId`) + `app/api/ai/route.ts` |
---
## Dev Commands
```bash
npm run dev # Start dev server
npm run db:push # Apply schema changes to professor.db (Drizzle)
npx tsc --noEmit # Type check
```