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

View File

@ -1,14 +0,0 @@
# Since the ".env" file is gitignored, you can use the ".env.example" file to
# build a new ".env" file when you clone the repo. Keep this file up-to-date
# when you add new variables to `.env`.
# This file will be committed to version control, so make sure not to have any
# secrets in it. If you are cloning this repo, create a copy of this file named
# ".env" and populate it with your secrets.
# When adding additional environment variables, the schema in "/src/env.mjs"
# should be updated accordingly.
# Prisma
# https://www.prisma.io/docs/reference/database-reference/connection-urls#env
DATABASE_URL="file:./db.sqlite"

36
.gitignore vendored
View File

@ -1,3 +1,4 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
@ -8,17 +9,10 @@
# testing
/coverage
# database
/prisma/db.sqlite
/prisma/db.sqlite-journal
# next.js
/.next/
/out/
next-env.d.ts
# production
/build
#cache
.turbo
.next
.vercel
# misc
.DS_Store
@ -30,13 +24,19 @@ yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables
.env
.env*.local
# vercel
.vercel
# local env files
.env*
out/
build/
dist/
# plasmo - https://www.plasmo.com
.plasmo
# bpp - http://bpp.browser.market/
keys.json
# typescript
*.tsbuildinfo
.tsbuildinfo

View File

@ -1,2 +0,0 @@
extension
py_server

