Spaces:
Running
Running
| 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> | |
| ); | |
| } | |