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:
163
components/editor/EditorToolbar.tsx
Normal file
163
components/editor/EditorToolbar.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
'use client';
|
||||
|
||||
import type { ResponseMode } from '@/types';
|
||||
|
||||
interface Props {
|
||||
language: string;
|
||||
providerLabel: string;
|
||||
isExecuting: boolean;
|
||||
isStreaming: boolean;
|
||||
onRun: () => void;
|
||||
onSubmit: () => void;
|
||||
onReset: () => void;
|
||||
onOpenSettings: () => void;
|
||||
savedIndicator: boolean;
|
||||
authUser: { id: string; email: string } | null;
|
||||
onShowAuth: () => void;
|
||||
onLogout: () => void;
|
||||
responseMode: ResponseMode;
|
||||
onToggleHintMode: () => void;
|
||||
onToggleStrict: () => void;
|
||||
}
|
||||
|
||||
export default function EditorToolbar({
|
||||
language,
|
||||
providerLabel,
|
||||
isExecuting,
|
||||
isStreaming,
|
||||
onRun,
|
||||
onSubmit,
|
||||
onReset,
|
||||
onOpenSettings,
|
||||
savedIndicator,
|
||||
authUser,
|
||||
onShowAuth,
|
||||
onLogout,
|
||||
responseMode,
|
||||
onToggleHintMode,
|
||||
onToggleStrict,
|
||||
}: Props) {
|
||||
const busy = isExecuting || isStreaming;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 border-b border-zinc-700 bg-zinc-900 px-4 py-2">
|
||||
{/* Language badge */}
|
||||
<span className="rounded bg-zinc-700 px-2 py-0.5 text-xs font-mono text-zinc-300">
|
||||
{language}
|
||||
</span>
|
||||
|
||||
{/* Hint mode toggle */}
|
||||
<button
|
||||
onClick={onToggleHintMode}
|
||||
title={responseMode.hintMode ? 'Hints on — click to disable' : 'Hints off — click to enable'}
|
||||
className={`flex items-center gap-1 rounded px-2 py-1 text-xs transition-colors ${
|
||||
responseMode.hintMode
|
||||
? 'bg-yellow-500/20 text-yellow-300 hover:bg-yellow-500/30'
|
||||
: 'text-zinc-500 hover:bg-zinc-700 hover:text-zinc-300'
|
||||
}`}
|
||||
>
|
||||
<svg className="h-3.5 w-3.5" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 2a7 7 0 0 1 7 7c0 2.73-1.56 5.1-3.85 6.33L15 17H9l-.15-1.67C6.56 14.1 5 11.73 5 9a7 7 0 0 1 7-7zm-1 16h2v1a1 1 0 0 1-2 0v-1zm0 3h2v.5a1 1 0 0 1-2 0V21z" />
|
||||
</svg>
|
||||
Hints
|
||||
</button>
|
||||
|
||||
{/* Strict / Lenient pill */}
|
||||
<button
|
||||
onClick={onToggleStrict}
|
||||
title={responseMode.strict ? 'Strict — exact approach required. Click for Lenient.' : 'Lenient — equivalent solutions accepted. Click for Strict.'}
|
||||
className={`rounded px-2 py-1 text-xs font-medium transition-colors ${
|
||||
responseMode.strict
|
||||
? 'bg-red-500/20 text-red-300 hover:bg-red-500/30'
|
||||
: 'bg-zinc-700 text-zinc-400 hover:bg-zinc-600 hover:text-zinc-200'
|
||||
}`}
|
||||
>
|
||||
{responseMode.strict ? 'Strict' : 'Lenient'}
|
||||
</button>
|
||||
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
{/* Saved indicator */}
|
||||
<span
|
||||
className={`text-xs text-zinc-500 transition-opacity duration-300 ${savedIndicator ? 'opacity-100' : 'opacity-0'}`}
|
||||
aria-live="polite"
|
||||
>
|
||||
Saved
|
||||
</span>
|
||||
|
||||
{/* Auth area */}
|
||||
{authUser ? (
|
||||
<>
|
||||
<span className="max-w-[120px] truncate text-xs text-zinc-500" title={authUser.email}>
|
||||
{authUser.email}
|
||||
</span>
|
||||
<button
|
||||
onClick={onLogout}
|
||||
className="rounded px-2 py-1 text-xs text-zinc-500 hover:bg-zinc-700 hover:text-zinc-300 transition-colors"
|
||||
>
|
||||
Sign out
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
onClick={onShowAuth}
|
||||
className="rounded px-2 py-1 text-xs text-zinc-400 hover:bg-zinc-700 hover:text-zinc-200 transition-colors"
|
||||
>
|
||||
Sign in
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Provider settings */}
|
||||
<button
|
||||
onClick={onOpenSettings}
|
||||
title="AI provider settings"
|
||||
className="flex items-center gap-1.5 rounded px-2 py-1.5 text-xs text-zinc-500 hover:bg-zinc-700 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}>
|
||||
<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>
|
||||
<span className="max-w-[80px] truncate">{providerLabel}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={onReset}
|
||||
title="Pick a new topic"
|
||||
className="rounded px-3 py-1.5 text-xs text-zinc-400 hover:bg-zinc-700 hover:text-zinc-200 transition-colors"
|
||||
>
|
||||
Change topic
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={onRun}
|
||||
disabled={busy}
|
||||
className="flex items-center gap-1.5 rounded bg-zinc-700 px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-600 disabled:cursor-not-allowed disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{isExecuting ? (
|
||||
<>
|
||||
<div className="h-3 w-3 animate-spin rounded-full border border-zinc-500 border-t-blue-400" />
|
||||
Running…
|
||||
</>
|
||||
) : (
|
||||
<>▶ Run</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={onSubmit}
|
||||
disabled={busy}
|
||||
className="flex items-center gap-1.5 rounded bg-blue-600 px-3 py-1.5 text-xs text-white hover:bg-blue-500 disabled:cursor-not-allowed disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{isStreaming ? (
|
||||
<>
|
||||
<div className="h-3 w-3 animate-spin rounded-full border border-blue-300 border-t-white" />
|
||||
Reviewing…
|
||||
</>
|
||||
) : (
|
||||
'Submit for Review'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user