Frontend-Data-Eyond / src /app /components /chat /MessageBubble.tsx
ishaq101's picture
[NOTICKET] Major update, re-stylign and upgrade using maintiva demo setup
c0ddd13
import { useEffect } from "react";
import { motion } from "motion/react";
import { Bot } from "lucide-react";
import type { Message } from "./types";
import TypingIndicator from "./TypingIndicator";
import FeedbackWidget from "./FeedbackWidget";
import MarkdownRenderer from "./renderers/MarkdownRenderer";
interface MessageBubbleProps {
message: Message;
isStreamingPlaceholder?: boolean;
}
export default function MessageBubble({ message, isStreamingPlaceholder }: MessageBubbleProps) {
useEffect(() => {
if (message.role === "assistant") {
// console.log(
// `[MessageBubble] id=...${message.id.slice(-6)} isStreamingPlaceholder=${isStreamingPlaceholder} contentLen=${message.content.length}`
// );
}
}, [isStreamingPlaceholder, message.id, message.role, message.content.length]);
if (message.role === "user") {
return (
<motion.div
className="flex justify-end px-3 sm:px-4 py-1"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, ease: "easeOut" }}
>
<div className="max-w-[88%] sm:max-w-[75%] xl:max-w-[65%] px-4 py-3 rounded-2xl rounded-br-sm bg-gradient-to-br from-brand-green-light to-brand-green text-white text-sm xl:text-base leading-relaxed shadow-md shadow-brand-green/20">
{message.content}
</div>
</motion.div>
);
}
// Show TypingIndicator when the assistant placeholder has no content yet
if (isStreamingPlaceholder && message.content === "") {
return <TypingIndicator />;
}
return (
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3, ease: "easeOut" }}
>
<div className="flex gap-3 px-3 sm:px-4 py-2">
<div className="w-7 h-7 xl:w-8 xl:h-8 rounded-full bg-gradient-to-br from-brand-green-light to-brand-green flex-shrink-0 flex items-center justify-center shadow-sm">
<Bot className="h-4 w-4 text-white" />
</div>
<div className="max-w-[92%] sm:max-w-[88%] xl:max-w-[80%] bg-white border border-neutral-100 rounded-2xl rounded-tl-sm shadow-sm px-4 xl:px-5 py-3 xl:py-4">
<MarkdownRenderer
key={isStreamingPlaceholder ? "streaming" : "final"}
content={message.content}
skipPreprocess={isStreamingPlaceholder}
/>
{message.sources && message.sources.length > 0 && (
<div className="mt-3 pt-2 border-t border-neutral-100">
<p className="text-[10px] text-neutral-400 mb-1.5">Sources:</p>
<div className="flex flex-wrap gap-1">
{message.sources.map((src, i) => (
<span
key={i}
className="text-[10px] bg-neutral-100 text-neutral-600 px-2 py-0.5 rounded-full border border-neutral-200"
title={src.page_label ? `Page ${src.page_label}` : undefined}
>
๐Ÿ“„ {src.filename}
{src.page_label ? ` p.${src.page_label}` : ""}
</span>
))}
</div>
</div>
)}
</div>
</div>
{!isStreamingPlaceholder && (
<FeedbackWidget
messageId={message.id}
content={message.content}
audioText={message.audioText}
audioChunks={message.audioChunks}
audioSampleRate={message.audioSampleRate}
/>
)}
</motion.div>
);
}