fix: Update entrypointsDir and manifest version in wxt.config.ts

feat: Update routing imports in options and sidepanel App components
fix: Remove unused SquarePen icon import in Header component
This commit is contained in:
n4ze3m 2024-12-27 20:16:07 +05:30
parent 93879fe5ab
commit e4357677a7
15 changed files with 617 additions and 17 deletions

View File

@ -6,7 +6,6 @@ import {
ComputerIcon,
GithubIcon,
PanelLeftIcon,
SquarePen,
ZapIcon
} from "lucide-react"
import { useTranslation } from "react-i18next"

View File

@ -0,0 +1,267 @@
import { getOllamaURL, isOllamaRunning } from "../services/ollama"
import { browser } from "wxt/browser"
import { clearBadge, streamDownload } from "@/utils/pull-ollama"
export default defineBackground({
main() {
let isCopilotRunning: boolean = false
browser.runtime.onMessage.addListener(async (message) => {
if (message.type === "sidepanel") {
await browser.sidebarAction.open()
} else if (message.type === "pull_model") {
const ollamaURL = await getOllamaURL()
const isRunning = await isOllamaRunning()
if (!isRunning) {
setBadgeText({ text: "E" })
setBadgeBackgroundColor({ color: "#FF0000" })
setTitle({ title: "Ollama is not running" })
setTimeout(() => {
clearBadge()
}, 5000)
}
await streamDownload(ollamaURL, message.modelName)
}
})
browser.runtime.onConnect.addListener((port) => {
if (port.name === "pgCopilot") {
isCopilotRunning = true
port.onDisconnect.addListener(() => {
isCopilotRunning = false
})
}
})
if (import.meta.env.BROWSER === "chrome") {
chrome.action.onClicked.addListener((tab) => {
chrome.tabs.create({ url: chrome.runtime.getURL("/options.html") })
})
} else {
browser.browserAction.onClicked.addListener((tab) => {
console.log("browser.browserAction.onClicked.addListener")
browser.tabs.create({ url: browser.runtime.getURL("/options.html") })
})
}
const contextMenuTitle = {
webUi: browser.i18n.getMessage("openOptionToChat"),
sidePanel: browser.i18n.getMessage("openSidePanelToChat")
}
const contextMenuId = {
webUi: "open-web-ui-pa",
sidePanel: "open-side-panel-pa"
}
browser.contextMenus.create({
id: contextMenuId["sidePanel"],
title: contextMenuTitle["sidePanel"],
contexts: ["page", "selection"]
})
browser.contextMenus.create({
id: "summarize-pa",
title: browser.i18n.getMessage("contextSummarize"),
contexts: ["selection"]
})
browser.contextMenus.create({
id: "explain-pa",
title: browser.i18n.getMessage("contextExplain"),
contexts: ["selection"]
})
browser.contextMenus.create({
id: "rephrase-pa",
title: browser.i18n.getMessage("contextRephrase"),
contexts: ["selection"]
})
browser.contextMenus.create({
id: "translate-pg",
title: browser.i18n.getMessage("contextTranslate"),
contexts: ["selection"]
})
browser.contextMenus.create({
id: "custom-pg",
title: browser.i18n.getMessage("contextCustom"),
contexts: ["selection"]
})
if (import.meta.env.BROWSER === "chrome") {
browser.contextMenus.onClicked.addListener(async (info, tab) => {
if (info.menuItemId === "open-side-panel-pa") {
chrome.sidePanel.open({
tabId: tab.id!
})
} else if (info.menuItemId === "open-web-ui-pa") {
browser.tabs.create({
url: browser.runtime.getURL("/options.html")
})
} else if (info.menuItemId === "summarize-pa") {
chrome.sidePanel.open({
tabId: tab.id!
})
// this is a bad method hope somone can fix it :)
setTimeout(async () => {
await browser.runtime.sendMessage({
from: "background",
type: "summary",
text: info.selectionText
})
}, isCopilotRunning ? 0 : 5000)
} else if (info.menuItemId === "rephrase-pa") {
chrome.sidePanel.open({
tabId: tab.id!
})
setTimeout(async () => {
await browser.runtime.sendMessage({
type: "rephrase",
from: "background",
text: info.selectionText
})
}, isCopilotRunning ? 0 : 5000)
} else if (info.menuItemId === "translate-pg") {
chrome.sidePanel.open({
tabId: tab.id!
})
setTimeout(async () => {
await browser.runtime.sendMessage({
type: "translate",
from: "background",
text: info.selectionText
})
}, isCopilotRunning ? 0 : 5000)
} else if (info.menuItemId === "explain-pa") {
chrome.sidePanel.open({
tabId: tab.id!
})
setTimeout(async () => {
await browser.runtime.sendMessage({
type: "explain",
from: "background",
text: info.selectionText
})
}, isCopilotRunning ? 0 : 5000)
} else if (info.menuItemId === "custom-pg") {
chrome.sidePanel.open({
tabId: tab.id!
})
setTimeout(async () => {
await browser.runtime.sendMessage({
type: "custom",
from: "background",
text: info.selectionText
})
}, isCopilotRunning ? 0 : 5000)
}
})
browser.commands.onCommand.addListener((command) => {
switch (command) {
case "execute_side_panel":
chrome.tabs.query(
{ active: true, currentWindow: true },
async (tabs) => {
const tab = tabs[0]
chrome.sidePanel.open({
tabId: tab.id!
})
}
)
break
default:
break
}
})
}
if (import.meta.env.BROWSER === "firefox") {
browser.contextMenus.onClicked.addListener((info, tab) => {
if (info.menuItemId === "open-side-panel-pa") {
browser.sidebarAction.toggle()
} else if (info.menuItemId === "open-web-ui-pa") {
browser.tabs.create({
url: browser.runtime.getURL("/options.html")
})
} else if (info.menuItemId === "summarize-pa") {
if (!isCopilotRunning) {
browser.sidebarAction.toggle()
}
setTimeout(async () => {
await browser.runtime.sendMessage({
from: "background",
type: "summary",
text: info.selectionText
})
}, isCopilotRunning ? 0 : 5000)
} else if (info.menuItemId === "rephrase-pa") {
if (!isCopilotRunning) {
browser.sidebarAction.toggle()
}
setTimeout(async () => {
await browser.runtime.sendMessage({
type: "rephrase",
from: "background",
text: info.selectionText
})
}, isCopilotRunning ? 0 : 5000)
} else if (info.menuItemId === "translate-pg") {
if (!isCopilotRunning) {
browser.sidebarAction.toggle()
}
setTimeout(async () => {
await browser.runtime.sendMessage({
type: "translate",
from: "background",
text: info.selectionText
})
}, isCopilotRunning ? 0 : 5000)
} else if (info.menuItemId === "explain-pa") {
if (!isCopilotRunning) {
browser.sidebarAction.toggle()
}
setTimeout(async () => {
await browser.runtime.sendMessage({
type: "explain",
from: "background",
text: info.selectionText
})
}, isCopilotRunning ? 0 : 5000)
} else if (info.menuItemId === "custom-pg") {
if (!isCopilotRunning) {
browser.sidebarAction.toggle()
}
setTimeout(async () => {
await browser.runtime.sendMessage({
type: "custom",
from: "background",
text: info.selectionText
})
}, isCopilotRunning ? 0 : 5000)
}
})
browser.commands.onCommand.addListener((command) => {
switch (command) {
case "execute_side_panel":
browser.sidebarAction.toggle()
break
default:
break
}
})
}
},
persistent: true
})

