v2 initial commit
This commit is contained in:
@@ -1,276 +0,0 @@
|
||||
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;
|
||||
};
|
||||
|
||||
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 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<Message[]>([
|
||||
{
|
||||
isBot: true,
|
||||
message: "Hi, I'm PageAssist Bot. How can I help you?",
|
||||
},
|
||||
]);
|
||||
|
||||
// const fetchSession = async () => {
|
||||
|
||||
// const {data}= await supabase.auth.getSession();
|
||||
// data.session?.access_token
|
||||
// }
|
||||
|
||||
const [history, setHistory] = React.useState<History[]>([]);
|
||||
const divRef = React.useRef(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
//@ts-ignore
|
||||
divRef.current.scrollIntoView({ behavior: "smooth" });
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const { mutateAsync: deleteChatByIdAsync, isLoading: isDeleting } =
|
||||
api.chat.deleteChatById.useMutation({
|
||||
onSuccess: () => {
|
||||
router.push("/dashboard");
|
||||
},
|
||||
});
|
||||
|
||||
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
|
||||
onClick={async () => {
|
||||
const isOk = confirm(
|
||||
"Are you sure you want to delete this chat?"
|
||||
);
|
||||
|
||||
if (isOk) {
|
||||
await deleteChatByIdAsync({
|
||||
id: props.id,
|
||||
});
|
||||
}
|
||||
}}
|
||||
disabled={isDeleting}
|
||||
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"
|
||||
>
|
||||
{isDeleting ? (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
className="h-5 w-5 animate-spin fill-white text-white dark:text-gray-600"
|
||||
viewBox="0 0 100 101"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M100 50.59c0 27.615-22.386 50.001-50 50.001s-50-22.386-50-50 22.386-50 50-50 50 22.386 50 50zm-90.919 0c0 22.6 18.32 40.92 40.919 40.92 22.599 0 40.919-18.32 40.919-40.92 0-22.598-18.32-40.918-40.919-40.918-22.599 0-40.919 18.32-40.919 40.919z"
|
||||
></path>
|
||||
<path
|
||||
fill="currentFill"
|
||||
d="M93.968 39.04c2.425-.636 3.894-3.128 3.04-5.486A50 50 0 0041.735 1.279c-2.474.414-3.922 2.919-3.285 5.344.637 2.426 3.12 3.849 5.6 3.484a40.916 40.916 0 0144.131 25.769c.902 2.34 3.361 3.802 5.787 3.165z"
|
||||
></path>
|
||||
</svg>
|
||||
) : (
|
||||
<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>
|
||||
);
|
||||
})}
|
||||
{isSending && (
|
||||
<div className="mt-2 flex w-full max-w-xs space-x-3">
|
||||
<div>
|
||||
<div className="rounded-r-lg rounded-bl-lg bg-gray-300 p-3">
|
||||
<p className="text-sm">Hold on, I'm looking...</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}
|
||||
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}
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
98
src/components/Common/Markdown.tsx
Normal file
98
src/components/Common/Markdown.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
|
||||
import remarkGfm from "remark-gfm"
|
||||
import { nightOwl } from "react-syntax-highlighter/dist/cjs/styles/prism"
|
||||
import rehypeMathjax from "rehype-mathjax"
|
||||
import remarkMath from "remark-math"
|
||||
import ReactMarkdown from "react-markdown"
|
||||
import { ClipboardIcon, CheckIcon, EyeIcon } from "@heroicons/react/24/outline"
|
||||
import React from "react"
|
||||
import { Tooltip } from "antd"
|
||||
|
||||
export default function Markdown({ message }: { message: string }) {
|
||||
const [isBtnPressed, setIsBtnPressed] = React.useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isBtnPressed) {
|
||||
setTimeout(() => {
|
||||
setIsBtnPressed(false)
|
||||
}, 4000)
|
||||
}
|
||||
}, [isBtnPressed])
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ReactMarkdown
|
||||
className="prose break-words dark:prose-invert text-sm prose-p:leading-relaxed prose-pre:p-0 dark:prose-dark"
|
||||
remarkPlugins={[remarkGfm, remarkMath]}
|
||||
rehypePlugins={[rehypeMathjax]}
|
||||
components={{
|
||||
code({ node, inline, className, children, ...props }) {
|
||||
const match = /language-(\w+)/.exec(className || "")
|
||||
return !inline ? (
|
||||
<div className="code relative text-base bg-gray-800 rounded-md overflow-hidden">
|
||||
<div className="flex items-center justify-between py-1.5 px-4">
|
||||
<span className="text-xs lowercase text-gray-200">
|
||||
{className && className.replace("language-", "")}
|
||||
</span>
|
||||
|
||||
<div className="flex items-center">
|
||||
|
||||
<Tooltip title="Copy to clipboard">
|
||||
<button
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(children[0] as string)
|
||||
setIsBtnPressed(true)
|
||||
}}
|
||||
className="flex gap-1.5 items-center rounded bg-none p-1 text-xs text-gray-200 hover:bg-gray-700 hover:text-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 focus:ring-offset-gray-100">
|
||||
{!isBtnPressed ? (
|
||||
<ClipboardIcon className="h-4 w-4" />
|
||||
) : (
|
||||
<CheckIcon className="h-4 w-4 text-green-400" />
|
||||
)}
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<SyntaxHighlighter
|
||||
{...props}
|
||||
children={String(children).replace(/\n$/, "")}
|
||||
style={nightOwl}
|
||||
key={Math.random()}
|
||||
customStyle={{
|
||||
margin: 0,
|
||||
fontSize: "1rem",
|
||||
lineHeight: "1.5rem"
|
||||
}}
|
||||
language={(match && match[1]) || ""}
|
||||
codeTagProps={{
|
||||
className: "text-sm"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<code className={`${className} font-semibold`} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
)
|
||||
},
|
||||
a({ node, ...props }) {
|
||||
return (
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-blue-500 text-sm hover:underline"
|
||||
{...props}>
|
||||
{props.children}
|
||||
</a>
|
||||
)
|
||||
},
|
||||
p({ children }) {
|
||||
return <p className="mb-2 last:mb-0">{children}</p>
|
||||
}
|
||||
}}>
|
||||
{message}
|
||||
</ReactMarkdown>
|
||||
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
78
src/components/Common/Playground/Message.tsx
Normal file
78
src/components/Common/Playground/Message.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import {
|
||||
CheckIcon,
|
||||
ClipboardIcon,
|
||||
} from "@heroicons/react/24/outline"
|
||||
import Markdown from "../../Common/Markdown"
|
||||
import React from "react"
|
||||
|
||||
type Props = {
|
||||
message: string
|
||||
hideCopy?: boolean
|
||||
botAvatar?: JSX.Element
|
||||
userAvatar?: JSX.Element
|
||||
isBot: boolean
|
||||
}
|
||||
|
||||
export const PlaygroundMessage = (props: Props) => {
|
||||
const [isBtnPressed, setIsBtnPressed] = React.useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isBtnPressed) {
|
||||
setTimeout(() => {
|
||||
setIsBtnPressed(false)
|
||||
}, 4000)
|
||||
}
|
||||
}, [isBtnPressed])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`group w-full text-gray-800 dark:text-gray-100 border-b border-black/10 dark:border-gray-900/50 ${
|
||||
!props.isBot ? "dark:bg-black" : "bg-gray-50 dark:bg-[#0a0a0a]"
|
||||
}`}>
|
||||
<div className="text-base gap-4 md:gap-6 md:max-w-2xl lg:max-w-xl xl:max-w-3xl flex lg:px-0 m-auto w-full">
|
||||
<div className="flex flex-row gap-4 md:gap-6 md:max-w-2xl lg:max-w-xl xl:max-w-3xl p-4 md:py-6 lg:px-0 m-auto w-full">
|
||||
<div className="w-8 flex flex-col relative items-end">
|
||||
<div className="relative h-7 w-7 p-1 rounded-sm text-white flex items-center justify-center text-opacity-100r">
|
||||
{props.isBot ? (
|
||||
!props.botAvatar ? (
|
||||
<div className="absolute h-8 w-8 rounded-full bg-gradient-to-r from-green-300 to-purple-400"></div>
|
||||
) : (
|
||||
props.botAvatar
|
||||
)
|
||||
) : !props.userAvatar ? (
|
||||
<div className="absolute h-8 w-8 rounded-full from-blue-400 to-blue-600 bg-gradient-to-r"></div>
|
||||
) : (
|
||||
props.userAvatar
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative flex w-[calc(100%-50px)] flex-col gap-1 md:gap-3 lg:w-[calc(100%-115px)]">
|
||||
<div className="flex flex-grow flex-col gap-3">
|
||||
<Markdown message={props.message} />
|
||||
</div>
|
||||
{/* source if aviable */}
|
||||
</div>
|
||||
|
||||
{props.isBot && (
|
||||
<div className="flex space-x-2">
|
||||
{!props.hideCopy && (
|
||||
<button
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(props.message)
|
||||
setIsBtnPressed(true)
|
||||
}}
|
||||
className="flex items-center justify-center w-8 h-8 rounded-full bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500">
|
||||
{!isBtnPressed ? (
|
||||
<ClipboardIcon className="w-4 h-4 text-gray-400 group-hover:text-gray-500" />
|
||||
) : (
|
||||
<CheckIcon className="w-4 h-4 text-green-400 group-hover:text-green-500" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import { useRouter } from "next/router";
|
||||
import React from "react";
|
||||
|
||||
export default function Empty() {
|
||||
const router = useRouter();
|
||||
return (
|
||||
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<div className=" px-4 py-8 sm:px-10">
|
||||
<div className="text-center">
|
||||
<svg
|
||||
className="mx-auto h-12 w-12 text-gray-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
vectorEffect="non-scaling-stroke"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 13h6m-3-3v6m-9 1V7a2 2 0 012-2h6l2 2h6a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z"
|
||||
/>
|
||||
</svg>
|
||||
<h3 className="mt-2 text-sm font-medium text-gray-900">
|
||||
No Chats Yet
|
||||
</h3>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
Get started by installing the Page Assist Chrome Extension.
|
||||
</p>
|
||||
<div className="mt-6">
|
||||
<button
|
||||
onClick={() => {
|
||||
router.push(
|
||||
"https://chrome.google.com/webstore/detail/page-assist/ehkjdalbpmmaddcfdilplgknkgepeakd?hl=en&authuser=2"
|
||||
);
|
||||
}}
|
||||
type="button"
|
||||
className="inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="-ml-1 mr-2 h-5 w-5"
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<circle cx="12" cy="12" r="4"></circle>
|
||||
<path d="M21.17 8L12 8"></path>
|
||||
<path d="M3.95 6.06L8.54 14"></path>
|
||||
<path d="M10.88 21.94L15.46 14"></path>
|
||||
</svg>
|
||||
Install Extension
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
{/* create skelon loadinf */}
|
||||
{[1, 2, 3, 4, 5, 6].map((item) => (
|
||||
<div
|
||||
className="flex h-35 cursor-pointer rounded-md shadow-sm transition-shadow duration-300 ease-in-out hover:shadow-lg"
|
||||
key={item}
|
||||
>
|
||||
<div className="flex flex-1 items-center justify-between truncate rounded-md border border-gray-200 bg-white">
|
||||
<div className="flex-1 truncate px-4 py-4 text-sm">
|
||||
<h3 className="h-10 animate-pulse bg-gray-400 font-medium text-gray-900 hover:text-gray-600" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import React from "react";
|
||||
import Empty from "./Empty";
|
||||
import Loading from "./Loading";
|
||||
import { api } from "~/utils/api";
|
||||
import { ChevronRightIcon } from "@heroicons/react/24/outline";
|
||||
import Link from "next/link";
|
||||
import { iconUrl } from "~/utils/icon";
|
||||
|
||||
export default function DashboardBoby() {
|
||||
const { data: savedSites, status } = api.chat.getSavedSitesForChat.useQuery();
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{status === "loading" && <Loading />}
|
||||
{status === "success" && savedSites.data.length === 0 && <Empty />}
|
||||
{status === "success" && savedSites.data.length > 0 && (
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
{savedSites.data.map((site, idx) => (
|
||||
<Link
|
||||
href={`/dashboard/chat/${site.id}`}
|
||||
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"
|
||||
>
|
||||
<div className="flex h-full w-full flex-col space-y-2 ">
|
||||
<div className="text-scale-1200">
|
||||
<div className="flex w-full flex-row justify-between gap-1">
|
||||
<span
|
||||
className={`flex-shrink ${
|
||||
site?.title && site?.title?.length > 50
|
||||
? "truncate"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{site.title}
|
||||
</span>
|
||||
<ChevronRightIcon className="h-10 w-10 text-gray-400 group-hover:text-gray-500" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bottom-0">
|
||||
<div className="flex w-full flex-row gap-1">
|
||||
<img
|
||||
className="h-5 w-5 rounded-md"
|
||||
// @ts-ignore
|
||||
src={iconUrl(site.icon, site.url)}
|
||||
alt=""
|
||||
/>
|
||||
<span className="text-scale-1000 ml-3 flex-shrink truncate text-xs text-gray-400">
|
||||
{site.url && new URL(site.url).hostname}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
import { Fragment } from "react";
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||
import Link from "next/link";
|
||||
import { useUser } from "@supabase/auth-helpers-react";
|
||||
import YouTube from "react-youtube";
|
||||
|
||||
export default function Hero() {
|
||||
const user = useUser();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="relative overflow-hidden">
|
||||
<div className="relative pb-16 pt-6 sm:pb-24">
|
||||
<Popover>
|
||||
<div className="mx-auto max-w-7xl px-4 sm:px-6">
|
||||
<nav
|
||||
className="relative flex items-center justify-between sm:h-10 md:justify-center"
|
||||
aria-label="Global"
|
||||
>
|
||||
<div className="flex flex-1 items-center md:absolute md:inset-y-0 md:left-0">
|
||||
<div className="flex w-full items-center justify-between md:w-auto">
|
||||
<Link href="/">
|
||||
<span className="sr-only">Feedback Board</span>
|
||||
<img
|
||||
className="h-8 w-auto sm:h-10"
|
||||
src="/logo.png"
|
||||
alt="Feedback Board"
|
||||
/>
|
||||
</Link>
|
||||
<div className="-mr-2 flex items-center md:hidden">
|
||||
<Popover.Button className="inline-flex items-center justify-center rounded-md bg-gray-50 p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-sky-500">
|
||||
<span className="sr-only">Open main menu</span>
|
||||
<Bars3Icon className="h-6 w-6" aria-hidden="true" />
|
||||
</Popover.Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="hidden md:absolute md:inset-y-0 md:right-0 md:flex md:items-center md:justify-end">
|
||||
<span className="inline-flex rounded-md shadow">
|
||||
{user ? (
|
||||
<Link
|
||||
href="/dashboard"
|
||||
className="inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||
>
|
||||
Dashboard
|
||||
</Link>
|
||||
) : (
|
||||
<Link
|
||||
href="/auth"
|
||||
className="inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||
>
|
||||
Login
|
||||
</Link>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="duration-150 ease-out"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="duration-100 ease-in"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Popover.Panel
|
||||
focus
|
||||
className="absolute inset-x-0 top-0 z-10 origin-top-right transform p-2 transition md:hidden"
|
||||
>
|
||||
<div className="overflow-hidden rounded-lg bg-white shadow-md ring-1 ring-black ring-opacity-5">
|
||||
<div className="flex items-center justify-between px-5 pt-4">
|
||||
<div>
|
||||
<img
|
||||
className="h-8 w-auto"
|
||||
src="/logo.png"
|
||||
alt="PageAssist"
|
||||
/>
|
||||
</div>
|
||||
<div className="-mr-2">
|
||||
<Popover.Button className="inline-flex items-center justify-center rounded-md bg-white p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-sky-500">
|
||||
<span className="sr-only">Close main menu</span>
|
||||
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
|
||||
</Popover.Button>
|
||||
</div>
|
||||
</div>
|
||||
{user ? (
|
||||
<Link
|
||||
href="/dashboard"
|
||||
className="m-3 inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||
>
|
||||
Account
|
||||
</Link>
|
||||
) : (
|
||||
<Link
|
||||
href="/auth"
|
||||
className="m-3 inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||
>
|
||||
Login
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</Popover>
|
||||
<div className="py-24 sm:py-32 lg:pb-40">
|
||||
<div className="mx-auto max-w-7xl px-6 lg:px-8">
|
||||
<div className="mx-auto max-w-2xl text-center">
|
||||
<h1 className="text-2xl font-bold tracking-tight text-gray-900 sm:text-5xl">
|
||||
Chat with Any Webpage using PageAssist
|
||||
</h1>
|
||||
<p className="mt-6 text-lg leading-8 text-gray-600">
|
||||
Revolutionize your browsing experience with PageAssist, an
|
||||
open source Chrome extension that allows you to easily chat
|
||||
with any webpage using the power of ChatGPT API.
|
||||
</p>
|
||||
<div className="mt-10 flex items-center justify-center gap-x-6">
|
||||
<a
|
||||
href="https://chrome.google.com/webstore/detail/page-assist/ehkjdalbpmmaddcfdilplgknkgepeakd?hl=en&authuser=2"
|
||||
className="inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="-ml-0.5 mr-2 h-4 w-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<circle cx="12" cy="12" r="4"></circle>
|
||||
<path d="M21.17 8L12 8"></path>
|
||||
<path d="M3.95 6.06L8.54 14"></path>
|
||||
<path d="M10.88 21.94L15.46 14"></path>
|
||||
</svg>
|
||||
Install Now
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/n4ze3m/page-assist"
|
||||
className="inline-flex items-center text-base font-semibold leading-7 text-gray-900 "
|
||||
>
|
||||
Star on GitHub
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="ml-2 h-4 w-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M15 22v-4a4.8 4.8 0 00-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 004 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4M9 18c-4.51 2-5-2-7-2"></path>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<iframe
|
||||
className="yt-video relative mx-auto mt-12 w-full max-w-4xl rounded-3xl border border-gray-300 shadow-2xl dark:border-gray-700 lg:mt-20"
|
||||
src="https://www.youtube.com/embed/UB1PdZ32vBc"
|
||||
title="YouTube video player"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
frameBorder={0}
|
||||
/>
|
||||
|
||||
<div className="absolute inset-x-0 top-[calc(100%-13rem)] -z-10 transform-gpu overflow-hidden blur-3xl sm:top-[calc(100%-30rem)]">
|
||||
<svg
|
||||
className="relative left-[calc(50%+3rem)] h-[21.1875rem] max-w-none -translate-x-1/2 sm:left-[calc(50%+36rem)] sm:h-[42.375rem]"
|
||||
viewBox="0 0 1155 678"
|
||||
>
|
||||
<path
|
||||
fill="url(#b9e4a85f-ccd5-4151-8e84-ab55c66e5aa1)"
|
||||
fillOpacity=".3"
|
||||
d="M317.219 518.975L203.852 678 0 438.341l317.219 80.634 204.172-286.402c1.307 132.337 45.083 346.658 209.733 145.248C936.936 126.058 882.053-94.234 1031.02 41.331c119.18 108.451 130.68 295.337 121.53 375.223L855 299l21.173 362.054-558.954-142.079z"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="b9e4a85f-ccd5-4151-8e84-ab55c66e5aa1"
|
||||
x1="1155.49"
|
||||
x2="-78.208"
|
||||
y1=".177"
|
||||
y2="474.645"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#9089FC" />
|
||||
<stop offset={1} stopColor="#FF80B5" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,268 +0,0 @@
|
||||
import { Fragment, useState } from "react";
|
||||
import { Dialog, Menu, Transition } from "@headlessui/react";
|
||||
import {
|
||||
Bars3CenterLeftIcon,
|
||||
CogIcon,
|
||||
HomeIcon,
|
||||
XMarkIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { ChevronDownIcon } from "@heroicons/react/20/solid";
|
||||
import { useSupabaseClient, useUser } from "@supabase/auth-helpers-react";
|
||||
import { useRouter } from "next/router";
|
||||
import Link from "next/link";
|
||||
|
||||
const navigation = [
|
||||
{ name: "Home", href: "/dashboard", icon: HomeIcon, current: true },
|
||||
{
|
||||
name: "Settings",
|
||||
href: "/dashboard/settings",
|
||||
icon: CogIcon,
|
||||
current: false,
|
||||
},
|
||||
];
|
||||
|
||||
//@ts-ignore
|
||||
function classNames(...classes) {
|
||||
return classes.filter(Boolean).join(" ");
|
||||
}
|
||||
|
||||
export default function DashboardLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
const user = useUser();
|
||||
const router = useRouter();
|
||||
const supabase = useSupabaseClient();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="min-h-full">
|
||||
<Transition.Root show={sidebarOpen} as={Fragment}>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="relative z-40 lg:hidden"
|
||||
onClose={setSidebarOpen}
|
||||
>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="transition-opacity ease-linear duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity ease-linear duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-gray-600 bg-opacity-75" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-40 flex">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="transition ease-in-out duration-300 transform"
|
||||
enterFrom="-translate-x-full"
|
||||
enterTo="translate-x-0"
|
||||
leave="transition ease-in-out duration-300 transform"
|
||||
leaveFrom="translate-x-0"
|
||||
leaveTo="-translate-x-full"
|
||||
>
|
||||
<Dialog.Panel className="relative flex w-full max-w-xs flex-1 flex-col bg-white pb-4 pt-5">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-in-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in-out duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="absolute right-0 top-0 -mr-12 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
className="ml-1 flex h-10 w-10 items-center justify-center rounded-full focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white"
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
>
|
||||
<span className="sr-only">Close sidebar</span>
|
||||
<XMarkIcon
|
||||
className="h-6 w-6 text-white"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</Transition.Child>
|
||||
<div className="flex flex-shrink-0 items-center px-4">
|
||||
<img
|
||||
className="h-10 w-auto"
|
||||
src="/logo.png"
|
||||
alt="PageAssist"
|
||||
/>
|
||||
</div>
|
||||
<nav
|
||||
className="mt-5 h-full flex-shrink-0 divide-y divide-gray-200 overflow-y-auto"
|
||||
aria-label="Sidebar"
|
||||
>
|
||||
<div className="space-y-1 px-2">
|
||||
{navigation.map((item) => (
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className={classNames(
|
||||
router.pathname === item.href
|
||||
? "bg-gray-100 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"
|
||||
)}
|
||||
aria-current={
|
||||
router.pathname === item.href ? "page" : undefined
|
||||
}
|
||||
>
|
||||
<item.icon
|
||||
className={classNames(
|
||||
router.pathname === item.href
|
||||
? "text-gray-500"
|
||||
: "text-gray-400 group-hover:text-gray-500",
|
||||
"mr-3 h-6 w-6 flex-shrink-0"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
<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-shrink-0 items-center px-4">
|
||||
<img className="h-10 w-auto" src="/logo.png" alt="PageAssist" />
|
||||
</div>
|
||||
<nav
|
||||
className="mt-5 flex flex-1 flex-col divide-y divide-gray-200 overflow-y-auto"
|
||||
aria-label="Sidebar"
|
||||
>
|
||||
<div className="space-y-1 px-2">
|
||||
{navigation.map((item) => (
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className={classNames(
|
||||
router.pathname === item.href
|
||||
? "bg-gray-100 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"
|
||||
)}
|
||||
aria-current={
|
||||
router.pathname === item.href ? "page" : undefined
|
||||
}
|
||||
>
|
||||
<item.icon
|
||||
className={classNames(
|
||||
router.pathname === item.href
|
||||
? "text-gray-500"
|
||||
: "text-gray-400 group-hover:text-gray-500",
|
||||
"mr-3 h-6 w-6 flex-shrink-0"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 flex-col lg:pl-64">
|
||||
<div className="flex h-16 flex-shrink-0 border-b border-gray-200 bg-white lg:border-none">
|
||||
<button
|
||||
type="button"
|
||||
className="border-r border-gray-200 px-4 text-gray-400 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-gray-200 lg:hidden"
|
||||
onClick={() => setSidebarOpen(true)}
|
||||
>
|
||||
<span className="sr-only">Open sidebar</span>
|
||||
<Bars3CenterLeftIcon className="h-6 w-6" aria-hidden="true" />
|
||||
</button>
|
||||
{/* Search bar */}
|
||||
<div className="flex flex-1 justify-end px-4 sm:px-6 lg:mx-auto lg:max-w-6xl lg:px-8">
|
||||
<div className="ml-4 flex items-center md:ml-6">
|
||||
{/* Profile dropdown */}
|
||||
<Menu as="div" className="relative ml-3">
|
||||
<div>
|
||||
<Menu.Button className="flex max-w-xs items-center rounded-full bg-white text-sm focus:outline-none focus:ring-2 focus:ring-gray-200 focus:ring-offset-2 lg:rounded-md lg:p-2 lg:hover:bg-gray-50">
|
||||
<img
|
||||
className="h-8 w-8 rounded-full"
|
||||
src={`https://ui-avatars.com/api/?name=${user?.email}`}
|
||||
alt=""
|
||||
/>
|
||||
<span className="ml-3 hidden text-sm font-medium text-gray-700 lg:block">
|
||||
<span className="sr-only">Open user menu for </span>
|
||||
{user?.email}
|
||||
</span>
|
||||
<ChevronDownIcon
|
||||
className="ml-1 hidden h-5 w-5 flex-shrink-0 text-gray-400 lg:block"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Menu.Button>
|
||||
</div>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<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>
|
||||
{({ active }) => (
|
||||
<Link
|
||||
href="/dashboard/settings"
|
||||
className={classNames(
|
||||
active ? "bg-gray-100" : "",
|
||||
"block px-4 py-2 text-sm text-gray-700"
|
||||
)}
|
||||
>
|
||||
Settings
|
||||
</Link>
|
||||
)}
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<div
|
||||
onClick={async () => {
|
||||
await supabase.auth.signOut();
|
||||
router.push("/");
|
||||
}}
|
||||
className={classNames(
|
||||
active ? "bg-gray-100" : "",
|
||||
"block px-4 py-2 text-sm text-gray-700"
|
||||
)}
|
||||
>
|
||||
Logout
|
||||
</div>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<main className="flex-1 pb-8">
|
||||
<div className="mt-8">
|
||||
<div className="mx-auto max-w-6xl px-4 sm:px-6 lg:px-8">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import React from "react";
|
||||
import { ClipboardIcon } from "@heroicons/react/24/outline";
|
||||
import { api } from "~/utils/api";
|
||||
|
||||
export default function SettingsBody() {
|
||||
const { data, status } = api.settings.getAccessToken.useQuery();
|
||||
const [isCopied, setIsCopied] = React.useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
{status === "loading" && <div>Loading...</div>}
|
||||
{status === "success" && (
|
||||
<div className="divide-ylg:col-span-9">
|
||||
<div className="px-4 py-6 sm:p-6 lg:pb-8">
|
||||
<div>
|
||||
<h2 className="text-lg font-medium leading-6 text-gray-900">
|
||||
Chrom Extension
|
||||
</h2>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
Copy the following code and paste it into the extension.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-6 flex flex-col lg:flex-row">
|
||||
<div className="flex-grow space-y-6">
|
||||
<div className="flex">
|
||||
<div className="flex-grow">
|
||||
<input
|
||||
type="password"
|
||||
readOnly
|
||||
defaultValue={data?.accessToken || ""}
|
||||
className="block w-full rounded-md border-gray-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
<span className="ml-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setIsCopied(false);
|
||||
navigator.clipboard.writeText(data?.accessToken || "");
|
||||
setIsCopied(true);
|
||||
}}
|
||||
className="inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2"
|
||||
>
|
||||
<ClipboardIcon
|
||||
className="h-5 w-5 text-gray-500"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="ml-2">
|
||||
{isCopied ? "Copied" : "Copy"}
|
||||
</span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
28
src/components/Sidepanel/body.tsx
Normal file
28
src/components/Sidepanel/body.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import React from "react"
|
||||
import { PlaygroundMessage } from "~components/Common/Playground/Message"
|
||||
import { useMessage } from "~hooks/useMessage"
|
||||
import { EmptySidePanel } from "./empty"
|
||||
|
||||
export const SidePanelBody = () => {
|
||||
const { messages } = useMessage()
|
||||
const divRef = React.useRef<HTMLDivElement>(null)
|
||||
React.useEffect(() => {
|
||||
if (divRef.current) {
|
||||
divRef.current.scrollIntoView({ behavior: "smooth" })
|
||||
}
|
||||
})
|
||||
return (
|
||||
<div className="grow flex flex-col md:translate-x-0 transition-transform duration-300 ease-in-out">
|
||||
{messages.length === 0 && <EmptySidePanel />}
|
||||
{messages.map((message, index) => (
|
||||
<PlaygroundMessage
|
||||
key={index}
|
||||
isBot={message.isBot}
|
||||
message={message.message}
|
||||
/>
|
||||
))}
|
||||
<div className="w-full h-32 md:h-48 flex-shrink-0"></div>
|
||||
<div ref={divRef} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
111
src/components/Sidepanel/empty.tsx
Normal file
111
src/components/Sidepanel/empty.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useMessage } from "~hooks/useMessage"
|
||||
import {
|
||||
fetchModels,
|
||||
getOllamaURL,
|
||||
isOllamaRunning,
|
||||
setOllamaURL as saveOllamaURL
|
||||
} from "~services/ollama"
|
||||
|
||||
export const EmptySidePanel = () => {
|
||||
const [ollamaURL, setOllamaURL] = useState<string>("")
|
||||
const {
|
||||
data: ollamaInfo,
|
||||
status: ollamaStatus,
|
||||
refetch,
|
||||
isRefetching
|
||||
} = useQuery({
|
||||
queryKey: ["ollamaStatus"],
|
||||
queryFn: async () => {
|
||||
const ollamaURL = await getOllamaURL()
|
||||
const isOk = await isOllamaRunning()
|
||||
const models = await fetchModels()
|
||||
|
||||
return {
|
||||
isOk,
|
||||
models,
|
||||
ollamaURL
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (ollamaInfo?.ollamaURL) {
|
||||
setOllamaURL(ollamaInfo.ollamaURL)
|
||||
}
|
||||
}, [ollamaInfo])
|
||||
|
||||
const { setSelectedModel, selectedModel } = useMessage()
|
||||
|
||||
return (
|
||||
<div className="mx-auto sm:max-w-md px-4 mt-10">
|
||||
<div className="rounded-lg justify-center items-center flex flex-col border p-8 bg-white dark:bg-black shadow-sm">
|
||||
{(ollamaStatus === "pending" || isRefetching) && (
|
||||
<div className="inline-flex items-center space-x-2">
|
||||
<div className="w-3 h-3 bg-blue-500 rounded-full animate-bounce"></div>
|
||||
<p className="dark:text-gray-400 text-gray-900">
|
||||
Searching for your Ollama 🦙
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{!isRefetching && ollamaStatus === "success" ? (
|
||||
ollamaInfo.isOk ? (
|
||||
<div className="inline-flex items-center space-x-2">
|
||||
<div className="w-3 h-3 bg-green-500 rounded-full"></div>
|
||||
<p className="dark:text-gray-400 text-gray-900">
|
||||
Ollama is running 🦙
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col space-y-2 justify-center items-center">
|
||||
<div className="inline-flex space-x-2">
|
||||
<div className="w-3 h-3 bg-red-500 rounded-full"></div>
|
||||
<p className="dark:text-gray-400 text-gray-900">
|
||||
We couldn't find your Ollama 🦙
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<input
|
||||
className="bg-gray-100 dark:bg-black dark:text-gray-100 rounded-md px-4 py-2 mt-2 w-full"
|
||||
type="url"
|
||||
value={ollamaURL}
|
||||
onChange={(e) => setOllamaURL(e.target.value)}
|
||||
/>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
saveOllamaURL(ollamaURL)
|
||||
refetch()
|
||||
}}
|
||||
className="bg-blue-500 mt-4 hover:bg-blue-600 text-white px-4 py-2 rounded-md">
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
) : null}
|
||||
|
||||
{ollamaStatus === "success" && ollamaInfo.isOk && (
|
||||
<div className="mt-4">
|
||||
<p className="dark:text-gray-400 text-gray-900">Models:</p>
|
||||
|
||||
<select
|
||||
onChange={(e) => {
|
||||
if (e.target.value === "") {
|
||||
return
|
||||
}
|
||||
setSelectedModel(e.target.value)
|
||||
}}
|
||||
value={selectedModel}
|
||||
className="bg-gray-100 w-full dark:bg-black dark:text-gray-100 rounded-md px-4 py-2 mt-2">
|
||||
<option value={""}>Select a model</option>
|
||||
{ollamaInfo.models.map((model) => (
|
||||
<option value={model.name}>{model.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
102
src/components/Sidepanel/form.tsx
Normal file
102
src/components/Sidepanel/form.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import { useForm } from "@mantine/form"
|
||||
import { useMutation } from "@tanstack/react-query"
|
||||
import React from "react"
|
||||
import { useMessage } from "~hooks/useMessage"
|
||||
|
||||
export const SidepanelForm = () => {
|
||||
const textareaRef = React.useRef<HTMLTextAreaElement>(null)
|
||||
React.useEffect(() => {
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.focus()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const resetHeight = () => {
|
||||
const textarea = textareaRef.current
|
||||
if (textarea) {
|
||||
textarea.style.height = "auto"
|
||||
}
|
||||
}
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
message: ""
|
||||
}
|
||||
})
|
||||
|
||||
const { onSubmit, selectedModel } = useMessage()
|
||||
|
||||
const { mutateAsync: sendMessage, isPending: isSending } = useMutation({
|
||||
mutationFn: onSubmit
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="p-3 md:p-6 md:bg-white dark:bg-[#0a0a0a] border rounded-t-xl border-black/10 dark:border-gray-900/50">
|
||||
<div className="flex-grow space-y-6 ">
|
||||
<div className="flex">
|
||||
<form
|
||||
onSubmit={form.onSubmit(async (value) => {
|
||||
if (!selectedModel || selectedModel.length === 0) {
|
||||
form.setFieldError("message", "Please select a model")
|
||||
return
|
||||
}
|
||||
form.reset()
|
||||
resetHeight()
|
||||
await sendMessage(value.message)
|
||||
})}
|
||||
className="shrink-0 flex-grow flex items-center ">
|
||||
<div className="flex items-center p-2 rounded-full border bg-gray-100 w-full dark:bg-black dark:border-gray-800">
|
||||
<textarea
|
||||
disabled={isSending}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey && !isSending) {
|
||||
e.preventDefault()
|
||||
form.onSubmit(async (value) => {
|
||||
if (value.message.trim().length === 0) {
|
||||
return
|
||||
}
|
||||
if (!selectedModel || selectedModel.length === 0) {
|
||||
form.setFieldError("message", "Please select a model")
|
||||
return
|
||||
}
|
||||
form.reset()
|
||||
resetHeight()
|
||||
await sendMessage(value.message)
|
||||
})()
|
||||
}
|
||||
}}
|
||||
ref={textareaRef}
|
||||
className="rounded-full pl-4 pr-2 py-2 w-full resize-none bg-transparent focus-within:outline-none sm:text-sm focus:ring-0 focus-visible:ring-0 ring-0 dark:ring-0 border-0 dark:text-gray-100"
|
||||
required
|
||||
rows={1}
|
||||
tabIndex={0}
|
||||
placeholder="Type a message..."
|
||||
{...form.getInputProps("message")}
|
||||
/>
|
||||
<button
|
||||
disabled={isSending || form.values.message.length === 0}
|
||||
className="mx-2 flex items-center justify-center w-10 h-10 text-white bg-black rounded-xl disabled:opacity-50">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
className="h-6 w-6"
|
||||
viewBox="0 0 24 24">
|
||||
<path d="M9 10L4 15 9 20"></path>
|
||||
<path d="M20 4v7a4 4 0 01-4 4H4"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{form.errors.message && (
|
||||
<div className="text-red-500 text-center text-sm mt-1">
|
||||
{form.errors.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
26
src/components/Sidepanel/header.tsx
Normal file
26
src/components/Sidepanel/header.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import logoImage from "data-base64:~assets/icon.png"
|
||||
import CogIcon from "@heroicons/react/24/outline/CogIcon"
|
||||
import { ArrowPathIcon } from "@heroicons/react/24/outline"
|
||||
import { useMessage } from "~hooks/useMessage"
|
||||
export const SidepanelHeader = () => {
|
||||
const { clearChat } = useMessage()
|
||||
return (
|
||||
<div className="flex px-3 justify-between bg-white dark:bg-black border-b border-gray-200 dark:border-gray-800 py-4 items-center">
|
||||
<div className="focus:outline-none focus-visible:ring-2 focus-visible:ring-pink-700 flex items-center dark:text-white">
|
||||
<img className="h-6 w-auto" src={logoImage} alt="Page Assist" />
|
||||
<span className="ml-1 text-sm ">Page Assist</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
<button
|
||||
onClick={() => {
|
||||
clearChat()
|
||||
}}
|
||||
className="flex items-center space-x-1 focus:outline-none focus-visible:ring-2 focus-visible:ring-pink-700">
|
||||
<ArrowPathIcon className="h-5 w-5 text-gray-500 dark:text-gray-400" />
|
||||
</button>
|
||||
<CogIcon className="h-5 w-5 text-gray-500 dark:text-gray-400" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user