commit
8a24faf7e4
14
.env.example
14
.env.example
@ -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
36
.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
@ -8,17 +9,10 @@
|
|||||||
# testing
|
# testing
|
||||||
/coverage
|
/coverage
|
||||||
|
|
||||||
# database
|
#cache
|
||||||
/prisma/db.sqlite
|
.turbo
|
||||||
/prisma/db.sqlite-journal
|
.next
|
||||||
|
.vercel
|
||||||
# next.js
|
|
||||||
/.next/
|
|
||||||
/out/
|
|
||||||
next-env.d.ts
|
|
||||||
|
|
||||||
# production
|
|
||||||
/build
|
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
@ -30,13 +24,19 @@ yarn-debug.log*
|
|||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
.pnpm-debug.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
|
# local env files
|
||||||
.vercel
|
.env*
|
||||||
|
|
||||||
|
out/
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# plasmo - https://www.plasmo.com
|
||||||
|
.plasmo
|
||||||
|
|
||||||
|
# bpp - http://bpp.browser.market/
|
||||||
|
keys.json
|
||||||
|
|
||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
.tsbuildinfo
|
||||||
|
@ -1,2 +0,0 @@
|
|||||||
extension
|
|
||||||
py_server
|
|
37
README.md
37
README.md
@ -1,35 +1,22 @@
|
|||||||
# Page Assist
|
# Page Assist
|
||||||
|
|
||||||
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.
|
A simple browser extension to assist you in talking with the current page, along with a web UI for the [Ollama](https://github.com/ollama/ollama) project.
|
||||||
|
|
||||||
Here's a demo of how it works:
|
|
||||||
|
|
||||||
[](https://www.youtube.com/watch?v=UB1PdZ32vBc)
|
|
||||||
|
|
||||||
|
|
||||||
## Tools
|
## Features
|
||||||
|
|
||||||
- NextJs
|
- [X] Fully local, no data is sent to any server
|
||||||
- Supabase
|
- [x] Chat with the current page
|
||||||
- FastAPI
|
- [X] Web UI for Ollama
|
||||||
- OpenAI's ChatGPT api
|
- [ ] Chat with Youtube videos
|
||||||
- Docker
|
- [ ] Chat with PDFs
|
||||||
- Plasmo for chrome extension
|
- [ ] Other Local AI providers
|
||||||
|
|
||||||
|
|
||||||
## Supabase
|
## V1
|
||||||
|
|
||||||
- Used for Authentication and Database
|
If you are looking for the V1 of this project, you can find it on v1 branch. I created it as a hackathon project and it is not maintained anymore.
|
||||||
|
|
||||||
- Used Supabase Auth UI in the dashboard for authentication
|
## License
|
||||||
|
|
||||||
- USed Supabase Python client for FastAPI
|
MIT
|
||||||
|
|
||||||
## 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)
|
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
42
extension/.gitignore
vendored
42
extension/.gitignore
vendored
@ -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
|
|
@ -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!
|
|
@ -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,
|
|
||||||
});
|
|
||||||
});
|
|
@ -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,iVBORw0KGgoAAAANSUhEUgAAAHgAAAB4CAQAAACTbf5ZAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAB3RJTUUH5gcFDDQINdz8vAAAAAJiS0dEAP+Hj8y/AAAKmklEQVR42u2ceXCU5R3Hv++72evd7ObYHJvDJGbZ3AdJCAmHWosEhKGCAuGQoC1XPJjWFhG5RJ0600qxHTuCHAmHwgyIoOPYIir0D61EiJUOGgNKolxKEJFCAsl++8f77hLJ4SLv7r7E/ea/vMPk+fB7jt/1PEBIIYUUUkghhRRSSCGFFFJIIYUUUkgh+aonsUK8y5wWMTCp1FnoKnSVOsuS0iPGmteJf+pLmONwv5hnTymOmxK9TKq1vq2vMzVIzebj5uNSs6lBX2d921JrX+aYnFYywD5XnHTjghISKvRprpgq2xpTfViL2CEQPfwIFNv1Z8z1tnWO6RkZ4/Q3gzcW7IPYI+TEx1eGbzE0ie3dAlJkd/8BYrux2bbFMbnIcV546saAHY1FQpoz6jFTva7tCoiOduZxFGdxKZ/jGm7gBq7hc1zCmRzFPNqp64StuyR9FLsg07lQ+JW2YasAOFMjlxgaBbdn8FaW8mFu5AF+zVZ2p1ae4gFu4kMcQOsVW7tNh2OW5KYCj2h1zUahwGafZTzogTWwkAv4Ls/QTV/kZgv38DEWUO+FNh90zCq3xWlvTQ/HeiG51LJTVKaxxBHcyJM+ov4Q+wQ3soKSZ3q3WXeml/5bmKgl3EwUSfYHDM3yEI2s4HZ+z+vROW7ncBoUaGNzfPVQqUgbsE/ACleidZWuVR5cHtfwLNXQt1zNHI+dWyNX5SfasS7YuPMApORKb8nr1sI5PEw11chZtMhHmTt8V0Yu8Hwwce8AcNMg0wHZCunc0MM+fD26yFrerJze0oH0cmBcsHBvA5B4i+mQjHsLP6C/9AGHKlPbfCjtFuCuYODeDiB5kIwrcjyP0p/6gncrnpn5kLMcGB94f8qKm3Llyazjffya/tYpVlGUj7z9GbnRmB1YJyMJrkTpLdm697GFgdBpTqNAUKD1nwUJzkA6I7kol6yr5Ck2PgDW9egkxynbV9TKUeayQOH+EkD0A/K5eyu/YCD1ubJ9hbU6qoGpgcB9GRYkD5S9Kif3MdB6XzmkjE3OUhv+Fwg3ssBm2Sm7GRsYDNUofrbt1SE2v7ubkwDYZ4uXQLDaD26Gb67ITCWscMz0e/B4E9LTjAdBMF9lJ/Ja1KD42NLHeakZ/sQdCiByiUDQwLUMplZRT1BkzCK/+l1JSHUZGkFwhEoR0U+PpO4gCJo+y3Q6/elMRi0QCEp8lcHWNpoJioydD0zwD7ALWQ5TPQiOvM7wXg19p9hYOjAgvtAfuM9AQOwk8RKo5yZqQbUMI6hrS64UsVd94DLco7dsAcH+PKEJ4GPMJwhGbH5UP1p94GSkZuibQPBxakWPyj7X0SyX6ofTIwDs08UO0Ma9mgF+m+EEde2J00SsURd4DD4RwteC4ECe0QzwaZYQBKNWU5ijLnAe8mLkHXoutaQHlJTAYPsgdYEdSC4JawF1fElTwOspEjS09CtOVRfYjJgpYgcYw3pNAX/IaIK69qRJVrW3rcgnBYL5Acxv+JYDySEoMHapqrBTQcG8HgRHs01TwBc5Uq5R1lBYpmaUVGEOfwcEZ1Nr+o3sfOyeZh6rHnACEiLC6kDwCZ8GcYn13Mo9/K6H7818ja+xucdC2h5uZT0v+fS3Fsml2X1Ztkz1gIvRP8n4GQj+zadptoxxFGnhBDZ18/1NFlBPPQv4ZjdfmziBFoqM4zJe8OGv/UUOEz8dmniresBF6O80fwkKPoX9W721XXAuO7pYt9D7tbCLlTs41/tV4lafUgEgaGkamq4icA6yXabjoMiNPgxhdqdujXyeuurr696qL2jg613qC/md/rUvO0YNBYLSsYEuFfPU2cjKkIE3XTPw1zcicBEKlSm97ucxpUtQlByoTav5Gjet5f7YtFKRGqn/8NqOpW29Hkuv/8ixtO0ajyWjusfSFcdjzs/D8ajCRUHSqGs5wh+uZaVmg4cT/gkeKmH6uYWHWk0A1MoJgNP9ilROAOQhL8aowRRPtZLiGRI9WF3gu3BGsCpJvBbN4J5msTeJ95C6wNUAYpQ07R7NAO/2pml1qFW/MuxJxC/QDPA8JRGf3S8TqqscE/WWzZostbz8eNgY9YGXQ/QW0zZqZIeWi2lJE3Wo90f90IlMh9x7N4LntFMu3V8a76fWlmGdCuLbgw68VSmIx82XPUE/tbSk9ZPDxAp+G1TcMxwmh4UN2c5+fm5qWSw3tawOKvBKpakldhFwtz/7eFLgTDX+BwRz2Rg03AZmy+v3Iz+3LSmNaTPluyuzf9CYdplf8j2+wTdZx1NdEjvqhoSexrSEGcB8f/fiZaDAZtkBghH8lzKEC3yD0+iilUYaGckCzuX7vOwn4LWe1sPtt1pL/N9r+RosSC41HgUjlF6ARk5neJdbhLFcyG/8gPse0xT/ylkaEZie6WEgRv166vlVbCVZzzIvpJmxtHvTsCLH85jKuEc4WG4fvuiYE6D2YQBww21wr6CbPMohCl4/Ps5dPMiPuIOzGa/8tkpVF+Ukx3oaxF8YbS5HAOWGO4HvXubDii3v4Sedrt21c69idwNfUDEYvDdYVwCAyyBY9s7hOILgmC4FFfJjxbXvr9K0Psl7vZc8MnOiMQsB1lJQSFstEEzooS/+JZoI6vmyCrifc5xyjUc65CzzozPZq8KxBwSns73bQbYo07r6unHf9+4U5kNpQ4N0UQtACj4HwRd7HKjcVDScF6/LzahRDiKB0n5nGTAZwVIWToBh3NnjYJ8kCJb3WHL5cX3GmYqbIbituzJygBoET058BYq9rNHHKF/1+WnNxt/yRcVnBnUXo1YWJkRhB4IpO+p7azZt5RiC4KQe1nhvOsdXOOzKhekmx5xfmEsQZAki1oNgSQ85rn2UD61nrwnVzePcwOGdrsTbXk0f0CRUQguaiDZQx6e6iY7OcQpB0OFzccbNFr7L+czv/OjBx44Zg6ypmnn0IBrvgGAUV15VVTzLecqwq3mZ5AUe5slenrXYzw18kCWdghDRbWqMXZybAvxOI7AQAaACp0AwnLP4Ac+xnZfZwn9wjIJbyE9JXuBvmcwcjuQMLuYKruZ6rudqruBizuCdzGX0Dx8uaZPqY+dnO5/V1tsOgABBxByclYdp5xBO4DgWe+0kndxxhG1kAxOvfoJGcRO7eZqmKWJzYmVx/Hks1QilHvFIQZRsYABhqMIX3QzejTrzHUzleP71m/duOyG09/b4kK5d3yIdiFyTMC3LVakv0MyaNWIMtuC/OII6LEe+19CFWIWv0OFFuIQGPI0UJa4SaHs01zExZml4jW23oc7UIDVLyvNShn223eE19icSJjuLB9nnifdragZb8Ay+72SZwxgbB8Fj52zcjz+jBmvxNCYgxTsDvFqJV8QpZldEeVJ5erGryFWeXp7ktE00bxP+Dm3q92jzTEEF+SgGdl3W6CNyohEEU7iE61hFs4xcgzD0UU1FO2jlK0oM86AMfASpfRV4IQgWe+v/u2giiLMY0FeB/wCCWfzSW8zSE8RpFPZV4DtxAQzjQp5lBz/l7fKUrkdsXwWOxl75tazBvIcuz069BH1YFTh2lY+0Cwl9llaAKGA09sPzbON5bIYTfV5JuA/PoxZ/xAjBghvtwc2QQgoppJBCCimkQOr/ecIE+5d512IAAAAldEVYdGRhdGU6Y3JlYXRlADIwMjItMDctMDVUMTI6NTE6NTkrMDA6MDCsFSHZAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIyLTA3LTA1VDEyOjUxOjU5KzAwOjAw3UiZZQAAAABJRU5ErkJggg==')";
|
|
||||||
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();
|
|
@ -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
5993
extension/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,20 +0,0 @@
|
|||||||
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';
|
|
||||||
|
|
||||||
const queryClient = new QueryClient()
|
|
||||||
|
|
||||||
function IndexPopup() {
|
|
||||||
return (
|
|
||||||
<MemoryRouter>
|
|
||||||
<QueryClientProvider client={queryClient}>
|
|
||||||
<Routing />
|
|
||||||
<ToastContainer />
|
|
||||||
</QueryClientProvider>
|
|
||||||
</MemoryRouter>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default IndexPopup
|
|
@ -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
|
|
@ -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 />}</>
|
|
||||||
}
|
|
@ -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>
|
|
||||||
)
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -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
|
|
@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "plasmo/templates/tsconfig.base",
|
|
||||||
"exclude": ["node_modules"],
|
|
||||||
"include": [".plasmo/index.d.ts", "./**/*.ts", "./**/*.tsx"],
|
|
||||||
"compilerOptions": {
|
|
||||||
"paths": {
|
|
||||||
"~*": ["./*"]
|
|
||||||
},
|
|
||||||
"baseUrl": "."
|
|
||||||
}
|
|
||||||
}
|
|
5305
extension/yarn.lock
5305
extension/yarn.lock
File diff suppressed because it is too large
Load Diff
@ -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;
|
|
120
package.json
120
package.json
@ -1,58 +1,84 @@
|
|||||||
{
|
{
|
||||||
"name": "page-assist",
|
"name": "pageassist",
|
||||||
"version": "0.1.0",
|
"displayName": "Page Assist - A Web UI for Local AI Models",
|
||||||
"private": true,
|
"version": "1.0.0",
|
||||||
|
"description": "Use your locally running AI models to assist you in your web browsing.",
|
||||||
|
"author": "n4ze3m",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "next build",
|
"dev": "plasmo dev",
|
||||||
"dev": "next dev",
|
"build": "plasmo build",
|
||||||
"postinstall": "prisma generate",
|
"package": "plasmo package"
|
||||||
"lint": "next lint",
|
|
||||||
"start": "next start"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^1.7.13",
|
"@ant-design/cssinjs": "^1.18.4",
|
||||||
"@heroicons/react": "^2.0.17",
|
"@headlessui/react": "^1.7.18",
|
||||||
"@mantine/form": "^6.0.7",
|
"@heroicons/react": "^2.1.1",
|
||||||
"@prisma/client": "^4.11.0",
|
"@langchain/community": "^0.0.21",
|
||||||
"@supabase/auth-helpers-nextjs": "^0.6.0",
|
"@langchain/core": "^0.1.22",
|
||||||
"@supabase/auth-helpers-react": "^0.3.1",
|
"@mantine/form": "^7.5.0",
|
||||||
"@supabase/auth-ui-react": "^0.3.5",
|
"@plasmohq/storage": "^1.9.0",
|
||||||
"@supabase/auth-ui-shared": "^0.1.3",
|
"@tailwindcss/forms": "^0.5.7",
|
||||||
"@supabase/supabase-js": "^2.15.0",
|
"@tailwindcss/typography": "^0.5.10",
|
||||||
"@tailwindcss/forms": "^0.5.3",
|
"@tanstack/react-query": "^5.17.19",
|
||||||
"@tanstack/react-query": "^4.28.0",
|
"antd": "^5.13.3",
|
||||||
"@trpc/client": "^10.18.0",
|
"axios": "^1.6.7",
|
||||||
"@trpc/next": "^10.18.0",
|
"html-to-text": "^9.0.5",
|
||||||
"@trpc/react-query": "^10.18.0",
|
"langchain": "^0.1.9",
|
||||||
"@trpc/server": "^10.18.0",
|
"lucide-react": "^0.323.0",
|
||||||
"axios": "^1.3.5",
|
"plasmo": "0.84.1",
|
||||||
"langchain": "^0.0.55",
|
"property-information": "^6.4.1",
|
||||||
"next": "^13.2.4",
|
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-youtube": "^10.1.0",
|
"react-markdown": "8.0.0",
|
||||||
"superjson": "1.12.2",
|
"react-router-dom": "6.10.0",
|
||||||
"zod": "^3.21.4"
|
"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": {
|
"devDependencies": {
|
||||||
"@types/eslint": "^8.21.3",
|
"@plasmohq/prettier-plugin-sort-imports": "4.0.1",
|
||||||
"@types/node": "^18.15.5",
|
"@types/chrome": "0.0.259",
|
||||||
"@types/prettier": "^2.7.2",
|
"@types/html-to-text": "^9.0.4",
|
||||||
"@types/react": "^18.0.28",
|
"@types/node": "20.11.9",
|
||||||
"@types/react-dom": "^18.0.11",
|
"@types/react": "18.2.48",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.56.0",
|
"@types/react-dom": "18.2.18",
|
||||||
"@typescript-eslint/parser": "^5.56.0",
|
"@types/react-syntax-highlighter": "^15.5.11",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.17",
|
||||||
"eslint": "^8.36.0",
|
"postcss": "^8.4.33",
|
||||||
"eslint-config-next": "^13.2.4",
|
"prettier": "3.2.4",
|
||||||
"postcss": "^8.4.21",
|
"tailwindcss": "^3.4.1",
|
||||||
"prettier": "^2.8.6",
|
"typescript": "5.3.3"
|
||||||
"prettier-plugin-tailwindcss": "^0.2.6",
|
|
||||||
"prisma": "^4.11.0",
|
|
||||||
"tailwindcss": "^3.3.0",
|
|
||||||
"typescript": "^5.0.2"
|
|
||||||
},
|
},
|
||||||
"ct3aMetadata": {
|
"manifest": {
|
||||||
"initVersion": "7.10.3"
|
"host_permissions": [
|
||||||
|
"http://*/*",
|
||||||
|
"https://*/*"
|
||||||
|
],
|
||||||
|
"commands": {
|
||||||
|
"_execute_action": {
|
||||||
|
"suggested_key": {
|
||||||
|
"default": "Ctrl+Shift+L"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"execute_side_panel": {
|
||||||
|
"description": "Open the side panel",
|
||||||
|
"suggested_key": {
|
||||||
|
"default": "Ctrl+Shift+P"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"permissions": [
|
||||||
|
"storage",
|
||||||
|
"activeTab",
|
||||||
|
"scripting",
|
||||||
|
"declarativeNetRequest",
|
||||||
|
"action",
|
||||||
|
"unlimitedStorage",
|
||||||
|
"contextMenus"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
6869
pnpm-lock.yaml
generated
6869
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,8 +0,0 @@
|
|||||||
const config = {
|
|
||||||
plugins: {
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = config;
|
|
@ -1,6 +0,0 @@
|
|||||||
/** @type {import("prettier").Config} */
|
|
||||||
const config = {
|
|
||||||
plugins: [require.resolve("prettier-plugin-tailwindcss")],
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = config;
|
|
@ -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 |
BIN
public/logo.png
BIN
public/logo.png
Binary file not shown.
Before Width: | Height: | Size: 2.9 KiB |
5
py_server/.gitignore
vendored
5
py_server/.gitignore
vendored
@ -1,5 +0,0 @@
|
|||||||
dev.bat
|
|
||||||
*.pyc
|
|
||||||
*.pyo
|
|
||||||
.pytest_cache/
|
|
||||||
*/.env
|
|
@ -1,7 +0,0 @@
|
|||||||
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.11
|
|
||||||
|
|
||||||
COPY requirements.txt .
|
|
||||||
|
|
||||||
RUN pip install -r requirements.txt
|
|
||||||
|
|
||||||
COPY . .
|
|
@ -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
|
|
||||||
|
|
@ -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,
|
|
||||||
}
|
|
@ -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")
|
|
@ -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)
|
|
@ -1,2 +0,0 @@
|
|||||||
from .chat import ChatBody, ChatAppBody
|
|
||||||
from .user import UserValidation, SaveChatToApp
|
|
@ -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
|
|
@ -1,11 +0,0 @@
|
|||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
|
|
||||||
class UserValidation(BaseModel):
|
|
||||||
token: str
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class SaveChatToApp(BaseModel):
|
|
||||||
html: str
|
|
||||||
url: str
|
|
@ -1,14 +0,0 @@
|
|||||||
fastapi
|
|
||||||
uvicorn
|
|
||||||
pydantic
|
|
||||||
pandas
|
|
||||||
openai
|
|
||||||
beautifulsoup4
|
|
||||||
numpy
|
|
||||||
pydantic
|
|
||||||
langchain
|
|
||||||
lxml
|
|
||||||
faiss-cpu
|
|
||||||
supabase
|
|
||||||
tiktoken
|
|
||||||
chromadb
|
|
@ -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)
|
|
@ -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)
|
|
48
src/background.ts
Normal file
48
src/background.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
export {}
|
||||||
|
|
||||||
|
chrome.runtime.onMessage.addListener(async (message) => {
|
||||||
|
if (message.type === "sidepanel") {
|
||||||
|
chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => {
|
||||||
|
const tab = tabs[0]
|
||||||
|
await chrome.sidePanel.open({
|
||||||
|
tabId: tab.id
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
chrome.action.onClicked.addListener((tab) => {
|
||||||
|
chrome.tabs.create({ url: chrome.runtime.getURL("options.html") })
|
||||||
|
})
|
||||||
|
|
||||||
|
chrome.commands.onCommand.addListener((command) => {
|
||||||
|
switch (command) {
|
||||||
|
case "execute_side_panel":
|
||||||
|
chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => {
|
||||||
|
const tab = tabs[0]
|
||||||
|
await chrome.sidePanel.open({
|
||||||
|
tabId: tab.id
|
||||||
|
})
|
||||||
|
})
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
chrome.contextMenus.create({
|
||||||
|
id: "open-side-panel-pa",
|
||||||
|
title: "Open Side Panel to Chat",
|
||||||
|
contexts: ["all"]
|
||||||
|
})
|
||||||
|
|
||||||
|
chrome.contextMenus.onClicked.addListener((info, tab) => {
|
||||||
|
if (info.menuItemId === "open-side-panel-pa") {
|
||||||
|
chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => {
|
||||||
|
const tab = tabs[0]
|
||||||
|
await chrome.sidePanel.open({
|
||||||
|
tabId: tab.id
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
171
src/chain/chat-with-website.ts
Normal file
171
src/chain/chat-with-website.ts
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
import { BaseLanguageModel } from "langchain/base_language";
|
||||||
|
import { Document } from "@langchain/core/documents";
|
||||||
|
import {
|
||||||
|
ChatPromptTemplate,
|
||||||
|
MessagesPlaceholder,
|
||||||
|
PromptTemplate,
|
||||||
|
} from "langchain/prompts";
|
||||||
|
import { AIMessage, BaseMessage, HumanMessage } from "langchain/schema";
|
||||||
|
import { StringOutputParser } from "langchain/schema/output_parser";
|
||||||
|
import {
|
||||||
|
Runnable,
|
||||||
|
RunnableBranch,
|
||||||
|
RunnableLambda,
|
||||||
|
RunnableMap,
|
||||||
|
RunnableSequence,
|
||||||
|
} from "langchain/schema/runnable";
|
||||||
|
import type { ChatHistory } from "~store";
|
||||||
|
type RetrievalChainInput = {
|
||||||
|
chat_history: string;
|
||||||
|
question: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function groupMessagesByConversation(messages: ChatHistory) {
|
||||||
|
if (messages.length % 2 !== 0) {
|
||||||
|
messages.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupedMessages = [];
|
||||||
|
for (let i = 0; i < messages.length; i += 2) {
|
||||||
|
groupedMessages.push({
|
||||||
|
human: messages[i].content,
|
||||||
|
ai: messages[i + 1].content,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return groupedMessages;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatChatHistoryAsString = (history: BaseMessage[]) => {
|
||||||
|
return history
|
||||||
|
.map((message) => `${message._getType()}: ${message.content}`)
|
||||||
|
.join("\n");
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDocs = (docs: Document[]) => {
|
||||||
|
return docs
|
||||||
|
.map((doc, i) => `<doc id='${i}'>${doc.pageContent}</doc>`)
|
||||||
|
.join("\n");
|
||||||
|
};
|
||||||
|
|
||||||
|
const serializeHistory = (input: any) => {
|
||||||
|
const chatHistory = input.chat_history || [];
|
||||||
|
const convertedChatHistory = [];
|
||||||
|
for (const message of chatHistory) {
|
||||||
|
if (message.human !== undefined) {
|
||||||
|
convertedChatHistory.push(new HumanMessage({ content: message.human }));
|
||||||
|
}
|
||||||
|
if (message["ai"] !== undefined) {
|
||||||
|
convertedChatHistory.push(new AIMessage({ content: message.ai }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return convertedChatHistory;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createRetrieverChain = (
|
||||||
|
llm: BaseLanguageModel,
|
||||||
|
retriever: Runnable,
|
||||||
|
question_template: string
|
||||||
|
) => {
|
||||||
|
const CONDENSE_QUESTION_PROMPT =
|
||||||
|
PromptTemplate.fromTemplate(question_template);
|
||||||
|
const condenseQuestionChain = RunnableSequence.from([
|
||||||
|
CONDENSE_QUESTION_PROMPT,
|
||||||
|
llm,
|
||||||
|
new StringOutputParser(),
|
||||||
|
]).withConfig({
|
||||||
|
runName: "CondenseQuestion",
|
||||||
|
});
|
||||||
|
const hasHistoryCheckFn = RunnableLambda.from(
|
||||||
|
(input: RetrievalChainInput) => input.chat_history.length > 0
|
||||||
|
).withConfig({ runName: "HasChatHistoryCheck" });
|
||||||
|
const conversationChain = condenseQuestionChain.pipe(retriever).withConfig({
|
||||||
|
runName: "RetrievalChainWithHistory",
|
||||||
|
});
|
||||||
|
const basicRetrievalChain = RunnableLambda.from(
|
||||||
|
(input: RetrievalChainInput) => input.question
|
||||||
|
)
|
||||||
|
.withConfig({
|
||||||
|
runName: "Itemgetter:question",
|
||||||
|
})
|
||||||
|
.pipe(retriever)
|
||||||
|
.withConfig({ runName: "RetrievalChainWithNoHistory" });
|
||||||
|
|
||||||
|
return RunnableBranch.from([
|
||||||
|
[hasHistoryCheckFn, conversationChain],
|
||||||
|
basicRetrievalChain,
|
||||||
|
]).withConfig({
|
||||||
|
runName: "FindDocs",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createChatWithWebsiteChain = ({
|
||||||
|
llm,
|
||||||
|
question_template,
|
||||||
|
question_llm,
|
||||||
|
retriever,
|
||||||
|
response_template,
|
||||||
|
}: {
|
||||||
|
llm: BaseLanguageModel;
|
||||||
|
question_llm: BaseLanguageModel;
|
||||||
|
retriever: Runnable;
|
||||||
|
question_template: string;
|
||||||
|
response_template: string;
|
||||||
|
}) => {
|
||||||
|
const retrieverChain = createRetrieverChain(
|
||||||
|
question_llm,
|
||||||
|
retriever,
|
||||||
|
question_template
|
||||||
|
);
|
||||||
|
const context = RunnableMap.from({
|
||||||
|
context: RunnableSequence.from([
|
||||||
|
({ question, chat_history }) => {
|
||||||
|
return {
|
||||||
|
question: question,
|
||||||
|
chat_history: formatChatHistoryAsString(chat_history),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
retrieverChain,
|
||||||
|
RunnableLambda.from(formatDocs).withConfig({
|
||||||
|
runName: "FormatDocumentChunks",
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
question: RunnableLambda.from(
|
||||||
|
(input: RetrievalChainInput) => input.question
|
||||||
|
).withConfig({
|
||||||
|
runName: "Itemgetter:question",
|
||||||
|
}),
|
||||||
|
chat_history: RunnableLambda.from(
|
||||||
|
(input: RetrievalChainInput) => input.chat_history
|
||||||
|
).withConfig({
|
||||||
|
runName: "Itemgetter:chat_history",
|
||||||
|
}),
|
||||||
|
}).withConfig({ tags: ["RetrieveDocs"] });
|
||||||
|
const prompt = ChatPromptTemplate.fromMessages([
|
||||||
|
["system", response_template],
|
||||||
|
new MessagesPlaceholder("chat_history"),
|
||||||
|
["human", "{question}"],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const responseSynthesizerChain = RunnableSequence.from([
|
||||||
|
prompt,
|
||||||
|
llm,
|
||||||
|
new StringOutputParser(),
|
||||||
|
]).withConfig({
|
||||||
|
tags: ["GenerateResponse"],
|
||||||
|
});
|
||||||
|
return RunnableSequence.from([
|
||||||
|
{
|
||||||
|
question: RunnableLambda.from(
|
||||||
|
(input: RetrievalChainInput) => input.question
|
||||||
|
).withConfig({
|
||||||
|
runName: "Itemgetter:question",
|
||||||
|
}),
|
||||||
|
chat_history: RunnableLambda.from(serializeHistory).withConfig({
|
||||||
|
runName: "SerializeHistory",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
context,
|
||||||
|
responseSynthesizerChain,
|
||||||
|
]);
|
||||||
|
};
|
@ -1,276 +0,0 @@
|
|||||||
import { TrashIcon } from "@heroicons/react/24/outline";
|
|
||||||
import { useSupabaseClient } from "@supabase/auth-helpers-react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import React from "react";
|
|
||||||
import { api } from "~/utils/api";
|
|
||||||
import { iconUrl } from "~/utils/icon";
|
|
||||||
|
|
||||||
import { useForm } from "@mantine/form";
|
|
||||||
import axios from "axios";
|
|
||||||
import { useMutation } from "@tanstack/react-query";
|
|
||||||
|
|
||||||
type Message = {
|
|
||||||
isBot: boolean;
|
|
||||||
message: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type History = {
|
|
||||||
bot_response: string;
|
|
||||||
human_message: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
title: string | null;
|
|
||||||
id: string;
|
|
||||||
created_at: Date | null;
|
|
||||||
icon: string | null;
|
|
||||||
url: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CahtBox = (props: Props) => {
|
|
||||||
const supabase = useSupabaseClient();
|
|
||||||
|
|
||||||
const form = useForm({
|
|
||||||
initialValues: {
|
|
||||||
message: "",
|
|
||||||
isBot: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const sendToBot = async (message: string) => {
|
|
||||||
const { data } = await supabase.auth.getSession();
|
|
||||||
|
|
||||||
const response = await axios.post(
|
|
||||||
`${process.env.NEXT_PUBLIC_PAGEASSIST_URL}/api/v1/chat/app`,
|
|
||||||
{
|
|
||||||
user_message: message,
|
|
||||||
history: history,
|
|
||||||
url: props.url,
|
|
||||||
id: props.id,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
"X-Auth-Token": data.session?.access_token,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return response.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
const { mutateAsync: sendToBotAsync, isLoading: isSending } = useMutation(
|
|
||||||
sendToBot,
|
|
||||||
{
|
|
||||||
onSuccess: (data) => {
|
|
||||||
setMessages([...messages, { isBot: true, message: data.bot_response }]);
|
|
||||||
setHistory([...history, data]);
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
setMessages([
|
|
||||||
...messages,
|
|
||||||
{ isBot: true, message: "Something went wrong" },
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const [messages, setMessages] = React.useState<Message[]>([
|
|
||||||
{
|
|
||||||
isBot: true,
|
|
||||||
message: "Hi, I'm PageAssist Bot. How can I help you?",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
// const fetchSession = async () => {
|
|
||||||
|
|
||||||
// const {data}= await supabase.auth.getSession();
|
|
||||||
// data.session?.access_token
|
|
||||||
// }
|
|
||||||
|
|
||||||
const [history, setHistory] = React.useState<History[]>([]);
|
|
||||||
const divRef = React.useRef(null);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
//@ts-ignore
|
|
||||||
divRef.current.scrollIntoView({ behavior: "smooth" });
|
|
||||||
});
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const { mutateAsync: deleteChatByIdAsync, isLoading: isDeleting } =
|
|
||||||
api.chat.deleteChatById.useMutation({
|
|
||||||
onSuccess: () => {
|
|
||||||
router.push("/dashboard");
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col border bg-white">
|
|
||||||
{/* header */}
|
|
||||||
<div className="bg-grey-lighter flex flex-row items-center justify-between px-3 py-2">
|
|
||||||
<Link
|
|
||||||
target="_blank"
|
|
||||||
href={props.url ? props.url : "#"}
|
|
||||||
className="flex items-center"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<img
|
|
||||||
className="h-10 w-10 rounded-full"
|
|
||||||
//@ts-ignore
|
|
||||||
src={iconUrl(props?.icon, props?.url)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="ml-4">
|
|
||||||
<p className="text-grey-darkest">
|
|
||||||
{props?.title && props?.title?.length > 100
|
|
||||||
? props?.title?.slice(0, 100) + "..."
|
|
||||||
: props?.title}
|
|
||||||
</p>
|
|
||||||
<p className="mt-1 text-xs text-gray-400">
|
|
||||||
{props.url && new URL(props.url).hostname}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<div className="flex">
|
|
||||||
<button
|
|
||||||
onClick={async () => {
|
|
||||||
const isOk = confirm(
|
|
||||||
"Are you sure you want to delete this chat?"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isOk) {
|
|
||||||
await deleteChatByIdAsync({
|
|
||||||
id: props.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={isDeleting}
|
|
||||||
type="button"
|
|
||||||
className="inline-flex items-center rounded-full border border-transparent bg-red-600 p-1.5 text-white shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2"
|
|
||||||
>
|
|
||||||
{isDeleting ? (
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
className="h-5 w-5 animate-spin fill-white text-white dark:text-gray-600"
|
|
||||||
viewBox="0 0 100 101"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill="currentColor"
|
|
||||||
d="M100 50.59c0 27.615-22.386 50.001-50 50.001s-50-22.386-50-50 22.386-50 50-50 50 22.386 50 50zm-90.919 0c0 22.6 18.32 40.92 40.919 40.92 22.599 0 40.919-18.32 40.919-40.92 0-22.598-18.32-40.918-40.919-40.918-22.599 0-40.919 18.32-40.919 40.919z"
|
|
||||||
></path>
|
|
||||||
<path
|
|
||||||
fill="currentFill"
|
|
||||||
d="M93.968 39.04c2.425-.636 3.894-3.128 3.04-5.486A50 50 0 0041.735 1.279c-2.474.414-3.922 2.919-3.285 5.344.637 2.426 3.12 3.849 5.6 3.484a40.916 40.916 0 0144.131 25.769c.902 2.34 3.361 3.802 5.787 3.165z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
) : (
|
|
||||||
<TrashIcon className="h-5 w-5" aria-hidden="true" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* */}
|
|
||||||
<div
|
|
||||||
style={{ height: "calc(100vh - 260px)" }}
|
|
||||||
className="flex-grow overflow-auto"
|
|
||||||
>
|
|
||||||
<div className="px-3 py-2">
|
|
||||||
{messages.map((message, index) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className={
|
|
||||||
message.isBot
|
|
||||||
? "mt-2 flex w-full max-w-xs space-x-3"
|
|
||||||
: "ml-auto mt-2 flex w-full max-w-xs justify-end space-x-3"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
message.isBot
|
|
||||||
? "rounded-r-lg rounded-bl-lg bg-gray-300 p-3"
|
|
||||||
: "rounded-l-lg rounded-br-lg bg-blue-600 p-3 text-white"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<p className="text-sm">
|
|
||||||
{/* <ReactMarkdown>{message.message}</ReactMarkdown> */}
|
|
||||||
{message.message}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{isSending && (
|
|
||||||
<div className="mt-2 flex w-full max-w-xs space-x-3">
|
|
||||||
<div>
|
|
||||||
<div className="rounded-r-lg rounded-bl-lg bg-gray-300 p-3">
|
|
||||||
<p className="text-sm">Hold on, I'm looking...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div ref={divRef} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="items-center bg-gray-300 px-4 py-4">
|
|
||||||
<form
|
|
||||||
onSubmit={form.onSubmit(async (values) => {
|
|
||||||
setMessages([...messages, values]);
|
|
||||||
form.reset();
|
|
||||||
await sendToBotAsync(values.message);
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<div className="flex-grow space-y-6">
|
|
||||||
<div className="flex">
|
|
||||||
<span className="mr-3">
|
|
||||||
<button
|
|
||||||
disabled={isSending}
|
|
||||||
onClick={() => {
|
|
||||||
setHistory([]);
|
|
||||||
setMessages([
|
|
||||||
{
|
|
||||||
message: "Hi, I'm PageAssist. How can I help you?",
|
|
||||||
isBot: true,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}}
|
|
||||||
className="inline-flex h-10 items-center rounded-md border border-gray-700 bg-white px-3 text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
className="h-5 w-5 text-gray-600"
|
|
||||||
>
|
|
||||||
<path d="M18.37 2.63 14 7l-1.59-1.59a2 2 0 0 0-2.82 0L8 7l9 9 1.59-1.59a2 2 0 0 0 0-2.82L17 10l4.37-4.37a2.12 2.12 0 1 0-3-3Z"></path>
|
|
||||||
<path d="M9 8c-2 3-4 3.5-7 4l8 10c2-1 6-5 6-7"></path>
|
|
||||||
<path d="M14.5 17.5 4.5 15"></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
<div className="flex-grow">
|
|
||||||
<input
|
|
||||||
disabled={isSending}
|
|
||||||
className="flex h-10 w-full items-center rounded px-3 text-sm"
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
placeholder="Type your message…"
|
|
||||||
{...form.getInputProps("message")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,26 +0,0 @@
|
|||||||
import { useRouter } from "next/router";
|
|
||||||
import React from "react";
|
|
||||||
import { api } from "~/utils/api";
|
|
||||||
import { CahtBox } from "./ChatBox";
|
|
||||||
|
|
||||||
export const DashboardChatBody = () => {
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const { id } = router.query;
|
|
||||||
|
|
||||||
const { data: chat, status } = api.chat.getChatById.useQuery(
|
|
||||||
{ id: id as string },
|
|
||||||
{
|
|
||||||
onError: (err) => {
|
|
||||||
router.push("/dashboard");
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{status === "loading" && <div>Loading...</div>}
|
|
||||||
{status === "success" && <CahtBox {...chat} />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
97
src/components/Common/Markdown.tsx
Normal file
97
src/components/Common/Markdown.tsx
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
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 "property-information"
|
||||||
|
import { ClipboardIcon, CheckIcon } 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>
|
||||||
|
)
|
||||||
|
}
|
97
src/components/Common/Playground/Message.tsx
Normal file
97
src/components/Common/Playground/Message.tsx
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
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
|
||||||
|
name: string
|
||||||
|
images?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
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 `}>
|
||||||
|
<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)]">
|
||||||
|
{props.isBot && (
|
||||||
|
<span className="absolute mb-8 -top-4 left-0 text-xs text-gray-400 dark:text-gray-500">
|
||||||
|
{props.name}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<div className="flex flex-grow flex-col gap-3">
|
||||||
|
<Markdown message={props.message} />
|
||||||
|
</div>
|
||||||
|
{/* source if aviable */}
|
||||||
|
{props.images && (
|
||||||
|
<div className="flex md:max-w-2xl lg:max-w-xl xl:max-w-3xl mt-4 m-auto w-full">
|
||||||
|
{props.images
|
||||||
|
.filter((image) => image.length > 0)
|
||||||
|
.map((image, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="h-full rounded-md shadow relative">
|
||||||
|
<img
|
||||||
|
src={image}
|
||||||
|
alt="Uploaded"
|
||||||
|
className="h-full w-auto object-cover rounded-md min-w-[50px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</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-6 h-6 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-3 h-3 text-gray-400 group-hover:text-gray-500" />
|
||||||
|
) : (
|
||||||
|
<CheckIcon className="w-3 h-3 text-green-400 group-hover:text-green-500" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
33
src/components/Common/SaveButton.tsx
Normal file
33
src/components/Common/SaveButton.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { useState } from "react"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onClick: () => void
|
||||||
|
disabled?: boolean
|
||||||
|
className?: string
|
||||||
|
text?: string
|
||||||
|
textOnSave?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SaveButton = ({
|
||||||
|
onClick,
|
||||||
|
disabled,
|
||||||
|
className,
|
||||||
|
text = "Save",
|
||||||
|
textOnSave = "Saved"
|
||||||
|
}: Props) => {
|
||||||
|
const [clickedSave, setClickedSave] = useState(false)
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setClickedSave(true)
|
||||||
|
onClick()
|
||||||
|
setTimeout(() => {
|
||||||
|
setClickedSave(false)
|
||||||
|
}, 1000)
|
||||||
|
}}
|
||||||
|
disabled={disabled}
|
||||||
|
className={`bg-pink-500 text-r mt-4 hover:bg-pink-600 text-white px-4 py-2 rounded-md dark:bg-pink-600 dark:hover:bg-pink-700 ${className}`}>
|
||||||
|
{clickedSave ? textOnSave : text}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
@ -1,65 +0,0 @@
|
|||||||
import { useRouter } from "next/router";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
export default function Empty() {
|
|
||||||
const router = useRouter();
|
|
||||||
return (
|
|
||||||
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
|
||||||
<div className=" px-4 py-8 sm:px-10">
|
|
||||||
<div className="text-center">
|
|
||||||
<svg
|
|
||||||
className="mx-auto h-12 w-12 text-gray-400"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
vectorEffect="non-scaling-stroke"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M9 13h6m-3-3v6m-9 1V7a2 2 0 012-2h6l2 2h6a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<h3 className="mt-2 text-sm font-medium text-gray-900">
|
|
||||||
No Chats Yet
|
|
||||||
</h3>
|
|
||||||
<p className="mt-1 text-sm text-gray-500">
|
|
||||||
Get started by installing the Page Assist Chrome Extension.
|
|
||||||
</p>
|
|
||||||
<div className="mt-6">
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
router.push(
|
|
||||||
"https://chrome.google.com/webstore/detail/page-assist/ehkjdalbpmmaddcfdilplgknkgepeakd?hl=en&authuser=2"
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
type="button"
|
|
||||||
className="inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
className="-ml-1 mr-2 h-5 w-5"
|
|
||||||
aria-hidden="true"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth="2"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<circle cx="12" cy="12" r="10"></circle>
|
|
||||||
<circle cx="12" cy="12" r="4"></circle>
|
|
||||||
<path d="M21.17 8L12 8"></path>
|
|
||||||
<path d="M3.95 6.06L8.54 14"></path>
|
|
||||||
<path d="M10.88 21.94L15.46 14"></path>
|
|
||||||
</svg>
|
|
||||||
Install Extension
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
export default function Loading() {
|
|
||||||
return (
|
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
|
||||||
{/* create skelon loadinf */}
|
|
||||||
{[1, 2, 3, 4, 5, 6].map((item) => (
|
|
||||||
<div
|
|
||||||
className="flex h-35 cursor-pointer rounded-md shadow-sm transition-shadow duration-300 ease-in-out hover:shadow-lg"
|
|
||||||
key={item}
|
|
||||||
>
|
|
||||||
<div className="flex flex-1 items-center justify-between truncate rounded-md border border-gray-200 bg-white">
|
|
||||||
<div className="flex-1 truncate px-4 py-4 text-sm">
|
|
||||||
<h3 className="h-10 animate-pulse bg-gray-400 font-medium text-gray-900 hover:text-gray-600" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,61 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import Empty from "./Empty";
|
|
||||||
import Loading from "./Loading";
|
|
||||||
import { api } from "~/utils/api";
|
|
||||||
import { ChevronRightIcon } from "@heroicons/react/24/outline";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { iconUrl } from "~/utils/icon";
|
|
||||||
|
|
||||||
export default function DashboardBoby() {
|
|
||||||
const { data: savedSites, status } = api.chat.getSavedSitesForChat.useQuery();
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{status === "loading" && <Loading />}
|
|
||||||
{status === "success" && savedSites.data.length === 0 && <Empty />}
|
|
||||||
{status === "success" && savedSites.data.length > 0 && (
|
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
|
||||||
{savedSites.data.map((site, idx) => (
|
|
||||||
<Link
|
|
||||||
href={`/dashboard/chat/${site.id}`}
|
|
||||||
key={idx}
|
|
||||||
className="bg-panel-header-light border-panel-border-light hover:bg-panel-border-light hover:border-panel-border-hover-light h-30 group relative flex cursor-pointer flex-row rounded-md border px-6 py-4 text-left transition duration-150 ease-in-out hover:border-gray-300"
|
|
||||||
>
|
|
||||||
<div className="flex h-full w-full flex-col space-y-2 ">
|
|
||||||
<div className="text-scale-1200">
|
|
||||||
<div className="flex w-full flex-row justify-between gap-1">
|
|
||||||
<span
|
|
||||||
className={`flex-shrink ${
|
|
||||||
site?.title && site?.title?.length > 50
|
|
||||||
? "truncate"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{site.title}
|
|
||||||
</span>
|
|
||||||
<ChevronRightIcon className="h-10 w-10 text-gray-400 group-hover:text-gray-500" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bottom-0">
|
|
||||||
<div className="flex w-full flex-row gap-1">
|
|
||||||
<img
|
|
||||||
className="h-5 w-5 rounded-md"
|
|
||||||
// @ts-ignore
|
|
||||||
src={iconUrl(site.icon, site.url)}
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
<span className="text-scale-1000 ml-3 flex-shrink truncate text-xs text-gray-400">
|
|
||||||
{site.url && new URL(site.url).hostname}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,204 +0,0 @@
|
|||||||
import { Fragment } from "react";
|
|
||||||
import { Popover, Transition } from "@headlessui/react";
|
|
||||||
import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/outline";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useUser } from "@supabase/auth-helpers-react";
|
|
||||||
import YouTube from "react-youtube";
|
|
||||||
|
|
||||||
export default function Hero() {
|
|
||||||
const user = useUser();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="relative overflow-hidden">
|
|
||||||
<div className="relative pb-16 pt-6 sm:pb-24">
|
|
||||||
<Popover>
|
|
||||||
<div className="mx-auto max-w-7xl px-4 sm:px-6">
|
|
||||||
<nav
|
|
||||||
className="relative flex items-center justify-between sm:h-10 md:justify-center"
|
|
||||||
aria-label="Global"
|
|
||||||
>
|
|
||||||
<div className="flex flex-1 items-center md:absolute md:inset-y-0 md:left-0">
|
|
||||||
<div className="flex w-full items-center justify-between md:w-auto">
|
|
||||||
<Link href="/">
|
|
||||||
<span className="sr-only">Feedback Board</span>
|
|
||||||
<img
|
|
||||||
className="h-8 w-auto sm:h-10"
|
|
||||||
src="/logo.png"
|
|
||||||
alt="Feedback Board"
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
<div className="-mr-2 flex items-center md:hidden">
|
|
||||||
<Popover.Button className="inline-flex items-center justify-center rounded-md bg-gray-50 p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-sky-500">
|
|
||||||
<span className="sr-only">Open main menu</span>
|
|
||||||
<Bars3Icon className="h-6 w-6" aria-hidden="true" />
|
|
||||||
</Popover.Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="hidden md:absolute md:inset-y-0 md:right-0 md:flex md:items-center md:justify-end">
|
|
||||||
<span className="inline-flex rounded-md shadow">
|
|
||||||
{user ? (
|
|
||||||
<Link
|
|
||||||
href="/dashboard"
|
|
||||||
className="inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
|
||||||
>
|
|
||||||
Dashboard
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<Link
|
|
||||||
href="/auth"
|
|
||||||
className="inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
|
||||||
>
|
|
||||||
Login
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Transition
|
|
||||||
as={Fragment}
|
|
||||||
enter="duration-150 ease-out"
|
|
||||||
enterFrom="opacity-0 scale-95"
|
|
||||||
enterTo="opacity-100 scale-100"
|
|
||||||
leave="duration-100 ease-in"
|
|
||||||
leaveFrom="opacity-100 scale-100"
|
|
||||||
leaveTo="opacity-0 scale-95"
|
|
||||||
>
|
|
||||||
<Popover.Panel
|
|
||||||
focus
|
|
||||||
className="absolute inset-x-0 top-0 z-10 origin-top-right transform p-2 transition md:hidden"
|
|
||||||
>
|
|
||||||
<div className="overflow-hidden rounded-lg bg-white shadow-md ring-1 ring-black ring-opacity-5">
|
|
||||||
<div className="flex items-center justify-between px-5 pt-4">
|
|
||||||
<div>
|
|
||||||
<img
|
|
||||||
className="h-8 w-auto"
|
|
||||||
src="/logo.png"
|
|
||||||
alt="PageAssist"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="-mr-2">
|
|
||||||
<Popover.Button className="inline-flex items-center justify-center rounded-md bg-white p-2 text-gray-400 hover:bg-gray-100 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-sky-500">
|
|
||||||
<span className="sr-only">Close main menu</span>
|
|
||||||
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
|
|
||||||
</Popover.Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{user ? (
|
|
||||||
<Link
|
|
||||||
href="/dashboard"
|
|
||||||
className="m-3 inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
|
||||||
>
|
|
||||||
Account
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<Link
|
|
||||||
href="/auth"
|
|
||||||
className="m-3 inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
|
||||||
>
|
|
||||||
Login
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Popover.Panel>
|
|
||||||
</Transition>
|
|
||||||
</Popover>
|
|
||||||
<div className="py-24 sm:py-32 lg:pb-40">
|
|
||||||
<div className="mx-auto max-w-7xl px-6 lg:px-8">
|
|
||||||
<div className="mx-auto max-w-2xl text-center">
|
|
||||||
<h1 className="text-2xl font-bold tracking-tight text-gray-900 sm:text-5xl">
|
|
||||||
Chat with Any Webpage using PageAssist
|
|
||||||
</h1>
|
|
||||||
<p className="mt-6 text-lg leading-8 text-gray-600">
|
|
||||||
Revolutionize your browsing experience with PageAssist, an
|
|
||||||
open source Chrome extension that allows you to easily chat
|
|
||||||
with any webpage using the power of ChatGPT API.
|
|
||||||
</p>
|
|
||||||
<div className="mt-10 flex items-center justify-center gap-x-6">
|
|
||||||
<a
|
|
||||||
href="https://chrome.google.com/webstore/detail/page-assist/ehkjdalbpmmaddcfdilplgknkgepeakd?hl=en&authuser=2"
|
|
||||||
className="inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
className="-ml-0.5 mr-2 h-4 w-4"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth="2"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<circle cx="12" cy="12" r="10"></circle>
|
|
||||||
<circle cx="12" cy="12" r="4"></circle>
|
|
||||||
<path d="M21.17 8L12 8"></path>
|
|
||||||
<path d="M3.95 6.06L8.54 14"></path>
|
|
||||||
<path d="M10.88 21.94L15.46 14"></path>
|
|
||||||
</svg>
|
|
||||||
Install Now
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="https://github.com/n4ze3m/page-assist"
|
|
||||||
className="inline-flex items-center text-base font-semibold leading-7 text-gray-900 "
|
|
||||||
>
|
|
||||||
Star on GitHub
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
className="ml-2 h-4 w-4"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth="2"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path d="M15 22v-4a4.8 4.8 0 00-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 004 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4M9 18c-4.51 2-5-2-7-2"></path>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<iframe
|
|
||||||
className="yt-video relative mx-auto mt-12 w-full max-w-4xl rounded-3xl border border-gray-300 shadow-2xl dark:border-gray-700 lg:mt-20"
|
|
||||||
src="https://www.youtube.com/embed/UB1PdZ32vBc"
|
|
||||||
title="YouTube video player"
|
|
||||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
||||||
allowFullScreen
|
|
||||||
frameBorder={0}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="absolute inset-x-0 top-[calc(100%-13rem)] -z-10 transform-gpu overflow-hidden blur-3xl sm:top-[calc(100%-30rem)]">
|
|
||||||
<svg
|
|
||||||
className="relative left-[calc(50%+3rem)] h-[21.1875rem] max-w-none -translate-x-1/2 sm:left-[calc(50%+36rem)] sm:h-[42.375rem]"
|
|
||||||
viewBox="0 0 1155 678"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill="url(#b9e4a85f-ccd5-4151-8e84-ab55c66e5aa1)"
|
|
||||||
fillOpacity=".3"
|
|
||||||
d="M317.219 518.975L203.852 678 0 438.341l317.219 80.634 204.172-286.402c1.307 132.337 45.083 346.658 209.733 145.248C936.936 126.058 882.053-94.234 1031.02 41.331c119.18 108.451 130.68 295.337 121.53 375.223L855 299l21.173 362.054-558.954-142.079z"
|
|
||||||
/>
|
|
||||||
<defs>
|
|
||||||
<linearGradient
|
|
||||||
id="b9e4a85f-ccd5-4151-8e84-ab55c66e5aa1"
|
|
||||||
x1="1155.49"
|
|
||||||
x2="-78.208"
|
|
||||||
y1=".177"
|
|
||||||
y2="474.645"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
>
|
|
||||||
<stop stopColor="#9089FC" />
|
|
||||||
<stop offset={1} stopColor="#FF80B5" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,268 +0,0 @@
|
|||||||
import { Fragment, useState } from "react";
|
|
||||||
import { Dialog, Menu, Transition } from "@headlessui/react";
|
|
||||||
import {
|
|
||||||
Bars3CenterLeftIcon,
|
|
||||||
CogIcon,
|
|
||||||
HomeIcon,
|
|
||||||
XMarkIcon,
|
|
||||||
} from "@heroicons/react/24/outline";
|
|
||||||
import { ChevronDownIcon } from "@heroicons/react/20/solid";
|
|
||||||
import { useSupabaseClient, useUser } from "@supabase/auth-helpers-react";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
const navigation = [
|
|
||||||
{ name: "Home", href: "/dashboard", icon: HomeIcon, current: true },
|
|
||||||
{
|
|
||||||
name: "Settings",
|
|
||||||
href: "/dashboard/settings",
|
|
||||||
icon: CogIcon,
|
|
||||||
current: false,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
//@ts-ignore
|
|
||||||
function classNames(...classes) {
|
|
||||||
return classes.filter(Boolean).join(" ");
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function DashboardLayout({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
}) {
|
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
|
||||||
const user = useUser();
|
|
||||||
const router = useRouter();
|
|
||||||
const supabase = useSupabaseClient();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="min-h-full">
|
|
||||||
<Transition.Root show={sidebarOpen} as={Fragment}>
|
|
||||||
<Dialog
|
|
||||||
as="div"
|
|
||||||
className="relative z-40 lg:hidden"
|
|
||||||
onClose={setSidebarOpen}
|
|
||||||
>
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="transition-opacity ease-linear duration-300"
|
|
||||||
enterFrom="opacity-0"
|
|
||||||
enterTo="opacity-100"
|
|
||||||
leave="transition-opacity ease-linear duration-300"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
>
|
|
||||||
<div className="fixed inset-0 bg-gray-600 bg-opacity-75" />
|
|
||||||
</Transition.Child>
|
|
||||||
|
|
||||||
<div className="fixed inset-0 z-40 flex">
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="transition ease-in-out duration-300 transform"
|
|
||||||
enterFrom="-translate-x-full"
|
|
||||||
enterTo="translate-x-0"
|
|
||||||
leave="transition ease-in-out duration-300 transform"
|
|
||||||
leaveFrom="translate-x-0"
|
|
||||||
leaveTo="-translate-x-full"
|
|
||||||
>
|
|
||||||
<Dialog.Panel className="relative flex w-full max-w-xs flex-1 flex-col bg-white pb-4 pt-5">
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="ease-in-out duration-300"
|
|
||||||
enterFrom="opacity-0"
|
|
||||||
enterTo="opacity-100"
|
|
||||||
leave="ease-in-out duration-300"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
>
|
|
||||||
<div className="absolute right-0 top-0 -mr-12 pt-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="ml-1 flex h-10 w-10 items-center justify-center rounded-full focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white"
|
|
||||||
onClick={() => setSidebarOpen(false)}
|
|
||||||
>
|
|
||||||
<span className="sr-only">Close sidebar</span>
|
|
||||||
<XMarkIcon
|
|
||||||
className="h-6 w-6 text-white"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Transition.Child>
|
|
||||||
<div className="flex flex-shrink-0 items-center px-4">
|
|
||||||
<img
|
|
||||||
className="h-10 w-auto"
|
|
||||||
src="/logo.png"
|
|
||||||
alt="PageAssist"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<nav
|
|
||||||
className="mt-5 h-full flex-shrink-0 divide-y divide-gray-200 overflow-y-auto"
|
|
||||||
aria-label="Sidebar"
|
|
||||||
>
|
|
||||||
<div className="space-y-1 px-2">
|
|
||||||
{navigation.map((item) => (
|
|
||||||
<Link
|
|
||||||
key={item.name}
|
|
||||||
href={item.href}
|
|
||||||
className={classNames(
|
|
||||||
router.pathname === item.href
|
|
||||||
? "bg-gray-100 text-gray-900"
|
|
||||||
: "text-gray-600 hover:bg-gray-50 hover:text-gray-900",
|
|
||||||
"group flex items-center rounded-md px-2 py-2 text-sm font-medium"
|
|
||||||
)}
|
|
||||||
aria-current={
|
|
||||||
router.pathname === item.href ? "page" : undefined
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<item.icon
|
|
||||||
className={classNames(
|
|
||||||
router.pathname === item.href
|
|
||||||
? "text-gray-500"
|
|
||||||
: "text-gray-400 group-hover:text-gray-500",
|
|
||||||
"mr-3 h-6 w-6 flex-shrink-0"
|
|
||||||
)}
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
{item.name}
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</Dialog.Panel>
|
|
||||||
</Transition.Child>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
</Transition.Root>
|
|
||||||
<div className="hidden lg:fixed lg:inset-y-0 lg:flex lg:w-64 lg:flex-col">
|
|
||||||
<div className="flex flex-grow flex-col overflow-y-auto border-r border-gray-200 bg-white pb-4 pt-5">
|
|
||||||
<div className="flex flex-shrink-0 items-center px-4">
|
|
||||||
<img className="h-10 w-auto" src="/logo.png" alt="PageAssist" />
|
|
||||||
</div>
|
|
||||||
<nav
|
|
||||||
className="mt-5 flex flex-1 flex-col divide-y divide-gray-200 overflow-y-auto"
|
|
||||||
aria-label="Sidebar"
|
|
||||||
>
|
|
||||||
<div className="space-y-1 px-2">
|
|
||||||
{navigation.map((item) => (
|
|
||||||
<Link
|
|
||||||
key={item.name}
|
|
||||||
href={item.href}
|
|
||||||
className={classNames(
|
|
||||||
router.pathname === item.href
|
|
||||||
? "bg-gray-100 text-gray-900"
|
|
||||||
: "text-gray-600 hover:bg-gray-50 hover:text-gray-900",
|
|
||||||
"group flex items-center rounded-md px-2 py-2 text-sm font-medium"
|
|
||||||
)}
|
|
||||||
aria-current={
|
|
||||||
router.pathname === item.href ? "page" : undefined
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<item.icon
|
|
||||||
className={classNames(
|
|
||||||
router.pathname === item.href
|
|
||||||
? "text-gray-500"
|
|
||||||
: "text-gray-400 group-hover:text-gray-500",
|
|
||||||
"mr-3 h-6 w-6 flex-shrink-0"
|
|
||||||
)}
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
{item.name}
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-1 flex-col lg:pl-64">
|
|
||||||
<div className="flex h-16 flex-shrink-0 border-b border-gray-200 bg-white lg:border-none">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="border-r border-gray-200 px-4 text-gray-400 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-gray-200 lg:hidden"
|
|
||||||
onClick={() => setSidebarOpen(true)}
|
|
||||||
>
|
|
||||||
<span className="sr-only">Open sidebar</span>
|
|
||||||
<Bars3CenterLeftIcon className="h-6 w-6" aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
{/* Search bar */}
|
|
||||||
<div className="flex flex-1 justify-end px-4 sm:px-6 lg:mx-auto lg:max-w-6xl lg:px-8">
|
|
||||||
<div className="ml-4 flex items-center md:ml-6">
|
|
||||||
{/* Profile dropdown */}
|
|
||||||
<Menu as="div" className="relative ml-3">
|
|
||||||
<div>
|
|
||||||
<Menu.Button className="flex max-w-xs items-center rounded-full bg-white text-sm focus:outline-none focus:ring-2 focus:ring-gray-200 focus:ring-offset-2 lg:rounded-md lg:p-2 lg:hover:bg-gray-50">
|
|
||||||
<img
|
|
||||||
className="h-8 w-8 rounded-full"
|
|
||||||
src={`https://ui-avatars.com/api/?name=${user?.email}`}
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
<span className="ml-3 hidden text-sm font-medium text-gray-700 lg:block">
|
|
||||||
<span className="sr-only">Open user menu for </span>
|
|
||||||
{user?.email}
|
|
||||||
</span>
|
|
||||||
<ChevronDownIcon
|
|
||||||
className="ml-1 hidden h-5 w-5 flex-shrink-0 text-gray-400 lg:block"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</Menu.Button>
|
|
||||||
</div>
|
|
||||||
<Transition
|
|
||||||
as={Fragment}
|
|
||||||
enter="transition ease-out duration-100"
|
|
||||||
enterFrom="transform opacity-0 scale-95"
|
|
||||||
enterTo="transform opacity-100 scale-100"
|
|
||||||
leave="transition ease-in duration-75"
|
|
||||||
leaveFrom="transform opacity-100 scale-100"
|
|
||||||
leaveTo="transform opacity-0 scale-95"
|
|
||||||
>
|
|
||||||
<Menu.Items className="absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
|
||||||
<Menu.Item>
|
|
||||||
{({ active }) => (
|
|
||||||
<Link
|
|
||||||
href="/dashboard/settings"
|
|
||||||
className={classNames(
|
|
||||||
active ? "bg-gray-100" : "",
|
|
||||||
"block px-4 py-2 text-sm text-gray-700"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
Settings
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item>
|
|
||||||
{({ active }) => (
|
|
||||||
<div
|
|
||||||
onClick={async () => {
|
|
||||||
await supabase.auth.signOut();
|
|
||||||
router.push("/");
|
|
||||||
}}
|
|
||||||
className={classNames(
|
|
||||||
active ? "bg-gray-100" : "",
|
|
||||||
"block px-4 py-2 text-sm text-gray-700"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
Logout
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Menu.Item>
|
|
||||||
</Menu.Items>
|
|
||||||
</Transition>
|
|
||||||
</Menu>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<main className="flex-1 pb-8">
|
|
||||||
<div className="mt-8">
|
|
||||||
<div className="mx-auto max-w-6xl px-4 sm:px-6 lg:px-8">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
129
src/components/Option/Layout.tsx
Normal file
129
src/components/Option/Layout.tsx
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import React, { Fragment, useState } from "react"
|
||||||
|
import { Dialog, Menu, Transition } from "@headlessui/react"
|
||||||
|
import {
|
||||||
|
Bars3BottomLeftIcon,
|
||||||
|
XMarkIcon,
|
||||||
|
TagIcon,
|
||||||
|
CircleStackIcon,
|
||||||
|
CogIcon,
|
||||||
|
ChatBubbleLeftIcon,
|
||||||
|
Bars3Icon,
|
||||||
|
Bars4Icon,
|
||||||
|
ArrowPathIcon
|
||||||
|
} from "@heroicons/react/24/outline"
|
||||||
|
import logoImage from "data-base64:~assets/icon.png"
|
||||||
|
|
||||||
|
import { Link, useParams, useLocation, useNavigate } from "react-router-dom"
|
||||||
|
import { Sidebar } from "./Sidebar"
|
||||||
|
import { Drawer, Layout, Modal, Select } from "antd"
|
||||||
|
import { useQuery } from "@tanstack/react-query"
|
||||||
|
import { fetchModels } from "~services/ollama"
|
||||||
|
import { useMessageOption } from "~hooks/useMessageOption"
|
||||||
|
import { PanelLeftIcon, Settings2 } from "lucide-react"
|
||||||
|
import { Settings } from "./Settings"
|
||||||
|
import { useDarkMode } from "~hooks/useDarkmode"
|
||||||
|
|
||||||
|
const navigation = [
|
||||||
|
{ name: "Embed", href: "/bot/:id", icon: TagIcon },
|
||||||
|
{
|
||||||
|
name: "Preview",
|
||||||
|
href: "/bot/:id/preview",
|
||||||
|
icon: ChatBubbleLeftIcon
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Data Sources",
|
||||||
|
href: "/bot/:id/data-sources",
|
||||||
|
icon: CircleStackIcon
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Settings",
|
||||||
|
href: "/bot/:id/settings",
|
||||||
|
icon: CogIcon
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
//@ts-ignore -
|
||||||
|
function classNames(...classes) {
|
||||||
|
return classes.filter(Boolean).join(" ")
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function OptionLayout({
|
||||||
|
children
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
const [sidebarOpen, setSidebarOpen] = useState(false)
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: models,
|
||||||
|
isLoading: isModelsLoading,
|
||||||
|
refetch: refetchModels,
|
||||||
|
isFetching: isModelsFetching
|
||||||
|
} = useQuery({
|
||||||
|
queryKey: ["fetchModel"],
|
||||||
|
queryFn: fetchModels
|
||||||
|
})
|
||||||
|
|
||||||
|
const { selectedModel, setSelectedModel } = useMessageOption()
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout className="bg-white dark:bg-[#171717] md:flex">
|
||||||
|
<div className="flex items-center p-3 fixed flex-row justify-between border-b border-gray-200 dark:border-gray-600 bg-white dark:bg-[#171717] w-full z-10">
|
||||||
|
<div className="flex items-center flex-row gap-3">
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
className="text-gray-500 dark:text-gray-400"
|
||||||
|
onClick={() => setSidebarOpen(true)}>
|
||||||
|
<PanelLeftIcon className="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Select
|
||||||
|
value={selectedModel}
|
||||||
|
onChange={setSelectedModel}
|
||||||
|
size="large"
|
||||||
|
loading={isModelsLoading || isModelsFetching}
|
||||||
|
placeholder="Select a model"
|
||||||
|
className="w-64"
|
||||||
|
options={models?.map((model) => ({
|
||||||
|
label: model.name,
|
||||||
|
value: model.model
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button>
|
||||||
|
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
className="text-gray-500 dark:text-gray-400">
|
||||||
|
<CogIcon className="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Layout.Content>{children}</Layout.Content>
|
||||||
|
|
||||||
|
<Drawer
|
||||||
|
title={"Chat History"}
|
||||||
|
placement="left"
|
||||||
|
closeIcon={null}
|
||||||
|
onClose={() => setSidebarOpen(false)}
|
||||||
|
open={sidebarOpen}>
|
||||||
|
<Sidebar />
|
||||||
|
</Drawer>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
width={800}
|
||||||
|
title={"Settings"}
|
||||||
|
onOk={() => setOpen(false)}
|
||||||
|
footer={null}
|
||||||
|
onCancel={() => setOpen(false)}>
|
||||||
|
<Settings setClose={() => setOpen(false)} />
|
||||||
|
</Modal>
|
||||||
|
</Layout>
|
||||||
|
)
|
||||||
|
}
|
89
src/components/Option/Playground/Playground.tsx
Normal file
89
src/components/Option/Playground/Playground.tsx
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import React from "react"
|
||||||
|
import { PlaygroundForm } from "./PlaygroundForm"
|
||||||
|
import { PlaygroundChat } from "./PlaygroundChat"
|
||||||
|
|
||||||
|
export const Playground = () => {
|
||||||
|
const drop = React.useRef<HTMLDivElement>(null)
|
||||||
|
const [dropedFile, setDropedFile] = React.useState<File | undefined>()
|
||||||
|
const [dropState, setDropState] = React.useState<
|
||||||
|
"idle" | "dragging" | "error"
|
||||||
|
>("idle")
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!drop.current) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const handleDragOver = (e: DragEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDrop = (e: DragEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
setDropState("idle")
|
||||||
|
|
||||||
|
const files = Array.from(e.dataTransfer?.files || [])
|
||||||
|
|
||||||
|
const isImage = files.every((file) => file.type.startsWith("image/"))
|
||||||
|
|
||||||
|
if (!isImage) {
|
||||||
|
setDropState("error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const newFiles = Array.from(e.dataTransfer?.files || []).slice(0, 1)
|
||||||
|
if (newFiles.length > 0) {
|
||||||
|
setDropedFile(newFiles[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDragEnter = (e: DragEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
setDropState("dragging")
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDragLeave = (e: DragEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
setDropState("idle")
|
||||||
|
}
|
||||||
|
|
||||||
|
drop.current.addEventListener("dragover", handleDragOver)
|
||||||
|
drop.current.addEventListener("drop", handleDrop)
|
||||||
|
drop.current.addEventListener("dragenter", handleDragEnter)
|
||||||
|
drop.current.addEventListener("dragleave", handleDragLeave)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (drop.current) {
|
||||||
|
drop.current.removeEventListener("dragover", handleDragOver)
|
||||||
|
drop.current.removeEventListener("drop", handleDrop)
|
||||||
|
drop.current.removeEventListener("dragenter", handleDragEnter)
|
||||||
|
drop.current.removeEventListener("dragleave", handleDragLeave)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={drop}
|
||||||
|
className={`${
|
||||||
|
dropState === "dragging" ? "bg-gray-100 dark:bg-gray-800 z-10" : ""
|
||||||
|
} min-h-screen`}>
|
||||||
|
<PlaygroundChat />
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<div className="flex-grow">
|
||||||
|
<div className="w-full flex justify-center">
|
||||||
|
<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 justify-center items-center">
|
||||||
|
<div className="relative h-full flex-1 items-center justify-center md:flex-col">
|
||||||
|
<PlaygroundForm dropedFile={dropedFile} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
38
src/components/Option/Playground/PlaygroundChat.tsx
Normal file
38
src/components/Option/Playground/PlaygroundChat.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import React from "react"
|
||||||
|
import { useMessage } from "~hooks/useMessage"
|
||||||
|
import { useMessageOption } from "~hooks/useMessageOption"
|
||||||
|
import { PlaygroundMessage } from "./PlaygroundMessage"
|
||||||
|
import { PlaygroundEmpty } from "./PlaygroundEmpty"
|
||||||
|
|
||||||
|
export const PlaygroundChat = () => {
|
||||||
|
const { messages } = useMessageOption()
|
||||||
|
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 && (
|
||||||
|
<div className="mt-32">
|
||||||
|
<PlaygroundEmpty />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{messages.length > 0 && <div className="w-full h-14 flex-shrink-0"></div>}
|
||||||
|
{messages.map((message, index) => (
|
||||||
|
<PlaygroundMessage
|
||||||
|
key={index}
|
||||||
|
isBot={message.isBot}
|
||||||
|
message={message.message}
|
||||||
|
name={message.name}
|
||||||
|
images={message.images || []}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{messages.length > 0 && (
|
||||||
|
<div className="w-full h-32 md:h-48 flex-shrink-0"></div>
|
||||||
|
)}
|
||||||
|
<div ref={divRef} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
86
src/components/Option/Playground/PlaygroundEmpty.tsx
Normal file
86
src/components/Option/Playground/PlaygroundEmpty.tsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query"
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
import { useMessage } from "~hooks/useMessage"
|
||||||
|
import { useMessageOption } from "~hooks/useMessageOption"
|
||||||
|
import {
|
||||||
|
getOllamaURL,
|
||||||
|
isOllamaRunning,
|
||||||
|
setOllamaURL as saveOllamaURL
|
||||||
|
} from "~services/ollama"
|
||||||
|
|
||||||
|
export const PlaygroundEmpty = () => {
|
||||||
|
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()
|
||||||
|
|
||||||
|
return {
|
||||||
|
isOk,
|
||||||
|
ollamaURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (ollamaInfo?.ollamaURL) {
|
||||||
|
setOllamaURL(ollamaInfo.ollamaURL)
|
||||||
|
}
|
||||||
|
}, [ollamaInfo])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto sm:max-w-xl px-4 mt-10">
|
||||||
|
<div className="rounded-lg justify-center items-center flex flex-col border p-8 bg-white dark:bg-[#262626] shadow-sm dark:border-gray-600">
|
||||||
|
{(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">
|
||||||
|
Unable to connect to Ollama 🦙
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
className="bg-gray-100 dark:bg-[#262626] 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-pink-500 mt-4 hover:bg-pink-600 text-white px-4 py-2 rounded-md dark:bg-pink-600 dark:hover:bg-pink-700">
|
||||||
|
Retry
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
190
src/components/Option/Playground/PlaygroundForm.tsx
Normal file
190
src/components/Option/Playground/PlaygroundForm.tsx
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
import { useForm } from "@mantine/form"
|
||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query"
|
||||||
|
import React from "react"
|
||||||
|
import useDynamicTextareaSize from "~hooks/useDynamicTextareaSize"
|
||||||
|
import PhotoIcon from "@heroicons/react/24/outline/PhotoIcon"
|
||||||
|
import XMarkIcon from "@heroicons/react/24/outline/XMarkIcon"
|
||||||
|
import { toBase64 } from "~libs/to-base64"
|
||||||
|
import { useMessageOption } from "~hooks/useMessageOption"
|
||||||
|
import { ArrowPathIcon } from "@heroicons/react/24/outline"
|
||||||
|
import { Tooltip } from "antd"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
dropedFile: File | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PlaygroundForm = ({ dropedFile }: Props) => {
|
||||||
|
const textareaRef = React.useRef<HTMLTextAreaElement>(null)
|
||||||
|
const inputRef = React.useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
const resetHeight = () => {
|
||||||
|
const textarea = textareaRef.current
|
||||||
|
if (textarea) {
|
||||||
|
textarea.style.height = "auto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const form = useForm({
|
||||||
|
initialValues: {
|
||||||
|
message: "",
|
||||||
|
image: ""
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const onInputChange = async (
|
||||||
|
e: React.ChangeEvent<HTMLInputElement> | File
|
||||||
|
) => {
|
||||||
|
if (e instanceof File) {
|
||||||
|
const base64 = await toBase64(e)
|
||||||
|
form.setFieldValue("image", base64)
|
||||||
|
} else {
|
||||||
|
if (e.target.files) {
|
||||||
|
const base64 = await toBase64(e.target.files[0])
|
||||||
|
form.setFieldValue("image", base64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (dropedFile) {
|
||||||
|
onInputChange(dropedFile)
|
||||||
|
}
|
||||||
|
}, [dropedFile])
|
||||||
|
|
||||||
|
useDynamicTextareaSize(textareaRef, form.values.message, 300)
|
||||||
|
|
||||||
|
const { onSubmit, selectedModel, chatMode, clearChat } = useMessageOption()
|
||||||
|
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
const { mutateAsync: sendMessage, isPending: isSending } = useMutation({
|
||||||
|
mutationFn: onSubmit,
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["fetchChatHistory"]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-3 md:p-6 md:bg-white dark:bg-[#262626] border rounded-t-xl border-black/10 dark:border-gray-600">
|
||||||
|
<div className="flex-grow space-y-6 ">
|
||||||
|
<div
|
||||||
|
className={`h-full rounded-md shadow relative ${
|
||||||
|
form.values.image.length === 0 ? "hidden" : "block"
|
||||||
|
}`}>
|
||||||
|
<div>
|
||||||
|
<img
|
||||||
|
src={form.values.image}
|
||||||
|
alt="Uploaded"
|
||||||
|
className="h-full w-auto object-cover rounded-md min-w-[50px]"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
form.setFieldValue("image", "")
|
||||||
|
}}
|
||||||
|
className="absolute top-2 right-2 bg-white dark:bg-[#262626] p-1 rounded-full hover:bg-gray-100 dark:hover:bg-gray-600 text-black dark:text-gray-100">
|
||||||
|
<XMarkIcon className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
<Tooltip title="New Chat">
|
||||||
|
<button
|
||||||
|
onClick={clearChat}
|
||||||
|
className="text-gray-500 dark:text-gray-100 mr-3">
|
||||||
|
<ArrowPathIcon className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
<form
|
||||||
|
onSubmit={form.onSubmit(async (value) => {
|
||||||
|
if (!selectedModel || selectedModel.length === 0) {
|
||||||
|
form.setFieldError("message", "Please select a model")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
form.reset()
|
||||||
|
resetHeight()
|
||||||
|
await sendMessage({
|
||||||
|
image: value.image,
|
||||||
|
message: value.message.trim()
|
||||||
|
})
|
||||||
|
})}
|
||||||
|
className="shrink-0 flex-grow flex items-center ">
|
||||||
|
<div className="flex items-center p-2 rounded-2xl border bg-gray-100 w-full dark:bg-[#262626] dark:border-gray-600">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
inputRef.current?.click()
|
||||||
|
}}
|
||||||
|
className={`flex ml-3 items-center justify-center dark:text-gray-100 ${
|
||||||
|
chatMode === "rag" ? "hidden" : "block"
|
||||||
|
}`}>
|
||||||
|
<PhotoIcon className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
id="file-upload"
|
||||||
|
name="file-upload"
|
||||||
|
type="file"
|
||||||
|
className="sr-only"
|
||||||
|
ref={inputRef}
|
||||||
|
accept="image/*"
|
||||||
|
multiple={false}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
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({
|
||||||
|
image: value.image,
|
||||||
|
message: value.message.trim()
|
||||||
|
})
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
ref={textareaRef}
|
||||||
|
className="px-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="ml-2 flex items-center justify-center w-10 h-10 text-white bg-[#262626] 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>
|
||||||
|
)
|
||||||
|
}
|
98
src/components/Option/Playground/PlaygroundMessage.tsx
Normal file
98
src/components/Option/Playground/PlaygroundMessage.tsx
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
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
|
||||||
|
name: string
|
||||||
|
images?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
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 `}>
|
||||||
|
<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)]">
|
||||||
|
{props.isBot && (
|
||||||
|
<span className="absolute mb-8 -top-4 left-0 text-xs text-gray-400 dark:text-gray-500">
|
||||||
|
{props.name}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<div className="flex flex-grow flex-col gap-3">
|
||||||
|
<Markdown message={props.message} />
|
||||||
|
</div>
|
||||||
|
{/* source if aviable */}
|
||||||
|
{props.images && (
|
||||||
|
<div className="flex md:max-w-2xl lg:max-w-xl xl:max-w-3xl mt-4 m-auto w-full">
|
||||||
|
{props.images
|
||||||
|
.filter((image) => image.length > 0)
|
||||||
|
.map((image, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="h-full rounded-md shadow relative">
|
||||||
|
<img
|
||||||
|
src={image}
|
||||||
|
alt="Uploaded"
|
||||||
|
className="h-full w-auto object-cover rounded-md min-w-[50px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
</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-6 h-6 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-3 h-3 text-gray-400 group-hover:text-gray-500" />
|
||||||
|
) : (
|
||||||
|
<CheckIcon className="w-3 h-3 text-green-400 group-hover:text-green-500" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
25
src/components/Option/Playground/PlaygroundNewChat.tsx
Normal file
25
src/components/Option/Playground/PlaygroundNewChat.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { PencilSquareIcon } from "@heroicons/react/24/outline"
|
||||||
|
import { useMessage } from "../../../hooks/useMessage"
|
||||||
|
|
||||||
|
export const PlaygroundNewChat = () => {
|
||||||
|
const { setHistory, setMessages, setHistoryId } = useMessage()
|
||||||
|
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
setHistoryId(null)
|
||||||
|
setMessages([])
|
||||||
|
setHistory([])
|
||||||
|
// navigate(`/bot/${params.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={handleClick}
|
||||||
|
className="flex w-full border bg-transparent hover:bg-gray-200 dark:hover:bg-gray-800 text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 focus:ring-offset-gray-100 rounded-md p-2 dark:border-gray-800">
|
||||||
|
<PencilSquareIcon className="mx-3 h-5 w-5" aria-hidden="true" />
|
||||||
|
<span className="inline-flex font-semibol text-white text-sm">
|
||||||
|
New Chat
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
42
src/components/Option/Playground/PlaygroundSettings.tsx
Normal file
42
src/components/Option/Playground/PlaygroundSettings.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { Modal } from "antd"
|
||||||
|
import { useState } from "react"
|
||||||
|
|
||||||
|
export const PlaygroundSettings = () => {
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
return (
|
||||||
|
<div className="flex-shrink-0 flex flex-col items-center justify-center py-1 ">
|
||||||
|
<div className="flex items-center justify-center space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
className="flex items-center justify-center w-8 h-8 rounded-full transition-colors duration-200 focus:outline-none">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
stroke="currentColor"
|
||||||
|
className="w-5 h-5 text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200">
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
footer={null}
|
||||||
|
title="Playground Settings"
|
||||||
|
open={open}
|
||||||
|
onCancel={() => setOpen(false)}>
|
||||||
|
Nothing to see here
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
39
src/components/Option/Settings.tsx
Normal file
39
src/components/Option/Settings.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { Tabs } from "antd"
|
||||||
|
import { SettingsOllama } from "./Settings/ollama"
|
||||||
|
import { SettingPrompt } from "./Settings/prompt"
|
||||||
|
import { SettingOther } from "./Settings/other"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
setClose: (close: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Settings = ({ setClose }: Props) => {
|
||||||
|
return (
|
||||||
|
<div className="my-6">
|
||||||
|
<Tabs
|
||||||
|
tabPosition="left"
|
||||||
|
defaultActiveKey="1"
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
key: "1",
|
||||||
|
label: "Prompt",
|
||||||
|
children: <SettingPrompt />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
key: "2",
|
||||||
|
label: "Ollama",
|
||||||
|
children: <SettingsOllama />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
key: "3",
|
||||||
|
label: "Other",
|
||||||
|
children: <SettingOther />
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
55
src/components/Option/Settings/ollama.tsx
Normal file
55
src/components/Option/Settings/ollama.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query"
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
import { SaveButton } from "~components/Common/SaveButton"
|
||||||
|
import { getOllamaURL, setOllamaURL as saveOllamaURL } from "~services/ollama"
|
||||||
|
|
||||||
|
export const SettingsOllama = () => {
|
||||||
|
const [ollamaURL, setOllamaURL] = useState<string>("")
|
||||||
|
const { data: ollamaInfo } = useQuery({
|
||||||
|
queryKey: ["fetchOllamURL"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const ollamaURL = await getOllamaURL()
|
||||||
|
|
||||||
|
return {
|
||||||
|
ollamaURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (ollamaInfo?.ollamaURL) {
|
||||||
|
setOllamaURL(ollamaInfo.ollamaURL)
|
||||||
|
}
|
||||||
|
}, [ollamaInfo])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="ollamaURL"
|
||||||
|
className="text-sm font-medium dark:text-gray-200">
|
||||||
|
Ollama URL
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
id="ollamaURL"
|
||||||
|
value={ollamaURL}
|
||||||
|
onChange={(e) => {
|
||||||
|
setOllamaURL(e.target.value)
|
||||||
|
}}
|
||||||
|
placeholder="Your Ollama URL"
|
||||||
|
className="w-full p-2 border border-gray-300 rounded-md dark:bg-[#262626] dark:text-gray-100"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<SaveButton
|
||||||
|
onClick={() => {
|
||||||
|
saveOllamaURL(ollamaURL)
|
||||||
|
}}
|
||||||
|
className="mt-2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
52
src/components/Option/Settings/other.tsx
Normal file
52
src/components/Option/Settings/other.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { useQueryClient } from "@tanstack/react-query"
|
||||||
|
import { useDarkMode } from "~hooks/useDarkmode"
|
||||||
|
import { useMessageOption } from "~hooks/useMessageOption"
|
||||||
|
import { PageAssitDatabase } from "~libs/db"
|
||||||
|
|
||||||
|
export const SettingOther = () => {
|
||||||
|
const { clearChat } = useMessageOption()
|
||||||
|
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
const { mode, toggleDarkMode } = useDarkMode()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col space-y-4">
|
||||||
|
<div className="flex flex-row justify-between">
|
||||||
|
<span className="text-gray-500 dark:text-gray-400 text-lg">
|
||||||
|
Change Theme
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={toggleDarkMode}
|
||||||
|
className="bg-blue-500 dark:bg-blue-600 text-white dark:text-gray-200 px-4 py-2 rounded-md">
|
||||||
|
{mode === "dark" ? "Light" : "Dark"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row justify-between">
|
||||||
|
<span className="text-gray-500 dark:text-gray-400 text-lg">
|
||||||
|
Delete Chat History
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={async () => {
|
||||||
|
const confirm = window.confirm(
|
||||||
|
"Are you sure you want to delete your chat history? This action cannot be undone."
|
||||||
|
)
|
||||||
|
|
||||||
|
if (confirm) {
|
||||||
|
const db = new PageAssitDatabase()
|
||||||
|
await db.deleteChatHistory()
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["fetchChatHistory"]
|
||||||
|
})
|
||||||
|
clearChat()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="bg-red-500 dark:bg-red-600 text-white dark:text-gray-200 px-4 py-2 rounded-md">
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
56
src/components/Option/Settings/prompt.tsx
Normal file
56
src/components/Option/Settings/prompt.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query"
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
import { SaveButton } from "~components/Common/SaveButton"
|
||||||
|
import {
|
||||||
|
setSystemPromptForNonRagOption,
|
||||||
|
systemPromptForNonRagOption
|
||||||
|
} from "~services/ollama"
|
||||||
|
|
||||||
|
export const SettingPrompt = () => {
|
||||||
|
const [ollamaPrompt, setOllamaPrompt] = useState<string>("")
|
||||||
|
const { data: ollamaInfo } = useQuery({
|
||||||
|
queryKey: ["fetchOllaPrompt"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const prompt = await systemPromptForNonRagOption()
|
||||||
|
|
||||||
|
return {
|
||||||
|
prompt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (ollamaInfo?.prompt) {
|
||||||
|
setOllamaPrompt(ollamaInfo.prompt)
|
||||||
|
}
|
||||||
|
}, [ollamaInfo])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="ollamaPrompt" className="text-sm font-medium dark:text-gray-200">
|
||||||
|
System Prompt
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={ollamaPrompt}
|
||||||
|
rows={5}
|
||||||
|
id="ollamaPrompt"
|
||||||
|
placeholder="Your System Prompt"
|
||||||
|
onChange={(e) => {
|
||||||
|
setOllamaPrompt(e.target.value)
|
||||||
|
}}
|
||||||
|
className="w-full p-2 border border-gray-300 rounded-md dark:bg-[#262626] dark:text-gray-100"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<SaveButton
|
||||||
|
onClick={() => {
|
||||||
|
setSystemPromptForNonRagOption(ollamaPrompt)
|
||||||
|
}}
|
||||||
|
className="mt-2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
61
src/components/Option/Sidebar.tsx
Normal file
61
src/components/Option/Sidebar.tsx
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query"
|
||||||
|
import {
|
||||||
|
PageAssitDatabase,
|
||||||
|
formatToChatHistory,
|
||||||
|
formatToMessage
|
||||||
|
} from "~libs/db"
|
||||||
|
import { Empty, Skeleton } from "antd"
|
||||||
|
import { useMessageOption } from "~hooks/useMessageOption"
|
||||||
|
|
||||||
|
type Props = {}
|
||||||
|
|
||||||
|
export const Sidebar = ({}: Props) => {
|
||||||
|
const { setMessages, setHistory, setHistoryId } = useMessageOption()
|
||||||
|
|
||||||
|
const { data: chatHistories, status } = useQuery({
|
||||||
|
queryKey: ["fetchChatHistory"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const db = new PageAssitDatabase()
|
||||||
|
const history = await db.getChatHistories()
|
||||||
|
return history
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="overflow-y-auto">
|
||||||
|
{status === "success" && chatHistories.length === 0 && (
|
||||||
|
<div className="flex justify-center items-center mt-20 overflow-hidden">
|
||||||
|
<Empty description="No history yet" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{status === "pending" && (
|
||||||
|
<div className="flex justify-center items-center mt-5">
|
||||||
|
<Skeleton active paragraph={{ rows: 8 }} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{status === "error" && (
|
||||||
|
<div className="flex justify-center items-center">
|
||||||
|
<span className="text-red-500">Error loading history</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{status === "success" && chatHistories.length > 0 && (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{chatHistories.map((chat, index) => (
|
||||||
|
<button
|
||||||
|
onClick={async () => {
|
||||||
|
const db = new PageAssitDatabase()
|
||||||
|
const history = await db.getChatHistory(chat.id)
|
||||||
|
setHistoryId(chat.id)
|
||||||
|
setHistory(formatToChatHistory(history))
|
||||||
|
setMessages(formatToMessage(history))
|
||||||
|
}}
|
||||||
|
key={index}
|
||||||
|
className="flex text-start py-2 px-2 cursor-pointer items-start gap-3 relative rounded-md truncate hover:pr-4 group transition-opacity duration-300 ease-in-out bg-gray-100 dark:bg-[#232222] dark:text-gray-100 text-gray-800 border hover:bg-gray-200 dark:hover:bg-[#2d2d2d] dark:border-gray-800">
|
||||||
|
<span className="flex-grow truncate">{chat.title}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -1,61 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { ClipboardIcon } from "@heroicons/react/24/outline";
|
|
||||||
import { api } from "~/utils/api";
|
|
||||||
|
|
||||||
export default function SettingsBody() {
|
|
||||||
const { data, status } = api.settings.getAccessToken.useQuery();
|
|
||||||
const [isCopied, setIsCopied] = React.useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{status === "loading" && <div>Loading...</div>}
|
|
||||||
{status === "success" && (
|
|
||||||
<div className="divide-ylg:col-span-9">
|
|
||||||
<div className="px-4 py-6 sm:p-6 lg:pb-8">
|
|
||||||
<div>
|
|
||||||
<h2 className="text-lg font-medium leading-6 text-gray-900">
|
|
||||||
Chrom Extension
|
|
||||||
</h2>
|
|
||||||
<p className="mt-1 text-sm text-gray-500">
|
|
||||||
Copy the following code and paste it into the extension.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="mt-6 flex flex-col lg:flex-row">
|
|
||||||
<div className="flex-grow space-y-6">
|
|
||||||
<div className="flex">
|
|
||||||
<div className="flex-grow">
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
readOnly
|
|
||||||
defaultValue={data?.accessToken || ""}
|
|
||||||
className="block w-full rounded-md border-gray-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 sm:text-sm"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span className="ml-3">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setIsCopied(false);
|
|
||||||
navigator.clipboard.writeText(data?.accessToken || "");
|
|
||||||
setIsCopied(true);
|
|
||||||
}}
|
|
||||||
className="inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2"
|
|
||||||
>
|
|
||||||
<ClipboardIcon
|
|
||||||
className="h-5 w-5 text-gray-500"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
<span className="ml-2">
|
|
||||||
{isCopied ? "Copied" : "Copy"}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
30
src/components/Sidepanel/Chat/body.tsx
Normal file
30
src/components/Sidepanel/Chat/body.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import React from "react"
|
||||||
|
import { PlaygroundMessage } from "~components/Common/Playground/Message"
|
||||||
|
import { useMessage } from "~hooks/useMessage"
|
||||||
|
import { EmptySidePanel } from "../Chat/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}
|
||||||
|
name={message.name}
|
||||||
|
images={message.images || []}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<div className="w-full h-32 md:h-48 flex-shrink-0"></div>
|
||||||
|
<div ref={divRef} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
153
src/components/Sidepanel/Chat/empty.tsx
Normal file
153
src/components/Sidepanel/Chat/empty.tsx
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
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, chatMode, setChatMode } =
|
||||||
|
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 dark:border-gray-700 p-8 bg-white dark:bg-[#262626] 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-pink-500 mt-4 hover:bg-pink-600 text-white px-4 py-2 rounded-md dark:bg-pink-600 dark:hover:bg-pink-700">
|
||||||
|
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 truncate w-full dark:bg-[#171717] dark:text-gray-100 rounded-md px-4 py-2 mt-2">
|
||||||
|
<option key="0x" value={""}>
|
||||||
|
Select a model
|
||||||
|
</option>
|
||||||
|
{ollamaInfo.models.map((model, index) => (
|
||||||
|
<option key={index} value={model.name}>
|
||||||
|
{model.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<div className="mt-4">
|
||||||
|
<div className="inline-flex items-center">
|
||||||
|
<label
|
||||||
|
className="relative flex items-center p-3 rounded-full cursor-pointer"
|
||||||
|
htmlFor="check">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={chatMode === "rag"}
|
||||||
|
onChange={(e) => {
|
||||||
|
setChatMode(e.target.checked ? "rag" : "normal")
|
||||||
|
}}
|
||||||
|
className="before:content[''] peer relative h-5 w-5 cursor-pointer appearance-none rounded-md border border-blue-gray-200 transition-all before:absolute before:top-2/4 before:left-2/4 before:block before:h-12 before:w-12 before:-translate-y-2/4 before:-translate-x-2/4 before:rounded-full before:bg-blue-gray-500 before:opacity-0 before:transition-opacity"
|
||||||
|
id="check"
|
||||||
|
/>
|
||||||
|
<span className="absolute text-white transition-opacity opacity-0 pointer-events-none top-2/4 left-2/4 -translate-y-2/4 -translate-x-2/4 peer-checked:opacity-100 ">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="h-3.5 w-3.5"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1">
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||||
|
clip-rule="evenodd"></path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
className="mt-px font-light cursor-pointer select-none text-gray-900 dark:text-gray-400"
|
||||||
|
htmlFor="check">
|
||||||
|
Chat with Current Page
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
173
src/components/Sidepanel/Chat/form.tsx
Normal file
173
src/components/Sidepanel/Chat/form.tsx
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
import { useForm } from "@mantine/form"
|
||||||
|
import { useMutation } from "@tanstack/react-query"
|
||||||
|
import React from "react"
|
||||||
|
import useDynamicTextareaSize from "~hooks/useDynamicTextareaSize"
|
||||||
|
import { useMessage } from "~hooks/useMessage"
|
||||||
|
import PhotoIcon from "@heroicons/react/24/outline/PhotoIcon"
|
||||||
|
import XMarkIcon from "@heroicons/react/24/outline/XMarkIcon"
|
||||||
|
import { toBase64 } from "~libs/to-base64"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
dropedFile: File | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SidepanelForm = ({ dropedFile }: Props) => {
|
||||||
|
const textareaRef = React.useRef<HTMLTextAreaElement>(null)
|
||||||
|
const inputRef = React.useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
const resetHeight = () => {
|
||||||
|
const textarea = textareaRef.current
|
||||||
|
if (textarea) {
|
||||||
|
textarea.style.height = "auto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const form = useForm({
|
||||||
|
initialValues: {
|
||||||
|
message: "",
|
||||||
|
image: ""
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const onInputChange = async (
|
||||||
|
e: React.ChangeEvent<HTMLInputElement> | File
|
||||||
|
) => {
|
||||||
|
if (e instanceof File) {
|
||||||
|
const base64 = await toBase64(e)
|
||||||
|
form.setFieldValue("image", base64)
|
||||||
|
} else {
|
||||||
|
if (e.target.files) {
|
||||||
|
const base64 = await toBase64(e.target.files[0])
|
||||||
|
form.setFieldValue("image", base64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (dropedFile) {
|
||||||
|
onInputChange(dropedFile)
|
||||||
|
}
|
||||||
|
}, [dropedFile])
|
||||||
|
|
||||||
|
useDynamicTextareaSize(textareaRef, form.values.message, 120)
|
||||||
|
|
||||||
|
const { onSubmit, selectedModel, chatMode } = useMessage()
|
||||||
|
|
||||||
|
const { mutateAsync: sendMessage, isPending: isSending } = useMutation({
|
||||||
|
mutationFn: onSubmit
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-3 md:p-6 md:bg-white dark:bg-[#262626] border rounded-t-xl border-black/10 dark:border-gray-700">
|
||||||
|
<div className="flex-grow space-y-6 ">
|
||||||
|
{chatMode === "normal" && form.values.image && (
|
||||||
|
<div className="h-full rounded-md shadow relative">
|
||||||
|
<div>
|
||||||
|
<img
|
||||||
|
src={form.values.image}
|
||||||
|
alt="Uploaded"
|
||||||
|
className="h-full w-auto object-cover rounded-md min-w-[50px]"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
form.setFieldValue("image", "")
|
||||||
|
}}
|
||||||
|
className="absolute top-2 right-2 bg-white dark:bg-[#262626] p-1 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 text-black dark:text-gray-100">
|
||||||
|
<XMarkIcon className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<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({
|
||||||
|
image: value.image,
|
||||||
|
message: value.message.trim()
|
||||||
|
})
|
||||||
|
})}
|
||||||
|
className="shrink-0 flex-grow flex items-center ">
|
||||||
|
<div className="flex items-center p-2 rounded-2xl border bg-gray-100 w-full dark:bg-[#262626] dark:border-gray-700">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
inputRef.current?.click()
|
||||||
|
}}
|
||||||
|
className={`flex ml-3 items-center justify-center dark:text-gray-100 ${
|
||||||
|
chatMode === "rag" ? "hidden" : "block"
|
||||||
|
}`}>
|
||||||
|
<PhotoIcon className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
id="file-upload"
|
||||||
|
name="file-upload"
|
||||||
|
type="file"
|
||||||
|
className="sr-only"
|
||||||
|
ref={inputRef}
|
||||||
|
accept="image/*"
|
||||||
|
multiple={false}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
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({
|
||||||
|
image: value.image,
|
||||||
|
message: value.message.trim()
|
||||||
|
})
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
ref={textareaRef}
|
||||||
|
className="px-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="ml-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>
|
||||||
|
)
|
||||||
|
}
|
39
src/components/Sidepanel/Chat/header.tsx
Normal file
39
src/components/Sidepanel/Chat/header.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import logoImage from "data-base64:~assets/icon.png"
|
||||||
|
import CogIcon from "@heroicons/react/24/outline/CogIcon"
|
||||||
|
import Squares2X2Icon from "@heroicons/react/24/outline/Squares2X2Icon"
|
||||||
|
import { ArrowPathIcon } from "@heroicons/react/24/outline"
|
||||||
|
import { useMessage } from "~hooks/useMessage"
|
||||||
|
import { Link } from "react-router-dom"
|
||||||
|
import { Tooltip } from "antd"
|
||||||
|
export const SidepanelHeader = () => {
|
||||||
|
const { clearChat, isEmbedding } = useMessage()
|
||||||
|
return (
|
||||||
|
<div className="flex px-3 justify-between bg-white dark:bg-[#171717] border-b border-gray-300 dark:border-gray-700 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">
|
||||||
|
{isEmbedding ? (
|
||||||
|
<Tooltip
|
||||||
|
title="It may take a few minutes to embed the page. Please wait..."
|
||||||
|
>
|
||||||
|
<Squares2X2Icon className="h-5 w-5 text-gray-500 dark:text-gray-400 animate-bounce animate-infinite" />
|
||||||
|
|
||||||
|
</Tooltip>
|
||||||
|
) : null}
|
||||||
|
<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>
|
||||||
|
<Link to="/settings">
|
||||||
|
<CogIcon className="h-5 w-5 text-gray-500 dark:text-gray-400" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
172
src/components/Sidepanel/Settings/body.tsx
Normal file
172
src/components/Sidepanel/Settings/body.tsx
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query"
|
||||||
|
import React from "react"
|
||||||
|
import {
|
||||||
|
getOllamaURL,
|
||||||
|
systemPromptForNonRag,
|
||||||
|
promptForRag,
|
||||||
|
setOllamaURL as saveOllamaURL,
|
||||||
|
setPromptForRag,
|
||||||
|
setSystemPromptForNonRag
|
||||||
|
} from "~services/ollama"
|
||||||
|
|
||||||
|
import { Skeleton, Radio } from "antd"
|
||||||
|
import { useDarkMode } from "~hooks/useDarkmode"
|
||||||
|
import { SaveButton } from "~components/Common/SaveButton"
|
||||||
|
|
||||||
|
export const SettingsBody = () => {
|
||||||
|
const [ollamaURL, setOllamaURL] = React.useState<string>("")
|
||||||
|
const [systemPrompt, setSystemPrompt] = React.useState<string>("")
|
||||||
|
const [ragPrompt, setRagPrompt] = React.useState<string>("")
|
||||||
|
const [ragQuestionPrompt, setRagQuestionPrompt] = React.useState<string>("")
|
||||||
|
const [selectedValue, setSelectedValue] = React.useState<"normal" | "rag">(
|
||||||
|
"normal"
|
||||||
|
)
|
||||||
|
const { mode, toggleDarkMode } = useDarkMode()
|
||||||
|
|
||||||
|
const { data, status } = useQuery({
|
||||||
|
queryKey: ["sidebarSettings"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const [ollamaURL, systemPrompt, ragPrompt] = await Promise.all([
|
||||||
|
getOllamaURL(),
|
||||||
|
systemPromptForNonRag(),
|
||||||
|
promptForRag()
|
||||||
|
])
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: ollamaURL,
|
||||||
|
normalSystemPrompt: systemPrompt,
|
||||||
|
ragSystemPrompt: ragPrompt.ragPrompt,
|
||||||
|
ragQuestionPrompt: ragPrompt.ragQuestionPrompt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
setOllamaURL(data.url)
|
||||||
|
setSystemPrompt(data.normalSystemPrompt)
|
||||||
|
setRagPrompt(data.ragSystemPrompt)
|
||||||
|
setRagQuestionPrompt(data.ragQuestionPrompt)
|
||||||
|
}
|
||||||
|
}, [data])
|
||||||
|
|
||||||
|
if (status === "pending") {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4 p-4">
|
||||||
|
<Skeleton active />
|
||||||
|
<Skeleton active />
|
||||||
|
<Skeleton active />
|
||||||
|
<Skeleton active />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === "error") {
|
||||||
|
return <div>Error</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4 p-4 max-w-2xl mx-auto lg:max-w-3xl xl:max-w-4xl 2xl:max-w-5xl">
|
||||||
|
<div className="border border-gray-300 dark:border-gray-700 rounded p-4 bg-white dark:bg-[#171717]">
|
||||||
|
<h2 className="text-md mb-4 font-semibold dark:text-white">
|
||||||
|
Ollama URL
|
||||||
|
</h2>
|
||||||
|
<input
|
||||||
|
className="w-full border border-gray-300 dark:border-gray-700 rounded p-2 dark:bg-[#171717] dark:text-white dark:placeholder-gray-400"
|
||||||
|
value={ollamaURL}
|
||||||
|
type="url"
|
||||||
|
onChange={(e) => setOllamaURL(e.target.value)}
|
||||||
|
placeholder="Enter Ollama URL here"
|
||||||
|
/>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<SaveButton
|
||||||
|
onClick={() => {
|
||||||
|
saveOllamaURL(ollamaURL)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="border border-gray-300 dark:border-gray-700 rounded p-4 bg-white dark:bg-[#171717]">
|
||||||
|
<h2 className="text-md font-semibold dark:text-white">Prompt</h2>
|
||||||
|
<div className="my-3 flex justify-end">
|
||||||
|
<Radio.Group
|
||||||
|
defaultValue={selectedValue}
|
||||||
|
onChange={(e) => setSelectedValue(e.target.value)}>
|
||||||
|
<Radio.Button value="normal">Normal</Radio.Button>
|
||||||
|
<Radio.Button value="rag">Rag</Radio.Button>
|
||||||
|
</Radio.Group>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedValue === "normal" && (
|
||||||
|
<div>
|
||||||
|
<span className="text-md font-thin text-gray-500 dark:text-gray-400">
|
||||||
|
System Prompt
|
||||||
|
</span>
|
||||||
|
<textarea
|
||||||
|
className="w-full border border-gray-300 dark:border-gray-700 rounded p-2 dark:bg-[#171717] dark:text-white dark:placeholder-gray-400"
|
||||||
|
value={systemPrompt}
|
||||||
|
onChange={(e) => setSystemPrompt(e.target.value)}
|
||||||
|
/>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<SaveButton
|
||||||
|
onClick={() => {
|
||||||
|
setSystemPromptForNonRag(systemPrompt)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedValue === "rag" && (
|
||||||
|
<div>
|
||||||
|
<div className="mb-3">
|
||||||
|
<span className="text-md font-thin text-gray-500 dark:text-gray-400">
|
||||||
|
System Prompt
|
||||||
|
</span>
|
||||||
|
<textarea
|
||||||
|
className="w-full border border-gray-300 dark:border-gray-700 rounded p-2 dark:bg-[#171717] dark:text-white dark:placeholder-gray-400"
|
||||||
|
value={ragPrompt}
|
||||||
|
onChange={(e) => setRagPrompt(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mb-3">
|
||||||
|
<span className="text-md font-thin text-gray-500 dark:text-gray-400">
|
||||||
|
Question Prompt
|
||||||
|
</span>
|
||||||
|
<textarea
|
||||||
|
className="w-full border border-gray-300 dark:border-gray-700 rounded p-2 dark:bg-[#171717] dark:text-white dark:placeholder-gray-400"
|
||||||
|
value={ragQuestionPrompt}
|
||||||
|
onChange={(e) => setRagQuestionPrompt(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<SaveButton
|
||||||
|
onClick={() => {
|
||||||
|
setPromptForRag(ragPrompt, ragQuestionPrompt)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border border-gray-300 dark:border-gray-700 rounded p-4 bg-white dark:bg-[#171717]">
|
||||||
|
<h2 className="text-md mb-4 font-semibold dark:text-white">Theme</h2>
|
||||||
|
{mode === "dark" ? (
|
||||||
|
<button
|
||||||
|
onClick={toggleDarkMode}
|
||||||
|
className="select-none w-full rounded-lg border border-gray-900 py-3 px-6 text-center align-middle font-sans text-xs font-bold uppercase text-gray-900 transition-all hover:opacity-75 focus:ring focus:ring-gray-300 active:opacity-[0.85] disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none dark:border-gray-100 dark:text-white dark:hover:opacity-75 dark:focus:ring-dark dark:active:opacity-75 dark:disabled:pointer-events-none dark:disabled:opacity-50 dark:disabled:shadow-none">
|
||||||
|
Light Mode
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={toggleDarkMode}
|
||||||
|
className="select-none w-full rounded-lg border border-gray-900 py-3 px-6 text-center align-middle font-sans text-xs font-bold uppercase text-gray-900 transition-all hover:opacity-75 focus:ring focus:ring-gray-300 active:opacity-[0.85] disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none dark:border-gray-100 dark:text-white dark:hover:opacity-75 dark:focus:ring-dark dark:active:opacity-75 dark:disabled:pointer-events-none dark:disabled:opacity-50 dark:disabled:shadow-none">
|
||||||
|
Dark Mode
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
16
src/components/Sidepanel/Settings/header.tsx
Normal file
16
src/components/Sidepanel/Settings/header.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import logoImage from "data-base64:~assets/icon.png"
|
||||||
|
import { ChevronLeftIcon } from "@heroicons/react/24/outline"
|
||||||
|
import { Link } from "react-router-dom"
|
||||||
|
export const SidepanelSettingsHeader = () => {
|
||||||
|
return (
|
||||||
|
<div className="flex px-3 justify-start gap-3 bg-white dark:bg-[#171717] border-b border-gray-300 dark:border-gray-700 py-4 items-center">
|
||||||
|
<Link to="/">
|
||||||
|
<ChevronLeftIcon className="h-5 w-5 text-gray-500 dark:text-gray-400" />
|
||||||
|
</Link>
|
||||||
|
<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>
|
||||||
|
)
|
||||||
|
}
|
@ -2,7 +2,7 @@
|
|||||||
font-family: 'font';
|
font-family: 'font';
|
||||||
src: url('font.ttf') format('truetype');
|
src: url('font.ttf') format('truetype');
|
||||||
}
|
}
|
||||||
body {
|
* {
|
||||||
font-family: 'font' !important;
|
font-family: 'font' !important;
|
||||||
}
|
}
|
||||||
|
|
76
src/env.mjs
76
src/env.mjs
@ -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 };
|
|
67
src/hooks/useDarkmode.tsx
Normal file
67
src/hooks/useDarkmode.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { create } from "zustand";
|
||||||
|
|
||||||
|
type DarkModeState = {
|
||||||
|
mode: "system" | "dark" | "light";
|
||||||
|
setMode: (mode: "system" | "dark" | "light") => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDarkModeStore = create<DarkModeState>((set) => ({
|
||||||
|
mode: "system",
|
||||||
|
setMode: (mode) => set({ mode }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const useDarkMode = () => {
|
||||||
|
const { mode, setMode } = useDarkModeStore();
|
||||||
|
|
||||||
|
const getSystemTheme = () => {
|
||||||
|
const darkModeMediaQuery = window.matchMedia(
|
||||||
|
"(prefers-color-scheme: dark)"
|
||||||
|
);
|
||||||
|
const isDarkMode = darkModeMediaQuery.matches;
|
||||||
|
return isDarkMode ? "dark" : "light";
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDarkModeChange = (e: MediaQueryListEvent) => {
|
||||||
|
document.documentElement.classList.remove("dark", "light");
|
||||||
|
const mode = e.matches ? "dark" : "light";
|
||||||
|
document.documentElement.classList.add(mode);
|
||||||
|
setMode(mode);
|
||||||
|
};
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const theme = localStorage.getItem("theme") as "system" | "dark" | "light";
|
||||||
|
if (theme) {
|
||||||
|
if (theme !== "system") {
|
||||||
|
document.documentElement.classList.add(theme);
|
||||||
|
setMode(theme);
|
||||||
|
} else {
|
||||||
|
const systemTheme = getSystemTheme();
|
||||||
|
document.documentElement.classList.add(systemTheme);
|
||||||
|
setMode(systemTheme);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setMode(getSystemTheme());
|
||||||
|
localStorage.setItem("theme", getSystemTheme());
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const darkModeMediaQuery = window.matchMedia(
|
||||||
|
"(prefers-color-scheme: dark)"
|
||||||
|
);
|
||||||
|
darkModeMediaQuery.addEventListener("change", handleDarkModeChange);
|
||||||
|
return () =>
|
||||||
|
darkModeMediaQuery.removeEventListener("change", handleDarkModeChange);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggleDarkMode = () => {
|
||||||
|
const newMode = mode === "dark" ? "light" : "dark";
|
||||||
|
document.documentElement.classList.remove("dark", "light");
|
||||||
|
document.documentElement.classList.add(newMode);
|
||||||
|
setMode(newMode);
|
||||||
|
localStorage.setItem("theme", newMode);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { mode, toggleDarkMode };
|
||||||
|
};
|
38
src/hooks/useDynamicTextareaSize.tsx
Normal file
38
src/hooks/useDynamicTextareaSize.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
// copied from https://gist.github.com/KristofferEriksson/87ea5b8195339577151a236a9e9b46ff
|
||||||
|
/**
|
||||||
|
* Custom hook for dynamically resizing a textarea to fit its content.
|
||||||
|
* @param {React.RefObject<HTMLTextAreaElement>} textareaRef - Reference to the textarea element.
|
||||||
|
* @param {string} textContent - Current text content of the textarea.
|
||||||
|
* @param {number} maxHeight - Optional: maxHeight of the textarea in pixels.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useEffect } from "react";
|
||||||
|
const useDynamicTextareaSize = (
|
||||||
|
textareaRef: React.RefObject<HTMLTextAreaElement>,
|
||||||
|
textContent: string,
|
||||||
|
// optional maximum height after which textarea becomes scrollable
|
||||||
|
maxHeight?: number
|
||||||
|
): void => {
|
||||||
|
useEffect(() => {
|
||||||
|
const currentTextarea = textareaRef.current;
|
||||||
|
if (currentTextarea) {
|
||||||
|
// Temporarily collapse the textarea to calculate the required height
|
||||||
|
currentTextarea.style.height = "0px";
|
||||||
|
const contentHeight = currentTextarea.scrollHeight;
|
||||||
|
|
||||||
|
if (maxHeight) {
|
||||||
|
|
||||||
|
// Set max-height and adjust overflow behavior if maxHeight is provided
|
||||||
|
currentTextarea.style.maxHeight = `${maxHeight}px`;
|
||||||
|
currentTextarea.style.overflowY = contentHeight > maxHeight ? "scroll" : "hidden";
|
||||||
|
currentTextarea.style.height = `${Math.min(contentHeight, maxHeight)}px`;
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// Adjust height without max height constraint
|
||||||
|
currentTextarea.style.height = `${contentHeight}px`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [textareaRef, textContent, maxHeight]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useDynamicTextareaSize;
|
444
src/hooks/useMessage.tsx
Normal file
444
src/hooks/useMessage.tsx
Normal file
@ -0,0 +1,444 @@
|
|||||||
|
import React from "react"
|
||||||
|
import { cleanUrl } from "~libs/clean-url"
|
||||||
|
import {
|
||||||
|
getOllamaURL,
|
||||||
|
promptForRag,
|
||||||
|
systemPromptForNonRag
|
||||||
|
} from "~services/ollama"
|
||||||
|
import { useStoreMessage, type ChatHistory, type Message } from "~store"
|
||||||
|
import { ChatOllama } from "@langchain/community/chat_models/ollama"
|
||||||
|
import {
|
||||||
|
HumanMessage,
|
||||||
|
AIMessage,
|
||||||
|
type MessageContent,
|
||||||
|
SystemMessage
|
||||||
|
} from "@langchain/core/messages"
|
||||||
|
import { getHtmlOfCurrentTab } from "~libs/get-html"
|
||||||
|
import { PageAssistHtmlLoader } from "~loader/html"
|
||||||
|
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter"
|
||||||
|
import { OllamaEmbeddings } from "@langchain/community/embeddings/ollama"
|
||||||
|
import { createChatWithWebsiteChain, groupMessagesByConversation } from "~chain/chat-with-website"
|
||||||
|
import { MemoryVectorStore } from "langchain/vectorstores/memory"
|
||||||
|
|
||||||
|
export type BotResponse = {
|
||||||
|
bot: {
|
||||||
|
text: string
|
||||||
|
sourceDocuments: any[]
|
||||||
|
}
|
||||||
|
history: ChatHistory
|
||||||
|
history_id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const generateHistory = (
|
||||||
|
messages: {
|
||||||
|
role: "user" | "assistant" | "system"
|
||||||
|
content: string
|
||||||
|
image?: string
|
||||||
|
}[]
|
||||||
|
) => {
|
||||||
|
let history = []
|
||||||
|
for (const message of messages) {
|
||||||
|
if (message.role === "user") {
|
||||||
|
let content: MessageContent = [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: message.content
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
if (message.image) {
|
||||||
|
content = [
|
||||||
|
{
|
||||||
|
type: "image_url",
|
||||||
|
image_url: message.image
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: message.content
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
history.push(
|
||||||
|
new HumanMessage({
|
||||||
|
content: 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,
|
||||||
|
chatMode,
|
||||||
|
setChatMode,
|
||||||
|
setIsEmbedding,
|
||||||
|
isEmbedding
|
||||||
|
} = useStoreMessage()
|
||||||
|
|
||||||
|
const abortControllerRef = React.useRef<AbortController | null>(null)
|
||||||
|
|
||||||
|
const [keepTrackOfEmbedding, setKeepTrackOfEmbedding] = React.useState<{
|
||||||
|
[key: string]: MemoryVectorStore
|
||||||
|
}>({})
|
||||||
|
|
||||||
|
const clearChat = () => {
|
||||||
|
stopStreamingRequest()
|
||||||
|
setMessages([])
|
||||||
|
setHistory([])
|
||||||
|
setHistoryId(null)
|
||||||
|
setIsFirstMessage(true)
|
||||||
|
setIsLoading(false)
|
||||||
|
setIsProcessing(false)
|
||||||
|
setStreaming(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const memoryEmbedding = async (
|
||||||
|
url: string,
|
||||||
|
html: string,
|
||||||
|
ollamaEmbedding: OllamaEmbeddings
|
||||||
|
) => {
|
||||||
|
const loader = new PageAssistHtmlLoader({
|
||||||
|
html,
|
||||||
|
url
|
||||||
|
})
|
||||||
|
const docs = await loader.load()
|
||||||
|
const textSplitter = new RecursiveCharacterTextSplitter({
|
||||||
|
chunkSize: 1000,
|
||||||
|
chunkOverlap: 200
|
||||||
|
})
|
||||||
|
|
||||||
|
const chunks = await textSplitter.splitDocuments(docs)
|
||||||
|
|
||||||
|
const store = new MemoryVectorStore(ollamaEmbedding)
|
||||||
|
|
||||||
|
setIsEmbedding(true)
|
||||||
|
|
||||||
|
await store.addDocuments(chunks)
|
||||||
|
setKeepTrackOfEmbedding({
|
||||||
|
...keepTrackOfEmbedding,
|
||||||
|
[url]: store
|
||||||
|
})
|
||||||
|
setIsEmbedding(false)
|
||||||
|
|
||||||
|
return store
|
||||||
|
}
|
||||||
|
|
||||||
|
const chatWithWebsiteMode = async (message: string) => {
|
||||||
|
const ollamaUrl = await getOllamaURL()
|
||||||
|
const { html, url } = await getHtmlOfCurrentTab()
|
||||||
|
const isAlreadyExistEmbedding = keepTrackOfEmbedding[url]
|
||||||
|
let newMessage: Message[] = [
|
||||||
|
...messages,
|
||||||
|
{
|
||||||
|
isBot: false,
|
||||||
|
name: "You",
|
||||||
|
message,
|
||||||
|
sources: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isBot: true,
|
||||||
|
name: selectedModel,
|
||||||
|
message: "▋",
|
||||||
|
sources: []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const appendingIndex = newMessage.length - 1
|
||||||
|
setMessages(newMessage)
|
||||||
|
const ollamaEmbedding = new OllamaEmbeddings({
|
||||||
|
model: selectedModel,
|
||||||
|
baseUrl: cleanUrl(ollamaUrl)
|
||||||
|
})
|
||||||
|
|
||||||
|
const ollamaChat = new ChatOllama({
|
||||||
|
model: selectedModel,
|
||||||
|
baseUrl: cleanUrl(ollamaUrl)
|
||||||
|
})
|
||||||
|
|
||||||
|
let vectorstore: MemoryVectorStore
|
||||||
|
|
||||||
|
if (isAlreadyExistEmbedding) {
|
||||||
|
vectorstore = isAlreadyExistEmbedding
|
||||||
|
} else {
|
||||||
|
vectorstore = await memoryEmbedding(url, html, ollamaEmbedding)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { ragPrompt: systemPrompt, ragQuestionPrompt: questionPrompt } =
|
||||||
|
await promptForRag()
|
||||||
|
|
||||||
|
const sanitizedQuestion = message.trim().replaceAll("\n", " ")
|
||||||
|
|
||||||
|
const chain = createChatWithWebsiteChain({
|
||||||
|
llm: ollamaChat,
|
||||||
|
question_llm: ollamaChat,
|
||||||
|
question_template: questionPrompt,
|
||||||
|
response_template: systemPrompt,
|
||||||
|
retriever: vectorstore.asRetriever()
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
const chunks = await chain.stream({
|
||||||
|
question: sanitizedQuestion,
|
||||||
|
chat_history: groupMessagesByConversation(history),
|
||||||
|
})
|
||||||
|
let count = 0
|
||||||
|
for await (const chunk of chunks) {
|
||||||
|
if (count === 0) {
|
||||||
|
setIsProcessing(true)
|
||||||
|
newMessage[appendingIndex].message = chunk + "▋"
|
||||||
|
setMessages(newMessage)
|
||||||
|
} else {
|
||||||
|
newMessage[appendingIndex].message =
|
||||||
|
newMessage[appendingIndex].message.slice(0, -1) + chunk + "▋"
|
||||||
|
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,
|
||||||
|
name: selectedModel,
|
||||||
|
message: `Something went wrong. Check out the following logs:
|
||||||
|
~~~
|
||||||
|
${e?.message}
|
||||||
|
~~~
|
||||||
|
`,
|
||||||
|
sources: []
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalChatMode = async (message: string, image: string) => {
|
||||||
|
const url = await getOllamaURL()
|
||||||
|
|
||||||
|
if (image.length > 0) {
|
||||||
|
image = `data:image/jpeg;base64,${image.split(",")[1]}`
|
||||||
|
}
|
||||||
|
abortControllerRef.current = new AbortController()
|
||||||
|
|
||||||
|
const ollama = new ChatOllama({
|
||||||
|
model: selectedModel,
|
||||||
|
baseUrl: cleanUrl(url)
|
||||||
|
})
|
||||||
|
|
||||||
|
let newMessage: Message[] = [
|
||||||
|
...messages,
|
||||||
|
{
|
||||||
|
isBot: false,
|
||||||
|
name: "You",
|
||||||
|
message,
|
||||||
|
sources: [],
|
||||||
|
images: [image]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isBot: true,
|
||||||
|
name: selectedModel,
|
||||||
|
message: "▋",
|
||||||
|
sources: []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const appendingIndex = newMessage.length - 1
|
||||||
|
setMessages(newMessage)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const prompt = await systemPromptForNonRag()
|
||||||
|
|
||||||
|
message = message.trim().replaceAll("\n", " ")
|
||||||
|
|
||||||
|
let humanMessage = new HumanMessage({
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
text: message,
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
if (image.length > 0) {
|
||||||
|
humanMessage = new HumanMessage({
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
text: message,
|
||||||
|
type: "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image_url: image,
|
||||||
|
type: "image_url"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const applicationChatHistory = generateHistory(history)
|
||||||
|
|
||||||
|
if (prompt) {
|
||||||
|
applicationChatHistory.unshift(
|
||||||
|
new SystemMessage({
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
text: prompt,
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const chunks = await ollama.stream(
|
||||||
|
[...applicationChatHistory, humanMessage],
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
image
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: newMessage[appendingIndex].message
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
setIsProcessing(false)
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
setIsProcessing(false)
|
||||||
|
setStreaming(false)
|
||||||
|
|
||||||
|
setMessages([
|
||||||
|
...messages,
|
||||||
|
{
|
||||||
|
isBot: true,
|
||||||
|
name: selectedModel,
|
||||||
|
message: `Something went wrong. Check out the following logs:
|
||||||
|
\`\`\`
|
||||||
|
${e?.message}
|
||||||
|
\`\`\`
|
||||||
|
`,
|
||||||
|
sources: []
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSubmit = async ({
|
||||||
|
message,
|
||||||
|
image
|
||||||
|
}: {
|
||||||
|
message: string
|
||||||
|
image: string
|
||||||
|
}) => {
|
||||||
|
if (chatMode === "normal") {
|
||||||
|
await normalChatMode(message, image)
|
||||||
|
} else {
|
||||||
|
await chatWithWebsiteMode(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,
|
||||||
|
chatMode,
|
||||||
|
setChatMode,
|
||||||
|
isEmbedding
|
||||||
|
}
|
||||||
|
}
|
306
src/hooks/useMessageOption.tsx
Normal file
306
src/hooks/useMessageOption.tsx
Normal file
@ -0,0 +1,306 @@
|
|||||||
|
import React from "react"
|
||||||
|
import { cleanUrl } from "~libs/clean-url"
|
||||||
|
import { getOllamaURL, systemPromptForNonRagOption } from "~services/ollama"
|
||||||
|
import { type ChatHistory, type Message } from "~store/option"
|
||||||
|
import { ChatOllama } from "@langchain/community/chat_models/ollama"
|
||||||
|
import {
|
||||||
|
HumanMessage,
|
||||||
|
AIMessage,
|
||||||
|
type MessageContent,
|
||||||
|
SystemMessage
|
||||||
|
} from "@langchain/core/messages"
|
||||||
|
import { useStoreMessageOption } from "~store/option"
|
||||||
|
import { saveHistory, saveMessage } from "~libs/db"
|
||||||
|
|
||||||
|
export type BotResponse = {
|
||||||
|
bot: {
|
||||||
|
text: string
|
||||||
|
sourceDocuments: any[]
|
||||||
|
}
|
||||||
|
history: ChatHistory
|
||||||
|
history_id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const generateHistory = (
|
||||||
|
messages: {
|
||||||
|
role: "user" | "assistant" | "system"
|
||||||
|
content: string
|
||||||
|
image?: string
|
||||||
|
}[]
|
||||||
|
) => {
|
||||||
|
let history = []
|
||||||
|
for (const message of messages) {
|
||||||
|
if (message.role === "user") {
|
||||||
|
let content: MessageContent = [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: message.content
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
if (message.image) {
|
||||||
|
content = [
|
||||||
|
{
|
||||||
|
type: "image_url",
|
||||||
|
image_url: message.image
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: message.content
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
history.push(
|
||||||
|
new HumanMessage({
|
||||||
|
content: content
|
||||||
|
})
|
||||||
|
)
|
||||||
|
} else if (message.role === "assistant") {
|
||||||
|
history.push(
|
||||||
|
new AIMessage({
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: message.content
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return history
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useMessageOption = () => {
|
||||||
|
const {
|
||||||
|
history,
|
||||||
|
messages,
|
||||||
|
setHistory,
|
||||||
|
setMessages,
|
||||||
|
setStreaming,
|
||||||
|
streaming,
|
||||||
|
setIsFirstMessage,
|
||||||
|
historyId,
|
||||||
|
setHistoryId,
|
||||||
|
isLoading,
|
||||||
|
setIsLoading,
|
||||||
|
isProcessing,
|
||||||
|
setIsProcessing,
|
||||||
|
selectedModel,
|
||||||
|
setSelectedModel,
|
||||||
|
chatMode,
|
||||||
|
setChatMode
|
||||||
|
} = useStoreMessageOption()
|
||||||
|
|
||||||
|
const abortControllerRef = React.useRef<AbortController | null>(null)
|
||||||
|
|
||||||
|
const clearChat = () => {
|
||||||
|
// stopStreamingRequest()
|
||||||
|
setMessages([])
|
||||||
|
setHistory([])
|
||||||
|
setHistoryId(null)
|
||||||
|
setIsFirstMessage(true)
|
||||||
|
setIsLoading(false)
|
||||||
|
setIsProcessing(false)
|
||||||
|
setStreaming(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalChatMode = async (message: string, image: string) => {
|
||||||
|
const url = await getOllamaURL()
|
||||||
|
|
||||||
|
if (image.length > 0) {
|
||||||
|
image = `data:image/jpeg;base64,${image.split(",")[1]}`
|
||||||
|
}
|
||||||
|
abortControllerRef.current = new AbortController()
|
||||||
|
|
||||||
|
const ollama = new ChatOllama({
|
||||||
|
model: selectedModel,
|
||||||
|
baseUrl: cleanUrl(url)
|
||||||
|
})
|
||||||
|
|
||||||
|
let newMessage: Message[] = [
|
||||||
|
...messages,
|
||||||
|
{
|
||||||
|
isBot: false,
|
||||||
|
name: "You",
|
||||||
|
message,
|
||||||
|
sources: [],
|
||||||
|
images: [image]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isBot: true,
|
||||||
|
name: selectedModel,
|
||||||
|
message: "▋",
|
||||||
|
sources: []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const appendingIndex = newMessage.length - 1
|
||||||
|
setMessages(newMessage)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const prompt = await systemPromptForNonRagOption()
|
||||||
|
|
||||||
|
message = message.trim().replaceAll("\n", " ")
|
||||||
|
|
||||||
|
let humanMessage = new HumanMessage({
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
text: message,
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
if (image.length > 0) {
|
||||||
|
humanMessage = new HumanMessage({
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
text: message,
|
||||||
|
type: "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image_url: image,
|
||||||
|
type: "image_url"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const applicationChatHistory = generateHistory(history)
|
||||||
|
|
||||||
|
if (prompt) {
|
||||||
|
applicationChatHistory.unshift(
|
||||||
|
new SystemMessage({
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
text: prompt,
|
||||||
|
type: "text"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const chunks = await ollama.stream(
|
||||||
|
[...applicationChatHistory, humanMessage],
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
image
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: newMessage[appendingIndex].message
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
if (historyId) {
|
||||||
|
await saveMessage(historyId, selectedModel, "user", message, [image])
|
||||||
|
await saveMessage(
|
||||||
|
historyId,
|
||||||
|
selectedModel,
|
||||||
|
"assistant",
|
||||||
|
newMessage[appendingIndex].message,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
const newHistoryId = await saveHistory(message)
|
||||||
|
await saveMessage(newHistoryId.id, selectedModel, "user", message, [
|
||||||
|
image
|
||||||
|
])
|
||||||
|
await saveMessage(
|
||||||
|
newHistoryId.id,
|
||||||
|
selectedModel,
|
||||||
|
"assistant",
|
||||||
|
newMessage[appendingIndex].message,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
setHistoryId(newHistoryId.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsProcessing(false)
|
||||||
|
} catch (e) {
|
||||||
|
setIsProcessing(false)
|
||||||
|
setStreaming(false)
|
||||||
|
|
||||||
|
setMessages([
|
||||||
|
...messages,
|
||||||
|
{
|
||||||
|
isBot: true,
|
||||||
|
name: selectedModel,
|
||||||
|
message: `Something went wrong. Check out the following logs:
|
||||||
|
\`\`\`
|
||||||
|
${e?.message}
|
||||||
|
\`\`\`
|
||||||
|
`,
|
||||||
|
sources: []
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSubmit = async ({
|
||||||
|
message,
|
||||||
|
image
|
||||||
|
}: {
|
||||||
|
message: string
|
||||||
|
image: string
|
||||||
|
}) => {
|
||||||
|
await normalChatMode(message, image)
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
chatMode,
|
||||||
|
setChatMode
|
||||||
|
}
|
||||||
|
}
|
3
src/libs/class-name.tsx
Normal file
3
src/libs/class-name.tsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const classNames = (...classes: string[]) => {
|
||||||
|
return classes.filter(Boolean).join(" ")
|
||||||
|
}
|
7
src/libs/clean-url.ts
Normal file
7
src/libs/clean-url.ts
Normal 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
|
||||||
|
}
|
147
src/libs/db.ts
Normal file
147
src/libs/db.ts
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
import {
|
||||||
|
type ChatHistory as ChatHistoryType,
|
||||||
|
type Message as MessageType
|
||||||
|
} from "~store/option"
|
||||||
|
|
||||||
|
type HistoryInfo = {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
createdAt: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type Message = {
|
||||||
|
id: string
|
||||||
|
history_id: string
|
||||||
|
name: string
|
||||||
|
role: string
|
||||||
|
content: string
|
||||||
|
images?: string[]
|
||||||
|
sources?: string[]
|
||||||
|
createdAt: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type MessageHistory = Message[]
|
||||||
|
|
||||||
|
type ChatHistory = HistoryInfo[]
|
||||||
|
|
||||||
|
export class PageAssitDatabase {
|
||||||
|
db: chrome.storage.StorageArea
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.db = chrome.storage.local
|
||||||
|
}
|
||||||
|
|
||||||
|
async getChatHistory(id: string): Promise<MessageHistory> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.db.get(id, (result) => {
|
||||||
|
resolve(result[id] || [])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async getChatHistories(): Promise<ChatHistory> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.db.get("chatHistories", (result) => {
|
||||||
|
resolve(result.chatHistories || [])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async addChatHistory(history: HistoryInfo) {
|
||||||
|
const chatHistories = await this.getChatHistories()
|
||||||
|
const newChatHistories = [history, ...chatHistories]
|
||||||
|
this.db.set({ chatHistories: newChatHistories })
|
||||||
|
}
|
||||||
|
|
||||||
|
async addMessage(message: Message) {
|
||||||
|
const history_id = message.history_id
|
||||||
|
const chatHistory = await this.getChatHistory(history_id)
|
||||||
|
const newChatHistory = [message, ...chatHistory]
|
||||||
|
this.db.set({ [history_id]: newChatHistory })
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeChatHistory(id: string) {
|
||||||
|
const chatHistories = await this.getChatHistories()
|
||||||
|
const newChatHistories = chatHistories.filter(
|
||||||
|
(history) => history.id !== id
|
||||||
|
)
|
||||||
|
this.db.set({ chatHistories: newChatHistories })
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeMessage(history_id: string, message_id: string) {
|
||||||
|
const chatHistory = await this.getChatHistory(history_id)
|
||||||
|
const newChatHistory = chatHistory.filter(
|
||||||
|
(message) => message.id !== message_id
|
||||||
|
)
|
||||||
|
this.db.set({ [history_id]: newChatHistory })
|
||||||
|
}
|
||||||
|
|
||||||
|
async clear() {
|
||||||
|
this.db.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteChatHistory() {
|
||||||
|
const chatHistories = await this.getChatHistories()
|
||||||
|
for (const history of chatHistories) {
|
||||||
|
this.db.remove(history.id)
|
||||||
|
}
|
||||||
|
this.db.remove("chatHistories")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const generateID = () => {
|
||||||
|
return "pa_xxxx-xxxx-xxx-xxxx".replace(/[x]/g, () => {
|
||||||
|
const r = Math.floor(Math.random() * 16)
|
||||||
|
return r.toString(16)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const saveHistory = async (title: string) => {
|
||||||
|
const id = generateID()
|
||||||
|
const createdAt = Date.now()
|
||||||
|
const history = { id, title, createdAt }
|
||||||
|
const db = new PageAssitDatabase()
|
||||||
|
await db.addChatHistory(history)
|
||||||
|
return history
|
||||||
|
}
|
||||||
|
|
||||||
|
export const saveMessage = async (
|
||||||
|
history_id: string,
|
||||||
|
name: string,
|
||||||
|
role: string,
|
||||||
|
content: string,
|
||||||
|
images: string[]
|
||||||
|
) => {
|
||||||
|
const id = generateID()
|
||||||
|
const createdAt = Date.now()
|
||||||
|
const message = { id, history_id, name, role, content, images, createdAt }
|
||||||
|
const db = new PageAssitDatabase()
|
||||||
|
await db.addMessage(message)
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
|
export const formatToChatHistory = (
|
||||||
|
messages: MessageHistory
|
||||||
|
): ChatHistoryType => {
|
||||||
|
messages.sort((a, b) => a.createdAt - b.createdAt)
|
||||||
|
return messages.map((message) => {
|
||||||
|
return {
|
||||||
|
content: message.content,
|
||||||
|
role: message.role as "user" | "assistant" | "system",
|
||||||
|
images: message.images
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const formatToMessage = (messages: MessageHistory): MessageType[] => {
|
||||||
|
messages.sort((a, b) => a.createdAt - b.createdAt)
|
||||||
|
return messages.map((message) => {
|
||||||
|
return {
|
||||||
|
isBot: message.role === "assistant",
|
||||||
|
message: message.content,
|
||||||
|
name: message.name,
|
||||||
|
sources: message?.sources || [],
|
||||||
|
images: message.images || []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
31
src/libs/get-html.ts
Normal file
31
src/libs/get-html.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
const _getHtml = () => {
|
||||||
|
const url = window.location.href
|
||||||
|
const html = Array.from(document.querySelectorAll("script")).reduce(
|
||||||
|
(acc, script) => {
|
||||||
|
return acc.replace(script.outerHTML, "")
|
||||||
|
},
|
||||||
|
document.documentElement.outerHTML
|
||||||
|
)
|
||||||
|
return { url, html }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getHtmlOfCurrentTab = async () => {
|
||||||
|
const result = new Promise((resolve) => {
|
||||||
|
chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => {
|
||||||
|
const tab = tabs[0]
|
||||||
|
const data = await chrome.scripting.executeScript({
|
||||||
|
target: { tabId: tab.id },
|
||||||
|
func: _getHtml
|
||||||
|
})
|
||||||
|
|
||||||
|
if (data.length > 0) {
|
||||||
|
resolve(data[0].result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}) as Promise<{
|
||||||
|
url: string
|
||||||
|
html: string
|
||||||
|
}>
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
31
src/libs/runtime.ts
Normal file
31
src/libs/runtime.ts
Normal 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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
7
src/libs/to-base64.ts
Normal file
7
src/libs/to-base64.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export const toBase64 = (file: File) =>
|
||||||
|
new Promise<string>((resolve, reject) => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
reader.onload = () => resolve(reader.result as string)
|
||||||
|
reader.onerror = (error) => reject(error)
|
||||||
|
})
|
31
src/loader/html.ts
Normal file
31
src/loader/html.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { BaseDocumentLoader } from "langchain/document_loaders/base"
|
||||||
|
import { Document } from "@langchain/core/documents"
|
||||||
|
import { compile } from "html-to-text"
|
||||||
|
|
||||||
|
export interface WebLoaderParams {
|
||||||
|
html: string
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PageAssistHtmlLoader
|
||||||
|
extends BaseDocumentLoader
|
||||||
|
implements WebLoaderParams
|
||||||
|
{
|
||||||
|
html: string
|
||||||
|
url: string
|
||||||
|
|
||||||
|
constructor({ html, url }: WebLoaderParams) {
|
||||||
|
super()
|
||||||
|
this.html = html
|
||||||
|
this.url = url
|
||||||
|
}
|
||||||
|
|
||||||
|
async load(): Promise<Document<Record<string, any>>[]> {
|
||||||
|
const htmlCompiler = compile({
|
||||||
|
wordwrap: false
|
||||||
|
})
|
||||||
|
const text = htmlCompiler(this.html)
|
||||||
|
const metadata = { source: this.url }
|
||||||
|
return [new Document({ pageContent: text, metadata })]
|
||||||
|
}
|
||||||
|
}
|
31
src/options.tsx
Normal file
31
src/options.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
|
||||||
|
import { MemoryRouter } from "react-router-dom"
|
||||||
|
import { ToastContainer } from "react-toastify"
|
||||||
|
import "react-toastify/dist/ReactToastify.css"
|
||||||
|
const queryClient = new QueryClient()
|
||||||
|
import "./css/tailwind.css"
|
||||||
|
import { ConfigProvider, theme } from "antd"
|
||||||
|
import { StyleProvider } from "@ant-design/cssinjs"
|
||||||
|
import { useDarkMode } from "~hooks/useDarkmode"
|
||||||
|
import { OptionRouting } from "~routes"
|
||||||
|
function IndexOption() {
|
||||||
|
const { mode } = useDarkMode()
|
||||||
|
return (
|
||||||
|
<MemoryRouter>
|
||||||
|
<ConfigProvider
|
||||||
|
theme={{
|
||||||
|
algorithm:
|
||||||
|
mode === "dark" ? theme.darkAlgorithm : theme.defaultAlgorithm
|
||||||
|
}}>
|
||||||
|
<StyleProvider hashPriority="high">
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<OptionRouting />
|
||||||
|
<ToastContainer />
|
||||||
|
</QueryClientProvider>
|
||||||
|
</StyleProvider>
|
||||||
|
</ConfigProvider>
|
||||||
|
</MemoryRouter>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IndexOption
|
@ -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);
|
|
@ -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,
|
|
||||||
});
|
|
@ -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'})
|
|
||||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user