diff --git a/package.json b/package.json index 1240a3d55..2dfbacaf4 100644 --- a/package.json +++ b/package.json @@ -27,13 +27,17 @@ "ajv": "^6.12.6", "ajv-keywords": "^3.5.2", "clsx": "^2.0.0", + "framer-motion": "^12.23.24", "lint": "^0.8.19", "lucide-react": "^0.544.0", "prism-react-renderer": "^2.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-icons": "^5.5.0", + "react-markdown": "^10.1.0", "react-player": "^2.6.0", + "rehype-highlight": "^7.0.2", + "remark-gfm": "^4.0.1", "remark-typescript-tools": "1.0.9", "typescript": "5", "uuid": "^8.3.2", diff --git a/src/components/Ragchat.js b/src/components/Ragchat.js new file mode 100644 index 000000000..790bf29a4 --- /dev/null +++ b/src/components/Ragchat.js @@ -0,0 +1,686 @@ +import React, {useState, useRef, useEffect, useCallback} from "react"; +import {motion, AnimatePresence} from "framer-motion"; + +const CHAT_STORAGE_KEY = "keploy_chat_history"; + +const CEO_WELCOME_MESSAGE = { + id: "ceo-welcome", + text: `Hey, I'm Neha, founder of Keploy.io. We are building Keploy to make testing faster with ai. Curious? [Book a Call](https://calendar.app.google/cXVaj6hbMUjvmrnt9) and I'll walk you through!`, + sender: "ceo", + timestamp: new Date(), +}; + +const FAQ_QUESTIONS = [ + "What is Keploy and how does it work?", + "How does Keploy differ from traditional testing tools?", + "Can Keploy integrate with our existing CI/CD pipeline?", + "How does Keploy handle test maintenance?", +]; + +const RagChat = () => { + const [isOpen, setIsOpen] = useState(false); + const [messages, setMessages] = useState([]); + const [inputValue, setInputValue] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [botStatus, setBotStatus] = useState(""); + const messagesEndRef = useRef(null); + const messagesContainerRef = useRef(null); + const [inputHeight, setInputHeight] = useState(80); + const [isResizing, setIsResizing] = useState(false); + const resizeRef = useRef(null); + const startYRef = useRef(0); + const startHeightRef = useRef(0); + const [showGreetingMessage, setShowGreetingMessage] = useState(true); + const [showScrollButton, setShowScrollButton] = useState(false); + const [showSupportForm, setShowSupportForm] = useState(false); + const [formStatus, setFormStatus] = useState("idle"); + const [description, setDescription] = useState(""); + + const toggleChat = () => setIsOpen(!isOpen); + + const handleResizeMouseDown = (e) => { + setIsResizing(true); + startYRef.current = e.clientY; + startHeightRef.current = inputHeight; + document.body.style.cursor = "row-resize"; + document.body.style.userSelect = "none"; + }; + + const handleResizeMouseMove = (e) => { + if (!isResizing) return; + const deltaY = e.clientY - startYRef.current; + let newHeight = startHeightRef.current - deltaY; + newHeight = Math.max(60, Math.min(200, newHeight)); + setInputHeight(newHeight); + }; + + const handleResizeMouseUp = () => { + setIsResizing(false); + document.body.style.cursor = ""; + document.body.style.userSelect = ""; + }; + + useEffect(() => { + if (isResizing) { + window.addEventListener("mousemove", handleResizeMouseMove); + window.addEventListener("mouseup", handleResizeMouseUp); + } + return () => { + window.removeEventListener("mousemove", handleResizeMouseMove); + window.removeEventListener("mouseup", handleResizeMouseUp); + }; + }, [isResizing]); + + const scrollToBottom = useCallback(() => { + messagesEndRef.current?.scrollIntoView({behavior: "smooth"}); + }, []); + + useEffect(() => { + scrollToBottom(); + }, [messages, scrollToBottom]); + + useEffect(() => { + const messagesContainer = messagesContainerRef.current; + if (!messagesContainer) return; + + const handleScroll = () => { + const {scrollTop, scrollHeight, clientHeight} = messagesContainer; + const isAtBottom = scrollHeight - scrollTop - clientHeight < 50; + setShowScrollButton(!isAtBottom); + }; + + messagesContainer.addEventListener("scroll", handleScroll); + setTimeout(() => handleScroll(), 100); + + return () => { + messagesContainer.removeEventListener("scroll", handleScroll); + }; + }, [isOpen, messages]); + + useEffect(() => { + if (isOpen && messages.length === 0) { + setMessages([CEO_WELCOME_MESSAGE]); + } + }, [isOpen, messages.length]); + + useEffect(() => { + if (typeof window !== "undefined") { + const savedChat = localStorage.getItem(CHAT_STORAGE_KEY); + if (savedChat) { + try { + const parsed = JSON.parse(savedChat); + setMessages( + parsed.map((msg) => ({ + ...msg, + timestamp: new Date(msg.timestamp), + })) + ); + } catch (e) { + console.error("Failed to parse chat history", e); + } + } + } + }, []); + + useEffect(() => { + if (messages.length > 0 && typeof window !== "undefined") { + localStorage.setItem(CHAT_STORAGE_KEY, JSON.stringify(messages)); + } + }, [messages]); + + useEffect(() => { + if (!isOpen && showGreetingMessage) { + const timer = setTimeout(() => { + setShowGreetingMessage(false); + }, 7000); + return () => clearTimeout(timer); + } + }, [isOpen, showGreetingMessage]); + + const TypingIndicator = () => ( +
+
+
+
+
+ ); + + const handleSendMessage = async (messageText) => { + if (!messageText.trim()) return; + + if (isLoading) { + setMessages((prev) => [ + ...prev, + { + id: Date.now().toString(), + text: "Please wait for the current response to complete.", + sender: "bot", + timestamp: new Date(), + }, + ]); + return; + } + + const userMessage = { + id: Date.now().toString(), + text: messageText, + sender: "user", + timestamp: new Date(), + }; + + setMessages((prev) => [...prev, userMessage]); + setInputValue(""); + setIsLoading(true); + + const statuses = [ + "Reviewing your query...", + "Searching our knowledge base...", + "Formulating the best response...", + ]; + + let statusIndex = 0; + const statusInterval = setInterval(() => { + setBotStatus(statuses[statusIndex]); + statusIndex = (statusIndex + 1) % statuses.length; + }, 2000); + + try { + const response = await fetch("https://docbot.keploy.io/chat", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({question: messageText}), + }); + + const data = await response.json(); + + const botMessage = { + id: Date.now().toString(), + text: + data.answer || + "I couldn't find an answer to that question. Please try rephrasing or ask something else about Keploy.", + sender: "bot", + timestamp: new Date(), + }; + + setMessages((prev) => [...prev, botMessage]); + } catch (error) { + const errorMsg = { + id: Date.now().toString(), + text: "Sorry, I encountered an error while processing your request. Please try again later.", + sender: "bot", + timestamp: new Date(), + }; + setMessages((prev) => [...prev, errorMsg]); + } finally { + clearInterval(statusInterval); + setBotStatus(""); + setIsLoading(false); + } + }; + + const handleInputChange = useCallback((e) => { + setInputValue(e.target.value); + }, []); + + const handleFAQClick = (question) => { + handleSendMessage(question); + }; + + const handleKeyPress = (e) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSendMessage(inputValue); + } + }; + + const renderMarkdown = (text) => { + let html = text + .replace( + /\[([^\]]+)\]\(([^)]+)\)/g, + '$1' + ) + .replace( + /\*\*([^*]+)\*\*/g, + '$1' + ) + .replace(/\*([^*]+)\*/g, '$1') + .replace( + /`([^`]+)`/g, + '$1' + ); + + return ( +
+ ); + }; + + const formatTime = (date) => { + return date.toLocaleTimeString([], {hour: "2-digit", minute: "2-digit"}); + }; + + return ( +
+ {/* Chat button */} + + {!isOpen && ( + + {showGreetingMessage && ( + +

+ Hey, I'm Keploy AI Assistant! +

+

May I help you?

+
+ + )} + + + + )} + + + {/* Chat window */} + + {isOpen && ( + + {/* Chat header */} +
+
+
+
+ Keploy Bot +
+

Keploy AI Assistant

+
+ +
+
+ +
+ {/* Support Button */} + {!showSupportForm && ( +
+ +
+
+ Support +
+
+
+ )} + + {/* Support Form */} + {showSupportForm && ( + + + +

+ Need further assistance? +

+ +
{ + e.preventDefault(); + setFormStatus("submitting"); + const name = + e.currentTarget.elements.namedItem("name").value; + const email = + e.currentTarget.elements.namedItem("email").value; + const desc = + e.currentTarget.elements.namedItem("description").value; + + try { + const res = await fetch( + "https://docbot.keploy.io/support", + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name, + email, + description: desc, + }), + } + ); + + if (res.ok) { + setFormStatus("success"); + } else { + setFormStatus("error"); + } + } catch (err) { + console.error("Fetch Error:", err); + setFormStatus("error"); + } + + setTimeout(() => { + setFormStatus("idle"); + setShowSupportForm(false); + }, 3000); + }} + className="space-y-2" + > + + +