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 (
-
-
-
+
+
- {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",
};
});