/** * Live Chatbot Widget โ€” Tidio-inspired greeting with quick-reply buttons * Shows: "What can we help you with today?" + Orders, Shipping, Returning, Cancelling buttons */ import { useState, useRef, useEffect } from "react"; import { trpc } from "@/lib/trpc"; import { Bot, X, Send, Loader2, User, Headphones, Sparkles, ChevronDown, MoreVertical } from "lucide-react"; import { Streamdown } from "streamdown"; const QUICK_REPLIES = [ { label: "Orders", value: "I need help with my order" }, { label: "Shipping", value: "I have a shipping question" }, { label: "Returning", value: "I want to return an item" }, { label: "Cancelling", value: "I need to cancel my order" }, ]; export default function ChatbotWidgetLive() { const [isOpen, setIsOpen] = useState(false); const [inputText, setInputText] = useState(""); const [sessionId, setSessionId] = useState(null); const [localMessages, setLocalMessages] = useState>([]); const [isTyping, setIsTyping] = useState(false); const [showQuickReplies, setShowQuickReplies] = useState(true); const messagesEndRef = useRef(null); const startSession = trpc.chat.startSession.useMutation({ onSuccess: (data) => { setSessionId(data.sessionId); }, }); const { data: messagesData } = trpc.chat.getMessages.useQuery( { sessionId: sessionId || "" }, { enabled: !!sessionId, refetchInterval: 3000 } ); const sendMessage = trpc.chat.sendMessage.useMutation({ onSuccess: () => { setIsTyping(false); }, onError: () => { setIsTyping(false); }, }); // Sync server messages to local state useEffect(() => { if (messagesData?.messages) { setLocalMessages(messagesData.messages.map((m: any) => ({ id: m.id, sender: m.sender, content: m.content, createdAt: m.createdAt?.toString() || new Date().toISOString(), }))); } }, [messagesData]); // Auto-scroll useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [localMessages, isTyping]); const handleOpen = () => { setIsOpen(true); if (!sessionId) { startSession.mutate({}); } }; const handleSend = (text?: string) => { const content = text || inputText.trim(); if (!content || !sessionId || isTyping) return; setInputText(""); setIsTyping(true); setShowQuickReplies(false); // Optimistically add user message setLocalMessages(prev => [...prev, { id: Date.now(), sender: "visitor", content, createdAt: new Date().toISOString(), }]); sendMessage.mutate({ sessionId, content }); }; const handleQuickReply = (reply: typeof QUICK_REPLIES[0]) => { handleSend(reply.value); }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSend(); } }; // Determine if we should show the greeting state (no user messages yet) const hasUserMessages = localMessages.some(m => m.sender === "visitor"); return ( <> {/* Chat window */} {isOpen && (
{/* Header โ€” Tidio-inspired gradient */}
{/* Online indicator */}
Ellie
{messagesData?.status === "escalated" ? "Connected to agent" : "Always happy to help"}
{/* Messages */}
{startSession.isPending ? (
Starting conversation...
) : ( <> {/* Greeting bubble โ€” always shown at top */} {!hasUserMessages && (
๐Ÿ‘‹ What can we help you with today?
)} {/* Quick reply buttons โ€” Tidio-style */} {showQuickReplies && !hasUserMessages && sessionId && (
{QUICK_REPLIES.map((reply) => ( ))}
)} {/* Conversation messages */} {localMessages.map((msg) => { const isVisitor = msg.sender === "visitor"; const isBot = msg.sender === "bot"; return (
{!isVisitor && (
{isBot ? : }
)}
{isBot || msg.sender === "agent" ? (
{msg.content}
) : ( msg.content )}
{isVisitor && (
)}
); })} )} {isTyping && (
)}
{/* Input area */}
{showQuickReplies && !hasUserMessages ? (
Hit the buttons to respond
) : null}
setInputText(e.target.value)} onKeyDown={handleKeyDown} placeholder="Type your message..." className="flex-1 px-3 py-2 rounded-xl text-sm border" style={{ borderColor: "#e7e0d5", fontFamily: "'Source Sans 3', sans-serif", background: "#fff" }} disabled={isTyping || startSession.isPending} />
Powered by Ellie ยท Homelegance AI
)} {/* Floating button */} ); }