diff --git a/.gitignore b/.gitignore index c76b5c4..a396c30 100644 --- a/.gitignore +++ b/.gitignore @@ -42,4 +42,7 @@ keys.json # typescript .tsbuildinfo -.wxt \ No newline at end of file +# WXT +.wxt +# WebStorm +.idea \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index a4deaa6..11bd10c 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 98f5370..ec33b1b 100644 --- a/package.json +++ b/package.json @@ -5,12 +5,12 @@ "description": "Use your locally running AI models to assist you in your web browsing.", "author": "n4ze3m", "scripts": { - "dev": "wxt", - "dev:firefox": "wxt -b firefox", - "build": "wxt build", - "build:firefox": "wxt build -b firefox", - "zip": "wxt zip", - "zip:firefox": "wxt zip -b firefox", + "dev": "cross-env TARGET=chrome wxt", + "dev:firefox": "cross-env TARGET=firefox wxt -b firefox", + "build": "cross-env TARGET=chrome wxt build", + "build:firefox": "cross-env TARGET=chrome cross-env TARGET=firefox wxt build -b firefox", + "zip": "cross-env TARGET=chrome wxt zip", + "zip:firefox": "cross-env TARGET=firefox wxt zip -b firefox", "compile": "tsc --noEmit", "postinstall": "wxt prepare" }, @@ -66,6 +66,7 @@ "@types/react-syntax-highlighter": "^15.5.11", "@types/turndown": "^5.0.4", "autoprefixer": "^10.4.17", + "cross-env": "^7.0.3", "postcss": "^8.4.33", "prettier": "3.2.4", "tailwindcss": "^3.4.1", diff --git a/page-share.md b/page-share.md index f0646f2..a0b43d4 100644 --- a/page-share.md +++ b/page-share.md @@ -23,7 +23,7 @@ Click the button below to deploy the code to Railway. ```bash git clone https://github.com/n4ze3m/page-share-app.git -cd page-assist-app +cd page-share-app ``` 2. Run the server diff --git a/src/components/Option/Models/index.tsx b/src/components/Option/Models/index.tsx index 7728484..e342edc 100644 --- a/src/components/Option/Models/index.tsx +++ b/src/components/Option/Models/index.tsx @@ -60,7 +60,7 @@ export const ModelsBody = () => { form.reset() - chrome.runtime.sendMessage({ + browser.runtime.sendMessage({ type: "pull_model", modelName }) diff --git a/src/components/Option/Settings/about.tsx b/src/components/Option/Settings/about.tsx index 024bf82..6250ab1 100644 --- a/src/components/Option/Settings/about.tsx +++ b/src/components/Option/Settings/about.tsx @@ -11,7 +11,7 @@ export const AboutApp = () => { const { data, status } = useQuery({ queryKey: ["fetchOllamURL"], queryFn: async () => { - const chromeVersion = chrome.runtime.getManifest().version + const chromeVersion = browser.runtime.getManifest().version try { const url = await getOllamaURL() const req = await fetch(`${cleanUrl(url)}/api/version`) diff --git a/src/db/index.ts b/src/db/index.ts index 6ca341f..f709905 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -455,4 +455,4 @@ export const importPrompts = async (prompts: Prompts) => { for (const prompt of prompts) { await db.addPrompt(prompt) } -} +} \ No newline at end of file diff --git a/src/db/knowledge.ts b/src/db/knowledge.ts index 9579536..3f90880 100644 --- a/src/db/knowledge.ts +++ b/src/db/knowledge.ts @@ -202,4 +202,4 @@ export const importKnowledge = async (data: Knowledge[]) => { for (const d of data) { await db.create(d) } -} +} \ No newline at end of file diff --git a/src/db/vector.ts b/src/db/vector.ts index 09624fc..f5dd141 100644 --- a/src/db/vector.ts +++ b/src/db/vector.ts @@ -165,4 +165,4 @@ export const exportVectors = async () => { export const importVectors = async (data: VectorData[]) => { const db = new PageAssistVectorDb() return db.saveImportedData(data) -} +} \ No newline at end of file diff --git a/src/entries/background.ts b/src/entries/background.ts index f671467..91c71a7 100644 --- a/src/entries/background.ts +++ b/src/entries/background.ts @@ -1,13 +1,13 @@ import { getOllamaURL, isOllamaRunning } from "../services/ollama" -import { Storage } from "@plasmohq/storage" - +import { browser } from "wxt/browser" +import { setBadgeBackgroundColor, setBadgeText, setTitle } from "@/utils/action" const progressHuman = (completed: number, total: number) => { return ((completed / total) * 100).toFixed(0) + "%" } const clearBadge = () => { - chrome.action.setBadgeText({ text: "" }) - chrome.action.setTitle({ title: "" }) + setBadgeText({ text: "" }) + setTitle({ title: "" }) } const streamDownload = async (url: string, model: string) => { url += "/api/pull" @@ -42,16 +42,16 @@ const streamDownload = async (url: string, model: string) => { completed?: number } if (json.total && json.completed) { - chrome.action.setBadgeText({ + setBadgeText({ text: progressHuman(json.completed, json.total) }) - chrome.action.setBadgeBackgroundColor({ color: "#0000FF" }) + setBadgeBackgroundColor({ color: "#0000FF" }) } else { - chrome.action.setBadgeText({ text: "🏋️‍♂️" }) - chrome.action.setBadgeBackgroundColor({ color: "#FFFFFF" }) + setBadgeText({ text: "🏋️‍♂️" }) + setBadgeBackgroundColor({ color: "#FFFFFF" }) } - chrome.action.setTitle({ title: json.status }) + setTitle({ title: json.status }) if (json.status === "success") { isSuccess = true @@ -62,13 +62,13 @@ const streamDownload = async (url: string, model: string) => { } if (isSuccess) { - chrome.action.setBadgeText({ text: "✅" }) - chrome.action.setBadgeBackgroundColor({ color: "#00FF00" }) - chrome.action.setTitle({ title: "Model pulled successfully" }) + setBadgeText({ text: "✅" }) + setBadgeBackgroundColor({ color: "#00FF00" }) + setTitle({ title: "Model pulled successfully" }) } else { - chrome.action.setBadgeText({ text: "❌" }) - chrome.action.setBadgeBackgroundColor({ color: "#FF0000" }) - chrome.action.setTitle({ title: "Model pull failed" }) + setBadgeText({ text: "❌" }) + setBadgeBackgroundColor({ color: "#FF0000" }) + setTitle({ title: "Model pull failed" }) } setTimeout(() => { @@ -77,29 +77,18 @@ const streamDownload = async (url: string, model: string) => { } export default defineBackground({ main() { - const storage = new Storage() - - chrome.runtime.onMessage.addListener(async (message) => { + browser.runtime.onMessage.addListener(async (message) => { if (message.type === "sidepanel") { - chrome.tabs.query( - { active: true, currentWindow: true }, - async (tabs) => { - const tab = tabs[0] - chrome.sidePanel.open({ - // tabId: tab.id!, - windowId: tab.windowId! - }) - } - ) + browser.sidebarAction.open() } 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" }) + setBadgeText({ text: "E" }) + setBadgeBackgroundColor({ color: "#FF0000" }) + setTitle({ title: "Ollama is not running" }) setTimeout(() => { clearBadge() }, 5000) @@ -109,47 +98,73 @@ export default defineBackground({ } }) - chrome.action.onClicked.addListener((tab) => { - chrome.tabs.create({ url: chrome.runtime.getURL("options.html") }) - }) + if (import.meta.env.BROWSER === "chrome") { + chrome.action.onClicked.addListener((tab) => { + browser.tabs.create({ url: browser.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") }) + }) + } - chrome.commands.onCommand.addListener((command) => { - switch (command) { - case "execute_side_panel": + browser.contextMenus.create({ + id: "open-side-panel-pa", + title: browser.i18n.getMessage("openSidePanelToChat"), + contexts: ["all"] + }) + if (import.meta.env.BROWSER === "chrome") { + browser.contextMenus.onClicked.addListener((info, tab) => { + if (info.menuItemId === "open-side-panel-pa") { chrome.tabs.query( { active: true, currentWindow: true }, async (tabs) => { const tab = tabs[0] chrome.sidePanel.open({ - windowId: tab.windowId! + tabId: tab.id! }) } ) - break - default: - break - } - }) + } + }) - chrome.contextMenus.create({ - id: "open-side-panel-pa", - title: browser.i18n.getMessage("openSidePanelToChat"), - contexts: ["all"] - }) + 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 + } + }) + } - chrome.contextMenus.onClicked.addListener((info, tab) => { - if (info.menuItemId === "open-side-panel-pa") { - chrome.tabs.query( - { active: true, currentWindow: true }, - async (tabs) => { - const tab = tabs[0] - chrome.sidePanel.open({ - tabId: tab.id! - }) - } - ) - } - }) + if (import.meta.env.BROWSER === "firefox") { + browser.contextMenus.onClicked.addListener((info, tab) => { + if (info.menuItemId === "open-side-panel-pa") { + browser.sidebarAction.toggle() + } + }) + + browser.commands.onCommand.addListener((command) => { + switch (command) { + case "execute_side_panel": + browser.sidebarAction.toggle() + break + default: + break + } + }) + } }, persistent: true }) diff --git a/src/entries/ollama-pull.content.ts b/src/entries/ollama-pull.content.ts index 2740f44..6acbccd 100644 --- a/src/entries/ollama-pull.content.ts +++ b/src/entries/ollama-pull.content.ts @@ -9,7 +9,7 @@ export default defineContentScript({ `[Page Assist Extension] Pulling ${modelName} model. For more details, check the extension icon.` ) - await chrome.runtime.sendMessage({ + await browser.runtime.sendMessage({ type: "pull_model", modelName }) diff --git a/src/entries/sidepanel/index.html b/src/entries/sidepanel/index.html index 2abb927..f18de84 100644 --- a/src/entries/sidepanel/index.html +++ b/src/entries/sidepanel/index.html @@ -4,6 +4,7 @@ Page Assist - A Web UI for Local AI Models + diff --git a/src/hooks/useTTS.tsx b/src/hooks/useTTS.tsx index 075f005..5274956 100644 --- a/src/hooks/useTTS.tsx +++ b/src/hooks/useTTS.tsx @@ -2,7 +2,6 @@ import { useEffect, useState } from "react" import { notification } from "antd" import { getVoice, isSSMLEnabled } from "@/services/tts" import { markdownToSSML } from "@/utils/markdown-to-ssml" - type VoiceOptions = { utterance: string } @@ -17,16 +16,28 @@ export const useTTS = () => { if (isSSML) { utterance = markdownToSSML(utterance) } - chrome.tts.speak(utterance, { - voiceName: voice, - onEvent(event) { - if (event.type === "start") { - setIsSpeaking(true) - } else if (event.type === "end") { - setIsSpeaking(false) + if (import.meta.env.BROWSER === "chrome") { + chrome.tts.speak(utterance, { + voiceName: voice, + onEvent(event) { + if (event.type === "start") { + setIsSpeaking(true) + } else if (event.type === "end") { + setIsSpeaking(false) + } } + }) + } else { + // browser tts + window.speechSynthesis.speak(new SpeechSynthesisUtterance(utterance)) + window.speechSynthesis.onvoiceschanged = () => { + const voices = window.speechSynthesis.getVoices() + const voice = voices.find((v) => v.name === voice) + const utter = new SpeechSynthesisUtterance(utterance) + utter.voice = voice + window.speechSynthesis.speak(utter) } - }) + } } catch (error) { notification.error({ message: "Error", @@ -36,7 +47,11 @@ export const useTTS = () => { } const cancel = () => { - chrome.tts.stop() + if (import.meta.env.BROWSER === "chrome") { + chrome.tts.stop() + } else { + window.speechSynthesis.cancel() + } setIsSpeaking(false) } diff --git a/src/libs/get-html.ts b/src/libs/get-html.ts index 40499d4..ddb2ad7 100644 --- a/src/libs/get-html.ts +++ b/src/libs/get-html.ts @@ -4,7 +4,7 @@ import { isTweet, isTwitterTimeline, parseTweet, - parseTwitterTimeline, + parseTwitterTimeline } from "@/parser/twitter" import { isGoogleDocs, parseGoogleDocs } from "@/parser/google-docs" import { cleanUnwantedUnicode } from "@/utils/clean" @@ -24,18 +24,35 @@ const _getHtml = () => { export const getDataFromCurrentTab = async () => { const result = new Promise((resolve) => { - chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => { - const tab = tabs[0] + if (import.meta.env.BROWSER === "chrome") { + chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => { + const tab = tabs[0] - const data = await chrome.scripting.executeScript({ - target: { tabId: tab.id }, - func: _getHtml + const data = await chrome.scripting.executeScript({ + target: { tabId: tab.id }, + func: _getHtml + }) + + if (data.length > 0) { + resolve(data[0].result) + } }) + } else { + browser.tabs + .query({ active: true, currentWindow: true }) + .then(async (tabs) => { + const tab = tabs[0] - if (data.length > 0) { - resolve(data[0].result) - } - }) + const data = await browser.scripting.executeScript({ + target: { tabId: tab.id }, + func: _getHtml + }) + + if (data.length > 0) { + resolve(data[0].result) + } + }) + } }) as Promise<{ url: string content: string diff --git a/src/libs/runtime.ts b/src/libs/runtime.ts index 8f55cc0..ce447f6 100644 --- a/src/libs/runtime.ts +++ b/src/libs/runtime.ts @@ -1,31 +1,51 @@ export const chromeRunTime = async function (domain: string) { - if (typeof chrome !== "undefined" && chrome.runtime && chrome.runtime.id) { - const url = new URL(domain) - const domains = [url.hostname] - const rules = [ - { - id: 1, - priority: 1, - condition: { - requestDomains: domains - }, - action: { - type: "modifyHeaders", - requestHeaders: [ - { - header: "Origin", - operation: "set", - value: `${url.protocol}//${url.hostname}` - } - ] + if (browser.runtime && browser.runtime.id) { + if (import.meta.env.BROWSER === "chrome") { + const url = new URL(domain) + const domains = [url.hostname] + const rules = [ + { + id: 1, + priority: 1, + condition: { + requestDomains: domains + }, + action: { + type: "modifyHeaders", + requestHeaders: [ + { + header: "Origin", + operation: "set", + value: `${url.protocol}//${url.hostname}` + } + ] + } } - } - ] + ] - await chrome.declarativeNetRequest.updateDynamicRules({ - removeRuleIds: rules.map((r) => r.id), - // @ts-ignore - addRules: rules - }) + await browser.declarativeNetRequest.updateDynamicRules({ + removeRuleIds: rules.map((r) => r.id), + // @ts-ignore + addRules: rules + }) + } + + if (import.meta.env.BROWSER === "firefox") { + const url = new URL(domain) + const domains = [`*://${url.hostname}/*`] + browser.webRequest.onBeforeSendHeaders.addListener( + (details) => { + for (let i = 0; i < details.requestHeaders.length; i++) { + if (details.requestHeaders[i].name === "Origin") { + details.requestHeaders[i].value = + `${url.protocol}//${url.hostname}` + } + } + return { requestHeaders: details.requestHeaders } + }, + { urls: domains }, + ["blocking", "requestHeaders"] + ) + } } } diff --git a/src/parser/google-docs.ts b/src/parser/google-docs.ts index 23e7316..0781396 100644 --- a/src/parser/google-docs.ts +++ b/src/parser/google-docs.ts @@ -1,7 +1,6 @@ - export const isGoogleDocs = (url: string) => { - const GOOGLE_DOCS_REGEX = /docs\.google\.com\/document/g - return GOOGLE_DOCS_REGEX.test(url) + const GOOGLE_DOCS_REGEX = /docs\.google\.com\/document/g + return GOOGLE_DOCS_REGEX.test(url) } const getGoogleDocs = () => { @@ -96,24 +95,41 @@ const getGoogleDocs = () => { export const parseGoogleDocs = async () => { const result = new Promise((resolve) => { - chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => { - const tab = tabs[0] + if (import.meta.env.BROWSER === "chrome") { + chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => { + const tab = tabs[0] - const data = await chrome.scripting.executeScript({ - target: { tabId: tab.id }, - world: "MAIN", - func: getGoogleDocs + const data = await chrome.scripting.executeScript({ + target: { tabId: tab.id }, + world: "MAIN", + func: getGoogleDocs + }) + + if (data.length > 0) { + resolve(data[0].result) + } }) + } else { + browser.tabs + .query({ active: true, currentWindow: true }) + .then(async (tabs) => { + const tab = tabs[0] - if (data.length > 0) { - resolve(data[0].result) - } - }) + const data = await browser.scripting.executeScript({ + target: { tabId: tab.id }, + func: getGoogleDocs + }) + + if (data.length > 0) { + resolve(data[0].result) + } + }) + } }) as Promise<{ content?: string }> const { content } = await result - + return content } diff --git a/src/services/app.ts b/src/services/app.ts new file mode 100644 index 0000000..e9d0cbb --- /dev/null +++ b/src/services/app.ts @@ -0,0 +1,20 @@ +import { Storage } from "@plasmohq/storage" +const storage = new Storage() + +export const isUrlRewriteEnabled = async () => { + const enabled = await storage.get("urlRewriteEnabled") + return enabled === "true" +} + +export const setUrlRewriteEnabled = async (enabled: boolean) => { + await storage.set("urlRewriteEnabled", enabled ? "true" : "false") +} + +export const getRewriteUrl = async () => { + const rewriteUrl = await storage.get("rewriteUrl") + return rewriteUrl +} + +export const setRewriteUrl = async (url: string) => { + await storage.set("rewriteUrl", url) +} diff --git a/src/services/tts.ts b/src/services/tts.ts index 2d38d3e..847efb4 100644 --- a/src/services/tts.ts +++ b/src/services/tts.ts @@ -21,8 +21,16 @@ export const setTTSProvider = async (ttsProvider: string) => { } export const getBrowserTTSVoices = async () => { - const tts = await chrome.tts.getVoices() - return tts + if (import.meta.env.BROWSER === "chrome") { + const tts = await chrome.tts.getVoices() + return tts + } else { + const tts = await speechSynthesis.getVoices() + return tts.map((voice) => ({ + voiceName: voice.name, + lang: voice.lang + })) + } } export const getVoice = async () => { diff --git a/src/utils/action.ts b/src/utils/action.ts new file mode 100644 index 0000000..5881aa4 --- /dev/null +++ b/src/utils/action.ts @@ -0,0 +1,25 @@ +import { browser } from "wxt/browser" + +export const setTitle = ({ title }: { title: string }) => { + if (import.meta.env.BROWSER === "chrome") { + chrome.action.setTitle({ title }) + } else { + browser.browserAction.setTitle({ title }) + } +} + +export const setBadgeBackgroundColor = ({ color }: { color: string }) => { + if (import.meta.env.BROWSER === "chrome") { + chrome.action.setBadgeBackgroundColor({ color }) + } else { + browser.browserAction.setBadgeBackgroundColor({ color }) + } +} + +export const setBadgeText = ({ text }: { text: string }) => { + if (import.meta.env.BROWSER === "chrome") { + chrome.action.setBadgeText({ text }) + } else { + browser.browserAction.setBadgeText({ text }) + } +} diff --git a/wxt.config.ts b/wxt.config.ts index d80478d..d14a859 100644 --- a/wxt.config.ts +++ b/wxt.config.ts @@ -2,35 +2,70 @@ import { defineConfig } from "wxt" import react from "@vitejs/plugin-react" import topLevelAwait from "vite-plugin-top-level-await" +const chromeMV3Permissions = [ + "storage", + "sidePanel", + "activeTab", + "scripting", + "declarativeNetRequest", + "action", + "unlimitedStorage", + "contextMenus", + "tts" +] + +const firefoxMV2Permissions = [ + "storage", + "activeTab", + "scripting", + "unlimitedStorage", + "contextMenus", + "webRequest", + "webRequestBlocking", + "http://*/*", + "https://*/*", + "file://*/*" +] + // See https://wxt.dev/api/config.html export default defineConfig({ vite: () => ({ - plugins: [react(), + plugins: [ + react(), topLevelAwait({ - promiseExportName: '__tla', - promiseImportName: i => `__tla_${i}`, - }), + promiseExportName: "__tla", + promiseImportName: (i) => `__tla_${i}` + }) ], build: { rollupOptions: { - external: [ - "langchain", - "@langchain/community", - ] + external: ["langchain", "@langchain/community"] } } }), entrypointsDir: "entries", srcDir: "src", outDir: "build", + manifest: { - version: "1.1.6", - name: '__MSG_extName__', - description: '__MSG_extDescription__', - default_locale: 'en', + version: "1.1.7", + name: "__MSG_extName__", + description: "__MSG_extDescription__", + default_locale: "en", action: {}, author: "n4ze3m", - host_permissions: ["http://*/*", "https://*/*", "file://*/*"], + browser_specific_settings: + process.env.TARGET === "firefox" + ? { + gecko: { + id: "page-assist@n4ze3m" + } + } + : undefined, + host_permissions: + process.env.TARGET !== "firefox" + ? ["http://*/*", "https://*/*", "file://*/*"] + : undefined, commands: { _execute_action: { suggested_key: { @@ -44,16 +79,9 @@ export default defineConfig({ } } }, - permissions: [ - "storage", - "sidePanel", - "activeTab", - "scripting", - "declarativeNetRequest", - "action", - "unlimitedStorage", - "contextMenus", - "tts" - ] + permissions: + process.env.TARGET === "firefox" + ? firefoxMV2Permissions + : chromeMV3Permissions } })