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 fd03e6a5..0e2dba84 100644
--- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/index.jsx
+++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/index.jsx
@@ -6,6 +6,7 @@ import {
ThumbsUp,
ThumbsDown,
ArrowsClockwise,
+ Copy,
} from "@phosphor-icons/react";
import { Tooltip } from "react-tooltip";
import Workspace from "@/models/workspace";
@@ -19,6 +20,7 @@ const Actions = ({
slug,
isLastMessage,
regenerateMessage,
+ isEditing,
role,
}) => {
const [selectedFeedback, setSelectedFeedback] = useState(feedbackScore);
@@ -33,15 +35,15 @@ const Actions = ({
-
- {isLastMessage && (
+
+ {isLastMessage && !isEditing && (
)}
- {chatId && role !== "user" && (
+ {chatId && role !== "user" && !isEditing && (
<>
) : (
-
+
)}
@@ -93,6 +93,7 @@ const HistoricalMessage = ({
slug={workspace?.slug}
isLastMessage={isLastMessage}
regenerateMessage={regenerateMessage}
+ isEditing={isEditing}
role={role}
/>
@@ -116,8 +117,7 @@ function ProfileImage({ role, workspace }) {
}
return (
-
;
+ return
;
}
export default memo(PromptReply);
diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx
index a9e28ce4..5e797367 100644
--- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx
+++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx
@@ -8,14 +8,17 @@ import debounce from "lodash.debounce";
import useUser from "@/hooks/useUser";
import Chartable from "./Chartable";
import Workspace from "@/models/workspace";
+import { useParams } from "react-router-dom";
export default function ChatHistory({
history = [],
workspace,
sendCommand,
+ updateHistory,
regenerateAssistantMessage,
}) {
const { user } = useUser();
+ const { threadSlug = null } = useParams();
const { showing, showModal, hideModal } = useManageWorkspaceModal();
const [isAtBottom, setIsAtBottom] = useState(true);
const chatHistoryRef = useRef(null);
@@ -89,8 +92,6 @@ export default function ChatHistory({
};
// TODO: Be able to edit both user and system response message.
- // TODO: Pencil does not appear under user message while chatting because it
- // does not know its own chatId since not response is present.
const saveEditedMessage = async ({ editedMessage, chatId, role }) => {
if (!editedMessage) return; // Don't save empty edits.
@@ -107,13 +108,27 @@ export default function ChatHistory({
// update last message in history to edited message
updatedHistory[updatedHistory.length - 1].content = editedMessage;
// remove all edited messages after the edited message in backend
- await Workspace.deleteEditedChats(workspace.slug, chatId);
+ await Workspace.deleteEditedChats(workspace.slug, threadSlug, chatId);
sendCommand(editedMessage, true, updatedHistory);
return;
}
// If role is an assistant we simply want to update the comment and save on the backend as an edit.
if (role === "assistant") {
+ const updatedHistory = [...history];
+ const targetIdx = history.findIndex(
+ (msg) => msg.chatId === chatId && msg.role === role
+ );
+ if (targetIdx < 0) return;
+ updatedHistory[targetIdx].content = editedMessage;
+ updateHistory(updatedHistory);
+ await Workspace.updateChatResponse(
+ workspace.slug,
+ threadSlug,
+ chatId,
+ editedMessage
+ );
+ return;
}
};
diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx
index 494ee57d..28d87e0d 100644
--- a/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx
+++ b/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx
@@ -240,6 +240,7 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
history={chatHistory}
workspace={workspace}
sendCommand={sendCommand}
+ updateHistory={setChatHistory}
regenerateAssistantMessage={regenerateAssistantMessage}
/>
{
- if (res.ok) return true;
- throw new Error("Failed to delete chats.");
- })
- .catch((e) => {
- console.log(e);
- return false;
- });
+ deleteEditedChats: async function (slug = "", threadSlug = "", startingId) {
+ if (!!threadSlug)
+ return this.threads._deleteEditedChats(slug, threadSlug, startingId);
+ return this._deleteEditedChats(slug, startingId);
+ },
+ updateChatResponse: async function (
+ slug = "",
+ threadSlug = "",
+ chatId,
+ newText
+ ) {
+ if (!!threadSlug)
+ return this.threads._updateChatResponse(
+ slug,
+ threadSlug,
+ chatId,
+ newText
+ );
+ return this._updateChatResponse(slug, chatId, newText);
},
streamChat: async function ({ slug }, message, handleChat) {
const ctrl = new AbortController();
@@ -304,8 +307,6 @@ const Workspace = {
return null;
});
},
- threads: WorkspaceThread,
-
uploadPfp: async function (formData, slug) {
return await fetch(`${API_BASE}/workspace/${slug}/upload-pfp`, {
method: "POST",
@@ -353,6 +354,37 @@ const Workspace = {
return { success: false, error: e.message };
});
},
+ _updateChatResponse: async function (slug = "", chatId, newText) {
+ return await fetch(`${API_BASE}/workspace/${slug}/update-chat`, {
+ method: "POST",
+ headers: baseHeaders(),
+ body: JSON.stringify({ chatId, newText }),
+ })
+ .then((res) => {
+ if (res.ok) return true;
+ throw new Error("Failed to update chat.");
+ })
+ .catch((e) => {
+ console.log(e);
+ return false;
+ });
+ },
+ _deleteEditedChats: async function (slug = "", startingId) {
+ return await fetch(`${API_BASE}/workspace/${slug}/delete-edited-chats`, {
+ method: "DELETE",
+ headers: baseHeaders(),
+ body: JSON.stringify({ startingId }),
+ })
+ .then((res) => {
+ if (res.ok) return true;
+ throw new Error("Failed to delete chats.");
+ })
+ .catch((e) => {
+ console.log(e);
+ return false;
+ });
+ },
+ threads: WorkspaceThread,
};
export default Workspace;
diff --git a/frontend/src/models/workspaceThread.js b/frontend/src/models/workspaceThread.js
index 039ee186..a73006c9 100644
--- a/frontend/src/models/workspaceThread.js
+++ b/frontend/src/models/workspaceThread.js
@@ -163,6 +163,51 @@ const WorkspaceThread = {
}
);
},
+ _deleteEditedChats: async function (
+ workspaceSlug = "",
+ threadSlug = "",
+ startingId
+ ) {
+ return await fetch(
+ `${API_BASE}/workspace/${workspaceSlug}/thread/${threadSlug}/delete-edited-chats`,
+ {
+ method: "DELETE",
+ headers: baseHeaders(),
+ body: JSON.stringify({ startingId }),
+ }
+ )
+ .then((res) => {
+ if (res.ok) return true;
+ throw new Error("Failed to delete chats.");
+ })
+ .catch((e) => {
+ console.log(e);
+ return false;
+ });
+ },
+ _updateChatResponse: async function (
+ workspaceSlug = "",
+ threadSlug = "",
+ chatId,
+ newText
+ ) {
+ return await fetch(
+ `${API_BASE}/workspace/${workspaceSlug}/thread/${threadSlug}/update-chat`,
+ {
+ method: "POST",
+ headers: baseHeaders(),
+ body: JSON.stringify({ chatId, newText }),
+ }
+ )
+ .then((res) => {
+ if (res.ok) return true;
+ throw new Error("Failed to update chat.");
+ })
+ .catch((e) => {
+ console.log(e);
+ return false;
+ });
+ },
};
export default WorkspaceThread;
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index 93bdc088..911af51e 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -487,14 +487,6 @@
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
-"@metamask/jazzicon@^2.0.0":
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/@metamask/jazzicon/-/jazzicon-2.0.0.tgz#5615528e91c0fc5c9d79202d1f0954a7922525a0"
- integrity sha512-7M+WSZWKcQAo0LEhErKf1z+D3YX0tEDAcGvcKbDyvDg34uvgeKR00mFNIYwAhdAS9t8YXxhxZgsrRBBg6X8UQg==
- dependencies:
- color "^0.11.3"
- mersenne-twister "^1.1.0"
-
"@microsoft/fetch-event-source@^2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz#9ceecc94b49fbaa15666e38ae8587f64acce007d"
@@ -1025,11 +1017,6 @@ cliui@^8.0.1:
strip-ansi "^6.0.1"
wrap-ansi "^7.0.0"
-clone@^1.0.2:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
- integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==
-
clsx@^1.1.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
@@ -1040,7 +1027,7 @@ clsx@^2.0.0:
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.0.tgz#e851283bcb5c80ee7608db18487433f7b23f77cb"
integrity sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==
-color-convert@^1.3.0, color-convert@^1.9.0:
+color-convert@^1.9.0:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
@@ -1059,27 +1046,11 @@ color-name@1.1.3:
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==
-color-name@^1.0.0, color-name@~1.1.4:
+color-name@~1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
-color-string@^0.3.0:
- version "0.3.0"
- resolved "https://registry.yarnpkg.com/color-string/-/color-string-0.3.0.tgz#27d46fb67025c5c2fa25993bfbf579e47841b991"
- integrity sha512-sz29j1bmSDfoAxKIEU6zwoIZXN6BrFbAMIhfYCNyiZXBDuU/aiHlN84lp/xDzL2ubyFhLDobHIlU1X70XRrMDA==
- dependencies:
- color-name "^1.0.0"
-
-color@^0.11.3:
- version "0.11.4"
- resolved "https://registry.yarnpkg.com/color/-/color-0.11.4.tgz#6d7b5c74fb65e841cd48792ad1ed5e07b904d764"
- integrity sha512-Ajpjd8asqZ6EdxQeqGzU5WBhhTfJ/0cA4Wlbre7e5vXfmDSmda7Ov6jeKoru+b0vHcb1CqvuroTHp5zIWzhVMA==
- dependencies:
- clone "^1.0.2"
- color-convert "^1.3.0"
- color-string "^0.3.0"
-
commander@^4.0.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068"
@@ -2401,11 +2372,6 @@ merge2@^1.3.0:
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
-mersenne-twister@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/mersenne-twister/-/mersenne-twister-1.1.0.tgz#f916618ee43d7179efcf641bec4531eb9670978a"
- integrity sha512-mUYWsMKNrm4lfygPkL3OfGzOPTR2DBlTkBNHM//F6hGp8cLThY897crAlk3/Jo17LEOOjQUrNAx6DvgO77QJkA==
-
micromatch@^4.0.4, micromatch@^4.0.5:
version "4.0.5"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
diff --git a/server/endpoints/workspaceThreads.js b/server/endpoints/workspaceThreads.js
index e2aead97..1c207e52 100644
--- a/server/endpoints/workspaceThreads.js
+++ b/server/endpoints/workspaceThreads.js
@@ -1,4 +1,9 @@
-const { multiUserMode, userFromSession, reqBody } = require("../utils/http");
+const {
+ multiUserMode,
+ userFromSession,
+ reqBody,
+ safeJsonParse,
+} = require("../utils/http");
const { validatedRequest } = require("../utils/middleware/validatedRequest");
const { Telemetry } = require("../models/telemetry");
const {
@@ -168,6 +173,77 @@ function workspaceThreadEndpoints(app) {
}
}
);
+
+ app.delete(
+ "/workspace/:slug/thread/:threadSlug/delete-edited-chats",
+ [
+ validatedRequest,
+ flexUserRoleValid([ROLES.all]),
+ validWorkspaceAndThreadSlug,
+ ],
+ async (request, response) => {
+ try {
+ const { startingId } = reqBody(request);
+ const user = await userFromSession(request, response);
+ const workspace = response.locals.workspace;
+ const thread = response.locals.thread;
+
+ await WorkspaceChats.delete({
+ workspaceId: Number(workspace.id),
+ thread_id: Number(thread.id),
+ user_id: user?.id,
+ id: { gte: Number(startingId) },
+ });
+
+ response.sendStatus(200).end();
+ } catch (e) {
+ console.log(e.message, e);
+ response.sendStatus(500).end();
+ }
+ }
+ );
+
+ app.post(
+ "/workspace/:slug/thread/:threadSlug/update-chat",
+ [
+ validatedRequest,
+ flexUserRoleValid([ROLES.all]),
+ validWorkspaceAndThreadSlug,
+ ],
+ async (request, response) => {
+ try {
+ const { chatId, newText = null } = reqBody(request);
+ if (!newText || !String(newText).trim())
+ throw new Error("Cannot save empty response");
+
+ const user = await userFromSession(request, response);
+ const workspace = response.locals.workspace;
+ const thread = response.locals.thread;
+ const existingChat = await WorkspaceChats.get({
+ workspaceId: workspace.id,
+ thread_id: thread.id,
+ user_id: user?.id,
+ id: Number(chatId),
+ });
+ if (!existingChat) throw new Error("Invalid chat.");
+
+ const chatResponse = safeJsonParse(existingChat.response, null);
+ if (!chatResponse) throw new Error("Failed to parse chat response");
+
+ await WorkspaceChats._update(existingChat.id, {
+ response: JSON.stringify({
+ ...chatResponse,
+ text: String(newText),
+ }),
+ });
+
+ response.sendStatus(200).end();
+ } catch (e) {
+ console.log(e.message, e);
+ response.sendStatus(500).end();
+ }
+ }
+ );
}
module.exports = { workspaceThreadEndpoints };
diff --git a/server/endpoints/workspaces.js b/server/endpoints/workspaces.js
index 856641d6..6d6f29bb 100644
--- a/server/endpoints/workspaces.js
+++ b/server/endpoints/workspaces.js
@@ -377,14 +377,9 @@ function workspaceEndpoints(app) {
return;
}
- console.log("in here");
-
const history = multiUserMode(response)
? await WorkspaceChats.forWorkspaceByUser(workspace.id, user.id)
: await WorkspaceChats.forWorkspace(workspace.id);
-
- console.log(history);
-
response.status(200).json({ history: convertToChatHistory(history) });
} catch (e) {
console.log(e.message, e);
@@ -430,15 +425,13 @@ function workspaceEndpoints(app) {
async (request, response) => {
try {
const { startingId } = reqBody(request);
+ const user = await userFromSession(request, response);
const workspace = response.locals.workspace;
- if (!workspace) {
- response.sendStatus(400).end();
- return;
- }
-
await WorkspaceChats.delete({
workspaceId: workspace.id,
+ thread_id: null,
+ user_id: user?.id,
id: { gte: Number(startingId) },
});
@@ -450,6 +443,43 @@ function workspaceEndpoints(app) {
}
);
+ app.post(
+ "/workspace/:slug/update-chat",
+ [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],
+ async (request, response) => {
+ try {
+ const { chatId, newText = null } = reqBody(request);
+ if (!newText || !String(newText).trim())
+ throw new Error("Cannot save empty response");
+
+ const user = await userFromSession(request, response);
+ const workspace = response.locals.workspace;
+ const existingChat = await WorkspaceChats.get({
+ workspaceId: workspace.id,
+ thread_id: null,
+ user_id: user?.id,
+ id: Number(chatId),
+ });
+ if (!existingChat) throw new Error("Invalid chat.");
+
+ const chatResponse = safeJsonParse(existingChat.response, null);
+ if (!chatResponse) throw new Error("Failed to parse chat response");
+
+ await WorkspaceChats._update(existingChat.id, {
+ response: JSON.stringify({
+ ...chatResponse,
+ text: String(newText),
+ }),
+ });
+
+ 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],
diff --git a/server/models/workspaceChats.js b/server/models/workspaceChats.js
index c81992ca..bda40064 100644
--- a/server/models/workspaceChats.js
+++ b/server/models/workspaceChats.js
@@ -220,6 +220,24 @@ const WorkspaceChats = {
console.error(error.message);
}
},
+
+ // Explicit update of settings + key validations.
+ // Only use this method when directly setting a key value
+ // that takes no user input for the keys being modified.
+ _update: async function (id = null, data = {}) {
+ if (!id) throw new Error("No workspace chat id provided for update");
+
+ try {
+ await prisma.workspace_chats.update({
+ where: { id },
+ data,
+ });
+ return true;
+ } catch (error) {
+ console.error(error.message);
+ return false;
+ }
+ },
};
module.exports = { WorkspaceChats };
diff --git a/server/utils/helpers/chat/responses.js b/server/utils/helpers/chat/responses.js
index ac804eca..609b1819 100644
--- a/server/utils/helpers/chat/responses.js
+++ b/server/utils/helpers/chat/responses.js
@@ -168,8 +168,6 @@ function convertToChatHistory(history = []) {
const formattedHistory = [];
history.forEach((history) => {
const { prompt, response, createdAt, feedbackScore = null, id } = history;
-
- console.log("HISTORY", history);
const data = JSON.parse(response);
formattedHistory.push([
{