-
- {error ? (
+ const { isEditing } = useEditMessage({ chatId, role });
+ const adjustTextArea = (event) => {
+ const element = event.target;
+ element.style.height = "auto";
+ element.style.height = element.scrollHeight + "px";
+ };
+
+ if (!!error) {
+ return (
+
+
+
+
-
+
Could not
respond to message.
@@ -42,6 +51,30 @@ const HistoricalMessage = ({
{error}
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ {isEditing ? (
+
) : (
)}
- {role === "assistant" && !error && (
-
- )}
+
{role === "assistant" &&
}
@@ -84,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 6e9f4e77..19b65453 100644
--- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx
+++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx
@@ -7,14 +7,18 @@ import { ArrowDown } from "@phosphor-icons/react";
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);
@@ -87,6 +91,46 @@ export default function ChatHistory({
sendCommand(`${heading} ${message}`, true);
};
+ const saveEditedMessage = async ({ editedMessage, chatId, role }) => {
+ if (!editedMessage) return; // Don't save empty edits.
+
+ // if the edit was a user message, we will auto-regenerate the response and delete all
+ // messages post modified message
+ if (role === "user") {
+ // remove all messages after the edited message
+ // technically there are two chatIds per-message pair, this will split the first.
+ const updatedHistory = history.slice(
+ 0,
+ history.findIndex((msg) => msg.chatId === chatId) + 1
+ );
+
+ // 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, 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;
+ }
+ };
+
if (history.length === 0) {
return (
@@ -172,6 +216,7 @@ export default function ChatHistory({
error={props.error}
regenerateMessage={regenerateAssistantMessage}
isLastMessage={isLastBotReply}
+ saveEditedMessage={saveEditedMessage}
/>
);
})}
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 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/src/utils/chat/index.js b/frontend/src/utils/chat/index.js
index c5730dbe..a57b11e2 100644
--- a/frontend/src/utils/chat/index.js
+++ b/frontend/src/utils/chat/index.js
@@ -108,13 +108,10 @@ export default function handleChat(
} else if (type === "finalizeResponseStream") {
const chatIdx = _chatHistory.findIndex((chat) => chat.uuid === uuid);
if (chatIdx !== -1) {
- const existingHistory = { ..._chatHistory[chatIdx] };
- const updatedHistory = {
- ...existingHistory,
- chatId, // finalize response stream only has some specific keys for data. we are explicitly listing them here.
- };
- _chatHistory[chatIdx] = updatedHistory;
+ _chatHistory[chatIdx - 1] = { ..._chatHistory[chatIdx - 1], chatId }; // update prompt with chatID
+ _chatHistory[chatIdx] = { ..._chatHistory[chatIdx], chatId }; // update response with chatID
}
+
setChatHistory([..._chatHistory]);
setLoadingResponse(false);
} else if (type === "stopGeneration") {
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 2657eb97..6d6f29bb 100644
--- a/server/endpoints/workspaces.js
+++ b/server/endpoints/workspaces.js
@@ -380,7 +380,6 @@ function workspaceEndpoints(app) {
const history = multiUserMode(response)
? await WorkspaceChats.forWorkspaceByUser(workspace.id, user.id)
: await WorkspaceChats.forWorkspace(workspace.id);
-
response.status(200).json({ history: convertToChatHistory(history) });
} catch (e) {
console.log(e.message, e);
@@ -420,6 +419,67 @@ function workspaceEndpoints(app) {
}
);
+ app.delete(
+ "/workspace/:slug/delete-edited-chats",
+ [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug],
+ async (request, response) => {
+ try {
+ const { startingId } = reqBody(request);
+ const user = await userFromSession(request, response);
+ const workspace = response.locals.workspace;
+
+ await WorkspaceChats.delete({
+ workspaceId: workspace.id,
+ thread_id: null,
+ 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/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 d07eae30..609b1819 100644
--- a/server/utils/helpers/chat/responses.js
+++ b/server/utils/helpers/chat/responses.js
@@ -174,6 +174,7 @@ function convertToChatHistory(history = []) {
role: "user",
content: prompt,
sentAt: moment(createdAt).unix(),
+ chatId: id,
},
{
type: data?.type || "chart",