/** * 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 )}
{/* Left panel — Flow execution path */}

Test Controls

{/* Mode selector */}
Mode
{(["interactive", "flow"] as const).map(mode => ( ))}
{/* Quick flow test */} {testMode === "flow" && (
Test Flow {GREETING_OPTIONS.map(opt => ( ))}
)} {/* 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 => ( ))}
)} {/* 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 => ( ))}
)} )}
{/* 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
); }