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

68
lib/auth.ts Normal file
View File

@@ -0,0 +1,68 @@
import { SignJWT, jwtVerify } from 'jose';
import bcrypt from 'bcryptjs';
import { cookies } from 'next/headers';
const JWT_SECRET = new TextEncoder().encode(
process.env.JWT_SECRET ?? 'dev-secret-change-in-production-please'
);
const COOKIE_NAME = 'professor_auth';
const COOKIE_MAX_AGE = 60 * 60 * 24 * 30; // 30 days
export interface JWTPayload {
userId: string;
email: string;
}
// ─── Password helpers ────────────────────────────────────────────────────────
export async function hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, 12);
}
export async function verifyPassword(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash);
}
// ─── JWT helpers ─────────────────────────────────────────────────────────────
export async function signToken(payload: JWTPayload): Promise<string> {
return new SignJWT(payload as unknown as Record<string, unknown>)
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('30d')
.sign(JWT_SECRET);
}
export async function verifyToken(token: string): Promise<JWTPayload | null> {
try {
const { payload } = await jwtVerify(token, JWT_SECRET);
return payload as unknown as JWTPayload;
} catch {
return null;
}
}
// ─── Cookie helpers ───────────────────────────────────────────────────────────
export async function setAuthCookie(token: string): Promise<void> {
const cookieStore = await cookies();
cookieStore.set(COOKIE_NAME, token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: COOKIE_MAX_AGE,
path: '/',
});
}
export async function clearAuthCookie(): Promise<void> {
const cookieStore = await cookies();
cookieStore.delete(COOKIE_NAME);
}
export async function getAuthUser(): Promise<JWTPayload | null> {
const cookieStore = await cookies();
const token = cookieStore.get(COOKIE_NAME)?.value;
if (!token) return null;
return verifyToken(token);
}

36
lib/localSession.ts Normal file
View File

@@ -0,0 +1,36 @@
import type { Task, Message, ExecutionResult } from '@/types';
const STORAGE_KEY = 'professor_session';
export interface LocalSession {
topicId: string;
task: Task;
code: string;
messages: Message[];
executionResult: ExecutionResult | null;
}
export function saveLocalSession(session: LocalSession): void {
if (typeof window === 'undefined') return;
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(session));
} catch {
// Ignore quota errors
}
}
export function loadLocalSession(): LocalSession | null {
if (typeof window === 'undefined') return null;
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return null;
return JSON.parse(raw) as LocalSession;
} catch {
return null;
}
}
export function clearLocalSession(): void {
if (typeof window === 'undefined') return;
localStorage.removeItem(STORAGE_KEY);
}

57
lib/pistonClient.ts Normal file
View File

@@ -0,0 +1,57 @@
import type { ExecutionResult } from '@/types';
// Wandbox — free public code execution API, no auth required
// https://github.com/melpon/wandbox
const WANDBOX_API = 'https://wandbox.org/api/compile.json';
const TIMEOUT_MS = 15_000;
export async function executePython(code: string, stdin = ''): Promise<ExecutionResult> {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
try {
const res = await fetch(WANDBOX_API, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
signal: controller.signal,
body: JSON.stringify({
compiler: 'cpython-3.12.7',
code,
stdin,
}),
});
if (!res.ok) {
throw new Error(`Execution service error: ${res.status}`);
}
const data = await res.json();
const exitCode = parseInt(data.status ?? '0', 10);
// Wandbox separates compiler errors from runtime errors
const stderr = [data.program_error, data.compiler_error]
.filter(Boolean)
.join('\n')
.trim();
return {
stdout: data.program_output ?? '',
stderr,
exitCode,
timedOut: !!data.signal,
};
} catch (err: unknown) {
if (err instanceof Error && err.name === 'AbortError') {
return { stdout: '', stderr: '', exitCode: -1, timedOut: true, error: 'Execution timed out after 15 seconds.' };
}
return {
stdout: '',
stderr: '',
exitCode: -1,
timedOut: false,
error: err instanceof Error ? err.message : 'Unknown execution error',
};
} finally {
clearTimeout(timer);
}
}

