v2 initial commit

This commit is contained in:
n4ze3m
2024-02-01 13:40:44 +05:30
parent 43439e5511
commit 0aa4aefb08
95 changed files with 13517 additions and 16778 deletions

10
src/background.ts Normal file
View File

@@ -0,0 +1,10 @@
export {}
chrome.runtime.onMessage.addListener(async (message) => {
chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => {
const tab = tabs[0]
await chrome.sidePanel.open({
windowId: tab.windowId
})
})
})

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View 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>
)
}

View 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>
)
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
)}
</>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
</>
);
}

View File

@@ -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>
)}
</>
);
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

131
src/content.ts Normal file
View File

@@ -0,0 +1,131 @@
export {}
import { Storage } from "@plasmohq/storage"
const storage = new Storage()
// const main = async () => {
// const isChatWidgetEnabled = await storage.get("chat-widget");
// var iframe = document.createElement("iframe");
// iframe.id = "pageassist-iframe";
// iframe.style.backgroundColor = "white";
// iframe.style.position = "fixed";
// iframe.style.top = "0px";
// iframe.style.right = "0px";
// iframe.style.zIndex = "9000000000000000000";
// iframe.style.border = "0px";
// iframe.style.display = "none";
// iframe.style.width = "500px";
// iframe.style.height = "100%";
// iframe.src = chrome.runtime.getURL("popup.html");
// document.body.appendChild(iframe);
// var toggleIcon = document.createElement("div");
// if (isChatWidgetEnabled) {
// toggleIcon.style.display = "none";
// } else {
// toggleIcon.style.display = "block";
// }
// toggleIcon.id = "pageassist-icon";
// toggleIcon.style.position = "fixed";
// toggleIcon.style.top = "50%";
// toggleIcon.style.right = "0px";
// toggleIcon.style.transform = "translateY(-50%)";
// toggleIcon.style.zIndex = "9000000000000000000";
// toggleIcon.style.background = "linear-gradient(to bottom, #0c0d52, #023e8a)";
// toggleIcon.style.height = "50px";
// toggleIcon.style.width = "50px";
// toggleIcon.style.borderTopLeftRadius = "10px";
// toggleIcon.style.borderBottomLeftRadius = "10px";
// toggleIcon.style.cursor = "pointer";
// var iconBackground = document.createElement("div");
// iconBackground.style.backgroundRepeat = "no-repeat";
// iconBackground.style.backgroundSize = "contain";
// iconBackground.style.height = "100%";
// iconBackground.style.backgroundImage =
// "url('data:image/png;base64,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')";
// iconBackground.style.width = "100%";
// iconBackground.style.opacity = "0.7";
// iconBackground.style.position = "absolute";
// iconBackground.style.top = "0";
// iconBackground.style.left = "0";
// toggleIcon.appendChild(iconBackground);
// toggleIcon.addEventListener("click", function () {
// if (iframe.style.display === "none") {
// iframe.style.display = "block";
// toggleIcon.style.display = "none";
// toggleIcon.classList.add("hidden");
// } else {
// iframe.style.display = "none";
// toggleIcon.classList.remove("hidden");
// }
// });
// document.body.appendChild(toggleIcon);
// // iframe.addEventListener("load", function () {
// // var closeButton = iframe.contentDocument.createElement("button");
// // closeButton.innerText = "Close";
// // closeButton.style.position = "fixed";
// // closeButton.style.top = "20px";
// // closeButton.style.right = "20px";
// // closeButton.addEventListener("click", function () {
// // toggleIcon.classList.remove("hidden");
// // iframe.style.display = "none";
// // });
// // iframe.contentDocument.body.appendChild(closeButton);
// // });
// window.addEventListener("message", function (event) {
// if (event.data === "pageassist-close") {
// iframe.style.display = "none";
// if (!isChatWidgetEnabled) {
// toggleIcon.style.display = "block";
// toggleIcon.classList.remove("hidden");
// }
// } else if (event.data === "pageassist-html") {
// console.log("pageassist-html");
// let html = document.documentElement.outerHTML;
// let url = window.location.href;
// iframe.contentWindow.postMessage({
// type: "pageassist-html",
// html: html,
// url: url,
// }, "*");
// }
// });
// };
const sidePanelController = async () => {
// get sidepanel open or close command from storage else Ctrl+0
const sidepanelCommand = await storage.get("sidepanel-command")
const command = sidepanelCommand || "Ctrl+0"
// listen to keydown event
document.addEventListener("keydown", (event) => {
let pressedKey = ""
if (event.ctrlKey) {
pressedKey += "Ctrl+"
}
if (event.shiftKey) {
pressedKey += "Shift+"
}
pressedKey += event.key
console.log(pressedKey)
if (pressedKey === command) {
// send a message to background.js to open or close sidepanel
chrome.runtime.sendMessage({ type: "sidepanel" })
}
})
}
sidePanelController()

