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 & { providerConfig: ProviderConfig }, onChunk: (chunk: string) => void, signal?: AbortSignal ): Promise { 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, providerConfig: ProviderConfig) { async function generateTask(topic: Topic): Promise { 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 { 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 { 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 { 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 { 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 }; }