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>
164 lines
5.3 KiB
TypeScript
164 lines
5.3 KiB
TypeScript
'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>
|
|
);
|
|
}
|