import React, { memo, useState } from 'react'
import ReactMarkdown from 'react-markdown'
import Image from 'next/image'
import { IoTrashOutline } from 'react-icons/io5'
import { MessageUI } from '@/lib/types/database'
interface MessageProps {
message: MessageUI & { _isStreaming?: boolean }
index: number
isStreaming?: boolean
isLastMessage?: boolean
onViewDiff?: () => void
getAvatarUrl: () => string | null
getInitials: () => string
onFileClick?: (fileId: string, fileName: string) => void
onDeleteMessage?: (messageId: string) => Promise<void>
}
// Custom link renderer for file links
const FileLink = ({ href, children, onFileClick }: {
href?: string
children: React.ReactNode
onFileClick?: (fileId: string, fileName: string) => void
}) => {
const handleClick = (e: React.MouseEvent) => {
e.preventDefault()
if (href && onFileClick) {
if (href.startsWith('file://')) {
const fileId = href.replace('file://', '')
const fileName = typeof children === 'string' ? children.replace(/^📄\s*/, '') : 'Unknown File'
onFileClick(fileId, fileName)
}
// Future: handle folder:// links for folder navigation
}
}
if (href?.startsWith('file://') || href?.startsWith('folder://')) {
return (
<button
onClick={handleClick}
className="text-teal-400 hover:text-teal-300 underline decoration-dotted hover:decoration-solid transition-all duration-150 bg-transparent border-none p-0 font-inherit cursor-pointer"
title={href.startsWith('file://') ? 'Click to open file' : 'Click to navigate to folder'}
>
{children}
</button>
)
}
// Regular external link
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className="text-blue-400 hover:text-blue-300 underline"
>
{children}
</a>
)
}
const Message = memo(function Message({
message,
index,
isStreaming,
isLastMessage,
onViewDiff,
getAvatarUrl,
getInitials,
onFileClick,
onDeleteMessage
}: MessageProps) {
const [isDeleting, setIsDeleting] = useState(false)
const [showDeleteButton, setShowDeleteButton] = useState(false)
const handleDelete = async () => {
if (!onDeleteMessage || isDeleting) return
setIsDeleting(true)
try {
await onDeleteMessage(message.id.toString())
} catch (error) {
console.error('Error deleting message:', error)
} finally {
setIsDeleting(false)
}
}
return (
<div
className={`flex items-start gap-1.5 group relative ${
message.type === 'user' ? 'justify-end' : 'justify-start'
}`}
onMouseEnter={() => setShowDeleteButton(true)}
onMouseLeave={() => setShowDeleteButton(false)}
>
{/* AI Avatar - reduced size */}
{message.type === 'ai' && (
<div className="flex-shrink-0 w-5 h-5 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center shadow-sm border border-slate-600">
<span className="text-[10px]">🤖</span>
</div>
)}
{/* Message Content - reduced padding */}
<div
className={`max-w-[85%] px-3 py-2 rounded-lg relative ${
message.type === 'user'
? 'bg-slate-700/60 backdrop-blur-sm text-slate-100 border border-slate-600/50'
: 'bg-white/5 backdrop-blur-sm text-slate-100 shadow-sm border border-white/10'
}`}
>
{/* Delete Button - appears on hover */}
{showDeleteButton && onDeleteMessage && !(message as any)._isStreaming && (
<button
onClick={handleDelete}
disabled={isDeleting}
className="absolute -top-1 -right-1 p-1 bg-slate-800/80 hover:bg-red-500/90 text-slate-400 hover:text-white rounded-full shadow-sm transition-all duration-300 opacity-0 group-hover:opacity-100 hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed z-10 backdrop-blur-sm border border-slate-600/30 hover:border-red-400/50"
title="Delete message"
>
{isDeleting ? (
<div className="w-3 h-3 border border-current border-t-transparent rounded-full animate-spin" />
) : (
<IoTrashOutline className="w-3 h-3 transition-transform duration-200 group-hover:scale-110" />
)}
</button>
)}
{/* Enhanced markdown rendering with file link support */}
<div className="prose prose-sm prose-invert max-w-none [&>*:first-child]:mt-0 [&>*:last-child]:mb-0">
<ReactMarkdown
components={{
a: ({ href, children }) => (
<FileLink href={href} onFileClick={onFileClick}>
{children}
</FileLink>
),
// Enhance code blocks
code: ({ className, children, ...props }) => {
const match = /language-(\w+)/.exec(className || '')
return (
<code
className={`${className} bg-slate-900/50 px-1 py-0.5 rounded text-xs`}
{...props}
>
{children}
</code>
)
},
// Enhance pre blocks
pre: ({ children }) => (
<pre className="bg-slate-900/50 p-2 rounded text-xs overflow-x-auto border border-slate-700/30">
{children}
</pre>
),
}}
>
{message.content}
</ReactMarkdown>
</div>
{/* Show streaming indicator */}
{(message as any)._isStreaming && (
<div className="flex items-center gap-1 mt-1 opacity-70">
<div className="w-1 h-1 bg-teal-400 rounded-full animate-pulse"></div>
<div className="w-1 h-1 bg-teal-400 rounded-full animate-pulse" style={{ animationDelay: '0.2s' }}></div>
<div className="w-1 h-1 bg-teal-400 rounded-full animate-pulse" style={{ animationDelay: '0.4s' }}></div>
<span className="text-xs text-slate-400 ml-1">AI is working...</span>
</div>
)}
</div>
{/* User Avatar - reduced size */}
{message.type === 'user' && (
<div className="flex-shrink-0 w-5 h-5">
{getAvatarUrl() ? (
<Image
src={getAvatarUrl()!}
alt="User avatar"
width={20}
height={20}
className="w-5 h-5 rounded-full border border-slate-600"
/>
) : (
<div className="w-5 h-5 bg-gradient-to-br from-teal-600 to-cyan-600 rounded-full flex items-center justify-center shadow-sm border border-slate-600">
<span className="text-[8px] font-semibold text-white">
{getInitials()}
</span>
</div>
)}
</div>
)}
</div>
)
})
export default Message