Files
Portfolio/src/components/chat/ChatMessage.tsx
T
damjan_savicandClaude Opus 4.5 a1cfc33dea auto-claude: subtask-5-1 - Create ChatMessage component for individual messages
Created ChatMessage.tsx component with:
- ChatMessage component for displaying user/assistant messages
- Styled differently based on role (user right-aligned, assistant left-aligned)
- Framer Motion animations for smooth appearance
- Streaming indicator for real-time response display
- TypingIndicator component for loading state
- WelcomeMessage component for empty chat state
- Timestamp formatting utility
- Responsive design following existing Tailwind patterns

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 00:58:13 +01:00

172 lines
4.5 KiB
TypeScript

'use client';
import { motion } from 'framer-motion';
import { User, Bot } from 'lucide-react';
export type ChatRole = 'user' | 'assistant' | 'system';
export interface ChatMessageData {
id?: string;
role: ChatRole;
content: string;
created_at?: string;
isStreaming?: boolean;
}
interface ChatMessageProps {
message: ChatMessageData;
index?: number;
}
/**
* Formats a timestamp for display
*/
function formatTime(timestamp?: string): string {
if (!timestamp) return '';
try {
const date = new Date(timestamp);
return date.toLocaleTimeString(undefined, {
hour: '2-digit',
minute: '2-digit',
});
} catch {
return '';
}
}
/**
* ChatMessage component for displaying individual chat messages
* Styled differently based on message role (user vs assistant)
*/
export function ChatMessage({ message, index = 0 }: ChatMessageProps) {
const { role, content, created_at, isStreaming } = message;
// Don't render system messages
if (role === 'system') {
return null;
}
const isUser = role === 'user';
const formattedTime = formatTime(created_at);
return (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05, duration: 0.2 }}
className={`flex gap-3 ${isUser ? 'flex-row-reverse' : 'flex-row'}`}
>
{/* Avatar */}
<div
className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center ${
isUser
? 'bg-zinc-600/50'
: 'bg-zinc-800/50 border border-zinc-700'
}`}
>
{isUser ? (
<User className="w-4 h-4 text-zinc-300" />
) : (
<Bot className="w-4 h-4 text-zinc-400" />
)}
</div>
{/* Message Content */}
<div
className={`flex flex-col max-w-[80%] ${
isUser ? 'items-end' : 'items-start'
}`}
>
<div
className={`px-4 py-3 rounded-2xl ${
isUser
? 'bg-zinc-600/50 text-white rounded-tr-sm'
: 'bg-zinc-800/50 border border-zinc-800 text-zinc-200 rounded-tl-sm'
}`}
>
<p className="text-sm whitespace-pre-wrap break-words leading-relaxed">
{content}
{isStreaming && (
<span className="inline-block w-1.5 h-4 ml-1 bg-zinc-400 animate-pulse rounded-sm" />
)}
</p>
</div>
{/* Timestamp */}
{formattedTime && !isStreaming && (
<span className="text-xs text-zinc-500 mt-1 px-1">
{formattedTime}
</span>
)}
</div>
</motion.div>
);
}
/**
* Typing indicator shown when assistant is generating a response
*/
export function TypingIndicator() {
return (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="flex gap-3"
>
{/* Avatar */}
<div className="flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center bg-zinc-800/50 border border-zinc-700">
<Bot className="w-4 h-4 text-zinc-400" />
</div>
{/* Typing dots */}
<div className="px-4 py-3 rounded-2xl rounded-tl-sm bg-zinc-800/50 border border-zinc-800">
<div className="flex gap-1.5">
{[0, 1, 2].map((i) => (
<motion.span
key={i}
className="w-2 h-2 bg-zinc-500 rounded-full"
animate={{
opacity: [0.4, 1, 0.4],
scale: [0.8, 1, 0.8],
}}
transition={{
duration: 1,
repeat: Infinity,
delay: i * 0.2,
}}
/>
))}
</div>
</div>
</motion.div>
);
}
/**
* Welcome message component for empty chat state
*/
export function WelcomeMessage() {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="flex flex-col items-center justify-center py-8 px-4 text-center"
>
<div className="w-12 h-12 rounded-full bg-zinc-800/50 border border-zinc-700 flex items-center justify-center mb-4">
<Bot className="w-6 h-6 text-zinc-400" />
</div>
<h3 className="text-lg font-medium text-white mb-2">
Hi, I&apos;m Damjan&apos;s AI Assistant
</h3>
<p className="text-sm text-zinc-400 max-w-xs">
I can help you learn about Damjan&apos;s skills, services, and experience.
Feel free to ask me anything!
</p>
</motion.div>
);
}
export default ChatMessage;