mirror of
https://github.com/Mintplex-Labs/anything-llm.git
synced 2024-11-10 17:00:11 +01:00
[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>
This commit is contained in:
parent
98cef508a6
commit
26c220503c
@ -63,4 +63,4 @@
|
||||
"tailwindcss": "^3.3.1",
|
||||
"vite": "^4.3.0"
|
||||
}
|
||||
}
|
||||
}
|
@ -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}
|
||||
/>
|
||||
|
@ -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`}
|
||||
|
@ -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
|
||||
|
BIN
frontend/src/components/UserIcon/workspace.png
Normal file
BIN
frontend/src/components/UserIcon/workspace.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
@ -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>
|
||||
);
|
||||
}
|
@ -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
|
||||
|
@ -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,20 +22,28 @@ const HistoricalMessage = ({
|
||||
chatId = null,
|
||||
isLastMessage = false,
|
||||
regenerateMessage,
|
||||
saveEditedMessage,
|
||||
}) => {
|
||||
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-[80%] flex-col`}>
|
||||
<div className="flex gap-x-5">
|
||||
<ProfileImage role={role} workspace={workspace} />
|
||||
{error ? (
|
||||
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 `}>
|
||||
<span className="inline-block">
|
||||
<Warning className="h-4 w-4 mb-1 inline-block" /> Could not
|
||||
respond to message.
|
||||
</span>
|
||||
@ -42,6 +51,30 @@ const HistoricalMessage = ({
|
||||
{error}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={uuid}
|
||||
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} />
|
||||
{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,
|
||||
}}
|
||||
|
@ -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);
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
@ -240,6 +240,7 @@ export default function ChatContainer({ workspace, knownHistory = [] }) {
|
||||
history={chatHistory}
|
||||
workspace={workspace}
|
||||
sendCommand={sendCommand}
|
||||
updateHistory={setChatHistory}
|
||||
regenerateAssistantMessage={regenerateAssistantMessage}
|
||||
/>
|
||||
<PromptInput
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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") {
|
||||
|
@ -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 };
|
||||
|
@ -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],
|
||||
|
@ -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 };
|
||||
|
@ -174,6 +174,7 @@ function convertToChatHistory(history = []) {
|
||||
role: "user",
|
||||
content: prompt,
|
||||
sentAt: moment(createdAt).unix(),
|
||||
chatId: id,
|
||||
},
|
||||
{
|
||||
type: data?.type || "chart",
|
||||
|
Loading…
Reference in New Issue
Block a user