From 734c5a9e964bf7d7c839b9be5878d9291a48c7b5 Mon Sep 17 00:00:00 2001 From: Sean Hatfield <seanhatfield5@gmail.com> Date: Fri, 10 May 2024 14:47:29 -0700 Subject: [PATCH] [FEAT] Implement regenerate response button (#1341) * implement regenerate response button * wip on rerenders * remove function that was duplicate * update delete-chats function --------- Co-authored-by: timothycarambat <rambat1010@gmail.com> --- .../HistoricalMessage/Actions/index.jsx | 40 ++++++++++++++- .../ChatHistory/HistoricalMessage/index.jsx | 4 ++ .../ChatContainer/ChatHistory/index.jsx | 9 +++- .../WorkspaceChat/ChatContainer/index.jsx | 50 ++++++++++++++----- frontend/src/models/workspace.js | 16 ++++++ server/endpoints/workspaces.js | 31 ++++++++++++ 6 files changed, 135 insertions(+), 15 deletions(-) diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/index.jsx index 23914963f..b7e540cb6 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/index.jsx @@ -5,11 +5,19 @@ import { ClipboardText, ThumbsUp, ThumbsDown, + ArrowsClockwise, } from "@phosphor-icons/react"; import { Tooltip } from "react-tooltip"; import Workspace from "@/models/workspace"; -const Actions = ({ message, feedbackScore, chatId, slug }) => { +const Actions = ({ + message, + feedbackScore, + chatId, + slug, + isLastMessage, + regenerateMessage, +}) => { const [selectedFeedback, setSelectedFeedback] = useState(feedbackScore); const handleFeedback = async (newFeedback) => { @@ -22,6 +30,14 @@ const Actions = ({ message, feedbackScore, chatId, slug }) => { return ( <div className="flex justify-start items-center gap-x-4"> <CopyMessage message={message} /> + {isLastMessage && + !message?.includes("Workspace chat memory was reset!") && ( + <RegenerateMessage + regenerateMessage={regenerateMessage} + slug={slug} + chatId={chatId} + /> + )} {chatId && ( <> <FeedbackButton @@ -106,4 +122,26 @@ function CopyMessage({ message }) { ); } +function RegenerateMessage({ regenerateMessage, chatId }) { + return ( + <div className="mt-3 relative"> + <button + onClick={() => regenerateMessage(chatId)} + data-tooltip-id="regenerate-assistant-text" + data-tooltip-content="Regenerate response" + className="border-none text-zinc-300" + aria-label="Regenerate" + > + <ArrowsClockwise size={18} className="mb-1" weight="fill" /> + </button> + <Tooltip + id="regenerate-assistant-text" + place="bottom" + delayShow={300} + className="tooltip !text-xs" + /> + </div> + ); +} + export default memo(Actions); diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/index.jsx index 0371d64e5..5f4e6c672 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/index.jsx @@ -19,6 +19,8 @@ const HistoricalMessage = ({ error = false, feedbackScore = null, chatId = null, + isLastMessage = false, + regenerateMessage, }) => { return ( <div @@ -59,6 +61,8 @@ const HistoricalMessage = ({ feedbackScore={feedbackScore} chatId={chatId} slug={workspace?.slug} + isLastMessage={isLastMessage} + regenerateMessage={regenerateMessage} /> </div> )} diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx index c0eb5bf4c..3c2c47a05 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx @@ -8,7 +8,12 @@ import debounce from "lodash.debounce"; import useUser from "@/hooks/useUser"; import Chartable from "./Chartable"; -export default function ChatHistory({ history = [], workspace, sendCommand }) { +export default function ChatHistory({ + history = [], + workspace, + sendCommand, + regenerateAssistantMessage, +}) { const { user } = useUser(); const { showing, showModal, hideModal } = useManageWorkspaceModal(); const [isAtBottom, setIsAtBottom] = useState(true); @@ -165,6 +170,8 @@ export default function ChatHistory({ history = [], workspace, sendCommand }) { feedbackScore={props.feedbackScore} chatId={props.chatId} error={props.error} + regenerateMessage={regenerateAssistantMessage} + isLastMessage={isLastBotReply} /> ); })} diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx index b3cc0d942..494ee57d9 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx @@ -26,7 +26,7 @@ export default function ChatContainer({ workspace, knownHistory = [] }) { setMessage(event.target.value); }; - // Emit an update to the sate of the prompt input without directly + // Emit an update to the state of the prompt input without directly // passing a prop in so that it does not re-render constantly. function setMessageEmit(messageContent = "") { setMessage(messageContent); @@ -56,24 +56,47 @@ export default function ChatContainer({ workspace, knownHistory = [] }) { setLoadingResponse(true); }; - const sendCommand = async (command, submit = false) => { + const regenerateAssistantMessage = (chatId) => { + const updatedHistory = chatHistory.slice(0, -1); + const lastUserMessage = updatedHistory.slice(-1)[0]; + Workspace.deleteChats(workspace.slug, [chatId]) + .then(() => sendCommand(lastUserMessage.content, true, updatedHistory)) + .catch((e) => console.error(e)); + }; + + const sendCommand = async (command, submit = false, history = []) => { if (!command || command === "") return false; if (!submit) { setMessageEmit(command); return; } - const prevChatHistory = [ - ...chatHistory, - { content: command, role: "user" }, - { - content: "", - role: "assistant", - pending: true, - userMessage: command, - animate: true, - }, - ]; + let prevChatHistory; + if (history.length > 0) { + // use pre-determined history chain. + prevChatHistory = [ + ...history, + { + content: "", + role: "assistant", + pending: true, + userMessage: command, + animate: true, + }, + ]; + } else { + prevChatHistory = [ + ...chatHistory, + { content: command, role: "user" }, + { + content: "", + role: "assistant", + pending: true, + userMessage: command, + animate: true, + }, + ]; + } setChatHistory(prevChatHistory); setMessageEmit(""); @@ -217,6 +240,7 @@ export default function ChatContainer({ workspace, knownHistory = [] }) { history={chatHistory} workspace={workspace} sendCommand={sendCommand} + regenerateAssistantMessage={regenerateAssistantMessage} /> <PromptInput submit={handleSubmit} diff --git a/frontend/src/models/workspace.js b/frontend/src/models/workspace.js index 7bc95ef8c..91f4a2db3 100644 --- a/frontend/src/models/workspace.js +++ b/frontend/src/models/workspace.js @@ -74,6 +74,22 @@ const Workspace = { .catch(() => false); return result; }, + + deleteChats: async function (slug = "", chatIds = []) { + return await fetch(`${API_BASE}/workspace/${slug}/delete-chats`, { + method: "DELETE", + headers: baseHeaders(), + body: JSON.stringify({ chatIds }), + }) + .then((res) => { + if (res.ok) return true; + throw new Error("Failed to delete chats."); + }) + .catch((e) => { + console.log(e); + return false; + }); + }, streamChat: async function ({ slug }, message, handleChat) { const ctrl = new AbortController(); diff --git a/server/endpoints/workspaces.js b/server/endpoints/workspaces.js index e9df2613c..f85c213fc 100644 --- a/server/endpoints/workspaces.js +++ b/server/endpoints/workspaces.js @@ -372,6 +372,37 @@ function workspaceEndpoints(app) { } ); + app.delete( + "/workspace/:slug/delete-chats", + [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug], + async (request, response) => { + try { + const { chatIds = [] } = reqBody(request); + const user = await userFromSession(request, response); + const workspace = response.locals.workspace; + + if (!workspace || !Array.isArray(chatIds)) { + response.sendStatus(400).end(); + return; + } + + // This works for both workspace and threads. + // we simplify this by just looking at workspace<>user overlap + // since they are all on the same table. + await WorkspaceChats.delete({ + id: { in: chatIds.map((id) => Number(id)) }, + user_id: user?.id ?? null, + workspaceId: workspace.id, + }); + + response.sendStatus(200).end(); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + } + ); + app.post( "/workspace/:slug/chat-feedback/:chatId", [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],