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:
90
components/chat/MessageList.tsx
Normal file
90
components/chat/MessageList.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
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 { Message } from '@/types';
|
||||
import MessageBubble from './MessageBubble';
|
||||
|
||||
interface Props {
|
||||
messages: Message[];
|
||||
isStreaming: boolean;
|
||||
streamingContent: string;
|
||||
emptyText?: string;
|
||||
}
|
||||
|
||||
export default function MessageList({ messages, isStreaming, streamingContent, emptyText }: Props) {
|
||||
const bottomRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
}, [messages, streamingContent]);
|
||||
|
||||
if (messages.length === 0 && !isStreaming) {
|
||||
return (
|
||||
<div className="flex flex-1 items-center justify-center p-6 text-center">
|
||||
<p className="text-sm text-zinc-500">
|
||||
{emptyText ?? 'Run your code or submit it for review — or ask me anything about the task.'}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-3">
|
||||
{messages.map((msg) => (
|
||||
<MessageBubble key={msg.id} message={msg} />
|
||||
))}
|
||||
|
||||
{/* Live streaming bubble */}
|
||||
{isStreaming && streamingContent && (
|
||||
<div className="flex justify-start">
|
||||
<div className="max-w-[92%] rounded-2xl rounded-tl-sm bg-zinc-800 px-4 py-3 text-sm text-zinc-200">
|
||||
<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.5rem 0', borderRadius: '0.375rem', fontSize: '0.8rem' }}
|
||||
>
|
||||
{String(children).replace(/\n$/, '')}
|
||||
</SyntaxHighlighter>
|
||||
) : (
|
||||
<code className="rounded bg-zinc-700 px-1 py-0.5 text-xs font-mono text-zinc-200" {...rest}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
p: ({ children }) => <p className="mb-2 last:mb-0 leading-relaxed">{children}</p>,
|
||||
}}
|
||||
>
|
||||
{streamingContent}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Typing indicator when streaming but no content yet */}
|
||||
{isStreaming && !streamingContent && (
|
||||
<div className="flex justify-start">
|
||||
<div className="rounded-2xl rounded-tl-sm bg-zinc-800 px-4 py-3">
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="h-2 w-2 animate-bounce rounded-full bg-zinc-500 [animation-delay:-0.3s]" />
|
||||
<div className="h-2 w-2 animate-bounce rounded-full bg-zinc-500 [animation-delay:-0.15s]" />
|
||||
<div className="h-2 w-2 animate-bounce rounded-full bg-zinc-500" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div ref={bottomRef} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user