/** * Playground — Test the chatbot in a simulated customer view * Features: Interactive chatbot preview, flow selection, execution path tracking, reset */ import { useState, useRef, useEffect, useMemo } from "react"; import { trpc } from "@/lib/trpc"; import { Button } from "@/components/ui/button"; import { toast } from "sonner"; import { Play, RotateCcw, Sparkles, User, Headphones, Send, Bot, Loader2, Package, Truck, RotateCw, XCircle, ChevronRight, Eye, MessageCircle, } from "lucide-react"; import { Streamdown } from "streamdown"; /* ─── Quick-reply options matching the greeting ─── */ const GREETING_OPTIONS = [ { id: "orders", label: "Orders", icon: , color: "#14532D" }, { id: "shipping", label: "Shipping", icon: , color: "#0369a1" }, { id: "returning", label: "Returning", icon: , color: "#ca8a04" }, { id: "cancelling", label: "Cancelling", icon: , color: "#dc2626" }, ]; interface TestMessage { id: number; sender: "visitor" | "bot" | "agent" | "system"; content: string; timestamp: string; flowStep?: string; } export default function Playground() { const [sessionId, setSessionId] = useState(null); const [messages, setMessages] = useState([]); const [inputText, setInputText] = useState(""); const [isTyping, setIsTyping] = useState(false); const [showGreeting, setShowGreeting] = useState(true); const [selectedFlow, setSelectedFlow] = useState(null); const [flowPath, setFlowPath] = useState([]); const [testMode, setTestMode] = useState<"interactive" | "flow">("interactive"); const messagesEndRef = useRef(null); const startSession = trpc.chat.startSession.useMutation({ onSuccess: (data) => { setSessionId(data.sessionId); }, }); const { data: serverMessages } = trpc.chat.getMessages.useQuery( { sessionId: sessionId || "" }, { enabled: !!sessionId, refetchInterval: 2000 } ); const sendMessage = trpc.chat.sendMessage.useMutation({ onSuccess: () => setIsTyping(false), onError: () => setIsTyping(false), }); const trackEvent = trpc.analytics.track.useMutation(); // Sync server messages useEffect(() => { if (serverMessages?.messages) { const mapped: TestMessage[] = serverMessages.messages.map((m: any) => ({ id: m.id, sender: m.sender, content: m.content, timestamp: m.createdAt?.toString() || new Date().toISOString(), })); setMessages(mapped); if (mapped.length > 0) setShowGreeting(false); } }, [serverMessages]); // Auto-scroll useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages, isTyping]); const handleStartTest = () => { setMessages([]); setShowGreeting(true); setSelectedFlow(null); setFlowPath([]); setSessionId(null); startSession.mutate({}); }; const handleReset = () => { setMessages([]); setShowGreeting(true); setSelectedFlow(null); setFlowPath([]); setSessionId(null); setInputText(""); setIsTyping(false); }; const handleOptionClick = (optionId: string) => { if (!sessionId) return; setShowGreeting(false); setSelectedFlow(optionId); setFlowPath(prev => [...prev, `Greeting → ${optionId}`]); trackEvent.mutate({ sessionId, eventType: "button_clicked", category: optionId, metadata: { source: "playground", button: optionId }, }); // Send the option as a message const text = `I need help with ${optionId}`; setIsTyping(true); setMessages(prev => [...prev, { id: Date.now(), sender: "visitor", content: text, timestamp: new Date().toISOString(), flowStep: `User selected: ${optionId}`, }]); sendMessage.mutate({ sessionId, content: text }); }; const handleSend = () => { if (!inputText.trim() || !sessionId || isTyping) return; const text = inputText.trim(); setInputText(""); setIsTyping(true); setShowGreeting(false); setMessages(prev => [...prev, { id: Date.now(), sender: "visitor", content: text, timestamp: new Date().toISOString(), }]); sendMessage.mutate({ sessionId, content: text }); }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSend(); } }; return ( {/* Top toolbar */} Playground {sessionId && ( Session active )} Reset {sessionId ? "Restart" : "Start Test"} {/* Left panel — Flow execution path */} Test Controls {/* Mode selector */} Mode {(["interactive", "flow"] as const).map(mode => ( setTestMode(mode)} className="flex-1 px-2 py-1.5 rounded-lg text-[11px] font-medium capitalize transition-colors" style={{ background: testMode === mode ? "#7c3aed14" : "transparent", color: testMode === mode ? "#7c3aed" : "#a8a29e", border: testMode === mode ? "1px solid #7c3aed30" : "1px solid transparent", }} > {mode} ))} {/* Quick flow test */} {testMode === "flow" && ( Test Flow {GREETING_OPTIONS.map(opt => ( { if (sessionId) handleOptionClick(opt.id); else toast.info("Start a test session first"); }} className="w-full flex items-center gap-2 px-2 py-1.5 rounded-lg text-left hover:bg-black/5 transition-colors" > {opt.icon} {opt.label} ))} )} {/* Execution path */} Execution Path {flowPath.length === 0 ? ( No flow steps recorded yet ) : ( {flowPath.map((step, i) => ( {i + 1} {step} ))} )} {/* Session info */} {sessionId && ( Session {sessionId} {messages.length} messages · {selectedFlow ? `Flow: ${selectedFlow}` : "No flow selected"} )} {/* Center — Chat preview (simulated customer view) */} {/* Chat header */} Ellie Always happy to help TEST MODE {/* Messages area */} {!sessionId ? ( Ready to test Click "Start Test" to begin a chatbot session ) : ( <> {/* Greeting with options */} {showGreeting && messages.length === 0 && ( 👋 What can we help you with today? {GREETING_OPTIONS.map(opt => ( handleOptionClick(opt.id)} className="px-4 py-2 rounded-full text-[12px] font-medium transition-all hover:shadow-md" style={{ background: "#fff", color: "#0369a1", border: "1.5px solid #0369a130", }} > {opt.label} ))} )} {/* Message list */} {messages.map((msg) => { const isVisitor = msg.sender === "visitor"; const isBot = msg.sender === "bot"; return ( {!isVisitor && ( {isBot ? : } )} {isBot || msg.sender === "agent" ? ( {msg.content} ) : ( msg.content )} {isVisitor && ( )} ); })} {/* Typing indicator */} {isTyping && ( )} {/* Greeting options after messages if still in greeting state */} {showGreeting && messages.length > 0 && ( {GREETING_OPTIONS.map(opt => ( handleOptionClick(opt.id)} className="px-4 py-2 rounded-full text-[12px] font-medium transition-all hover:shadow-md" style={{ background: "#fff", color: "#0369a1", border: "1.5px solid #0369a130" }} > {opt.label} ))} )} > )} {/* Input area */} {showGreeting && sessionId && messages.length === 0 ? ( Hit the buttons to respond ) : ( 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 || !sessionId} /> )} Powered by Ellie · Homelegance AI ); }
No flow steps recorded yet
{sessionId}
{messages.length} messages · {selectedFlow ? `Flow: ${selectedFlow}` : "No flow selected"}
Ready to test
Click "Start Test" to begin a chatbot session
Hit the buttons to respond