commit
912e6c8b0a
16
README.md
16
README.md
@ -85,9 +85,14 @@ This will start a development server and watch for changes in the source files.
|
|||||||
|
|
||||||
## Browser Support
|
## Browser Support
|
||||||
|
|
||||||
- Any Chromium-based browser that supports Chrome Extensions.
|
| Browser | Sidebar | Chat With Webpage | Web UI |
|
||||||
|
| -------- | ------- | ----------------- | ------ |
|
||||||
- Firefox support is planned for the future.
|
| Chrome | ✅ | ✅ | ✅ |
|
||||||
|
| Brave | ✅ | ✅ | ✅ |
|
||||||
|
| Edge | ✅ | ❌ | ✅ |
|
||||||
|
| Opera GX | ❌ | ❌ | ✅ |
|
||||||
|
| Arc | ❌ | ❌ | ✅ |
|
||||||
|
| Firefox | ❌ | ❌ | ❌ |
|
||||||
|
|
||||||
## Local AI Provider
|
## Local AI Provider
|
||||||
|
|
||||||
@ -105,11 +110,6 @@ This will start a development server and watch for changes in the source files.
|
|||||||
|
|
||||||
Contributions are welcome. If you have any feature requests, bug reports, or questions, feel free to create an issue.
|
Contributions are welcome. If you have any feature requests, bug reports, or questions, feel free to create an issue.
|
||||||
|
|
||||||
## 0.0.1
|
|
||||||
|
|
||||||
If you are looking for the v0.0.1 of this project, you can find it on v0.0.1 branch. I created it as a hackathon project and it is not maintained anymore.
|
|
||||||
|
|
||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
If you like the project and want to support it, you can buy me a coffee. It will help me to keep working on the project.
|
If you like the project and want to support it, you can buy me a coffee. It will help me to keep working on the project.
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "pageassist",
|
"name": "pageassist",
|
||||||
"displayName": "Page Assist - A Web UI for Local AI Models",
|
"displayName": "Page Assist - A Web UI for Local AI Models",
|
||||||
"version": "1.0.5",
|
"version": "1.0.8",
|
||||||
"description": "Use your locally running AI models to assist you in your web browsing.",
|
"description": "Use your locally running AI models to assist you in your web browsing.",
|
||||||
"author": "n4ze3m",
|
"author": "n4ze3m",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -26,7 +26,7 @@
|
|||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"html-to-text": "^9.0.5",
|
"html-to-text": "^9.0.5",
|
||||||
"langchain": "^0.1.9",
|
"langchain": "^0.1.9",
|
||||||
"lucide-react": "^0.340.0",
|
"lucide-react": "^0.350.0",
|
||||||
"plasmo": "0.84.1",
|
"plasmo": "0.84.1",
|
||||||
"property-information": "^6.4.1",
|
"property-information": "^6.4.1",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
@ -38,6 +38,7 @@
|
|||||||
"rehype-mathjax": "4.0.3",
|
"rehype-mathjax": "4.0.3",
|
||||||
"remark-gfm": "3.0.1",
|
"remark-gfm": "3.0.1",
|
||||||
"remark-math": "5.1.1",
|
"remark-math": "5.1.1",
|
||||||
|
"yt-transcript": "^0.0.2",
|
||||||
"zustand": "^4.5.0"
|
"zustand": "^4.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
37
page-share.md
Normal file
37
page-share.md
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# Page Share
|
||||||
|
|
||||||
|
Page Share allows you to share the chat publicly, similar to ChatGPT Share. You can self-host Page Share for privacy and security.
|
||||||
|
|
||||||
|
The default Page Share is hosted at [https://pageassist.xyz](https://pageassist.xyz).
|
||||||
|
|
||||||
|
## Self-Host
|
||||||
|
|
||||||
|
You can self-host Page Share using two methods:
|
||||||
|
|
||||||
|
- Railway
|
||||||
|
- Docker
|
||||||
|
|
||||||
|
### Railway
|
||||||
|
|
||||||
|
Click the button below to deploy the code to Railway.
|
||||||
|
|
||||||
|
[](https://railway.app/template/VbiS2Q?referralCode=olbszX)
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
1. Clone the repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/n4ze3m/page-assist-app.git
|
||||||
|
cd page-assist-app
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Run the server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Open the app
|
||||||
|
|
||||||
|
Navigate to [http://localhost:3000](http://localhost:3000) in your browser.
|
193
src/components/Common/ShareBtn.tsx
Normal file
193
src/components/Common/ShareBtn.tsx
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
import { Form, Image, Input, Modal, Tooltip, message } from "antd"
|
||||||
|
import { Share } from "lucide-react"
|
||||||
|
import { useState } from "react"
|
||||||
|
import type { Message } from "~store/option"
|
||||||
|
import Markdown from "./Markdown"
|
||||||
|
import React from "react"
|
||||||
|
import { useMutation } from "@tanstack/react-query"
|
||||||
|
import { getPageShareUrl } from "~services/ollama"
|
||||||
|
import { cleanUrl } from "~libs/clean-url"
|
||||||
|
import { getUserId, saveWebshare } from "~libs/db"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
messages: Message[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const reformatMessages = (messages: Message[], username: string) => {
|
||||||
|
return messages.map((message, idx) => {
|
||||||
|
return {
|
||||||
|
id: idx,
|
||||||
|
name: message.isBot ? message.name : username,
|
||||||
|
isBot: message.isBot,
|
||||||
|
message: message.message,
|
||||||
|
images: message.images
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PlaygroundMessage = (
|
||||||
|
props: Message & {
|
||||||
|
username: string
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
<div className="group w-full text-gray-800 dark:text-gray-100">
|
||||||
|
<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 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 ? (
|
||||||
|
<div className="absolute h-8 w-8 rounded-full bg-gradient-to-r from-green-300 to-purple-400"></div>
|
||||||
|
) : (
|
||||||
|
<div className="absolute h-8 w-8 rounded-full from-blue-400 to-blue-600 bg-gradient-to-r"></div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-[calc(100%-50px)] flex-col gap-3 lg:w-[calc(100%-115px)]">
|
||||||
|
<span className="text-xs font-bold text-gray-800 dark:text-white">
|
||||||
|
{props.isBot ? props.name : props.username}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div className="flex flex-grow flex-col">
|
||||||
|
<Markdown message={props.message} />
|
||||||
|
</div>
|
||||||
|
{/* source if aviable */}
|
||||||
|
{props.images && props.images.length > 0 && (
|
||||||
|
<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) => (
|
||||||
|
<Image
|
||||||
|
key={index}
|
||||||
|
src={image}
|
||||||
|
alt="Uploaded Image"
|
||||||
|
width={180}
|
||||||
|
className="rounded-md relative"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShareBtn: React.FC<Props> = ({ messages }) => {
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const [form] = Form.useForm()
|
||||||
|
const name = Form.useWatch("name", form)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (messages.length > 0) {
|
||||||
|
form.setFieldsValue({
|
||||||
|
title: messages[0].message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [messages])
|
||||||
|
|
||||||
|
const onSubmit = async (values: { title: string; name: string }) => {
|
||||||
|
const owner_id = await getUserId()
|
||||||
|
const chat = reformatMessages(messages, values.name)
|
||||||
|
const title = values.title
|
||||||
|
const url = await getPageShareUrl()
|
||||||
|
const res = await fetch(`${cleanUrl(url)}/api/v1/share/create`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
owner_id,
|
||||||
|
messages: chat,
|
||||||
|
title
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res.ok) throw new Error("Failed to create share link")
|
||||||
|
|
||||||
|
const data = await res.json()
|
||||||
|
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
url: `${cleanUrl(url)}/share/${data.chat_id}`,
|
||||||
|
api_url: cleanUrl(url),
|
||||||
|
share_id: data.chat_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { mutate: createShareLink, isPending } = useMutation({
|
||||||
|
mutationFn: onSubmit,
|
||||||
|
onSuccess: async (data) => {
|
||||||
|
const url = data.url
|
||||||
|
navigator.clipboard.writeText(url)
|
||||||
|
message.success("Link copied to clipboard")
|
||||||
|
await saveWebshare({ title: data.title, url, api_url: data.api_url, share_id: data.share_id })
|
||||||
|
setOpen(false)
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
message.error(error?.message || "Failed to create share link")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tooltip title="Share">
|
||||||
|
<button
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
className="!text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
|
||||||
|
<Share className="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title="Share link to Chat"
|
||||||
|
open={open}
|
||||||
|
footer={null}
|
||||||
|
width={600}
|
||||||
|
onCancel={() => setOpen(false)}>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
layout="vertical"
|
||||||
|
onFinish={createShareLink}
|
||||||
|
initialValues={{
|
||||||
|
title: "Untitled Chat",
|
||||||
|
name: "Anonymous"
|
||||||
|
}}>
|
||||||
|
<Form.Item
|
||||||
|
name="title"
|
||||||
|
label="Chat Title"
|
||||||
|
rules={[{ required: true, message: "Please enter chat title" }]}>
|
||||||
|
<Input size="large" placeholder="Enter chat title" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="name"
|
||||||
|
label="Your Name"
|
||||||
|
rules={[{ required: true, message: "Please enter your name" }]}>
|
||||||
|
<Input size="large" placeholder="Enter your name" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item>
|
||||||
|
<div className="max-h-[180px] overflow-x-auto border dark:border-gray-700 rounded-md p-2">
|
||||||
|
<div className="flex flex-col p-3">
|
||||||
|
{messages.map((message, index) => (
|
||||||
|
<PlaygroundMessage key={index} {...message} username={name} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="inline-flex items-center rounded-md border border-transparent bg-black px-2 py-2.5 text-md font-medium leading-4 text-white shadow-sm dark:bg-white dark:text-gray-800 disabled:opacity-50 ">
|
||||||
|
{isPending ? "Generating link..." : "Generate Link"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
@ -16,6 +16,7 @@ import {
|
|||||||
ZapIcon
|
ZapIcon
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { getAllPrompts } from "~libs/db"
|
import { getAllPrompts } from "~libs/db"
|
||||||
|
import { ShareBtn } from "~components/Common/ShareBtn"
|
||||||
|
|
||||||
export default function OptionLayout({
|
export default function OptionLayout({
|
||||||
children
|
children
|
||||||
@ -29,7 +30,9 @@ export default function OptionLayout({
|
|||||||
clearChat,
|
clearChat,
|
||||||
selectedSystemPrompt,
|
selectedSystemPrompt,
|
||||||
setSelectedQuickPrompt,
|
setSelectedQuickPrompt,
|
||||||
setSelectedSystemPrompt
|
setSelectedSystemPrompt,
|
||||||
|
messages,
|
||||||
|
streaming
|
||||||
} = useMessageOption()
|
} = useMessageOption()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -67,7 +70,7 @@ export default function OptionLayout({
|
|||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="sticky top-0 z-[999] flex h-16 p-3 bg-white border-b border-gray-200 dark:bg-[#171717] dark:border-gray-600">
|
<div className="sticky top-0 z-[999] flex h-16 p-3 bg-white border-b dark:bg-[#171717] dark:border-gray-600">
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
{pathname !== "/" && (
|
{pathname !== "/" && (
|
||||||
<div>
|
<div>
|
||||||
@ -88,7 +91,7 @@ export default function OptionLayout({
|
|||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
onClick={clearChat}
|
onClick={clearChat}
|
||||||
className="inline-flex items-center rounded-lg border dark:border-gray-700 bg-transparent px-3 py-3 text-sm font-medium leading-4 text-gray-800 shadow-sm dark:text-white disabled:opacity-50 ">
|
className="inline-flex items-center rounded-lg border dark:border-gray-700 bg-transparent px-3 py-3 text-sm font-medium leading-4 text-gray-800 dark:text-white disabled:opacity-50 ">
|
||||||
<SquarePen className="h-4 w-4 mr-3" />
|
<SquarePen className="h-4 w-4 mr-3" />
|
||||||
New Chat
|
New Chat
|
||||||
</button>
|
</button>
|
||||||
@ -155,13 +158,9 @@ export default function OptionLayout({
|
|||||||
<div className="flex flex-1 justify-end px-4">
|
<div className="flex flex-1 justify-end px-4">
|
||||||
<div className="ml-4 flex items-center md:ml-6">
|
<div className="ml-4 flex items-center md:ml-6">
|
||||||
<div className="flex gap-4 items-center">
|
<div className="flex gap-4 items-center">
|
||||||
{/* <Tooltip title="Manage Prompts">
|
{pathname === "/" && messages.length > 0 && !streaming && (
|
||||||
<NavLink
|
<ShareBtn messages={messages} />
|
||||||
to="/prompts"
|
)}
|
||||||
className="!text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
|
|
||||||
<Book className="w-6 h-6" />
|
|
||||||
</NavLink>
|
|
||||||
</Tooltip> */}
|
|
||||||
<Tooltip title="Github Repository">
|
<Tooltip title="Github Repository">
|
||||||
<a
|
<a
|
||||||
href="https://github.com/n4ze3m/page-assist"
|
href="https://github.com/n4ze3m/page-assist"
|
||||||
@ -170,13 +169,6 @@ export default function OptionLayout({
|
|||||||
<GithubIcon className="w-6 h-6" />
|
<GithubIcon className="w-6 h-6" />
|
||||||
</a>
|
</a>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{/* <Tooltip title="Manage Ollama Models">
|
|
||||||
<NavLink
|
|
||||||
to="/models"
|
|
||||||
className="!text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
|
|
||||||
<BrainCircuit className="w-6 h-6" />
|
|
||||||
</NavLink>
|
|
||||||
</Tooltip> */}
|
|
||||||
<Tooltip title="Manage Ollama Models">
|
<Tooltip title="Manage Ollama Models">
|
||||||
<NavLink
|
<NavLink
|
||||||
to="/settings"
|
to="/settings"
|
||||||
@ -198,7 +190,9 @@ export default function OptionLayout({
|
|||||||
closeIcon={null}
|
closeIcon={null}
|
||||||
onClose={() => setSidebarOpen(false)}
|
onClose={() => setSidebarOpen(false)}
|
||||||
open={sidebarOpen}>
|
open={sidebarOpen}>
|
||||||
<Sidebar />
|
<Sidebar
|
||||||
|
onClose={() => setSidebarOpen(false)}
|
||||||
|
/>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -2,7 +2,8 @@ import {
|
|||||||
Book,
|
Book,
|
||||||
BrainCircuit,
|
BrainCircuit,
|
||||||
CircuitBoardIcon,
|
CircuitBoardIcon,
|
||||||
Orbit
|
Orbit,
|
||||||
|
Share
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { Link, useLocation } from "react-router-dom"
|
import { Link, useLocation } from "react-router-dom"
|
||||||
|
|
||||||
@ -22,15 +23,15 @@ const LinkComponent = (item: {
|
|||||||
to={item.href}
|
to={item.href}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
item.current === item.href
|
item.current === item.href
|
||||||
? "bg-gray-100 text-indigo-600 dark:bg-[#262626] dark:text-white"
|
? "bg-gray-100 text-gray-600 dark:bg-[#262626] dark:text-white"
|
||||||
: "text-gray-700 hover:text-indigo-600 hover:bg-gray-100 dark:text-gray-200 dark:hover:text-white dark:hover:bg-[#262626]",
|
: "text-gray-700 hover:text-gray-600 hover:bg-gray-100 dark:text-gray-200 dark:hover:text-white dark:hover:bg-[#262626]",
|
||||||
"group flex gap-x-3 rounded-md py-2 pl-2 pr-3 text-sm leading-6 font-semibold"
|
"group flex gap-x-3 rounded-md py-2 pl-2 pr-3 text-sm leading-6 font-semibold"
|
||||||
)}>
|
)}>
|
||||||
<item.icon
|
<item.icon
|
||||||
className={classNames(
|
className={classNames(
|
||||||
item.current === item.href
|
item.current === item.href
|
||||||
? "text-indigo-600 dark:text-white"
|
? "text-gray-600 dark:text-white"
|
||||||
: "text-gray-400 group-hover:text-indigo-600 dark:text-gray-200 dark:group-hover:text-white",
|
: "text-gray-400 group-hover:text-gray-600 dark:text-gray-200 dark:group-hover:text-white",
|
||||||
"h-6 w-6 shrink-0"
|
"h-6 w-6 shrink-0"
|
||||||
)}
|
)}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
@ -46,7 +47,7 @@ export const SettingsLayout = ({ children }: { children: React.ReactNode }) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="mx-auto max-w-7xl lg:flex lg:gap-x-16 lg:px-8">
|
<div className="mx-auto max-w-7xl lg:flex lg:gap-x-16 lg:px-8">
|
||||||
<aside className="flex lg:rounded-md bg-white lg:h-56 lg:p-4 lg:mt-20 overflow-x-auto lg:border border-b py-4 lg:block lg:w-64 lg:flex-none dark:bg-[#171717] dark:border-gray-600">
|
<aside className="flex lg:rounded-md bg-white lg:p-4 lg:mt-20 overflow-x-auto lg:border-0 border-b py-4 lg:block lg:w-64 lg:flex-none dark:bg-[#171717] dark:border-gray-600">
|
||||||
<nav className="flex-none px-4 sm:px-6 lg:px-0">
|
<nav className="flex-none px-4 sm:px-6 lg:px-0">
|
||||||
<ul
|
<ul
|
||||||
role="list"
|
role="list"
|
||||||
@ -75,6 +76,12 @@ export const SettingsLayout = ({ children }: { children: React.ReactNode }) => {
|
|||||||
icon={Book}
|
icon={Book}
|
||||||
current={location.pathname}
|
current={location.pathname}
|
||||||
/>
|
/>
|
||||||
|
<LinkComponent
|
||||||
|
href="/settings/share"
|
||||||
|
name="Manage Share"
|
||||||
|
icon={Share}
|
||||||
|
current={location.pathname}
|
||||||
|
/>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
|
@ -35,7 +35,7 @@ export const PlaygroundEmpty = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto sm:max-w-xl px-4 mt-10">
|
<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">
|
<div className="rounded-lg justify-center items-center flex flex-col border p-8 bg-white dark:bg-[#262626] dark:border-gray-600">
|
||||||
{(ollamaStatus === "pending" || isRefetching) && (
|
{(ollamaStatus === "pending" || isRefetching) && (
|
||||||
<div className="inline-flex items-center space-x-2">
|
<div className="inline-flex items-center space-x-2">
|
||||||
<div className="w-3 h-3 bg-blue-500 rounded-full animate-bounce"></div>
|
<div className="w-3 h-3 bg-blue-500 rounded-full animate-bounce"></div>
|
||||||
|
@ -18,6 +18,7 @@ type Props = {
|
|||||||
|
|
||||||
export const PlaygroundForm = ({ dropedFile }: Props) => {
|
export const PlaygroundForm = ({ dropedFile }: Props) => {
|
||||||
const inputRef = React.useRef<HTMLInputElement>(null)
|
const inputRef = React.useRef<HTMLInputElement>(null)
|
||||||
|
const [typing, setTyping] = React.useState<boolean>(false)
|
||||||
const {
|
const {
|
||||||
onSubmit,
|
onSubmit,
|
||||||
selectedModel,
|
selectedModel,
|
||||||
@ -115,14 +116,14 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
if (e.key === "Process" || e.key === "229") return
|
if (e.key === "Process" || e.key === "229" ) return
|
||||||
if (
|
if (
|
||||||
|
!typing &&
|
||||||
e.key === "Enter" &&
|
e.key === "Enter" &&
|
||||||
!e.shiftKey &&
|
!e.shiftKey &&
|
||||||
!isSending &&
|
!isSending &&
|
||||||
sendWhenEnter &&
|
sendWhenEnter
|
||||||
!e.isComposing
|
|
||||||
) {
|
) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
form.onSubmit(async (value) => {
|
form.onSubmit(async (value) => {
|
||||||
@ -153,7 +154,7 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="px-3 pt-3 md:px-6 md:pt-6 md:bg-white dark:bg-[#262626] border rounded-t-xl border-black/10 dark:border-gray-600">
|
<div className="px-3 pt-3 md:px-6 md:pt-6 md:bg-white dark:bg-[#262626] border rounded-t-xl dark:border-gray-600">
|
||||||
<div
|
<div
|
||||||
className={`h-full rounded-md shadow relative ${
|
className={`h-full rounded-md shadow relative ${
|
||||||
form.values.image.length === 0 ? "hidden" : "block"
|
form.values.image.length === 0 ? "hidden" : "block"
|
||||||
@ -213,7 +214,9 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
|
|||||||
/>
|
/>
|
||||||
<div className="w-full border-x border-t flex flex-col dark:border-gray-600 rounded-t-xl p-2">
|
<div className="w-full border-x border-t flex flex-col dark:border-gray-600 rounded-t-xl p-2">
|
||||||
<textarea
|
<textarea
|
||||||
onKeyDown={(e) => handleKeyDown(e as unknown as KeyboardEvent)}
|
onCompositionStart={() => setTyping(true)}
|
||||||
|
onCompositionEnd={() => setTyping(false)}
|
||||||
|
onKeyDown={(e) => handleKeyDown(e)}
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
className="px-2 py-2 w-full resize-none bg-transparent focus-within:outline-none focus:ring-0 focus-visible:ring-0 ring-0 dark:ring-0 border-0 dark:text-gray-100"
|
className="px-2 py-2 w-full resize-none bg-transparent focus-within:outline-none focus:ring-0 focus-visible:ring-0 ring-0 dark:ring-0 border-0 dark:text-gray-100"
|
||||||
required
|
required
|
||||||
|
@ -24,7 +24,7 @@ export const SettingOther = () => {
|
|||||||
<div className="border border-b border-gray-200 dark:border-gray-600 mt-3"></div>
|
<div className="border border-b border-gray-200 dark:border-gray-600 mt-3"></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row justify-between">
|
<div className="flex flex-row justify-between">
|
||||||
<span className="text-gray-500 dark:text-gray-400 text-lg">
|
<span className="text-gray-500 dark:text-neutral-50">
|
||||||
Speech Recognition Language
|
Speech Recognition Language
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@ -44,9 +44,7 @@ export const SettingOther = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row justify-between">
|
<div className="flex flex-row justify-between">
|
||||||
<span className="text-gray-500 dark:text-gray-400 text-lg">
|
<span className="text-gray-500 dark:text-neutral-50 ">Change Theme</span>
|
||||||
Change Theme
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={toggleDarkMode}
|
onClick={toggleDarkMode}
|
||||||
@ -59,8 +57,9 @@ export const SettingOther = () => {
|
|||||||
{mode === "dark" ? "Light" : "Dark"}
|
{mode === "dark" ? "Light" : "Dark"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<SearchModeSettings />
|
||||||
<div className="flex flex-row justify-between">
|
<div className="flex flex-row justify-between">
|
||||||
<span className="text-gray-500 dark:text-gray-400 text-lg">
|
<span className="text-gray-500 dark:text-neutral-50 ">
|
||||||
Delete Chat History
|
Delete Chat History
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@ -83,7 +82,6 @@ export const SettingOther = () => {
|
|||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<SearchModeSettings />
|
|
||||||
</dl>
|
</dl>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ export const SearchModeSettings = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row justify-between">
|
<div className="flex flex-row justify-between">
|
||||||
<span className="text-gray-500 dark:text-gray-400 text-lg">
|
<span className="text-gray-500 dark:text-neutral-50 ">
|
||||||
Perform Simple Internet Search
|
Perform Simple Internet Search
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
190
src/components/Option/Share/index.tsx
Normal file
190
src/components/Option/Share/index.tsx
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||||
|
import { Form, Input, Skeleton, Table, Tooltip, message } from "antd"
|
||||||
|
import { Trash2 } from "lucide-react"
|
||||||
|
import { SaveButton } from "~components/Common/SaveButton"
|
||||||
|
import { deleteWebshare, getAllWebshares, getUserId } from "~libs/db"
|
||||||
|
import { getPageShareUrl, setPageShareUrl } from "~services/ollama"
|
||||||
|
import { verifyPageShareURL } from "~utils/verify-page-share"
|
||||||
|
|
||||||
|
export const OptionShareBody = () => {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const { status, data } = useQuery({
|
||||||
|
queryKey: ["fetchShareInfo"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const [url, shares] = await Promise.all([
|
||||||
|
getPageShareUrl(),
|
||||||
|
getAllWebshares()
|
||||||
|
])
|
||||||
|
return { url, shares }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const onSubmit = async (values: { url: string }) => {
|
||||||
|
const isOk = await verifyPageShareURL(values.url)
|
||||||
|
if (isOk) {
|
||||||
|
await setPageShareUrl(values.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDelete = async ({
|
||||||
|
api_url,
|
||||||
|
share_id,
|
||||||
|
id
|
||||||
|
}: {
|
||||||
|
id: string
|
||||||
|
share_id: string
|
||||||
|
api_url: string
|
||||||
|
}) => {
|
||||||
|
const owner_id = await getUserId()
|
||||||
|
const res = await fetch(`${api_url}/api/v1/share/delete`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
share_id,
|
||||||
|
owner_id
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if (!res.ok) throw new Error("Failed to delete share link")
|
||||||
|
await deleteWebshare(id)
|
||||||
|
return "ok"
|
||||||
|
}
|
||||||
|
|
||||||
|
const { mutate: updatePageShareUrl, isPending: isUpdatePending } =
|
||||||
|
useMutation({
|
||||||
|
mutationFn: onSubmit,
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["fetchShareInfo"]
|
||||||
|
})
|
||||||
|
message.success("Page Share URL updated successfully")
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
message.error(error?.message || "Failed to update Page Share URL")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const { mutate: deleteMutation } = useMutation({
|
||||||
|
mutationFn: onDelete,
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["fetchShareInfo"]
|
||||||
|
})
|
||||||
|
message.success("Webshare deleted successfully")
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
message.error(error?.message || "Failed to delete Webshare")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col space-y-3">
|
||||||
|
{status === "pending" && <Skeleton paragraph={{ rows: 4 }} active />}
|
||||||
|
{status === "success" && (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-base font-semibold leading-7 text-gray-900 dark:text-white">
|
||||||
|
Configure Page Share URL
|
||||||
|
</h2>
|
||||||
|
<div className="border border-b border-gray-200 dark:border-gray-600 mt-3 mb-6"></div>
|
||||||
|
</div>
|
||||||
|
<Form
|
||||||
|
layout="vertical"
|
||||||
|
onFinish={updatePageShareUrl}
|
||||||
|
initialValues={{
|
||||||
|
url: data.url
|
||||||
|
}}>
|
||||||
|
<Form.Item
|
||||||
|
name="url"
|
||||||
|
help={
|
||||||
|
<span>
|
||||||
|
For privacy reasons, you can self-host the page share and
|
||||||
|
provide the URL here.{" "}
|
||||||
|
<a
|
||||||
|
href="https://github.com/n4ze3m/page-assist/blob/main/page-share.md"
|
||||||
|
target="__blank"
|
||||||
|
className="text-blue-600 dark:text-blue-400">
|
||||||
|
Learn more
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: "Please input your Page Share URL!"
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
label="Page Share URL">
|
||||||
|
<Input placeholder="Page Share URL" size="large" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<SaveButton disabled={isUpdatePending} btnType="submit" />
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-base font-semibold leading-7 text-gray-900 dark:text-white">
|
||||||
|
Webshares
|
||||||
|
</h2>
|
||||||
|
<div className="border border-b border-gray-200 dark:border-gray-600 mt-3 mb-6"></div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Table
|
||||||
|
dataSource={data.shares}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
title: "Title",
|
||||||
|
dataIndex: "title",
|
||||||
|
key: "title"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "URL",
|
||||||
|
dataIndex: "url",
|
||||||
|
key: "url",
|
||||||
|
render: (url: string) => (
|
||||||
|
<a
|
||||||
|
href={url}
|
||||||
|
target="__blank"
|
||||||
|
className="text-blue-600 dark:text-blue-400">
|
||||||
|
{url}
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Actions",
|
||||||
|
render: (_, render) => (
|
||||||
|
<Tooltip title="Delete Share">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
if (
|
||||||
|
window.confirm(
|
||||||
|
"Are you sure you want to delete this webshare?"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
deleteMutation({
|
||||||
|
id: render.id,
|
||||||
|
share_id: render.share_id,
|
||||||
|
api_url: render.api_url
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="text-red-500 dark:text-red-400">
|
||||||
|
<Trash2 className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -10,14 +10,17 @@ import { Empty, Skeleton } from "antd"
|
|||||||
import { useMessageOption } from "~hooks/useMessageOption"
|
import { useMessageOption } from "~hooks/useMessageOption"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { PencilIcon, Trash2 } from "lucide-react"
|
import { PencilIcon, Trash2 } from "lucide-react"
|
||||||
|
import { useNavigate } from "react-router-dom"
|
||||||
|
|
||||||
type Props = {}
|
type Props = {
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
export const Sidebar = ({}: Props) => {
|
export const Sidebar = ({ onClose }: Props) => {
|
||||||
const { setMessages, setHistory, setHistoryId, historyId, clearChat } =
|
const { setMessages, setHistory, setHistoryId, historyId, clearChat } =
|
||||||
useMessageOption()
|
useMessageOption()
|
||||||
const [processingId, setProcessingId] = useState<string>("")
|
|
||||||
const client = useQueryClient()
|
const client = useQueryClient()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const { data: chatHistories, status } = useQuery({
|
const { data: chatHistories, status } = useQuery({
|
||||||
queryKey: ["fetchChatHistory"],
|
queryKey: ["fetchChatHistory"],
|
||||||
@ -28,21 +31,20 @@ export const Sidebar = ({}: Props) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const { isPending: isDeleting, mutate: deleteHistory } = useMutation({
|
const { mutate: deleteHistory } = useMutation({
|
||||||
mutationKey: ["deleteHistory"],
|
mutationKey: ["deleteHistory"],
|
||||||
mutationFn: deleteByHistoryId,
|
mutationFn: deleteByHistoryId,
|
||||||
onSuccess: (history_id) => {
|
onSuccess: (history_id) => {
|
||||||
client.invalidateQueries({
|
client.invalidateQueries({
|
||||||
queryKey: ["fetchChatHistory"]
|
queryKey: ["fetchChatHistory"]
|
||||||
})
|
})
|
||||||
setProcessingId("")
|
|
||||||
if (historyId === history_id) {
|
if (historyId === history_id) {
|
||||||
clearChat()
|
clearChat()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const { isPending: isEditing, mutate: editHistory } = useMutation({
|
const { mutate: editHistory } = useMutation({
|
||||||
mutationKey: ["editHistory"],
|
mutationKey: ["editHistory"],
|
||||||
mutationFn: async (data: { id: string; title: string }) => {
|
mutationFn: async (data: { id: string; title: string }) => {
|
||||||
return await updateHistory(data.id, data.title)
|
return await updateHistory(data.id, data.title)
|
||||||
@ -51,7 +53,6 @@ export const Sidebar = ({}: Props) => {
|
|||||||
client.invalidateQueries({
|
client.invalidateQueries({
|
||||||
queryKey: ["fetchChatHistory"]
|
queryKey: ["fetchChatHistory"]
|
||||||
})
|
})
|
||||||
setProcessingId("")
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -86,6 +87,8 @@ export const Sidebar = ({}: Props) => {
|
|||||||
setHistoryId(chat.id)
|
setHistoryId(chat.id)
|
||||||
setHistory(formatToChatHistory(history))
|
setHistory(formatToChatHistory(history))
|
||||||
setMessages(formatToMessage(history))
|
setMessages(formatToMessage(history))
|
||||||
|
navigate("/")
|
||||||
|
onClose()
|
||||||
}}>
|
}}>
|
||||||
<span className="flex-grow truncate">{chat.title}</span>
|
<span className="flex-grow truncate">{chat.title}</span>
|
||||||
</button>
|
</button>
|
||||||
@ -97,8 +100,6 @@ export const Sidebar = ({}: Props) => {
|
|||||||
if (newTitle) {
|
if (newTitle) {
|
||||||
editHistory({ id: chat.id, title: newTitle })
|
editHistory({ id: chat.id, title: newTitle })
|
||||||
}
|
}
|
||||||
|
|
||||||
setProcessingId(chat.id)
|
|
||||||
}}
|
}}
|
||||||
className="text-gray-500 dark:text-gray-400 opacity-80">
|
className="text-gray-500 dark:text-gray-400 opacity-80">
|
||||||
<PencilIcon className="w-4 h-4" />
|
<PencilIcon className="w-4 h-4" />
|
||||||
@ -111,7 +112,6 @@ export const Sidebar = ({}: Props) => {
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
deleteHistory(chat.id)
|
deleteHistory(chat.id)
|
||||||
setProcessingId(chat.id)
|
|
||||||
}}
|
}}
|
||||||
className="text-red-500 dark:text-red-400 opacity-80">
|
className="text-red-500 dark:text-red-400 opacity-80">
|
||||||
<Trash2 className=" w-4 h-4 " />
|
<Trash2 className=" w-4 h-4 " />
|
||||||
|
@ -38,7 +38,7 @@ export const EmptySidePanel = () => {
|
|||||||
}
|
}
|
||||||
}, [ollamaInfo])
|
}, [ollamaInfo])
|
||||||
|
|
||||||
const { setSelectedModel, selectedModel, chatMode, setChatMode } =
|
const { setSelectedModel, selectedModel, chatMode, setChatMode, } =
|
||||||
useMessage()
|
useMessage()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -18,6 +18,7 @@ export const SidepanelForm = ({ dropedFile }: Props) => {
|
|||||||
const textareaRef = React.useRef<HTMLTextAreaElement>(null)
|
const textareaRef = React.useRef<HTMLTextAreaElement>(null)
|
||||||
const inputRef = React.useRef<HTMLInputElement>(null)
|
const inputRef = React.useRef<HTMLInputElement>(null)
|
||||||
const { sendWhenEnter, setSendWhenEnter } = useWebUI()
|
const { sendWhenEnter, setSendWhenEnter } = useWebUI()
|
||||||
|
const [typing, setTyping] = React.useState<boolean>(false)
|
||||||
|
|
||||||
const textAreaFocus = () => {
|
const textAreaFocus = () => {
|
||||||
if (textareaRef.current) {
|
if (textareaRef.current) {
|
||||||
@ -72,14 +73,14 @@ export const SidepanelForm = ({ dropedFile }: Props) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
if (e.key === "Process" || e.key === "229") return
|
if (e.key === "Process" || e.key === "229") return
|
||||||
if (
|
if (
|
||||||
e.key === "Enter" &&
|
e.key === "Enter" &&
|
||||||
!e.shiftKey &&
|
!e.shiftKey &&
|
||||||
!isSending &&
|
!isSending &&
|
||||||
sendWhenEnter &&
|
sendWhenEnter &&
|
||||||
!e.isComposing
|
!typing
|
||||||
) {
|
) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
form.onSubmit(async (value) => {
|
form.onSubmit(async (value) => {
|
||||||
@ -171,13 +172,15 @@ export const SidepanelForm = ({ dropedFile }: Props) => {
|
|||||||
/>
|
/>
|
||||||
<div className="w-full border-x border-t flex flex-col dark:border-gray-600 rounded-t-xl p-2">
|
<div className="w-full border-x border-t flex flex-col dark:border-gray-600 rounded-t-xl p-2">
|
||||||
<textarea
|
<textarea
|
||||||
onKeyDown={(e) => handleKeyDown(e as unknown as KeyboardEvent)}
|
onKeyDown={(e) => handleKeyDown(e)}
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
className="px-2 py-2 w-full resize-none bg-transparent focus-within:outline-none focus:ring-0 focus-visible:ring-0 ring-0 dark:ring-0 border-0 dark:text-gray-100"
|
className="px-2 py-2 w-full resize-none bg-transparent focus-within:outline-none focus:ring-0 focus-visible:ring-0 ring-0 dark:ring-0 border-0 dark:text-gray-100"
|
||||||
required
|
required
|
||||||
rows={1}
|
rows={1}
|
||||||
style={{ minHeight: "60px" }}
|
style={{ minHeight: "60px" }}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
|
onCompositionStart={() => setTyping(true)}
|
||||||
|
onCompositionEnd={() => setTyping(false)}
|
||||||
placeholder="Type a message..."
|
placeholder="Type a message..."
|
||||||
{...form.getInputProps("message")}
|
{...form.getInputProps("message")}
|
||||||
/>
|
/>
|
||||||
|
@ -20,9 +20,12 @@ import { getHtmlOfCurrentTab } from "~libs/get-html"
|
|||||||
import { PageAssistHtmlLoader } from "~loader/html"
|
import { PageAssistHtmlLoader } from "~loader/html"
|
||||||
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter"
|
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter"
|
||||||
import { OllamaEmbeddings } from "@langchain/community/embeddings/ollama"
|
import { OllamaEmbeddings } from "@langchain/community/embeddings/ollama"
|
||||||
import { createChatWithWebsiteChain, groupMessagesByConversation } from "~chain/chat-with-website"
|
import {
|
||||||
|
createChatWithWebsiteChain,
|
||||||
|
groupMessagesByConversation
|
||||||
|
} from "~chain/chat-with-website"
|
||||||
import { MemoryVectorStore } from "langchain/vectorstores/memory"
|
import { MemoryVectorStore } from "langchain/vectorstores/memory"
|
||||||
|
import { chromeRunTime } from "~libs/runtime"
|
||||||
export type BotResponse = {
|
export type BotResponse = {
|
||||||
bot: {
|
bot: {
|
||||||
text: string
|
text: string
|
||||||
@ -104,7 +107,9 @@ export const useMessage = () => {
|
|||||||
setIsEmbedding,
|
setIsEmbedding,
|
||||||
isEmbedding,
|
isEmbedding,
|
||||||
speechToTextLanguage,
|
speechToTextLanguage,
|
||||||
setSpeechToTextLanguage
|
setSpeechToTextLanguage,
|
||||||
|
currentURL,
|
||||||
|
setCurrentURL
|
||||||
} = useStoreMessage()
|
} = useStoreMessage()
|
||||||
|
|
||||||
const abortControllerRef = React.useRef<AbortController | null>(null)
|
const abortControllerRef = React.useRef<AbortController | null>(null)
|
||||||
@ -134,11 +139,11 @@ export const useMessage = () => {
|
|||||||
url
|
url
|
||||||
})
|
})
|
||||||
const docs = await loader.load()
|
const docs = await loader.load()
|
||||||
const chunkSize = await defaultEmbeddingChunkSize();
|
const chunkSize = await defaultEmbeddingChunkSize()
|
||||||
const chunkOverlap = await defaultEmbeddingChunkOverlap();
|
const chunkOverlap = await defaultEmbeddingChunkOverlap()
|
||||||
const textSplitter = new RecursiveCharacterTextSplitter({
|
const textSplitter = new RecursiveCharacterTextSplitter({
|
||||||
chunkSize,
|
chunkSize,
|
||||||
chunkOverlap,
|
chunkOverlap
|
||||||
})
|
})
|
||||||
|
|
||||||
const chunks = await textSplitter.splitDocuments(docs)
|
const chunks = await textSplitter.splitDocuments(docs)
|
||||||
@ -158,9 +163,19 @@ export const useMessage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const chatWithWebsiteMode = async (message: string) => {
|
const chatWithWebsiteMode = async (message: string) => {
|
||||||
const ollamaUrl = await getOllamaURL()
|
try {
|
||||||
|
let isAlreadyExistEmbedding: MemoryVectorStore
|
||||||
|
let embedURL: string, embedHTML: string
|
||||||
|
if (messages.length === 0) {
|
||||||
const { html, url } = await getHtmlOfCurrentTab()
|
const { html, url } = await getHtmlOfCurrentTab()
|
||||||
const isAlreadyExistEmbedding = keepTrackOfEmbedding[url]
|
embedHTML = html
|
||||||
|
embedURL = url
|
||||||
|
setCurrentURL(url)
|
||||||
|
isAlreadyExistEmbedding = keepTrackOfEmbedding[currentURL]
|
||||||
|
} else {
|
||||||
|
isAlreadyExistEmbedding = keepTrackOfEmbedding[currentURL]
|
||||||
|
embedURL = currentURL
|
||||||
|
}
|
||||||
let newMessage: Message[] = [
|
let newMessage: Message[] = [
|
||||||
...messages,
|
...messages,
|
||||||
{
|
{
|
||||||
@ -179,13 +194,14 @@ export const useMessage = () => {
|
|||||||
|
|
||||||
const appendingIndex = newMessage.length - 1
|
const appendingIndex = newMessage.length - 1
|
||||||
setMessages(newMessage)
|
setMessages(newMessage)
|
||||||
|
const ollamaUrl = await getOllamaURL()
|
||||||
const embeddingModle = await defaultEmbeddingModelForRag()
|
const embeddingModle = await defaultEmbeddingModelForRag()
|
||||||
|
|
||||||
const ollamaEmbedding = new OllamaEmbeddings({
|
const ollamaEmbedding = new OllamaEmbeddings({
|
||||||
model: embeddingModle || selectedModel,
|
model: embeddingModle || selectedModel,
|
||||||
baseUrl: cleanUrl(ollamaUrl)
|
baseUrl: cleanUrl(ollamaUrl)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
const ollamaChat = new ChatOllama({
|
const ollamaChat = new ChatOllama({
|
||||||
model: selectedModel,
|
model: selectedModel,
|
||||||
baseUrl: cleanUrl(ollamaUrl)
|
baseUrl: cleanUrl(ollamaUrl)
|
||||||
@ -196,7 +212,11 @@ export const useMessage = () => {
|
|||||||
if (isAlreadyExistEmbedding) {
|
if (isAlreadyExistEmbedding) {
|
||||||
vectorstore = isAlreadyExistEmbedding
|
vectorstore = isAlreadyExistEmbedding
|
||||||
} else {
|
} else {
|
||||||
vectorstore = await memoryEmbedding(url, html, ollamaEmbedding)
|
vectorstore = await memoryEmbedding(
|
||||||
|
embedURL,
|
||||||
|
embedHTML,
|
||||||
|
ollamaEmbedding
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { ragPrompt: systemPrompt, ragQuestionPrompt: questionPrompt } =
|
const { ragPrompt: systemPrompt, ragQuestionPrompt: questionPrompt } =
|
||||||
@ -212,10 +232,9 @@ export const useMessage = () => {
|
|||||||
retriever: vectorstore.asRetriever()
|
retriever: vectorstore.asRetriever()
|
||||||
})
|
})
|
||||||
|
|
||||||
try {
|
|
||||||
const chunks = await chain.stream({
|
const chunks = await chain.stream({
|
||||||
question: sanitizedQuestion,
|
question: sanitizedQuestion,
|
||||||
chat_history: groupMessagesByConversation(history),
|
chat_history: groupMessagesByConversation(history)
|
||||||
})
|
})
|
||||||
let count = 0
|
let count = 0
|
||||||
for await (const chunk of chunks) {
|
for await (const chunk of chunks) {
|
||||||
@ -258,7 +277,8 @@ export const useMessage = () => {
|
|||||||
{
|
{
|
||||||
isBot: true,
|
isBot: true,
|
||||||
name: selectedModel,
|
name: selectedModel,
|
||||||
message: `Something went wrong. Check out the following logs:
|
message: `Error in chat with website mode. Check out the following logs:
|
||||||
|
|
||||||
~~~
|
~~~
|
||||||
${e?.message}
|
${e?.message}
|
||||||
~~~
|
~~~
|
||||||
|
@ -32,6 +32,16 @@ type Message = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type Webshare = {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
url: string
|
||||||
|
api_url: string
|
||||||
|
share_id: string
|
||||||
|
createdAt: number
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
type Prompt = {
|
type Prompt = {
|
||||||
id: string
|
id: string
|
||||||
title: string
|
title: string
|
||||||
@ -154,6 +164,47 @@ export class PageAssitDatabase {
|
|||||||
return prompts.find((prompt) => prompt.id === id)
|
return prompts.find((prompt) => prompt.id === id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async getWebshare(id: string) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.db.get(id, (result) => {
|
||||||
|
resolve(result[id] || [])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async getAllWebshares(): Promise<Webshare[]> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.db.get("webshares", (result) => {
|
||||||
|
resolve(result.webshares || [])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async addWebshare(webshare: Webshare) {
|
||||||
|
const webshares = await this.getAllWebshares()
|
||||||
|
const newWebshares = [webshare, ...webshares]
|
||||||
|
this.db.set({ webshares: newWebshares })
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteWebshare(id: string) {
|
||||||
|
const webshares = await this.getAllWebshares()
|
||||||
|
const newWebshares = webshares.filter((webshare) => webshare.id !== id)
|
||||||
|
this.db.set({ webshares: newWebshares })
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserID() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.db.get("user_id", (result) => {
|
||||||
|
resolve(result.user_id || "")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async setUserID(id: string) {
|
||||||
|
this.db.set({ user_id: id })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -278,3 +329,38 @@ export const getPromptById = async (id: string) => {
|
|||||||
const db = new PageAssitDatabase()
|
const db = new PageAssitDatabase()
|
||||||
return await db.getPromptById(id)
|
return await db.getPromptById(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getAllWebshares = async () => {
|
||||||
|
const db = new PageAssitDatabase()
|
||||||
|
return await db.getAllWebshares()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteWebshare = async (id: string) => {
|
||||||
|
const db = new PageAssitDatabase()
|
||||||
|
await db.deleteWebshare(id)
|
||||||
|
return id
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const saveWebshare = async ({ title, url, api_url, share_id }: { title: string, url: string, api_url: string, share_id: string }) => {
|
||||||
|
const db = new PageAssitDatabase()
|
||||||
|
const id = generateID()
|
||||||
|
const createdAt = Date.now()
|
||||||
|
const webshare = { id, title, url, share_id, createdAt, api_url }
|
||||||
|
await db.addWebshare(webshare)
|
||||||
|
return webshare
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getUserId = async () => {
|
||||||
|
const db = new PageAssitDatabase()
|
||||||
|
const id = await db.getUserID() as string
|
||||||
|
if (!id || id?.trim() === "") {
|
||||||
|
const user_id = "user_xxxx-xxxx-xxx-xxxx-xxxx".replace(/[x]/g, () => {
|
||||||
|
const r = Math.floor(Math.random() * 16)
|
||||||
|
return r.toString(16)
|
||||||
|
})
|
||||||
|
db.setUserID(user_id)
|
||||||
|
return user_id
|
||||||
|
}
|
||||||
|
return id
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
const _getHtml = () => {
|
const _getHtml = () => {
|
||||||
const url = window.location.href
|
const url = window.location.href
|
||||||
const html = Array.from(document.querySelectorAll("script")).reduce(
|
const html = Array.from(document.querySelectorAll("script")).reduce(
|
||||||
@ -29,3 +30,4 @@ export const getHtmlOfCurrentTab = async () => {
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,20 @@ import { BaseDocumentLoader } from "langchain/document_loaders/base"
|
|||||||
import { Document } from "@langchain/core/documents"
|
import { Document } from "@langchain/core/documents"
|
||||||
import { compile } from "html-to-text"
|
import { compile } from "html-to-text"
|
||||||
import { chromeRunTime } from "~libs/runtime"
|
import { chromeRunTime } from "~libs/runtime"
|
||||||
|
import { YtTranscript } from "yt-transcript"
|
||||||
|
|
||||||
|
const YT_REGEX =
|
||||||
|
/(?:https?:\/\/)?(?:www\.)?(?:youtube\.com|youtu\.be)\/(?:watch\?v=)?([a-zA-Z0-9_-]+)/
|
||||||
|
|
||||||
|
const isYoutubeLink = (url: string) => {
|
||||||
|
return YT_REGEX.test(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTranscript = async (url: string) => {
|
||||||
|
const ytTranscript = new YtTranscript({ url })
|
||||||
|
return await ytTranscript.getTranscript()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface WebLoaderParams {
|
export interface WebLoaderParams {
|
||||||
html: string
|
html: string
|
||||||
@ -21,6 +35,29 @@ export class PageAssistHtmlLoader
|
|||||||
}
|
}
|
||||||
|
|
||||||
async load(): Promise<Document<Record<string, any>>[]> {
|
async load(): Promise<Document<Record<string, any>>[]> {
|
||||||
|
if (isYoutubeLink(this.url)) {
|
||||||
|
const transcript = await getTranscript(this.url)
|
||||||
|
if (!transcript) {
|
||||||
|
throw new Error("Transcript not found for this video.")
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = ""
|
||||||
|
|
||||||
|
transcript.forEach((item) => {
|
||||||
|
text += item.text + " "
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
metadata: {
|
||||||
|
source: this.url,
|
||||||
|
audio: { chunks: transcript }
|
||||||
|
},
|
||||||
|
pageContent: text
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
const htmlCompiler = compile({
|
const htmlCompiler = compile({
|
||||||
wordwrap: false
|
wordwrap: false
|
||||||
})
|
})
|
||||||
@ -30,6 +67,29 @@ export class PageAssistHtmlLoader
|
|||||||
}
|
}
|
||||||
|
|
||||||
async loadByURL(): Promise<Document<Record<string, any>>[]> {
|
async loadByURL(): Promise<Document<Record<string, any>>[]> {
|
||||||
|
if (isYoutubeLink(this.url)) {
|
||||||
|
const transcript = await getTranscript(this.url)
|
||||||
|
if (!transcript) {
|
||||||
|
throw new Error("Transcript not found for this video.")
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = ""
|
||||||
|
|
||||||
|
transcript.forEach((item) => {
|
||||||
|
text += item.text + " "
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
metadata: {
|
||||||
|
source: this.url,
|
||||||
|
audio: { chunks: transcript }
|
||||||
|
},
|
||||||
|
pageContent: text
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
await chromeRunTime(this.url)
|
await chromeRunTime(this.url)
|
||||||
const fetchHTML = await fetch(this.url)
|
const fetchHTML = await fetch(this.url)
|
||||||
const html = await fetchHTML.text()
|
const html = await fetchHTML.text()
|
||||||
|
@ -7,6 +7,7 @@ import { OptionModal } from "./option-settings-model"
|
|||||||
import { OptionPrompt } from "./option-settings-prompt"
|
import { OptionPrompt } from "./option-settings-prompt"
|
||||||
import { OptionOllamaSettings } from "./options-settings-ollama"
|
import { OptionOllamaSettings } from "./options-settings-ollama"
|
||||||
import { OptionSettings } from "./option-settings"
|
import { OptionSettings } from "./option-settings"
|
||||||
|
import { OptionShare } from "./option-settings-share"
|
||||||
|
|
||||||
export const OptionRouting = () => {
|
export const OptionRouting = () => {
|
||||||
const { mode } = useDarkMode()
|
const { mode } = useDarkMode()
|
||||||
@ -19,6 +20,7 @@ export const OptionRouting = () => {
|
|||||||
<Route path="/settings/model" element={<OptionModal />} />
|
<Route path="/settings/model" element={<OptionModal />} />
|
||||||
<Route path="/settings/prompt" element={<OptionPrompt />} />
|
<Route path="/settings/prompt" element={<OptionPrompt />} />
|
||||||
<Route path="/settings/ollama" element={<OptionOllamaSettings />} />
|
<Route path="/settings/ollama" element={<OptionOllamaSettings />} />
|
||||||
|
<Route path="/settings/share" element={<OptionShare />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
13
src/routes/option-settings-share.tsx
Normal file
13
src/routes/option-settings-share.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { SettingsLayout } from "~components/Layouts/SettingsOptionLayout"
|
||||||
|
import OptionLayout from "~components/Layouts/Layout"
|
||||||
|
import { OptionShareBody } from "~components/Option/Share"
|
||||||
|
|
||||||
|
export const OptionShare = () => {
|
||||||
|
return (
|
||||||
|
<OptionLayout>
|
||||||
|
<SettingsLayout>
|
||||||
|
<OptionShareBody />
|
||||||
|
</SettingsLayout>
|
||||||
|
</OptionLayout>
|
||||||
|
)
|
||||||
|
}
|
@ -6,6 +6,7 @@ const storage = new Storage()
|
|||||||
|
|
||||||
const DEFAULT_OLLAMA_URL = "http://127.0.0.1:11434"
|
const DEFAULT_OLLAMA_URL = "http://127.0.0.1:11434"
|
||||||
const DEFAULT_ASK_FOR_MODEL_SELECTION_EVERY_TIME = true
|
const DEFAULT_ASK_FOR_MODEL_SELECTION_EVERY_TIME = true
|
||||||
|
const DEFAULT_PAGE_SHARE_URL = "https://pageassist.xyz"
|
||||||
|
|
||||||
const DEFAULT_RAG_QUESTION_PROMPT =
|
const DEFAULT_RAG_QUESTION_PROMPT =
|
||||||
"Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question. Chat History: {chat_history} Follow Up Input: {question} Standalone question:"
|
"Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question. Chat History: {chat_history} Follow Up Input: {question} Standalone question:"
|
||||||
@ -303,3 +304,16 @@ export const getIsSimpleInternetSearch = async () => {
|
|||||||
export const setIsSimpleInternetSearch = async (isSimpleInternetSearch: boolean) => {
|
export const setIsSimpleInternetSearch = async (isSimpleInternetSearch: boolean) => {
|
||||||
await storage.set("isSimpleInternetSearch", isSimpleInternetSearch.toString())
|
await storage.set("isSimpleInternetSearch", isSimpleInternetSearch.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getPageShareUrl = async () => {
|
||||||
|
const pageShareUrl = await storage.get("pageShareUrl")
|
||||||
|
if (!pageShareUrl || pageShareUrl.length === 0) {
|
||||||
|
return DEFAULT_PAGE_SHARE_URL
|
||||||
|
}
|
||||||
|
return pageShareUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const setPageShareUrl = async (pageShareUrl: string) => {
|
||||||
|
await storage.set("pageShareUrl", pageShareUrl)
|
||||||
|
}
|
@ -37,6 +37,8 @@ type State = {
|
|||||||
setIsEmbedding: (isEmbedding: boolean) => void
|
setIsEmbedding: (isEmbedding: boolean) => void
|
||||||
speechToTextLanguage: string
|
speechToTextLanguage: string
|
||||||
setSpeechToTextLanguage: (speechToTextLanguage: string) => void
|
setSpeechToTextLanguage: (speechToTextLanguage: string) => void
|
||||||
|
currentURL: string
|
||||||
|
setCurrentURL: (currentURL: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useStoreMessage = create<State>((set) => ({
|
export const useStoreMessage = create<State>((set) => ({
|
||||||
@ -63,5 +65,7 @@ export const useStoreMessage = create<State>((set) => ({
|
|||||||
setIsEmbedding: (isEmbedding) => set({ isEmbedding }),
|
setIsEmbedding: (isEmbedding) => set({ isEmbedding }),
|
||||||
speechToTextLanguage: "en-US",
|
speechToTextLanguage: "en-US",
|
||||||
setSpeechToTextLanguage: (speechToTextLanguage) =>
|
setSpeechToTextLanguage: (speechToTextLanguage) =>
|
||||||
set({ speechToTextLanguage })
|
set({ speechToTextLanguage }),
|
||||||
|
currentURL: "",
|
||||||
|
setCurrentURL: (currentURL) => set({ currentURL })
|
||||||
}))
|
}))
|
||||||
|
10
src/utils/verify-page-share.ts
Normal file
10
src/utils/verify-page-share.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { cleanUrl } from "~libs/clean-url"
|
||||||
|
|
||||||
|
export const verifyPageShareURL = async (url: string) => {
|
||||||
|
const res = await fetch(`${cleanUrl(url)}/api/v1/ping`)
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error("Unable to verify page share")
|
||||||
|
}
|
||||||
|
const data = await res.text()
|
||||||
|
return data === "pong"
|
||||||
|
}
|
23
yarn.lock
23
yarn.lock
@ -4967,10 +4967,10 @@ lru-cache@^6.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3"
|
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3"
|
||||||
integrity sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==
|
integrity sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==
|
||||||
|
|
||||||
lucide-react@^0.340.0:
|
lucide-react@^0.350.0:
|
||||||
version "0.340.0"
|
version "0.350.0"
|
||||||
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.340.0.tgz#67a6fac6a5e257f2036dffae0dd94d6ccb28ce8e"
|
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.350.0.tgz#78b45342f4daff4535290e37b1ea7eb0961a3dab"
|
||||||
integrity sha512-mWzYhbyy2d+qKuKHh+GWElPwa+kIquTnKbmSLGWOuZy+bjfZCkYD8DQWVFlqI4mQwc4HNxcqcOvtQ7ZS2PwURg==
|
integrity sha512-5IZVKsxxG8Nn81gpsz4XLNgCAXkppCh0Y0P0GLO39h5iVD2WEaB9of6cPkLtzys1GuSfxJxmwsDh487y7LAf/g==
|
||||||
|
|
||||||
magic-string@^0.30.0:
|
magic-string@^0.30.0:
|
||||||
version "0.30.6"
|
version "0.30.6"
|
||||||
@ -7772,6 +7772,13 @@ ws@^8.11.0:
|
|||||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.16.0.tgz#d1cd774f36fbc07165066a60e40323eab6446fd4"
|
resolved "https://registry.yarnpkg.com/ws/-/ws-8.16.0.tgz#d1cd774f36fbc07165066a60e40323eab6446fd4"
|
||||||
integrity sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==
|
integrity sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==
|
||||||
|
|
||||||
|
xml-js@^1.6.11:
|
||||||
|
version "1.6.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/xml-js/-/xml-js-1.6.11.tgz#927d2f6947f7f1c19a316dd8eea3614e8b18f8e9"
|
||||||
|
integrity sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==
|
||||||
|
dependencies:
|
||||||
|
sax "^1.2.4"
|
||||||
|
|
||||||
xml-name-validator@^4.0.0:
|
xml-name-validator@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835"
|
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835"
|
||||||
@ -7817,6 +7824,14 @@ yaml@^2.2.1, yaml@^2.3.4:
|
|||||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.4.tgz#53fc1d514be80aabf386dc6001eb29bf3b7523b2"
|
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.4.tgz#53fc1d514be80aabf386dc6001eb29bf3b7523b2"
|
||||||
integrity sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==
|
integrity sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==
|
||||||
|
|
||||||
|
yt-transcript@^0.0.2:
|
||||||
|
version "0.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/yt-transcript/-/yt-transcript-0.0.2.tgz#1c54aede89bb8a03bbca3ba58520dbbd9c828571"
|
||||||
|
integrity sha512-+cNRqW6tSQNDkQDVrWNT6hc6X2TnaQLvUJIepzn9r7XdEvPtUDkfsyhptW5+j0EPIEpnlsKyA/epCUrE4QKn2g==
|
||||||
|
dependencies:
|
||||||
|
axios "^1.6.7"
|
||||||
|
xml-js "^1.6.11"
|
||||||
|
|
||||||
zod-to-json-schema@^3.22.3:
|
zod-to-json-schema@^3.22.3:
|
||||||
version "3.22.4"
|
version "3.22.4"
|
||||||
resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.22.4.tgz#f8cc691f6043e9084375e85fb1f76ebafe253d70"
|
resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.22.4.tgz#f8cc691f6043e9084375e85fb1f76ebafe253d70"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user