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

View File

@@ -0,0 +1,102 @@
'use client';
import type { Dispatch } from 'react';
import type { AppState, AppAction, AppMode } from '@/types';
import TaskCard from './TaskCard';
import MessageList from './MessageList';
import ChatInput from './ChatInput';
interface Props {
state: AppState;
dispatch: Dispatch<AppAction>;
onSendMessage: (text: string) => void;
onSendClassroomMessage: (text: string) => void;
onSubmit: () => void;
onSetAppMode: (mode: AppMode) => void;
}
export default function ChatPane({ state, onSendMessage, onSendClassroomMessage, onSetAppMode }: Props) {
const isClassroom = state.appMode === 'classroom';
const isTaskLoading = state.phase === 'loading_task';
// In classroom mode "busy" only covers classroom chat replies, not lesson generation
const isBusy = isClassroom
? state.isStreaming && state.lessonContent !== null
: state.isStreaming || state.phase === 'executing';
const messages = isClassroom ? state.classroomMessages : state.messages;
const handleSend = isClassroom ? onSendClassroomMessage : onSendMessage;
const showChatStreaming = isClassroom
? state.isStreaming && state.lessonContent !== null
: state.isStreaming && state.phase !== 'loading_task';
const chatStreamingContent = showChatStreaming ? state.streamingContent : '';
return (
<div className="flex flex-col h-full bg-zinc-900">
{/* Header */}
<div className="flex items-center gap-2 border-b border-zinc-700 px-4 py-2">
<div className="h-2 w-2 rounded-full bg-blue-400" />
<span className="text-sm font-semibold text-zinc-200">Professor</span>
{isBusy && (
<div className="flex items-center gap-1.5 text-xs text-zinc-500">
<div className="h-2 w-2 animate-pulse rounded-full bg-blue-400" />
Thinking
</div>
)}
{/* Mode toggle */}
<div className="ml-auto flex overflow-hidden rounded-lg border border-zinc-700">
<button
onClick={() => onSetAppMode('classroom')}
className={`px-3 py-1 text-xs transition-colors ${
isClassroom ? 'bg-zinc-700 text-zinc-100' : 'text-zinc-500 hover:bg-zinc-800 hover:text-zinc-300'
}`}
>
Classroom
</button>
<button
onClick={() => onSetAppMode('homework')}
className={`border-l border-zinc-700 px-3 py-1 text-xs transition-colors ${
!isClassroom ? 'bg-zinc-700 text-zinc-100' : 'text-zinc-500 hover:bg-zinc-800 hover:text-zinc-300'
}`}
>
Homework
</button>
</div>
</div>
{/* Task card — homework only */}
{!isClassroom && (
<TaskCard
task={state.task}
isLoading={isTaskLoading}
streamingContent={isTaskLoading ? state.streamingContent : ''}
/>
)}
{/* Error banner */}
{state.error && (
<div className="mx-3 mt-3 rounded-lg border border-red-800 bg-red-900/30 px-4 py-2.5 text-sm text-red-300">
{state.error}
</div>
)}
{/* Messages */}
<MessageList
messages={messages}
isStreaming={showChatStreaming}
streamingContent={chatStreamingContent}
emptyText={
isClassroom
? 'Ask me anything about this topic.'
: 'Run your code or submit it for review — or ask me anything about the task.'
}
/>
{/* Input */}
<ChatInput isDisabled={state.isStreaming} onSend={handleSend} />
</div>
);
}