Spaces:
Running
Running
| 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, "<") | |
| .replace(/>/g, ">") | |
| .replace(/"/g, """) | |
| .replace(/'/g, "'"); | |
| } | |
| 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> | |
| ); | |
| } | |