View File

@ -0,0 +1,95 @@
export default defineContentScript({
main(ctx) {
const downloadModel = async (modelName: string) => {
const ok = confirm(
`[Page Assist Extension] Do you want to pull the ${modelName} model? This has nothing to do with the huggingface.co website. The model will be pulled locally once you confirm. Make sure Ollama is running.`
)
if (ok) {
alert(
`[Page Assist Extension] Pulling ${modelName} model. For more details, check the extension icon.`
)
await browser.runtime.sendMessage({
type: "pull_model",
modelName
})
return true
}
return false
}
const downloadSVG = `
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 24 24" width="16" height="16">
<path d="M12 16l-6-6h4V4h4v6h4l-6 6z"/>
<path d="M4 20h16v-2H4v2z"/>
</svg>
`
const injectDownloadButton = (modal: HTMLElement) => {
const copyButton = modal.querySelector(
'button[title="Copy snippet to clipboard"]'
)
if (copyButton && !modal.querySelector(".pageassist-download-button")) {
const downloadButton = copyButton.cloneNode(true) as HTMLElement
downloadButton.classList.add("pageassist-download-button")
downloadButton.querySelector("svg")!.outerHTML = downloadSVG
downloadButton.querySelector("span")!.textContent =
"Pull from Page Assist"
downloadButton.addEventListener("click", async () => {
const preElement = modal.querySelector("pre")
if (preElement) {
let modelCommand = ""
preElement.childNodes.forEach((node) => {
if (node.nodeType === Node.TEXT_NODE) {
modelCommand += node.textContent
} else if (node instanceof HTMLSelectElement) {
modelCommand += node.value
} else if (node instanceof HTMLElement) {
const selectElement = node.querySelector(
"select"
) as HTMLSelectElement
if (selectElement) {
modelCommand += selectElement.value
} else {
modelCommand += node.textContent
}
}
})
modelCommand = modelCommand.trim()
await downloadModel(
modelCommand
?.replaceAll("ollama run", "")
?.replaceAll("ollama pull", "")
?.trim()
)
}
})
const buttonContainer = document.createElement('div')
buttonContainer.classList.add("mb-3")
buttonContainer.style.display = 'flex'
buttonContainer.style.justifyContent = 'flex-end'
buttonContainer.appendChild(downloadButton)
modal.querySelector("pre")!.insertAdjacentElement("afterend", buttonContainer)
}
}
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
mutation.addedNodes.forEach((node) => {
if (node instanceof HTMLElement) {
const modal = node.querySelector(".shadow-alternate") as HTMLElement
if (modal) {
injectDownloadButton(modal)
}
}
})
}
})
observer.observe(document.body, { childList: true, subtree: true })
},
allFrames: true,
matches: ["*://huggingface.co/*"]
})

