From dcd1e081fa81c7f58d31a4a3d3a44f563ecabf82 Mon Sep 17 00:00:00 2001 From: Harish Mohan Raj Date: Wed, 29 Nov 2023 11:07:25 +0530 Subject: [PATCH] WIP --- main.wasp | 44 ++-- .../migration.sql | 27 +++ .../migration.sql | 2 + src/client/AccountPage.tsx | 90 ++++++--- src/client/PricingPage.tsx | 81 +++++--- src/client/components/ConversationList.tsx | 136 ++++++------- src/client/components/ConversationWrapper.tsx | 123 +++-------- src/client/helpers.tsx | 50 +++++ src/client/tests/helpers.test.tsx | 191 ++++++++++++++++++ src/server/actions.ts | 171 +++------------- src/server/config.js | 4 + src/server/queries.ts | 65 ++---- src/server/webSocket.js | 126 ++++++------ 13 files changed, 611 insertions(+), 499 deletions(-) create mode 100644 migrations/20231128104925_save_each_conversation_as_a_new_entry_in_the_conversation_model/migration.sql create mode 100644 migrations/20231128124202_make_type_field_as_optional_in_conversation_model/migration.sql create mode 100644 src/client/helpers.tsx create mode 100644 src/client/tests/helpers.test.tsx diff --git a/main.wasp b/main.wasp index e076da7..332ac90 100644 --- a/main.wasp +++ b/main.wasp @@ -68,9 +68,9 @@ app chatApp { ("stripe", "11.15.0"), ("markdown-to-jsx", "7.3.2"), ], - webSocket: { - fn: import { webSocketFn } from "@server/webSocket.js" - }, + // webSocket: { + // fn: import { webSocketFn } from "@server/webSocket.js" + // }, } /* 💽 Wasp defines DB entities via Prisma Database Models: @@ -128,15 +128,25 @@ entity Chat {=psl psl=} entity Conversation {=psl - id Int @id @default(autoincrement()) - conversation Json - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - status String? - chat Chat? @relation(fields: [chatId], references: [id]) - chatId Int? - user User? @relation(fields: [userId], references: [id]) - userId Int? + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + message String + role String + type String? + team_id Int? + team_name String? + team_status String? + replyToConversationId Int? + replyToConversation Conversation? @relation("replyToConversation", fields: [replyToConversationId], references: [id]) + replyToConversationRecord Conversation[] @relation("replyToConversation") + previousConversationId Int? + previousConversation Conversation? @relation("previousConversation", fields: [previousConversationId], references: [id]) + previousConversationRecord Conversation[] @relation("previousConversation") + chat Chat? @relation(fields: [chatId], references: [id]) + chatId Int? + user User? @relation(fields: [userId], references: [id]) + userId Int? psl=} @@ -208,12 +218,6 @@ page ChatPage { // 📝 Actions aka Mutations -action generateGptResponse { - fn: import { generateGptResponse } from "@server/actions.js", - // entities: [User, RelatedObject] - entities: [User] -} - action stripePayment { fn: import { stripePayment } from "@server/actions.js", entities: [User] @@ -224,8 +228,8 @@ action createChat { entities: [Chat, Conversation] } -action updateConversation { - fn: import { updateConversation } from "@server/actions.js", +action addNewConversationToChat { + fn: import { addNewConversationToChat } from "@server/actions.js", entities: [Chat, Conversation] } diff --git a/migrations/20231128104925_save_each_conversation_as_a_new_entry_in_the_conversation_model/migration.sql b/migrations/20231128104925_save_each_conversation_as_a_new_entry_in_the_conversation_model/migration.sql new file mode 100644 index 0000000..806695a --- /dev/null +++ b/migrations/20231128104925_save_each_conversation_as_a_new_entry_in_the_conversation_model/migration.sql @@ -0,0 +1,27 @@ +/* + Warnings: + + - You are about to drop the column `conversation` on the `Conversation` table. All the data in the column will be lost. + - You are about to drop the column `status` on the `Conversation` table. All the data in the column will be lost. + - Added the required column `message` to the `Conversation` table without a default value. This is not possible if the table is not empty. + - Added the required column `role` to the `Conversation` table without a default value. This is not possible if the table is not empty. + - Added the required column `type` to the `Conversation` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "Conversation" DROP COLUMN "conversation", +DROP COLUMN "status", +ADD COLUMN "message" TEXT NOT NULL, +ADD COLUMN "previousConversationId" INTEGER, +ADD COLUMN "replyToConversationId" INTEGER, +ADD COLUMN "role" TEXT NOT NULL, +ADD COLUMN "team_id" INTEGER, +ADD COLUMN "team_name" TEXT, +ADD COLUMN "team_status" TEXT, +ADD COLUMN "type" TEXT NOT NULL; + +-- AddForeignKey +ALTER TABLE "Conversation" ADD CONSTRAINT "Conversation_replyToConversationId_fkey" FOREIGN KEY ("replyToConversationId") REFERENCES "Conversation"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Conversation" ADD CONSTRAINT "Conversation_previousConversationId_fkey" FOREIGN KEY ("previousConversationId") REFERENCES "Conversation"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/migrations/20231128124202_make_type_field_as_optional_in_conversation_model/migration.sql b/migrations/20231128124202_make_type_field_as_optional_in_conversation_model/migration.sql new file mode 100644 index 0000000..d2da36b --- /dev/null +++ b/migrations/20231128124202_make_type_field_as_optional_in_conversation_model/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Conversation" ALTER COLUMN "type" DROP NOT NULL; diff --git a/src/client/AccountPage.tsx b/src/client/AccountPage.tsx index e8b7c5d..4277c33 100644 --- a/src/client/AccountPage.tsx +++ b/src/client/AccountPage.tsx @@ -1,12 +1,13 @@ -import { User } from '@wasp/entities'; -import { useQuery } from '@wasp/queries' +import { User } from "@wasp/entities"; +import { useQuery } from "@wasp/queries"; // import getRelatedObjects from '@wasp/queries/getRelatedObjects' -import logout from '@wasp/auth/logout'; -import stripePayment from '@wasp/actions/stripePayment'; -import { useState, Dispatch, SetStateAction } from 'react'; +import logout from "@wasp/auth/logout"; +import stripePayment from "@wasp/actions/stripePayment"; +import { useState, Dispatch, SetStateAction } from "react"; // get your own link from your stripe dashboard: https://dashboard.stripe.com/settings/billing/portal -const CUSTOMER_PORTAL_LINK = 'https://billing.stripe.com/p/login/test_8wM8x17JN7DT4zC000'; +const CUSTOMER_PORTAL_LINK = + "https://billing.stripe.com/p/login/test_8wM8x17JN7DT4zC000"; export default function Example({ user }: { user: User }) { const [isLoading, setIsLoading] = useState(false); @@ -14,16 +15,22 @@ export default function Example({ user }: { user: User }) { // const { data: relatedObjects, isLoading: isLoadingRelatedObjects } = useQuery(getRelatedObjects) return ( -
-
-
-

