From ea216f5644fede9eb116fb30c8d0f9a84e4e829b Mon Sep 17 00:00:00 2001 From: Harish Mohan Raj Date: Tue, 19 Dec 2023 12:01:09 +0530 Subject: [PATCH] Create single team per chat (#67) --- main.wasp | 19 ++- .../migration.sql | 19 +++ .../migration.sql | 2 + src/client/chatConversationHelper.tsx | 75 ++++----- src/client/components/ConversationList.tsx | 96 +++++------ src/client/components/ConversationWrapper.tsx | 159 +++++++++--------- src/client/helpers.tsx | 7 +- src/server/actions.ts | 72 ++++---- src/server/queries.ts | 24 ++- src/server/webSocket.js | 29 +--- 10 files changed, 253 insertions(+), 249 deletions(-) create mode 100644 migrations/20231218010807_move_team_related_columns_to_chat_model/migration.sql create mode 100644 migrations/20231218111917_add_show_loader_column_to_chat_model/migration.sql diff --git a/main.wasp b/main.wasp index 83f1ed8..2c81180 100644 --- a/main.wasp +++ b/main.wasp @@ -121,6 +121,10 @@ entity Chat {=psl id Int @id @default(autoincrement()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + team_id Int? + team_name String? + team_status String? + showLoader Boolean @default(false) user User? @relation(fields: [userId], references: [id]) userId Int? name String? @@ -133,10 +137,6 @@ entity Conversation {=psl updatedAt DateTime @updatedAt message String role String - team_id Int? - team_name String? - team_status String? - is_question_from_agent Boolean @default(false) chat Chat? @relation(fields: [chatId], references: [id]) chatId Int? user User? @relation(fields: [userId], references: [id]) @@ -227,9 +227,9 @@ action addNewConversationToChat { entities: [Chat, Conversation] } -action updateExistingConversation { - fn: import { updateExistingConversation } from "@server/actions.js", - entities: [Chat, Conversation] +action updateExistingChat { + fn: import { updateExistingChat } from "@server/actions.js", + entities: [Chat] } action getAgentResponse { @@ -259,6 +259,11 @@ query getChats { entities: [Chat] } +query getChat { + fn: import { getChat } from "@server/queries.js", + entities: [Chat] +} + query getConversations { fn: import { getConversations } from "@server/queries.js", entities: [Conversation] diff --git a/migrations/20231218010807_move_team_related_columns_to_chat_model/migration.sql b/migrations/20231218010807_move_team_related_columns_to_chat_model/migration.sql new file mode 100644 index 0000000..ad5e1c2 --- /dev/null +++ b/migrations/20231218010807_move_team_related_columns_to_chat_model/migration.sql @@ -0,0 +1,19 @@ +/* + Warnings: + + - You are about to drop the column `is_question_from_agent` on the `Conversation` table. All the data in the column will be lost. + - You are about to drop the column `team_id` on the `Conversation` table. All the data in the column will be lost. + - You are about to drop the column `team_name` on the `Conversation` table. All the data in the column will be lost. + - You are about to drop the column `team_status` on the `Conversation` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "Chat" ADD COLUMN "team_id" INTEGER, +ADD COLUMN "team_name" TEXT, +ADD COLUMN "team_status" TEXT; + +-- AlterTable +ALTER TABLE "Conversation" DROP COLUMN "is_question_from_agent", +DROP COLUMN "team_id", +DROP COLUMN "team_name", +DROP COLUMN "team_status"; diff --git a/migrations/20231218111917_add_show_loader_column_to_chat_model/migration.sql b/migrations/20231218111917_add_show_loader_column_to_chat_model/migration.sql new file mode 100644 index 0000000..be19786 --- /dev/null +++ b/migrations/20231218111917_add_show_loader_column_to_chat_model/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Chat" ADD COLUMN "showLoader" BOOLEAN NOT NULL DEFAULT false; diff --git a/src/client/chatConversationHelper.tsx b/src/client/chatConversationHelper.tsx index 4bdfa95..6e8a27d 100644 --- a/src/client/chatConversationHelper.tsx +++ b/src/client/chatConversationHelper.tsx @@ -1,75 +1,62 @@ import getAgentResponse from "@wasp/actions/getAgentResponse"; import addNewConversationToChat from "@wasp/actions/addNewConversationToChat"; -import updateExistingConversation from "@wasp/actions/updateExistingConversation"; +import updateExistingChat from "@wasp/actions/updateExistingChat"; import { prepareOpenAIRequest } from "./helpers"; export async function addUserMessageToConversation( chat_id: number, - userQuery: string, - conv_id?: number, - team_name?: string, - team_id?: number + userQuery: string ) { let userMessage = userQuery; - let isAnswerToAgentQuestion = false; - let user_answer_to_team_id = null; - if (team_id) { - if (conv_id) { - const payload = { - chat_id: chat_id, - conv_id: conv_id, - is_question_from_agent: false, - team_status: null, - }; - await updateExistingConversation(payload); - } - userMessage = `

Replying to ${team_name}:



` + userQuery; - isAnswerToAgentQuestion = true; - user_answer_to_team_id = team_id; - } - const payload = { chat_id: chat_id, message: userMessage, role: "user", }; - const updatedConversation: any = await addNewConversationToChat(payload); - - const [messages, latestConversationID]: [ - messages: any, - latestConversationID: number - ] = prepareOpenAIRequest(updatedConversation); - return [ - messages, - latestConversationID, - isAnswerToAgentQuestion, - user_answer_to_team_id, - ]; + const messages: any = prepareOpenAIRequest(updatedConversation); + return messages; } export async function addAgentMessageToConversation( chat_id: number, message: any, - conv_id: number, - isAnswerToAgentQuestion: boolean, - userResponseToTeamId: number | null | undefined + team_id: number | null | undefined ) { const response: any = await getAgentResponse({ chat_id: chat_id, message: message, - conv_id: conv_id, - isAnswerToAgentQuestion: isAnswerToAgentQuestion, - userResponseToTeamId: userResponseToTeamId, + team_id: team_id, }); - const openAIResponse = { + // ...(response.team_name && { team_name: response.team_name }), + // ...(response.team_id && { team_id: response.team_id }), + // ...(response.team_status && { team_status: response.team_status }), + + if (response.team_name) { + const payload = { + chat_id: chat_id, + team_name: response.team_name, + team_id: response.team_id, + team_status: response.team_status, + }; + await updateExistingChat(payload); + } + + if (response.content) { + const openAIResponse = { + chat_id: Number(chat_id), + message: response.content, + role: "assistant", + }; + + await addNewConversationToChat(openAIResponse); + } + + return { chat_id: Number(chat_id), - message: response.content, - role: "assistant", ...(response.team_name && { team_name: response.team_name }), ...(response.team_id && { team_id: response.team_id }), ...(response.team_status && { team_status: response.team_status }), }; - return await addNewConversationToChat(openAIResponse); } diff --git a/src/client/components/ConversationList.tsx b/src/client/components/ConversationList.tsx index 3347573..46a3f45 100644 --- a/src/client/components/ConversationList.tsx +++ b/src/client/components/ConversationList.tsx @@ -4,22 +4,16 @@ import { useState } from "react"; import Markdown from "markdown-to-jsx"; import type { Conversation } from "@wasp/entities"; - import logo from "../static/captn-logo.png"; type ConversationsListProps = { conversations: Conversation[]; - onInlineFormSubmit: ( - userQuery: string, - conv_id: number, - team_name: string, - team_id: number - ) => void; + isLoading: boolean; }; export default function ConversationsList({ conversations, - onInlineFormSubmit, + isLoading, }: ConversationsListProps) { return (
@@ -62,18 +56,6 @@ export default function ConversationsList({ /> ); - const handleFormSubmit = ( - event: React.FormEvent, - conv_id: number, - team_name: string, - team_id: number - ) => { - event.preventDefault(); - const target = event.target as HTMLFormElement; - const userQuery = target.userQuery.value; - target.reset(); - onInlineFormSubmit(userQuery, conv_id, team_name, team_id); - }; return (
{conversation.message}
- {conversation.is_question_from_agent && ( -
- handleFormSubmit( - event, - conversation.id, - conversation.team_name, - conversation.team_id - ) - } - className="relative block w-full mt-[15px]" - > - -
- - -
-
- )}
); })} +
+
+ + captn logo + +
+ + I am presently navigating the waters of your request. +
+ Kindly stay anchored, and I will promptly return to you once I + have information to share. +
+
+
+
); } diff --git a/src/client/components/ConversationWrapper.tsx b/src/client/components/ConversationWrapper.tsx index 15b5d0e..c33690e 100644 --- a/src/client/components/ConversationWrapper.tsx +++ b/src/client/components/ConversationWrapper.tsx @@ -5,7 +5,9 @@ import { Redirect } from "react-router-dom"; import { useQuery } from "@wasp/queries"; import getConversations from "@wasp/queries/getConversations"; +import getChat from "@wasp/queries/getChat"; import { useSocket, useSocketListener } from "@wasp/webSocket"; +import updateExistingChat from "@wasp/actions/updateExistingChat"; import ConversationsList from "./ConversationList"; import Loader from "./Loader"; @@ -21,7 +23,19 @@ export default function ConversationWrapper() { const { id }: { id: string } = useParams(); const { socket, isConnected } = useSocket(); const [isLoading, setIsLoading] = useState(false); + const [formInputValue, setFormInputValue] = useState(""); const chatWindowRef = useRef(null); + const { + data: currentChatDetails, + refetch: refetchChat, + }: { data: any; refetch: any } = useQuery( + getChat, + { + chatId: Number(id), + }, + { enabled: !!id } + ); + const [isSubmitButtonDisabled, setIsSubmitButtonDisabled] = useState(false); const { data: conversations, refetch } = useQuery( getConversations, { @@ -31,94 +45,83 @@ export default function ConversationWrapper() { ); const googleRedirectLoginMsg: any = getQueryParam("msg"); - const googleRedirectLoginTeamName: any = getQueryParam("team_name"); - const googleRedirectLoginTeadId: any = getQueryParam("team_id"); const formInputRef = useCallback( async (node: any) => { - if ( - node !== null && - googleRedirectLoginMsg && - googleRedirectLoginTeamName && - googleRedirectLoginTeadId - ) { - await addMessagesToConversation( - googleRedirectLoginMsg, - undefined, - googleRedirectLoginTeamName, - googleRedirectLoginTeadId - ); + if (node !== null && googleRedirectLoginMsg) { + await addMessagesToConversation(googleRedirectLoginMsg); } }, - [ - googleRedirectLoginMsg, - googleRedirectLoginTeamName, - googleRedirectLoginTeadId, - ] + [googleRedirectLoginMsg] ); + useEffect(() => { + if (currentChatDetails) { + setIsSubmitButtonDisabled( + currentChatDetails.team_status === "inprogress" || + currentChatDetails.showLoader + ); + setIsLoading(currentChatDetails.showLoader); + } + }, [currentChatDetails]); + useSocketListener("newConversationAddedToDB", reFetchConversations); function reFetchConversations() { refetch(); + refetchChat(); } + useEffect(() => { + setFormInputValue(""); + }, [id]); + useEffect(() => { scrollToBottom(); + refetchChat(); }, [conversations]); const scrollToBottom = () => { if (chatWindowRef.current) { - // @ts-ignore - chatWindowRef.current.scrollTo({ + // Delay the scrolling animation by 500ms + setTimeout(() => { + // Scroll to the bottom with a smooth behavior // @ts-ignore - top: chatWindowRef.current.scrollHeight, - behavior: "smooth", - }); + chatWindowRef.current.scrollTo({ + // @ts-ignore + top: chatWindowRef.current.scrollHeight, + behavior: "smooth", + }); + }, 200); } }; - async function addMessagesToConversation( - userQuery: string, - conv_id?: number, - team_name?: string, - team_id?: number - ) { + async function addMessagesToConversation(userQuery: string) { try { - const [ - messages, - conversation_id, - isAnswerToAgentQuestion, - user_answer_to_team_id, - ] = await addUserMessageToConversation( + const messages = await addUserMessageToConversation( Number(id), - userQuery, - conv_id, - team_name, - team_id + userQuery ); - setIsLoading(true); - const updatedConversations: any = await addAgentMessageToConversation( + // setIsLoading(true); + await updateExistingChat({ chat_id: Number(id), showLoader: true }); + const teamId = googleRedirectLoginMsg + ? Number(id) + : // @ts-ignore + currentChatDetails?.team_id; + const response: any = await addAgentMessageToConversation( Number(id), messages, - conversation_id, - isAnswerToAgentQuestion, - user_answer_to_team_id + teamId ); - - const lastConversation = - updatedConversations[updatedConversations.length - 1]; - if (lastConversation.team_status === "inprogress") { - socket.emit( - "newConversationAdded", - lastConversation.team_id, - lastConversation.id, - lastConversation.chatId - ); + if (response.team_status === "inprogress") { + // setIsSubmitButtonDisabled(true); + socket.emit("newConversationAdded", response.chat_id); } - setIsLoading(false); + // setIsLoading(false); + await updateExistingChat({ chat_id: Number(id), showLoader: false }); } catch (err: any) { - setIsLoading(false); + // setIsLoading(false); + await updateExistingChat({ chat_id: Number(id), showLoader: false }); console.log("Error: " + err.message); window.alert("Error: Something went wrong. Please try again later."); } @@ -128,20 +131,12 @@ export default function ConversationWrapper() { event.preventDefault(); const target = event.target as HTMLFormElement; const userQuery = target.userQuery.value; - target.reset(); + // target.reset(); + setFormInputValue(""); await addMessagesToConversation(userQuery); }; - const handleInlineFormSubmit = async ( - userQuery: string, - conv_id: number, - team_name: string, - team_id: number - ) => { - await addMessagesToConversation(userQuery, conv_id, team_name, team_id); - }; - - const chatContainerClass = `flex h-full flex-col items-center justify-between pb-24 overflow-y-auto bg-captn-light-blue ${ + const chatContainerClass = `flex h-full flex-col items-center justify-between overflow-y-auto bg-captn-light-blue ${ isLoading ? "opacity-40" : "opacity-100" }`; @@ -158,22 +153,21 @@ export default function ConversationWrapper() {
-
-
+
+
{conversations && ( )}
{isLoading && } {id ? ( -
+
-
+
setFormInputValue(e.target.value)} /> diff --git a/src/client/helpers.tsx b/src/client/helpers.tsx index 35d701e..7183e63 100644 --- a/src/client/helpers.tsx +++ b/src/client/helpers.tsx @@ -33,17 +33,14 @@ function getLatestConversationID(input: InputMessage[]): number { return latestConversationID; } -export function prepareOpenAIRequest( - input: InputMessage[] -): [OutputMessage[], number] { +export function prepareOpenAIRequest(input: InputMessage[]): OutputMessage[] { const messages: OutputMessage[] = input.map((message) => { return { role: message.role, content: message.message, }; }); - const latestConversationID = getLatestConversationID(input); - return [messages, latestConversationID]; + return messages; } // A custom hook that builds on useLocation to parse diff --git a/src/server/actions.ts b/src/server/actions.ts index 75a2f87..359f8b8 100644 --- a/src/server/actions.ts +++ b/src/server/actions.ts @@ -7,7 +7,7 @@ import type { StripePayment, CreateChat, AddNewConversationToChat, - UpdateExistingConversation, + UpdateExistingChat, GetAgentResponse, } from "@wasp/actions/types"; import type { StripePaymentResult, OpenAIResponse } from "./types"; @@ -107,9 +107,6 @@ type AddNewConversationToChatPayload = { message: string; role: string; chat_id: number; - team_name?: string; - team_id?: number; - team_status?: string; }; export const addNewConversationToChat: AddNewConversationToChat< @@ -126,9 +123,9 @@ export const addNewConversationToChat: AddNewConversationToChat< role: args.role, chat: { connect: { id: args.chat_id } }, user: { connect: { id: context.user.id } }, - ...(args.team_name && { team_name: args.team_name }), - ...(args.team_id && { team_id: args.team_id }), - ...(args.team_status && { team_status: args.team_status }), + // ...(args.team_name && { team_name: args.team_name }), + // ...(args.team_id && { team_id: args.team_id }), + // ...(args.team_status && { team_status: args.team_status }), }, }); @@ -138,52 +135,59 @@ export const addNewConversationToChat: AddNewConversationToChat< }); }; -type UpdateExistingConversationPayload = { +type UpdateExistingChatPayload = { chat_id: number; - conv_id: number; - is_question_from_agent: boolean; - team_status: null; + team_name?: string; + team_id?: number; + team_status?: boolean; + showLoader?: boolean; }; -export const updateExistingConversation: UpdateExistingConversation< - UpdateExistingConversationPayload, +export const updateExistingChat: UpdateExistingChat< + UpdateExistingChatPayload, void -> = async (args, context) => { +> = async (args: any, context: any) => { if (!context.user) { throw new HttpError(401); } - await context.entities.Conversation.update({ - where: { - id: args.conv_id, - }, - data: { - team_status: args.team_status, - is_question_from_agent: args.is_question_from_agent, - }, - }); + if (args.showLoader === true || args.showLoader === false) { + await context.entities.Chat.update({ + where: { + id: args.chat_id, + }, + data: { + showLoader: args.showLoader, + }, + }); + } else { + await context.entities.Chat.update({ + where: { + id: args.chat_id, + }, + data: { + team_id: args.team_id, + team_name: args.team_name, + team_status: args.team_status, + }, + }); + } }; type AgentPayload = { chat_id: number; message: any; - conv_id: number; - isAnswerToAgentQuestion: boolean; - userResponseToTeamId: number | null | undefined; + team_id: number | null | undefined; }; export const getAgentResponse: GetAgentResponse = async ( { chat_id, message, - conv_id, - isAnswerToAgentQuestion, - userResponseToTeamId, + team_id, }: { chat_id: number; message: any; - conv_id: number; - isAnswerToAgentQuestion: boolean; - userResponseToTeamId: number | null | undefined; + team_id: number | null | undefined; }, context: any ) => { @@ -194,10 +198,8 @@ export const getAgentResponse: GetAgentResponse = async ( const payload = { chat_id: chat_id, message: message, - conv_id: conv_id, user_id: context.user.id, - is_answer_to_agent_question: isAnswerToAgentQuestion, - user_answer_to_team_id: userResponseToTeamId, + team_id: team_id, }; console.log("==========="); console.log("Payload to Python server"); diff --git a/src/server/queries.ts b/src/server/queries.ts index 3d10fc7..335b839 100644 --- a/src/server/queries.ts +++ b/src/server/queries.ts @@ -1,7 +1,7 @@ import HttpError from "@wasp/core/HttpError.js"; import type { Chat, Conversation } from "@wasp/entities"; -import type { GetChats, GetConversations } from "@wasp/queries/types"; +import type { GetChats, GetChat, GetConversations } from "@wasp/queries/types"; export const getChats: GetChats = async (args, context) => { if (!context.user) { @@ -17,6 +17,28 @@ export const getChats: GetChats = async (args, context) => { }); }; +type GetChatPayload = { + chatId: number; +}; + +export const getChat: GetChat = async ( + args: any, + context: any +) => { + if (!context.user) { + throw new HttpError(401); + } + if (!!args.chatId) { + return context.entities.Chat.findUnique({ + where: { + id: args.chatId, + }, + }); + } else { + return null; + } +}; + type GetConversationPayload = { chatId: number; }; diff --git a/src/server/webSocket.js b/src/server/webSocket.js index ce0542b..6735540 100644 --- a/src/server/webSocket.js +++ b/src/server/webSocket.js @@ -2,13 +2,13 @@ import HttpError from "@wasp/core/HttpError.js"; import { ADS_SERVER_URL } from "./config.js"; -async function checkTeamStatus(context, socket, team_id, conv_id, chat_id) { +async function checkTeamStatus(context, socket, chat_id) { let json; try { while (true) { // Infinite loop, adjust the exit condition as needed const payload = { - team_id: team_id, + team_id: chat_id, }; const response = await fetch(`${ADS_SERVER_URL}/openai/get-team-status`, { method: "POST", @@ -17,8 +17,7 @@ async function checkTeamStatus(context, socket, team_id, conv_id, chat_id) { }); if (!response.ok) { - const errorMsg = - json.detail || `HTTP error with status code ${response.status}`; + const errorMsg = `HTTP error with status code ${response.status}`; console.error("Server Error:", errorMsg); } else { json = await response.json(); @@ -33,24 +32,18 @@ async function checkTeamStatus(context, socket, team_id, conv_id, chat_id) { await new Promise((resolve) => setTimeout(resolve, 1000)); } // Call another function after breaking the loop - await updateConversationsInDb(context, socket, json, conv_id, chat_id); + await updateConversationsInDb(context, socket, json, chat_id); } catch (error) { console.log(`Error while fetching record`); console.log(error); } } -async function updateConversationsInDb( - context, - socket, - json, - conv_id, - chat_id -) { - await context.entities.Conversation.update({ +async function updateConversationsInDb(context, socket, json, chat_id) { + await context.entities.Chat.update({ where: { // userId: socket.data.user.id, - id: conv_id, + id: chat_id, }, data: { team_status: null, @@ -61,10 +54,6 @@ async function updateConversationsInDb( data: { message: json["msg"], role: "assistant", - team_name: json["team_name"], - team_id: Number(json["team_id"]), - team_status: json["team_status"], - is_question_from_agent: json["is_question"], chat: { connect: { id: chat_id } }, user: { connect: { id: socket.data.user.id } }, }, @@ -81,8 +70,8 @@ export const checkTeamStatusAndUpdateInDB = (io, context) => { console.log("========"); console.log("a user connected: ", userEmail); - socket.on("newConversationAdded", async (team_id, conv_id, chat_id) => { - await checkTeamStatus(context, socket, team_id, conv_id, chat_id); + socket.on("newConversationAdded", async (chat_id) => { + await checkTeamStatus(context, socket, chat_id); }); } });