From 26c220503cbf22ec2b55fa588e1f795914beb89a Mon Sep 17 00:00:00 2001
From: Sean Hatfield <seanhatfield5@gmail.com>
Date: Thu, 6 Jun 2024 12:56:11 -0700
Subject: [PATCH] [FEAT] Edit message button (#1392)

* WIP edit message feature

* WIP edit message

* WIP editing messages feature

* Fix PFPs
TODO: Fix default user profile image
Add User and Assistant workspace response

* unset PFP changes for later PR

---------

Co-authored-by: timothycarambat <rambat1010@gmail.com>
---
 frontend/package.json                         |   2 +-
 frontend/src/components/ChatBubble/index.jsx  |   5 +-
 frontend/src/components/DefaultChat/index.jsx |  23 ++--
 frontend/src/components/UserIcon/index.jsx    |   2 +-
 .../src/components/UserIcon/workspace.png     | Bin 0 -> 1486 bytes
 .../Actions/EditMessage/index.jsx             | 126 ++++++++++++++++++
 .../HistoricalMessage/Actions/index.jsx       |  13 +-
 .../ChatHistory/HistoricalMessage/index.jsx   |  86 ++++++++----
 .../ChatHistory/PromptReply/index.jsx         |   4 +-
 .../ChatContainer/ChatHistory/index.jsx       |  45 +++++++
 .../WorkspaceChat/ChatContainer/index.jsx     |   1 +
 .../src/components/WorkspaceChat/index.jsx    |   1 +
 frontend/src/models/workspace.js              |  53 +++++++-
 frontend/src/models/workspaceThread.js        |  45 +++++++
 frontend/src/utils/chat/index.js              |   9 +-
 server/endpoints/workspaceThreads.js          |  78 ++++++++++-
 server/endpoints/workspaces.js                |  62 ++++++++-
 server/models/workspaceChats.js               |  18 +++
 server/utils/helpers/chat/responses.js        |   1 +
 19 files changed, 512 insertions(+), 62 deletions(-)
 create mode 100644 frontend/src/components/UserIcon/workspace.png
 create mode 100644 frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/EditMessage/index.jsx

diff --git a/frontend/package.json b/frontend/package.json
index 2b669731a..8aa4dcfa5 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 8d3118838..c5a1f1907 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 }) {
     <div className={`flex justify-center items-end w-full ${backgroundColor}`}>
       <div className={`py-8 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col`}>
         <div className="flex gap-x-5">
-          <Jazzicon
-            size={36}
+          <UserIcon
             user={{ uid: isUser ? userFromStorage()?.username : "system" }}
             role={type}
           />
diff --git a/frontend/src/components/DefaultChat/index.jsx b/frontend/src/components/DefaultChat/index.jsx
index 43ae6e7a6..ae52a0d2b 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`}
         >
           <div className="flex gap-x-5">
-            <Jazzicon size={36} user={{ uid: "system" }} role={"assistant"} />
+            <UserIcon user={{ uid: "system" }} role={"assistant"} />
 
             <span
               className={`whitespace-pre-line text-white font-normal text-sm md:text-sm flex flex-col gap-y-1 mt-2`}
@@ -70,7 +70,7 @@ export default function DefaultChatContainer() {
           className={`pb-4 pt-2 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col`}
         >
           <div className="flex gap-x-5">
-            <Jazzicon size={36} user={{ uid: "system" }} role={"assistant"} />
+            <UserIcon user={{ uid: "system" }} role={"assistant"} />
 
             <span
               className={`whitespace-pre-line text-white font-normal text-sm md:text-sm flex flex-col gap-y-1 mt-2`}
@@ -93,7 +93,7 @@ export default function DefaultChatContainer() {
           className={`pt-2 pb-6 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col`}
         >
           <div className="flex gap-x-5">
-            <Jazzicon size={36} user={{ uid: "system" }} role={"assistant"} />
+            <UserIcon user={{ uid: "system" }} role={"assistant"} />
             <div>
               <span
                 className={`whitespace-pre-line text-white font-normal text-sm md:text-sm flex flex-col gap-y-1 mt-2`}
@@ -127,8 +127,7 @@ export default function DefaultChatContainer() {
           className={`py-6 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col`}
         >
           <div className="flex gap-x-5">
-            <Jazzicon
-              size={36}
+            <UserIcon
               user={{ uid: userFromStorage()?.username }}
               role={"user"}
             />
@@ -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`}
         >
           <div className="flex gap-x-5">
