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:
81
components/chat/TaskCard.tsx
Normal file
81
components/chat/TaskCard.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import type { Task } from '@/types';
|
||||
|
||||
interface Props {
|
||||
task: Task | null;
|
||||
isLoading: boolean;
|
||||
streamingContent: string;
|
||||
}
|
||||
|
||||
export default function TaskCard({ task, isLoading, streamingContent }: Props) {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
|
||||
if (!isLoading && !task) return null;
|
||||
|
||||
return (
|
||||
<div className="border-b border-zinc-700 bg-zinc-800/50">
|
||||
<button
|
||||
onClick={() => setCollapsed((c) => !c)}
|
||||
className="flex w-full items-center gap-2 px-4 py-3 text-left hover:bg-zinc-800 transition-colors"
|
||||
>
|
||||
<span className="text-xs text-zinc-400">{collapsed ? '▶' : '▼'}</span>
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-blue-400">
|
||||
{isLoading ? 'Generating task…' : task?.title ?? 'Task'}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{!collapsed && (
|
||||
<div className="px-4 pb-4 text-sm text-zinc-300">
|
||||
{isLoading ? (
|
||||
<div className="space-y-2">
|
||||
{/* Skeleton shimmer while task streams in */}
|
||||
{streamingContent ? (
|
||||
<p className="whitespace-pre-wrap text-xs text-zinc-400 font-mono">{streamingContent}</p>
|
||||
) : (
|
||||
<>
|
||||
<div className="h-3 w-3/4 animate-pulse rounded bg-zinc-700" />
|
||||
<div className="h-3 w-full animate-pulse rounded bg-zinc-700" />
|
||||
<div className="h-3 w-2/3 animate-pulse rounded bg-zinc-700" />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : task ? (
|
||||
<div className="space-y-3">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
p: ({ children }) => <p className="leading-relaxed text-zinc-300">{children}</p>,
|
||||
ul: ({ children }) => <ul className="list-disc pl-5 space-y-1">{children}</ul>,
|
||||
li: ({ children }) => <li className="text-zinc-400">{children}</li>,
|
||||
strong: ({ children }) => <strong className="font-semibold text-zinc-200">{children}</strong>,
|
||||
code: ({ children }) => (
|
||||
<code className="rounded bg-zinc-700 px-1 py-0.5 text-xs font-mono text-zinc-300">
|
||||
{children}
|
||||
</code>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{task.description}
|
||||
</ReactMarkdown>
|
||||
|
||||
{task.hints.length > 0 && (
|
||||
<div>
|
||||
<p className="mb-1 text-xs font-semibold text-zinc-500 uppercase tracking-wider">Hints</p>
|
||||
<ul className="list-disc pl-5 space-y-1">
|
||||
{task.hints.map((hint, i) => (
|
||||
<li key={i} className="text-xs text-zinc-500">{hint}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user