commit
						71a2b7656e
					
				| @ -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.1", |   "version": "1.0.2", | ||||||
|   "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": { | ||||||
| @ -11,7 +11,6 @@ | |||||||
|   }, |   }, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@ant-design/cssinjs": "^1.18.4", |     "@ant-design/cssinjs": "^1.18.4", | ||||||
|     "@headlessui/react": "^1.7.18", |  | ||||||
|     "@heroicons/react": "^2.1.1", |     "@heroicons/react": "^2.1.1", | ||||||
|     "@langchain/community": "^0.0.21", |     "@langchain/community": "^0.0.21", | ||||||
|     "@langchain/core": "^0.1.22", |     "@langchain/core": "^0.1.22", | ||||||
| @ -22,6 +21,7 @@ | |||||||
|     "@tanstack/react-query": "^5.17.19", |     "@tanstack/react-query": "^5.17.19", | ||||||
|     "antd": "^5.13.3", |     "antd": "^5.13.3", | ||||||
|     "axios": "^1.6.7", |     "axios": "^1.6.7", | ||||||
|  |     "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.323.0", |     "lucide-react": "^0.323.0", | ||||||
|  | |||||||
| @ -1,5 +1,80 @@ | |||||||
|  | import { getOllamaURL, isOllamaRunning } from "~services/ollama" | ||||||
|  | 
 | ||||||
| export {} | export {} | ||||||
| 
 | 
 | ||||||
