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:
198
CLAUDE.md
Normal file
198
CLAUDE.md
Normal 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
|
||||
```
|
||||
Reference in New Issue
Block a user