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",
|
||||||
@ -81,4 +81,4 @@
|
|||||||
"contextMenus"
|
"contextMenus"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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">
|
||||||
<a
|
<Tooltip title="Github Repository">
|
||||||
href="https://github.com/n4ze3m/page-assist"
|
<a
|
||||||
target="_blank"
|
href="https://github.com/n4ze3m/page-assist"
|
||||||
className="text-gray-500 dark:text-gray-400">
|
target="_blank"
|
||||||
<GithubIcon className="w-6 h-6" />
|
className="!text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
|
||||||
</a>
|
<GithubIcon className="w-6 h-6" />
|
||||||
|
</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,23 +213,34 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
|
|||||||
<PhotoIcon className="h-5 w-5" />
|
<PhotoIcon className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<button
|
{!isSending ? (
|
||||||
disabled={isSending || form.values.message.length === 0}
|
<button
|
||||||
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 ">
|
disabled={isSending || form.values.message.length === 0}
|
||||||
<svg
|
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 ">
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<svg
|
||||||
fill="none"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
stroke="currentColor"
|
fill="none"
|
||||||
strokeLinecap="round"
|
stroke="currentColor"
|
||||||
strokeLinejoin="round"
|
strokeLinecap="round"
|
||||||
strokeWidth="2"
|
strokeLinejoin="round"
|
||||||
className="h-4 w-4 mr-2"
|
strokeWidth="2"
|
||||||
viewBox="0 0 24 24">
|
className="h-4 w-4 mr-2"
|
||||||
<path d="M9 10L4 15 9 20"></path>
|
viewBox="0 0 24 24">
|
||||||
<path d="M20 4v7a4 4 0 01-4 4H4"></path>
|
<path d="M9 10L4 15 9 20"></path>
|
||||||
</svg>
|
<path d="M20 4v7a4 4 0 01-4 4H4"></path>
|
||||||
Send
|
</svg>
|
||||||
</button>
|
Send
|
||||||
|
</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) {
|
||||||
|
console.log(e)
|
||||||
|
|
||||||
|
if (e?.name === "AbortError") {
|
||||||
|
newMessage[appendingIndex].message = newMessage[
|
||||||
|
appendingIndex
|
||||||
|
].message.slice(0, -1)
|
||||||
|
|
||||||
|
setHistory([
|
||||||
|
...history,
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: message,
|
||||||
|
image
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: newMessage[appendingIndex].message
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
if (historyId) {
|
||||||
|
await saveMessage(historyId, selectedModel, "user", message, [image])
|
||||||
|
await saveMessage(
|
||||||
|
historyId,
|
||||||
|
selectedModel,
|
||||||
|
"assistant",
|
||||||
|
newMessage[appendingIndex].message,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
const newHistoryId = await saveHistory(message)
|
||||||
|
await saveMessage(newHistoryId.id, selectedModel, "user", message, [
|
||||||
|
image
|
||||||
|
])
|
||||||
|
await saveMessage(
|
||||||
|
newHistoryId.id,
|
||||||
|
selectedModel,
|
||||||
|
"assistant",
|
||||||
|
newMessage[appendingIndex].message,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
setHistoryId(newHistoryId.id)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
notification.error({
|
||||||
|
message: "Error",
|
||||||
|
description: e?.message || "Something went wrong"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
setIsProcessing(false)
|
setIsProcessing(false)
|
||||||
setStreaming(false)
|
setStreaming(false)
|
||||||
|
|
||||||
setMessages([
|
|
||||||
...messages,
|
|
||||||
{
|
|
||||||
isBot: true,
|
|
||||||
name: selectedModel,
|
|
||||||
message: `Something went wrong. Check out the following logs:
|
|
||||||
\`\`\`
|
|
||||||
${e?.message}
|
|
||||||
\`\`\`
|
|
||||||
`,
|
|
||||||
sources: []
|
|
||||||
}
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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