hotel-ai-widget
Version:
A customizable hotel chat widget for React and vanilla HTML
518 lines (517 loc) • 32.8 kB
JavaScript
/* 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 }))] })] }))] }) }));
}