BIN
src/css/font.ttf Normal file

Binary file not shown.

12
src/css/tailwind.css Normal file
View File

@@ -0,0 +1,12 @@
@font-face {
font-family: 'font';
src: url('font.ttf') format('truetype');
}
body {
font-family: 'font' !important;
}
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -1,76 +0,0 @@
import { z } from "zod";
/**
* Specify your server-side environment variables schema here. This way you can ensure the app isn't
* built with invalid env vars.
*/
const server = z.object({
DATABASE_URL: z.string().url(),
NODE_ENV: z.enum(["development", "test", "production"]),
});
/**
* Specify your client-side environment variables schema here. This way you can ensure the app isn't
* built with invalid env vars. To expose them to the client, prefix them with `NEXT_PUBLIC_`.
*/
const client = z.object({
// NEXT_PUBLIC_CLIENTVAR: z.string().min(1),
});
/**
* You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g.
* middlewares) or client-side so we need to destruct manually.
*
* @type {Record<keyof z.infer<typeof server> | keyof z.infer<typeof client>, string | undefined>}
*/
const processEnv = {
DATABASE_URL: process.env.DATABASE_URL,
NODE_ENV: process.env.NODE_ENV,
// NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
};
// Don't touch the part below
// --------------------------
const merged = server.merge(client);
/** @typedef {z.input<typeof merged>} MergedInput */
/** @typedef {z.infer<typeof merged>} MergedOutput */
/** @typedef {z.SafeParseReturnType<MergedInput, MergedOutput>} MergedSafeParseReturn */
let env = /** @type {MergedOutput} */ (process.env);
if (!!process.env.SKIP_ENV_VALIDATION == false) {
const isServer = typeof window === "undefined";
const parsed = /** @type {MergedSafeParseReturn} */ (
isServer
? merged.safeParse(processEnv) // on server we can validate all env vars
: client.safeParse(processEnv) // on client we can only validate the ones that are exposed
);
if (parsed.success === false) {
console.error(
"❌ Invalid environment variables:",
parsed.error.flatten().fieldErrors,
);
throw new Error("Invalid environment variables");
}
env = new Proxy(parsed.data, {
get(target, prop) {
if (typeof prop !== "string") return undefined;
// Throw a descriptive error if a server-side env var is accessed on the client
// Otherwise it would just be returning `undefined` and be annoying to debug
if (!isServer && !prop.startsWith("NEXT_PUBLIC_"))
throw new Error(
process.env.NODE_ENV === "production"
? "❌ Attempted to access a server-side environment variable on the client"
: `❌ Attempted to access server-side environment variable '${prop}' on the client`,
);
return target[/** @type {keyof typeof target} */ (prop)];
},
});
}
export { env };

40
src/hooks/useLocal.tsx Normal file
View File

@@ -0,0 +1,40 @@
import React from "react"
export default function useLocal(key: string) {
const [value, setValue] = React.useState<string | null>(null)
React.useEffect(() => {
chrome.storage.local.get(key, (result) => {
setValue(result[key])
})
}, [key])
const update = (newValue: string) => {
chrome.storage.local.set({ [key]: newValue }, () => {
setValue(newValue)
})
}
const remove = () => {
chrome.storage.local.remove(key)
setValue(null)
}
return { value, update, remove }
}
export function useChatWidget() {
const { value, update } = useLocal("chat-widget")
const [active, setActive] = React.useState<boolean>(value === "show")
const setActiveValue = (newValue: boolean) => {
if (newValue) {
update("show")
} else {
update("hide")
}
setActive(newValue)
}
return { active, setActiveValue }
}