View File

@ -0,0 +1,57 @@
export default defineContentScript({
main(ctx) {
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 browser.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)
if (button.parentNode) {
button.parentNode.appendChild(span)
}
}
}
},
allFrames: true,
matches: ["*://ollama.com/*"],
})

View File

@ -0,0 +1,57 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { MemoryRouter } from "react-router-dom"
import { useEffect, useState } from "react"
const queryClient = new QueryClient()
import { ConfigProvider, Empty, theme } from "antd"
import { StyleProvider } from "@ant-design/cssinjs"
import { useDarkMode } from "~/hooks/useDarkmode"
import { OptionRouting } from "@/routes/firefox-route"
import "~/i18n"
import { useTranslation } from "react-i18next"
import { PageAssistProvider } from "@/components/Common/PageAssistProvider"
function IndexOption() {
const { mode } = useDarkMode()
const { t, i18n } = useTranslation()
const [direction, setDirection] = useState<"ltr" | "rtl">("ltr")
useEffect(() => {
if (i18n.resolvedLanguage) {
document.documentElement.lang = i18n.resolvedLanguage
document.documentElement.dir = i18n.dir(i18n.resolvedLanguage)
setDirection(i18n.dir(i18n.resolvedLanguage))
}
}, [i18n, i18n.resolvedLanguage])
return (
<MemoryRouter>
<ConfigProvider
theme={{
algorithm:
mode === "dark" ? theme.darkAlgorithm : theme.defaultAlgorithm,
token: {
fontFamily: "Arimo"
}
}}
renderEmpty={() => (
<Empty
imageStyle={{
height: 60
}}
description={t("common:noData")}
/>
)}
direction={direction}>
<StyleProvider hashPriority="high">
<QueryClientProvider client={queryClient}>
<PageAssistProvider>
<OptionRouting />
</PageAssistProvider>
</QueryClientProvider>
</StyleProvider>
</ConfigProvider>
</MemoryRouter>
)
}
export default IndexOption

View File

@ -0,0 +1,15 @@
<!doctype html>
<html>
<head>
<title>Page Assist - A Web UI for Local AI Models</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="manifest.type" content="browser_action" />
<meta name="manifest.browser_style" content="false" />
<link href="~/assets/tailwind.css" rel="stylesheet" />
<meta charset="utf-8" />
</head>
<body class="bg-white dark:bg-[#171717]">
<div id="root"></div>
<script type="module" src="./main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,10 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import IndexOption from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<IndexOption />
</React.StrictMode>,
);

View File

@ -0,0 +1,54 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { MemoryRouter } from "react-router-dom"
import { useEffect } from "react"
import { SidepanelRouting } from "@/routes/firefox-route"
const queryClient = new QueryClient()
import { ConfigProvider, Empty, theme } from "antd"
import { StyleProvider } from "@ant-design/cssinjs"
import { useDarkMode } from "~/hooks/useDarkmode"
import "~/i18n"
import { useTranslation } from "react-i18next"
import { PageAssistProvider } from "@/components/Common/PageAssistProvider"
function IndexSidepanel() {
const { mode } = useDarkMode()
const { t, i18n } = useTranslation()
useEffect(() => {
if (i18n.resolvedLanguage) {
document.documentElement.lang = i18n.resolvedLanguage;
document.documentElement.dir = i18n.dir(i18n.resolvedLanguage);
}
}, [i18n, i18n.resolvedLanguage]);
return (
<MemoryRouter>
<ConfigProvider
theme={{
algorithm:
mode === "dark" ? theme.darkAlgorithm : theme.defaultAlgorithm,
token: {
fontFamily: "Arimo"
}
}}
renderEmpty={() => (
<Empty
imageStyle={{
height: 60
}}
description={t("common:noData")}
/>
)}>
<StyleProvider hashPriority="high">
<QueryClientProvider client={queryClient}>
<PageAssistProvider>
<SidepanelRouting />
</PageAssistProvider>
</QueryClientProvider>
</StyleProvider>
</ConfigProvider>
</MemoryRouter>
)
}
export default IndexSidepanel