-            <Jazzicon size={36} user={{ uid: "system" }} role={"assistant"} />
+            <UserIcon user={{ uid: "system" }} role={"assistant"} />
             <div>
               <span
                 className={`whitespace-pre-line text-white font-normal text-sm md:text-sm flex flex-col gap-y-1 mt-2`}
@@ -188,8 +187,7 @@ export default function DefaultChatContainer() {
           className={`py-6 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col`}
         >
           <div className="flex gap-x-5">
-            <Jazzicon
-              size={36}
+            <UserIcon
               user={{ uid: userFromStorage()?.username }}
               role={"user"}
             />
@@ -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`}
         >
           <div className="flex gap-x-5">
-            <Jazzicon size={36} user={{ uid: "system" }} role={"assistant"} />
+            <UserIcon user={{ uid: "system" }} role={"assistant"} />
 
             <span
               className={`whitespace-pre-line text-white font-normal text-sm md:text-sm flex flex-col gap-y-1 mt-2`}
@@ -251,8 +249,7 @@ export default function DefaultChatContainer() {
           className={`py-6 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col`}
         >
           <div className="flex gap-x-5">
-            <Jazzicon
-              size={36}
+            <UserIcon
               user={{ uid: userFromStorage()?.username }}
               role={"user"}
             />
@@ -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`}
         >
           <div className="flex gap-x-5">
-            <Jazzicon size={36} user={{ uid: "system" }} role={"assistant"} />
+            <UserIcon user={{ uid: "system" }} role={"assistant"} />
             <div>
               <span
                 className={`whitespace-pre-line text-white font-normal text-sm md:text-sm flex flex-col gap-y-1 mt-2`}
diff --git a/frontend/src/components/UserIcon/index.jsx b/frontend/src/components/UserIcon/index.jsx
index 6cc9b57d0..7fc6b8df6 100644
--- a/frontend/src/components/UserIcon/index.jsx
+++ b/frontend/src/components/UserIcon/index.jsx
@@ -2,7 +2,7 @@ import React, { useRef, useEffect } from "react";
 import JAZZ from "@metamask/jazzicon";
 import usePfp from "../../hooks/usePfp";
 
