Files
professor/hooks/useAI.ts
Aodhan Collins f644937604 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>
2026-03-04 21:48:34 +00:00

212 lines
6.0 KiB
TypeScript

import type { Dispatch } from 'react';
import type { AppAction, AppState, Topic, Task, ProviderConfig } from '@/types';
// Parse the task generation response into a structured Task object
function parseTask(raw: string, topic: Topic): Task {
const titleMatch = raw.match(/TITLE:\s*(.+)/);
const descMatch = raw.match(/DESCRIPTION:\s*([\s\S]*?)(?=HINTS:|STARTER_CODE:|$)/);
const hintsMatch = raw.match(/HINTS:\s*([\s\S]*?)(?=STARTER_CODE:|$)/);
const codeMatch = raw.match(/STARTER_CODE:\s*```[\w]*\n([\s\S]*?)```/);
const hints = hintsMatch
? hintsMatch[1]
.trim()
.split('\n')
.map((h) => h.replace(/^[-*]\s*/, '').trim())
.filter(Boolean)
: [];
return {
title: titleMatch ? titleMatch[1].trim() : topic.label,
description: descMatch ? descMatch[1].trim() : raw,
hints,
starterCode: codeMatch ? codeMatch[1] : topic.starterCode,
};
}
async function streamFromAPI(
body: Record<string, unknown> & { providerConfig: ProviderConfig },
onChunk: (chunk: string) => void,
signal?: AbortSignal
): Promise<string> {
const res = await fetch('/api/ai', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
signal,
});
if (!res.ok || !res.body) {
throw new Error(`AI request failed: ${res.status}`);
}
const reader = res.body.getReader();
const decoder = new TextDecoder();
let full = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
full += chunk;
onChunk(chunk);
}
return full;
}
export function useAI(dispatch: Dispatch<AppAction>, providerConfig: ProviderConfig) {
async function generateTask(topic: Topic): Promise<void> {
dispatch({ type: 'TASK_STREAM_START' });
try {
const raw = await streamFromAPI(
{ mode: 'generate_task', topic, code: topic.starterCode, messages: [], providerConfig },
(chunk) => dispatch({ type: 'TASK_STREAM_CHUNK', payload: chunk })
);
const task = parseTask(raw, topic);
dispatch({ type: 'TASK_STREAM_DONE', payload: task });
} catch (err) {
dispatch({
type: 'SET_ERROR',
payload: err instanceof Error ? err.message : 'Failed to generate task',
});
}
}
async function reviewCode(state: AppState): Promise<void> {
if (!state.topic) return;
dispatch({ type: 'REVIEW_START' });
try {
const raw = await streamFromAPI(
{
mode: 'review_code',
topic: state.topic,
code: state.code,
executionResult: state.executionResult ?? undefined,
messages: state.messages.map((m) => ({ role: m.role, content: m.content })),
providerConfig,
responseMode: state.responseMode,
},
(chunk) => dispatch({ type: 'STREAM_CHUNK', payload: chunk })
);
dispatch({
type: 'STREAM_DONE',
payload: {
id: crypto.randomUUID(),
role: 'assistant',
content: raw,
timestamp: Date.now(),
type: 'review',
},
});
} catch (err) {
dispatch({
type: 'SET_ERROR',
payload: err instanceof Error ? err.message : 'Failed to review code',
});
}
}
async function sendMessage(state: AppState, userMessage: string): Promise<void> {
if (!state.topic) return;
dispatch({ type: 'SEND_USER_MESSAGE', payload: userMessage });
const history = [
...state.messages.map((m) => ({ role: m.role, content: m.content })),
{ role: 'user' as const, content: userMessage },
];
try {
const raw = await streamFromAPI(
{
mode: 'chat',
topic: state.topic,
code: state.code,
messages: history,
providerConfig,
responseMode: state.responseMode,
},
(chunk) => dispatch({ type: 'STREAM_CHUNK', payload: chunk })
);
dispatch({
type: 'STREAM_DONE',
payload: {
id: crypto.randomUUID(),
role: 'assistant',
content: raw,
timestamp: Date.now(),
type: 'chat',
},
});
} catch (err) {
dispatch({
type: 'SET_ERROR',
payload: err instanceof Error ? err.message : 'Failed to send message',
});
}
}
async function generateLesson(topic: Topic): Promise<void> {
dispatch({ type: 'LESSON_STREAM_START' });
try {
const raw = await streamFromAPI(
{ mode: 'generate_lesson', topic, code: '', messages: [], providerConfig },
(chunk) => dispatch({ type: 'STREAM_CHUNK', payload: chunk })
);
dispatch({ type: 'LESSON_STREAM_DONE', payload: raw });
} catch (err) {
dispatch({
type: 'SET_ERROR',
payload: err instanceof Error ? err.message : 'Failed to generate lesson',
});
}
}
async function sendClassroomMessage(state: AppState, userMessage: string): Promise<void> {
if (!state.topic) return;
dispatch({ type: 'SEND_CLASSROOM_MESSAGE', payload: userMessage });
const history = [
...state.classroomMessages.map((m) => ({ role: m.role, content: m.content })),
{ role: 'user' as const, content: userMessage },
];
try {
const raw = await streamFromAPI(
{
mode: 'classroom_chat',
topic: state.topic,
code: '',
messages: history,
providerConfig,
},
(chunk) => dispatch({ type: 'STREAM_CHUNK', payload: chunk })
);
dispatch({
type: 'CLASSROOM_MESSAGE_DONE',
payload: {
id: crypto.randomUUID(),
role: 'assistant',
content: raw,
timestamp: Date.now(),
type: 'chat',
},
});
} catch (err) {
dispatch({
type: 'SET_ERROR',
payload: err instanceof Error ? err.message : 'Failed to send message',
});
}
}
return { generateTask, reviewCode, sendMessage, generateLesson, sendClassroomMessage };
}