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>
158 lines
5.8 KiB
TypeScript
158 lines
5.8 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import type { Topic, SupportedLanguage } from '@/types';
|
|
import { TOPICS } from '@/lib/topics';
|
|
|
|
const PYTHON_ICON = '🐍';
|
|
const HTML_ICON = '🌐';
|
|
|
|
const CUSTOM_STARTER: Record<SupportedLanguage, string> = {
|
|
python: '# Your code here\n',
|
|
html: '<!DOCTYPE html>\n<html lang="en">\n<head>\n <meta charset="UTF-8">\n <title>Page</title>\n</head>\n<body>\n <!-- Your code here -->\n</body>\n</html>\n',
|
|
};
|
|
|
|
interface Props {
|
|
onSelect: (topic: Topic) => void;
|
|
onOpenSettings: () => void;
|
|
}
|
|
|
|
export default function TopicSelector({ onSelect, onOpenSettings }: Props) {
|
|
const pythonTopics = TOPICS.filter((t) => t.language === 'python');
|
|
const htmlTopics = TOPICS.filter((t) => t.language === 'html');
|
|
|
|
const [customLabel, setCustomLabel] = useState('');
|
|
const [customLang, setCustomLang] = useState<SupportedLanguage>('python');
|
|
|
|
function handleCustomSubmit(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
const label = customLabel.trim();
|
|
if (!label) return;
|
|
onSelect({
|
|
id: `custom-${Date.now()}`,
|
|
label,
|
|
language: customLang,
|
|
description: 'Custom topic',
|
|
starterCode: CUSTOM_STARTER[customLang],
|
|
});
|
|
}
|
|
|
|
return (
|
|
<div className="flex h-full w-full flex-col items-center justify-center bg-zinc-950 p-8">
|
|
<div className="w-full max-w-2xl">
|
|
{/* Header */}
|
|
<div className="mb-8 text-center">
|
|
<h1 className="mb-2 text-3xl font-bold tracking-tight text-zinc-100">Professor</h1>
|
|
<p className="text-sm text-zinc-400">Choose a topic to start learning with your AI tutor</p>
|
|
<button
|
|
onClick={onOpenSettings}
|
|
className="mt-3 inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-xs text-zinc-500 hover:bg-zinc-800 hover:text-zinc-300 transition-colors"
|
|
>
|
|
<svg className="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} suppressHydrationWarning>
|
|
<circle cx="12" cy="12" r="3" />
|
|
<path d="M19.07 4.93a10 10 0 0 1 0 14.14M4.93 4.93a10 10 0 0 0 0 14.14" />
|
|
<path d="M12 2v2M12 20v2M2 12h2M20 12h2" />
|
|
</svg>
|
|
AI Provider Settings
|
|
</button>
|
|
</div>
|
|
|
|
{/* Python section */}
|
|
<Section icon={PYTHON_ICON} label="Python" topics={pythonTopics} onSelect={onSelect} />
|
|
|
|
<div className="my-6 border-t border-zinc-800" />
|
|
|
|
{/* HTML / CSS section */}
|
|
<Section icon={HTML_ICON} label="HTML & CSS" topics={htmlTopics} onSelect={onSelect} />
|
|
|
|
<div className="my-6 border-t border-zinc-800" />
|
|
|
|
{/* Custom topic */}
|
|
<div>
|
|
<h2 className="mb-3 flex items-center gap-2 text-xs font-semibold uppercase tracking-widest text-zinc-500">
|
|
<span>✦</span>
|
|
Custom
|
|
</h2>
|
|
<form onSubmit={handleCustomSubmit} className="flex gap-2">
|
|
<input
|
|
type="text"
|
|
value={customLabel}
|
|
onChange={(e) => setCustomLabel(e.target.value)}
|
|
placeholder="e.g. Python Classes, CSS Grid, JavaScript Promises…"
|
|
className="min-w-0 flex-1 rounded-lg border border-zinc-700 bg-zinc-900 px-3 py-2.5 text-sm text-zinc-200 placeholder:text-zinc-600 focus:border-zinc-500 focus:outline-none transition-colors"
|
|
/>
|
|
<div className="flex overflow-hidden rounded-lg border border-zinc-700">
|
|
<button
|
|
type="button"
|
|
onClick={() => setCustomLang('python')}
|
|
className={`px-3 py-2 text-xs transition-colors ${
|
|
customLang === 'python'
|
|
? 'bg-zinc-600 text-zinc-100'
|
|
: 'text-zinc-500 hover:bg-zinc-800 hover:text-zinc-300'
|
|
}`}
|
|
>
|
|
{PYTHON_ICON} Python
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setCustomLang('html')}
|
|
className={`border-l border-zinc-700 px-3 py-2 text-xs transition-colors ${
|
|
customLang === 'html'
|
|
? 'bg-zinc-600 text-zinc-100'
|
|
: 'text-zinc-500 hover:bg-zinc-800 hover:text-zinc-300'
|
|
}`}
|
|
>
|
|
{HTML_ICON} HTML
|
|
</button>
|
|
</div>
|
|
<button
|
|
type="submit"
|
|
disabled={!customLabel.trim()}
|
|
className="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-500 disabled:cursor-not-allowed disabled:opacity-40 transition-colors"
|
|
>
|
|
Start
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function Section({
|
|
icon,
|
|
label,
|
|
topics,
|
|
onSelect,
|
|
}: {
|
|
icon: string;
|
|
label: string;
|
|
topics: Topic[];
|
|
onSelect: (topic: Topic) => void;
|
|
}) {
|
|
return (
|
|
<div>
|
|
<h2 className="mb-3 flex items-center gap-2 text-xs font-semibold uppercase tracking-widest text-zinc-500">
|
|
<span>{icon}</span>
|
|
{label}
|
|
</h2>
|
|
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3">
|
|
{topics.map((topic) => (
|
|
<button
|
|
key={topic.id}
|
|
onClick={() => onSelect(topic)}
|
|
className="group flex flex-col gap-1 rounded-xl border border-zinc-800 bg-zinc-900 p-4 text-left transition-all hover:border-zinc-600 hover:bg-zinc-800 hover:shadow-lg hover:shadow-black/20 active:scale-[0.98]"
|
|
>
|
|
<span className="text-sm font-semibold text-zinc-200 group-hover:text-white transition-colors">
|
|
{topic.label}
|
|
</span>
|
|
<span className="text-xs leading-relaxed text-zinc-500 group-hover:text-zinc-400 transition-colors">
|
|
{topic.description}
|
|
</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|