UNPKG

hotel-ai-widget

Version:

A customizable hotel chat widget for React and vanilla HTML

518 lines (517 loc) 32.8 kB
/* eslint-disable @typescript-eslint/no-explicit-any */ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; import { useState, useRef, useEffect } from "react"; import React from "react"; import { MessageCircle, X, Calendar, Images, Minimize2, Maximize2, Plane, Hotel, Expand, Map, } from "lucide-react"; import ReactMarkdown from "react-markdown"; import remarkBreaks from "remark-breaks"; import remarkGfm from "remark-gfm"; import toast from "react-hot-toast"; import MapSection from "./MapSection"; import { ItinerarySection } from "./ItinerarySection"; import { InclusionSection } from "./InclusionSection"; import { HotelImagesSection } from "./HotelImagesSection"; import { ChatInput } from "./ChatInput"; import clsx from "clsx"; const useAuth = () => { const isConnected = true; const address = "demo-user-123"; // Mock user address return { isConnected, address }; }; const useSession = () => { const [currentSession, setCurrentSession] = useState(null); return { currentSession, setCurrentSession }; }; export default function HotelChatWidget({ config = {} }) { const [isOpen, setIsOpen] = useState(false); const [isMinimized, setIsMinimized] = useState(false); const [isExpanded, setIsExpanded] = useState(false); const [activeTab, setActiveTab] = useState("chat"); const [expandedRightPanel, setExpandedRightPanel] = useState("itinerary"); const [unifiedMessages, setUnifiedMessages] = useState([]); const [inputValue, setInputValue] = useState(""); const [isStreaming, setIsStreaming] = useState(false); const [itinerary, setItinerary] = useState(); //const [hotelImagesData, setHotelImagesData] = useState<HotelImagesData>(); const [hotelImagesData, setHotelImagesData] = useState(); const [chunks, setChunks] = useState([]); const [markers, setMarkers] = useState([]); const [hoveredMarker, setHoveredMarker] = useState(); console.log("Debug hoveredMarker", hoveredMarker); console.log("isStreaming", isStreaming); // Use config values const CHAT_BASE_URL = config.baseUrl || "https://trib-api.bukprotocol.ai"; const position = config.position || "bottom-right"; const theme = config.theme || "light"; // Authentication and session const { isConnected, address } = useAuth(); const { currentSession, setCurrentSession } = useSession(); const textareaRef = useRef(null); const messagesEndRef = useRef(null); // Auto-resize textarea useEffect(() => { if (textareaRef.current) { textareaRef.current.style.height = "auto"; textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`; } }, [inputValue]); // Auto-scroll to bottom const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }; // Auto-scroll when messages change or streaming updates useEffect(() => { scrollToBottom(); }, [unifiedMessages, isStreaming, chunks]); // Process chunks into unified message structure useEffect(() => { if (chunks.length === 0) return; const flattenedParts = chunks.flat(); if (flattenedParts.length === 0) return; const unifiedMessage = { id: `unified-${Date.now()}`, timestamp: Date.now(), statusContent: "", mainContent: "", places: [], itineraryData: null, hotelImagesData: [], isStreaming: true, type: "ai_unified", }; const newHotelImages = []; flattenedParts.forEach((part) => { const isStatusUpdate = part?.metadata?.category === "status_update"; const isText = part?.type === "text"; const isPlace = part?.type === "place"; const isData = part?.type === "data"; if (isText) { if (isStatusUpdate) { unifiedMessage.statusContent += part.text || ""; } else { unifiedMessage.mainContent += part.text || ""; } } else if (isPlace) { unifiedMessage.places.push(part.place); } else if (isData) { if (part.metadata?.agent === "hotel-agent") { const image = { ...part.data?.image, kind: part.data?.kind, section: part.data?.section, }; unifiedMessage.hotelImagesData = [ ...unifiedMessage.hotelImagesData, image, ]; newHotelImages.push(image); } else { unifiedMessage.itineraryData = part; setItinerary(part); } } }); // Only update hotel images if there are new ones if (newHotelImages.length > 0) { setHotelImagesData(newHotelImages); } setUnifiedMessages((prev) => { const existingIndex = prev.findIndex((msg) => msg.type === "ai_unified" && msg.isStreaming); if (existingIndex !== -1) { const updated = [...prev]; updated[existingIndex] = unifiedMessage; return updated; } else { return [...prev, unifiedMessage]; } }); }, [chunks]); // Mark streaming as complete useEffect(() => { if (!isStreaming) { setUnifiedMessages((prev) => { const updated = prev.map((msg) => msg.type === "ai_unified" && msg.isStreaming ? { ...msg, isStreaming: false } : msg); return updated; }); } }, [isStreaming]); // Integrated handleSend function with your API logic const handleSend = async (userInput) => { if (!isConnected) { toast.error("Please login to send messages."); return; } const trimmedInput = userInput.trim(); setInputValue(""); if (!trimmedInput) return; // Add user message to unified messages const newUserMessage = { id: `user-${Date.now()}`, type: "user", content: trimmedInput, timestamp: Date.now(), }; setUnifiedMessages((prev) => [...prev, newUserMessage]); // Clear chunks for new AI response setChunks([]); setIsStreaming(true); try { const response = await fetch(`${CHAT_BASE_URL}/route`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(currentSession ? { message: trimmedInput, userId: address, sessionId: currentSession, } : { message: trimmedInput, userId: address, }), }); if (!response.ok || !response.body) { console.error("❌ Invalid response"); toast.error("Failed to get response from server"); setIsStreaming(false); return; } const reader = response.body.getReader(); const decoder = new TextDecoder("utf-8"); while (true) { const { value, done } = await reader.read(); if (done) { break; } const chunk = decoder.decode(value, { stream: true }); const lines = chunk.split(/\r?\n/); for (let line of lines) { line = line.trim(); if (!line) continue; if (line.startsWith("data:")) line = line.replace(/^data:\s*/, ""); try { const parsed = JSON.parse(line); console.log("parsed", parsed); if (parsed.sessionId) { setCurrentSession(parsed.sessionId); } if (parsed?.status?.message?.parts) { const parts = parsed.status.message.parts; const newParts = parts.map((part) => ({ ...part, metadata: parsed.status.metadata, sender: "ai", })); setChunks((prev) => [...prev, newParts]); // Process markers for map const newMarkers = newParts .filter((p) => p.type === "place") .map((p, i) => ({ id: Date.now() + i, lat: p.place.latitude, lng: p.place.longitude, label: p.place.name, })); setMarkers((prev) => [...prev, ...newMarkers]); const typeData = newParts.find((p) => p.type === "data"); if (typeData && typeData.metadata?.agent !== "hotel-agent") { console.log("🔍 Type data found:", typeData); setItinerary(typeData); // Auto-switch to itinerary tab when itinerary data is received // if (!isExpanded) { // setActiveTab("itinerary"); // } else { // setExpandedRightPanel("itinerary"); // } } if (typeData && typeData.metadata?.agent === "hotel-agent") { console.log("🏨 Hotel images data found:", typeData); setHotelImagesData(typeData); // Auto-switch to gallery tab when hotel images are received // if (!isExpanded) { // setActiveTab("gallery"); // } else { // setExpandedRightPanel("gallery"); // } } } } catch (error) { console.error("❌ Error parsing chunk:", chunk, error); } } } } catch (error) { console.error("❌ Error in handleSend:", error); toast.error("Failed to send message"); } finally { console.log("🛑 Streaming ended"); setIsStreaming(false); // Unified messages will be marked as complete in the useEffect above } }; const handleSendMessage = async () => { if (!inputValue.trim()) return; await handleSend(inputValue); }; const hasMarkdownContent = (text) => { const cleanText = text.replace(/\\\\n/g, "\n").replace(/\\n/g, "\n"); const markdownPatterns = [ /^#{1,6}\s+/m, /\*\*[^*]+\*\*/, /\*[^*]+\*/, /`[^`]+`/, /```[\s\S]*?```/, /^\s*[-*+]\s+/m, /^\s*\d+\.\s+/m, /^\s*\|.*\|.*$/m, /^\s*>\s+/m, /\[([^\]]+)\]$$([^)]+)$$/, /!\[([^\]]*)\]$$([^)]+)$$/, /^\s*---+\s*$/m, /\n\s*\n/, ]; return markdownPatterns.some((pattern) => pattern.test(cleanText)); }; const createMarkdownComponents = (places) => ({ p: ({ children }) => (_jsx("p", { className: "mb-2 leading-relaxed text-sm", children: processMarkdownChildren(children, places) })), strong: ({ children }) => (_jsx("strong", { className: "font-semibold", children: processMarkdownChildren(children, places) })), em: ({ children }) => (_jsx("em", { className: "italic", children: processMarkdownChildren(children, places) })), code: ({ children }) => (_jsx("code", { className: "bg-gray-100 px-1.5 py-0.5 rounded text-xs font-mono", children: processMarkdownChildren(children, places) })), pre: ({ children }) => (_jsx("pre", { className: "bg-gray-100 p-3 rounded-lg overflow-x-auto my-3", children: _jsx("code", { className: "text-xs font-mono", children: processMarkdownChildren(children, places) }) })), ul: ({ children }) => (_jsx("ul", { className: "list-disc pl-4 my-2 space-y-1", children: processMarkdownChildren(children, places) })), ol: ({ children }) => (_jsx("ol", { className: "list-decimal pl-4 my-2 space-y-1", children: processMarkdownChildren(children, places) })), li: ({ children }) => (_jsx("li", { className: "leading-relaxed text-sm", children: processMarkdownChildren(children, places) })), a: ({ href, children }) => (_jsx("a", { href: href, className: "text-blue-600 underline hover:text-blue-800 transition-colors text-sm", children: processMarkdownChildren(children, places) })), br: () => _jsx("br", {}), div: ({ children }) => (_jsx("div", { children: processMarkdownChildren(children, places) })), h1: ({ children }) => (_jsx("h1", { className: "text-lg font-bold mb-3 mt-4 text-gray-900", children: processMarkdownChildren(children, places) })), h2: ({ children }) => (_jsx("h2", { className: "text-base font-bold mb-2 mt-3 text-gray-900", children: processMarkdownChildren(children, places) })), h3: ({ children }) => (_jsx("h3", { className: "text-sm font-semibold mb-2 mt-3 text-gray-900", children: processMarkdownChildren(children, places) })), h4: ({ children }) => (_jsx("h4", { className: "text-sm font-semibold mb-1 mt-2 text-gray-900", children: processMarkdownChildren(children, places) })), h5: ({ children }) => (_jsx("h5", { className: "text-xs font-semibold mb-1 mt-2 text-gray-900", children: processMarkdownChildren(children, places) })), h6: ({ children }) => (_jsx("h6", { className: "text-xs font-semibold mb-1 mt-2 text-gray-900", children: processMarkdownChildren(children, places) })), blockquote: ({ children }) => (_jsx("blockquote", { className: "border-l-4 border-gray-300 pl-3 italic my-3 text-gray-700 text-sm", children: processMarkdownChildren(children, places) })), hr: () => _jsx("hr", { className: "border-gray-300 my-4" }), table: ({ children }) => (_jsx("div", { className: "overflow-x-auto my-3", children: _jsx("table", { className: "min-w-full border-collapse border border-gray-300 text-xs", children: processMarkdownChildren(children, places) }) })), thead: ({ children }) => (_jsx("thead", { className: "bg-gray-50", children: processMarkdownChildren(children, places) })), tbody: ({ children }) => (_jsx("tbody", { children: processMarkdownChildren(children, places) })), tr: ({ children }) => (_jsx("tr", { className: "border-b border-gray-200 hover:bg-gray-50", children: processMarkdownChildren(children, places) })), th: ({ children }) => (_jsx("th", { className: "border border-gray-300 px-3 py-2 text-left font-semibold text-gray-900 bg-gray-50 text-xs", children: processMarkdownChildren(children, places) })), td: ({ children }) => (_jsx("td", { className: "border border-gray-300 px-3 py-2 text-xs", children: processMarkdownChildren(children, places) })), }); const renderUnifiedMessage = (message) => { return (_jsxs("div", { children: [message.statusContent.trim() && (_jsx("div", { className: "text-xs text-gray-600 italic mb-2", children: _jsx("div", { className: "whitespace-pre-wrap", children: message.statusContent.trim() }) })), message.mainContent.trim() && (_jsx("div", { className: "text-sm leading-relaxed mb-3 group relative", children: hasMarkdownContent(message.mainContent.trim()) ? (_jsx(ReactMarkdown, { components: createMarkdownComponents(message.places || []), remarkPlugins: [remarkBreaks, remarkGfm], children: message.mainContent.trim() })) : (_jsx("div", { className: "whitespace-pre-wrap text-sm", children: message.mainContent.trim() })) }))] }, message.id)); }; const handleKeyDown = (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSendMessage(); } }; // Helper function to convert text with inline place links const renderTextWithPlaceLinks = (text, places) => { if (!places || places.length === 0) { return text; } // Sort places by name length (longest first) to handle overlapping names correctly const sortedPlaces = [...places].sort((a, b) => b.name.length - a.name.length); // Find all place mentions in the text const placeMatches = []; sortedPlaces.forEach((place) => { // Create multiple regex patterns to catch different variations const patterns = [ // Exact match with word boundaries new RegExp(`\\b${place.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "gi"), // Match without strict word boundaries (for places with special characters) new RegExp(`${place.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "gi"), // Match with flexible spacing/punctuation new RegExp(`${place.name .replace(/[.*+?^${}()|[\]\\]/g, "\\$&") .replace(/\s+/g, "\\s*")}`, "gi"), ]; patterns.forEach((regex) => { let match; // Reset regex lastIndex for each pattern regex.lastIndex = 0; while ((match = regex.exec(text)) !== null) { const matchStart = match.index; const matchEnd = match.index + match[0].length; // Check if this match overlaps with existing matches const overlaps = placeMatches.some((existing) => { return ((matchStart >= existing.start && matchStart < existing.end) || (matchEnd > existing.start && matchEnd <= existing.end) || (matchStart <= existing.start && matchEnd >= existing.end)); }); if (!overlaps) { placeMatches.push({ start: matchStart, end: matchEnd, place: place, matchedText: match[0], }); } // Prevent infinite loop if (regex.lastIndex === match.index) { regex.lastIndex++; } } }); }); // Sort matches by start position placeMatches.sort((a, b) => a.start - b.start); // Remove overlapping matches, keeping the longest ones const filteredMatches = []; for (const match of placeMatches) { const hasOverlap = filteredMatches.some((existing) => { return ((match.start >= existing.start && match.start < existing.end) || (match.end > existing.start && existing.end) || (match.start <= existing.start && match.end >= existing.end)); }); if (!hasOverlap) { filteredMatches.push(match); } } // If no matches found, return original text if (filteredMatches.length === 0) { return text; } // Build the final JSX with clickable place links const parts = []; let lastIndex = 0; filteredMatches.forEach((match, index) => { // Add text before this match if (match.start > lastIndex) { parts.push(text.slice(lastIndex, match.start)); } // Add clickable place link parts.push(_jsx("span", { className: "text-blue-600 underline cursor-pointer hover:text-blue-800 font-medium", title: match.place.description || match.place.name, onMouseEnter: () => setHoveredMarker({ id: index, label: match?.place?.name, lat: match?.place?.latitude, lng: match?.place?.longitude, }), onMouseLeave: () => setHoveredMarker(undefined), onClick: () => { console.log("match", match); alert(`📍 ${match.place.name}\n${match.place.description || "No description available"}`); }, children: match.matchedText }, `place-link-${index}-${match.start}`)); lastIndex = match.end; }); // Add remaining text after last match if (lastIndex < text.length) { parts.push(text.slice(lastIndex)); } return _jsx(_Fragment, { children: parts }); }; // Helper function to process markdown children and apply place linking const processMarkdownChildren = (children, places) => { if (typeof children === "string") { return renderTextWithPlaceLinks(children, places); } if (Array.isArray(children)) { return children.map((child, index) => { if (typeof child === "string") { return (_jsx(React.Fragment, { children: renderTextWithPlaceLinks(child, places) }, index)); } return child; }); } return children; }; // Chat Messages Component const ChatMessages = () => (_jsxs("div", { className: "space-y-3 scroll-smooth flex-1 px-4 pt-3", children: [unifiedMessages.length === 0 && !isStreaming && (_jsxs("div", { className: "mb-4", children: [_jsx("p", { className: "text-base font-semibold mb-2", children: "Where to today?" }), _jsxs("div", { className: "flex gap-2 items-start", children: [_jsx(Plane, { className: "w-4 h-4 mt-0.5 flex-shrink-0 text-blue-600" }), _jsx("p", { className: "text-xs text-gray-600", children: "Hey there! I'm here to assist you in planning your experience. Ask me anything travel related." })] })] })), unifiedMessages .sort((a, b) => a.timestamp - b.timestamp) .map((message) => { if (message.type === "user") { return (_jsx("div", { className: "flex justify-end mb-3", children: _jsx("div", { className: "bg-gray-100 text-black px-3 py-2 rounded-2xl rounded-br-md max-w-[80%]", children: _jsx("p", { className: "text-xs whitespace-pre-wrap", children: message.content }) }) }, message.id)); } else { return renderUnifiedMessage(message); } }), isStreaming && (_jsxs("div", { className: "flex gap-1 items-center text-gray-500 mb-3", children: [_jsx("span", { className: "animate-bounce text-xs", children: "\u25CF" }), _jsx("span", { className: "animate-bounce text-xs", style: { animationDelay: "0.2s" }, children: "\u25CF" }), _jsx("span", { className: "animate-bounce text-xs", style: { animationDelay: "0.4s" }, children: "\u25CF" })] })), _jsx("div", { ref: messagesEndRef, style: { overflowAnchor: "none" } })] })); // Chat Input Component // const ChatInput = ({ className = "" }: { className?: string }) => ( // <div className={`p-3 ${className}`}> // <div className="py-2 px-3 border-2 border-black rounded-2xl flex gap-1 w-full"> // <form // onSubmit={(e) => { // e.preventDefault(); // handleSendMessage(); // }} // className="w-full" // > // <textarea // ref={textareaRef} // value={inputValue} // onChange={(e) => setInputValue(e.target.value)} // onKeyDown={handleKeyDown} // className="w-full focus:outline-none placeholder:text-gray-400 resize-none min-h-[20px] max-h-[80px] overflow-y-auto text-xs" // placeholder="Ask about rooms, itineraries, images..." // disabled={isStreaming} // rows={1} // /> // </form> // <div className="flex justify-between items-center"> // <Send // className={`w-4 h-4 text-gray-700 cursor-pointer ${ // isStreaming || !inputValue.trim() // ? "opacity-50 pointer-events-none" // : "" // }`} // onClick={handleSendMessage} // /> // </div> // </div> // </div> // ); // Position classes const positionClasses = { "bottom-right": "bottom-6 right-6", "bottom-left": "bottom-6 left-6", }; const themeClasses = theme === "dark" ? "dark" : ""; if (!isOpen) { return (_jsx("div", { className: clsx("fixed bottom-6 right-6 z-50", positionClasses[position], themeClasses), children: _jsx("button", { onClick: () => setIsOpen(true), className: "rounded-full w-14 h-14 bg-blue-600 hover:bg-blue-700 shadow-lg text-white flex items-center justify-center transition-colors duration-200", children: _jsx(MessageCircle, { className: "w-5 h-5" }) }) })); } // Expanded Modal View if (isExpanded) { return (_jsx("div", { className: clsx("fixed inset-0 z-50 bg-black/50 flex items-center justify-center p-4"), children: _jsxs("div", { className: "w-full max-w-7xl h-full max-h-[90vh] bg-white rounded-lg shadow-2xl flex flex-col", children: [_jsxs("div", { className: "flex items-center justify-between p-4 bg-blue-600 text-white rounded-t-lg flex-shrink-0", children: [_jsxs("div", { className: "flex items-center space-x-2", children: [_jsx(MessageCircle, { className: "w-5 h-5" }), _jsx("h3", { className: "text-lg font-semibold", children: "Hotel Assistant" })] }), _jsxs("div", { className: "flex items-center space-x-2", children: [_jsx("button", { onClick: () => setIsExpanded(false), className: "text-white hover:bg-blue-700 p-2 rounded transition-colors duration-200", children: _jsx(Minimize2, { className: "w-4 h-4" }) }), _jsx("button", { onClick: () => { setIsExpanded(false); setIsOpen(false); }, className: "text-white hover:bg-blue-700 p-2 rounded transition-colors duration-200", children: _jsx(X, { className: "w-4 h-4" }) })] })] }), _jsxs("div", { className: "flex-1 flex overflow-hidden", children: [_jsxs("div", { className: "w-1/2 flex flex-col border-r border-gray-300", children: [_jsx("div", { className: "bg-gray-50 px-4 py-[10px] border-b border-gray-300", children: _jsx("h2", { className: "font-semibold text-gray-900", children: "Chat" }) }), _jsx("div", { className: "flex-1 overflow-y-auto", children: _jsx(ChatMessages, {}) }), _jsx(ChatInput, { handleKeyDown: handleKeyDown, handleSendMessage: handleSendMessage, inputValue: inputValue, isStreaming: isStreaming, setInputValue: setInputValue, textareaRef: textareaRef })] }), _jsxs("div", { className: "w-1/2 flex flex-col", children: [_jsx("div", { className: "bg-gray-50 px-4 py-2 border-b border-gray-300 flex items-center justify-between", children: _jsx("div", { className: "flex items-center space-x-2", children: [ { id: "itinerary", label: "Itinerary", icon: Calendar }, { id: "gallery", label: "Gallery", icon: Images }, { id: "map", label: "Map", icon: Map }, { id: "inclusion", label: "Inclusion", icon: Hotel }, ].map(({ id, label, icon: Icon }) => { const isActive = expandedRightPanel === id; return (_jsxs("button", { onClick: () => setExpandedRightPanel(id), className: `flex items-center gap-1 px-2 py-1 rounded-md text-xs font-medium transition-colors ${isActive ? "bg-blue-600 text-white" : "text-gray-600 hover:text-gray-900 hover:bg-gray-200"}`, "aria-pressed": isActive, children: [_jsx(Icon, { className: "w-3 h-3" }), label] }, id)); }) }) }), expandedRightPanel === "itinerary" ? (_jsx(ItinerarySection, { isStreaming: isStreaming, itinerary: itinerary })) : expandedRightPanel === "gallery" ? (_jsx(HotelImagesSection, { hotelImagesData: hotelImagesData, isStreaming: isStreaming })) : expandedRightPanel === "map" ? (_jsx(MapSection, { markers: markers, hoveredMarker: hoveredMarker })) : (_jsx(InclusionSection, { isStreaming: isStreaming, itinerary: itinerary }))] })] })] }) })); } // Regular Widget View return (_jsx("div", { className: clsx("fixed bottom-6 right-6 z-50", positionClasses[position], themeClasses), children: _jsxs("div", { className: `w-96 bg-white rounded-lg shadow-2xl transition-all duration-300 ${isMinimized ? "h-14" : "h-[600px]"}`, children: [_jsxs("div", { className: "flex items-center justify-between p-3 bg-blue-600 text-white rounded-t-lg", children: [_jsxs("div", { className: "flex items-center space-x-2", children: [_jsx(MessageCircle, { className: "w-4 h-4" }), _jsx("h3", { className: "text-sm font-semibold", children: "Hotel Assistant" })] }), _jsxs("div", { className: "flex items-center space-x-1", children: [_jsx("button", { onClick: () => setIsExpanded(true), className: "text-white hover:bg-blue-700 p-1 rounded transition-colors duration-200", title: "Expand", children: _jsx(Expand, { className: "w-3 h-3" }) }), _jsx("button", { onClick: () => setIsMinimized(!isMinimized), className: "text-white hover:bg-blue-700 p-1 rounded transition-colors duration-200", children: isMinimized ? (_jsx(Maximize2, { className: "w-3 h-3" })) : (_jsx(Minimize2, { className: "w-3 h-3" })) }), _jsx("button", { onClick: () => setIsOpen(false), className: "text-white hover:bg-blue-700 p-1 rounded transition-colors duration-200", children: _jsx(X, { className: "w-3 h-3" }) })] })] }), !isMinimized && (_jsxs(_Fragment, { children: [_jsx("div", { className: "flex border-b border-gray-300", children: [ { id: "chat", label: "Chat", icon: MessageCircle }, { id: "itinerary", label: "Itinerary", icon: Calendar }, { id: "gallery", label: "Gallery", icon: Images }, { id: "map", label: "Map", icon: Map }, { id: "inclusion", label: "Inclusion", icon: Hotel }, ].map(({ id, label, icon: Icon }) => (_jsxs("button", { onClick: () => setActiveTab(id), className: `flex-1 px-2 py-2 text-xs font-medium flex items-center justify-center gap-1 transition-colors duration-200 ${activeTab === id ? "bg-blue-600 text-white" : "bg-gray-100 text-gray-700 hover:bg-gray-200"}`, children: [_jsx(Icon, { className: "w-3 h-3" }), label] }, id))) }), _jsxs("div", { className: "h-[520px] flex flex-col", children: [activeTab === "chat" && (_jsxs(_Fragment, { children: [_jsx("div", { className: "flex-1 flex flex-col overflow-y-auto", children: _jsx(ChatMessages, {}) }), _jsx(ChatInput, { handleKeyDown: handleKeyDown, handleSendMessage: handleSendMessage, inputValue: inputValue, isStreaming: isStreaming, setInputValue: setInputValue, textareaRef: textareaRef })] })), activeTab === "itinerary" && (_jsx(ItinerarySection, { isStreaming: isStreaming, itinerary: itinerary })), activeTab === "gallery" && (_jsx(HotelImagesSection, { hotelImagesData: hotelImagesData, isStreaming: isStreaming })), activeTab === "map" && (_jsx(MapSection, { markers: markers, hoveredMarker: hoveredMarker })), activeTab === "inclusion" && (_jsx(InclusionSection, { isStreaming: isStreaming, itinerary: itinerary }))] })] }))] }) })); }