90 lines
2.5 KiB
TypeScript
90 lines
2.5 KiB
TypeScript
|
|
import React from 'react';
|
||
|
|
import { Bot, User } from 'lucide-react';
|
||
|
|
import { MarkdownRenderer } from '../MarkdownRenderer';
|
||
|
|
import { ToolCallCard, ToolCall } from './ToolCallCard';
|
||
|
|
|
||
|
|
export interface Message {
|
||
|
|
id: string;
|
||
|
|
role: 'user' | 'assistant';
|
||
|
|
content: string;
|
||
|
|
timestamp: Date;
|
||
|
|
isStreaming?: boolean;
|
||
|
|
toolCalls?: ToolCall[];
|
||
|
|
}
|
||
|
|
|
||
|
|
interface ChatMessageProps {
|
||
|
|
message: Message;
|
||
|
|
}
|
||
|
|
|
||
|
|
export const ChatMessage: React.FC<ChatMessageProps> = ({ message }) => {
|
||
|
|
const isAssistant = message.role === 'assistant';
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div
|
||
|
|
className={`flex gap-3 ${isAssistant ? '' : 'flex-row-reverse'}`}
|
||
|
|
>
|
||
|
|
{/* Avatar */}
|
||
|
|
<div
|
||
|
|
className={`flex-shrink-0 w-8 h-8 rounded-lg flex items-center justify-center ${
|
||
|
|
isAssistant
|
||
|
|
? 'bg-primary-500/20 text-primary-400'
|
||
|
|
: 'bg-dark-600 text-dark-300'
|
||
|
|
}`}
|
||
|
|
>
|
||
|
|
{isAssistant ? (
|
||
|
|
<Bot className="w-4 h-4" />
|
||
|
|
) : (
|
||
|
|
<User className="w-4 h-4" />
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Message Content */}
|
||
|
|
<div
|
||
|
|
className={`flex-1 min-w-0 ${isAssistant ? 'pr-8' : 'pl-8'}`}
|
||
|
|
>
|
||
|
|
<div
|
||
|
|
className={`rounded-lg px-3 py-2 ${
|
||
|
|
isAssistant
|
||
|
|
? 'bg-dark-700/50 text-dark-100'
|
||
|
|
: 'bg-primary-500/10 text-dark-100 border border-primary-500/20'
|
||
|
|
}`}
|
||
|
|
>
|
||
|
|
{isAssistant ? (
|
||
|
|
<div className="prose prose-invert prose-sm max-w-none">
|
||
|
|
{/* Tool calls */}
|
||
|
|
{message.toolCalls && message.toolCalls.length > 0 && (
|
||
|
|
<div className="not-prose -mx-1">
|
||
|
|
{message.toolCalls.map((toolCall, idx) => (
|
||
|
|
<ToolCallCard key={idx} toolCall={toolCall} />
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
{/* Message content */}
|
||
|
|
{message.content && (
|
||
|
|
<MarkdownRenderer content={message.content} />
|
||
|
|
)}
|
||
|
|
{message.isStreaming && (
|
||
|
|
<span className="inline-block w-2 h-4 bg-primary-400 animate-pulse ml-1" />
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
<p className="text-sm whitespace-pre-wrap">{message.content}</p>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Timestamp */}
|
||
|
|
<div
|
||
|
|
className={`text-xs text-dark-500 mt-1 ${
|
||
|
|
isAssistant ? 'text-left' : 'text-right'
|
||
|
|
}`}
|
||
|
|
>
|
||
|
|
{message.timestamp.toLocaleTimeString([], {
|
||
|
|
hour: '2-digit',
|
||
|
|
minute: '2-digit',
|
||
|
|
})}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|