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>
103 lines
3.5 KiB
TypeScript
103 lines
3.5 KiB
TypeScript
'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>
|
|
);
|
|
}
|