21
LICENCE
View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2023 Muhammed Nazeem
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,35 +1,33 @@
# Page Assist
This is a [Plasmo extension](https://docs.plasmo.com/) project bootstrapped with [`plasmo init`](https://www.npmjs.com/package/plasmo).
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.
## Getting Started
Here's a demo of how it works:
First, run the development server:
[![PageAssist Demo](https://img.youtube.com/vi/UB1PdZ32vBc/0.jpg)](https://www.youtube.com/watch?v=UB1PdZ32vBc)
```bash
pnpm dev
# or
npm run dev
```
Open your browser and load the appropriate development build. For example, if you are developing for the chrome browser, using manifest v3, use: `build/chrome-mv3-dev`.
## Tools
You can start editing the popup by modifying `popup.tsx`. It should auto-update as you make changes. To add an options page, simply add a `options.tsx` file to the root of the project, with a react component default exported. Likewise to add a content page, add a `content.ts` file to the root of the project, importing some module and do some logic, then reload the extension on your browser.
- NextJs
- Supabase
- FastAPI
- OpenAI's ChatGPT api
- Docker
- Plasmo for chrome extension
For further guidance, [visit our Documentation](https://docs.plasmo.com/)
## Making production build
## Supabase
Run the following:
- Used for Authentication and Database
```bash
pnpm build
# or
npm run build
```
- Used Supabase Auth UI in the dashboard for authentication
This should create a production bundle for your extension, ready to be zipped and published to the stores.
- USed Supabase Python client for FastAPI
## Submit to the webstores
## Demo
- [PageAssist](https://pageassist.n4ze3m.com/)
- [Chrome Extension](https://chrome.google.com/webstore/detail/page-assist/ehkjdalbpmmaddcfdilplgknkgepeakd?hl=en&authuser=2)
## Team
- [Muhammad Nazeem](https://twitter.com/n4ze3m)
The easiest way to deploy your Plasmo extension is to use the built-in [bpp](https://bpp.browser.market) GitHub action. Prior to using this action however, make sure to build your extension and upload the first version to the store to establish the basic credentials. Then, simply follow [this setup instruction](https://docs.plasmo.com/framework/workflows/submit) and you should be on your way for automated submission!

View File

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

42
extension/.gitignore vendored
View File

@ -1,42 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
#cache
.turbo
.next
.vercel
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*
out/
build/
dist/
# plasmo - https://www.plasmo.com
.plasmo
# bpp - http://bpp.browser.market/
keys.json
# typescript
.tsbuildinfo

View File

@ -1,33 +0,0 @@
This is a [Plasmo extension](https://docs.plasmo.com/) project bootstrapped with [`plasmo init`](https://www.npmjs.com/package/plasmo).
## Getting Started
First, run the development server:
```bash
pnpm dev
# or
npm run dev
```
Open your browser and load the appropriate development build. For example, if you are developing for the chrome browser, using manifest v3, use: `build/chrome-mv3-dev`.
You can start editing the popup by modifying `popup.tsx`. It should auto-update as you make changes. To add an options page, simply add a `options.tsx` file to the root of the project, with a react component default exported. Likewise to add a content page, add a `content.ts` file to the root of the project, importing some module and do some logic, then reload the extension on your browser.
For further guidance, [visit our Documentation](https://docs.plasmo.com/)
## Making production build
Run the following:
```bash
pnpm build
# or
npm run build
```
This should create a production bundle for your extension, ready to be zipped and published to the stores.
## Submit to the webstores
The easiest way to deploy your Plasmo extension is to use the built-in [bpp](https://bpp.browser.market) GitHub action. Prior to using this action however, make sure to build your extension and upload the first version to the store to establish the basic credentials. Then, simply follow [this setup instruction](https://docs.plasmo.com/framework/workflows/submit) and you should be on your way for automated submission!

View File

@ -1,25 +0,0 @@
export {};
const toogle = () => {
const iframe = document.getElementById("pageassist-iframe");
const widget = document.getElementById("pageassist-icon");
if (iframe) {
const display = iframe.style.display;
if (display === "none") {
if (widget) {
widget.style.display = "none";
}
iframe.style.display = "block";
} else {
iframe.style.display = "none";
// if user enabled show widget in settings and close from action then show widget will be disappear inorder to show widget again we need to reload the page
}
}
};
chrome.action.onClicked.addListener((tab) => {
chrome.scripting.executeScript({
target: { tabId: tab.id },
func: toogle,
});
});

View File

@ -1,104 +0,0 @@
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,
}, "*");
}
});
};
main();

View File

@ -1,60 +0,0 @@
{
"name": "pageassist",
"displayName": "Page Assist",
"version": "0.0.1",
"description": "Chat with any webpage using an intelligent chat feature",
"author": "n4ze3m",
"scripts": {
"dev": "plasmo dev",
"build": "plasmo build",
"package": "plasmo package"
},
"dependencies": {
"@headlessui/react": "^1.7.13",
"@heroicons/react": "^2.0.16",
"@mantine/form": "^6.0.5",
"@plasmohq/storage": "^1.4.0",
"@tailwindcss/forms": "^0.5.3",
"@tanstack/react-query": "^4.28.0",
"axios": "^1.3.4",
"plasmo": "0.67.4",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-markdown": "^8.0.6",
"react-router-dom": "^6.10.0",
"react-toastify": "^9.1.2"
},
"devDependencies": {
"@plasmohq/prettier-plugin-sort-imports": "3.6.3",
"@types/chrome": "0.0.210",
"@types/node": "18.11.18",
"@types/react": "18.0.27",
"@types/react-dom": "18.0.10",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.21",
"prettier": "2.8.3",
"tailwindcss": "^3.3.0",
"typescript": "4.9.4"
},
"manifest": {
"host_permissions": [
"https://*/*"
],
"web_accessible_resources": [
{
"resources": [
"popup.html"
],
"matches": [
"https://*/*",
"http://*/*"
]
}
],
"permissions": [
"storage",
"activeTab",
"scripting"
]
}
}

5993
extension/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,294 +0,0 @@
import { useEffect, useRef, useState } from "react"
import "./tailwind.css"
import {
ArrowUpOnSquareIcon,
Cog6ToothIcon,
XMarkIcon
} from "@heroicons/react/20/solid"
import { useForm } from "@mantine/form"
import { useMutation } from "@tanstack/react-query"
import axios from "axios"
import logoImage from "data-base64:~assets/icon.png"
import ReactMarkdown from "react-markdown"
import { Link, useNavigate } from "react-router-dom"
import { toast } from "react-toastify"
import { useStorage } from "@plasmohq/storage/hook"
function Chat() {
type Message = {
isBot: boolean
message: string
}
type History = {
bot_response: string
human_message: string
}
const [messages, setMessages] = useState<Message[]>([
{
isBot: true,
message: "Hi, I'm PageAssist Bot. How can I help you?"
}
])
const [history, setHistory] = useState<History[]>([])
const [userToken] = useStorage("pa-token", null)
const route = useNavigate()
const form = useForm({
initialValues: {
message: "",
isBot: false
}
})
const divRef = useRef(null)
useEffect(() => {
divRef.current.scrollIntoView({ behavior: "smooth" })
})
const getHtmlFromParent = () => {
window.parent.postMessage("pageassist-html", "*")
return new Promise((resolve, reject) => {
window.addEventListener("message", (event) => {
if (event.data.type === "pageassist-html") {
resolve(event.data)
} else {
reject("Error")
}
})
})
}
const sendToBot = async (message: string) => {
// @ts-ignore
const { html } = await getHtmlFromParent()
const response = await axios.post(
`${process.env.PLASMO_PUBLIC_API_URL}/chat/chrome`,
{
user_message: message,
html: html,
history: history
}
)
return response.data
}
const onSave = async () => {
const data = await getHtmlFromParent()
const response = await axios.post(
`${process.env.PLASMO_PUBLIC_API_URL}/user/save`,
data,
{
headers: {
"x-auth-token": userToken
}
}
)
return response.data
}
const { mutateAsync: saveAsync, isLoading: isSaving } = useMutation(onSave, {
onSuccess: (data) => {
toast.success("Saved Successfully")
},
onError: (er) => {
console.log(er)
toast.error("Error in saving")
}
})
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" }
])
}
}
)
return (
<div className="isolate bg-gray-100 text-gray-800">
<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"
xmlns="http://www.w3.org/2000/svg">
<path
fill="url(#45de2b6b-92d5-4d68-a6a0-9b9b2abad533)"
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="45de2b6b-92d5-4d68-a6a0-9b9b2abad533"
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>
{/* Component Start */}
<div className="flex items-center justify-between px-6 pt-4 pb-2 md:justify-start md:space-x-10">
<div>
<Link to="/" className="flex">
<img className="h-10 w-auto" src={logoImage} alt="PageAssist" />
</Link>
</div>
<div className="flex items-center space-x-4">
<button
type="button"
className="inline-flex items-center rounded-md border border-transparent bg-white px-3 py-2 text-sm font-medium leading-4 text-gray-800 hover:text-gray-500 shadow-sm hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
onClick={async () => {
// Send data to the app
await saveAsync()
}}>
<ArrowUpOnSquareIcon
className="-ml-1 mr-3 h-5 w-5"
aria-hidden="true"
/>
{isSaving ? "Saving..." : "Send to App"}
</button>
<button
type="button"
className="bg-white rounded-md p-2 inline-flex items-center justify-center text-gray-800 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 shadow-sm focus:ring-inset focus:ring-indigo-500"
onClick={() => {
route("/settings")
}}>
<Cog6ToothIcon className="h-5 w-5" aria-hidden="true" />
</button>
<div className="-my-2 -mr-2 md:hidden">
<button
type="button"
className="bg-white rounded-md p-2 inline-flex items-center justify-center text-gray-800 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 shadow-sm focus:ring-inset focus:ring-indigo-500"
aria-expanded="false"
onClick={() => {
window.parent.postMessage("pageassist-close", "*")
}}>
<XMarkIcon className="h-5 w-5" aria-hidden="true" />
</button>
</div>
</div>
</div>
<div
style={{
minHeight: "calc(100vh - 4rem)"
}}
className="flex flex-col p-6 items-center justify-center w-screen">
<div className="flex flex-col flex-grow w-full max-w-xl bg-white shadow-sm rounded-lg overflow-hidden">
<div className="flex flex-col flex-grow h-0 p-4 overflow-auto">
{messages.map((message, index) => {
return (
<div
key={index}
className={
message.isBot
? "flex w-full mt-2 space-x-3 max-w-xs"
: "flex w-full mt-2 space-x-3 max-w-xs ml-auto justify-end"
}>
<div>
<div
className={
message.isBot
? "bg-gray-300 p-3 rounded-r-lg rounded-bl-lg"
: "bg-blue-600 text-white p-3 rounded-l-lg rounded-br-lg"
}>
<p className="text-sm">
<ReactMarkdown>{message.message}</ReactMarkdown>
</p>
</div>
</div>
</div>
)
})}
{isSending && (
<div className="flex w-full mt-2 space-x-3 max-w-xs">
<div>
<div className="bg-gray-300 p-3 rounded-r-lg rounded-bl-lg">
<p className="text-sm">Hold on, I'm looking...</p>
</div>
</div>
</div>
)}
<div ref={divRef} />
</div>
<div className="bg-gray-300 p-4">
<form
onSubmit={form.onSubmit(async (values) => {
setMessages([...messages, values])
form.reset()
await sendToBotAsync(values.message)
})}>
<div className="flex-grow space-y-6">
<div className="flex">
<span className="mr-3">
<button
disabled={isSending || isSaving}
onClick={() => {
setHistory([])
setMessages([
{
message: "Hi, I'm PageAssist. How can I help you?",
isBot: true
}
])
}}
className="inline-flex items-center rounded-md border border-gray-700 bg-white px-3 h-10 text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2"
type="button">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
className="h-5 w-5 text-gray-600">
<path d="M18.37 2.63 14 7l-1.59-1.59a2 2 0 0 0-2.82 0L8 7l9 9 1.59-1.59a2 2 0 0 0 0-2.82L17 10l4.37-4.37a2.12 2.12 0 1 0-3-3Z"></path>
<path d="M9 8c-2 3-4 3.5-7 4l8 10c2-1 6-5 6-7"></path>
<path d="M14.5 17.5 4.5 15"></path>
</svg>
</button>
</span>
<div className="flex-grow">
<input
disabled={isSending || isSaving}
className="flex items-center h-10 w-full rounded px-3 text-sm"
type="text"
required
placeholder="Type your message…"
{...form.getInputProps("message")}
/>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
)
}
export default Chat

