mirror of
https://github.com/Mintplex-Labs/anything-llm.git
synced 2024-11-15 02:50:10 +01:00
[FEAT] Ability to delete messages from conversation history (#1812)
* implement delete message feature * add new delete chat endpoint that just hides messages from ui and fix perms * refactor: add delete-event hooks for delete so we dont need to pass so much refactor: implmentation of delete endpoint PUT * update animation linting remove element on animiation to prevent popin * linting and cleanup imports --------- Co-authored-by: timothycarambat <rambat1010@gmail.com>
This commit is contained in:
parent
8658b1e7c7
commit
ab56bae1bb
@ -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 (
|
||||||
|
<div className="mt-3 relative">
|
||||||
|
<button
|
||||||
|
onClick={emitDeleteEvent}
|
||||||
|
data-tooltip-id={`delete-message-${chatId}`}
|
||||||
|
data-tooltip-content="Delete message"
|
||||||
|
className="border-none text-zinc-300"
|
||||||
|
aria-label="Delete"
|
||||||
|
>
|
||||||
|
<Trash size={18} className="mb-1" />
|
||||||
|
</button>
|
||||||
|
<Tooltip
|
||||||
|
id={`delete-message-${chatId}`}
|
||||||
|
place="bottom"
|
||||||
|
delayShow={300}
|
||||||
|
className="tooltip !text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -12,6 +12,7 @@ import { Tooltip } from "react-tooltip";
|
|||||||
import Workspace from "@/models/workspace";
|
import Workspace from "@/models/workspace";
|
||||||
import TTSMessage from "./TTSButton";
|
import TTSMessage from "./TTSButton";
|
||||||
import { EditMessageAction } from "./EditMessage";
|
import { EditMessageAction } from "./EditMessage";
|
||||||
|
import { DeleteMessage } from "./DeleteMessage";
|
||||||
|
|
||||||
const Actions = ({
|
const Actions = ({
|
||||||
message,
|
message,
|
||||||
@ -50,6 +51,7 @@ const Actions = ({
|
|||||||
chatId={chatId}
|
chatId={chatId}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<DeleteMessage chatId={chatId} role={role} isEditing={isEditing} />
|
||||||
{chatId && role !== "user" && !isEditing && (
|
{chatId && role !== "user" && !isEditing && (
|
||||||
<>
|
<>
|
||||||
<FeedbackButton
|
<FeedbackButton
|
||||||
|
@ -9,6 +9,7 @@ import { AI_BACKGROUND_COLOR, USER_BACKGROUND_COLOR } from "@/utils/constants";
|
|||||||
import { v4 } from "uuid";
|
import { v4 } from "uuid";
|
||||||
import createDOMPurify from "dompurify";
|
import createDOMPurify from "dompurify";
|
||||||
import { EditMessageForm, useEditMessage } from "./Actions/EditMessage";
|
import { EditMessageForm, useEditMessage } from "./Actions/EditMessage";
|
||||||
|
import { useWatchDeleteMessage } from "./Actions/DeleteMessage";
|
||||||
|
|
||||||
const DOMPurify = createDOMPurify(window);
|
const DOMPurify = createDOMPurify(window);
|
||||||
const HistoricalMessage = ({
|
const HistoricalMessage = ({
|
||||||
@ -26,6 +27,10 @@ const HistoricalMessage = ({
|
|||||||
forkThread,
|
forkThread,
|
||||||
}) => {
|
}) => {
|
||||||
const { isEditing } = useEditMessage({ chatId, role });
|
const { isEditing } = useEditMessage({ chatId, role });
|
||||||
|
const { isDeleted, completeDelete, onEndAnimation } = useWatchDeleteMessage({
|
||||||
|
chatId,
|
||||||
|
role,
|
||||||
|
});
|
||||||
const adjustTextArea = (event) => {
|
const adjustTextArea = (event) => {
|
||||||
const element = event.target;
|
const element = event.target;
|
||||||
element.style.height = "auto";
|
element.style.height = "auto";
|
||||||
@ -58,10 +63,14 @@ const HistoricalMessage = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (completeDelete) return null;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={uuid}
|
key={uuid}
|
||||||
className={`flex justify-center items-end w-full group ${
|
onAnimationEnd={onEndAnimation}
|
||||||
|
className={`${
|
||||||
|
isDeleted ? "animate-remove" : ""
|
||||||
|
} flex justify-center items-end w-full group ${
|
||||||
role === "user" ? USER_BACKGROUND_COLOR : AI_BACKGROUND_COLOR
|
role === "user" ? USER_BACKGROUND_COLOR : AI_BACKGROUND_COLOR
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
@ -746,3 +746,23 @@ does not extend the close button beyond the viewport. */
|
|||||||
.search-input::-webkit-search-cancel-button {
|
.search-input::-webkit-search-cancel-button {
|
||||||
filter: grayscale(100%) invert(1) brightness(100) opacity(0.5);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -384,6 +384,17 @@ const Workspace = {
|
|||||||
return false;
|
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) {
|
forkThread: async function (slug = "", threadSlug = null, chatId = null) {
|
||||||
return await fetch(`${API_BASE}/workspace/${slug}/thread/fork`, {
|
return await fetch(`${API_BASE}/workspace/${slug}/thread/fork`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
@ -833,11 +833,36 @@ function workspaceEndpoints(app) {
|
|||||||
);
|
);
|
||||||
response.status(200).json({ newThreadSlug: newThread.slug });
|
response.status(200).json({ newThreadSlug: newThread.slug });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e.message, e);
|
console.error(e.message, e);
|
||||||
response.status(500).json({ message: "Internal server error" });
|
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 };
|
module.exports = { workspaceEndpoints };
|
||||||
|
Loading…
Reference in New Issue
Block a user