Account Information

+
+
+
+

+ Account Information +

-
-
-
-
Email address
-
{user.email}
+
+
+
+
+ Email address +
+
+ {user.email} +
{/*
Your Plan
@@ -46,8 +53,8 @@ export default function Example({ user }: { user: User }) {
I'm a cool customer.
*/} {/*
*/} - {/*
Most Recent User RelatedObject
*/} - {/*
+ {/*
Most Recent User RelatedObject
*/} + {/*
{!!relatedObjects && relatedObjects.length > 0 ? relatedObjects[relatedObjects.length - 1].content : "You don't have any at this time."} @@ -56,10 +63,10 @@ export default function Example({ user }: { user: User }) {
-
+
@@ -68,42 +75,63 @@ export default function Example({ user }: { user: User }) { ); } -function BuyMoreButton({ isLoading, setIsLoading }: { isLoading: boolean, setIsLoading: Dispatch> }) { +function BuyMoreButton({ + isLoading, + setIsLoading, +}: { + isLoading: boolean; + setIsLoading: Dispatch>; +}) { const handleClick = async () => { try { setIsLoading(true); const stripeResults = await stripePayment(); if (stripeResults?.sessionUrl) { - window.open(stripeResults.sessionUrl, '_self'); + window.open(stripeResults.sessionUrl, "_self"); } - } catch (error: any) { - alert(error?.message ?? 'Something went wrong.') + alert(error?.message ?? "Something went wrong."); } finally { setIsLoading(false); } }; return ( -
-
); } -function CustomerPortalButton({ isLoading, setIsLoading }: { isLoading: boolean, setIsLoading: Dispatch> }) { +function CustomerPortalButton({ + isLoading, + setIsLoading, +}: { + isLoading: boolean; + setIsLoading: Dispatch>; +}) { const handleClick = () => { setIsLoading(true); - window.open(CUSTOMER_PORTAL_LINK, '_blank'); + window.open(CUSTOMER_PORTAL_LINK, "_blank"); setIsLoading(false); }; return ( -
-
); diff --git a/src/client/PricingPage.tsx b/src/client/PricingPage.tsx index 7566a39..5a93c19 100644 --- a/src/client/PricingPage.tsx +++ b/src/client/PricingPage.tsx @@ -1,24 +1,28 @@ -import { AiOutlineCheck } from 'react-icons/ai'; -import stripePayment from '@wasp/actions/stripePayment'; -import { useState } from 'react'; +import { AiOutlineCheck } from "react-icons/ai"; +import stripePayment from "@wasp/actions/stripePayment"; +import { useState } from "react"; const prices = [ { - name: 'Credits', - id: 'credits', - href: '', - price: '$2.95', - description: 'Buy credits to use for your projects.', - features: ['10 credits', 'Use them any time', 'No expiration date'], + name: "Credits", + id: "credits", + href: "", + price: "$2.95", + description: "Buy credits to use for your projects.", + features: ["10 credits", "Use them any time", "No expiration date"], disabled: true, }, { - name: 'Monthly Subscription', - id: 'monthly', - href: '#', - priceMonthly: '$9.99', - description: 'Get unlimited usage for your projects.', - features: ['Unlimited usage of all features', 'Priority support', 'Cancel any time'], + name: "Monthly Subscription", + id: "monthly", + href: "#", + priceMonthly: "$9.99", + description: "Get unlimited usage for your projects.", + features: [ + "Unlimited usage of all features", + "Priority support", + "Cancel any time", + ], }, ]; @@ -30,43 +34,55 @@ export default function PricingPage() { try { const response = await stripePayment(); if (response?.sessionUrl) { - window.open(response.sessionUrl, '_self'); + window.open(response.sessionUrl, "_self"); } } catch (e) { - alert('Something went wrong. Please try again.'); + alert("Something went wrong. Please try again."); console.error(e); } finally { setIsLoading(false); } }; - return ( -
-
-
+
+
+
{prices.map((price) => (
-

+

{price.name}

-
- +
+ {price.priceMonthly || price.price} {price.priceMonthly && ( - /month + + /month + )}
-

{price.description}

-
    +

    + {price.description} +

    +
      {price.features.map((feature) => ( -
    • -
    • +
    • ))} @@ -77,10 +93,11 @@ export default function PricingPage() { aria-describedby={price.id} disabled={price.disabled} className={`${ - price.disabled && 'disabled:opacity-25 disabled:cursor-not-allowed' + price.disabled && + "disabled:opacity-25 disabled:cursor-not-allowed" } mt-8 block rounded-md bg-yellow-400 px-3.5 py-2 text-center text-sm font-semibold leading-6 text-black shadow-sm hover:bg-yellow-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-yellow-600`} > - {isLoading ? 'Loading...' : 'Buy Now'} + {isLoading ? "Loading..." : "Buy Now"}
))} diff --git a/src/client/components/ConversationList.tsx b/src/client/components/ConversationList.tsx index 508b930..06e92ae 100644 --- a/src/client/components/ConversationList.tsx +++ b/src/client/components/ConversationList.tsx @@ -4,82 +4,82 @@ import type { Conversation } from "@wasp/entities"; import logo from "../static/captn-logo.png"; -export default function ConversationsList(conversations: Conversation[]) { +type ConversationsListType = { + conversations: Conversation[]; +}; + +export default function ConversationsList( + conversations: ConversationsListType +) { return (
- { - // Todo: remove the below ignore comment - // @ts-ignore - conversations.conversations.map((conversation, idx) => { - const conversationBgColor = - conversation.role === "user" - ? "captn-light-blue" - : "captn-dark-blue"; - const conversationTextColor = - conversation.role === "user" - ? "captn-dark-blue" - : "captn-light-cream"; - const conversationLogo = - conversation.role === "user" ? ( -
-
You
-
- ) : ( - captn logo - ); - return ( -
+ {conversations.conversations.map((conversation, idx) => { + const conversationBgColor = + conversation.role === "user" ? "captn-light-blue" : "captn-dark-blue"; + const conversationTextColor = + conversation.role === "user" + ? "captn-dark-blue" + : "captn-light-cream"; + const conversationLogo = + conversation.role === "user" ? ( +
+
You
+
+ ) : ( + captn logo + ); + return ( +
+
-
- - {conversationLogo} - -
- {conversation.content} -
+ {conversationLogo} + +
+ {conversation.message}
- ); - }) - } +
+ ); + })}
); } diff --git a/src/client/components/ConversationWrapper.tsx b/src/client/components/ConversationWrapper.tsx index d9bc096..09f15f1 100644 --- a/src/client/components/ConversationWrapper.tsx +++ b/src/client/components/ConversationWrapper.tsx @@ -4,12 +4,14 @@ import { useParams } from "react-router"; import { Redirect, useLocation } from "react-router-dom"; import { useQuery } from "@wasp/queries"; -import updateConversation from "@wasp/actions/updateConversation"; -import getAgentResponse from "@wasp/actions/getAgentResponse"; import getConversations from "@wasp/queries/getConversations"; +import getAgentResponse from "@wasp/actions/getAgentResponse"; +import addNewConversationToChat from "@wasp/actions/addNewConversationToChat"; +import type { Conversation } from "@wasp/entities"; import ConversationsList from "./ConversationList"; import Loader from "./Loader"; +import { prepareOpenAIRequest } from "../helpers"; // A custom hook that builds on useLocation to parse // the query string for you. @@ -20,48 +22,9 @@ function getQueryParam(paramName: string) { ); } -export function setRedirectMsg(formInputRef: any, loginMsgQuery: string) { - if (loginMsgQuery) { - formInputRef.value = decodeURIComponent(loginMsgQuery); - } -} - -export function triggerSubmit( - node: any, - loginMsgQuery: string, - formInputRef: any -) { - if (loginMsgQuery && formInputRef && formInputRef.value !== "") { - node.click(); - } -} - export default function ConversationWrapper() { - // Todo: remove the below ignore comment - // @ts-ignore - const { id } = useParams(); + const { id }: { id: string } = useParams(); const [isLoading, setIsLoading] = useState(false); - const chatContainerRef = useRef(null); - - const loginMsgQuery: any = getQueryParam("msg"); - const formInputRef = useCallback( - (node: any) => { - if (node !== null) { - setRedirectMsg(node, loginMsgQuery); - } - }, - [loginMsgQuery] - ); - - const submitBtnRef = useCallback( - (node: any) => { - if (node !== null) { - triggerSubmit(node, loginMsgQuery, formInputRef); - } - }, - [loginMsgQuery, formInputRef] - ); - const { data: conversations, isLoading: isConversationLoading, @@ -71,50 +34,43 @@ export default function ConversationWrapper() { { chatId: Number(id), }, - { enabled: !!id, refetchInterval: 1000 } + { enabled: !!id } + // { enabled: !!id, refetchInterval: 1000 } ); - useEffect(() => { - // if (chatContainerRef.current) { - // // Todo: remove the below ignore comment - // // @ts-ignore - // chatContainerRef.current.scrollTop = - // // Todo: remove the below ignore comment - // // @ts-ignore - // chatContainerRef.current.scrollHeight; - // } - }, [conversations]); - async function callAgent(userQuery: string) { try { // 1. add new conversation to table const payload = { // @ts-ignore - conversation_id: conversations.id, - conversations: [...[{ role: "user", content: userQuery }]], + chat_id: Number(id), + message: userQuery, + role: "user", }; - const updatedConversation = await updateConversation(payload); + const updatedConversation: Conversation[] = + await addNewConversationToChat(payload); + const [message, conv_id, is_answer_to_agent_question]: [ + message: any, + conv_id: number, + is_answer_to_agent_question: boolean + ] = prepareOpenAIRequest(updatedConversation); // 2. call backend python server to get agent response setIsLoading(true); - const response = await getAgentResponse({ - message: updatedConversation["conversation"], - conv_id: updatedConversation.id, - // @ts-ignore - is_answer_to_agent_question: updatedConversation.status === "pause", + const response: any = await getAgentResponse({ + message: message, + conv_id: conv_id, + is_answer_to_agent_question: is_answer_to_agent_question, }); // 3. add agent response as new conversation in the table const openAIResponse = { - // @ts-ignore - conversation_id: conversations.id, - conversations: [ - // @ts-ignore - ...[{ role: "assistant", content: response.content }], - ], + chat_id: Number(id), + message: response.content, + role: "assistant", // @ts-ignore ...(response.team_status && { status: response.team_status }), }; - await updateConversation(openAIResponse); + await addNewConversationToChat(openAIResponse); setIsLoading(false); } catch (err: any) { setIsLoading(false); @@ -124,25 +80,12 @@ export default function ConversationWrapper() { const handleFormSubmit = async (event: React.FormEvent) => { event.preventDefault(); - const target = event.target; - // Todo: remove the below ignore comment - // @ts-ignore + const target = event.target as HTMLFormElement; const userQuery = target.userQuery.value; - // Todo: remove the below ignore comment - // @ts-ignore target.reset(); await callAgent(userQuery); }; - if (isConversationLoading && !!id) return ; - if (isConversationError) { - return ( - <> - - - ); - } - const chatContainerClass = `flex h-full flex-col items-center justify-between pb-24 overflow-y-auto bg-captn-light-blue ${ isLoading ? "opacity-40" : "opacity-100" }`; @@ -152,15 +95,9 @@ export default function ConversationWrapper() {
-
+
{conversations && ( - // Todo: remove the below ignore comment - // @ts-ignore - + )}
{isLoading && } @@ -181,10 +118,10 @@ export default function ConversationWrapper() { className="block w-full p-4 pl-5 text-sm text-captn-light-cream border border-gray-300 rounded-lg bg-captn-dark-blue focus:ring-blue-500 focus:border-blue-500 dark:bg-captn-dark-blue dark:border-gray-600 dark:placeholder-gray-400 dark:text-captn-light-cream dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Send a message" required - ref={formInputRef} + // ref={formInputRef} />