View File

@ -0,0 +1,16 @@
<!doctype html>
<html>
<head>
<title>Page Assist - A Web UI for Local AI Models</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="manifest.type" content="browser_action" />
<meta name="manifest.open_at_install" content="false" />
<meta name="manifest.browser_style" content="false" />
<link href="~/assets/tailwind.css" rel="stylesheet" />
<meta charset="utf-8" />
</head>
<body class="bg-white dark:bg-[#171717]">
<div id="root"></div>
<script type="module" src="./main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from "react"
import ReactDOM from "react-dom/client"
import IndexSidepanel from "./App"
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<IndexSidepanel />
</React.StrictMode>
)

View File

@ -5,7 +5,7 @@ const queryClient = new QueryClient()
import { ConfigProvider, Empty, theme } from "antd"
import { StyleProvider } from "@ant-design/cssinjs"
import { useDarkMode } from "~/hooks/useDarkmode"
import { OptionRouting } from "~/routes"
import { OptionRouting } from "@/routes/chrome-route"
import "~/i18n"
import { useTranslation } from "react-i18next"
import { PageAssistProvider } from "@/components/Common/PageAssistProvider"

View File

@ -1,7 +1,7 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { MemoryRouter } from "react-router-dom"
import { useEffect } from "react"
import { SidepanelRouting } from "~/routes"
import { SidepanelRouting } from "@/routes/chrome-route"
const queryClient = new QueryClient()
import { ConfigProvider, Empty, theme } from "antd"
import { StyleProvider } from "@ant-design/cssinjs"

View File

@ -0,0 +1,28 @@
import { Suspense } from "react"
import { useDarkMode } from "~/hooks/useDarkmode"
import { OptionRoutingChrome, SidepanelRoutingChrome } from "./chrome"
import { PageAssistLoader } from "@/components/Common/PageAssistLoader"
export const OptionRouting = () => {
const { mode } = useDarkMode()
return (
<div className={`${mode === "dark" ? "dark" : "light"} arimo`}>
<Suspense fallback={<PageAssistLoader />}>
<OptionRoutingChrome />
</Suspense>
</div>
)
}
export const SidepanelRouting = () => {
const { mode } = useDarkMode()
return (
<div className={`${mode === "dark" ? "dark" : "light"} arimo`}>
<Suspense fallback={<PageAssistLoader />}>
<SidepanelRoutingChrome />
</Suspense>
</div>
)
}

View File

@ -1,22 +1,16 @@
import { Suspense } from "react"
import { useTranslation } from "react-i18next"
import { useDarkMode } from "~/hooks/useDarkmode"
import { OptionRoutingChrome, SidepanelRoutingChrome } from "./chrome"
import { OptionRoutingFirefox, SidepanelRoutingFirefox } from "./firefox"
import { PageAssistLoader } from "@/components/Common/PageAssistLoader"
export const OptionRouting = () => {
const { mode } = useDarkMode()
const { i18n } = useTranslation()
return (
<div className={`${mode === "dark" ? "dark" : "light"} arimo`}>
<Suspense fallback={<PageAssistLoader />}>
{import.meta.env.BROWSER === "chrome" ? (
<OptionRoutingChrome />
) : (
<OptionRoutingFirefox />
)}
</Suspense>
</div>
)
@ -24,16 +18,12 @@ export const OptionRouting = () => {
export const SidepanelRouting = () => {
const { mode } = useDarkMode()
const { i18n } = useTranslation()
return (
<div className={`${mode === "dark" ? "dark" : "light"} arimo`}>
<Suspense fallback={<PageAssistLoader />}>
{import.meta.env.BROWSER === "chrome" ? (
<SidepanelRoutingChrome />
) : (
<SidepanelRoutingFirefox />
)}
</Suspense>
</div>
)

View File

@ -45,12 +45,15 @@ export default defineConfig({
}
}
}),
entrypointsDir: "entries",
entrypointsDir:
process.env.TARGET === "firefox" ?
"entries-firefox" :
"entries",
srcDir: "src",
outDir: "build",
manifest: {
version: "1.3.9",
version: "1.3.10",
name:
process.env.TARGET === "firefox"
? "Page Assist - A Web UI for Local AI Models"