138
lib/prompts.ts Normal file
View File

@@ -0,0 +1,138 @@
import type { Topic, ExecutionResult, ResponseMode } from '@/types';
function buildContext(topic: Topic, code: string, result?: ExecutionResult): string {
const execSection = result
? `\nEXECUTION OUTPUT:\nstdout: ${result.stdout || '(empty)'}\nstderr: ${result.stderr || '(empty)'}\nexit code: ${result.exitCode}`
: '';
return `CURRENT TOPIC: ${topic.label} (${topic.language})
CURRENT CODE:
\`\`\`${topic.language}
${code}
\`\`\`${execSection}`;
}
// ─── Task Generation ────────────────────────────────────────────────────────
export function buildTaskGenerationPrompt(topic: Topic): string {
return `You are Professor, an expert and encouraging coding tutor. Generate a hands-on coding task for a student learning "${topic.label}" in ${topic.language}.
Respond in EXACTLY this format — no preamble, no extra text:
TITLE: <short task title>
DESCRIPTION:
<2-4 sentence description of what the student should build or accomplish. Be specific and concrete.>
HINTS:
- <hint 1>
- <hint 2>
- <hint 3>
STARTER_CODE:
\`\`\`${topic.language}
<starter code with comments guiding the student. Leave key parts blank for them to fill in.>
\`\`\`
Rules:
- The task should be completable in 10-15 minutes
- Keep it focused on the topic, not broader concepts
- The starter code should be runnable but incomplete — scaffold the structure, leave the logic for the student
- For Python tasks, use print() to show output so the student can verify their work
- For HTML tasks, include visible elements so the student can see results immediately`;
}
// ─── Code Review ────────────────────────────────────────────────────────────
export function buildCodeReviewPrompt(topic: Topic, code: string, result?: ExecutionResult, responseMode?: ResponseMode): string {
const context = buildContext(topic, code, result);
const hintInstruction = responseMode && !responseMode.hintMode
? '3. Do NOT proactively suggest hints or next steps — only confirm correctness or point out specific errors. The student must ask explicitly to receive hints.'
: '3. Give hints to fix issues rather than directly writing the correct code';
const strictInstruction = responseMode?.strict
? `\n9. STRICT MODE: Only accept code that uses the exact approach and techniques the task requires. If the student\'s solution works but uses a different method (e.g. a built-in instead of a manual implementation, or a different algorithm), explicitly flag it as not meeting the task requirements and explain what approach is expected.`
: '';
return `You are Professor, a patient and encouraging coding tutor. Review the student's code for the topic "${topic.label}".
${context}
Guidelines for your review:
1. Start by acknowledging what the student got right — be specific
2. Point out any issues clearly but kindly — explain WHY it's a problem, not just what
${hintInstruction}
4. If the code is correct, praise it and challenge them to extend it
5. Reference the execution output if provided — point out what the output reveals about correctness
6. Keep your response concise and actionable — avoid walls of text
7. Use markdown with code blocks where helpful${strictInstruction}`;
}
// ─── Classroom Lesson ───────────────────────────────────────────────────────
export function buildLessonPrompt(topic: Topic): string {
return `You are Professor, an expert coding tutor. Write a comprehensive lesson on "${topic.label}" in ${topic.language}.
Structure the lesson as a well-organised markdown document with these sections:
## Introduction
What this concept is and why it matters (23 sentences).
## Key Concepts
The core ideas, clearly explained with simple language.
## Syntax & Usage
Concrete ${topic.language} examples with labelled code blocks.
## Common Patterns
23 real-world usage patterns the student will encounter.
## Common Mistakes
Frequent pitfalls beginners make and how to avoid them.
## Quick Reference
A concise bullet-list summary of the most important points.
Rules:
- Use ${topic.language} fenced code blocks for all examples
- Keep explanations beginner-friendly and concise
- Include practical, runnable examples throughout
- Do NOT include exercises or tasks — those belong in Homework mode`;
}
// ─── Classroom Q&A Chat ─────────────────────────────────────────────────────
export function buildClassroomChatPrompt(topic: Topic): string {
return `You are Professor, a knowledgeable and patient coding tutor. You are in a Classroom session helping a student learn "${topic.label}" in ${topic.language}.
The student has just read a lesson on this topic and may ask conceptual questions, request clarification, or ask for more examples.
Guidelines:
- Answer questions clearly and concisely
- Use ${topic.language} code examples where helpful, with fenced code blocks
- Focus on explanation and understanding — this is not a code review
- If the student asks for an exercise or task, suggest they switch to Homework mode
- Keep responses conversational and reasonably short`;
}
// ─── Free Chat ──────────────────────────────────────────────────────────────
export function buildChatPrompt(topic: Topic, code: string, responseMode?: ResponseMode): string {
const context = buildContext(topic, code);
const hintInstruction = responseMode && !responseMode.hintMode
? '- Do NOT proactively suggest hints or next steps. Answer only what is explicitly asked — no unsolicited guidance.'
: '- Never just give them the answer to their task — guide with hints and explanations';
return `You are Professor, a friendly and knowledgeable coding tutor. You are currently helping a student with the topic "${topic.label}".
${context}
Guidelines:
- Answer the student's questions clearly and concisely
- Stay focused on the current topic and their code, but help with related questions too
${hintInstruction}
- Use markdown with code blocks for code examples
- Keep responses reasonably short — this is a conversation, not a lecture`;
}