View File

@ -1,12 +0,0 @@
import React from "react"
import { useStorage } from "@plasmohq/storage/hook"
import Chat from "./chat"
import Login from "./login"
export default function Home() {
const [token] = useStorage("pa-token", null)
return <>{token ? <Chat /> : <Login />}</>
}

View File

@ -1,10 +0,0 @@
import { Route, Routes } from "react-router-dom"
import Settings from "./settings"
import Home from "./home"
export const Routing = () => (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/settings" element={<Settings />} />
</Routes>
)

View File

@ -1,158 +0,0 @@
import { XMarkIcon } from "@heroicons/react/20/solid"
import { useForm } from "@mantine/form"
import { useMutation } from "@tanstack/react-query"
import axios from "axios"
import logoImage from "data-base64:~assets/icon.png"
import React from "react"
import { Link, useNavigate } from "react-router-dom"
import { useStorage } from "@plasmohq/storage/hook"
export default function Login() {
const navigate = useNavigate()
const [_, setToken] = useStorage("pa-token", null)
const [err, setErr] = React.useState<string | null>(null)
const form = useForm({
initialValues: {
passcode: ""
}
})
const onSubmit = async (token: string) => {
const response = await axios.post(
`${process.env.PLASMO_PUBLIC_API_URL}/user/validate`,
{
token
}
)
return response.data
}
const { mutateAsync: verifyToken, isLoading: isVerifyingToken } = useMutation(
onSubmit,
{
onSuccess: () => {
setToken(form.values.passcode)
navigate("/")
},
onError: (e:any) => {
if (axios.isAxiosError(e)) {
setErr(e.response?.data.detail)
} else {
setErr(e?.message)
}
}
}
)
return (
<div className="isolate bg-gray-100 text-gray-800">
<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"
xmlns="http://www.w3.org/2000/svg">
<path
fill="url(#45de2b6b-92d5-4d68-a6a0-9b9b2abad533)"
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="45de2b6b-92d5-4d68-a6a0-9b9b2abad533"
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>
{/* Component Start */}
<div className="flex items-center justify-between px-6 pt-4 pb-2 md:justify-start md:space-x-10">
<div>
<Link to="/" className="flex">
<img className="h-10 w-auto" src={logoImage} alt="PageAssist" />
</Link>
</div>
<div className="flex items-center space-x-4">
<div className="-my-2 -mr-2 md:hidden">
<button
type="button"
className="bg-white rounded-md p-2 inline-flex items-center justify-center text-gray-800 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 shadow-sm focus:ring-inset focus:ring-indigo-500"
aria-expanded="false"
onClick={() => {
window.parent.postMessage("pageassist-close", "*")
}}>
<XMarkIcon className="h-5 w-5" aria-hidden="true" />
</button>
</div>
</div>
</div>
<div
style={{
minHeight: "calc(100vh - 4rem)"
}}
className="flex flex-col p-6 w-screen">
<h2 className="mt-6 text-center text-3xl font-bold tracking-tight text-gray-900">
Sign in to your account
</h2>
<div className="bg-white py-8 px-4 shadow-sm rounded-lg sm:px-10 mt-8">
<ul className="list-disc list-inside text-gray-800 text-md">
<li>Log in to your Page Assist account.</li>
<li>Go to your account settings.</li>
<li>Find your passcode under "Chrome Extension".</li>
<li>Copy your passcode to your clipboard.</li>
<li>Open the PageAssist extension and paste your passcode.</li>
<li>Click "Save" and Happy Chatting!</li>
</ul>
<form
className="space-y-6"
onSubmit={form.onSubmit(async (values) => {
await verifyToken(values.passcode)
})}>
<div>
<div className="mt-3">
<input
id="passcode"
name="passcode"
type="password"
autoComplete="current-passcode"
placeholder="Passcode"
required
{...form.getInputProps("passcode")}
className="block w-full appearance-none rounded-md border border-gray-300 px-3 py-2 placeholder-gray-400 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm"
/>
{
err && (
<div className="text-red-500 text-sm mt-2">
{err}
</div>
)
}
</div>
</div>
<div>
<button
type="submit"
disabled={isVerifyingToken}
className="flex w-full justify-center rounded-md border border-transparent bg-teal-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-teal-700 focus:outline-none focus:ring-2 focus:ring-teal-500 focus:ring-offset-2">
{isVerifyingToken ? "Saving..." : "Save"}
</button>
</div>
</form>
</div>
</div>
</div>
)
}

