diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/DeleteMessage/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/DeleteMessage/index.jsx
new file mode 100644
index 00000000..262fdb3e
--- /dev/null
+++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/DeleteMessage/index.jsx
@@ -0,0 +1,67 @@
+import { useState, useEffect } from "react";
+import { Trash } from "@phosphor-icons/react";
+import { Tooltip } from "react-tooltip";
+import Workspace from "@/models/workspace";
+const DELETE_EVENT = "delete-message";
+
+export function useWatchDeleteMessage({ chatId = null, role = "user" }) {
+ const [isDeleted, setIsDeleted] = useState(false);
+ const [completeDelete, setCompleteDelete] = useState(false);
+
+ useEffect(() => {
+ function listenForEvent() {
+ if (!chatId) return;
+ window.addEventListener(DELETE_EVENT, onDeleteEvent);
+ }
+ listenForEvent();
+ return () => {
+ window.removeEventListener(DELETE_EVENT, onDeleteEvent);
+ };
+ }, [chatId]);
+
+ function onEndAnimation() {
+ if (!isDeleted) return;
+ setCompleteDelete(true);
+ }
+
+ async function onDeleteEvent(e) {
+ if (e.detail.chatId === chatId) {
+ setIsDeleted(true);
+ // Do this to prevent double-emission of the PUT/DELETE api call
+ // because then there will be a race condition and it will make an error log for nothing
+ // as one call will complete and the other will fail.
+ if (role === "assistant") await Workspace.deleteChat(chatId);
+ return false;
+ }
+ }
+
+ return { isDeleted, completeDelete, onEndAnimation };
+}
+
+export function DeleteMessage({ chatId, isEditing, role }) {
+ if (!chatId || isEditing || role === "user") return null;
+
+ function emitDeleteEvent() {
+ window.dispatchEvent(new CustomEvent(DELETE_EVENT, { detail: { chatId } }));
+ }
+
+ 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 abe1f00e..061f7724 100644
--- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/index.jsx
+++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/index.jsx
@@ -12,6 +12,7 @@ import { Tooltip } from "react-tooltip";
import Workspace from "@/models/workspace";
import TTSMessage from "./TTSButton";
import { EditMessageAction } from "./EditMessage";
+import { DeleteMessage } from "./DeleteMessage";
const Actions = ({
message,
@@ -50,6 +51,7 @@ const Actions = ({
chatId={chatId}
/>
)}
+
{chatId && role !== "user" && !isEditing && (
<>
{
const { isEditing } = useEditMessage({ chatId, role });
+ const { isDeleted, completeDelete, onEndAnimation } = useWatchDeleteMessage({
+ chatId,
+ role,
+ });
const adjustTextArea = (event) => {
const element = event.target;
element.style.height = "auto";
@@ -58,10 +63,14 @@ const HistoricalMessage = ({
);
}
+ if (completeDelete) return null;
return (
diff --git a/frontend/src/index.css b/frontend/src/index.css
index 0d796116..830d8ea6 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -746,3 +746,23 @@ does not extend the close button beyond the viewport. */
.search-input::-webkit-search-cancel-button {
filter: grayscale(100%) invert(1) brightness(100) opacity(0.5);
}
+
+.animate-remove {
+ animation: fadeAndShrink 800ms forwards;
+}
+
+@keyframes fadeAndShrink {
+ 50% {
+ opacity: 25%;
+ }
+
+ 75% {
+ opacity: 10%;
+ }
+
+ 100% {
+ height: 0px;
+ opacity: 0%;
+ display: none;
+ }
+}
diff --git a/frontend/src/models/workspace.js b/frontend/src/models/workspace.js
index 43c723f7..369ae986 100644
--- a/frontend/src/models/workspace.js
+++ b/frontend/src/models/workspace.js
@@ -384,6 +384,17 @@ const Workspace = {
return false;
});
},
+ deleteChat: async (chatId) => {
+ return await fetch(`${API_BASE}/workspace/workspace-chats/${chatId}`, {
+ method: "PUT",
+ headers: baseHeaders(),
+ })
+ .then((res) => res.json())
+ .catch((e) => {
+ console.error(e);
+ return { success: false, error: e.message };
+ });
+ },
forkThread: async function (slug = "", threadSlug = null, chatId = null) {
return await fetch(`${API_BASE}/workspace/${slug}/thread/fork`, {
method: "POST",
diff --git a/server/endpoints/workspaces.js b/server/endpoints/workspaces.js
index 460d9422..aa3ef19b 100644
--- a/server/endpoints/workspaces.js
+++ b/server/endpoints/workspaces.js
@@ -833,11 +833,36 @@ function workspaceEndpoints(app) {
);
response.status(200).json({ newThreadSlug: newThread.slug });
} catch (e) {
- console.log(e.message, e);
+ console.error(e.message, e);
response.status(500).json({ message: "Internal server error" });
}
}
);
+
+ app.put(
+ "/workspace/workspace-chats/:id",
+ [validatedRequest, flexUserRoleValid([ROLES.all])],
+ async (request, response) => {
+ try {
+ const { id } = request.params;
+ const user = await userFromSession(request, response);
+ const validChat = await WorkspaceChats.get({
+ id: Number(id),
+ user_id: user?.id ?? null,
+ });
+ if (!validChat)
+ return response
+ .status(404)
+ .json({ success: false, error: "Chat not found." });
+
+ await WorkspaceChats._update(validChat.id, { include: false });
+ response.json({ success: true, error: null });
+ } catch (e) {
+ console.error(e.message, e);
+ response.status(500).json({ success: false, error: "Server error" });
+ }
+ }
+ );
}
module.exports = { workspaceEndpoints };