208
src/hooks/useMessage.tsx Normal file
View File

@@ -0,0 +1,208 @@
import React from "react"
import { cleanUrl } from "~libs/clean-url"
import { getOllamaURL, isOllamaRunning } from "~services/ollama"
import { useStoreMessage, type ChatHistory } from "~store"
import { ChatOllama } from "@langchain/community/chat_models/ollama"
import { HumanMessage, AIMessage } from "@langchain/core/messages"
export type BotResponse = {
bot: {
text: string
sourceDocuments: any[]
}
history: ChatHistory
history_id: string
}
const generateHistory = (
messages: {
role: "user" | "assistant" | "system"
content: string
}[]
) => {
let history = []
for (const message of messages) {
if (message.role === "user") {
history.push(
new HumanMessage({
content: [
{
type: "text",
text: message.content
}
]
})
)
} else if (message.role === "assistant") {
history.push(
new AIMessage({
content: [
{
type: "text",
text: message.content
}
]
})
)
}
}
return history
}
export const useMessage = () => {
const {
history,
messages,
setHistory,
setMessages,
setStreaming,
streaming,
setIsFirstMessage,
historyId,
setHistoryId,
isLoading,
setIsLoading,
isProcessing,
setIsProcessing,
selectedModel,
setSelectedModel
} = useStoreMessage()
const abortControllerRef = React.useRef<AbortController | null>(null)
const clearChat = () => {
stopStreamingRequest()
setMessages([])
setHistory([])
setHistoryId(null)
setIsFirstMessage(true)
}
const normalChatMode = async (message: string) => {
const url = await getOllamaURL()
abortControllerRef.current = new AbortController()
const ollama = new ChatOllama({
model: selectedModel,
baseUrl: cleanUrl(url)
})
let newMessage = [
...messages,
{
isBot: false,
message,
sources: []
},
{
isBot: true,
message: "▋",
sources: []
}
]
const appendingIndex = newMessage.length - 1
setMessages(newMessage)
try {
const chunks = await ollama.stream(
[
...generateHistory(history),
new HumanMessage({
content: [
{
type: "text",
text: message
}
]
})
],
{
signal: abortControllerRef.current.signal
}
)
let count = 0
for await (const chunk of chunks) {
if (count === 0) {
setIsProcessing(true)
newMessage[appendingIndex].message = chunk.content + "▋"
setMessages(newMessage)
} else {
newMessage[appendingIndex].message =
newMessage[appendingIndex].message.slice(0, -1) +
chunk.content +
"▋"
setMessages(newMessage)
}
count++
}
newMessage[appendingIndex].message = newMessage[
appendingIndex
].message.slice(0, -1)
setHistory([
...history,
{
role: "user",
content: message
},
{
role: "assistant",
content: newMessage[appendingIndex].message
}
])
setIsProcessing(false)
} catch (e) {
console.log(e)
setIsProcessing(false)
setStreaming(false)
setMessages([
...messages,
{
isBot: true,
message: `Something went wrong. Check out the following logs:
\`\`\`
${e?.message}
\`\`\`
`,
sources: []
}
])
}
}
const onSubmit = async (message: string) => {
await normalChatMode(message)
}
const stopStreamingRequest = () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort()
abortControllerRef.current = null
}
}
return {
messages,
setMessages,
onSubmit,
setStreaming,
streaming,
setHistory,
historyId,
setHistoryId,
setIsFirstMessage,
isLoading,
setIsLoading,
isProcessing,
stopStreamingRequest,
clearChat,
selectedModel,
setSelectedModel
}
}

25
src/langchain/normal.ts Normal file
View File

@@ -0,0 +1,25 @@
import { HumanMessage, AIMessage } from "@langchain/core/messages"
import { ChatMessageHistory } from "langchain/stores/message/in_memory"
import { ChatOllama } from "@langchain/community/chat_models/ollama"
import { getOllamaURL } from "~services/ollama"
import { cleanUrl } from "~libs/clean-url"
export class NormalChatOllama {
ollama: ChatOllama
async _init() {
const ollamaURL = await getOllamaURL()
this.ollama = new ChatOllama({
baseUrl: cleanUrl(ollamaURL),
model: "qwen:1.8b-chat"
})
}
constructor() {
this._init()
}
async send(message: HumanMessage) {
if (!this.ollama) return null
}
}

