negoptimAi / frontend /src /components /MessageBubble.tsx
samir12321's picture
Initial commit: Negoptim AI RAG chatbot (backend + frontend + deploy config)
af404c9
Raw
History Blame Contribute Delete
3.27 kB
import { Message, EmailActionOptions } from "@/types/chat";
import SourceCard from "./SourceCard";
import RichResponseRenderer from "./rich/RichResponseRenderer";
import HexAvatar from "./HexAvatar";
interface MessageBubbleProps {
message: Message;
onEmailAction?: (content: string, options: EmailActionOptions) => void;
}
// Escape HTML before applying markdown — the result feeds
// dangerouslySetInnerHTML, and LLM output must never inject markup.
function escapeHtml(text: string): string {
return text
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function formatContent(text: string): string {
return escapeHtml(text)
.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>")
.replace(/\*(.*?)\*/g, "<em>$1</em>")
.replace(/`([^`]+)`/g, "<code>$1</code>")
.replace(/\n/g, "<br />");
}
export default function MessageBubble({ message, onEmailAction }: MessageBubbleProps) {
const isUser = message.role === "user";
const time = message.timestamp.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
if (isUser) {
return (
<div className="flex justify-end animate-slide-up">
<div className="max-w-[72%] lg:max-w-[60%]">
<div className="bg-gradient-to-br from-[#0B1221] to-[#16233D] text-white rounded-2xl rounded-tr-sm px-4 py-3 text-[14px] leading-relaxed shadow-sm">
{message.content}
</div>
<p className="text-right text-[11px] text-[#CBD5E1] mt-1 pr-1">{time}</p>
</div>
</div>
);
}
return (
<div className="flex gap-3 animate-slide-up">
{/* NegOptim AI avatar — hex node mark */}
<div className="flex-shrink-0 mt-0.5">
<HexAvatar size={32} />
</div>
<div className="flex-1 min-w-0">
{/* Sender label */}
<div className="flex items-center gap-2 mb-1.5">
<span className="text-[12px] font-bold gradient-text tracking-wide">NegOptim AI</span>
<span className="text-[11px] text-[#CBD5E1]">{time}</span>
</div>
{/* Answer card */}
<div className="bg-white border border-[#E8EDF2] rounded-2xl rounded-tl-sm px-4 py-3.5 shadow-sm">
{/* Gradient left accent */}
<div className="flex gap-3">
<div className="w-0.5 rounded-full bg-gradient-to-b from-[#47C3A6] to-[#14B7CC] flex-shrink-0 self-stretch" />
<div
className="text-sm text-[#334155] leading-relaxed message-content flex-1 min-w-0"
dangerouslySetInnerHTML={{ __html: formatContent(message.content) }}
/>
</div>
{message.sources && message.sources.length > 0 && (
<div className="mt-3 pt-3 border-t border-[#F1F5F9]">
<SourceCard sources={message.sources} />
</div>
)}
</div>
{/* Rich layout below the card */}
{message.responseType && message.responseType !== "plain" && message.structuredData && (
<RichResponseRenderer
responseType={message.responseType}
data={message.structuredData}
onAction={onEmailAction}
/>
)}
</div>
</div>
);
}