95
lib/providers.ts Normal file
View File

@@ -0,0 +1,95 @@
import type { ProviderId, ProviderConfig } from '@/types';
export interface ProviderDefinition {
id: ProviderId;
label: string;
requiresApiKey: boolean;
hasCustomBaseUrl: boolean;
defaultBaseUrl: string;
defaultModel: string;
modelSuggestions: string[];
}
export const PROVIDERS: ProviderDefinition[] = [
{
id: 'anthropic',
label: 'Anthropic (Claude)',
requiresApiKey: true,
hasCustomBaseUrl: false,
defaultBaseUrl: 'https://api.anthropic.com',
defaultModel: 'claude-sonnet-4-6',
modelSuggestions: [
'claude-sonnet-4-6',
'claude-haiku-4-5-20251001',
'claude-opus-4-6',
],
},
{
id: 'openrouter',
label: 'OpenRouter',
requiresApiKey: true,
hasCustomBaseUrl: false,
defaultBaseUrl: 'https://openrouter.ai/api/v1',
defaultModel: 'meta-llama/llama-3.1-8b-instruct:free',
modelSuggestions: [
'meta-llama/llama-3.1-8b-instruct:free',
'google/gemma-3-27b-it:free',
'mistralai/mistral-7b-instruct:free',
'deepseek/deepseek-chat-v3-0324:free',
'qwen/qwen3-8b:free',
],
},
{
id: 'lmstudio',
label: 'LM Studio',
requiresApiKey: false,
hasCustomBaseUrl: true,
defaultBaseUrl: 'http://localhost:1234/v1',
defaultModel: 'local-model',
modelSuggestions: [],
},
{
id: 'ollama',
label: 'Ollama',
requiresApiKey: false,
hasCustomBaseUrl: true,
defaultBaseUrl: 'http://localhost:11434/v1',
defaultModel: 'llama3.2',
modelSuggestions: [
'llama3.2',
'llama3.1',
'mistral',
'qwen2.5-coder',
'deepseek-coder-v2',
'phi4',
],
},
];
export const PROVIDER_MAP = Object.fromEntries(PROVIDERS.map((p) => [p.id, p])) as Record<
ProviderId,
ProviderDefinition
>;
export const DEFAULT_PROVIDER_CONFIG: ProviderConfig = {
provider: 'anthropic',
model: 'claude-sonnet-4-6',
};
const STORAGE_KEY = 'professor_provider_config';
export function loadProviderConfig(): ProviderConfig {
if (typeof window === 'undefined') return DEFAULT_PROVIDER_CONFIG;
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return DEFAULT_PROVIDER_CONFIG;
return JSON.parse(raw) as ProviderConfig;
} catch {
return DEFAULT_PROVIDER_CONFIG;
}
}
export function saveProviderConfig(config: ProviderConfig): void {
if (typeof window === 'undefined') return;
localStorage.setItem(STORAGE_KEY, JSON.stringify(config));
}

