diff --git a/package.json b/package.json index 3ce89cc..fef43e6 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dependencies": { "@headlessui/react": "^1.7.13", "@heroicons/react": "^2.0.17", + "@mantine/form": "^6.0.7", "@prisma/client": "^4.11.0", "@supabase/auth-helpers-nextjs": "^0.6.0", "@supabase/auth-helpers-react": "^0.3.1", @@ -24,6 +25,7 @@ "@trpc/next": "^10.18.0", "@trpc/react-query": "^10.18.0", "@trpc/server": "^10.18.0", + "axios": "^1.3.5", "langchain": "^0.0.55", "next": "^13.2.4", "react": "18.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 936e909..46b6379 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,9 @@ dependencies: '@heroicons/react': specifier: ^2.0.17 version: 2.0.17(react@18.2.0) + '@mantine/form': + specifier: ^6.0.7 + version: 6.0.7(react@18.2.0) '@prisma/client': specifier: ^4.11.0 version: 4.11.0(prisma@4.11.0) @@ -43,6 +46,9 @@ dependencies: '@trpc/server': specifier: ^10.18.0 version: 10.18.0 + axios: + specifier: ^1.3.5 + version: 1.3.5 langchain: specifier: ^0.0.55 version: 0.0.55(@supabase/supabase-js@2.15.0) @@ -244,6 +250,16 @@ packages: '@jridgewell/resolve-uri': 3.1.0 '@jridgewell/sourcemap-codec': 1.4.14 + /@mantine/form@6.0.7(react@18.2.0): + resolution: {integrity: sha512-5fApVmV9gqqh0h04KkeLzZ79LCBMt91Nydj/h1uzFNluiIxcLCpN/VrOvqiG9DbvIi6h6yeBP/7rbZheHjuXgg==} + peerDependencies: + react: '>=16.8.0' + dependencies: + fast-deep-equal: 3.1.3 + klona: 2.0.6 + react: 18.2.0 + dev: false + /@next/env@13.2.4: resolution: {integrity: sha512-+Mq3TtpkeeKFZanPturjcXt+KHfKYnLlX6jMLyCrmpq6OOs4i1GqBOAauSkii9QeKCMTYzGppar21JU57b/GEA==} dev: false @@ -969,6 +985,16 @@ packages: - debug dev: false + /axios@1.3.5: + resolution: {integrity: sha512-glL/PvG/E+xCWwV8S6nCHcrfg1exGx7vxyUIivIA1iL7BIh6bePylCfVHwp6k13ao7SATxB6imau2kqY+I67kw==} + dependencies: + follow-redirects: 1.15.2 + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + dev: false + /axobject-query@3.1.1: resolution: {integrity: sha512-goKlv8DZrK9hUh975fnHzhNIO4jUnFCfv/dszV5VwUGDFjI6vQ2VwoyjYjYNEbBE8AH87TduWP5uyDR1D+Iteg==} dependencies: @@ -1672,7 +1698,6 @@ packages: /fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - dev: true /fast-glob@3.2.12: resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==} @@ -2220,6 +2245,11 @@ packages: object.assign: 4.1.4 dev: true + /klona@2.0.6: + resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==} + engines: {node: '>= 8'} + dev: false + /langchain@0.0.55(@supabase/supabase-js@2.15.0): resolution: {integrity: sha512-ScL53LvBm2X0rIO1fdMLEoCFYESLVTmY0d71qX7qDrB1y8Y8nCtCA1ZiUNYl4WDQeEvKcvB39qWmAJ2XcB8tqQ==} engines: {node: '>=18'} @@ -2891,6 +2921,10 @@ packages: object-assign: 4.1.1 react-is: 16.13.1 + /proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + dev: false + /punycode@2.3.0: resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} engines: {node: '>=6'} diff --git a/py_server/db/supa.py b/py_server/db/supa.py index 7de1b05..5822ba9 100644 --- a/py_server/db/supa.py +++ b/py_server/db/supa.py @@ -22,4 +22,19 @@ class SupaService: "url": url, "user_id": user_id }).execute() - return result \ No newline at end of file + return result + + + + def find_website(self, id: str, user_id: str): + result = self.supabase.table("Website").select("*").eq("id", id).eq("user_id", user_id).execute() + return result + + + def get_user(self, jwt: str): + try: + result = self.supabase.auth.get_user(jwt) + return result + except: + return None + \ No newline at end of file diff --git a/py_server/handlers/chat.py b/py_server/handlers/chat.py index 4a6f469..fc169a9 100644 --- a/py_server/handlers/chat.py +++ b/py_server/handlers/chat.py @@ -1,4 +1,4 @@ -from models import ChatBody +from models import ChatBody, ChatAppBody from bs4 import BeautifulSoup from langchain.docstore.document import Document as LDocument @@ -14,21 +14,43 @@ from langchain.prompts.chat import ( ) from langchain.vectorstores import Chroma +from db.supa import SupaService -async def chat_extension_handler(body: ChatBody): + +supabase = SupaService() + + + +async def chat_app_handler(body: ChatAppBody, jwt: str): try: - soup = BeautifulSoup(body.html, 'lxml') - iframe = soup.find('iframe', id='pageassist-iframe') - if iframe: - iframe.decompose() - div = soup.find('div', id='pageassist-icon') - if div: - div.decompose() - div = soup.find('div', id='__plasmo-loading__') - if div: - div.decompose() - text = soup.get_text() + + user = supabase.get_user(jwt) + + if not user: + return { + "bot_response": "You are not logged in", + "human_message": body.user_message, + } + + + user_id = user.user.id + + + website_response = supabase.find_website(body.id, user_id) + + website = website_response.data + + if len(website) == 0: + return { + "bot_response": "Website not found", + "human_message": body.user_message, + } + + website = website[0] + + + text = website["html"] result = [LDocument(page_content=text, metadata={"source": "test"})] token_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0) @@ -43,7 +65,76 @@ async def chat_extension_handler(body: ChatBody): messages = [ SystemMessagePromptTemplate.from_template("""You are PageAssist bot. Use the following pieces of context from this webpage to answer the question from the user. If you don't know the answer, just say you don't know. DO NOT try to make up an answer. -If user want recommendation, helping based on the context then help the user. +If user want recommendation, help from the context, or any other information, please provide it. +If the question is not related to the context, politely respond that you are tuned to only answer questions that are related to the context. Helpful answer in markdown: +----------------- +{context} + """), + HumanMessagePromptTemplate.from_template("{question}") + ] + + prompt = ChatPromptTemplate.from_messages(messages) + + + chat = ConversationalRetrievalChain.from_llm(OpenAI(temperature=0, model_name="gpt-3.5-turbo"), vectorstore.as_retriever(search_kwargs={"k": 1}), return_source_documents=True, qa_prompt=prompt,) + + history = [(d["human_message"], d["bot_response"]) for d in body.history] + + response = chat({ + "question": body.user_message, + "chat_history": history + }) + + + answer = response["answer"] + answer = answer[answer.find(":")+1:].strip() + + + return { + "bot_response": answer, + "human_message": body.user_message, + } + + except Exception as e: + print(e) + return { + "bot_response": "Something went wrong please try again later", + "human_message": body.user_message, + } + + + + + +async def chat_extension_handler(body: ChatBody): + try: + soup = BeautifulSoup(body.html, 'lxml') + + iframe = soup.find('iframe', id='pageassist-iframe') + if iframe: + iframe.decompose() + div = soup.find('div', id='pageassist-icon') + if div: + div.decompose() + div = soup.find('div', id='__plasmo-loading__') + if div: + div.decompose() + text = soup.get_text() + + result = [LDocument(page_content=text, metadata={"source": "test"})] + token_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0) + doc = token_splitter.split_documents(result) + + print(f'Number of documents: {len(doc)}') + + + vectorstore = Chroma.from_documents(doc, OpenAIEmbeddings()) + + + messages = [ + SystemMessagePromptTemplate.from_template("""You are PageAssist bot. Use the following pieces of context from this webpage to answer the question from the user. +If you don't know the answer, just say you don't know. DO NOT try to make up an answer. +If user want recommendation, help from the context, or any other information, please provide it. If the question is not related to the context, politely respond that you are tuned to only answer questions that are related to the context. Helpful answer in markdown: ----------------- {context} diff --git a/py_server/models/__init__.py b/py_server/models/__init__.py index 933c6c5..61499ae 100644 --- a/py_server/models/__init__.py +++ b/py_server/models/__init__.py @@ -1,2 +1,2 @@ -from .chat import ChatBody +from .chat import ChatBody, ChatAppBody from .user import UserValidation, SaveChatToApp \ No newline at end of file diff --git a/py_server/models/chat.py b/py_server/models/chat.py index b8b835e..71ba243 100644 --- a/py_server/models/chat.py +++ b/py_server/models/chat.py @@ -5,3 +5,9 @@ class ChatBody(BaseModel): html: str history: list # url: str + +class ChatAppBody(BaseModel): + id: str + user_message: str + url: str + history: list \ No newline at end of file diff --git a/py_server/routers/chat.py b/py_server/routers/chat.py index 09fa628..68dc655 100644 --- a/py_server/routers/chat.py +++ b/py_server/routers/chat.py @@ -1,9 +1,14 @@ -from fastapi import APIRouter -from models import ChatBody -from handlers.chat import chat_extension_handler +from fastapi import APIRouter, Header +from models import ChatBody, ChatAppBody +from handlers.chat import chat_extension_handler, chat_app_handler router = APIRouter(prefix="/api/v1") @router.post("/chat/chrome", tags=["chat"]) async def chat_extension(body: ChatBody): - return await chat_extension_handler(body) \ No newline at end of file + return await chat_extension_handler(body) + + +@router.post("/chat/app", tags=["chat"]) +async def chat_app(body: ChatAppBody, x_auth_token: str = Header()): + return await chat_app_handler(body, x_auth_token) \ No newline at end of file diff --git a/src/components/Chat/ChatBox.tsx b/src/components/Chat/ChatBox.tsx index abcd94a..6a35d7f 100644 --- a/src/components/Chat/ChatBox.tsx +++ b/src/components/Chat/ChatBox.tsx @@ -1,8 +1,15 @@ import { TrashIcon } from "@heroicons/react/24/outline"; +import { useSupabaseClient } from "@supabase/auth-helpers-react"; import Link from "next/link"; +import { useRouter } from "next/router"; import React from "react"; +import { api } from "~/utils/api"; import { iconUrl } from "~/utils/icon"; +import { useForm } from "@mantine/form"; +import axios from "axios"; +import { useMutation } from "@tanstack/react-query"; + type Message = { isBot: boolean; message: string; @@ -22,6 +29,51 @@ type Props = { }; export const CahtBox = (props: Props) => { + const supabase = useSupabaseClient(); + + const form = useForm({ + initialValues: { + message: "", + isBot: false, + }, + }); + + const sendToBot = async (message: string) => { + const { data } = await supabase.auth.getSession(); + + const response = await axios.post( + `${process.env.NEXT_PUBLIC_PAGEASSIST_URL}/api/v1/chat/app`, + { + user_message: message, + history: history, + url: props.url, + id: props.id, + }, + { + headers: { + "X-Auth-Token": data.session?.access_token, + }, + } + ); + + return response.data; + }; + + const { mutateAsync: sendToBotAsync, isLoading: isSending } = useMutation( + sendToBot, + { + onSuccess: (data) => { + setMessages([...messages, { isBot: true, message: data.bot_response }]); + setHistory([...history, data]); + }, + onError: (error) => { + setMessages([ + ...messages, + { isBot: true, message: "Something went wrong" }, + ]); + }, + } + ); const [messages, setMessages] = React.useState([ { isBot: true, @@ -29,6 +81,12 @@ export const CahtBox = (props: Props) => { }, ]); + // const fetchSession = async () => { + + // const {data}= await supabase.auth.getSession(); + // data.session?.access_token + // } + const [history, setHistory] = React.useState([]); const divRef = React.useRef(null); @@ -37,6 +95,15 @@ export const CahtBox = (props: Props) => { divRef.current.scrollIntoView({ behavior: "smooth" }); }); + const router = useRouter(); + + const { mutateAsync: deleteChatByIdAsync, isLoading: isDeleting } = + api.chat.deleteChatById.useMutation({ + onSuccess: () => { + router.push("/dashboard"); + }, + }); + return (
{/* header */} @@ -67,10 +134,40 @@ export const CahtBox = (props: Props) => {
@@ -107,22 +204,31 @@ export const CahtBox = (props: Props) => { ); })} + {isSending && ( +
+
+
+

Hold on, I'm looking...

+
+
+
+ )}
{ - // setMessages([...messages, values]) - // form.reset() - // await sendToBotAsync(values.message) - // })} + onSubmit={form.onSubmit(async (values) => { + setMessages([...messages, values]); + form.reset(); + await sendToBotAsync(values.message); + })} >
diff --git a/src/server/api/routers/chat.ts b/src/server/api/routers/chat.ts index 327d1b8..af416c1 100644 --- a/src/server/api/routers/chat.ts +++ b/src/server/api/routers/chat.ts @@ -48,7 +48,6 @@ export const chatRouter = createTRPCRouter({ }); } - const site = await prisma.website.findFirst({ where: { id: input.id, @@ -74,4 +73,39 @@ export const chatRouter = createTRPCRouter({ return site; }), + + deleteChatById: publicProcedure.input(z.object({ + id: z.string(), + })).mutation(async ({ ctx, input }) => { + const user = ctx.user; + const prisma = ctx.prisma; + if (!user) { + throw new TRPCError({ + "code": "UNAUTHORIZED", + "message": "You are not authorized to access this resource", + }); + } + + const site = await prisma.website.findFirst({ + where: { + id: input.id, + user_id: user.id, + }, + }); + + if (!site) { + throw new TRPCError({ + "code": "NOT_FOUND", + "message": "Chat not found", + }); + } + + await prisma.website.delete({ + where: { + id: input.id, + }, + }); + + return site; + }), });