θΏ™ζ˜―indexlocζδΎ›ηš„ζœεŠ‘οΌŒδΈθ¦θΎ“ε…₯任何密码
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -50,6 +51,7 @@ const Actions = ({
chatId={chatId}
/>
)}
<DeleteMessage chatId={chatId} role={role} isEditing={isEditing} />
{chatId && role !== "user" && !isEditing && (
<>
<FeedbackButton
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { AI_BACKGROUND_COLOR, USER_BACKGROUND_COLOR } from "@/utils/constants";
import { v4 } from "uuid";
import createDOMPurify from "dompurify";
import { EditMessageForm, useEditMessage } from "./Actions/EditMessage";
import { useWatchDeleteMessage } from "./Actions/DeleteMessage";

const DOMPurify = createDOMPurify(window);
const HistoricalMessage = ({
Expand All @@ -26,6 +27,10 @@ const HistoricalMessage = ({
forkThread,
}) => {
const { isEditing } = useEditMessage({ chatId, role });
const { isDeleted, completeDelete, onEndAnimation } = useWatchDeleteMessage({
chatId,
role,
});
const adjustTextArea = (event) => {
const element = event.target;
element.style.height = "auto";
Expand Down Expand Up @@ -58,10 +63,14 @@ const HistoricalMessage = ({
);
}

if (completeDelete) return null;
return (
<div
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
}`}
>
Expand Down
20 changes: 20 additions & 0 deletions frontend/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
11 changes: 11 additions & 0 deletions frontend/src/models/workspace.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
27 changes: 26 additions & 1 deletion server/endpoints/workspaces.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 };