View File

@ -1,128 +0,0 @@
import { Switch } from "@headlessui/react"
import { ChevronLeftIcon, XMarkIcon } from "@heroicons/react/20/solid"
import logoImage from "data-base64:~assets/icon.png"
import { useState } from "react"
import { Link, useNavigate } from "react-router-dom"
import { useStorage } from "@plasmohq/storage/hook"
import { useChatWidget } from "~hooks/useLocal"
//@ts-ignore
function classNames(...classes) {
return classes.filter(Boolean).join(" ")
}
function Settings() {
const route = useNavigate()
const [active, setActiveValue] = useStorage("chat-widget", false)
const [_, setToken] = useStorage("pa-token", null)
return (
<div className="isolate bg-gray-100 text-gray-800">
<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"
xmlns="http://www.w3.org/2000/svg">
<path
fill="url(#45de2b6b-92d5-4d68-a6a0-9b9b2abad533)"
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="45de2b6b-92d5-4d68-a6a0-9b9b2abad533"
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 className="flex items-center justify-between px-6 pt-4 pb-2 md:justify-start md:space-x-10">
<div>
<Link to="/" className="flex">
<img className="h-10 w-auto" src={logoImage} alt="PageAssist" />
</Link>
</div>
<div className="flex items-center space-x-4">
<button
type="button"
className="bg-white shadow-sm rounded-md p-2 inline-flex items-center justify-center text-gray-800 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500"
onClick={() => {
route("/")
}}>
<ChevronLeftIcon className="h-5 w-5" aria-hidden="true" />
</button>
<div className="-my-2 -mr-2 md:hidden">
<button
type="button"
className="bg-white shadow-sm rounded-md p-2 inline-flex items-center justify-center text-gray-800 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500"
aria-expanded="false"
onClick={() => {
window.parent.postMessage("pageassist-close", "*")
}}>
<XMarkIcon className="h-5 w-5" aria-hidden="true" />
</button>
</div>
</div>
</div>
<div
style={{
minHeight: "calc(100vh - 4rem)"
}}
className="flex flex-col p-6 w-screen">
<ul role="list" className="mt-2 divide-y divide-gray-200">
<Switch.Group
as="li"
className="flex items-center justify-between py-4">
<div className="flex flex-col">
<Switch.Label
as="p"
className="text-sm font-medium text-gray-900"
passive>
Hide Widget Icon
</Switch.Label>
<Switch.Description className="text-sm text-gray-500">
Hide or Show the widget icon on websites you visit.
</Switch.Description>
</div>
<Switch
checked={active}
onChange={setActiveValue}
className={classNames(
active ? "bg-teal-500" : "bg-gray-200",
"relative ml-4 inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2"
)}>
<span
aria-hidden="true"
className={classNames(
active ? "translate-x-5" : "translate-x-0",
"inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
)}
/>
</Switch>
</Switch.Group>
</ul>
<button
type="button"
onClick={() => {
setToken(null)
route("/")
}}
className="flex w-full justify-center rounded-md border border-transparent bg-red-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2">
Logout
</button>
</div>
</div>
)
}
export default Settings

