diff --git a/embed/src/components/ChatWindow/ChatContainer/ChatHistory/index.jsx b/embed/src/components/ChatWindow/ChatContainer/ChatHistory/index.jsx index 6a5e7eb6..786d630a 100644 --- a/embed/src/components/ChatWindow/ChatContainer/ChatHistory/index.jsx +++ b/embed/src/components/ChatWindow/ChatContainer/ChatHistory/index.jsx @@ -89,6 +89,8 @@ export default function ChatHistory({ settings = {}, history = [] }) { message={props.content} role={props.role} sources={props.sources} + chatId={props.chatId} + feedbackScore={props.feedbackScore} error={props.error} /> ); 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 12fa7dc7..b68bc92b 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/Actions/index.jsx @@ -1,27 +1,91 @@ +import React, { memo, useState } from "react"; import useCopyText from "@/hooks/useCopyText"; -import { Check, ClipboardText } from "@phosphor-icons/react"; -import { memo } from "react"; +import { + Check, + ClipboardText, + ThumbsUp, + ThumbsDown, +} from "@phosphor-icons/react"; import { Tooltip } from "react-tooltip"; +import Workspace from "@/models/workspace"; + +const Actions = ({ message, feedbackScore, chatId, slug }) => { + const [selectedFeedback, setSelectedFeedback] = useState(feedbackScore); + + const handleFeedback = async (newFeedback) => { + const updatedFeedback = + selectedFeedback === newFeedback ? null : newFeedback; + await Workspace.updateChatFeedback(chatId, slug, updatedFeedback); + setSelectedFeedback(updatedFeedback); + }; -const Actions = ({ message }) => { return (
- {/* Other actions to go here later. */} + {chatId && ( + <> + handleFeedback(true)} + tooltipId={`${chatId}-thumbs-up`} + tooltipContent="Good response" + IconComponent={ThumbsUp} + /> + handleFeedback(false)} + tooltipId={`${chatId}-thumbs-down`} + tooltipContent="Bad response" + IconComponent={ThumbsDown} + /> + + )}
); }; +function FeedbackButton({ + isSelected, + handleFeedback, + tooltipId, + tooltipContent, + IconComponent, +}) { + return ( +
+ + +
+ ); +} + function CopyMessage({ message }) { const { copied, copyText } = useCopyText(); + return ( <>
+
- ); } diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/index.jsx index ba3b687b..f4c5c86a 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/HistoricalMessage/index.jsx @@ -1,4 +1,4 @@ -import React, { memo, forwardRef } from "react"; +import React, { memo } from "react"; import { Warning } from "@phosphor-icons/react"; import Jazzicon from "../../../../UserIcon"; import Actions from "./Actions"; @@ -10,64 +10,70 @@ import { v4 } from "uuid"; import createDOMPurify from "dompurify"; const DOMPurify = createDOMPurify(window); -const HistoricalMessage = forwardRef( - ( - { uuid = v4(), message, role, workspace, sources = [], error = false }, - ref - ) => { - return ( +const HistoricalMessage = ({ + uuid = v4(), + message, + role, + workspace, + sources = [], + error = false, + feedbackScore = null, + chatId = null, +}) => { + return ( +
-
-
- +
+ - {error ? ( -
- - Could not - respond to message. - -

- {error} -

-
- ) : ( - - )} -
- {role === "assistant" && !error && ( -
-
- + {error ? ( +
+ + Could not + respond to message. + +

+ {error} +

+ ) : ( + )} - {role === "assistant" && }
+ {role === "assistant" && !error && ( +
+
+ +
+ )} + {role === "assistant" && }
- ); - } -); +
+ ); +}; export default memo(HistoricalMessage); diff --git a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/PromptReply/index.jsx b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/PromptReply/index.jsx index a219f120..f7777841 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/PromptReply/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/PromptReply/index.jsx @@ -1,67 +1,44 @@ -import { forwardRef, memo } from "react"; +import { memo } from "react"; import { Warning } from "@phosphor-icons/react"; import Jazzicon from "../../../../UserIcon"; import renderMarkdown from "@/utils/chat/markdown"; import Citations from "../Citation"; -const PromptReply = forwardRef( - ( - { uuid, reply, pending, error, workspace, sources = [], closed = true }, - ref - ) => { - const assistantBackgroundColor = "bg-historical-msg-system"; +const PromptReply = ({ + uuid, + reply, + pending, + error, + workspace, + sources = [], + closed = true, +}) => { + const assistantBackgroundColor = "bg-historical-msg-system"; - if (!reply && sources.length === 0 && !pending && !error) return null; - - if (pending) { - return ( -
-
-
- -
-
-
-
- ); - } - - if (error) { - return ( -
-
-
- - - Could not - respond to message. - Reason: {error || "unknown"} - -
-
-
- ); - } + if (!reply && sources.length === 0 && !pending && !error) return null; + if (pending) { + return ( +
+
+
+ +
+
+
+
+ ); + } + + if (error) { return (
@@ -72,15 +49,35 @@ const PromptReply = forwardRef( role="assistant" /> + className={`inline-block p-2 rounded-lg bg-red-50 text-red-500`} + > + Could not + respond to message. + Reason: {error || "unknown"} +
-
); } -); + + 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 74c159f4..5be8afc1 100644 --- a/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx +++ b/frontend/src/components/WorkspaceChat/ChatContainer/ChatHistory/index.jsx @@ -7,7 +7,6 @@ import { ArrowDown } from "@phosphor-icons/react"; import debounce from "lodash.debounce"; export default function ChatHistory({ history = [], workspace, sendCommand }) { - const replyRef = useRef(null); const { showing, showModal, hideModal } = useManageWorkspaceModal(); const [isAtBottom, setIsAtBottom] = useState(true); const chatHistoryRef = useRef(null); @@ -89,7 +88,6 @@ export default function ChatHistory({ history = [], workspace, sendCommand }) { ref={chatHistoryRef} > {history.map((props, index) => { - const isLastMessage = index === history.length - 1; const isLastBotReply = index === history.length - 1 && props.role === "assistant"; @@ -97,7 +95,6 @@ export default function ChatHistory({ history = [], workspace, sendCommand }) { return ( ); diff --git a/frontend/src/models/workspace.js b/frontend/src/models/workspace.js index 0adcf3fa..3b31646d 100644 --- a/frontend/src/models/workspace.js +++ b/frontend/src/models/workspace.js @@ -60,6 +60,19 @@ const Workspace = { .catch(() => []); return history; }, + updateChatFeedback: async function (chatId, slug, feedback) { + const result = await fetch( + `${API_BASE}/workspace/${slug}/chat-feedback/${chatId}`, + { + method: "POST", + headers: baseHeaders(), + body: JSON.stringify({ feedback }), + } + ) + .then((res) => res.ok) + .catch(() => false); + return result; + }, streamChat: async function ({ slug }, message, mode = "query", handleChat) { const ctrl = new AbortController(); await fetchEventSource(`${API_BASE}/workspace/${slug}/stream-chat`, { diff --git a/frontend/src/utils/chat/index.js b/frontend/src/utils/chat/index.js index f2587484..f1df11fe 100644 --- a/frontend/src/utils/chat/index.js +++ b/frontend/src/utils/chat/index.js @@ -1,4 +1,4 @@ -// For handling of synchronous chats that are not utilizing streaming or chat requests. +// For handling of chat responses in the frontend by their various types. export default function handleChat( chatResult, setLoadingResponse, @@ -6,7 +6,15 @@ export default function handleChat( remHistory, _chatHistory ) { - const { uuid, textResponse, type, sources = [], error, close } = chatResult; + const { + uuid, + textResponse, + type, + sources = [], + error, + close, + chatId = null, + } = chatResult; if (type === "abort") { setLoadingResponse(false); @@ -46,6 +54,7 @@ export default function handleChat( error, animate: !close, pending: false, + chatId, }, ]); _chatHistory.push({ @@ -57,6 +66,7 @@ export default function handleChat( error, animate: !close, pending: false, + chatId, }); } else if (type === "textResponseChunk") { const chatIdx = _chatHistory.findIndex((chat) => chat.uuid === uuid); @@ -70,6 +80,7 @@ export default function handleChat( closed: close, animate: !close, pending: false, + chatId, }; _chatHistory[chatIdx] = updatedHistory; } else { @@ -82,9 +93,21 @@ export default function handleChat( closed: close, animate: !close, pending: false, + chatId, }); } setChatHistory([..._chatHistory]); + } 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; + } + setChatHistory([..._chatHistory]); } } diff --git a/server/endpoints/workspaces.js b/server/endpoints/workspaces.js index 57418062..c6a3ad93 100644 --- a/server/endpoints/workspaces.js +++ b/server/endpoints/workspaces.js @@ -21,6 +21,7 @@ const { EventLogs } = require("../models/eventLogs"); const { WorkspaceSuggestedMessages, } = require("../models/workspacesSuggestedMessages"); +const { validWorkspaceSlug } = require("../utils/middleware/validWorkspace"); const { handleUploads } = setupMulter(); function workspaceEndpoints(app) { @@ -321,6 +322,35 @@ function workspaceEndpoints(app) { } ); + app.post( + "/workspace/:slug/chat-feedback/:chatId", + [validatedRequest, flexUserRoleValid([ROLES.all]), validWorkspaceSlug], + async (request, response) => { + try { + const { chatId } = request.params; + const { feedback = null } = reqBody(request); + const existingChat = await WorkspaceChats.get({ + id: Number(chatId), + workspaceId: response.locals.workspace.id, + }); + + if (!existingChat) { + response.status(404).end(); + return; + } + + const result = await WorkspaceChats.updateFeedbackScore( + chatId, + feedback + ); + response.status(200).json({ success: result }); + } catch (error) { + console.error("Error updating chat feedback:", error); + response.status(500).end(); + } + } + ); + app.get( "/workspace/:slug/suggested-messages", [validatedRequest, flexUserRoleValid([ROLES.all])], diff --git a/server/models/workspaceChats.js b/server/models/workspaceChats.js index 4fae46b9..c81992ca 100644 --- a/server/models/workspaceChats.js +++ b/server/models/workspaceChats.js @@ -203,6 +203,23 @@ const WorkspaceChats = { return []; } }, + updateFeedbackScore: async function (chatId = null, feedbackScore = null) { + if (!chatId) return; + try { + await prisma.workspace_chats.update({ + where: { + id: Number(chatId), + }, + data: { + feedbackScore: + feedbackScore === null ? null : Number(feedbackScore) === 1, + }, + }); + return; + } catch (error) { + console.error(error.message); + } + }, }; module.exports = { WorkspaceChats }; diff --git a/server/prisma/migrations/20240210004405_init/migration.sql b/server/prisma/migrations/20240210004405_init/migration.sql new file mode 100644 index 00000000..3d824ab0 --- /dev/null +++ b/server/prisma/migrations/20240210004405_init/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "workspace_chats" ADD COLUMN "feedbackScore" BOOLEAN; diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index c52e1a4b..55b469cf 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -142,6 +142,7 @@ model workspace_chats { thread_id Int? // No relation to prevent whole table migration createdAt DateTime @default(now()) lastUpdatedAt DateTime @default(now()) + feedbackScore Boolean? users users? @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: Cascade) } diff --git a/server/utils/chats/index.js b/server/utils/chats/index.js index 8ec7d900..d25d2a93 100644 --- a/server/utils/chats/index.js +++ b/server/utils/chats/index.js @@ -7,7 +7,7 @@ const { getVectorDbClass, getLLMProvider } = require("../helpers"); function convertToChatHistory(history = []) { const formattedHistory = []; history.forEach((history) => { - const { prompt, response, createdAt } = history; + const { prompt, response, createdAt, feedbackScore = null, id } = history; const data = JSON.parse(response); formattedHistory.push([ { @@ -19,7 +19,9 @@ function convertToChatHistory(history = []) { role: "assistant", content: data.text, sources: data.sources || [], + chatId: id, sentAt: moment(createdAt).unix(), + feedbackScore, }, ]); }); @@ -185,8 +187,7 @@ async function chatWithWorkspace( error: "No text completion could be completed with this input.", }; } - - await WorkspaceChats.new({ + const { chat } = await WorkspaceChats.new({ workspaceId: workspace.id, prompt: message, response: { text: textResponse, sources, type: chatMode }, @@ -196,9 +197,10 @@ async function chatWithWorkspace( id: uuid, type: "textResponse", close: true, + error: null, + chatId: chat.id, textResponse, sources, - error, }; } @@ -271,7 +273,7 @@ async function emptyEmbeddingChat({ workspace, rawHistory ); - await WorkspaceChats.new({ + const { chat } = await WorkspaceChats.new({ workspaceId: workspace.id, prompt: message, response: { text: textResponse, sources: [], type: "chat" }, @@ -283,6 +285,7 @@ async function emptyEmbeddingChat({ sources: [], close: true, error: null, + chatId: chat.id, textResponse, }; } diff --git a/server/utils/chats/stream.js b/server/utils/chats/stream.js index 11190d63..0fe5a7ea 100644 --- a/server/utils/chats/stream.js +++ b/server/utils/chats/stream.js @@ -177,12 +177,20 @@ async function streamChatWithWorkspace( }); } - await WorkspaceChats.new({ + const { chat } = await WorkspaceChats.new({ workspaceId: workspace.id, prompt: message, response: { text: completeText, sources, type: chatMode }, - user, threadId: thread?.id, + user, + }); + + writeResponseChunk(response, { + uuid, + type: "finalizeResponseStream", + close: true, + error: false, + chatId: chat.id, }); return; } @@ -235,12 +243,20 @@ async function streamEmptyEmbeddingChat({ }); } - await WorkspaceChats.new({ + const { chat } = await WorkspaceChats.new({ workspaceId: workspace.id, prompt: message, response: { text: completeText, sources: [], type: "chat" }, - user, threadId: thread?.id, + user, + }); + + writeResponseChunk(response, { + uuid, + type: "finalizeResponseStream", + close: true, + error: false, + chatId: chat.id, }); return; } diff --git a/server/utils/helpers/chat/convertTo.js b/server/utils/helpers/chat/convertTo.js index 5bd5b37e..2109ecbe 100644 --- a/server/utils/helpers/chat/convertTo.js +++ b/server/utils/helpers/chat/convertTo.js @@ -6,7 +6,7 @@ const { WorkspaceChats } = require("../../../models/workspaceChats"); // Todo: add RLHF feedbackScore field support async function convertToCSV(preparedData) { - const rows = ["id,username,workspace,prompt,response,sent_at"]; + const rows = ["id,username,workspace,prompt,response,sent_at,rating"]; for (const item of preparedData) { const record = [ item.id, @@ -15,6 +15,7 @@ async function convertToCSV(preparedData) { escapeCsv(item.prompt), escapeCsv(item.response), item.sent_at, + item.feedback, ].join(","); rows.push(record); } @@ -53,6 +54,12 @@ async function prepareWorkspaceChatsForExport(format = "jsonl") { prompt: chat.prompt, response: responseJson.text, sent_at: chat.createdAt, + feedback: + chat.feedbackScore === null + ? "--" + : chat.feedbackScore + ? "GOOD" + : "BAD", }; });