chat ui added

This commit is contained in:
n4ze3m 2023-04-15 15:59:10 +05:30
parent 1c7b4c328f
commit 4efc9e05e0
8 changed files with 319 additions and 48 deletions

View File

@ -41,8 +41,9 @@ async def chat_extension_handler(body: ChatBody):
messages = [ messages = [
SystemMessagePromptTemplate.from_template("""You are PageAssist bot. Use the following pieces of context from this webpage to answer the question at the end. 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 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 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: 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} {context}

View File

@ -0,0 +1,170 @@
import { TrashIcon } from "@heroicons/react/24/outline";
import Link from "next/link";
import React from "react";
import { iconUrl } from "~/utils/icon";
type Message = {
isBot: boolean;
message: string;
};
type History = {
bot_response: string;
human_message: string;
};
type Props = {
title: string | null;
id: string;
created_at: Date | null;
icon: string | null;
url: string | null;
};
export const CahtBox = (props: Props) => {
const [messages, setMessages] = React.useState<Message[]>([
{
isBot: true,
message: "Hi, I'm PageAssist Bot. How can I help you?",
},
]);
const [history, setHistory] = React.useState<History[]>([]);
const divRef = React.useRef(null);
React.useEffect(() => {
//@ts-ignore
divRef.current.scrollIntoView({ behavior: "smooth" });
});
return (
<div className="flex flex-col border bg-white">
{/* header */}
<div className="bg-grey-lighter flex flex-row items-center justify-between px-3 py-2">
<Link
target="_blank"
href={props.url ? props.url : "#"}
className="flex items-center"
>
<div>
<img
className="h-10 w-10 rounded-full"
//@ts-ignore
src={iconUrl(props?.icon, props?.url)}
/>
</div>
<div className="ml-4">
<p className="text-grey-darkest">
{props?.title && props?.title?.length > 100
? props?.title?.slice(0, 100) + "..."
: props?.title}
</p>
<p className="mt-1 text-xs text-gray-400">
{props.url && new URL(props.url).hostname}
</p>
</div>
</Link>
<div className="flex">
<button
type="button"
className="inline-flex items-center rounded-full border border-transparent bg-red-600 p-1.5 text-white shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2"
>
<TrashIcon className="h-5 w-5" aria-hidden="true" />
</button>
</div>
</div>
{/* */}
<div
style={{ height: "calc(100vh - 260px)" }}
className="flex-grow overflow-auto"
>
<div className="px-3 py-2">
{messages.map((message, index) => {
return (
<div
key={index}
className={
message.isBot
? "mt-2 flex w-full max-w-xs space-x-3"
: "ml-auto mt-2 flex w-full max-w-xs justify-end space-x-3"
}
>
<div>
<div
className={
message.isBot
? "rounded-r-lg rounded-bl-lg bg-gray-300 p-3"
: "rounded-l-lg rounded-br-lg bg-blue-600 p-3 text-white"
}
>
<p className="text-sm">
{/* <ReactMarkdown>{message.message}</ReactMarkdown> */}
{message.message}
</p>
</div>
</div>
</div>
);
})}
<div ref={divRef} />
</div>
</div>
<div className="items-center bg-gray-300 px-4 py-4">
<form
// onSubmit={form.onSubmit(async (values) => {
// setMessages([...messages, values])
// form.reset()
// await sendToBotAsync(values.message)
// })}
>
<div className="flex-grow space-y-6">
<div className="flex">
<span className="mr-3">
<button
// disabled={isSending || isSaving}
onClick={() => {
setHistory([]);
setMessages([
{
message: "Hi, I'm PageAssist. How can I help you?",
isBot: true,
},
]);
}}
className="inline-flex h-10 items-center rounded-md border border-gray-700 bg-white px-3 text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2"
type="button"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
className="h-5 w-5 text-gray-600"
>
<path d="M18.37 2.63 14 7l-1.59-1.59a2 2 0 0 0-2.82 0L8 7l9 9 1.59-1.59a2 2 0 0 0 0-2.82L17 10l4.37-4.37a2.12 2.12 0 1 0-3-3Z"></path>
<path d="M9 8c-2 3-4 3.5-7 4l8 10c2-1 6-5 6-7"></path>
<path d="M14.5 17.5 4.5 15"></path>
</svg>
</button>
</span>
<div className="flex-grow">
<input
// disabled={isSending || isSaving}
className="flex h-10 w-full items-center rounded px-3 text-sm"
type="text"
required
placeholder="Type your message…"
// {...form.getInputProps("message")}
/>
</div>
</div>
</div>
</form>
</div>
</div>
);
};

View File

@ -0,0 +1,26 @@
import { useRouter } from "next/router";
import React from "react";
import { api } from "~/utils/api";
import { CahtBox } from "./ChatBox";
export const DashboardChatBody = () => {
const router = useRouter();
const { id } = router.query;
const { data: chat, status } = api.chat.getChatById.useQuery(
{ id: id as string },
{
onError: (err) => {
router.push("/dashboard");
},
}
);
return (
<div>
{status === "loading" && <div>Loading...</div>}
{status === "success" && <CahtBox {...chat} />}
</div>
);
};

View File

@ -3,27 +3,13 @@ import Empty from "./Empty";
import Loading from "./Loading"; import Loading from "./Loading";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
import { ChevronRightIcon } from "@heroicons/react/24/outline"; import { ChevronRightIcon } from "@heroicons/react/24/outline";
import Link from "next/link";
import { iconUrl } from "~/utils/icon";
export default function DashboardBoby() { export default function DashboardBoby() {
const { data: savedSites, status } = api.chat.getSavedSitesForChat.useQuery(); const { data: savedSites, status } = api.chat.getSavedSitesForChat.useQuery();
const iconUrl = (icon: string, url: string) => {
// check if icon is valid url (http:// or https://)
if (icon.startsWith("http://") || icon.startsWith("https://")) {
return icon;
}
// check if icon is valid url (//)
if (icon.startsWith("//")) {
return `https:${icon}`;
}
const host = new URL(url).hostname;
const protocol = new URL(url).protocol;
return `${protocol}//${host}/${icon}`;
};
return ( return (
<> <>
{status === "loading" && <Loading />} {status === "loading" && <Loading />}
@ -31,7 +17,8 @@ export default function DashboardBoby() {
{status === "success" && savedSites.data.length > 0 && ( {status === "success" && savedSites.data.length > 0 && (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3"> <div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
{savedSites.data.map((site, idx) => ( {savedSites.data.map((site, idx) => (
<div <Link
href={`/dashboard/chat/${site.id}`}
key={idx} key={idx}
className="bg-panel-header-light border-panel-border-light hover:bg-panel-border-light hover:border-panel-border-hover-light h-30 group relative flex cursor-pointer flex-row rounded-md border px-6 py-4 text-left transition duration-150 ease-in-out hover:border-gray-300" className="bg-panel-header-light border-panel-border-light hover:bg-panel-border-light hover:border-panel-border-hover-light h-30 group relative flex cursor-pointer flex-row rounded-md border px-6 py-4 text-left transition duration-150 ease-in-out hover:border-gray-300"
> >
@ -65,7 +52,7 @@ export default function DashboardBoby() {
</div> </div>
</div> </div>
</div> </div>
</div> </Link>
))} ))}
</div> </div>
)} )}

View File

@ -7,13 +7,18 @@ import {
XMarkIcon, XMarkIcon,
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
import { ChevronDownIcon } from "@heroicons/react/20/solid"; import { ChevronDownIcon } from "@heroicons/react/20/solid";
import { useUser } from "@supabase/auth-helpers-react"; import { useSupabaseClient, useUser } from "@supabase/auth-helpers-react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import Link from "next/link"; import Link from "next/link";
const navigation = [ const navigation = [
{ name: "Home", href: "/dashboard", icon: HomeIcon, current: true }, { name: "Home", href: "/dashboard", icon: HomeIcon, current: true },
{ name: "Settings", href: "/dashboard/settings", icon: CogIcon, current: false }, {
name: "Settings",
href: "/dashboard/settings",
icon: CogIcon,
current: false,
},
]; ];
//@ts-ignore //@ts-ignore
@ -29,6 +34,7 @@ export default function DashboardLayout({
const [sidebarOpen, setSidebarOpen] = useState(false); const [sidebarOpen, setSidebarOpen] = useState(false);
const user = useUser(); const user = useUser();
const router = useRouter(); const router = useRouter();
const supabase = useSupabaseClient();
return ( return (
<> <>
@ -127,13 +133,9 @@ export default function DashboardLayout({
</nav> </nav>
</Dialog.Panel> </Dialog.Panel>
</Transition.Child> </Transition.Child>
<div className="w-14 flex-shrink-0" aria-hidden="true">
{/* Dummy element to force sidebar to shrink to fit close icon */}
</div>
</div> </div>
</Dialog> </Dialog>
</Transition.Root> </Transition.Root>
<div className="hidden lg:fixed lg:inset-y-0 lg:flex lg:w-64 lg:flex-col"> <div className="hidden lg:fixed lg:inset-y-0 lg:flex lg:w-64 lg:flex-col">
<div className="flex flex-grow flex-col overflow-y-auto border-r border-gray-200 bg-white pb-4 pt-5"> <div className="flex flex-grow flex-col overflow-y-auto border-r border-gray-200 bg-white pb-4 pt-5">
<div className="flex flex-shrink-0 items-center px-4"> <div className="flex flex-shrink-0 items-center px-4">
@ -145,7 +147,7 @@ export default function DashboardLayout({
> >
<div className="space-y-1 px-2"> <div className="space-y-1 px-2">
{navigation.map((item) => ( {navigation.map((item) => (
<a <Link
key={item.name} key={item.name}
href={item.href} href={item.href}
className={classNames( className={classNames(
@ -154,7 +156,9 @@ export default function DashboardLayout({
: "text-gray-600 hover:bg-gray-50 hover:text-gray-900", : "text-gray-600 hover:bg-gray-50 hover:text-gray-900",
"group flex items-center rounded-md px-2 py-2 text-sm font-medium" "group flex items-center rounded-md px-2 py-2 text-sm font-medium"
)} )}
aria-current={router.pathname === item.href ? "page" : undefined} aria-current={
router.pathname === item.href ? "page" : undefined
}
> >
<item.icon <item.icon
className={classNames( className={classNames(
@ -166,7 +170,7 @@ export default function DashboardLayout({
aria-hidden="true" aria-hidden="true"
/> />
{item.name} {item.name}
</a> </Link>
))} ))}
</div> </div>
</nav> </nav>
@ -217,28 +221,31 @@ export default function DashboardLayout({
<Menu.Items className="absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"> <Menu.Items className="absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
<Menu.Item> <Menu.Item>
{({ active }) => ( {({ active }) => (
<a <Link
href="#" href="/dashboard/settings"
className={classNames( className={classNames(
active ? "bg-gray-100" : "", active ? "bg-gray-100" : "",
"block px-4 py-2 text-sm text-gray-700" "block px-4 py-2 text-sm text-gray-700"
)} )}
> >
Settings Settings
</a> </Link>
)} )}
</Menu.Item> </Menu.Item>
<Menu.Item> <Menu.Item>
{({ active }) => ( {({ active }) => (
<a <div
href="#" onClick={async () => {
await supabase.auth.signOut();
router.push("/");
}}
className={classNames( className={classNames(
active ? "bg-gray-100" : "", active ? "bg-gray-100" : "",
"block px-4 py-2 text-sm text-gray-700" "block px-4 py-2 text-sm text-gray-700"
)} )}
> >
Logout Logout
</a> </div>
)} )}
</Menu.Item> </Menu.Item>
</Menu.Items> </Menu.Items>

View File

@ -0,0 +1,38 @@
import { createServerSupabaseClient } from "@supabase/auth-helpers-nextjs";
import { GetServerSideProps, NextPage } from "next";
import Head from "next/head";
import { DashboardChatBody } from "~/components/Chat";
import DashboardLayout from "~/components/Layouts/DashboardLayout";
export const getServerSideProps: GetServerSideProps = async (ctx) => {
const supabase = createServerSupabaseClient(ctx);
const {
data: { session },
} = await supabase.auth.getSession();
if (!session) {
return {
redirect: {
destination: "/auth",
permanent: false,
},
};
}
return {
props: {},
};
};
const DashboardChatPage: NextPage = () => {
return (
<DashboardLayout>
<Head>
<title>Chat / PageAssist</title>
</Head>
<DashboardChatBody />
</DashboardLayout>
);
};
export default DashboardChatPage;

View File

@ -15,19 +15,6 @@ export const chatRouter = createTRPCRouter({
}); });
} }
const isUserExist = await prisma.user.findFirst({
where: {
id: user.id,
},
});
if (!isUserExist) {
throw new TRPCError({
"code": "UNAUTHORIZED",
"message": "You are not authorized to access this resource",
});
}
const sites = await prisma.website.findMany({ const sites = await prisma.website.findMany({
where: { where: {
user_id: user.id, user_id: user.id,
@ -48,4 +35,43 @@ export const chatRouter = createTRPCRouter({
length: sites.length, length: sites.length,
}; };
}), }),
getChatById: publicProcedure.input(z.object({
id: z.string(),
})).query(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,
},
select: {
user_id: false,
id: true,
created_at: true,
html: false,
icon: true,
title: true,
url: true,
},
});
if (!site) {
throw new TRPCError({
"code": "NOT_FOUND",
"message": "Chat not found",
});
}
return site;
}),
}); });

16
src/utils/icon.ts Normal file
View File

@ -0,0 +1,16 @@
export const iconUrl = (icon: string, url: string) => {
// check if icon is valid url (http:// or https://)
if (icon.startsWith("http://") || icon.startsWith("https://")) {
return icon;
}
// check if icon is valid url (//)
if (icon.startsWith("//")) {
return `https:${icon}`;
}
const host = new URL(url).hostname;
const protocol = new URL(url).protocol;
return `${protocol}//${host}/${icon}`;
};