Arrakis / src /components /ChatMessage.tsx
Trxon's picture
Added changes
8f67306
Raw
History Blame Contribute Delete
4.79 kB
import { motion } from "framer-motion";
import ReactMarkdown from "react-markdown";
import { User, Server, Sparkles } from "lucide-react";
import { Message, AIProvider } from "@/types/chat";
import { cn } from "@/lib/utils";
interface ChatMessageProps {
message: Message;
}
const providerConfig: Record<AIProvider, { icon: React.ElementType; color: string }> = {
ollama: { icon: Server, color: "text-ollama" },
openai: { icon: Sparkles, color: "text-foreground" },
};
export function ChatMessage({ message }: ChatMessageProps) {
const isUser = message.role === "user";
const provider = message.provider || "ollama";
const { icon: ProviderIcon, color } = providerConfig[provider];
return (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2 }}
className={cn(
"flex gap-3 py-4",
isUser ? "justify-end" : "justify-start"
)}
>
{!isUser && (
<div className={cn(
"shrink-0 w-8 h-8 rounded-xl flex items-center justify-center glass-card",
color
)}>
<ProviderIcon className="h-4 w-4" />
</div>
)}
<div
className={cn(
"max-w-[80%] rounded-2xl px-4 py-3",
isUser
? "bg-primary text-primary-foreground rounded-br-md"
: "glass-card rounded-bl-md"
)}
>
{isUser ? (
<p className="text-sm whitespace-pre-wrap">{message.content}</p>
) : (
<div className="prose prose-sm prose-invert max-w-none">
<ReactMarkdown
components={{
p: ({ children }) => (
<p className="mb-2 last:mb-0 text-foreground">{children}</p>
),
code: ({ className, children, ...props }) => {
const isInline = !className;
return isInline ? (
<code className="px-1.5 py-0.5 rounded-md bg-muted text-primary font-mono text-xs">
{children}
</code>
) : (
<code
className="block p-3 rounded-xl bg-muted/50 overflow-x-auto font-mono text-xs text-foreground"
{...props}
>
{children}
</code>
);
},
pre: ({ children }) => (
<pre className="my-2 overflow-hidden rounded-xl">{children}</pre>
),
ul: ({ children }) => (
<ul className="list-disc pl-4 mb-2 space-y-1 text-foreground">{children}</ul>
),
ol: ({ children }) => (
<ol className="list-decimal pl-4 mb-2 space-y-1 text-foreground">{children}</ol>
),
li: ({ children }) => (
<li className="text-sm text-foreground">{children}</li>
),
h1: ({ children }) => (
<h1 className="text-lg font-semibold mb-2 text-foreground">{children}</h1>
),
h2: ({ children }) => (
<h2 className="text-base font-semibold mb-2 text-foreground">{children}</h2>
),
h3: ({ children }) => (
<h3 className="text-sm font-semibold mb-1 text-foreground">{children}</h3>
),
a: ({ href, children }) => (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
{children}
</a>
),
blockquote: ({ children }) => (
<blockquote className="border-l-2 border-primary/50 pl-3 italic text-muted-foreground">
{children}
</blockquote>
),
}}
>
{message.content || (message.isStreaming ? "..." : "")}
</ReactMarkdown>
</div>
)}
{message.isStreaming && (
<div className="flex gap-1 mt-2">
<div className="w-1.5 h-1.5 rounded-full bg-primary animate-bounce" />
<div className="w-1.5 h-1.5 rounded-full bg-primary animate-bounce delay-100" />
<div className="w-1.5 h-1.5 rounded-full bg-primary animate-bounce delay-200" />
</div>
)}
</div>
{isUser && (
<div className="shrink-0 w-8 h-8 rounded-xl flex items-center justify-center bg-primary/20 text-primary">
<User className="h-4 w-4" />
</div>
)}
</motion.div>
);
}