7
src/libs/clean-url.ts Normal file
View File

@@ -0,0 +1,7 @@
// clean url ending if it with /
export const cleanUrl = (url: string) => {
if (url.endsWith("/")) {
return url.slice(0, -1)
}
return url
}

31
src/libs/runtime.ts Normal file
View File

@@ -0,0 +1,31 @@
export const chromeRunTime = async function (domain: string) {
if (typeof chrome !== "undefined" && chrome.runtime && chrome.runtime.id) {
const url = new URL(domain)
const domains = [url.hostname]
const rules = [
{
id: 1,
priority: 1,
condition: {
requestDomains: domains
},
action: {
type: "modifyHeaders",
requestHeaders: [
{
header: "Origin",
operation: "set",
value: `${url.protocol}//${url.hostname}`
}
]
}
}
]
await chrome.declarativeNetRequest.updateDynamicRules({
removeRuleIds: rules.map((r) => r.id),
// @ts-ignore
addRules: rules
})
}
}

18
src/option.tsx Normal file
View File

@@ -0,0 +1,18 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { MemoryRouter } from "react-router-dom"
import { ToastContainer } from "react-toastify"
import "react-toastify/dist/ReactToastify.css"
const queryClient = new QueryClient()
import "./css/tailwind.css"
function IndexOption() {
return (
<MemoryRouter>
<QueryClientProvider client={queryClient}>
<ToastContainer />
</QueryClientProvider>
</MemoryRouter>
)
}
export default IndexOption

View File

@@ -1,48 +0,0 @@
import { AppProps, type AppType } from "next/app";
import { api } from "~/utils/api";
import "~/styles/globals.css";
import { Poppins } from "next/font/google";
import {
createBrowserSupabaseClient,
Session,
} from "@supabase/auth-helpers-nextjs";
import React from "react";
import { SessionContextProvider } from "@supabase/auth-helpers-react";
const poppins = Poppins({
weight: ["400", "500", "600", "700", "800", "900"],
style: ["normal"],
subsets: ["latin"],
});
function MyApp({
Component,
pageProps,
}: AppProps<{
initialSession: Session;
}>): JSX.Element {
const [supabaseClient] = React.useState(() => createBrowserSupabaseClient());
return (
<>
<style jsx global>
{`
html,
body {
font-family: ${poppins.style.fontFamily} !important;
}
`}
</style>
<SessionContextProvider
supabaseClient={supabaseClient}
initialSession={pageProps.initialSession}
>
<Component {...pageProps} />
</SessionContextProvider>
</>
);
}
export default api.withTRPC(MyApp);

View File

@@ -1,19 +0,0 @@
import { createNextApiHandler } from "@trpc/server/adapters/next";
import { env } from "~/env.mjs";
import { createTRPCContext } from "~/server/api/trpc";
import { appRouter } from "~/server/api/root";
// export API handler
export default createNextApiHandler({
router: appRouter,
createContext: createTRPCContext,
onError:
env.NODE_ENV === "development"
? ({ path, error }) => {
console.error(
`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`,
);
}
: undefined,
});

View File

@@ -1,23 +0,0 @@
import {NextApiRequest, NextApiResponse} from 'next'
import { prisma } from '~/server/db'
export default async (req: NextApiRequest, res: NextApiResponse) => {
const {token } = req.body
if(!token) {
return res.status(400).json({error: 'Token is required'})
}
const isUserExist = await prisma.user.findFirst({
where: {
access_token: token
}
})
if(!isUserExist) {
return res.status(400).json({error: 'Invalid token'})
}
return res.status(200).json({message: 'Token is valid'})
}

View File

