From e4357677a73e9159fe22eaebbb5f717dbd270ae9 Mon Sep 17 00:00:00 2001 From: n4ze3m Date: Fri, 27 Dec 2024 20:16:07 +0530 Subject: [PATCH] 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 --- src/components/Layouts/Header.tsx | 1 - src/entries-firefox/background.ts | 267 ++++++++++++++++++++ src/entries-firefox/hf-pull.content.ts | 95 +++++++ src/entries-firefox/ollama-pull.content.ts | 57 +++++ src/entries-firefox/options/App.tsx | 57 +++++ src/entries-firefox/options/index.html | 15 ++ src/entries-firefox/options/main.tsx | 10 + src/entries-firefox/sidepanel/App.tsx | 54 ++++ src/entries-firefox/sidepanel/index.html | 16 ++ src/entries-firefox/sidepanel/main.tsx | 9 + src/entries/options/App.tsx | 2 +- src/entries/sidepanel/App.tsx | 2 +- src/routes/chrome-route.tsx | 28 ++ src/routes/{index.tsx => firefox-route.tsx} | 14 +- wxt.config.ts | 7 +- 15 files changed, 617 insertions(+), 17 deletions(-) create mode 100644 src/entries-firefox/background.ts create mode 100644 src/entries-firefox/hf-pull.content.ts create mode 100644 src/entries-firefox/ollama-pull.content.ts create mode 100644 src/entries-firefox/options/App.tsx create mode 100644 src/entries-firefox/options/index.html create mode 100644 src/entries-firefox/options/main.tsx create mode 100644 src/entries-firefox/sidepanel/App.tsx create mode 100644 src/entries-firefox/sidepanel/index.html create mode 100644 src/entries-firefox/sidepanel/main.tsx create mode 100644 src/routes/chrome-route.tsx rename src/routes/{index.tsx => firefox-route.tsx} (65%) 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 ( + + ( + + )} + direction={direction}> + + + + + + + + + + ) +} + +export default IndexOption diff --git a/src/entries-firefox/options/index.html b/src/entries-firefox/options/index.html new file mode 100644 index 0000000..c30dfad --- /dev/null +++ b/src/entries-firefox/options/index.html @@ -0,0 +1,15 @@ + + + + Page Assist - A Web UI for Local AI Models + + + + + + + +
+ + + diff --git a/src/entries-firefox/options/main.tsx b/src/entries-firefox/options/main.tsx new file mode 100644 index 0000000..9e68814 --- /dev/null +++ b/src/entries-firefox/options/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import IndexOption from './App'; + + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/src/entries-firefox/sidepanel/App.tsx b/src/entries-firefox/sidepanel/App.tsx new file mode 100644 index 0000000..e1cfafe --- /dev/null +++ b/src/entries-firefox/sidepanel/App.tsx @@ -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 ( + + ( + + )}> + + + + + + + + + + ) +} + +export default IndexSidepanel diff --git a/src/entries-firefox/sidepanel/index.html b/src/entries-firefox/sidepanel/index.html new file mode 100644 index 0000000..1e7c405 --- /dev/null +++ b/src/entries-firefox/sidepanel/index.html @@ -0,0 +1,16 @@ + + + + Page Assist - A Web UI for Local AI Models + + + + + + + + +
+ + + diff --git a/src/entries-firefox/sidepanel/main.tsx b/src/entries-firefox/sidepanel/main.tsx new file mode 100644 index 0000000..16cfcf7 --- /dev/null +++ b/src/entries-firefox/sidepanel/main.tsx @@ -0,0 +1,9 @@ +import React from "react" +import ReactDOM from "react-dom/client" +import IndexSidepanel from "./App" + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + +) diff --git a/src/entries/options/App.tsx b/src/entries/options/App.tsx index 997e1de..a0cb324 100644 --- a/src/entries/options/App.tsx +++ b/src/entries/options/App.tsx @@ -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" diff --git a/src/entries/sidepanel/App.tsx b/src/entries/sidepanel/App.tsx index 3abbf33..26aa58a 100644 --- a/src/entries/sidepanel/App.tsx +++ b/src/entries/sidepanel/App.tsx @@ -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" diff --git a/src/routes/chrome-route.tsx b/src/routes/chrome-route.tsx new file mode 100644 index 0000000..73ffda8 --- /dev/null +++ b/src/routes/chrome-route.tsx @@ -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 ( +
+ }> + + +
+ ) +} + +export const SidepanelRouting = () => { + const { mode } = useDarkMode() + + return ( +
+ }> + + +
+ ) +} diff --git a/src/routes/index.tsx b/src/routes/firefox-route.tsx similarity index 65% rename from src/routes/index.tsx rename to src/routes/firefox-route.tsx index 2a4dd60..456ba34 100644 --- a/src/routes/index.tsx +++ b/src/routes/firefox-route.tsx @@ -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 (
}> - {import.meta.env.BROWSER === "chrome" ? ( - - ) : ( + - )}
) @@ -24,16 +18,12 @@ export const OptionRouting = () => { export const SidepanelRouting = () => { const { mode } = useDarkMode() - const { i18n } = useTranslation() return (
}> - {import.meta.env.BROWSER === "chrome" ? ( - - ) : ( + - )}
) diff --git a/wxt.config.ts b/wxt.config.ts index 69c86bd..bb7ab51 100644 --- a/wxt.config.ts +++ b/wxt.config.ts @@ -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"