|  | const progressHuman = (completed: number, total: number) => { | ||||||
|  |   return ((completed / total) * 100).toFixed(0) + "%" | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const clearBadge = () => { | ||||||
|  |   chrome.action.setBadgeText({ text: "" }) | ||||||
|  |   chrome.action.setTitle({ title: "" }) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const streamDownload = async (url: string, model: string) => { | ||||||
|  |   url += "/api/pull" | ||||||
|  |   const response = await fetch(url, { | ||||||
|  |     method: "POST", | ||||||
|  |     headers: { | ||||||
|  |       "Content-Type": "application/json" | ||||||
|  |     }, | ||||||
|  |     body: JSON.stringify({ model, stream: true }) | ||||||
|  |   }) | ||||||
|  | 
 | ||||||
|  |   const reader = response.body?.getReader() | ||||||
|  | 
 | ||||||
|  |   const decoder = new TextDecoder() | ||||||
|  | 
 | ||||||
|  |   let isSuccess = true | ||||||
|  |   while (true) { | ||||||
|  |     const { done, value } = await reader.read() | ||||||
|  | 
 | ||||||
|  |     if (done) { | ||||||
|  |       break | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const text = decoder.decode(value) | ||||||
|  |     try { | ||||||
|  |       const json = JSON.parse(text.trim()) as { | ||||||
|  |         status: string | ||||||
|  |         total?: number | ||||||
|  |         completed?: number | ||||||
|  |       } | ||||||
|  |       if (json.total && json.completed) { | ||||||
|  |         chrome.action.setBadgeText({ | ||||||
|  |           text: progressHuman(json.completed, json.total) | ||||||
|  |         }) | ||||||
|  |         chrome.action.setBadgeBackgroundColor({ color: "#0000FF" }) | ||||||
|  |       } else { | ||||||
|  |         chrome.action.setBadgeText({ text: "🏋️♂️" }) | ||||||
|  |         chrome.action.setBadgeBackgroundColor({ color: "#FFFFFF" }) | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       chrome.action.setTitle({ title: json.status }) | ||||||
|  | 
 | ||||||
|  |       if (json.status === "success") { | ||||||
|  |         isSuccess = true | ||||||
|  |       } | ||||||
|  |     } catch (e) { | ||||||
|  |       console.error(e) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   if (isSuccess) { | ||||||
|  |     chrome.action.setBadgeText({ text: "✅" }) | ||||||
|  |     chrome.action.setBadgeBackgroundColor({ color: "#00FF00" }) | ||||||
|  |     chrome.action.setTitle({ title: "Model pulled successfully" }) | ||||||
|  |   } else { | ||||||
|  |     chrome.action.setBadgeText({ text: "❌" }) | ||||||
|  |     chrome.action.setBadgeBackgroundColor({ color: "#FF0000" }) | ||||||
|  |     chrome.action.setTitle({ title: "Model pull failed" }) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   setTimeout(() => { | ||||||
|  |     clearBadge() | ||||||
|  |   }, 5000) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| chrome.runtime.onMessage.addListener(async (message) => { | chrome.runtime.onMessage.addListener(async (message) => { | ||||||
|   if (message.type === "sidepanel") { |   if (message.type === "sidepanel") { | ||||||
|     chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => { |     chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => { | ||||||
| @ -8,6 +83,22 @@ chrome.runtime.onMessage.addListener(async (message) => { | |||||||
|         tabId: tab.id |         tabId: tab.id | ||||||
|       }) |       }) | ||||||
|     }) |     }) | ||||||
|  |   } else if (message.type === "pull_model") { | ||||||
|  |     const ollamaURL = await getOllamaURL() | ||||||
|  | 
 | ||||||
|  |     const isRunning = await isOllamaRunning() | ||||||
|  | 
 | ||||||
|  |     if (!isRunning) { | ||||||
|  |       chrome.action.setBadgeText({ text: "E" }) | ||||||
|  |       chrome.action.setBadgeBackgroundColor({ color: "#FF0000" }) | ||||||
|  |       chrome.action.setTitle({ title: "Ollama is not running" }) | ||||||
|  |       setTimeout(() => { | ||||||
|  |         clearBadge() | ||||||
|  |       }, 5000) | ||||||
|  |     } | ||||||
|  |     console.log("Pulling model", message.modelName) | ||||||
|  | 
 | ||||||
|  |     await streamDownload(ollamaURL, message.modelName) | ||||||
|   } |   } | ||||||
| }) | }) | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -24,8 +24,8 @@ export default function Markdown({ message }: { message: string }) { | |||||||
|     <React.Fragment> |     <React.Fragment> | ||||||
|       <ReactMarkdown |       <ReactMarkdown | ||||||
|         className="prose break-words dark:prose-invert text-sm prose-p:leading-relaxed prose-pre:p-0 dark:prose-dark" |         className="prose break-words dark:prose-invert text-sm prose-p:leading-relaxed prose-pre:p-0 dark:prose-dark" | ||||||
|         // remarkPlugins={[remarkGfm, remarkMath]}
 |         remarkPlugins={[remarkGfm, remarkMath]} | ||||||
|         // rehypePlugins={[rehypeMathjax]}
 |         rehypePlugins={[rehypeMathjax]} | ||||||
|         components={{ |         components={{ | ||||||
|           code({ node, inline, className, children, ...props }) { |           code({ node, inline, className, children, ...props }) { | ||||||
|             const match = /language-(\w+)/.exec(className || "") |             const match = /language-(\w+)/.exec(className || "") | ||||||
|  | |||||||
| @ -26,7 +26,7 @@ export const PlaygroundMessage = (props: Props) => { | |||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <div |     <div | ||||||
|       className={`group w-full text-gray-800 dark:text-gray-100 border-b border-black/10 dark:border-gray-900/50 `}> |       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="text-base gap-4 md:gap-6 md:max-w-2xl lg:max-w-xl xl:max-w-3xl flex lg:px-0 m-auto w-full"> | ||||||
|         <div className="flex flex-row gap-4 md:gap-6 md:max-w-2xl lg:max-w-xl xl:max-w-3xl p-4 md:py-6 lg:px-0 m-auto w-full"> |         <div className="flex flex-row gap-4 md:gap-6 md:max-w-2xl lg:max-w-xl xl:max-w-3xl p-4 md:py-6 lg:px-0 m-auto w-full"> | ||||||
|           <div className="w-8 flex flex-col relative items-end"> |           <div className="w-8 flex flex-col relative items-end"> | ||||||
|  | |||||||
| @ -1,51 +1,20 @@ | |||||||
| import React, { Fragment, useState } from "react" | import React, { useState } from "react" | ||||||
| import { Dialog, Menu, Transition } from "@headlessui/react" | import { CogIcon } from "@heroicons/react/24/outline" | ||||||
| import { |  | ||||||
|   Bars3BottomLeftIcon, |  | ||||||
|   XMarkIcon, |  | ||||||
|   TagIcon, |  | ||||||
|   CircleStackIcon, |  | ||||||
|   CogIcon, |  | ||||||
|   ChatBubbleLeftIcon, |  | ||||||
|   Bars3Icon, |  | ||||||
|   Bars4Icon, |  | ||||||
|   ArrowPathIcon |  | ||||||
| } from "@heroicons/react/24/outline" |  | ||||||
| import logoImage from "data-base64:~assets/icon.png" |  | ||||||
| 
 | 
 | ||||||
| import { Link, useParams, useLocation, useNavigate } from "react-router-dom" | import { useLocation, NavLink } from "react-router-dom" | ||||||
| import { Sidebar } from "./Sidebar" | import { Sidebar } from "./Sidebar" | ||||||
| import { Drawer, Layout, Modal, Select } from "antd" | import { Drawer, Layout, Modal, Select, Tooltip } from "antd" | ||||||
| import { useQuery } from "@tanstack/react-query" | import { useQuery } from "@tanstack/react-query" | ||||||
| import { fetchModels } from "~services/ollama" | import { fetchModels } from "~services/ollama" | ||||||
| import { useMessageOption } from "~hooks/useMessageOption" | import { useMessageOption } from "~hooks/useMessageOption" | ||||||
| import { GithubIcon, PanelLeftIcon, Settings2, SquarePen } from "lucide-react" | import { | ||||||
|  |   GithubIcon, | ||||||
|  |   PanelLeftIcon, | ||||||
|  |   BrainCircuit, | ||||||
|  |   SquarePen, | ||||||
|  |   ChevronLeft | ||||||
|  | } from "lucide-react" | ||||||
| import { Settings } from "./Settings" | import { Settings } from "./Settings" | ||||||
| import { useDarkMode } from "~hooks/useDarkmode" |  | ||||||
| 
 |  | ||||||
| const navigation = [ |  | ||||||
|   { name: "Embed", href: "/bot/:id", icon: TagIcon }, |  | ||||||
|   { |  | ||||||
|     name: "Preview", |  | ||||||
|     href: "/bot/:id/preview", |  | ||||||
|     icon: ChatBubbleLeftIcon |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     name: "Data Sources", |  | ||||||
|     href: "/bot/:id/data-sources", |  | ||||||
|     icon: CircleStackIcon |  | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     name: "Settings", |  | ||||||
|     href: "/bot/:id/settings", |  | ||||||
|     icon: CogIcon |  | ||||||
|   } |  | ||||||
| ] |  | ||||||
| 
 |  | ||||||
| //@ts-ignore -
 |  | ||||||
| function classNames(...classes) { |  | ||||||
|   return classes.filter(Boolean).join(" ") |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| export default function OptionLayout({ | export default function OptionLayout({ | ||||||
|   children |   children | ||||||
| @ -58,19 +27,29 @@ export default function OptionLayout({ | |||||||
|   const { |   const { | ||||||
|     data: models, |     data: models, | ||||||
|     isLoading: isModelsLoading, |     isLoading: isModelsLoading, | ||||||
|     refetch: refetchModels, |  | ||||||
|     isFetching: isModelsFetching |     isFetching: isModelsFetching | ||||||
|   } = useQuery({ |   } = useQuery({ | ||||||
|     queryKey: ["fetchModel"], |     queryKey: ["fetchModel"], | ||||||
|     queryFn: fetchModels |     queryFn: fetchModels, | ||||||
|  |     refetchInterval: 15000 | ||||||
|   }) |   }) | ||||||
| 
 | 
 | ||||||
|  |   const { pathname } = useLocation() | ||||||
|   const { selectedModel, setSelectedModel, clearChat } = useMessageOption() |   const { selectedModel, setSelectedModel, clearChat } = useMessageOption() | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <Layout className="bg-white dark:bg-[#171717] md:flex"> |     <Layout className="bg-white dark:bg-[#171717] md:flex"> | ||||||
|       <div className="flex items-center p-3 fixed flex-row justify-between border-b border-gray-200 dark:border-gray-600 bg-white dark:bg-[#171717] w-full z-10"> |       <div className="flex items-center p-3 fixed flex-row justify-between border-b border-gray-200 dark:border-gray-600 bg-white dark:bg-[#171717] w-full z-10"> | ||||||
|         <div className="flex items-center flex-row gap-3"> |         <div className="flex items-center flex-row gap-3"> | ||||||
|  |           {pathname !== "/" && ( | ||||||
|  |             <div> | ||||||
|  |               <NavLink | ||||||
|  |                 to="/" | ||||||
|  |                 className="text-gray-500 items-center dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"> | ||||||
|  |                 <ChevronLeft className="w-6 h-6" /> | ||||||
|  |               </NavLink> | ||||||
|  |             </div> | ||||||
|  |           )} | ||||||
|           <div> |           <div> | ||||||
|             <button |             <button | ||||||
|               className="text-gray-500 dark:text-gray-400" |               className="text-gray-500 dark:text-gray-400" | ||||||
| @ -109,13 +88,22 @@ export default function OptionLayout({ | |||||||
|             /> |             /> | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|         <div className="flex gap-3 items-center"> |         <div className="flex gap-4 items-center"> | ||||||
|  |           <Tooltip title="Github Repository"> | ||||||
|             <a |             <a | ||||||
|               href="https://github.com/n4ze3m/page-assist" |               href="https://github.com/n4ze3m/page-assist" | ||||||
|               target="_blank" |               target="_blank" | ||||||
|             className="text-gray-500 dark:text-gray-400"> |               className="!text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"> | ||||||
|               <GithubIcon className="w-6 h-6" /> |               <GithubIcon className="w-6 h-6" /> | ||||||
|             </a> |             </a> | ||||||
|  |           </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> | ||||||
|           <button |           <button | ||||||
|             onClick={() => setOpen(true)} |             onClick={() => setOpen(true)} | ||||||
|             className="text-gray-500 dark:text-gray-400"> |             className="text-gray-500 dark:text-gray-400"> | ||||||
|  | |||||||
							
								
								
									
										223
									
								
								src/components/Option/Models/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										223
									
								
								src/components/Option/Models/index.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,223 @@ | |||||||
|  | import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" | ||||||
|  | import { Skeleton, Table, Tag, Tooltip, notification, Modal, Input } from "antd" | ||||||
|  | import { bytePerSecondFormatter } from "~libs/byte-formater" | ||||||
|  | import { deleteModel, getAllModels } from "~services/ollama" | ||||||
|  | import { Trash, RotateCcw, Download } from "lucide-react" | ||||||
|  | import dayjs from "dayjs" | ||||||
|  | import relativeTime from "dayjs/plugin/relativeTime" | ||||||
|  | import { useState } from "react" | ||||||
|  | import { useForm } from "@mantine/form" | ||||||
|  | 
 | ||||||
|  | dayjs.extend(relativeTime) | ||||||
|  | 
 | ||||||
|  | export const ModelsBody = () => { | ||||||
|  |   const queryClient = useQueryClient() | ||||||
|  |   const [open, setOpen] = useState(false) | ||||||
|  | 
 | ||||||
|  |   const form = useForm({ | ||||||
|  |     initialValues: { | ||||||
|  |       model: "" | ||||||
|  |     } | ||||||
|  |   }) | ||||||
|  | 
 | ||||||
|  |   const { data, status } = useQuery({ | ||||||
|  |     queryKey: ["fetchAllModels"], | ||||||
|  |     queryFn: getAllModels | ||||||
|  |   }) | ||||||
|  | 
 | ||||||
|  |   const { mutate: deleteOllamaModel } = useMutation({ | ||||||
|  |     mutationFn: deleteModel, | ||||||
|  |     onSuccess: () => { | ||||||
|  |       queryClient.invalidateQueries({ | ||||||
|  |         queryKey: ["fetchAllModels"] | ||||||
|  |       }) | ||||||
|  |       notification.success({ | ||||||
|  |         message: "Model Deleted", | ||||||
|  |         description: "Model has been deleted successfully" | ||||||
|  |       }) | ||||||
|  |     }, | ||||||
|  |     onError: (error) => { | ||||||
|  |       notification.error({ | ||||||
|  |         message: "Error", | ||||||
|  |         description: error?.message || "Something went wrong" | ||||||
|  |       }) | ||||||
|  |     } | ||||||
|  |   }) | ||||||
|  | 
 | ||||||
|  |   const pullModel = async (modelName: string) => { | ||||||
|  |     notification.info({ | ||||||
|  |       message: "Pulling Model", | ||||||
|  |       description: `Pulling ${modelName} model. For more details, check the extension icon.` | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     setOpen(false) | ||||||
|  | 
 | ||||||
|  |     form.reset() | ||||||
|  | 
 | ||||||
|  |     chrome.runtime.sendMessage({ | ||||||
|  |       type: "pull_model", | ||||||
|  |       modelName | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     return true | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   const { mutate: pullOllamaModel } = useMutation({ | ||||||
|  |     mutationFn: pullModel | ||||||
|  |   }) | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <div className="z-10 min-h-screen"> | ||||||
|  |       <div className="mt-16 mx-auto py-10 max-w-7xl px-3 sm:px-6 lg:px-8"> | ||||||
|  |         {/* Add new model button */} | ||||||
|  |         <div className="mb-6"> | ||||||
|  |           <div className="-ml-4 -mt-2 flex flex-wrap items-center justify-end sm:flex-nowrap"> | ||||||
|  |             <div className="ml-4 mt-2 flex-shrink-0"> | ||||||
|  |               <button | ||||||
|  |                 onClick={() => setOpen(true)} | ||||||
|  |                 className="inline-flex items-center rounded-md border border-transparent bg-black px-3 py-3 text-md font-medium leading-4 text-white shadow-sm hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-white dark:text-gray-800 dark:hover:bg-gray-100 dark:focus:ring-gray-500 dark:focus:ring-offset-gray-100 disabled:opacity-50"> | ||||||
|  |                 Add New Model | ||||||
|  |               </button> | ||||||
|  |             </div> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  | 
 | ||||||
|  |         {status === "pending" && <Skeleton paragraph={{ rows: 8 }} />} | ||||||
|  | 
 | ||||||
|  |         {status === "success" && ( | ||||||
|  |           <Table | ||||||
|  |             columns={[ | ||||||
|  |               { | ||||||
|  |                 title: "Name", | ||||||
|  |                 dataIndex: "name", | ||||||
|  |                 key: "name" | ||||||
|  |               }, | ||||||
|  |               { | ||||||
|  |                 title: "Digest", | ||||||
|  |                 dataIndex: "digest", | ||||||
|  |                 key: "digest", | ||||||
|  |                 render: (text: string) => ( | ||||||
|  |                   <Tooltip title={text}> | ||||||
|  |                     <Tag | ||||||
|  |                       className="cursor-pointer" | ||||||
|  |                       color="blue">{`${text?.slice(0, 5)}...${text?.slice(-4)}`}</Tag> | ||||||
|  |                   </Tooltip> | ||||||
|  |                 ) | ||||||
|  |               }, | ||||||
|  |               { | ||||||
|  |                 title: "Modified", | ||||||
|  |                 dataIndex: "modified_at", | ||||||
|  |                 key: "modified_at", | ||||||
|  |                 render: (text: string) => dayjs(text).fromNow(true) | ||||||
|  |               }, | ||||||
|  |               { | ||||||
|  |                 title: "Size", | ||||||
|  |                 dataIndex: "size", | ||||||
|  |                 key: "size", | ||||||
|  |                 render: (text: number) => bytePerSecondFormatter(text) | ||||||
|  |               }, | ||||||
|  |               { | ||||||
|  |                 title: "Action", | ||||||
|  |                 render: (_, record) => ( | ||||||
|  |                   <div className="flex gap-4"> | ||||||
|  |                     <Tooltip title="Delete Model"> | ||||||
|  |                       <button | ||||||
|  |                         onClick={() => { | ||||||
|  |                           if ( | ||||||
|  |                             window.confirm( | ||||||
|  |                               "Are you sure you want to delete this model?" | ||||||
|  |                             ) | ||||||
|  |                           ) { | ||||||
|  |                             deleteOllamaModel(record.model) | ||||||
|  |                           } | ||||||
|  |                         }} | ||||||
|  |                         className="text-red-500 dark:text-red-400"> | ||||||
|  |                         <Trash className="w-5 h-5" /> | ||||||
|  |                       </button> | ||||||
|  |                     </Tooltip> | ||||||
|  |                     <Tooltip title="Re-Pull Model"> | ||||||
|  |                       <button | ||||||
|  |                         onClick={() => { | ||||||
|  |                           if ( | ||||||
|  |                             window.confirm( | ||||||
|  |                               "Are you sure you want to re-pull this model?" | ||||||
|  |                             ) | ||||||
|  |                           ) { | ||||||
|  |                             pullOllamaModel(record.model) | ||||||
|  |                           } | ||||||
|  |                         }} | ||||||
|  |                         className="text-gray-500 dark:text-gray-400"> | ||||||
|  |                         <RotateCcw className="w-5 h-5" /> | ||||||
|  |                       </button> | ||||||
|  |                     </Tooltip> | ||||||
|  |                   </div> | ||||||
|  |                 ) | ||||||
|  |               } | ||||||
|  |             ]} | ||||||
|  |             expandable={{ | ||||||
|  |               expandedRowRender: (record) => ( | ||||||
|  |                 <Table | ||||||
|  |                   pagination={false} | ||||||
|  |                   columns={[ | ||||||
|  |                     { | ||||||
|  |                       title: "Parent Model", | ||||||
|  |                       key: "parent_model", | ||||||
|  |                       dataIndex: "parent_model" | ||||||
|  |                     }, | ||||||
|  |                     { | ||||||
|  |                       title: "Format", | ||||||
|  |                       key: "format", | ||||||
|  |                       dataIndex: "format" | ||||||
|  |                     }, | ||||||
|  |                     { | ||||||
|  |                       title: "Family", | ||||||
|  |                       key: "family", | ||||||
|  |                       dataIndex: "family" | ||||||
|  |                     }, | ||||||
|  |                     { | ||||||
|  |                       title: "Parameter Size", | ||||||
|  |                       key: "parameter_size", | ||||||
|  |                       dataIndex: "parameter_size" | ||||||
|  |                     }, | ||||||
|  |                     { | ||||||
|  |                       title: "Quantization Level", | ||||||
|  |                       key: "quantization_level", | ||||||
|  |                       dataIndex: "quantization_level" | ||||||
|  |                     } | ||||||
|  |                   ]} | ||||||
|  |                   dataSource={[record.details]} | ||||||
|  |                 /> | ||||||
|  |               ), | ||||||
|  |               defaultExpandAllRows: false | ||||||
|  |             }} | ||||||
|  |             bordered | ||||||
|  |             dataSource={data} | ||||||
|  |             rowKey={(record) => `${record.model}-${record.digest}`} | ||||||
|  |           /> | ||||||
|  |         )} | ||||||
|  |       </div> | ||||||
|  | 
 | ||||||
|  |       <Modal | ||||||
|  |         footer={null} | ||||||
|  |         open={open} | ||||||
|  |         title="Add New Model" | ||||||
|  |         onCancel={() => setOpen(false)}> | ||||||
|  |         <form | ||||||
|  |           onSubmit={form.onSubmit((values) => pullOllamaModel(values.model))}> | ||||||
|  |           <Input | ||||||
|  |             {...form.getInputProps("model")} | ||||||
|  |             placeholder="Enter model name" | ||||||
|  |             size="large" | ||||||
|  |           /> | ||||||
|  | 
 | ||||||
|  |           <button | ||||||
|  |             type="submit" | ||||||
|  |             className="inline-flex justify-center w-full text-center mt-4 items-center rounded-md border border-transparent bg-black px-2 py-2 text-sm font-medium leading-4 text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-white dark:text-gray-800 dark:hover:bg-gray-100 dark:focus:ring-gray-500 dark:focus:ring-offset-gray-100 disabled:opacity-50 "> | ||||||
|  |             <Download className="w-5 h-5 mr-3" /> | ||||||
|  |             Pull Model | ||||||
|  |           </button> | ||||||
|  |         </form> | ||||||
|  |       </Modal> | ||||||
|  |     </div> | ||||||
|  |   ) | ||||||
|  | } | ||||||
| @ -7,7 +7,7 @@ import XMarkIcon from "@heroicons/react/24/outline/XMarkIcon" | |||||||
| import { toBase64 } from "~libs/to-base64" | import { toBase64 } from "~libs/to-base64" | ||||||
| import { useMessageOption } from "~hooks/useMessageOption" | import { useMessageOption } from "~hooks/useMessageOption" | ||||||
| import { Tooltip } from "antd" | import { Tooltip } from "antd" | ||||||
| import { MicIcon, MicOffIcon } from "lucide-react" | import { MicIcon, StopCircleIcon } from "lucide-react" | ||||||
| import { Image } from "antd" | import { Image } from "antd" | ||||||
| import { useSpeechRecognition } from "~hooks/useSpeechRecognition" | import { useSpeechRecognition } from "~hooks/useSpeechRecognition" | ||||||
| 
 | 
 | ||||||
| @ -60,8 +60,13 @@ export const PlaygroundForm = ({ dropedFile }: Props) => { | |||||||
| 
 | 
 | ||||||
|   useDynamicTextareaSize(textareaRef, form.values.message, 300) |   useDynamicTextareaSize(textareaRef, form.values.message, 300) | ||||||
| 
 | 
 | ||||||
|   const { onSubmit, selectedModel, chatMode, speechToTextLanguage } = |   const { | ||||||
|     useMessageOption() |     onSubmit, | ||||||
|  |     selectedModel, | ||||||
|  |     chatMode, | ||||||
|  |     speechToTextLanguage, | ||||||
|  |     stopStreamingRequest | ||||||
|  |   } = useMessageOption() | ||||||
| 
 | 
 | ||||||
|   const { isListening, start, stop, transcript } = useSpeechRecognition() |   const { isListening, start, stop, transcript } = useSpeechRecognition() | ||||||
| 
 | 
 | ||||||
| @ -208,9 +213,10 @@ export const PlaygroundForm = ({ dropedFile }: Props) => { | |||||||
|                     <PhotoIcon className="h-5 w-5" /> |                     <PhotoIcon className="h-5 w-5" /> | ||||||
|                   </button> |                   </button> | ||||||
|                 </Tooltip> |                 </Tooltip> | ||||||
|  |                 {!isSending ? ( | ||||||
|                   <button |                   <button | ||||||
|                     disabled={isSending || form.values.message.length === 0} |                     disabled={isSending || form.values.message.length === 0} | ||||||
|                   className="inline-flex items-center rounded-md border border-transparent bg-black px-2 py-2 text-sm font-medium leading-4 text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-white dark:text-gray-800 dark:hover:bg-gray-100 dark:focus:ring-gray-500 dark:focus:ring-offset-gray-100 disabled:opacity-50 "> |                     className="inline-flex items-center rounded-md border border-transparent bg-black px-2 py-2 text-sm font-medium leading-4 text-white shadow-sm hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-white dark:text-gray-800 dark:hover:bg-gray-100 dark:focus:ring-gray-500 dark:focus:ring-offset-gray-100 disabled:opacity-50 "> | ||||||
|                     <svg |                     <svg | ||||||
|                       xmlns="http://www.w3.org/2000/svg" |                       xmlns="http://www.w3.org/2000/svg" | ||||||
|                       fill="none" |                       fill="none" | ||||||
| @ -225,6 +231,16 @@ export const PlaygroundForm = ({ dropedFile }: Props) => { | |||||||
|                     </svg> |                     </svg> | ||||||
|                     Send |                     Send | ||||||
|                   </button> |                   </button> | ||||||
|  |                 ) : ( | ||||||
|  |                   <Tooltip title="Stop Streaming"> | ||||||
|  |                     <button | ||||||
|  |                       type="button" | ||||||
|  |                       onClick={stopStreamingRequest} | ||||||
|  |                       className="text-gray-800 dark:text-gray-300"> | ||||||
|  |                       <StopCircleIcon className="h-6 w-6" /> | ||||||
|  |                     </button> | ||||||
|  |                   </Tooltip> | ||||||
|  |                 )} | ||||||
|               </div> |               </div> | ||||||
|             </div> |             </div> | ||||||
|           </form> |           </form> | ||||||
|  | |||||||
| @ -25,8 +25,7 @@ export const PlaygroundMessage = (props: Props) => { | |||||||
|   }, [isBtnPressed]) |   }, [isBtnPressed]) | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <div |     <div className={`group w-full text-gray-800 dark:text-gray-100 `}> | ||||||
|       className={`group w-full text-gray-800 dark:text-gray-100 border-b border-black/10 dark:border-gray-900/50 `}> |  | ||||||
|       <div className="text-base gap-4 md:gap-6 md:max-w-2xl lg:max-w-xl xl:max-w-3xl flex lg:px-0 m-auto w-full"> |       <div className="text-base gap-4 md:gap-6 md:max-w-2xl lg:max-w-xl xl:max-w-3xl flex lg:px-0 m-auto w-full"> | ||||||
|         <div className="flex flex-row gap-4 md:gap-6 md:max-w-2xl lg:max-w-xl xl:max-w-3xl p-4 md:py-6 lg:px-0 m-auto w-full"> |         <div className="flex flex-row gap-4 md:gap-6 md:max-w-2xl lg:max-w-xl xl:max-w-3xl p-4 md:py-6 lg:px-0 m-auto w-full"> | ||||||
|           <div className="w-8 flex flex-col relative items-end"> |           <div className="w-8 flex flex-col relative items-end"> | ||||||
|  | |||||||
| @ -4,7 +4,6 @@ import { useMessage } from "../../../hooks/useMessage" | |||||||
| export const PlaygroundNewChat = () => { | export const PlaygroundNewChat = () => { | ||||||
|   const { setHistory, setMessages, setHistoryId } = useMessage() |   const { setHistory, setMessages, setHistoryId } = useMessage() | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|   const handleClick = () => { |   const handleClick = () => { | ||||||
|     setHistoryId(null) |     setHistoryId(null) | ||||||
|     setMessages([]) |     setMessages([]) | ||||||
|  | |||||||
							
								
								
									
										54
									
								
								src/contents/ollama-pull.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								src/contents/ollama-pull.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,54 @@ | |||||||
|  | import type { PlasmoCSConfig } from "plasmo" | ||||||
|  | 
 | ||||||
|  | export const config: PlasmoCSConfig = { | ||||||
|  |   matches: ["*://ollama.com/library/*"], | ||||||
|  |   all_frames: true | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const downloadModel = async (modelName: string) => { | ||||||
|  |   const ok = confirm( | ||||||
|  |     `[Page Assist Extension] Do you want to pull ${modelName} model? This has nothing to do with Ollama.com website. The model will be pulled locally once you confirm.` | ||||||
|  |   ) | ||||||
|  |   if (ok) { | ||||||
|  |     alert( | ||||||
|  |       `[Page Assist Extension] Pulling ${modelName} model. For more details, check the extension icon.` | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     await chrome.runtime.sendMessage({ | ||||||
|  |       type: "pull_model", | ||||||
|  |       modelName | ||||||
|  |     }) | ||||||
|  |     return true | ||||||
|  |   } | ||||||
|  |   return false | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const downloadSVG = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="h-5 w-5 pageasssist-icon">
 | ||||||
|  | <path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" /> | ||||||
|  | </svg> | ||||||
|  | ` | ||||||
|  | const codeDiv = document.querySelectorAll("div.language-none") | ||||||
|  | 
 | ||||||
|  | for (let i = 0; i < codeDiv.length; i++) { | ||||||
|  |   const button = codeDiv[i].querySelector("button") | ||||||
|  |   const command = codeDiv[i].querySelector("input") | ||||||
|  |   if (button && command) { | ||||||
|  |     const newButton = document.createElement("button") | ||||||
|  |     newButton.innerHTML = downloadSVG | ||||||
|  |     newButton.className = `border-l ${button.className}` | ||||||
|  |     newButton.id = `download-${i}-pageassist` | ||||||
|  |     const modelName = command?.value | ||||||
|  |       .replace("ollama run", "") | ||||||
|  |       .replace("ollama pull", "") | ||||||
|  |       .trim() | ||||||
|  |     newButton.addEventListener("click", () => { | ||||||
|  |       downloadModel(modelName) | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     const span = document.createElement("span") | ||||||
|  |     span.title = "Download model via Page Assist" | ||||||
|  |     span.appendChild(newButton) | ||||||
|  | 
 | ||||||
|  |     button.parentNode.appendChild(span) | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -11,6 +11,8 @@ import { | |||||||
| } from "@langchain/core/messages" | } from "@langchain/core/messages" | ||||||
| import { useStoreMessageOption } from "~store/option" | import { useStoreMessageOption } from "~store/option" | ||||||
| import { saveHistory, saveMessage } from "~libs/db" | import { saveHistory, saveMessage } from "~libs/db" | ||||||
|  | import { useNavigate } from "react-router-dom" | ||||||
|  | import { notification } from "antd" | ||||||
| 
 | 
 | ||||||
| export type BotResponse = { | export type BotResponse = { | ||||||
|   bot: { |   bot: { | ||||||
| @ -94,6 +96,8 @@ export const useMessageOption = () => { | |||||||
|     setSpeechToTextLanguage |     setSpeechToTextLanguage | ||||||
|   } = useStoreMessageOption() |   } = useStoreMessageOption() | ||||||
| 
 | 
 | ||||||
|  |   const navigate = useNavigate() | ||||||
|  | 
 | ||||||
|   const abortControllerRef = React.useRef<AbortController | null>(null) |   const abortControllerRef = React.useRef<AbortController | null>(null) | ||||||
| 
 | 
 | ||||||
|   const clearChat = () => { |   const clearChat = () => { | ||||||
| @ -105,6 +109,7 @@ export const useMessageOption = () => { | |||||||
|     setIsLoading(false) |     setIsLoading(false) | ||||||
|     setIsProcessing(false) |     setIsProcessing(false) | ||||||
|     setStreaming(false) |     setStreaming(false) | ||||||
|  |     navigate("/") | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   const normalChatMode = async (message: string, image: string) => { |   const normalChatMode = async (message: string, image: string) => { | ||||||
| @ -249,22 +254,58 @@ export const useMessageOption = () => { | |||||||
| 
 | 
 | ||||||
|       setIsProcessing(false) |       setIsProcessing(false) | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       setIsProcessing(false) |       console.log(e) | ||||||
|       setStreaming(false) |  | ||||||
|        |        | ||||||
|       setMessages([ |       if (e?.name === "AbortError") { | ||||||
|         ...messages, |         newMessage[appendingIndex].message = newMessage[ | ||||||
|  |           appendingIndex | ||||||
|  |         ].message.slice(0, -1) | ||||||
|  | 
 | ||||||
|  |         setHistory([ | ||||||
|  |           ...history, | ||||||
|           { |           { | ||||||
|           isBot: true, |             role: "user", | ||||||
|           name: selectedModel, |             content: message, | ||||||
|           message: `Something went wrong. Check out the following logs:
 |             image | ||||||
|         \`\`\` |           }, | ||||||
|         ${e?.message} |           { | ||||||
|         \`\`\` |             role: "assistant", | ||||||
|         `,
 |             content: newMessage[appendingIndex].message | ||||||
|           sources: [] |  | ||||||
|           } |           } | ||||||
|         ]) |         ]) | ||||||
|  | 
 | ||||||
|  |         if (historyId) { | ||||||
|  |           await saveMessage(historyId, selectedModel, "user", message, [image]) | ||||||
|  |           await saveMessage( | ||||||
|  |             historyId, | ||||||
|  |             selectedModel, | ||||||
|  |             "assistant", | ||||||
|  |             newMessage[appendingIndex].message, | ||||||
|  |             [] | ||||||
|  |           ) | ||||||
|  |         } else { | ||||||
|  |           const newHistoryId = await saveHistory(message) | ||||||
|  |           await saveMessage(newHistoryId.id, selectedModel, "user", message, [ | ||||||
|  |             image | ||||||
|  |           ]) | ||||||
|  |           await saveMessage( | ||||||
|  |             newHistoryId.id, | ||||||
|  |             selectedModel, | ||||||
|  |             "assistant", | ||||||
|  |             newMessage[appendingIndex].message, | ||||||
|  |             [] | ||||||
|  |           ) | ||||||
|  |           setHistoryId(newHistoryId.id) | ||||||
|  |         } | ||||||
|  |       } else { | ||||||
|  |         notification.error({ | ||||||
|  |           message: "Error", | ||||||
|  |           description: e?.message || "Something went wrong" | ||||||
|  |         }) | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       setIsProcessing(false) | ||||||
|  |       setStreaming(false) | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										23
									
								
								src/libs/byte-formater.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/libs/byte-formater.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,23 @@ | |||||||
|  | const UNITS = [ | ||||||
|  |   "byte", | ||||||
|  |   "kilobyte", | ||||||
|  |   "megabyte", | ||||||
|  |   "gigabyte", | ||||||
|  |   "terabyte", | ||||||
|  |   "petabyte" | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | const getValueAndUnit = (n: number) => { | ||||||
|  |   const i = n == 0 ? 0 : Math.floor(Math.log(n) / Math.log(1024)) | ||||||
|  |   const value = n / Math.pow(1024, i) | ||||||
|  |   return { value, unit: UNITS[i] } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const bytePerSecondFormatter = (n: number) => { | ||||||
|  |   const { unit, value } = getValueAndUnit(n) | ||||||
|  |   return new Intl.NumberFormat("en", { | ||||||
|  |     notation: "compact", | ||||||
|  |     style: "unit", | ||||||
|  |     unit | ||||||
|  |   }).format(value) | ||||||
|  | } | ||||||
| @ -3,6 +3,7 @@ import { SidepanelChat } from "./sidepanel-chat" | |||||||
| import { useDarkMode } from "~hooks/useDarkmode" | import { useDarkMode } from "~hooks/useDarkmode" | ||||||
| import { SidepanelSettings } from "./sidepanel-settings" | import { SidepanelSettings } from "./sidepanel-settings" | ||||||
| import { OptionIndex } from "./option-index" | import { OptionIndex } from "./option-index" | ||||||
|  | import { OptionModal } from "./option-model" | ||||||
| 
 | 
 | ||||||
| export const OptionRouting = () => { | export const OptionRouting = () => { | ||||||
|   const { mode } = useDarkMode() |   const { mode } = useDarkMode() | ||||||
| @ -11,6 +12,7 @@ export const OptionRouting = () => { | |||||||
|     <div className={mode === "dark" ? "dark" : "light"}> |     <div className={mode === "dark" ? "dark" : "light"}> | ||||||
|       <Routes> |       <Routes> | ||||||
|         <Route path="/" element={<OptionIndex />} /> |         <Route path="/" element={<OptionIndex />} /> | ||||||
|  |         <Route path="/models" element={<OptionModal />} /> | ||||||
|       </Routes> |       </Routes> | ||||||
|     </div> |     </div> | ||||||
|   ) |   ) | ||||||
|  | |||||||
| @ -1,7 +1,5 @@ | |||||||
| import OptionLayout from "~components/Option/Layout" | import OptionLayout from "~components/Option/Layout" | ||||||
| import { Playground } from "~components/Option/Playground/Playground" | import { Playground } from "~components/Option/Playground/Playground" | ||||||
| import { SettingsBody } from "~components/Sidepanel/Settings/body" |  | ||||||
| import { SidepanelSettingsHeader } from "~components/Sidepanel/Settings/header" |  | ||||||
| 
 | 
 | ||||||
| export const OptionIndex = () => { | export const OptionIndex = () => { | ||||||
|   return ( |   return ( | ||||||
|  | |||||||
							
								
								
									
										10
									
								
								src/routes/option-model.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/routes/option-model.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | |||||||
|  | import OptionLayout from "~components/Option/Layout" | ||||||
|  | import { ModelsBody } from "~components/Option/Models" | ||||||
|  | 
 | ||||||
|  | export const OptionModal = () => { | ||||||
|  |   return ( | ||||||
|  |     <OptionLayout> | ||||||
|  |       <ModelsBody /> | ||||||
|  |     </OptionLayout> | ||||||
|  |   ) | ||||||
|  | } | ||||||
| @ -53,6 +53,47 @@ export const isOllamaRunning = async () => { | |||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | export const getAllModels = async () => { | ||||||
|  |   const baseUrl = await getOllamaURL() | ||||||
|  |   const response = await fetch(`${cleanUrl(baseUrl)}/api/tags`) | ||||||
|  |   if (!response.ok) { | ||||||
|  |     throw new Error(response.statusText) | ||||||
|  |   } | ||||||
|  |   const json = await response.json() | ||||||
|  | 
 | ||||||
|  |   return json.models as { | ||||||
|  |     name: string | ||||||
|  |     model: string | ||||||
|  |     modified_at: string | ||||||
|  |     size: number | ||||||
|  |     digest: string | ||||||
|  |     details: { | ||||||
|  |       parent_model: string | ||||||
|  |       format: string | ||||||
|  |       family: string | ||||||
|  |       families: string[] | ||||||
|  |       parameter_size: string | ||||||
|  |       quantization_level: string | ||||||
|  |     } | ||||||
|  |   }[] | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const deleteModel= async (model: string) => { | ||||||
|  |   const baseUrl = await getOllamaURL() | ||||||
|  |   const response = await fetch(`${cleanUrl(baseUrl)}/api/delete`, { | ||||||
|  |     method: "DELETE", | ||||||
|  |     headers: { | ||||||
|  |       "Content-Type": "application/json" | ||||||
|  |     }, | ||||||
|  |     body: JSON.stringify({ name: model }) | ||||||
|  |   }) | ||||||
|  | 
 | ||||||
|  |   if (!response.ok) { | ||||||
|  |     throw new Error(response.statusText) | ||||||
|  |   } | ||||||
|  |   return response.json() | ||||||
|  | } | ||||||
|  | 
 | ||||||
| export const fetchModels = async () => { | export const fetchModels = async () => { | ||||||
|   try { |   try { | ||||||
|     const baseUrl = await getOllamaURL() |     const baseUrl = await getOllamaURL() | ||||||
| @ -65,6 +106,17 @@ export const fetchModels = async () => { | |||||||
|     return json.models as { |     return json.models as { | ||||||
|       name: string |       name: string | ||||||
|       model: string |       model: string | ||||||
|  |       modified_at: string | ||||||
|  |       size: number | ||||||
|  |       digest: string | ||||||
|  |       details: { | ||||||
|  |         parent_model: string | ||||||
|  |         format: string | ||||||
|  |         family: string | ||||||
|  |         families: string[] | ||||||
|  |         parameter_size: string | ||||||
|  |         quantization_level: string | ||||||
|  |       } | ||||||
|     }[] |     }[] | ||||||
|   } catch (e) { |   } catch (e) { | ||||||
|     console.error(e) |     console.error(e) | ||||||
|  | |||||||
							
								
								
									
										25
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								yarn.lock
									
									
									
									
									
								
							| @ -456,14 +456,6 @@ | |||||||
|   dependencies: |   dependencies: | ||||||
|     cross-spawn "^7.0.3" |     cross-spawn "^7.0.3" | ||||||
| 
 | 
 | ||||||
| "@headlessui/react@^1.7.18": |  | ||||||
|   version "1.7.18" |  | ||||||
|   resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.7.18.tgz#30af4634d2215b2ca1aa29d07f33d02bea82d9d7" |  | ||||||
|   integrity sha512-4i5DOrzwN4qSgNsL4Si61VMkUcWbcSKueUV7sFhpHzQcSShdlHENE5+QBntMSRvHt8NyoFO2AGG8si9lq+w4zQ== |  | ||||||
|   dependencies: |  | ||||||
|     "@tanstack/react-virtual" "^3.0.0-beta.60" |  | ||||||
|     client-only "^0.0.1" |  | ||||||
| 
 |  | ||||||
| "@heroicons/react@^2.1.1": | "@heroicons/react@^2.1.1": | ||||||
|   version "2.1.1" |   version "2.1.1" | ||||||
|   resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-2.1.1.tgz#422deb80c4d6caf3371aec6f4bee8361a354dc13" |   resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-2.1.1.tgz#422deb80c4d6caf3371aec6f4bee8361a354dc13" | ||||||
| @ -2314,18 +2306,6 @@ | |||||||
|   dependencies: |   dependencies: | ||||||
|     "@tanstack/query-core" "5.18.0" |     "@tanstack/query-core" "5.18.0" | ||||||
| 
 | 
 | ||||||
| "@tanstack/react-virtual@^3.0.0-beta.60": |  | ||||||
|   version "3.0.2" |  | ||||||
|   resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.0.2.tgz#e5a979f2585d3f583944840319cddf2c2d1b0e51" |  | ||||||
|   integrity sha512-9XbRLPKgnhMwwmuQMnJMv+5a9sitGNCSEtf/AZXzmJdesYk7XsjYHaEDny+IrJzvPNwZliIIDwCRiaUqR3zzCA== |  | ||||||
|   dependencies: |  | ||||||
|     "@tanstack/virtual-core" "3.0.0" |  | ||||||
| 
 |  | ||||||
| "@tanstack/virtual-core@3.0.0": |  | ||||||
|   version "3.0.0" |  | ||||||
|   resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.0.0.tgz#637bee36f0cabf96a1d436887c90f138a7e9378b" |  | ||||||
|   integrity sha512-SYXOBTjJb05rXa2vl55TTwO40A6wKu0R5i1qQwhJYNDIqaIGF7D0HsLw+pJAyi2OvntlEIVusx3xtbbgSUi6zg== |  | ||||||
| 
 |  | ||||||
| "@tootallnate/once@2": | "@tootallnate/once@2": | ||||||
|   version "2.0.0" |   version "2.0.0" | ||||||
|   resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" |   resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" | ||||||
| @ -3098,11 +3078,6 @@ cli-width@^4.1.0: | |||||||
|   resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-4.1.0.tgz#42daac41d3c254ef38ad8ac037672130173691c5" |   resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-4.1.0.tgz#42daac41d3c254ef38ad8ac037672130173691c5" | ||||||
|   integrity sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ== |   integrity sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ== | ||||||
| 
 | 
 | ||||||
| client-only@^0.0.1: |  | ||||||
|   version "0.0.1" |  | ||||||
|   resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1" |  | ||||||
|   integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== |  | ||||||
| 
 |  | ||||||
| clone@^1.0.2: | clone@^1.0.2: | ||||||
|   version "1.0.4" |   version "1.0.4" | ||||||
|   resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" |   resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user