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 = () => ( +
$1'
+ );
+
+ return (
+
+ );
+ };
+
+ const formatTime = (date) => {
+ return date.toLocaleTimeString([], {hour: "2-digit", minute: "2-digit"});
+ };
+
+ return (
+ + Hey, I'm Keploy AI Assistant! +
+May I help you?
+ ++ Need further assistance? +
+ + ++ You can also reach out to our team on{" "} + + Slack. + +
+{message.text}
+ )} ++ Try asking me about: +
+