diff --git a/frontend/package.json b/frontend/package.json
index 2b669731a3e..8aa4dcfa550 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -63,4 +63,4 @@
"tailwindcss": "^3.3.1",
"vite": "^4.3.0"
}
-}
+}
\ No newline at end of file
diff --git a/frontend/src/components/ChatBubble/index.jsx b/frontend/src/components/ChatBubble/index.jsx
index 8d3118838b7..c5a1f19076f 100644
--- a/frontend/src/components/ChatBubble/index.jsx
+++ b/frontend/src/components/ChatBubble/index.jsx
@@ -1,5 +1,5 @@
import React from "react";
-import Jazzicon from "../UserIcon";
+import UserIcon from "../UserIcon";
import { userFromStorage } from "@/utils/request";
import { AI_BACKGROUND_COLOR, USER_BACKGROUND_COLOR } from "@/utils/constants";
@@ -11,8 +11,7 @@ export default function ChatBubble({ message, type, popMsg }) {
-
diff --git a/frontend/src/components/DefaultChat/index.jsx b/frontend/src/components/DefaultChat/index.jsx
index 43ae6e7a6e4..ae52a0d2bc0 100644
--- a/frontend/src/components/DefaultChat/index.jsx
+++ b/frontend/src/components/DefaultChat/index.jsx
@@ -13,7 +13,7 @@ import { isMobile } from "react-device-detect";
import { SidebarMobileHeader } from "../Sidebar";
import ChatBubble from "../ChatBubble";
import System from "@/models/system";
-import Jazzicon from "../UserIcon";
+import UserIcon from "../UserIcon";
import { userFromStorage } from "@/utils/request";
import { AI_BACKGROUND_COLOR, USER_BACKGROUND_COLOR } from "@/utils/constants";
import useUser from "@/hooks/useUser";
@@ -46,7 +46,7 @@ export default function DefaultChatContainer() {
className={`pt-10 pb-6 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col`}
>
-
+
-
+
-
+
-
@@ -151,7 +150,7 @@ export default function DefaultChatContainer() {
className={`py-6 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col`}
>
-
+
-
@@ -213,7 +211,7 @@ export default function DefaultChatContainer() {
className={`py-6 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col`}
>
-
+
-
@@ -275,7 +272,7 @@ export default function DefaultChatContainer() {
className={`py-6 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col`}
>
-
+
!prev);
+ }
+
+ useEffect(() => {
+ function listenForEdits() {
+ if (!chatId || !role) return;
+ window.addEventListener(EDIT_EVENT, onEditEvent);
+ }
+ listenForEdits();
+ return () => {
+ window.removeEventListener(EDIT_EVENT, onEditEvent);
+ };
+ }, [chatId, role]);
+
+ return { isEditing, setIsEditing };
+}
+
+export function EditMessageAction({ chatId = null, role, isEditing }) {
+ function handleEditClick() {
+ window.dispatchEvent(
+ new CustomEvent(EDIT_EVENT, { detail: { chatId, role } })
+ );
+ }
+
+ if (!chatId || isEditing) return null;
+ return (
+
+ );
+}
+
+export function EditMessageForm({
+ role,
+ chatId,
+ message,
+ adjustTextArea,
+ saveChanges,
+}) {
+ const formRef = useRef(null);
+ function handleSaveMessage(e) {
+ e.preventDefault();
+ const form = new FormData(e.target);
+ const editedMessage = form.get("editedMessage");
+ saveChanges({ editedMessage, chatId, role });
+ window.dispatchEvent(
+ new CustomEvent(EDIT_EVENT, { detail: { chatId, role } })
+ );
+ }
+
+ function cancelEdits() {
+ window.dispatchEvent(
+ new CustomEvent(EDIT_EVENT, { detail: { chatId, role } })
+ );
+ return false;
+ }
+
+ useEffect(() => {
+ if (!formRef || !formRef.current) return;
+ formRef.current.focus();
+ adjustTextArea({ target: formRef.current });
+ }, [formRef]);
+
+ return (
+
+ );
+}
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 41fd7067b8a..85590e7f310 100644
--- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/index.jsx
+++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/index.jsx
@@ -2,14 +2,15 @@ import React, { memo, useState } from "react";
import useCopyText from "@/hooks/useCopyText";
import {
Check,
- ClipboardText,
ThumbsUp,
ThumbsDown,
ArrowsClockwise,
+ Copy,
} from "@phosphor-icons/react";
import { Tooltip } from "react-tooltip";
import Workspace from "@/models/workspace";
import TTSMessage from "./TTSButton";
+import { EditMessageAction } from "./EditMessage";
const Actions = ({
message,
@@ -18,9 +19,10 @@ const Actions = ({
slug,
isLastMessage,
regenerateMessage,
+ isEditing,
+ role,
}) => {
const [selectedFeedback, setSelectedFeedback] = useState(feedbackScore);
-
const handleFeedback = async (newFeedback) => {
const updatedFeedback =
selectedFeedback === newFeedback ? null : newFeedback;
@@ -32,14 +34,15 @@ const Actions = ({
- {isLastMessage && (
+
+ {isLastMessage && !isEditing && (
)}
- {chatId && (
+ {chatId && role !== "user" && !isEditing && (
<>
) : (
-
+
)}
{
+ 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.
+
+
+ {error}
+
+
+
+
+
+ );
+ }
+
return (
- {error ? (
-
-
- Could not
- respond to message.
-
-
- {error}
-
-
+ {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 6e9f4e779b6..19b65453a41 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 494ee57d9bf..28d87e0dfa5 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 039ee186832..a73006c99ec 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 c5730dbe0e9..a57b11e2127 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 e2aead974d4..1c207e5230f 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 2657eb976ec..6d6f29bbd51 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 c81992caadb..bda40064d5b 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 d07eae308e9..609b18190fe 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",