@@ -1,76 +0,0 @@
import { useSupabaseClient, useUser } from "@supabase/auth-helpers-react";
import { type NextPage } from "next";
import Head from "next/head";
import { useRouter } from "next/router";
import React from "react";
import { Auth } from "@supabase/auth-ui-react";
import { ThemeSupa, ThemeMinimal } from "@supabase/auth-ui-shared";
const AuthPage: NextPage = () => {
const supabaseClient = useSupabaseClient();
const user = useUser();
const router = useRouter();
React.useEffect(() => {
if (user) {
router.push("/dashboard");
}
}, [user]);
return (
<>
<Head>
<title>Get Started / Page Assist</title>
</Head>
<div className="relative isolate flex min-h-full flex-col justify-center overflow-hidden bg-white py-12 sm:px-6 lg:px-8">
<svg
className="absolute inset-0 -z-10 h-full w-full stroke-gray-200 [mask-image:radial-gradient(100%_100%_at_top_right,white,transparent)]"
aria-hidden="true"
>
<defs>
<pattern
id="0787a7c5-978c-4f66-83c7-11c213f99cb7"
width={200}
height={200}
x="50%"
y={-1}
patternUnits="userSpaceOnUse"
>
<path d="M.5 200V.5H200" fill="none" />
</pattern>
</defs>
<rect
width="100%"
height="100%"
strokeWidth={0}
fill="url(#0787a7c5-978c-4f66-83c7-11c213f99cb7)"
/>
</svg>
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<img
className="mx-auto h-12 w-auto"
src="logo.png"
alt="Page Assist"
/>
<h2 className="mt-6 text-center text-3xl font-bold tracking-tight text-gray-900">
Page Assist
</h2>
</div>
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div className="px-4 py-8 shadow sm:rounded-lg sm:px-10">
<Auth
supabaseClient={supabaseClient}
providers={[]}
appearance={{ theme: ThemeSupa }}
view="magic_link"
showLinks={false}
magicLink={true}
/>
</div>
</div>
</div>
</>
);
};
export default AuthPage;

View File

@@ -1,38 +0,0 @@
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

@@ -1,38 +0,0 @@
import { createServerSupabaseClient } from "@supabase/auth-helpers-nextjs";
import { GetServerSideProps, NextPage } from "next";
import Head from "next/head";
import DashboardBoby from "~/components/Dashboard";
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 DashboardPage: NextPage = () => {
return (
<DashboardLayout>
<Head>
<title>Dashboard / PageAssist</title>
</Head>
<DashboardBoby />
</DashboardLayout>
);
};
export default DashboardPage;

View File

@@ -1,38 +0,0 @@
import { createServerSupabaseClient } from "@supabase/auth-helpers-nextjs";
import { GetServerSideProps, NextPage } from "next";
import Head from "next/head";
import DashboardLayout from "~/components/Layouts/DashboardLayout";
import SettingsBody from "~/components/Settings";
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 DashboardSettingsPage: NextPage = () => {
return (
<DashboardLayout>
<Head>
<title>Settings / PageAssist</title>
</Head>
<SettingsBody />
</DashboardLayout>
);
};
export default DashboardSettingsPage;

View File

@@ -1,47 +0,0 @@
import { type NextPage } from "next";
import Head from "next/head";
import Link from "next/link";
import Hero from "~/components/Landing/Hero";
const Home: NextPage = () => {
return (
<>
<Head>
<title>PageAssist</title>
<link rel="icon" href="/logo.png" />
</Head>
<div className="isolate bg-white">
<div className="absolute inset-x-0 top-[-10rem] -z-10 transform-gpu overflow-hidden blur-3xl sm:top-[-20rem]">
<svg
className="relative left-[calc(50%-11rem)] -z-10 h-[21.1875rem] max-w-none -translate-x-1/2 rotate-[30deg] sm:left-[calc(50%-30rem)] sm:h-[42.375rem]"
viewBox="0 0 1155 678"
>
<path
fill="url(#9b2541ea-d39d-499b-bd42-aeea3e93f5ff)"
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="9b2541ea-d39d-499b-bd42-aeea3e93f5ff"
x1="1155.49"
x2="-78.208"
y1=".177"
y2="474.645"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#9089FC" />
<stop offset={1} stopColor="#023e8a" />
</linearGradient>
</defs>
</svg>
</div>
<Hero />
</div>
</>
);
};
export default Home;

12
src/routes/index.tsx Normal file
View File

