diff --git a/.gitignore b/.gitignore index c76b5c4..cf3d5c9 100644 --- a/.gitignore +++ b/.gitignore @@ -42,4 +42,7 @@ keys.json # typescript .tsbuildinfo -.wxt \ No newline at end of file +# WXT +.wxt +# WebStorm +.idea diff --git a/README.md b/README.md index db7ef0b..c1fdc5a 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,13 @@ Note: You can install the extension on any Chromium-based browser. It is not lim ### Manual Installation +#### Pre-requisites + +- Node.js (v18 or higher) - [Installation Guide](https://nodejs.org) +- npm +- Ollama (Local AI Provider) - [Installation Guide](https://ollama.com) + + 1. Clone the repository ```bash @@ -43,13 +50,19 @@ cd page-assist npm install ``` -3. Build the extension +3. Build the extension (by default it will build for Chrome) ```bash npm run build ``` -4. Load the extension +or you can build for Firefox + +```bash +npm run build:firefox +``` + +4. Load the extension (chrome) - Open the Extension Management page by navigating to `chrome://extensions`. @@ -57,6 +70,13 @@ npm run build - Click the `Load unpacked` button and select the `build` directory. +5. Load the extension (firefox) + +- Open the Add-ons page by navigating to `about:addons`. +- Click the `Extensions` tab. +- Click the `Manage Your Extensions` button. +- Click the `Load Temporary Add-on` button and select the `manifest.json` file from the `build` directory. + ## Usage ### Sidebar @@ -89,10 +109,10 @@ This will start a development server and watch for changes in the source files. | -------- | ------- | ----------------- | ------ | | Chrome | ✅ | ✅ | ✅ | | Brave | ✅ | ✅ | ✅ | +| Firefox | ✅ | ✅ | ✅ | | Edge | ✅ | ❌ | ✅ | | Opera GX | ❌ | ❌ | ✅ | | Arc | ❌ | ❌ | ✅ | -| Firefox | ❌ | ❌ | ❌ | ## Local AI Provider @@ -100,7 +120,7 @@ This will start a development server and watch for changes in the source files. ## Roadmap -- [ ] Firefox Support +- [X] Firefox Support - [ ] More Local AI Providers - [ ] More Features - [ ] More Customization Options diff --git a/bun.lockb b/bun.lockb index a4deaa6..c6cbef9 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/docs/connection-issue.md b/docs/connection-issue.md new file mode 100644 index 0000000..a5681f4 --- /dev/null +++ b/docs/connection-issue.md @@ -0,0 +1,33 @@ +# Ollama Connection Issues + +Connection issues can be caused by a number of reasons. Here are some common issues and how to resolve them on Page Assist. You will see the following error message if there is a connection issue: + +### 1. Direct Connection Error +![Direct connection error](https://image.pageassist.xyz/Screenshot%202024-05-13%20001742.png) + +### 2. `403` Error When Sending a Message +![403 error when sending a message](https://image.pageassist.xyz/Screenshot%202024-05-13%20001940.png) + +This issue usually occurs when Ollama is not running on [http://127.0.0.1:11434/](http://127.0.0.1:11434/), and the connection is from the private network or a different network. + +### Solutions + +Since Ollama has connection issues when directly accessed from the browser extension, Page Assist rewrites the request headers to make it work. However, automatic rewriting of headers only works on `http://127.0.0.1:*` and `http://localhost:*` URLs. To resolve the connection issue, you can try the following solutions: + +1. Go to Page Assist and click on the `Settings` icon. + +2. Click on the `Ollama Settings` tab. + +3. There you will see the `Advance Ollama URL Configuration` option. You need to expand it. + +![Advance Ollama URL Configuration](https://image.pageassist.xyz/Screenshot%202024-05-13%20003123.png) + +4. Enable the `Enable or Disable Custom Origin URL` option. + +![Enable or Disable Custom Origin URL](https://image.pageassist.xyz/Screenshot%202024-05-13%20003225.png) + +5. (Optional) If Ollama is running on a different port or host, then change the URL in the `Custom Origin URL` field; otherwise, leave it as it is. + +This will resolve the connection issue, and you will be able to use Ollama without any issues on Page Assist ❤ + +If you still face any issues, feel free to contact us [here](https://github.com/n4ze3m/page-assist/issues/new), and we will be happy to help you out. \ No newline at end of file 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/assets/locale/en/playground.json b/src/assets/locale/en/playground.json index 1d0794d..5df01bb 100644 --- a/src/assets/locale/en/playground.json +++ b/src/assets/locale/en/playground.json @@ -2,7 +2,8 @@ "ollamaState": { "searching": "Searching for Your Ollama 🦙", "running": "Ollama is running 🦙", - "notRunning": "Unable to connect to Ollama 🦙" + "notRunning": "Unable to connect to Ollama 🦙", + "connectionError": "It seems like you are having a connection error. Please refer to this documentation for troubleshooting." }, "formError": { "noModel": "Please select a model", diff --git a/src/assets/locale/en/settings.json b/src/assets/locale/en/settings.json index 589f557..acfcfa5 100644 --- a/src/assets/locale/en/settings.json +++ b/src/assets/locale/en/settings.json @@ -245,6 +245,17 @@ "webSearchFollowUpPromptHelp": "Do not remove `{chat_history}` and `{question}` from the prompt.", "webSearchFollowUpPromptError": "Please input your Web Search Follow Up Prompt!", "webSearchFollowUpPromptPlaceholder": "Your Web Search Follow Up Prompt" + }, + "advanced": { + "label": "Advance Ollama URL Configuration", + "urlRewriteEnabled": { + "label": "Enable or Disable Custom Origin URL" + }, + "rewriteUrl": { + "label": "Custom Origin URL", + "placeholder": "Enter Custom Origin URL" + }, + "help": "If you have connection issues with Ollama on Page Assist, you can configure a custom origin URL. To learn more about the configuration, click here." } } }, diff --git a/src/assets/locale/ja-JP/playground.json b/src/assets/locale/ja-JP/playground.json index a559d18..b5ebedf 100644 --- a/src/assets/locale/ja-JP/playground.json +++ b/src/assets/locale/ja-JP/playground.json @@ -2,7 +2,8 @@ "ollamaState": { "searching": "Ollamaを検索中 🦙", "running": "Ollamaが実行中 🦙", - "notRunning": "Ollamaに接続できません 🦙" + "notRunning": "Ollamaに接続できません 🦙", + "connectionError": "接続エラーが発生しているようです。トラブルシューティングについてはドキュメントをご覧ください。" }, "formError": { "noModel": "モデルを選択してください", diff --git a/src/assets/locale/ja-JP/settings.json b/src/assets/locale/ja-JP/settings.json index 1fae4f5..8fd2860 100644 --- a/src/assets/locale/ja-JP/settings.json +++ b/src/assets/locale/ja-JP/settings.json @@ -248,6 +248,17 @@ "webSearchFollowUpPromptHelp": "プロンプトから`{chat_history}`と`{question}`を削除しないでください。", "webSearchFollowUpPromptError": "Web検索フォローアッププロンプトを入力してください!", "webSearchFollowUpPromptPlaceholder": "Web検索フォローアッププロンプト" + }, + "advanced": { + "label": "Ollama URL の高度な設定", + "urlRewriteEnabled": { + "label": "カスタムOriginのURLを有効化または無効化する" + }, + "rewriteUrl": { + "label": "カスタムOriginのURL", + "placeholder": "カスタムOriginのURLを入力" + }, + "help": "PageAssistでOllamaに接続の問題がある場合は、カスタムOriginのURLを設定できます。設定の詳細については、ここをクリックしてください。" } } }, diff --git a/src/assets/locale/ml/playground.json b/src/assets/locale/ml/playground.json index 0c6b7ee..708782c 100644 --- a/src/assets/locale/ml/playground.json +++ b/src/assets/locale/ml/playground.json @@ -2,7 +2,8 @@ "ollamaState": { "searching": "നിങ്ങളുടെ ഒല്ലാമയ്ക്കായി തിരയുന്നു 🦙", "running": "ഒല്ലാമ പ്രവര്‍ത്തിക്കുന്നു 🦙", - "notRunning": "ഒല്ലാമയുമായി ബന്ധിപ്പിക്കാന്‍ കഴിയുന്നില്ല 🦙" + "notRunning": "ഒല്ലാമയുമായി ബന്ധിപ്പിക്കാന്‍ കഴിയുന്നില്ല 🦙", + "connectionError": "നിങ്ങൾക്ക് കണക്ഷൻ പ്രശ്നം ഉണ്ടെന്നു കാണുന്നു. ഈ ഡോക്യുമെന്റേഷൻ പരിശോധിക്കാൻ കൂടുതൽ സഹായത്തിനായി." }, "formError": { "noModel": "ദയവായി ഒരു മോഡല്‍ തിരഞ്ഞെടുക്കുക", diff --git a/src/assets/locale/ml/settings.json b/src/assets/locale/ml/settings.json index 97152cd..b3384c7 100644 --- a/src/assets/locale/ml/settings.json +++ b/src/assets/locale/ml/settings.json @@ -47,13 +47,13 @@ "label": "ചാറ്റ് ചരിത്രം, അറിവ് അടിസ്ഥാനം, പ്രോംപ്റ്റുകൾ എക്സ്പോർട്ട് ചെയ്യുക", "button": "ഡാറ്റ എക്സ്പോർട്ട് ചെയ്യുക", "success": "എക്സ്പോർട്ട് വിജയകരമായി" - }, - "import": { + }, + "import": { "label": "ചാറ്റ് ചരിത്രം, അറിവ് അടിസ്ഥാനം, പ്രോംപ്റ്റുകൾ ഇമ്പോർട്ട് ചെയ്യുക", "button": "ഡാറ്റ ഇമ്പോർട്ട് ചെയ്യുക", "success": "ഇമ്പോർട്ട് വിജയകരമായി", "error": "ഇമ്പോർട്ട് പിശക്" - } + } }, "tts": { "heading": "ടെക്സ്റ്റ്-ടു-സ്പീച്ച് ക്രമീകരണങ്ങൾ", @@ -248,6 +248,17 @@ "webSearchFollowUpPromptHelp": "പ്രോംപ്റ്റില്‍ നിന്ന് `{chat_history}` യും `{question}` യും നീക്കം ചെയ്യരുത്.", "webSearchFollowUpPromptError": "ദയവായി നിങ്ങളുടെ വെബ് തിരയല്‍ തുടര്‍പ്രോംപ്റ്റ് നല്കുക!", "webSearchFollowUpPromptPlaceholder": "നിങ്ങളുടെ വെബ് തിരയല്‍ തുടര്‍പ്രോംപ്റ്റ്" + }, + "advanced": { + "label": "Advance Ollama URL Configuration", + "urlRewriteEnabled": { + "label": "Enable or Disable Custom Origin URL" + }, + "rewriteUrl": { + "label": "Custom Origin URL", + "placeholder": "Enter Custom Origin URL" + }, + "help": "ഏജ് അസിസ്റ്റന്റിൽ Ollama-യുമായി ബന്ധപ്പെടുമ്പോൾ ബന്ധതടസ്സം ഉണ്ടെങ്കിൽ, നിങ്ങൾക്ക് ഒരു വ്യക്തിഗത അസ്ഥിരത്വം URL കോൺഫിഗർ ചെയ്യാം. കോൺഫിഗറേഷനെക്കുറിച്ച് കൂടുതലറിയാൻ, ഇവിടെ ക്ലിക്കുചെയ്യുക." } } }, diff --git a/src/assets/locale/ru/playground.json b/src/assets/locale/ru/playground.json index 498f2c8..433c67d 100644 --- a/src/assets/locale/ru/playground.json +++ b/src/assets/locale/ru/playground.json @@ -2,7 +2,8 @@ "ollamaState": { "searching": "Поиск вашего Ollama 🦙", "running": "Ollama работает 🦙", - "notRunning": "Не удалось подключиться к Ollama 🦙" + "notRunning": "Не удалось подключиться к Ollama 🦙", + "connectionError": "Похоже, у вас возникла ошибка соединения. Пожалуйста, обратитесь к этой документации для устранения неисправностей." }, "formError": { "noModel": "Пожалуйста, выберите модель", diff --git a/src/assets/locale/ru/settings.json b/src/assets/locale/ru/settings.json index c48c357..51fa44e 100644 --- a/src/assets/locale/ru/settings.json +++ b/src/assets/locale/ru/settings.json @@ -245,6 +245,17 @@ "webSearchFollowUpPromptHelp": "Не удаляйте `{chat_history}` и `{question}` из подсказки.", "webSearchFollowUpPromptError": "Введите подсказку для последующего веб-поиска!", "webSearchFollowUpPromptPlaceholder": "Ваша подсказка для последующего веб-поиска" + }, + "advanced": { + "label": "Расширенная конфигурация URL Ollama", + "urlRewriteEnabled": { + "label": "Включить или отключить пользовательский исходный URL" + }, + "rewriteUrl": { + "label": "Пользовательский исходный URL", + "placeholder": "Введите пользовательский исходный URL" + }, + "help": "Если у вас возникают проблемы с подключением к Ollama на странице помощника, вы можете настроить пользовательский исходный URL. Чтобы узнать больше о конфигурации, нажмите здесь." } } }, diff --git a/src/assets/locale/zh/playground.json b/src/assets/locale/zh/playground.json index 84a591c..bb74e7a 100644 --- a/src/assets/locale/zh/playground.json +++ b/src/assets/locale/zh/playground.json @@ -2,7 +2,8 @@ "ollamaState": { "searching": "正在搜索您的Ollama 🦙", "running": "Ollama正在运行 🦙", - "notRunning": "无法连接到Ollama 🦙" + "notRunning": "无法连接到Ollama 🦙", + "connectionError": "看起来你正在遇到连接错误。请参阅这文档进行故障排除。" }, "formError": { "noModel": "请选择一个模型", diff --git a/src/assets/locale/zh/settings.json b/src/assets/locale/zh/settings.json index 70b8d3c..883ae22 100644 --- a/src/assets/locale/zh/settings.json +++ b/src/assets/locale/zh/settings.json @@ -249,6 +249,17 @@ "webSearchFollowUpPromptHelp": "请勿从提示词中删除 `{chat_history}` 和 `{question}`。", "webSearchFollowUpPromptError": "请输入您的网页搜索追问提示词!", "webSearchFollowUpPromptPlaceholder": "您的网页搜索追问提示词" + }, + "advanced": { + "label": "Ollama URL 高级配置", + "urlRewriteEnabled": { + "label": "启用或禁用自定义来源 URL" + }, + "rewriteUrl": { + "label": "自定义来源 URL", + "placeholder": "输入自定义来源 URL" + }, + "help": "如果您在 Page Assist 上与 Ollama 有连接问题,您可以配置自定义来源 URL。要了解更多关于配置的信息,点击此处。" } } }, diff --git a/src/components/Common/AdvanceOllamaSettings.tsx b/src/components/Common/AdvanceOllamaSettings.tsx new file mode 100644 index 0000000..0027fb2 --- /dev/null +++ b/src/components/Common/AdvanceOllamaSettings.tsx @@ -0,0 +1,49 @@ +import { useStorage } from "@plasmohq/storage/hook" +import { Input, Switch } from "antd" +import { useTranslation } from "react-i18next" + +export const AdvanceOllamaSettings = () => { + const [urlRewriteEnabled, setUrlRewriteEnabled] = useStorage( + "urlRewriteEnabled", + false + ) + + const [rewriteUrl, setRewriteUrl] = useStorage( + "rewriteUrl", + "http://127.0.0.1:11434" + ) + const { t } = useTranslation("settings") + + return ( +
+
+ + {t("ollamaSettings.settings.advanced.urlRewriteEnabled.label")} + +
+ setUrlRewriteEnabled(checked)} + /> +
+
+
+ + {t("ollamaSettings.settings.advanced.rewriteUrl.label")} + +
+ setRewriteUrl(e.target.value)} + /> +
+
+
+ ) +} diff --git a/src/components/Common/PageAssistLoader.tsx b/src/components/Common/PageAssistLoader.tsx new file mode 100644 index 0000000..f98e014 --- /dev/null +++ b/src/components/Common/PageAssistLoader.tsx @@ -0,0 +1,10 @@ + +export const PageAssistLoader = () => { + return ( +
+

+ Loading... +

+
+ ) +} 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/Playground/PlaygroundEmpty.tsx b/src/components/Option/Playground/PlaygroundEmpty.tsx index 6636605..7fb94ab 100644 --- a/src/components/Option/Playground/PlaygroundEmpty.tsx +++ b/src/components/Option/Playground/PlaygroundEmpty.tsx @@ -1,7 +1,8 @@ +import { cleanUrl } from "@/libs/clean-url" import { useQuery } from "@tanstack/react-query" import { RotateCcw } from "lucide-react" import { useEffect, useState } from "react" -import { useTranslation } from "react-i18next" +import { Trans, useTranslation } from "react-i18next" import { getOllamaURL, isOllamaRunning, @@ -79,6 +80,23 @@ export const PlaygroundEmpty = () => { {t("common:retry")} + + {ollamaURL && + cleanUrl(ollamaURL) !== "http://127.0.0.1:11434" && ( +

+ + ) + }} + /> +

+ )} ) ) : null} 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/components/Option/Settings/ollama.tsx b/src/components/Option/Settings/ollama.tsx index e71045b..969a845 100644 --- a/src/components/Option/Settings/ollama.tsx +++ b/src/components/Option/Settings/ollama.tsx @@ -1,5 +1,5 @@ import { useMutation, useQuery } from "@tanstack/react-query" -import { Form, InputNumber, Select, Skeleton } from "antd" +import { Collapse, Form, InputNumber, Select, Skeleton } from "antd" import { useState } from "react" import { SaveButton } from "~/components/Common/SaveButton" import { @@ -12,10 +12,13 @@ import { setOllamaURL as saveOllamaURL } from "~/services/ollama" import { SettingPrompt } from "./prompt" -import { useTranslation } from "react-i18next" +import { Trans, useTranslation } from "react-i18next" +import { useStorage } from "@plasmohq/storage/hook" +import { AdvanceOllamaSettings } from "@/components/Common/AdvanceOllamaSettings" export const SettingsOllama = () => { const [ollamaURL, setOllamaURL] = useState("") + const { t } = useTranslation("settings") const { data: ollamaInfo, status } = useQuery({ @@ -61,7 +64,7 @@ export const SettingsOllama = () => {
-
+
+ +

+ {t("ollamaSettings.settings.advanced.label")} +

+

+ + ) + }} + /> +

+
+ ), + children: + } + ]} + /> +
{ @@ -130,7 +163,9 @@ export const SettingsOllama = () => { 0 } showSearch - placeholder={t("ollamaSettings.settings.ragSettings.model.placeholder")} + placeholder={t( + "ollamaSettings.settings.ragSettings.model.placeholder" + )} style={{ width: "100%" }} className="mt-4" options={ollamaInfo.models?.map((model) => ({ @@ -144,26 +179,38 @@ export const SettingsOllama = () => { name="chunkSize" label={t("ollamaSettings.settings.ragSettings.chunkSize.label")} rules={[ - { required: true, message: t("ollamaSettings.settings.ragSettings.chunkSize.required") - } + { + required: true, + message: t( + "ollamaSettings.settings.ragSettings.chunkSize.required" + ) + } ]}> diff --git a/src/components/Sidepanel/Chat/empty.tsx b/src/components/Sidepanel/Chat/empty.tsx index 499e320..000d4e3 100644 --- a/src/components/Sidepanel/Chat/empty.tsx +++ b/src/components/Sidepanel/Chat/empty.tsx @@ -1,8 +1,9 @@ +import { cleanUrl } from "@/libs/clean-url" import { useQuery, useQueryClient } from "@tanstack/react-query" import { Select } from "antd" import { RotateCcw } from "lucide-react" import { useEffect, useState } from "react" -import { useTranslation } from "react-i18next" +import { Trans, useTranslation } from "react-i18next" import { useMessage } from "~/hooks/useMessage" import { getAllModels, @@ -91,6 +92,22 @@ export const EmptySidePanel = () => { {t("common:retry")} + {ollamaURL && + cleanUrl(ollamaURL) !== "http://127.0.0.1:11434" && ( +

+ + ) + }} + /> +

+ )}
) ) : null} 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..dc54c4a 100644 --- a/src/libs/runtime.ts +++ b/src/libs/runtime.ts @@ -1,31 +1,63 @@ -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}` - } - ] - } +import { getAdvancedOllamaSettings } from "@/services/app" + +export const urlRewriteRuntime = async function ( + domain: string, + type = "ollama" +) { + if (browser.runtime && browser.runtime.id) { + const { isEnableRewriteUrl, rewriteUrl } = await getAdvancedOllamaSettings() + if (import.meta.env.BROWSER === "chrome") { + const url = new URL(domain) + const domains = [url.hostname] + let origin = `${url.protocol}//${url.hostname}` + if (isEnableRewriteUrl && rewriteUrl && type === "ollama") { + origin = rewriteUrl } - ] + const rules = [ + { + id: 1, + priority: 1, + condition: { + requestDomains: domains + }, + action: { + type: "modifyHeaders", + requestHeaders: [ + { + header: "Origin", + operation: "set", + value: origin + } + ] + } + } + ] + await browser.declarativeNetRequest.updateDynamicRules({ + removeRuleIds: rules.map((r) => r.id), + // @ts-ignore + addRules: rules + }) + } - await chrome.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) => { + let origin = `${url.protocol}//${url.hostname}` + if (isEnableRewriteUrl && rewriteUrl && type === "ollama") { + origin = rewriteUrl + } + for (let i = 0; i < details.requestHeaders.length; i++) { + if (details.requestHeaders[i].name === "Origin") { + details.requestHeaders[i].value = origin + } + } + return { requestHeaders: details.requestHeaders } + }, + { urls: domains }, + ["blocking", "requestHeaders"] + ) + } } } diff --git a/src/loader/html.ts b/src/loader/html.ts index 94eaed0..786c60e 100644 --- a/src/loader/html.ts +++ b/src/loader/html.ts @@ -1,7 +1,7 @@ import { BaseDocumentLoader } from "langchain/document_loaders/base" import { Document } from "@langchain/core/documents" import { compile } from "html-to-text" -import { chromeRunTime } from "~/libs/runtime" +import { urlRewriteRuntime } from "~/libs/runtime" import { YtTranscript } from "yt-transcript" import { isWikipedia, parseWikipedia } from "@/parser/wiki" @@ -102,7 +102,7 @@ export class PageAssistHtmlLoader } ] } - await chromeRunTime(this.url) + await urlRewriteRuntime(this.url, "web") const fetchHTML = await fetch(this.url) let html = await fetchHTML.text() @@ -111,11 +111,6 @@ export class PageAssistHtmlLoader html = parseWikipedia(await fetchHTML.text()) } - // else if (isTwitter(this.url)) { - // console.log("Twitter URL detected") - // html = parseTweet(await fetchHTML.text(), this.url) - // } - const htmlCompiler = compile({ wordwrap: false, selectors: [ 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/routes/chrome.tsx b/src/routes/chrome.tsx new file mode 100644 index 0000000..a7ccb7c --- /dev/null +++ b/src/routes/chrome.tsx @@ -0,0 +1,35 @@ +import { Route, Routes } from "react-router-dom" +import OptionIndex from "./option-index" +import OptionSettings from "./option-settings" +import OptionModal from "./option-settings-model" +import OptionPrompt from "./option-settings-prompt" +import OptionOllamaSettings from "./options-settings-ollama" +import OptionShare from "./option-settings-share" +import OptionKnowledgeBase from "./option-settings-knowledge" +import OptionAbout from "./option-settings-about" +import SidepanelChat from "./sidepanel-chat" +import SidepanelSettings from "./sidepanel-settings" + +export const OptionRoutingChrome = () => { + return ( + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + ) +} + +export const SidepanelRoutingChrome = () => { + return ( + + } /> + } /> + + ) +} diff --git a/src/routes/firefox.tsx b/src/routes/firefox.tsx new file mode 100644 index 0000000..1281a72 --- /dev/null +++ b/src/routes/firefox.tsx @@ -0,0 +1,39 @@ +// this is a temp fix for firefox +// because chunks getting 4mb+ and it's not working on firefox addon store +import { lazy } from "react" +import { Route , Routes} from "react-router-dom" + +const SidepanelChat = lazy(() => import("./sidepanel-chat")) +const SidepanelSettings = lazy(() => import("./sidepanel-settings")) +const OptionIndex = lazy(() => import("./option-index")) +const OptionModal = lazy(() => import("./option-settings-model")) +const OptionPrompt = lazy(() => import("./option-settings-prompt")) +const OptionOllamaSettings = lazy(() => import("./options-settings-ollama")) +const OptionSettings = lazy(() => import("./option-settings")) +const OptionShare = lazy(() => import("./option-settings-share")) +const OptionKnowledgeBase = lazy(() => import("./option-settings-knowledge")) +const OptionAbout = lazy(() => import("./option-settings-about")) + +export const OptionRoutingFirefox = () => { + return ( + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + ) +} + +export const SidepanelRoutingFirefox = () => { + return ( + + } /> + } /> + + ) +} diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 87581f8..86af217 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,16 +1,9 @@ -import { Route, Routes } from "react-router-dom" -import { SidepanelChat } from "./sidepanel-chat" -import { useDarkMode } from "~/hooks/useDarkmode" -import { SidepanelSettings } from "./sidepanel-settings" -import { OptionIndex } from "./option-index" -import { OptionModal } from "./option-settings-model" -import { OptionPrompt } from "./option-settings-prompt" -import { OptionOllamaSettings } from "./options-settings-ollama" -import { OptionSettings } from "./option-settings" -import { OptionShare } from "./option-settings-share" -import { OptionKnowledgeBase } from "./option-settings-knowledge" -import { OptionAbout } from "./option-settings-about" +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() @@ -21,16 +14,13 @@ export const OptionRouting = () => { className={`${mode === "dark" ? "dark" : "light"} ${ i18n.language === "ru" ? "onest" : "inter" }`}> - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - + }> + {import.meta.env.BROWSER === "chrome" ? ( + + ) : ( + + )} + ) } @@ -44,10 +34,13 @@ export const SidepanelRouting = () => { className={`${mode === "dark" ? "dark" : "light"} ${ i18n.language === "ru" ? "onest" : "inter" }`}> - - } /> - } /> - + }> + {import.meta.env.BROWSER === "chrome" ? ( + + ) : ( + + )} + ) } diff --git a/src/routes/option-index.tsx b/src/routes/option-index.tsx index 6178d0a..776bcce 100644 --- a/src/routes/option-index.tsx +++ b/src/routes/option-index.tsx @@ -1,10 +1,12 @@ import OptionLayout from "~/components/Layouts/Layout" import { Playground } from "~/components/Option/Playground/Playground" -export const OptionIndex = () => { + const OptionIndex = () => { return ( ) } + +export default OptionIndex \ No newline at end of file diff --git a/src/routes/option-settings-about.tsx b/src/routes/option-settings-about.tsx index 982f1ea..9d00233 100644 --- a/src/routes/option-settings-about.tsx +++ b/src/routes/option-settings-about.tsx @@ -2,7 +2,7 @@ import { SettingsLayout } from "~/components/Layouts/SettingsOptionLayout" import OptionLayout from "~/components/Layouts/Layout" import { AboutApp } from "@/components/Option/Settings/about" -export const OptionAbout = () => { +const OptionAbout = () => { return ( @@ -11,3 +11,5 @@ export const OptionAbout = () => { ) } + +export default OptionAbout diff --git a/src/routes/option-settings-knowledge.tsx b/src/routes/option-settings-knowledge.tsx index aedddd3..504035c 100644 --- a/src/routes/option-settings-knowledge.tsx +++ b/src/routes/option-settings-knowledge.tsx @@ -2,7 +2,7 @@ import { SettingsLayout } from "~/components/Layouts/SettingsOptionLayout" import OptionLayout from "~/components/Layouts/Layout" import { KnowledgeSettings } from "@/components/Option/Knowledge" -export const OptionKnowledgeBase = () => { + const OptionKnowledgeBase = () => { return ( @@ -11,3 +11,5 @@ export const OptionKnowledgeBase = () => { ) } + +export default OptionKnowledgeBase \ No newline at end of file diff --git a/src/routes/option-settings-model.tsx b/src/routes/option-settings-model.tsx index 99264d9..456e1dd 100644 --- a/src/routes/option-settings-model.tsx +++ b/src/routes/option-settings-model.tsx @@ -2,7 +2,7 @@ import { SettingsLayout } from "~/components/Layouts/SettingsOptionLayout" import OptionLayout from "~/components/Layouts/Layout" import { ModelsBody } from "~/components/Option/Models" -export const OptionModal = () => { +const OptionModal = () => { return ( @@ -11,3 +11,5 @@ export const OptionModal = () => { ) } + +export default OptionModal diff --git a/src/routes/option-settings-prompt.tsx b/src/routes/option-settings-prompt.tsx index f7a35cf..f796403 100644 --- a/src/routes/option-settings-prompt.tsx +++ b/src/routes/option-settings-prompt.tsx @@ -2,7 +2,7 @@ import { SettingsLayout } from "~/components/Layouts/SettingsOptionLayout" import OptionLayout from "~/components/Layouts/Layout" import { PromptBody } from "~/components/Option/Prompt" -export const OptionPrompt = () => { + const OptionPrompt = () => { return ( @@ -11,3 +11,5 @@ export const OptionPrompt = () => { ) } + +export default OptionPrompt \ No newline at end of file diff --git a/src/routes/option-settings-share.tsx b/src/routes/option-settings-share.tsx index dcd0da0..3ca7a30 100644 --- a/src/routes/option-settings-share.tsx +++ b/src/routes/option-settings-share.tsx @@ -2,7 +2,7 @@ import { SettingsLayout } from "~/components/Layouts/SettingsOptionLayout" import OptionLayout from "~/components/Layouts/Layout" import { OptionShareBody } from "~/components/Option/Share" -export const OptionShare = () => { + const OptionShare = () => { return ( @@ -11,3 +11,5 @@ export const OptionShare = () => { ) } + +export default OptionShare \ No newline at end of file diff --git a/src/routes/option-settings.tsx b/src/routes/option-settings.tsx index 6849f7f..3bb60c0 100644 --- a/src/routes/option-settings.tsx +++ b/src/routes/option-settings.tsx @@ -2,7 +2,7 @@ import { SettingsLayout } from "~/components/Layouts/SettingsOptionLayout" import OptionLayout from "~/components/Layouts/Layout" import { SettingOther } from "~/components/Option/Settings/other" -export const OptionSettings = () => { + const OptionSettings = () => { return ( @@ -11,3 +11,5 @@ export const OptionSettings = () => { ) } + +export default OptionSettings \ No newline at end of file diff --git a/src/routes/options-settings-ollama.tsx b/src/routes/options-settings-ollama.tsx index cdbbad1..dad6498 100644 --- a/src/routes/options-settings-ollama.tsx +++ b/src/routes/options-settings-ollama.tsx @@ -2,7 +2,7 @@ import { SettingsLayout } from "~/components/Layouts/SettingsOptionLayout" import OptionLayout from "~/components/Layouts/Layout" import { SettingsOllama } from "~/components/Option/Settings/ollama" -export const OptionOllamaSettings = () => { + const OptionOllamaSettings = () => { return ( @@ -11,3 +11,5 @@ export const OptionOllamaSettings = () => { ) } + +export default OptionOllamaSettings \ No newline at end of file diff --git a/src/routes/sidepanel-chat.tsx b/src/routes/sidepanel-chat.tsx index 0e87879..5921a2b 100644 --- a/src/routes/sidepanel-chat.tsx +++ b/src/routes/sidepanel-chat.tsx @@ -4,7 +4,7 @@ import { SidepanelForm } from "~/components/Sidepanel/Chat/form" import { SidepanelHeader } from "~/components/Sidepanel/Chat/header" import { useMessage } from "~/hooks/useMessage" -export const SidepanelChat = () => { + const SidepanelChat = () => { const drop = React.useRef(null) const [dropedFile, setDropedFile] = React.useState() const [dropState, setDropState] = React.useState< @@ -90,3 +90,5 @@ export const SidepanelChat = () => { ) } + +export default SidepanelChat diff --git a/src/routes/sidepanel-settings.tsx b/src/routes/sidepanel-settings.tsx index ad51649..eae0658 100644 --- a/src/routes/sidepanel-settings.tsx +++ b/src/routes/sidepanel-settings.tsx @@ -1,7 +1,7 @@ import { SettingsBody } from "~/components/Sidepanel/Settings/body" import { SidepanelSettingsHeader } from "~/components/Sidepanel/Settings/header" -export const SidepanelSettings = () => { +const SidepanelSettings = () => { return (
@@ -11,3 +11,5 @@ export const SidepanelSettings = () => {
) } + +export default SidepanelSettings diff --git a/src/services/app.ts b/src/services/app.ts new file mode 100644 index 0000000..8f0cc66 --- /dev/null +++ b/src/services/app.ts @@ -0,0 +1,36 @@ +import { Storage } from "@plasmohq/storage" +const storage = new Storage() + +const DEFAULT_URL_REWRITE_URL = "http://127.0.0.1:11434" + +export const isUrlRewriteEnabled = async () => { + const enabled = await storage.get("urlRewriteEnabled") + return enabled +} +export const setUrlRewriteEnabled = async (enabled: boolean) => { + await storage.set("urlRewriteEnabled", enabled ? "true" : "false") +} + +export const getRewriteUrl = async () => { + const rewriteUrl = await storage.get("rewriteUrl") + if (!rewriteUrl || rewriteUrl.trim() === "") { + return DEFAULT_URL_REWRITE_URL + } + return rewriteUrl +} + +export const setRewriteUrl = async (url: string) => { + await storage.set("rewriteUrl", url) +} + +export const getAdvancedOllamaSettings = async () => { + const [isEnableRewriteUrl, rewriteUrl] = await Promise.all([ + isUrlRewriteEnabled(), + getRewriteUrl() + ]) + + return { + isEnableRewriteUrl, + rewriteUrl + } +} diff --git a/src/services/ollama.ts b/src/services/ollama.ts index ccca697..e1ce53b 100644 --- a/src/services/ollama.ts +++ b/src/services/ollama.ts @@ -1,6 +1,6 @@ import { Storage } from "@plasmohq/storage" import { cleanUrl } from "../libs/clean-url" -import { chromeRunTime } from "../libs/runtime" +import { urlRewriteRuntime } from "../libs/runtime" const storage = new Storage() @@ -22,10 +22,10 @@ Search results: export const getOllamaURL = async () => { const ollamaURL = await storage.get("ollamaURL") if (!ollamaURL || ollamaURL.length === 0) { - await chromeRunTime(DEFAULT_OLLAMA_URL) + await urlRewriteRuntime(DEFAULT_OLLAMA_URL) return DEFAULT_OLLAMA_URL } - await chromeRunTime(cleanUrl(ollamaURL)) + await urlRewriteRuntime(cleanUrl(ollamaURL)) return ollamaURL } @@ -163,7 +163,7 @@ export const setOllamaURL = async (ollamaURL: string) => { "http://127.0.0.1:" ) } - await chromeRunTime(cleanUrl(formattedUrl)) + await urlRewriteRuntime(cleanUrl(formattedUrl)) await storage.set("ollamaURL", cleanUrl(formattedUrl)) } 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/src/web/search-engines/duckduckgo.ts b/src/web/search-engines/duckduckgo.ts index 089e571..374ce60 100644 --- a/src/web/search-engines/duckduckgo.ts +++ b/src/web/search-engines/duckduckgo.ts @@ -1,5 +1,5 @@ import { cleanUrl } from "@/libs/clean-url" -import { chromeRunTime } from "@/libs/runtime" +import { urlRewriteRuntime } from "@/libs/runtime" import { PageAssistHtmlLoader } from "@/loader/html" import { defaultEmbeddingChunkOverlap, @@ -18,7 +18,7 @@ import { RecursiveCharacterTextSplitter } from "langchain/text_splitter" import { MemoryVectorStore } from "langchain/vectorstores/memory" export const localDuckDuckGoSearch = async (query: string) => { - await chromeRunTime(cleanUrl("https://html.duckduckgo.com/html/?q=" + query)) + await urlRewriteRuntime(cleanUrl("https://html.duckduckgo.com/html/?q=" + query), "duckduckgo") const abortController = new AbortController() setTimeout(() => abortController.abort(), 10000) diff --git a/src/web/search-engines/google.ts b/src/web/search-engines/google.ts index 98fa2a8..8f0a16a 100644 --- a/src/web/search-engines/google.ts +++ b/src/web/search-engines/google.ts @@ -7,7 +7,7 @@ import type { Document } from "@langchain/core/documents" import { RecursiveCharacterTextSplitter } from "langchain/text_splitter" import { MemoryVectorStore } from "langchain/vectorstores/memory" import { cleanUrl } from "~/libs/clean-url" -import { chromeRunTime } from "~/libs/runtime" +import { urlRewriteRuntime } from "~/libs/runtime" import { PageAssistHtmlLoader } from "~/loader/html" import { defaultEmbeddingChunkOverlap, @@ -18,8 +18,9 @@ import { export const localGoogleSearch = async (query: string) => { - await chromeRunTime( - cleanUrl("https://www.google.com/search?hl=en&q=" + query) + await urlRewriteRuntime( + cleanUrl("https://www.google.com/search?hl=en&q=" + query), + "google" ) const abortController = new AbortController() setTimeout(() => abortController.abort(), 10000) diff --git a/src/web/search-engines/sogou.ts b/src/web/search-engines/sogou.ts index ffa7e6f..274d03f 100644 --- a/src/web/search-engines/sogou.ts +++ b/src/web/search-engines/sogou.ts @@ -1,5 +1,5 @@ import { cleanUrl } from "@/libs/clean-url" -import { chromeRunTime } from "@/libs/runtime" +import { urlRewriteRuntime } from "@/libs/runtime" import { PageAssistHtmlLoader } from "@/loader/html" import { defaultEmbeddingChunkOverlap, @@ -25,7 +25,10 @@ const getCorrectTargeUrl = async (url: string) => { return matches?.[1] || "" } export const localSogouSearch = async (query: string) => { - await chromeRunTime(cleanUrl("https://www.sogou.com/web?query=" + query)) + await urlRewriteRuntime( + cleanUrl("https://www.sogou.com/web?query=" + query), + "sogou" + ) const abortController = new AbortController() diff --git a/tailwind.config.js b/tailwind.config.js index a30235c..f4b105c 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -3,5 +3,5 @@ module.exports = { mode: "jit", darkMode: "class", content: ["./src/**/*.tsx"], - plugins: [require("@tailwindcss/forms"), require("@tailwindcss/typography")] + plugins: [require("@tailwindcss/forms"), require("@tailwindcss/typography"),] } diff --git a/wxt.config.ts b/wxt.config.ts index d80478d..0f5aeba 100644 --- a/wxt.config.ts +++ b/wxt.config.ts @@ -2,35 +2,73 @@ 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: + process.env.TARGET === "firefox" + ? "Page Assist - A Web UI for Local AI Models" + : "__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@nazeem" + } + } + : undefined, + host_permissions: + process.env.TARGET !== "firefox" + ? ["http://*/*", "https://*/*", "file://*/*"] + : undefined, commands: { _execute_action: { suggested_key: { @@ -44,16 +82,9 @@ export default defineConfig({ } } }, - permissions: [ - "storage", - "sidePanel", - "activeTab", - "scripting", - "declarativeNetRequest", - "action", - "unlimitedStorage", - "contextMenus", - "tts" - ] + permissions: + process.env.TARGET === "firefox" + ? firefoxMV2Permissions + : chromeMV3Permissions } })