View File

@ -1,11 +0,0 @@
{
"extends": "plasmo/templates/tsconfig.base",
"exclude": ["node_modules"],
"include": [".plasmo/index.d.ts", "./**/*.ts", "./**/*.tsx"],
"compilerOptions": {
"paths": {
"~*": ["./*"]
},
"baseUrl": "."
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,24 +0,0 @@
/**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation.
* This is especially useful for Docker builds.
*/
!process.env.SKIP_ENV_VALIDATION && (await import("./src/env.mjs"));
/** @type {import("next").NextConfig} */
const config = {
reactStrictMode: true,
typescript: {
ignoreBuildErrors: true,
},
/**
* If you have the "experimental: { appDir: true }" setting enabled, then you
* must comment the below `i18n` config out.
*
* @see https://github.com/vercel/next.js/issues/41980
*/
i18n: {
locales: ["en"],
defaultLocale: "en",
},
};
export default config;

View File

@ -1,58 +1,78 @@
{
"name": "page-assist",
"version": "0.1.0",
"private": true,
"name": "pageassist",
"displayName": "Page Assist - AI Powered Browser Assistant",
"version": "0.0.1",
"description": "Use your local AI models to assist you in your daily browsing.",
"author": "n4ze3m",
"scripts": {
"build": "next build",
"dev": "next dev",
"postinstall": "prisma generate",
"lint": "next lint",
"start": "next start"
"dev": "plasmo dev",
"build": "plasmo build",
"package": "plasmo package"
},
"dependencies": {
"@headlessui/react": "^1.7.13",
"@heroicons/react": "^2.0.17",
"@mantine/form": "^6.0.7",
"@prisma/client": "^4.11.0",
"@supabase/auth-helpers-nextjs": "^0.6.0",
"@supabase/auth-helpers-react": "^0.3.1",
"@supabase/auth-ui-react": "^0.3.5",
"@supabase/auth-ui-shared": "^0.1.3",
"@supabase/supabase-js": "^2.15.0",
"@tailwindcss/forms": "^0.5.3",
"@tanstack/react-query": "^4.28.0",
"@trpc/client": "^10.18.0",
"@trpc/next": "^10.18.0",
"@trpc/react-query": "^10.18.0",
"@trpc/server": "^10.18.0",
"axios": "^1.3.5",
"langchain": "^0.0.55",
"next": "^13.2.4",
"@headlessui/react": "^1.7.18",
"@heroicons/react": "^2.1.1",
"@langchain/community": "^0.0.21",
"@mantine/form": "^7.5.0",
"@plasmohq/storage": "^1.9.0",
"@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.10",
"@tanstack/react-query": "^5.17.19",
"antd": "^5.13.3",
"axios": "^1.6.7",
"langchain": "^0.1.9",
"plasmo": "0.84.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-youtube": "^10.1.0",
"superjson": "1.12.2",
"zod": "^3.21.4"
"react-markdown": "8.0.7",
"react-router-dom": "6.10.0",
"react-syntax-highlighter": "^15.5.0",
"react-toastify": "^10.0.4",
"rehype-mathjax": "4.0.3",
"remark-gfm": "3.0.1",
"remark-math": "5.1.1",
"voy-search": "^0.6.3",
"zustand": "^4.5.0"
},
"devDependencies": {
"@types/eslint": "^8.21.3",
"@types/node": "^18.15.5",
"@types/prettier": "^2.7.2",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@typescript-eslint/eslint-plugin": "^5.56.0",
"@typescript-eslint/parser": "^5.56.0",
"autoprefixer": "^10.4.14",
"eslint": "^8.36.0",
"eslint-config-next": "^13.2.4",
"postcss": "^8.4.21",
"prettier": "^2.8.6",
"prettier-plugin-tailwindcss": "^0.2.6",
"prisma": "^4.11.0",
"tailwindcss": "^3.3.0",
"typescript": "^5.0.2"
"@plasmohq/prettier-plugin-sort-imports": "4.0.1",
"@types/chrome": "0.0.259",
"@types/node": "20.11.9",
"@types/react": "18.2.48",
"@types/react-dom": "18.2.18",
"@types/react-syntax-highlighter": "^15.5.11",
"autoprefixer": "^10.4.17",
"postcss": "^8.4.33",
"prettier": "3.2.4",
"tailwindcss": "^3.4.1",
"typescript": "5.3.3"
},
"ct3aMetadata": {
"initVersion": "7.10.3"
"manifest": {
"host_permissions": [
"https://*/*",
"http://*/*",
"http://*:11434/api/tags",
"http://*:11434/api/chat",
"https://*:11434/api/tags",
"https://*:11434/api/chat"
],
"web_accessible_resources": [
{
"resources": [
"popup.html"
],
"matches": [
"https://*/*",
"http://*/*"
]
}
],
"permissions": [
"storage",
"activeTab",
"scripting",
"declarativeNetRequest",
"declarativeNetRequestFeedback"
]
}
}
}

6863
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +0,0 @@
const config = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
module.exports = config;

View File

@ -1,6 +0,0 @@
/** @type {import("prettier").Config} */
const config = {
plugins: [require.resolve("prettier-plugin-tailwindcss")],
};
module.exports = config;

View File

@ -1,27 +0,0 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @db.Uuid
email String?
access_token String?
created_at DateTime? @default(now()) @db.Timestamptz(6)
Website Website[]
}
model Website {
id String @id @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
title String?
icon String?
html String?
user_id String? @db.Uuid
created_at DateTime? @default(now()) @db.Timestamptz(6)
url String?
User User? @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -1,5 +0,0 @@
dev.bat
*.pyc
*.pyo
.pytest_cache/
*/.env

View File

@ -1,7 +0,0 @@
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.11
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .

View File

@ -1,40 +0,0 @@
import supabase
import os
class SupaService:
def __init__(self):
self.supabase_url = os.environ.get("SUPABASE_URL")
self.supabase_key = os.environ.get("SUPABASE_KEY")
self.supabase = supabase.create_client(self.supabase_url, self.supabase_key)
def validate_user(self, token):
user = self.supabase.table("User").select("*").eq("access_token", token).execute()
return user
def save_webiste(self, title: str, icon: str, html: str, url: str, user_id: str):
result = self.supabase.table("Website").insert( {
"title": title,
"icon": icon,
"html": html,
"url": url,
"user_id": user_id
}).execute()
return result
def find_website(self, id: str, user_id: str):
result = self.supabase.table("Website").select("*").eq("id", id).eq("user_id", user_id).execute()
return result
def get_user(self, jwt: str):
try:
result = self.supabase.auth.get_user(jwt)
return result
except:
return None

View File

@ -1,160 +0,0 @@
from models import ChatBody, ChatAppBody
from bs4 import BeautifulSoup
from langchain.docstore.document import Document as LDocument
from langchain.vectorstores.faiss import FAISS
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.llms import OpenAI
from langchain.text_splitter import CharacterTextSplitter
from langchain.chains import ConversationalRetrievalChain
from langchain.prompts.chat import (
ChatPromptTemplate,
SystemMessagePromptTemplate,
HumanMessagePromptTemplate
)
from langchain.vectorstores import Chroma
from db.supa import SupaService
supabase = SupaService()
async def chat_app_handler(body: ChatAppBody, jwt: str):
try:
user = supabase.get_user(jwt)
if not user:
return {
"bot_response": "You are not logged in",
"human_message": body.user_message,
}
user_id = user.user.id
website_response = supabase.find_website(body.id, user_id)
website = website_response.data
if len(website) == 0:
return {
"bot_response": "Website not found",
"human_message": body.user_message,
}
website = website[0]
text = website["html"]
text = text.strip()
result = [LDocument(page_content=text, metadata={"source": "test"})]
token_splitter = CharacterTextSplitter(
chunk_size=1000, chunk_overlap=0)
doc = token_splitter.split_documents(result)
print(f'Number of documents: {len(doc)}')
vectorstore = Chroma.from_documents(doc, OpenAIEmbeddings())
messages = [
SystemMessagePromptTemplate.from_template("""You are PageAssist bot. Answer the question based on the following context from the webpage you are on.
Answer must be in markdown format.
-----------------
context:
{context}
"""),
HumanMessagePromptTemplate.from_template("{question}")
]
prompt = ChatPromptTemplate.from_messages(messages)
chat = ConversationalRetrievalChain.from_llm(OpenAI(temperature=0, model_name="gpt-3.5-turbo"), vectorstore.as_retriever(
search_kwargs={"k": 1}), return_source_documents=True, qa_prompt=prompt,)
history = [(d["human_message"], d["bot_response"])
for d in body.history]
response = chat({
"question": body.user_message,
"chat_history": history
})
answer = response["answer"]
answer = answer[answer.find(":")+1:].strip()
return {
"bot_response": answer,
"human_message": body.user_message,
}
except Exception as e:
print(e)
return {
"bot_response": "Something went wrong please try again later",
"human_message": body.user_message,
}
async def chat_extension_handler(body: ChatBody):
try:
soup = BeautifulSoup(body.html, 'lxml')
iframe = soup.find('iframe', id='pageassist-iframe')
if iframe:
iframe.decompose()
div = soup.find('div', id='pageassist-icon')
if div:
div.decompose()
div = soup.find('div', id='__plasmo-loading__')
if div:
div.decompose()
text = soup.get_text()
text = text.strip()
result = [LDocument(page_content=text, metadata={"source": "test"})]
token_splitter = CharacterTextSplitter(
chunk_size=1000, chunk_overlap=0)
doc = token_splitter.split_documents(result)
print(f'Number of documents: {len(doc)}')
vectorstore = Chroma.from_documents(doc, OpenAIEmbeddings())
messages = [
SystemMessagePromptTemplate.from_template("""You are PageAssist bot. Answer the question based on the following context from the webpage you are on.
Answer must be in markdown format.
-----------------
context:
{context}
"""),
HumanMessagePromptTemplate.from_template("{question}")
]
prompt = ChatPromptTemplate.from_messages(messages)
chat = ConversationalRetrievalChain.from_llm(OpenAI(temperature=0, model_name="gpt-3.5-turbo"), vectorstore.as_retriever(
search_kwargs={"k": 1}), return_source_documents=True, qa_prompt=prompt,)
history = [(d["human_message"], d["bot_response"])
for d in body.history]
response = chat({
"question": body.user_message,
"chat_history": history
})
answer = response["answer"]
answer = answer[answer.find(":")+1:].strip()
return {
"bot_response": answer,
"human_message": body.user_message,
}
except Exception as e:
print(e)
return {
"bot_response": "Something went wrong please try again later",
"human_message": body.user_message,
}

View File

@ -1,57 +0,0 @@
from fastapi import HTTPException, Header
from models import UserValidation, SaveChatToApp
from db.supa import SupaService
from bs4 import BeautifulSoup
supabase = SupaService()
async def validate_user_handler(user: UserValidation):
if user.token is None or user.token == "":
raise HTTPException(status_code=400, detail="Token is required")
user = supabase.validate_user(user.token)
data = user.data
if len(data) == 0:
raise HTTPException(status_code=400, detail="Invalid token")
return {
"status": "success",
}
async def save_website_handler(body: SaveChatToApp, x_auth_token):
try:
if x_auth_token is None or x_auth_token == "":
raise HTTPException(status_code=400, detail="Token is required")
user = supabase.validate_user(x_auth_token)
data = user.data
if len(data) == 0:
raise HTTPException(status_code=400, detail="Invalid token")
soup = BeautifulSoup(body.html, 'lxml')
title = soup.title.string if soup.title else "Untitled Page"
icon = soup.find('link', rel='icon').get('href') if soup.find('link', rel='icon') else None
iframe = soup.find('iframe', id='pageassist-iframe')
if iframe:
iframe.decompose()
div = soup.find('div', id='pageassist-icon')
if div:
div.decompose()
div = soup.find('div', id='__plasmo-loading__')
if div:
div.decompose()
text = soup.get_text()
result = supabase.save_webiste(html=text, title=title, icon=icon, url=body.url, user_id=data[0]["id"])
return {
"status": "Success"
}
except Exception as e:
raise HTTPException(status_code=500, detail="Internal server error")

View File

@ -1,30 +0,0 @@
from fastapi import FastAPI
import os
from uvicorn import run
from fastapi.middleware.cors import CORSMiddleware
from routers import chat, user
os.environ["OPENAI_API_KEY"] = os.environ.get("OPENAI_API_KEY")
app = FastAPI()
origins = ["*"]
methods = ["*"]
headers = ["*"]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=methods,
allow_headers=headers
)
app.include_router(chat.router)
app.include_router(user.router)
if __name__ == "__main__":
port = int(os.environ.get('PORT', 5000))
run(app, host="0.0.0.0", port=port)

View File

@ -1,2 +0,0 @@
from .chat import ChatBody, ChatAppBody
from .user import UserValidation, SaveChatToApp

View File

@ -1,13 +0,0 @@
from pydantic import BaseModel
class ChatBody(BaseModel):
user_message: str
html: str
history: list
# url: str
class ChatAppBody(BaseModel):
id: str
user_message: str
url: str
history: list

View File

@ -1,11 +0,0 @@
from pydantic import BaseModel
class UserValidation(BaseModel):
token: str
class SaveChatToApp(BaseModel):
html: str
url: str

View File

@ -1,14 +0,0 @@
fastapi
uvicorn
pydantic
pandas
openai
beautifulsoup4
numpy
pydantic
langchain
lxml
faiss-cpu
supabase
tiktoken
chromadb

View File

@ -1,14 +0,0 @@
from fastapi import APIRouter, Header
from models import ChatBody, ChatAppBody
from handlers.chat import chat_extension_handler, chat_app_handler
router = APIRouter(prefix="/api/v1")
@router.post("/chat/chrome", tags=["chat"])
async def chat_extension(body: ChatBody):
return await chat_extension_handler(body)
@router.post("/chat/app", tags=["chat"])
async def chat_app(body: ChatAppBody, x_auth_token: str = Header()):
return await chat_app_handler(body, x_auth_token)

View File

@ -1,15 +0,0 @@
from fastapi import APIRouter, Header
from models import UserValidation, SaveChatToApp
from handlers.user import validate_user_handler, save_website_handler
router = APIRouter(prefix="/api/v1")
@router.post("/user/validate", tags=["user"])
async def validate_user(user: UserValidation):
return await validate_user_handler(user)
@router.post("/user/save", tags=["user"])
async def save_website(body: SaveChatToApp, x_auth_token: str = Header(None)):
return await save_website_handler(body, x_auth_token)

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()

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

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

View File

@ -1,20 +1,18 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { MemoryRouter } from "react-router-dom"
import { Routing } from "~routes"
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { ToastContainer } from "react-toastify"
import "react-toastify/dist/ReactToastify.css"
const queryClient = new QueryClient()
import "./css/tailwind.css"
function IndexPopup() {
function IndexOption() {
return (
<MemoryRouter>
<QueryClientProvider client={queryClient}>
<Routing />
<ToastContainer />
</QueryClientProvider>
</MemoryRouter>
)
}
export default IndexPopup
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}`;
};

View File

@ -2,9 +2,9 @@
module.exports = {
mode: "jit",
darkMode: "class",
content: ["./**/*.tsx"],
content: ["./src/**/*.tsx"],
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/typography')
]
}

View File

@ -1,11 +0,0 @@
import { type Config } from "tailwindcss";
export default {
content: ["./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {},
},
plugins: [
require('@tailwindcss/forms'),
],
} satisfies Config;

View File

@ -1,33 +1,11 @@
{
"extends": "plasmo/templates/tsconfig.base",
"exclude": ["node_modules"],
"include": [".plasmo/index.d.ts", "./**/*.ts", "./**/*.tsx"],
"compilerOptions": {
"target": "es2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"checkJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"noUncheckedIndexedAccess": true,
"baseUrl": ".",
"paths": {
"~/*": ["./src/*"]
}
},
"include": [
".eslintrc.cjs",
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
"**/*.cjs",
"**/*.mjs"
],
"exclude": ["node_modules", "extension", "server"]
}
"~*": ["./src/*"]
},
"baseUrl": "."
}
}

7702
yarn.lock Normal file

File diff suppressed because it is too large Load Diff