diff --git a/front-end/src/components/Chat.jsx b/front-end/src/components/Chat.jsx index e2ad6a1..6d02806 100644 --- a/front-end/src/components/Chat.jsx +++ b/front-end/src/components/Chat.jsx @@ -4,18 +4,24 @@ import axios from "axios"; import { marked } from "marked"; import { ReactTyped } from "react-typed"; import { GiInjustice } from "react-icons/gi"; -import { FiSend, FiCpu, FiUser, FiRefreshCw, FiArrowRight, FiThumbsUp, FiThumbsDown } from "react-icons/fi"; +import { FiSend, FiCpu, FiUser, FiRefreshCw, FiArrowRight, FiThumbsUp, FiThumbsDown, FiCopy, FiEdit2, FiCheck, FiChevronLeft, FiChevronRight, FiX } from "react-icons/fi"; import { motion, AnimatePresence } from "framer-motion"; import clsx from "clsx"; const Chat = () => { const [inputValue, setInputValue] = useState(""); + // History structure: [{ versions: [{ question, response, citations, feedback }], currentVersion: 0 }] const [history, setHistory] = useState([]); const [errorMessage, setErrorMessage] = useState(""); const [isLoading, setIsLoading] = useState(false); const [currentIndex, setCurrentIndex] = useState(null); const [isResponseComplete, setIsResponseComplete] = useState(false); + const [copiedIndex, setCopiedIndex] = useState(null); + const [editingIndex, setEditingIndex] = useState(null); // Index of the message currently being edited + const [editValue, setEditValue] = useState(""); // Text content of the inline edit + const textareaRef = useRef(null); + const editTextareaRef = useRef(null); const responseContainerRef = useRef(null); // Handle tab visibility change to fix background generation issue @@ -32,8 +38,10 @@ const Chat = () => { }; }, [currentIndex, isResponseComplete]); - const handleSubmit = useCallback(async () => { - if (inputValue.trim() === "") { + const handleSubmit = useCallback(async (overridePrompt = null) => { + const promptToUse = overridePrompt || inputValue; + + if (promptToUse.trim() === "") { setErrorMessage("Please ask something."); return; } @@ -41,7 +49,21 @@ const Chat = () => { setIsLoading(true); setErrorMessage(""); setIsResponseComplete(false); - const requestData = { prompt: inputValue }; + + // Optimistic Update: Add user question immediately + const newIndex = history.length; + setHistory((prevHistory) => [ + ...prevHistory, + { + versions: [{ question: promptToUse, response: "", citations: [], feedback: null }], + currentVersion: 0 + }, + ]); + + setInputValue(""); + setCurrentIndex(newIndex); + + const requestData = { prompt: promptToUse }; try { const { @@ -51,41 +73,124 @@ const Chat = () => { requestData ); - setHistory((prevHistory) => [ - ...prevHistory, - { question: inputValue, response, citations, feedback: null }, - ]); - - setInputValue(""); - setCurrentIndex(history.length); + // Update the history entry with the real response + setHistory((prevHistory) => { + const newHistory = [...prevHistory]; + // Ensure we are updating the correct index (should be the last one we added) + if (newHistory[newIndex]) { + newHistory[newIndex].versions[0].response = response; + newHistory[newIndex].versions[0].citations = citations; + } + return newHistory; + }); } catch (error) { console.log(error); setErrorMessage("Something went wrong. Please try again."); + // Optionally remove the optimistic entry or show error in it } finally { setIsLoading(false); } }, [inputValue, history]); + const handleEditSubmit = async (index) => { + if (editValue.trim() === "") return; + + setIsLoading(true); + setErrorMessage(""); + setEditingIndex(null); // Exit edit mode + setIsResponseComplete(false); + setCurrentIndex(index); // Set as current for typing effect + + const requestData = { prompt: editValue }; + + try { + const { + data: { response, citations }, + } = await axios.post( + "https://jurissmart-backend-60a68e25334a.herokuapp.com/generate", + requestData + ); + + setHistory((prevHistory) => { + const newHistory = [...prevHistory]; + const entry = newHistory[index]; + + // Add new version + entry.versions.push({ + question: editValue, + response, + citations, + feedback: null + }); + + // Switch to new version + entry.currentVersion = entry.versions.length - 1; + + return newHistory; + }); + + } catch (error) { + console.log(error); + setErrorMessage("Something went wrong with the edit. Please try again."); + } finally { + setIsLoading(false); + } + }; + const handleFeedback = async (index, rating) => { const entry = history[index]; + const currentVer = entry.versions[entry.currentVersion]; // Optimistically update UI const newHistory = [...history]; - newHistory[index].feedback = rating; + newHistory[index].versions[entry.currentVersion].feedback = rating; setHistory(newHistory); try { await axios.post("https://jurissmart-backend-60a68e25334a.herokuapp.com/feedback", { - prompt: entry.question, - response: entry.response, + prompt: currentVer.question, + response: currentVer.response, rating }); } catch (error) { console.error("Error sending feedback:", error); - // Optionally revert UI on error, but for feedback it's usually fine to keep it } }; + const handleCopy = (text, id) => { + navigator.clipboard.writeText(text); + setCopiedIndex(id); + setTimeout(() => setCopiedIndex(null), 2000); + }; + + const startEditing = (index, text) => { + setEditingIndex(index); + setEditValue(text); + // Focus will be handled by useEffect or autoFocus + }; + + const cancelEditing = () => { + setEditingIndex(null); + setEditValue(""); + }; + + const switchVersion = (index, direction) => { + setHistory(prev => { + const newHistory = [...prev]; + const entry = newHistory[index]; + const newVersion = entry.currentVersion + direction; + + if (newVersion >= 0 && newVersion < entry.versions.length) { + entry.currentVersion = newVersion; + // If we switch versions, we don't want to re-type the response unless it was the one just generated + if (index === currentIndex) { + setIsResponseComplete(true); + } + } + return newHistory; + }); + }; + useEffect(() => { if (textareaRef.current) { textareaRef.current.style.height = "auto"; @@ -93,11 +198,19 @@ const Chat = () => { } }, [inputValue]); + useEffect(() => { + if (editTextareaRef.current) { + editTextareaRef.current.style.height = "auto"; + editTextareaRef.current.style.height = `${editTextareaRef.current.scrollHeight}px`; + editTextareaRef.current.focus(); + } + }, [editingIndex]); + useEffect(() => { if (responseContainerRef.current) { responseContainerRef.current.scrollTop = responseContainerRef.current.scrollHeight; } - }, [history, isLoading, isResponseComplete]); + }, [history, isLoading, isResponseComplete, editingIndex]); const handleInputChange = (e) => { setInputValue(e.target.value); @@ -105,8 +218,7 @@ const Chat = () => { }; const handlePredefinedQuestionClick = (question) => { - setInputValue(question); - setErrorMessage(""); + handleSubmit(question); }; const handleKeyDown = (e) => { @@ -116,6 +228,13 @@ const Chat = () => { } }; + const handleEditKeyDown = (e, index) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleEditSubmit(index); + } + }; + // Process response to replace [docN] with [N] and map citations uniquely const processResponse = (text, citations) => { if (!text) return { processedText: "", usedCitations: [] }; @@ -130,8 +249,6 @@ const Chat = () => { // First pass: Identify unique citations and assign numbers matches.forEach((match) => { const docId = parseInt(match[1]); - // Assuming citations are 0-indexed in the array, but docId is 1-based from backend logic usually. - // Let's stick to the previous assumption: citations array corresponds to doc1, doc2... const citation = citations[docId - 1]; if (citation) { @@ -166,28 +283,28 @@ const Chat = () => { }; return ( -
+
{history.length === 0 ? (
- +
-
-

+
+

Legal Clarity in Seconds

-

+

Get precise answers about IP law from trusted legal texts across Nigeria, the US, and the UK.

-
+
{[ "How do I register a trademark in USA?", "How do I register an Industrial design in Nigeria?", @@ -198,12 +315,12 @@ const Chat = () => { whileHover={{ scale: 1.02, translateY: -2 }} whileTap={{ scale: 0.98 }} onClick={() => handlePredefinedQuestionClick(q)} - className="p-6 text-left bg-slate-900/50 border border-slate-800 hover:border-gold-500/50 rounded-2xl transition-all group" + className="p-3 md:p-6 text-left bg-slate-900/50 border border-slate-800 hover:border-gold-500/50 rounded-xl md:rounded-2xl transition-all group flex md:block items-center justify-between" > -

+

{q}

- + ))}
@@ -214,10 +331,12 @@ const Chat = () => { className="flex-1 overflow-y-auto space-y-8 pr-2 scrollbar-thin scrollbar-thumb-slate-800 scrollbar-track-transparent" > {history.map((entry, index) => { - const { processedText, usedCitations } = processResponse(entry.response, entry.citations || []); + const currentVer = entry.versions[entry.currentVersion]; + const { processedText, usedCitations } = processResponse(currentVer.response, currentVer.citations || []); const isCurrent = index === currentIndex; // Show references if it's a past message OR if it's current and typing is complete const showReferences = !isCurrent || isResponseComplete; + const isEditing = editingIndex === index; return ( { className="space-y-6" > {/* User Question */} -
-
-
-

{entry.question}

+
+
+
+ + {isEditing ? ( +
+