@@ -0,0 +1,12 @@
import { Route, Routes } from "react-router-dom"
import { SidepanelChat } from "./sidepanel-chat"
export const Routing = () => <Routes></Routes>
export const SidepanelRouting = () => (
<div className="dark">
<Routes>
<Route path="/" element={<SidepanelChat />} />
</Routes>
</div>
)

View File

@@ -0,0 +1,22 @@
import { SidePanelBody } from "~components/Sidepanel/body"
import { SidepanelForm } from "~components/Sidepanel/form"
import { SidepanelHeader } from "~components/Sidepanel/header"
export const SidepanelChat = () => {
return (
<div className="flex bg-white dark:bg-black flex-col min-h-screen mx-auto max-w-7xl">
<div className="sticky top-0 z-10">
<SidepanelHeader />
</div>
<SidePanelBody />
<div className="bottom-0 w-full bg-transparent border-0 fixed pt-2">
<div className="stretch mx-2 flex flex-row gap-3 md:mx-4 lg:mx-auto lg:max-w-2xl xl:max-w-3xl">
<div className="relative flex flex-col h-full flex-1 items-stretch md:flex-col">
<SidepanelForm />
</div>
</div>
</div>
</div>
)
}

View File

@@ -1,18 +0,0 @@
import { createTRPCRouter } from "~/server/api/trpc";
import { exampleRouter } from "~/server/api/routers/example";
import { settingsRouter } from "./routers/settings";
import { chatRouter } from "./routers/chat";
/**
* This is the primary router for your server.
*
* All routers added in /api/routers should be manually added here.
*/
export const appRouter = createTRPCRouter({
example: exampleRouter,
settings: settingsRouter,
chat: chatRouter
});
// export type definition of API
export type AppRouter = typeof appRouter;

View File

@@ -1,111 +0,0 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
export const chatRouter = createTRPCRouter({
getSavedSitesForChat: publicProcedure
.query(async ({ ctx }) => {
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 sites = await prisma.website.findMany({
where: {
user_id: user.id,
},
select: {
user_id: false,
id: true,
created_at: true,
html: false,
icon: true,
title: true,
url: true,
},
});
return {
data: sites,
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;
}),
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;
}),
});

View File

@@ -1,16 +0,0 @@
import { z } from "zod";
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
export const exampleRouter = createTRPCRouter({
hello: publicProcedure
.input(z.object({ text: z.string() }))
.query(({ input }) => {
return {
greeting: `Hello ${input.text}`,
};
}),
getAll: publicProcedure.query(({ ctx }) => {
return ctx.prisma.example.findMany();
}),
});

View File

@@ -1,35 +0,0 @@
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
export const settingsRouter = createTRPCRouter({
getAccessToken: publicProcedure
.query(async ({ ctx }) => {
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 accessToken = await prisma.user.findFirst({
where: {
id: user.id,
},
});
if (!accessToken) {
throw new TRPCError({
"code": "UNAUTHORIZED",
"message": "You are not authorized to access this resource",
});
}
return {
accessToken: accessToken.access_token,
};
}),
});

View File