79
lib/topics.ts Normal file
View File

@@ -0,0 +1,79 @@
import type { Topic } from '@/types';
export const TOPICS: Topic[] = [
// ─── Python ──────────────────────────────────────────────────────────────
{
id: 'python-variables',
label: 'Python Variables',
language: 'python',
description: 'Learn how to store and work with different types of data',
starterCode: '# Your code here\n',
},
{
id: 'python-lists',
label: 'Python Lists',
language: 'python',
description: 'Create, modify, and iterate over collections of items',
starterCode: '# Your code here\n',
},
{
id: 'python-loops',
label: 'Python Loops',
language: 'python',
description: 'Repeat actions with for and while loops',
starterCode: '# Your code here\n',
},
{
id: 'python-functions',
label: 'Python Functions',
language: 'python',
description: 'Define reusable blocks of code that accept inputs and return outputs',
starterCode: '# Your code here\n',
},
{
id: 'python-dicts',
label: 'Python Dictionaries',
language: 'python',
description: 'Store and retrieve data using key-value pairs',
starterCode: '# Your code here\n',
},
{
id: 'python-strings',
label: 'Python Strings',
language: 'python',
description: 'Manipulate text with string methods and formatting',
starterCode: '# Your code here\n',
},
// ─── HTML / CSS ───────────────────────────────────────────────────────────
{
id: 'html-structure',
label: 'HTML Structure',
language: 'html',
description: 'Build the skeleton of a webpage with semantic HTML elements',
starterCode: '<!DOCTYPE html>\n<html lang="en">\n<head>\n <meta charset="UTF-8">\n <title>My Page</title>\n</head>\n<body>\n <!-- Your code here -->\n</body>\n</html>\n',
},
{
id: 'css-box-model',
label: 'CSS Box Model',
language: 'html',
description: 'Understand margin, border, padding, and content sizing',
starterCode: '<!DOCTYPE html>\n<html lang="en">\n<head>\n <meta charset="UTF-8">\n <title>Box Model</title>\n <style>\n /* Your styles here */\n </style>\n</head>\n<body>\n <!-- Your code here -->\n</body>\n</html>\n',
},
{
id: 'css-flexbox',
label: 'CSS Flexbox',
language: 'html',
description: 'Arrange elements in rows and columns with flexbox layout',
starterCode: '<!DOCTYPE html>\n<html lang="en">\n<head>\n <meta charset="UTF-8">\n <title>Flexbox</title>\n <style>\n /* Your styles here */\n </style>\n</head>\n<body>\n <!-- Your code here -->\n</body>\n</html>\n',
},
{
id: 'html-forms',
label: 'HTML Forms',
language: 'html',
description: 'Collect user input with form elements and validation attributes',
starterCode: '<!DOCTYPE html>\n<html lang="en">\n<head>\n <meta charset="UTF-8">\n <title>Forms</title>\n</head>\n<body>\n <!-- Your form here -->\n</body>\n</html>\n',
},
];
export const TOPIC_BY_ID = Object.fromEntries(TOPICS.map((t) => [t.id, t]));