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 };