@@ -1,106 +0,0 @@
/**
* YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS:
* 1. You want to modify request context (see Part 1).
* 2. You want to create a new middleware or type of procedure (see Part 3).
*
* TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will
* need to use are documented accordingly near the end.
*/
/**
* 1. CONTEXT
*
* This section defines the "contexts" that are available in the backend API.
*
* These allow you to access things when processing a request, like the database, the session, etc.
*/
import { type CreateNextContextOptions } from "@trpc/server/adapters/next";
import { prisma } from "~/server/db";
type CreateContextOptions = Record<string, never>;
/**
* This helper generates the "internals" for a tRPC context. If you need to use it, you can export
* it from here.
*
* Examples of things you may need it for:
* - testing, so we don't have to mock Next.js' req/res
* - tRPC's `createSSGHelpers`, where we don't have req/res
*
* @see https://create.t3.gg/en/usage/trpc#-serverapitrpcts
*/
const createInnerTRPCContext = (_opts: CreateContextOptions) => {
return {
prisma,
};
};
/**
* This is the actual context you will use in your router. It will be used to process every request
* that goes through your tRPC endpoint.
*
* @see https://trpc.io/docs/context
*/
export const createTRPCContext = async (_opts: CreateNextContextOptions) => {
const supabaseServerClient = createServerSupabaseClient(_opts);
const {
data: { user },
} = await supabaseServerClient.auth.getUser();
return {
...createInnerTRPCContext({}),
user,
supabase: supabaseServerClient,
};
};
/**
* 2. INITIALIZATION
*
* This is where the tRPC API is initialized, connecting the context and transformer. We also parse
* ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation
* errors on the backend.
*/
import { initTRPC } from "@trpc/server";
import superjson from "superjson";
import { ZodError } from "zod";
import { createServerSupabaseClient } from "@supabase/auth-helpers-nextjs";
const t = initTRPC.context<typeof createTRPCContext>().create({
transformer: superjson,
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError: error.cause instanceof ZodError
? error.cause.flatten()
: null,
},
};
},
});
/**
* 3. ROUTER & PROCEDURE (THE IMPORTANT BIT)
*
* These are the pieces you use to build your tRPC API. You should import these a lot in the
* "/src/server/api/routers" directory.
*/
/**
* This is how you create new routers and sub-routers in your tRPC API.
*
* @see https://trpc.io/docs/router
*/
export const createTRPCRouter = t.router;
/**
* Public (unauthenticated) procedure
*
* This is the base piece you use to build new queries and mutations on your tRPC API. It does not
* guarantee that a user querying is authorized, but you can still access user session data if they
* are logged in.
*/
export const publicProcedure = t.procedure;

View File

