Files
professor/components/classroom/LessonPane.tsx
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

97 lines
4.7 KiB
TypeScript

'use client';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
import type { Topic } from '@/types';
interface Props {
topic: Topic;
lessonContent: string | null;
isGenerating: boolean;
streamingContent: string;
}
export default function LessonPane({ topic, lessonContent, isGenerating, streamingContent }: Props) {
const content = lessonContent ?? (isGenerating ? streamingContent : null);
return (
<div className="flex flex-col h-full bg-zinc-950">
{/* Header */}
<div className="flex items-center gap-2 border-b border-zinc-700 bg-zinc-900 px-4 py-2 flex-shrink-0">
<svg className="h-3.5 w-3.5 text-zinc-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z" />
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z" />
</svg>
<span className="text-sm font-semibold text-zinc-200">{topic.label}</span>
{isGenerating && (
<div className="ml-auto flex items-center gap-1.5 text-xs text-zinc-500">
<div className="h-2 w-2 animate-pulse rounded-full bg-blue-400" />
Writing lesson
</div>
)}
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto">
{!content ? (
<div className="flex h-full items-center justify-center">
<div className="flex items-center gap-2 text-sm text-zinc-500">
<div className="h-2 w-2 animate-bounce rounded-full bg-zinc-600 [animation-delay:-0.3s]" />
<div className="h-2 w-2 animate-bounce rounded-full bg-zinc-600 [animation-delay:-0.15s]" />
<div className="h-2 w-2 animate-bounce rounded-full bg-zinc-600" />
</div>
</div>
) : (
<div className="px-6 py-5 text-sm">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
code({ className, children, ...rest }) {
const match = /language-(\w+)/.exec(className ?? '');
return match ? (
<SyntaxHighlighter
style={vscDarkPlus as Record<string, React.CSSProperties>}
language={match[1]}
PreTag="div"
customStyle={{ margin: '0.75rem 0', borderRadius: '0.375rem', fontSize: '0.8rem' }}
>
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
) : (
<code className="rounded bg-zinc-800 px-1 py-0.5 text-xs font-mono text-zinc-300" {...rest}>
{children}
</code>
);
},
h1: ({ children }) => <h1 className="mb-4 mt-6 text-xl font-bold text-zinc-100 first:mt-0">{children}</h1>,
h2: ({ children }) => <h2 className="mb-3 mt-6 text-base font-bold text-zinc-100 border-b border-zinc-800 pb-1">{children}</h2>,
h3: ({ children }) => <h3 className="mb-2 mt-4 text-sm font-semibold text-zinc-200">{children}</h3>,
p: ({ children }) => <p className="mb-3 leading-relaxed text-zinc-300">{children}</p>,
ul: ({ children }) => <ul className="mb-3 list-disc pl-5 space-y-1.5 text-zinc-300">{children}</ul>,
ol: ({ children }) => <ol className="mb-3 list-decimal pl-5 space-y-1.5 text-zinc-300">{children}</ol>,
li: ({ children }) => <li className="leading-relaxed">{children}</li>,
strong: ({ children }) => <strong className="font-semibold text-zinc-100">{children}</strong>,
blockquote: ({ children }) => (
<blockquote className="my-3 border-l-2 border-blue-500 pl-4 italic text-zinc-400">{children}</blockquote>
),
hr: () => <hr className="my-5 border-zinc-800" />,
table: ({ children }) => (
<div className="mb-3 overflow-x-auto">
<table className="w-full border-collapse text-xs">{children}</table>
</div>
),
th: ({ children }) => <th className="border border-zinc-700 bg-zinc-800 px-3 py-1.5 text-left font-semibold text-zinc-200">{children}</th>,
td: ({ children }) => <td className="border border-zinc-700 px-3 py-1.5 text-zinc-300">{children}</td>,
}}
>
{content}
</ReactMarkdown>
</div>
)}
</div>
</div>
);
}