-export default function Jazzicon({ size = 10, user, role }) {
+export default function UserIcon({ size = 36, user, role }) {
   const { pfp } = usePfp();
   const divRef = useRef(null);
   const seed = user?.uid
diff --git a/frontend/src/components/UserIcon/workspace.png b/frontend/src/components/UserIcon/workspace.png
new file mode 100644
index 0000000000000000000000000000000000000000..537d583c5827cb6112b6d16e91bb28ef46c7ec8f
GIT binary patch
literal 1486
zcmV;<1u^=GP)<h;3K|Lk000e1NJLTq001Qb001Qj1^@s6#hxGo00009a7bBm000XU
z000XU0RWnu7ytkO0drDELIAGL9O(c600d`2O+f$vv5yP<VFdsH1!GA>K~#7F?N)tE
zl~ou&@8#YLmwUN_@*#LZmsoj`Vv#eGH3*rTTXS<mDqWK{TP`bWk+y7-rLJ0Ou0JAe
z*_yh=ooQ&vN66=Zh+3{u0U`zjT;zVkz3+KHPS5k+mtuIiLaYDov;Fp-kLNkh?|II1
z&Up!mNJJtMk%%l<xPPh3$p0uBa%Upi)YQ~i_{IUufJKO;q$Cr3^^7r&+#VeEbpWHP
zA3rV7Hd(D!{XBe!CWOYly}dhJE?2eN<7s!f-5u^A+U>V)*-A@Gt)LfMUS9sU1C?Y<
zZEbD7+wHl6b~Ax?Cr+GrNDV5AEKoEyo9(^;rOeOs3IaT~r@^y7hK3qIZzX0Di2)(B
z)15qdGS}z#yFp)8bOXG)M$m3#)cynL-35pyAxbD*O=+*x)^AzAew__FEltf$g21~t
zO_Y?Fl$f+;O?DoRf8W`8yr>}meQ4SN^4|UXGY;(A->B2+EgrYa(b3-4uGQ-)2N|@a
zXRKJ6x$1tuf6|_@Y}sRiAp8b4UNYYr&CSi#Rh~cp__))}5HPeIumO+*c(|##^@82$
zVt?AlebCPW;C94czd2YlZBwvuW0_W~EyCphYtNoL_YRKV93B~ie!kgkUK}bha~4}y
zR~HcknWKV8s371FE_4HGp%1>lm%sJqyW`{IKYe@on+oVlfSAsouN{mr#1^6crHdEO
zT&=5X6GgEb=6cX}N%B%B&QXCPktmPH6VkVqgb^VS;BXO46GY4#jYdBZ@k2LIUsKcl
zK=zt#JkO(pN!!IsTbCpz??wH_hWe_~oll*H@d3aD)Elj+sE}lilawGuiNSgV2y+V}
z1=~j`lrA645(?93G%`^R7=!gW9}o&aU-!nLBi5|!?Dvq6%i$Q@x&6sgNXTNbObiVT
z@jyzc8A*^B6@e5Lkr0cEiz8mIcmAYs`y}9lk)dEzPN!3e3<#kU+%j(W?%ioyw-wt&
zSvLFJo{86AEjy~$>-#CCV@OCnk!V9iNhHBcDS^&;$ii(OqreIyQ_sfi6T&4VB*d0H
zz3XF{5eqnDue`A5aBFj87tD>q9mi*7WpQc%1g;Gd2v80)FNhMBL&z+l%vtF9`T0bm
zl2YU$B11dF=eDm7>Z1)sJQYY?P4(H1wpJuGcIHCW=IuLoSt~0m4aLR9ijR?z5h4)S
zKFAa=DBCxe>$yom6dWZ|Fx>!%jDsVr0kUWled?^O#2mj^DeTskrmB4}?)d^74Z?V#
zC4I%|2lMjI<rNg%1H|yEz#Y->0YQWM!asEyQo^1-nNqc;7}-)%k{xX{MkSb+Osy*@
zNVF_pejMgFm&-9&_S~~r*!}RdIo)2D7q*ClOZMJpo`31>r5PE=Qqt1g{>e!l)YnQu
zEp%KIf@Gntg>W~z?zQqGMH>nWFV9eCL7QNCO-=e79~?e(puM@V3A%%snVEcFUthwp
zPd+PKwc7gf?XH4$unPlEY~Hv7n!f-%WLCX~dl0#nhVQ%^9xY5a7)@C+RI?<@oFq%|
zWdv@6sZ$?)c(k#ux&;XVd=7_0f@?th+V*8n&f0Zx=J<phiOEco&`u^I*b0<RU9GPE
z^xBV|osuLuz)o05rf?2_J<=Z8v?)iJq$b8Or3T@2GyCY6<9g4p1JE6YljiE~?xr}0
z4>SA)n1Eo)-K$rxN=!~ok!3k*+HP!Q*xBFPd&6Wh4f}jPXDCwS4y2+~VkriLAw4E0
zW;wJG=|~o^2;WFDBRKWf1<A*P<7CvGVV40&owieTLplyeO06NqiDuR~ilnwfz4DqF
owlhUm^<@%~h(shJ5&7@rFSk<l5#I9B$^ZZW07*qoM6N<$g1wrz`Tzg`

literal 0
HcmV?d00001

diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/EditMessage/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/EditMessage/index.jsx
new file mode 100644
index 000000000..f9346b26a
--- /dev/null
+++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/EditMessage/index.jsx
@@ -0,0 +1,126 @@
+import { AI_BACKGROUND_COLOR, USER_BACKGROUND_COLOR } from "@/utils/constants";
+import { Pencil } from "@phosphor-icons/react";
+import { useState, useEffect, useRef } from "react";
+import { Tooltip } from "react-tooltip";
+const EDIT_EVENT = "toggle-message-edit";
+
+export function useEditMessage({ chatId, role }) {
+  const [isEditing, setIsEditing] = useState(false);
+
+  function onEditEvent(e) {
+    if (e.detail.chatId !== chatId || e.detail.role !== role) {
+      setIsEditing(false);
+      return false;
+    }
+    setIsEditing((prev) => !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 (
+    <div
+      className={`mt-3 relative ${
+        role === "user" && !isEditing ? "opacity-0" : ""
+      } group-hover:opacity-100 transition-all duration-300`}
+    >
+      <button
+        onClick={handleEditClick}
+        data-tooltip-id="edit-input-text"
+        data-tooltip-content={`Edit ${
+          role === "user" ? "Prompt" : "Response"
+        } `}
+        className="border-none text-zinc-300"
+        aria-label={`Edit ${role === "user" ? "Prompt" : "Response"}`}
+      >
+        <Pencil size={18} className="mb-1" />
+      </button>
+      <Tooltip
+        id="edit-input-text"
+        place="bottom"
+        delayShow={300}
+        className="tooltip !text-xs"
+      />
+    </div>
+  );
+}
+
+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 (
+    <form onSubmit={handleSaveMessage} className="flex flex-col w-full">
+      <textarea
+        ref={formRef}
+        name="editedMessage"
+        className={`w-full rounded ${
+          role === "user" ? USER_BACKGROUND_COLOR : AI_BACKGROUND_COLOR
+        } border border-white/20 active:outline-none focus:outline-none focus:ring-0 pr-16 pl-1.5 pt-1.5 resize-y`}
+        defaultValue={message}
+        onChange={adjustTextArea}
+      />
+      <div className="mt-3 flex justify-center">
+        <button
+          type="submit"
+          className="px-2 py-1 bg-gray-200 text-gray-700 font-medium rounded-md mr-2 hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
+        >
+          Save & Submit
+        </button>
+        <button
+          type="button"
+          className="px-2 py-1 bg-historical-msg-system text-white font-medium rounded-md hover:bg-historical-msg-user/90 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2"
+          onClick={cancelEdits}
+        >
+          Cancel
+        </button>
+      </div>
+    </form>
+  );
+}
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 41fd7067b..85590e7f3 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 = ({
     <div className="flex w-full justify-between items-center">
       <div className="flex justify-start items-center gap-x-4">
         <CopyMessage message={message} />
-        {isLastMessage && (
+        <EditMessageAction chatId={chatId} role={role} isEditing={isEditing} />
+        {isLastMessage && !isEditing && (
           <RegenerateMessage
             regenerateMessage={regenerateMessage}
             slug={slug}
             chatId={chatId}
           />
         )}
-        {chatId && (
+        {chatId && role !== "user" && !isEditing && (
           <>
             <FeedbackButton
               isSelected={selectedFeedback === true}
@@ -111,7 +114,7 @@ function CopyMessage({ message }) {
           {copied ? (
             <Check size={18} className="mb-1" />
           ) : (
-            <ClipboardText size={18} className="mb-1" />
+            <Copy size={18} className="mb-1" />
           )}
         </button>
         <Tooltip
diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/index.jsx
index ae8f10b4a..1fdb61d45 100644
--- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/index.jsx
+++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/index.jsx
@@ -1,6 +1,6 @@
 import React, { memo } from "react";
 import { Warning } from "@phosphor-icons/react";
-import Jazzicon from "../../../../UserIcon";
+import UserIcon from "../../../../UserIcon";
 import Actions from "./Actions";
 import renderMarkdown from "@/utils/chat/markdown";
 import { userFromStorage } from "@/utils/request";
@@ -8,6 +8,7 @@ import Citations from "../Citation";
 import { AI_BACKGROUND_COLOR, USER_BACKGROUND_COLOR } from "@/utils/constants";
 import { v4 } from "uuid";
 import createDOMPurify from "dompurify";
+import { EditMessageForm, useEditMessage } from "./Actions/EditMessage";
 
 const DOMPurify = createDOMPurify(window);
 const HistoricalMessage = ({
@@ -21,27 +22,59 @@ const HistoricalMessage = ({
   chatId = null,
   isLastMessage = false,
   regenerateMessage,
+  saveEditedMessage,
 }) => {
+  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 (
+      <div
+        key={uuid}
+        className={`flex justify-center items-end w-full ${
+          role === "user" ? USER_BACKGROUND_COLOR : AI_BACKGROUND_COLOR
+        }`}
+      >
+        <div className="py-8 px-4 w-full flex gap-x-5 md:max-w-[800px] flex-col">
+          <div className="flex gap-x-5">
+            <ProfileImage role={role} workspace={workspace} />
+            <div className="p-2 rounded-lg bg-red-50 text-red-500">
+              <span className="inline-block">
+                <Warning className="h-4 w-4 mb-1 inline-block" /> Could not
+                respond to message.
+              </span>
+              <p className="text-xs font-mono mt-2 border-l-2 border-red-300 pl-2 bg-red-200 p-2 rounded-sm">
+                {error}
+              </p>
+            </div>
+          </div>
+        </div>
+      </div>
+    );
+  }
+
   return (
     <div
       key={uuid}
-      className={`flex justify-center items-end w-full ${
+      className={`flex justify-center items-end w-full group ${
         role === "user" ? USER_BACKGROUND_COLOR : AI_BACKGROUND_COLOR
       }`}
     >
       <div className={`py-8 px-4 w-full flex gap-x-5 md:max-w-[80%] flex-col`}>
         <div className="flex gap-x-5">
           <ProfileImage role={role} workspace={workspace} />
-          {error ? (
-            <div className="p-2 rounded-lg bg-red-50 text-red-500">
-              <span className={`inline-block `}>
-                <Warning className="h-4 w-4 mb-1 inline-block" /> Could not
-                respond to message.
-              </span>
-              <p className="text-xs font-mono mt-2 border-l-2 border-red-300 pl-2 bg-red-200 p-2 rounded-sm">
-                {error}
-              </p>
-            </div>
+          {isEditing ? (
+            <EditMessageForm
+              role={role}
+              chatId={chatId}
+              message={message}
+              adjustTextArea={adjustTextArea}
+              saveChanges={saveEditedMessage}
+            />
           ) : (
             <span
               className={`flex flex-col gap-y-1`}
@@ -51,19 +84,19 @@ const HistoricalMessage = ({
             />
           )}
         </div>
-        {role === "assistant" && !error && (
-          <div className="flex gap-x-5">
-            <div className="relative w-[35px] h-[35px] rounded-full flex-shrink-0 overflow-hidden" />
-            <Actions
-              message={message}
-              feedbackScore={feedbackScore}
-              chatId={chatId}
-              slug={workspace?.slug}
-              isLastMessage={isLastMessage}
-              regenerateMessage={regenerateMessage}
-            />
-          </div>
-        )}
+        <div className="flex gap-x-5">
+          <div className="relative w-[35px] h-[35px] rounded-full flex-shrink-0 overflow-hidden" />
+          <Actions
+            message={message}
+            feedbackScore={feedbackScore}
+            chatId={chatId}
+            slug={workspace?.slug}
+            isLastMessage={isLastMessage}
+            regenerateMessage={regenerateMessage}
+            isEditing={isEditing}
+            role={role}
+          />
+        </div>
         {role === "assistant" && <Citations sources={sources} />}
       </div>
     </div>
@@ -84,8 +117,7 @@ function ProfileImage({ role, workspace }) {
   }
 
   return (
-    <Jazzicon
-      size={36}
+    <UserIcon
       user={{
         uid: role === "user" ? userFromStorage()?.username : workspace.slug,
       }}
diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/PromptReply/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/PromptReply/index.jsx
index 07f8280a1..73275e9db 100644
--- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/PromptReply/index.jsx
+++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/PromptReply/index.jsx
@@ -1,6 +1,6 @@
 import { memo } from "react";
 import { Warning } from "@phosphor-icons/react";
-import Jazzicon from "../../../../UserIcon";
+import UserIcon from "../../../../UserIcon";
 import renderMarkdown from "@/utils/chat/markdown";
 import Citations from "../Citation";
 
@@ -84,7 +84,7 @@ export function WorkspaceProfileImage({ workspace }) {
     );
   }
 
-  return <Jazzicon size={36} user={{ uid: workspace.slug }} role="assistant" />;
+  return <UserIcon user={{ uid: workspace.slug }} role="assistant" />;
 }
 
 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 6e9f4e779..19b65453a 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 (
       <div className="flex flex-col h-full md:mt-0 pb-44 md:pb-40 w-full justify-end items-center">
@@ -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 494ee57d9..28d87e0df 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}
         />
         <PromptInput
diff --git a/frontend/src/components/WorkspaceChat/index.jsx b/frontend/src/components/WorkspaceChat/index.jsx
index 990ac7f5a..dec4c541f 100644
--- a/frontend/src/components/WorkspaceChat/index.jsx
+++ b/frontend/src/components/WorkspaceChat/index.jsx
@@ -22,6 +22,7 @@ export default function WorkspaceChat({ loading, workspace }) {
       const chatHistory = threadSlug
         ? await Workspace.threads.chatHistory(workspace.slug, threadSlug)
         : await Workspace.chatHistory(workspace.slug);
+
       setHistory(chatHistory);
       setLoadingHistory(false);
     }
diff --git a/frontend/src/models/workspace.js b/frontend/src/models/workspace.js
index 64732c044..cfbde704a 100644
--- a/frontend/src/models/workspace.js
+++ b/frontend/src/models/workspace.js
@@ -90,6 +90,26 @@ const Workspace = {
         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();
 
@@ -287,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",
@@ -336,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 039ee1868..a73006c99 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 c5730dbe0..a57b11e21 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 e2aead974..1c207e523 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 2657eb976..6d6f29bbd 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 c81992caa..bda40064d 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 d07eae308..609b18190 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",
-- 
GitLab