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