diff --git a/src/components/Layouts/Header.tsx b/src/components/Layouts/Header.tsx
index 0001ab7..b15a7d7 100644
--- a/src/components/Layouts/Header.tsx
+++ b/src/components/Layouts/Header.tsx
@@ -6,7 +6,6 @@ import {
ComputerIcon,
GithubIcon,
PanelLeftIcon,
- SquarePen,
ZapIcon
} from "lucide-react"
import { useTranslation } from "react-i18next"
diff --git a/src/entries-firefox/background.ts b/src/entries-firefox/background.ts
new file mode 100644
index 0000000..9a9bcd5
--- /dev/null
+++ b/src/entries-firefox/background.ts
@@ -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
+})
diff --git a/src/entries-firefox/hf-pull.content.ts b/src/entries-firefox/hf-pull.content.ts
new file mode 100644
index 0000000..ac44737
--- /dev/null
+++ b/src/entries-firefox/hf-pull.content.ts
@@ -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 = `
+
+ `
+
+ 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/*"]
+})
diff --git a/src/entries-firefox/ollama-pull.content.ts b/src/entries-firefox/ollama-pull.content.ts
new file mode 100644
index 0000000..89250e3
--- /dev/null
+++ b/src/entries-firefox/ollama-pull.content.ts
@@ -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 = `
+ `
+ 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/*"],
+
+})
\ No newline at end of file
diff --git a/src/entries-firefox/options/App.tsx b/src/entries-firefox/options/App.tsx
new file mode 100644
index 0000000..fceed88
--- /dev/null
+++ b/src/entries-firefox/options/App.tsx
@@ -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 (
+