diff --git a/frontend/package.json b/frontend/package.json index 11e612fc..038926d4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,7 +11,6 @@ "preview": "vite preview" }, "dependencies": { - "@metamask/jazzicon": "^2.0.0", "@microsoft/fetch-event-source": "^2.0.1", "@phosphor-icons/react": "^2.0.13", "@tremor/react": "^3.15.1", @@ -63,4 +62,4 @@ "tailwindcss": "^3.3.1", "vite": "^4.3.0" } -} +} \ No newline at end of file diff --git a/frontend/src/components/ChatBubble/index.jsx b/frontend/src/components/ChatBubble/index.jsx index 8d311883..c5a1f190 100644 --- a/frontend/src/components/ChatBubble/index.jsx +++ b/frontend/src/components/ChatBubble/index.jsx @@ -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 }) {
- diff --git a/frontend/src/components/DefaultChat/index.jsx b/frontend/src/components/DefaultChat/index.jsx index 43ae6e7a..ae52a0d2 100644 --- a/frontend/src/components/DefaultChat/index.jsx +++ b/frontend/src/components/DefaultChat/index.jsx @@ -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`} >
- +
- +
- +
- @@ -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`} >
- +
- @@ -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`} >
- +
- @@ -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`} >
- +
{ - if (!divRef.current || (role === "user" && pfp)) return; - - const result = JAZZ(size, seed); - divRef.current.appendChild(result); - }, [pfp, role, seed, size]); - - return ( -
-
- {role === "user" && pfp && ( + if (role === "user") { + if (!pfp) { + return ( +
+
+

{user?.username?.slice(0, 2) || "U"}

+
+
+ ); + } + return ( +
User profile picture - )} +
+ ); + } + + return ( +
+ User profile picture
); } - -function toPseudoRandomInteger(uidString = "") { - return uidString.split("").reduce((acc, char) => acc + char.charCodeAt(0), 0); -} diff --git a/frontend/src/components/UserIcon/workspace.png b/frontend/src/components/UserIcon/workspace.png new file mode 100644 index 00000000..537d583c Binary files /dev/null and b/frontend/src/components/UserIcon/workspace.png differ diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/EditMessage/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/EditMessage/index.jsx index 70083527..f9346b26 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/EditMessage/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/EditMessage/index.jsx @@ -29,24 +29,28 @@ export function useEditMessage({ chatId, role }) { return { isEditing, setIsEditing }; } -export function EditMessageAction({ chatId = null, role }) { - const [isEditing, setIsEditing] = useState(false); +export function EditMessageAction({ chatId = null, role, isEditing }) { function handleEditClick() { window.dispatchEvent( new CustomEvent(EDIT_EVENT, { detail: { chatId, role } }) ); - setIsEditing(true); } if (!chatId || isEditing) return null; 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 fd03e6a5..0e2dba84 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/index.jsx @@ -6,6 +6,7 @@ import { ThumbsUp, ThumbsDown, ArrowsClockwise, + Copy, } from "@phosphor-icons/react"; import { Tooltip } from "react-tooltip"; import Workspace from "@/models/workspace"; @@ -19,6 +20,7 @@ const Actions = ({ slug, isLastMessage, regenerateMessage, + isEditing, role, }) => { const [selectedFeedback, setSelectedFeedback] = useState(feedbackScore); @@ -33,15 +35,15 @@ const Actions = ({
- - {isLastMessage && ( + + {isLastMessage && !isEditing && ( )} - {chatId && role !== "user" && ( + {chatId && role !== "user" && !isEditing && ( <> ) : ( - + )} @@ -93,6 +93,7 @@ const HistoricalMessage = ({ slug={workspace?.slug} isLastMessage={isLastMessage} regenerateMessage={regenerateMessage} + isEditing={isEditing} role={role} />
@@ -116,8 +117,7 @@ function ProfileImage({ role, workspace }) { } return ( - ; + return ; } export default memo(PromptReply); diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx index a9e28ce4..5e797367 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx @@ -8,14 +8,17 @@ 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); @@ -89,8 +92,6 @@ export default function ChatHistory({ }; // TODO: Be able to edit both user and system response message. - // TODO: Pencil does not appear under user message while chatting because it - // does not know its own chatId since not response is present. const saveEditedMessage = async ({ editedMessage, chatId, role }) => { if (!editedMessage) return; // Don't save empty edits. @@ -107,13 +108,27 @@ export default function ChatHistory({ // 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, chatId); + 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; } }; diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx index 494ee57d..28d87e0d 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/index.jsx @@ -240,6 +240,7 @@ export default function ChatContainer({ workspace, knownHistory = [] }) { history={chatHistory} workspace={workspace} sendCommand={sendCommand} + updateHistory={setChatHistory} regenerateAssistantMessage={regenerateAssistantMessage} /> { - if (res.ok) return true; - throw new Error("Failed to delete chats."); - }) - .catch((e) => { - console.log(e); - 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(); @@ -304,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", @@ -353,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; diff --git a/frontend/src/models/workspaceThread.js b/frontend/src/models/workspaceThread.js index 039ee186..a73006c9 100644 --- a/frontend/src/models/workspaceThread.js +++ b/frontend/src/models/workspaceThread.js @@ -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; diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 93bdc088..911af51e 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -487,14 +487,6 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@metamask/jazzicon@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@metamask/jazzicon/-/jazzicon-2.0.0.tgz#5615528e91c0fc5c9d79202d1f0954a7922525a0" - integrity sha512-7M+WSZWKcQAo0LEhErKf1z+D3YX0tEDAcGvcKbDyvDg34uvgeKR00mFNIYwAhdAS9t8YXxhxZgsrRBBg6X8UQg== - dependencies: - color "^0.11.3" - mersenne-twister "^1.1.0" - "@microsoft/fetch-event-source@^2.0.1": version "2.0.1" resolved "https://registry.yarnpkg.com/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz#9ceecc94b49fbaa15666e38ae8587f64acce007d" @@ -1025,11 +1017,6 @@ cliui@^8.0.1: strip-ansi "^6.0.1" wrap-ansi "^7.0.0" -clone@^1.0.2: - version "1.0.4" - resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" - integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== - clsx@^1.1.1: version "1.2.1" resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" @@ -1040,7 +1027,7 @@ clsx@^2.0.0: resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.0.tgz#e851283bcb5c80ee7608db18487433f7b23f77cb" integrity sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg== -color-convert@^1.3.0, color-convert@^1.9.0: +color-convert@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== @@ -1059,27 +1046,11 @@ color-name@1.1.3: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== -color-name@^1.0.0, color-name@~1.1.4: +color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -color-string@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/color-string/-/color-string-0.3.0.tgz#27d46fb67025c5c2fa25993bfbf579e47841b991" - integrity sha512-sz29j1bmSDfoAxKIEU6zwoIZXN6BrFbAMIhfYCNyiZXBDuU/aiHlN84lp/xDzL2ubyFhLDobHIlU1X70XRrMDA== - dependencies: - color-name "^1.0.0" - -color@^0.11.3: - version "0.11.4" - resolved "https://registry.yarnpkg.com/color/-/color-0.11.4.tgz#6d7b5c74fb65e841cd48792ad1ed5e07b904d764" - integrity sha512-Ajpjd8asqZ6EdxQeqGzU5WBhhTfJ/0cA4Wlbre7e5vXfmDSmda7Ov6jeKoru+b0vHcb1CqvuroTHp5zIWzhVMA== - dependencies: - clone "^1.0.2" - color-convert "^1.3.0" - color-string "^0.3.0" - commander@^4.0.0: version "4.1.1" resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" @@ -2401,11 +2372,6 @@ merge2@^1.3.0: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -mersenne-twister@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/mersenne-twister/-/mersenne-twister-1.1.0.tgz#f916618ee43d7179efcf641bec4531eb9670978a" - integrity sha512-mUYWsMKNrm4lfygPkL3OfGzOPTR2DBlTkBNHM//F6hGp8cLThY897crAlk3/Jo17LEOOjQUrNAx6DvgO77QJkA== - micromatch@^4.0.4, micromatch@^4.0.5: version "4.0.5" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" diff --git a/server/endpoints/workspaceThreads.js b/server/endpoints/workspaceThreads.js index e2aead97..1c207e52 100644 --- a/server/endpoints/workspaceThreads.js +++ b/server/endpoints/workspaceThreads.js @@ -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 }; diff --git a/server/endpoints/workspaces.js b/server/endpoints/workspaces.js index 856641d6..6d6f29bb 100644 --- a/server/endpoints/workspaces.js +++ b/server/endpoints/workspaces.js @@ -377,14 +377,9 @@ function workspaceEndpoints(app) { return; } - console.log("in here"); - const history = multiUserMode(response) ? await WorkspaceChats.forWorkspaceByUser(workspace.id, user.id) : await WorkspaceChats.forWorkspace(workspace.id); - - console.log(history); - response.status(200).json({ history: convertToChatHistory(history) }); } catch (e) { console.log(e.message, e); @@ -430,15 +425,13 @@ function workspaceEndpoints(app) { async (request, response) => { try { const { startingId } = reqBody(request); + const user = await userFromSession(request, response); const workspace = response.locals.workspace; - if (!workspace) { - response.sendStatus(400).end(); - return; - } - await WorkspaceChats.delete({ workspaceId: workspace.id, + thread_id: null, + user_id: user?.id, id: { gte: Number(startingId) }, }); @@ -450,6 +443,43 @@ function workspaceEndpoints(app) { } ); + 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], diff --git a/server/models/workspaceChats.js b/server/models/workspaceChats.js index c81992ca..bda40064 100644 --- a/server/models/workspaceChats.js +++ b/server/models/workspaceChats.js @@ -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 }; diff --git a/server/utils/helpers/chat/responses.js b/server/utils/helpers/chat/responses.js index ac804eca..609b1819 100644 --- a/server/utils/helpers/chat/responses.js +++ b/server/utils/helpers/chat/responses.js @@ -168,8 +168,6 @@ function convertToChatHistory(history = []) { const formattedHistory = []; history.forEach((history) => { const { prompt, response, createdAt, feedbackScore = null, id } = history; - - console.log("HISTORY", history); const data = JSON.parse(response); formattedHistory.push([ {