@@ -1,16 +0,0 @@
import { PrismaClient } from "@prisma/client";
import { env } from "~/env.mjs";
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({
log:
env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
});
if (env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;

73
src/services/ollama.ts Normal file
View File

@@ -0,0 +1,73 @@
import { Storage } from "@plasmohq/storage"
import { cleanUrl } from "~libs/clean-url"
import { chromeRunTime } from "~libs/runtime"
const storage = new Storage()
const DEFAULT_OLLAMA_URL = "http://127.0.0.1:11434"
const DEFAULT_ASK_FOR_MODEL_SELECTION_EVERY_TIME = true
export const getOllamaURL = async () => {
const ollamaURL = await storage.get("ollamaURL")
if (!ollamaURL || ollamaURL.length === 0) {
await chromeRunTime(DEFAULT_OLLAMA_URL)
return DEFAULT_OLLAMA_URL
}
await chromeRunTime(cleanUrl(ollamaURL))
return ollamaURL
}
export const askForModelSelectionEveryTime = async () => {
const askForModelSelectionEveryTime = await storage.get(
"askForModelSelectionEveryTime"
)
if (
!askForModelSelectionEveryTime ||
askForModelSelectionEveryTime.length === 0
)
return DEFAULT_ASK_FOR_MODEL_SELECTION_EVERY_TIME
return askForModelSelectionEveryTime
}
export const defaultModel = async () => {
const defaultModel = await storage.get("defaultModel")
return defaultModel
}
export const isOllamaRunning = async () => {
try {
const baseUrl = await getOllamaURL()
const response = await fetch(`${cleanUrl(baseUrl)}`)
if (!response.ok) {
throw new Error(response.statusText)
}
return true
} catch (e) {
console.error(e)
return false
}
}
export const fetchModels = async () => {
try {
const baseUrl = await getOllamaURL()
const response = await fetch(`${cleanUrl(baseUrl)}/api/tags`)
if (!response.ok) {
throw new Error(response.statusText)
}
const json = await response.json()
return json.models as {
name: string
model: string
}[]
} catch (e) {
console.error(e)
return []
}
}
export const setOllamaURL = async (ollamaURL: string) => {
await chromeRunTime(cleanUrl(ollamaURL))
await storage.set("ollamaURL", cleanUrl(ollamaURL))
}

20
src/sidepanel.tsx Normal file
View File

@@ -0,0 +1,20 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { MemoryRouter } from "react-router-dom"
import { SidepanelRouting } from "~routes"
import { ToastContainer } from "react-toastify"
import "react-toastify/dist/ReactToastify.css"
const queryClient = new QueryClient()
import "./css/tailwind.css"
function IndexOption() {
return (
<MemoryRouter>
<QueryClientProvider client={queryClient}>
<SidepanelRouting />
<ToastContainer />
</QueryClientProvider>
</MemoryRouter>
)
}
export default IndexOption

51
src/store/index.tsx Normal file
View File

@@ -0,0 +1,51 @@
import { create } from "zustand"
export type Message = {
isBot: boolean
message: string
sources: any[]
}
export type ChatHistory = {
role: "user" | "assistant" | "system"
content: string
}[]
type State = {
messages: Message[]
setMessages: (messages: Message[]) => void
history: ChatHistory
setHistory: (history: ChatHistory) => void
streaming: boolean
setStreaming: (streaming: boolean) => void
isFirstMessage: boolean
setIsFirstMessage: (isFirstMessage: boolean) => void
historyId: string | null
setHistoryId: (history_id: string | null) => void
isLoading: boolean
setIsLoading: (isLoading: boolean) => void
isProcessing: boolean
setIsProcessing: (isProcessing: boolean) => void
selectedModel: string | null
setSelectedModel: (selectedModel: string) => void
}
export const useStoreMessage = create<State>((set) => ({
messages: [],
setMessages: (messages) => set({ messages }),
history: [],
setHistory: (history) => set({ history }),
streaming: true,
setStreaming: (streaming) => set({ streaming }),
isFirstMessage: true,
setIsFirstMessage: (isFirstMessage) => set({ isFirstMessage }),
historyId: null,
setHistoryId: (historyId) => set({ historyId }),
isLoading: false,
setIsLoading: (isLoading) => set({ isLoading }),
isProcessing: false,
setIsProcessing: (isProcessing) => set({ isProcessing }),
defaultSpeechToTextLanguage: "en-US",
selectedModel: null,
setSelectedModel: (selectedModel) => set({ selectedModel })
}))

View File

@@ -1,20 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
.yt-video {
height: 500px;
}
@media screen and (max-width: 768px) {
.yt-video {
height: 300px;
}
}
@media screen and (max-width: 480px) {
.yt-video {
height: 200px;
}
}

View File

@@ -1,68 +0,0 @@
/**
* This is the client-side entrypoint for your tRPC API. It is used to create the `api` object which
* contains the Next.js App-wrapper, as well as your type-safe React Query hooks.
*
* We also create a few inference helpers for input and output types.
*/
import { httpBatchLink, loggerLink } from "@trpc/client";
import { createTRPCNext } from "@trpc/next";
import { type inferRouterInputs, type inferRouterOutputs } from "@trpc/server";
import superjson from "superjson";
import { type AppRouter } from "~/server/api/root";
const getBaseUrl = () => {
if (typeof window !== "undefined") return ""; // browser should use relative url
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; // SSR should use vercel url
return `http://localhost:${process.env.PORT ?? 3000}`; // dev SSR should use localhost
};
/** A set of type-safe react-query hooks for your tRPC API. */
export const api = createTRPCNext<AppRouter>({
config() {
return {
/**
* Transformer used for data de-serialization from the server.
*
* @see https://trpc.io/docs/data-transformers
*/
transformer: superjson,
/**
* Links used to determine request flow from client to server.
*
* @see https://trpc.io/docs/links
*/
links: [
loggerLink({
enabled: (opts) =>
process.env.NODE_ENV === "development" ||
(opts.direction === "down" && opts.result instanceof Error),
}),
httpBatchLink({
url: `${getBaseUrl()}/api/trpc`,
}),
],
};
},
/**
* Whether tRPC should await queries when server rendering pages.
*
* @see https://trpc.io/docs/nextjs#ssr-boolean-default-false
*/
ssr: false,
});
/**
* Inference helper for inputs.
*
* @example type HelloInput = RouterInputs['example']['hello']
*/
export type RouterInputs = inferRouterInputs<AppRouter>;
/**
* Inference helper for outputs.
*
* @example type HelloOutput = RouterOutputs['example']['hello']
*/
export type RouterOutputs = inferRouterOutputs<AppRouter>;

View File

@@ -1,16 +0,0 @@
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}`;
};