From cd9c1d6c744e41bd8333c6ce197b70b7fb39632d Mon Sep 17 00:00:00 2001 From: Markson17 Date: Sat, 22 Nov 2025 18:18:45 +0100 Subject: [PATCH 1/2] Merge origin/main into frontend --- front-end/src/components/Chat.jsx | 224 +++++++++++++++++++++++++++--- 1 file changed, 203 insertions(+), 21 deletions(-) diff --git a/front-end/src/components/Chat.jsx b/front-end/src/components/Chat.jsx index e2ad6a1..20bcd89 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 @@ -53,7 +59,10 @@ const Chat = () => { setHistory((prevHistory) => [ ...prevHistory, - { question: inputValue, response, citations, feedback: null }, + { + versions: [{ question: inputValue, response, citations, feedback: null }], + currentVersion: 0 + }, ]); setInputValue(""); @@ -66,26 +75,105 @@ const Chat = () => { } }, [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 +181,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); @@ -116,6 +212,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 +233,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) { @@ -214,10 +315,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 ? ( +
+