From 734c5a9e964bf7d7c839b9be5878d9291a48c7b5 Mon Sep 17 00:00:00 2001 From: Sean Hatfield 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 --- .../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 23914963..b7e540cb 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 (
+ {isLastMessage && + !message?.includes("Workspace chat memory was reset!") && ( + + )} {chatId && ( <> + + +
+ ); +} + 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 0371d64e..5f4e6c67 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 (
)} diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx index c0eb5bf4..3c2c47a0 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 b3cc0d94..494ee57d 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} /> 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 e9df2613..f85c213f 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],