Merge pull request #16 from n4ze3m/next

latest
This commit is contained in:
Muhammed Nazeem 2024-03-10 13:48:24 +05:30 committed by GitHub
commit 912e6c8b0a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 782 additions and 130 deletions

View File

@ -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.

View File

@ -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
View 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.
[![Deploy on Railway](https://railway.app/button.svg)](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.

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

View File

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

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

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

View File

@ -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>

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

View File

@ -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 " />

View File

@ -38,7 +38,7 @@ export const EmptySidePanel = () => {
} }
}, [ollamaInfo]) }, [ollamaInfo])
const { setSelectedModel, selectedModel, chatMode, setChatMode } = const { setSelectedModel, selectedModel, chatMode, setChatMode, } =
useMessage() useMessage()
return ( return (

View File

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

View File

@ -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,64 +163,78 @@ export const useMessage = () => {
} }
const chatWithWebsiteMode = async (message: string) => { 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 embeddingModle = await defaultEmbeddingModelForRag()
const ollamaEmbedding = new OllamaEmbeddings({
model: embeddingModle || 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 { try {
let isAlreadyExistEmbedding: MemoryVectorStore
let embedURL: string, embedHTML: string
if (messages.length === 0) {
const { html, url } = await getHtmlOfCurrentTab()
embedHTML = html
embedURL = url
setCurrentURL(url)
isAlreadyExistEmbedding = keepTrackOfEmbedding[currentURL]
} else {
isAlreadyExistEmbedding = keepTrackOfEmbedding[currentURL]
embedURL = currentURL
}
let newMessage: Message[] = [
...messages,
{
isBot: false,
name: "You",
message,
sources: []
},
{
isBot: true,
name: selectedModel,
message: "▋",
sources: []
}
]
const appendingIndex = newMessage.length - 1
setMessages(newMessage)
const ollamaUrl = await getOllamaURL()
const embeddingModle = await defaultEmbeddingModelForRag()
const ollamaEmbedding = new OllamaEmbeddings({
model: embeddingModle || selectedModel,
baseUrl: cleanUrl(ollamaUrl)
})
const ollamaChat = new ChatOllama({
model: selectedModel,
baseUrl: cleanUrl(ollamaUrl)
})
let vectorstore: MemoryVectorStore
if (isAlreadyExistEmbedding) {
vectorstore = isAlreadyExistEmbedding
} else {
vectorstore = await memoryEmbedding(
embedURL,
embedHTML,
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()
})
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}
~~~ ~~~

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

View File

@ -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"