commit
a4a684f118
3
.gitignore
vendored
3
.gitignore
vendored
@ -42,4 +42,7 @@ keys.json
|
|||||||
|
|
||||||
# typescript
|
# typescript
|
||||||
.tsbuildinfo
|
.tsbuildinfo
|
||||||
|
# WXT
|
||||||
.wxt
|
.wxt
|
||||||
|
# WebStorm
|
||||||
|
.idea
|
||||||
|
28
README.md
28
README.md
@ -30,6 +30,13 @@ Note: You can install the extension on any Chromium-based browser. It is not lim
|
|||||||
|
|
||||||
### Manual Installation
|
### 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
|
1. Clone the repository
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -43,13 +50,19 @@ cd page-assist
|
|||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Build the extension
|
3. Build the extension (by default it will build for Chrome)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run build
|
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`.
|
- 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.
|
- 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
|
## Usage
|
||||||
|
|
||||||
### Sidebar
|
### Sidebar
|
||||||
@ -89,10 +109,10 @@ This will start a development server and watch for changes in the source files.
|
|||||||
| -------- | ------- | ----------------- | ------ |
|
| -------- | ------- | ----------------- | ------ |
|
||||||
| Chrome | ✅ | ✅ | ✅ |
|
| Chrome | ✅ | ✅ | ✅ |
|
||||||
| Brave | ✅ | ✅ | ✅ |
|
| Brave | ✅ | ✅ | ✅ |
|
||||||
|
| Firefox | ✅ | ✅ | ✅ |
|
||||||
| Edge | ✅ | ❌ | ✅ |
|
| Edge | ✅ | ❌ | ✅ |
|
||||||
| Opera GX | ❌ | ❌ | ✅ |
|
| Opera GX | ❌ | ❌ | ✅ |
|
||||||
| Arc | ❌ | ❌ | ✅ |
|
| Arc | ❌ | ❌ | ✅ |
|
||||||
| Firefox | ❌ | ❌ | ❌ |
|
|
||||||
|
|
||||||
## Local AI Provider
|
## Local AI Provider
|
||||||
|
|
||||||
@ -100,7 +120,7 @@ This will start a development server and watch for changes in the source files.
|
|||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
- [ ] Firefox Support
|
- [X] Firefox Support
|
||||||
- [ ] More Local AI Providers
|
- [ ] More Local AI Providers
|
||||||
- [ ] More Features
|
- [ ] More Features
|
||||||
- [ ] More Customization Options
|
- [ ] More Customization Options
|
||||||
|
33
docs/connection-issue.md
Normal file
33
docs/connection-issue.md
Normal file
@ -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
|
||||||
|

|
||||||
|
|
||||||
|
### 2. `403` Error When Sending a Message
|
||||||
|

|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
4. Enable the `Enable or Disable Custom Origin URL` option.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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.
|
13
package.json
13
package.json
@ -5,12 +5,12 @@
|
|||||||
"description": "Use your locally running AI models to assist you in your web browsing.",
|
"description": "Use your locally running AI models to assist you in your web browsing.",
|
||||||
"author": "n4ze3m",
|
"author": "n4ze3m",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "wxt",
|
"dev": "cross-env TARGET=chrome wxt",
|
||||||
"dev:firefox": "wxt -b firefox",
|
"dev:firefox": "cross-env TARGET=firefox wxt -b firefox",
|
||||||
"build": "wxt build",
|
"build": "cross-env TARGET=chrome wxt build",
|
||||||
"build:firefox": "wxt build -b firefox",
|
"build:firefox": "cross-env TARGET=chrome cross-env TARGET=firefox wxt build -b firefox",
|
||||||
"zip": "wxt zip",
|
"zip": "cross-env TARGET=chrome wxt zip",
|
||||||
"zip:firefox": "wxt zip -b firefox",
|
"zip:firefox": "cross-env TARGET=firefox wxt zip -b firefox",
|
||||||
"compile": "tsc --noEmit",
|
"compile": "tsc --noEmit",
|
||||||
"postinstall": "wxt prepare"
|
"postinstall": "wxt prepare"
|
||||||
},
|
},
|
||||||
@ -66,6 +66,7 @@
|
|||||||
"@types/react-syntax-highlighter": "^15.5.11",
|
"@types/react-syntax-highlighter": "^15.5.11",
|
||||||
"@types/turndown": "^5.0.4",
|
"@types/turndown": "^5.0.4",
|
||||||
"autoprefixer": "^10.4.17",
|
"autoprefixer": "^10.4.17",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
"postcss": "^8.4.33",
|
"postcss": "^8.4.33",
|
||||||
"prettier": "3.2.4",
|
"prettier": "3.2.4",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
|
@ -23,7 +23,7 @@ Click the button below to deploy the code to Railway.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/n4ze3m/page-share-app.git
|
git clone https://github.com/n4ze3m/page-share-app.git
|
||||||
cd page-assist-app
|
cd page-share-app
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Run the server
|
2. Run the server
|
||||||
|
@ -2,7 +2,8 @@
|
|||||||
"ollamaState": {
|
"ollamaState": {
|
||||||
"searching": "Searching for Your Ollama 🦙",
|
"searching": "Searching for Your Ollama 🦙",
|
||||||
"running": "Ollama is running 🦙",
|
"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 <anchor>documentation</anchor> for troubleshooting."
|
||||||
},
|
},
|
||||||
"formError": {
|
"formError": {
|
||||||
"noModel": "Please select a model",
|
"noModel": "Please select a model",
|
||||||
|
@ -245,6 +245,17 @@
|
|||||||
"webSearchFollowUpPromptHelp": "Do not remove `{chat_history}` and `{question}` from the prompt.",
|
"webSearchFollowUpPromptHelp": "Do not remove `{chat_history}` and `{question}` from the prompt.",
|
||||||
"webSearchFollowUpPromptError": "Please input your Web Search Follow Up Prompt!",
|
"webSearchFollowUpPromptError": "Please input your Web Search Follow Up Prompt!",
|
||||||
"webSearchFollowUpPromptPlaceholder": "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, <anchor>click here</anchor>."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -2,7 +2,8 @@
|
|||||||
"ollamaState": {
|
"ollamaState": {
|
||||||
"searching": "Ollamaを検索中 🦙",
|
"searching": "Ollamaを検索中 🦙",
|
||||||
"running": "Ollamaが実行中 🦙",
|
"running": "Ollamaが実行中 🦙",
|
||||||
"notRunning": "Ollamaに接続できません 🦙"
|
"notRunning": "Ollamaに接続できません 🦙",
|
||||||
|
"connectionError": "接続エラーが発生しているようです。トラブルシューティングについては<anchor>ドキュメント</anchor>をご覧ください。"
|
||||||
},
|
},
|
||||||
"formError": {
|
"formError": {
|
||||||
"noModel": "モデルを選択してください",
|
"noModel": "モデルを選択してください",
|
||||||
|
@ -248,6 +248,17 @@
|
|||||||
"webSearchFollowUpPromptHelp": "プロンプトから`{chat_history}`と`{question}`を削除しないでください。",
|
"webSearchFollowUpPromptHelp": "プロンプトから`{chat_history}`と`{question}`を削除しないでください。",
|
||||||
"webSearchFollowUpPromptError": "Web検索フォローアッププロンプトを入力してください!",
|
"webSearchFollowUpPromptError": "Web検索フォローアッププロンプトを入力してください!",
|
||||||
"webSearchFollowUpPromptPlaceholder": "Web検索フォローアッププロンプト"
|
"webSearchFollowUpPromptPlaceholder": "Web検索フォローアッププロンプト"
|
||||||
|
},
|
||||||
|
"advanced": {
|
||||||
|
"label": "Ollama URL の高度な設定",
|
||||||
|
"urlRewriteEnabled": {
|
||||||
|
"label": "カスタムOriginのURLを有効化または無効化する"
|
||||||
|
},
|
||||||
|
"rewriteUrl": {
|
||||||
|
"label": "カスタムOriginのURL",
|
||||||
|
"placeholder": "カスタムOriginのURLを入力"
|
||||||
|
},
|
||||||
|
"help": "PageAssistでOllamaに接続の問題がある場合は、カスタムOriginのURLを設定できます。設定の詳細については、<anchor>ここをクリック</anchor>してください。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -2,7 +2,8 @@
|
|||||||
"ollamaState": {
|
"ollamaState": {
|
||||||
"searching": "നിങ്ങളുടെ ഒല്ലാമയ്ക്കായി തിരയുന്നു 🦙",
|
"searching": "നിങ്ങളുടെ ഒല്ലാമയ്ക്കായി തിരയുന്നു 🦙",
|
||||||
"running": "ഒല്ലാമ പ്രവര്ത്തിക്കുന്നു 🦙",
|
"running": "ഒല്ലാമ പ്രവര്ത്തിക്കുന്നു 🦙",
|
||||||
"notRunning": "ഒല്ലാമയുമായി ബന്ധിപ്പിക്കാന് കഴിയുന്നില്ല 🦙"
|
"notRunning": "ഒല്ലാമയുമായി ബന്ധിപ്പിക്കാന് കഴിയുന്നില്ല 🦙",
|
||||||
|
"connectionError": "നിങ്ങൾക്ക് കണക്ഷൻ പ്രശ്നം ഉണ്ടെന്നു കാണുന്നു. ഈ <anchor>ഡോക്യുമെന്റേഷൻ</anchor> പരിശോധിക്കാൻ കൂടുതൽ സഹായത്തിനായി."
|
||||||
},
|
},
|
||||||
"formError": {
|
"formError": {
|
||||||
"noModel": "ദയവായി ഒരു മോഡല് തിരഞ്ഞെടുക്കുക",
|
"noModel": "ദയവായി ഒരു മോഡല് തിരഞ്ഞെടുക്കുക",
|
||||||
|
@ -47,13 +47,13 @@
|
|||||||
"label": "ചാറ്റ് ചരിത്രം, അറിവ് അടിസ്ഥാനം, പ്രോംപ്റ്റുകൾ എക്സ്പോർട്ട് ചെയ്യുക",
|
"label": "ചാറ്റ് ചരിത്രം, അറിവ് അടിസ്ഥാനം, പ്രോംപ്റ്റുകൾ എക്സ്പോർട്ട് ചെയ്യുക",
|
||||||
"button": "ഡാറ്റ എക്സ്പോർട്ട് ചെയ്യുക",
|
"button": "ഡാറ്റ എക്സ്പോർട്ട് ചെയ്യുക",
|
||||||
"success": "എക്സ്പോർട്ട് വിജയകരമായി"
|
"success": "എക്സ്പോർട്ട് വിജയകരമായി"
|
||||||
},
|
},
|
||||||
"import": {
|
"import": {
|
||||||
"label": "ചാറ്റ് ചരിത്രം, അറിവ് അടിസ്ഥാനം, പ്രോംപ്റ്റുകൾ ഇമ്പോർട്ട് ചെയ്യുക",
|
"label": "ചാറ്റ് ചരിത്രം, അറിവ് അടിസ്ഥാനം, പ്രോംപ്റ്റുകൾ ഇമ്പോർട്ട് ചെയ്യുക",
|
||||||
"button": "ഡാറ്റ ഇമ്പോർട്ട് ചെയ്യുക",
|
"button": "ഡാറ്റ ഇമ്പോർട്ട് ചെയ്യുക",
|
||||||
"success": "ഇമ്പോർട്ട് വിജയകരമായി",
|
"success": "ഇമ്പോർട്ട് വിജയകരമായി",
|
||||||
"error": "ഇമ്പോർട്ട് പിശക്"
|
"error": "ഇമ്പോർട്ട് പിശക്"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tts": {
|
"tts": {
|
||||||
"heading": "ടെക്സ്റ്റ്-ടു-സ്പീച്ച് ക്രമീകരണങ്ങൾ",
|
"heading": "ടെക്സ്റ്റ്-ടു-സ്പീച്ച് ക്രമീകരണങ്ങൾ",
|
||||||
@ -248,6 +248,17 @@
|
|||||||
"webSearchFollowUpPromptHelp": "പ്രോംപ്റ്റില് നിന്ന് `{chat_history}` യും `{question}` യും നീക്കം ചെയ്യരുത്.",
|
"webSearchFollowUpPromptHelp": "പ്രോംപ്റ്റില് നിന്ന് `{chat_history}` യും `{question}` യും നീക്കം ചെയ്യരുത്.",
|
||||||
"webSearchFollowUpPromptError": "ദയവായി നിങ്ങളുടെ വെബ് തിരയല് തുടര്പ്രോംപ്റ്റ് നല്കുക!",
|
"webSearchFollowUpPromptError": "ദയവായി നിങ്ങളുടെ വെബ് തിരയല് തുടര്പ്രോംപ്റ്റ് നല്കുക!",
|
||||||
"webSearchFollowUpPromptPlaceholder": "നിങ്ങളുടെ വെബ് തിരയല് തുടര്പ്രോംപ്റ്റ്"
|
"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 കോൺഫിഗർ ചെയ്യാം. കോൺഫിഗറേഷനെക്കുറിച്ച് കൂടുതലറിയാൻ, <anchor>ഇവിടെ ക്ലിക്കുചെയ്യുക</anchor>."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -2,7 +2,8 @@
|
|||||||
"ollamaState": {
|
"ollamaState": {
|
||||||
"searching": "Поиск вашего Ollama 🦙",
|
"searching": "Поиск вашего Ollama 🦙",
|
||||||
"running": "Ollama работает 🦙",
|
"running": "Ollama работает 🦙",
|
||||||
"notRunning": "Не удалось подключиться к Ollama 🦙"
|
"notRunning": "Не удалось подключиться к Ollama 🦙",
|
||||||
|
"connectionError": "Похоже, у вас возникла ошибка соединения. Пожалуйста, обратитесь к этой <anchor>документации</anchor> для устранения неисправностей."
|
||||||
},
|
},
|
||||||
"formError": {
|
"formError": {
|
||||||
"noModel": "Пожалуйста, выберите модель",
|
"noModel": "Пожалуйста, выберите модель",
|
||||||
|
@ -245,6 +245,17 @@
|
|||||||
"webSearchFollowUpPromptHelp": "Не удаляйте `{chat_history}` и `{question}` из подсказки.",
|
"webSearchFollowUpPromptHelp": "Не удаляйте `{chat_history}` и `{question}` из подсказки.",
|
||||||
"webSearchFollowUpPromptError": "Введите подсказку для последующего веб-поиска!",
|
"webSearchFollowUpPromptError": "Введите подсказку для последующего веб-поиска!",
|
||||||
"webSearchFollowUpPromptPlaceholder": "Ваша подсказка для последующего веб-поиска"
|
"webSearchFollowUpPromptPlaceholder": "Ваша подсказка для последующего веб-поиска"
|
||||||
|
},
|
||||||
|
"advanced": {
|
||||||
|
"label": "Расширенная конфигурация URL Ollama",
|
||||||
|
"urlRewriteEnabled": {
|
||||||
|
"label": "Включить или отключить пользовательский исходный URL"
|
||||||
|
},
|
||||||
|
"rewriteUrl": {
|
||||||
|
"label": "Пользовательский исходный URL",
|
||||||
|
"placeholder": "Введите пользовательский исходный URL"
|
||||||
|
},
|
||||||
|
"help": "Если у вас возникают проблемы с подключением к Ollama на странице помощника, вы можете настроить пользовательский исходный URL. Чтобы узнать больше о конфигурации, <anchor>нажмите здесь</anchor>."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -2,7 +2,8 @@
|
|||||||
"ollamaState": {
|
"ollamaState": {
|
||||||
"searching": "正在搜索您的Ollama 🦙",
|
"searching": "正在搜索您的Ollama 🦙",
|
||||||
"running": "Ollama正在运行 🦙",
|
"running": "Ollama正在运行 🦙",
|
||||||
"notRunning": "无法连接到Ollama 🦙"
|
"notRunning": "无法连接到Ollama 🦙",
|
||||||
|
"connectionError": "看起来你正在遇到连接错误。请参阅这<anchor>文档</anchor>进行故障排除。"
|
||||||
},
|
},
|
||||||
"formError": {
|
"formError": {
|
||||||
"noModel": "请选择一个模型",
|
"noModel": "请选择一个模型",
|
||||||
|
@ -249,6 +249,17 @@
|
|||||||
"webSearchFollowUpPromptHelp": "请勿从提示词中删除 `{chat_history}` 和 `{question}`。",
|
"webSearchFollowUpPromptHelp": "请勿从提示词中删除 `{chat_history}` 和 `{question}`。",
|
||||||
"webSearchFollowUpPromptError": "请输入您的网页搜索追问提示词!",
|
"webSearchFollowUpPromptError": "请输入您的网页搜索追问提示词!",
|
||||||
"webSearchFollowUpPromptPlaceholder": "您的网页搜索追问提示词"
|
"webSearchFollowUpPromptPlaceholder": "您的网页搜索追问提示词"
|
||||||
|
},
|
||||||
|
"advanced": {
|
||||||
|
"label": "Ollama URL 高级配置",
|
||||||
|
"urlRewriteEnabled": {
|
||||||
|
"label": "启用或禁用自定义来源 URL"
|
||||||
|
},
|
||||||
|
"rewriteUrl": {
|
||||||
|
"label": "自定义来源 URL",
|
||||||
|
"placeholder": "输入自定义来源 URL"
|
||||||
|
},
|
||||||
|
"help": "如果您在 Page Assist 上与 Ollama 有连接问题,您可以配置自定义来源 URL。要了解更多关于配置的信息,<anchor>点击此处</anchor>。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
49
src/components/Common/AdvanceOllamaSettings.tsx
Normal file
49
src/components/Common/AdvanceOllamaSettings.tsx
Normal file
@ -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 (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex sm:flex-row flex-col space-y-4 sm:space-y-0 sm:justify-between">
|
||||||
|
<span className="text-gray-500 dark:text-neutral-50 ">
|
||||||
|
{t("ollamaSettings.settings.advanced.urlRewriteEnabled.label")}
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<Switch
|
||||||
|
className="mt-4 sm:mt-0"
|
||||||
|
checked={urlRewriteEnabled}
|
||||||
|
onChange={(checked) => setUrlRewriteEnabled(checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col space-y-4 sm:space-y-0 sm:justify-between">
|
||||||
|
<span className="text-gray-500 dark:text-neutral-50 mb-3">
|
||||||
|
{t("ollamaSettings.settings.advanced.rewriteUrl.label")}
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<Input
|
||||||
|
className="w-full"
|
||||||
|
value={rewriteUrl}
|
||||||
|
disabled={!urlRewriteEnabled}
|
||||||
|
placeholder={t(
|
||||||
|
"ollamaSettings.settings.advanced.rewriteUrl.placeholder"
|
||||||
|
)}
|
||||||
|
onChange={(e) => setRewriteUrl(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
10
src/components/Common/PageAssistLoader.tsx
Normal file
10
src/components/Common/PageAssistLoader.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
|
||||||
|
export const PageAssistLoader = () => {
|
||||||
|
return (
|
||||||
|
<div className="fixed bg-[#171717] top-0 left-0 right-0 bottom-0 w-full h-screen z-50 overflow-hidden opacity-75 flex flex-col items-center justify-center">
|
||||||
|
<p className="text-center text-white text-lg mt-4">
|
||||||
|
Loading...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -60,7 +60,7 @@ export const ModelsBody = () => {
|
|||||||
|
|
||||||
form.reset()
|
form.reset()
|
||||||
|
|
||||||
chrome.runtime.sendMessage({
|
browser.runtime.sendMessage({
|
||||||
type: "pull_model",
|
type: "pull_model",
|
||||||
modelName
|
modelName
|
||||||
})
|
})
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
|
import { cleanUrl } from "@/libs/clean-url"
|
||||||
import { useQuery } from "@tanstack/react-query"
|
import { useQuery } from "@tanstack/react-query"
|
||||||
import { RotateCcw } from "lucide-react"
|
import { RotateCcw } from "lucide-react"
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { Trans, useTranslation } from "react-i18next"
|
||||||
import {
|
import {
|
||||||
getOllamaURL,
|
getOllamaURL,
|
||||||
isOllamaRunning,
|
isOllamaRunning,
|
||||||
@ -79,6 +80,23 @@ export const PlaygroundEmpty = () => {
|
|||||||
<RotateCcw className="h-4 w-4 mr-3" />
|
<RotateCcw className="h-4 w-4 mr-3" />
|
||||||
{t("common:retry")}
|
{t("common:retry")}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{ollamaURL &&
|
||||||
|
cleanUrl(ollamaURL) !== "http://127.0.0.1:11434" && (
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400 mb-4 text-center">
|
||||||
|
<Trans
|
||||||
|
i18nKey="playground:ollamaState.connectionError"
|
||||||
|
components={{
|
||||||
|
anchor: (
|
||||||
|
<a
|
||||||
|
href="https://github.com/n4ze3m/page-assist/blob/main/docs/connection-issue.md"
|
||||||
|
target="__blank"
|
||||||
|
className="text-blue-600 dark:text-blue-400"></a>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -11,7 +11,7 @@ export const AboutApp = () => {
|
|||||||
const { data, status } = useQuery({
|
const { data, status } = useQuery({
|
||||||
queryKey: ["fetchOllamURL"],
|
queryKey: ["fetchOllamURL"],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const chromeVersion = chrome.runtime.getManifest().version
|
const chromeVersion = browser.runtime.getManifest().version
|
||||||
try {
|
try {
|
||||||
const url = await getOllamaURL()
|
const url = await getOllamaURL()
|
||||||
const req = await fetch(`${cleanUrl(url)}/api/version`)
|
const req = await fetch(`${cleanUrl(url)}/api/version`)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useMutation, useQuery } from "@tanstack/react-query"
|
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 { useState } from "react"
|
||||||
import { SaveButton } from "~/components/Common/SaveButton"
|
import { SaveButton } from "~/components/Common/SaveButton"
|
||||||
import {
|
import {
|
||||||
@ -12,10 +12,13 @@ import {
|
|||||||
setOllamaURL as saveOllamaURL
|
setOllamaURL as saveOllamaURL
|
||||||
} from "~/services/ollama"
|
} from "~/services/ollama"
|
||||||
import { SettingPrompt } from "./prompt"
|
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 = () => {
|
export const SettingsOllama = () => {
|
||||||
const [ollamaURL, setOllamaURL] = useState<string>("")
|
const [ollamaURL, setOllamaURL] = useState<string>("")
|
||||||
|
|
||||||
const { t } = useTranslation("settings")
|
const { t } = useTranslation("settings")
|
||||||
|
|
||||||
const { data: ollamaInfo, status } = useQuery({
|
const { data: ollamaInfo, status } = useQuery({
|
||||||
@ -61,7 +64,7 @@ export const SettingsOllama = () => {
|
|||||||
</h2>
|
</h2>
|
||||||
<div className="border border-b border-gray-200 dark:border-gray-600 mt-3 mb-6"></div>
|
<div className="border border-b border-gray-200 dark:border-gray-600 mt-3 mb-6"></div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="mb-3">
|
||||||
<label
|
<label
|
||||||
htmlFor="ollamaURL"
|
htmlFor="ollamaURL"
|
||||||
className="text-sm font-medium dark:text-gray-200">
|
className="text-sm font-medium dark:text-gray-200">
|
||||||
@ -78,6 +81,36 @@ export const SettingsOllama = () => {
|
|||||||
className="w-full p-2 border border-gray-300 rounded-md dark:bg-[#262626] dark:text-gray-100"
|
className="w-full p-2 border border-gray-300 rounded-md dark:bg-[#262626] dark:text-gray-100"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<Collapse
|
||||||
|
size="small"
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
key: "1",
|
||||||
|
label: (
|
||||||
|
<div>
|
||||||
|
<h2 className="text-base font-semibold leading-7 text-gray-900 dark:text-white">
|
||||||
|
{t("ollamaSettings.settings.advanced.label")}
|
||||||
|
</h2>
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400 mb-4">
|
||||||
|
<Trans
|
||||||
|
i18nKey="settings:ollamaSettings.settings.advanced.help"
|
||||||
|
components={{
|
||||||
|
anchor: (
|
||||||
|
<a
|
||||||
|
href="https://github.com/n4ze3m/page-assist/blob/main/docs/connection-issue.md#solutions"
|
||||||
|
target="__blank"
|
||||||
|
className="text-blue-600 dark:text-blue-400"></a>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
children: <AdvanceOllamaSettings />
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<SaveButton
|
<SaveButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -130,7 +163,9 @@ export const SettingsOllama = () => {
|
|||||||
0
|
0
|
||||||
}
|
}
|
||||||
showSearch
|
showSearch
|
||||||
placeholder={t("ollamaSettings.settings.ragSettings.model.placeholder")}
|
placeholder={t(
|
||||||
|
"ollamaSettings.settings.ragSettings.model.placeholder"
|
||||||
|
)}
|
||||||
style={{ width: "100%" }}
|
style={{ width: "100%" }}
|
||||||
className="mt-4"
|
className="mt-4"
|
||||||
options={ollamaInfo.models?.map((model) => ({
|
options={ollamaInfo.models?.map((model) => ({
|
||||||
@ -143,27 +178,39 @@ export const SettingsOllama = () => {
|
|||||||
<Form.Item
|
<Form.Item
|
||||||
name="chunkSize"
|
name="chunkSize"
|
||||||
label={t("ollamaSettings.settings.ragSettings.chunkSize.label")}
|
label={t("ollamaSettings.settings.ragSettings.chunkSize.label")}
|
||||||
rules={[
|
|
||||||
{ required: true, message: t("ollamaSettings.settings.ragSettings.chunkSize.required")
|
|
||||||
}
|
|
||||||
]}>
|
|
||||||
<InputNumber
|
|
||||||
style={{ width: "100%" }}
|
|
||||||
placeholder={t("ollamaSettings.settings.ragSettings.chunkSize.placeholder")}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
name="chunkOverlap"
|
|
||||||
label={t("ollamaSettings.settings.ragSettings.chunkOverlap.label")}
|
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t("ollamaSettings.settings.ragSettings.chunkOverlap.required")
|
message: t(
|
||||||
|
"ollamaSettings.settings.ragSettings.chunkSize.required"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
]}>
|
]}>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
style={{ width: "100%" }}
|
style={{ width: "100%" }}
|
||||||
placeholder={t("ollamaSettings.settings.ragSettings.chunkOverlap.placeholder")}
|
placeholder={t(
|
||||||
|
"ollamaSettings.settings.ragSettings.chunkSize.placeholder"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="chunkOverlap"
|
||||||
|
label={t(
|
||||||
|
"ollamaSettings.settings.ragSettings.chunkOverlap.label"
|
||||||
|
)}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t(
|
||||||
|
"ollamaSettings.settings.ragSettings.chunkOverlap.required"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]}>
|
||||||
|
<InputNumber
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
placeholder={t(
|
||||||
|
"ollamaSettings.settings.ragSettings.chunkOverlap.placeholder"
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
|
import { cleanUrl } from "@/libs/clean-url"
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query"
|
import { useQuery, useQueryClient } from "@tanstack/react-query"
|
||||||
import { Select } from "antd"
|
import { Select } from "antd"
|
||||||
import { RotateCcw } from "lucide-react"
|
import { RotateCcw } from "lucide-react"
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { Trans, useTranslation } from "react-i18next"
|
||||||
import { useMessage } from "~/hooks/useMessage"
|
import { useMessage } from "~/hooks/useMessage"
|
||||||
import {
|
import {
|
||||||
getAllModels,
|
getAllModels,
|
||||||
@ -91,6 +92,22 @@ export const EmptySidePanel = () => {
|
|||||||
<RotateCcw className="h-4 w-4 mr-3" />
|
<RotateCcw className="h-4 w-4 mr-3" />
|
||||||
{t("common:retry")}
|
{t("common:retry")}
|
||||||
</button>
|
</button>
|
||||||
|
{ollamaURL &&
|
||||||
|
cleanUrl(ollamaURL) !== "http://127.0.0.1:11434" && (
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400 mb-4 text-center">
|
||||||
|
<Trans
|
||||||
|
i18nKey="playground:ollamaState.connectionError"
|
||||||
|
components={{
|
||||||
|
anchor: (
|
||||||
|
<a
|
||||||
|
href="https://github.com/n4ze3m/page-assist/blob/main/docs/connection-issue.md"
|
||||||
|
target="__blank"
|
||||||
|
className="text-blue-600 dark:text-blue-400"></a>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { getOllamaURL, isOllamaRunning } from "../services/ollama"
|
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) => {
|
const progressHuman = (completed: number, total: number) => {
|
||||||
return ((completed / total) * 100).toFixed(0) + "%"
|
return ((completed / total) * 100).toFixed(0) + "%"
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearBadge = () => {
|
const clearBadge = () => {
|
||||||
chrome.action.setBadgeText({ text: "" })
|
setBadgeText({ text: "" })
|
||||||
chrome.action.setTitle({ title: "" })
|
setTitle({ title: "" })
|
||||||
}
|
}
|
||||||
const streamDownload = async (url: string, model: string) => {
|
const streamDownload = async (url: string, model: string) => {
|
||||||
url += "/api/pull"
|
url += "/api/pull"
|
||||||
@ -42,16 +42,16 @@ const streamDownload = async (url: string, model: string) => {
|
|||||||
completed?: number
|
completed?: number
|
||||||
}
|
}
|
||||||
if (json.total && json.completed) {
|
if (json.total && json.completed) {
|
||||||
chrome.action.setBadgeText({
|
setBadgeText({
|
||||||
text: progressHuman(json.completed, json.total)
|
text: progressHuman(json.completed, json.total)
|
||||||
})
|
})
|
||||||
chrome.action.setBadgeBackgroundColor({ color: "#0000FF" })
|
setBadgeBackgroundColor({ color: "#0000FF" })
|
||||||
} else {
|
} else {
|
||||||
chrome.action.setBadgeText({ text: "🏋️♂️" })
|
setBadgeText({ text: "🏋️♂️" })
|
||||||
chrome.action.setBadgeBackgroundColor({ color: "#FFFFFF" })
|
setBadgeBackgroundColor({ color: "#FFFFFF" })
|
||||||
}
|
}
|
||||||
|
|
||||||
chrome.action.setTitle({ title: json.status })
|
setTitle({ title: json.status })
|
||||||
|
|
||||||
if (json.status === "success") {
|
if (json.status === "success") {
|
||||||
isSuccess = true
|
isSuccess = true
|
||||||
@ -62,13 +62,13 @@ const streamDownload = async (url: string, model: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
chrome.action.setBadgeText({ text: "✅" })
|
setBadgeText({ text: "✅" })
|
||||||
chrome.action.setBadgeBackgroundColor({ color: "#00FF00" })
|
setBadgeBackgroundColor({ color: "#00FF00" })
|
||||||
chrome.action.setTitle({ title: "Model pulled successfully" })
|
setTitle({ title: "Model pulled successfully" })
|
||||||
} else {
|
} else {
|
||||||
chrome.action.setBadgeText({ text: "❌" })
|
setBadgeText({ text: "❌" })
|
||||||
chrome.action.setBadgeBackgroundColor({ color: "#FF0000" })
|
setBadgeBackgroundColor({ color: "#FF0000" })
|
||||||
chrome.action.setTitle({ title: "Model pull failed" })
|
setTitle({ title: "Model pull failed" })
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -77,29 +77,18 @@ const streamDownload = async (url: string, model: string) => {
|
|||||||
}
|
}
|
||||||
export default defineBackground({
|
export default defineBackground({
|
||||||
main() {
|
main() {
|
||||||
const storage = new Storage()
|
browser.runtime.onMessage.addListener(async (message) => {
|
||||||
|
|
||||||
chrome.runtime.onMessage.addListener(async (message) => {
|
|
||||||
if (message.type === "sidepanel") {
|
if (message.type === "sidepanel") {
|
||||||
chrome.tabs.query(
|
browser.sidebarAction.open()
|
||||||
{ active: true, currentWindow: true },
|
|
||||||
async (tabs) => {
|
|
||||||
const tab = tabs[0]
|
|
||||||
chrome.sidePanel.open({
|
|
||||||
// tabId: tab.id!,
|
|
||||||
windowId: tab.windowId!
|
|
||||||
})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else if (message.type === "pull_model") {
|
} else if (message.type === "pull_model") {
|
||||||
const ollamaURL = await getOllamaURL()
|
const ollamaURL = await getOllamaURL()
|
||||||
|
|
||||||
const isRunning = await isOllamaRunning()
|
const isRunning = await isOllamaRunning()
|
||||||
|
|
||||||
if (!isRunning) {
|
if (!isRunning) {
|
||||||
chrome.action.setBadgeText({ text: "E" })
|
setBadgeText({ text: "E" })
|
||||||
chrome.action.setBadgeBackgroundColor({ color: "#FF0000" })
|
setBadgeBackgroundColor({ color: "#FF0000" })
|
||||||
chrome.action.setTitle({ title: "Ollama is not running" })
|
setTitle({ title: "Ollama is not running" })
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
clearBadge()
|
clearBadge()
|
||||||
}, 5000)
|
}, 5000)
|
||||||
@ -109,47 +98,73 @@ export default defineBackground({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
chrome.action.onClicked.addListener((tab) => {
|
if (import.meta.env.BROWSER === "chrome") {
|
||||||
chrome.tabs.create({ url: chrome.runtime.getURL("options.html") })
|
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) => {
|
browser.contextMenus.create({
|
||||||
switch (command) {
|
id: "open-side-panel-pa",
|
||||||
case "execute_side_panel":
|
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(
|
chrome.tabs.query(
|
||||||
{ active: true, currentWindow: true },
|
{ active: true, currentWindow: true },
|
||||||
async (tabs) => {
|
async (tabs) => {
|
||||||
const tab = tabs[0]
|
const tab = tabs[0]
|
||||||
chrome.sidePanel.open({
|
chrome.sidePanel.open({
|
||||||
windowId: tab.windowId!
|
tabId: tab.id!
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
break
|
}
|
||||||
default:
|
})
|
||||||
break
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
chrome.contextMenus.create({
|
browser.commands.onCommand.addListener((command) => {
|
||||||
id: "open-side-panel-pa",
|
switch (command) {
|
||||||
title: browser.i18n.getMessage("openSidePanelToChat"),
|
case "execute_side_panel":
|
||||||
contexts: ["all"]
|
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 (import.meta.env.BROWSER === "firefox") {
|
||||||
if (info.menuItemId === "open-side-panel-pa") {
|
browser.contextMenus.onClicked.addListener((info, tab) => {
|
||||||
chrome.tabs.query(
|
if (info.menuItemId === "open-side-panel-pa") {
|
||||||
{ active: true, currentWindow: true },
|
browser.sidebarAction.toggle()
|
||||||
async (tabs) => {
|
}
|
||||||
const tab = tabs[0]
|
})
|
||||||
chrome.sidePanel.open({
|
|
||||||
tabId: tab.id!
|
browser.commands.onCommand.addListener((command) => {
|
||||||
})
|
switch (command) {
|
||||||
}
|
case "execute_side_panel":
|
||||||
)
|
browser.sidebarAction.toggle()
|
||||||
}
|
break
|
||||||
})
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
persistent: true
|
persistent: true
|
||||||
})
|
})
|
||||||
|
@ -9,7 +9,7 @@ export default defineContentScript({
|
|||||||
`[Page Assist Extension] Pulling ${modelName} model. For more details, check the extension icon.`
|
`[Page Assist Extension] Pulling ${modelName} model. For more details, check the extension icon.`
|
||||||
)
|
)
|
||||||
|
|
||||||
await chrome.runtime.sendMessage({
|
await browser.runtime.sendMessage({
|
||||||
type: "pull_model",
|
type: "pull_model",
|
||||||
modelName
|
modelName
|
||||||
})
|
})
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
<title>Page Assist - A Web UI for Local AI Models</title>
|
<title>Page Assist - A Web UI for Local AI Models</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="manifest.type" content="browser_action" />
|
<meta name="manifest.type" content="browser_action" />
|
||||||
|
<meta name="manifest.open_at_install" content="false" />
|
||||||
<link href="~/assets/tailwind.css" rel="stylesheet" />
|
<link href="~/assets/tailwind.css" rel="stylesheet" />
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
</head>
|
</head>
|
||||||
|
@ -2,7 +2,6 @@ import { useEffect, useState } from "react"
|
|||||||
import { notification } from "antd"
|
import { notification } from "antd"
|
||||||
import { getVoice, isSSMLEnabled } from "@/services/tts"
|
import { getVoice, isSSMLEnabled } from "@/services/tts"
|
||||||
import { markdownToSSML } from "@/utils/markdown-to-ssml"
|
import { markdownToSSML } from "@/utils/markdown-to-ssml"
|
||||||
|
|
||||||
type VoiceOptions = {
|
type VoiceOptions = {
|
||||||
utterance: string
|
utterance: string
|
||||||
}
|
}
|
||||||
@ -17,16 +16,28 @@ export const useTTS = () => {
|
|||||||
if (isSSML) {
|
if (isSSML) {
|
||||||
utterance = markdownToSSML(utterance)
|
utterance = markdownToSSML(utterance)
|
||||||
}
|
}
|
||||||
chrome.tts.speak(utterance, {
|
if (import.meta.env.BROWSER === "chrome") {
|
||||||
voiceName: voice,
|
chrome.tts.speak(utterance, {
|
||||||
onEvent(event) {
|
voiceName: voice,
|
||||||
if (event.type === "start") {
|
onEvent(event) {
|
||||||
setIsSpeaking(true)
|
if (event.type === "start") {
|
||||||
} else if (event.type === "end") {
|
setIsSpeaking(true)
|
||||||
setIsSpeaking(false)
|
} 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) {
|
} catch (error) {
|
||||||
notification.error({
|
notification.error({
|
||||||
message: "Error",
|
message: "Error",
|
||||||
@ -36,7 +47,11 @@ export const useTTS = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cancel = () => {
|
const cancel = () => {
|
||||||
chrome.tts.stop()
|
if (import.meta.env.BROWSER === "chrome") {
|
||||||
|
chrome.tts.stop()
|
||||||
|
} else {
|
||||||
|
window.speechSynthesis.cancel()
|
||||||
|
}
|
||||||
setIsSpeaking(false)
|
setIsSpeaking(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import {
|
|||||||
isTweet,
|
isTweet,
|
||||||
isTwitterTimeline,
|
isTwitterTimeline,
|
||||||
parseTweet,
|
parseTweet,
|
||||||
parseTwitterTimeline,
|
parseTwitterTimeline
|
||||||
} from "@/parser/twitter"
|
} from "@/parser/twitter"
|
||||||
import { isGoogleDocs, parseGoogleDocs } from "@/parser/google-docs"
|
import { isGoogleDocs, parseGoogleDocs } from "@/parser/google-docs"
|
||||||
import { cleanUnwantedUnicode } from "@/utils/clean"
|
import { cleanUnwantedUnicode } from "@/utils/clean"
|
||||||
@ -24,18 +24,35 @@ const _getHtml = () => {
|
|||||||
|
|
||||||
export const getDataFromCurrentTab = async () => {
|
export const getDataFromCurrentTab = async () => {
|
||||||
const result = new Promise((resolve) => {
|
const result = new Promise((resolve) => {
|
||||||
chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => {
|
if (import.meta.env.BROWSER === "chrome") {
|
||||||
const tab = tabs[0]
|
chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => {
|
||||||
|
const tab = tabs[0]
|
||||||
|
|
||||||
const data = await chrome.scripting.executeScript({
|
const data = await chrome.scripting.executeScript({
|
||||||
target: { tabId: tab.id },
|
target: { tabId: tab.id },
|
||||||
func: _getHtml
|
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) {
|
const data = await browser.scripting.executeScript({
|
||||||
resolve(data[0].result)
|
target: { tabId: tab.id },
|
||||||
}
|
func: _getHtml
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (data.length > 0) {
|
||||||
|
resolve(data[0].result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}) as Promise<{
|
}) as Promise<{
|
||||||
url: string
|
url: string
|
||||||
content: string
|
content: string
|
||||||
|
@ -1,31 +1,63 @@
|
|||||||
export const chromeRunTime = async function (domain: string) {
|
import { getAdvancedOllamaSettings } from "@/services/app"
|
||||||
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}`
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
await chrome.declarativeNetRequest.updateDynamicRules({
|
export const urlRewriteRuntime = async function (
|
||||||
removeRuleIds: rules.map((r) => r.id),
|
domain: string,
|
||||||
// @ts-ignore
|
type = "ollama"
|
||||||
addRules: rules
|
) {
|
||||||
})
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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"]
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { BaseDocumentLoader } from "langchain/document_loaders/base"
|
import { BaseDocumentLoader } from "langchain/document_loaders/base"
|
||||||
import { Document } from "@langchain/core/documents"
|
import { Document } from "@langchain/core/documents"
|
||||||
import { compile } from "html-to-text"
|
import { compile } from "html-to-text"
|
||||||
import { chromeRunTime } from "~/libs/runtime"
|
import { urlRewriteRuntime } from "~/libs/runtime"
|
||||||
import { YtTranscript } from "yt-transcript"
|
import { YtTranscript } from "yt-transcript"
|
||||||
import { isWikipedia, parseWikipedia } from "@/parser/wiki"
|
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)
|
const fetchHTML = await fetch(this.url)
|
||||||
let html = await fetchHTML.text()
|
let html = await fetchHTML.text()
|
||||||
|
|
||||||
@ -111,11 +111,6 @@ export class PageAssistHtmlLoader
|
|||||||
html = parseWikipedia(await fetchHTML.text())
|
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({
|
const htmlCompiler = compile({
|
||||||
wordwrap: false,
|
wordwrap: false,
|
||||||
selectors: [
|
selectors: [
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
|
|
||||||
export const isGoogleDocs = (url: string) => {
|
export const isGoogleDocs = (url: string) => {
|
||||||
const GOOGLE_DOCS_REGEX = /docs\.google\.com\/document/g
|
const GOOGLE_DOCS_REGEX = /docs\.google\.com\/document/g
|
||||||
return GOOGLE_DOCS_REGEX.test(url)
|
return GOOGLE_DOCS_REGEX.test(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getGoogleDocs = () => {
|
const getGoogleDocs = () => {
|
||||||
@ -96,19 +95,36 @@ const getGoogleDocs = () => {
|
|||||||
|
|
||||||
export const parseGoogleDocs = async () => {
|
export const parseGoogleDocs = async () => {
|
||||||
const result = new Promise((resolve) => {
|
const result = new Promise((resolve) => {
|
||||||
chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => {
|
if (import.meta.env.BROWSER === "chrome") {
|
||||||
const tab = tabs[0]
|
chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => {
|
||||||
|
const tab = tabs[0]
|
||||||
|
|
||||||
const data = await chrome.scripting.executeScript({
|
const data = await chrome.scripting.executeScript({
|
||||||
target: { tabId: tab.id },
|
target: { tabId: tab.id },
|
||||||
world: "MAIN",
|
world: "MAIN",
|
||||||
func: getGoogleDocs
|
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) {
|
const data = await browser.scripting.executeScript({
|
||||||
resolve(data[0].result)
|
target: { tabId: tab.id },
|
||||||
}
|
func: getGoogleDocs
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (data.length > 0) {
|
||||||
|
resolve(data[0].result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}) as Promise<{
|
}) as Promise<{
|
||||||
content?: string
|
content?: string
|
||||||
}>
|
}>
|
||||||
|
35
src/routes/chrome.tsx
Normal file
35
src/routes/chrome.tsx
Normal file
@ -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 (
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<OptionIndex />} />
|
||||||
|
<Route path="/settings" element={<OptionSettings />} />
|
||||||
|
<Route path="/settings/model" element={<OptionModal />} />
|
||||||
|
<Route path="/settings/prompt" element={<OptionPrompt />} />
|
||||||
|
<Route path="/settings/ollama" element={<OptionOllamaSettings />} />
|
||||||
|
<Route path="/settings/share" element={<OptionShare />} />
|
||||||
|
<Route path="/settings/knowledge" element={<OptionKnowledgeBase />} />
|
||||||
|
<Route path="/settings/about" element={<OptionAbout />} />
|
||||||
|
</Routes>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SidepanelRoutingChrome = () => {
|
||||||
|
return (
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<SidepanelChat />} />
|
||||||
|
<Route path="/settings" element={<SidepanelSettings />} />
|
||||||
|
</Routes>
|
||||||
|
)
|
||||||
|
}
|
39
src/routes/firefox.tsx
Normal file
39
src/routes/firefox.tsx
Normal file
@ -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 (
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<OptionIndex />} />
|
||||||
|
<Route path="/settings" element={<OptionSettings />} />
|
||||||
|
<Route path="/settings/model" element={<OptionModal />} />
|
||||||
|
<Route path="/settings/prompt" element={<OptionPrompt />} />
|
||||||
|
<Route path="/settings/ollama" element={<OptionOllamaSettings />} />
|
||||||
|
<Route path="/settings/share" element={<OptionShare />} />
|
||||||
|
<Route path="/settings/knowledge" element={<OptionKnowledgeBase />} />
|
||||||
|
<Route path="/settings/about" element={<OptionAbout />} />
|
||||||
|
</Routes>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SidepanelRoutingFirefox = () => {
|
||||||
|
return (
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<SidepanelChat />} />
|
||||||
|
<Route path="/settings" element={<SidepanelSettings />} />
|
||||||
|
</Routes>
|
||||||
|
)
|
||||||
|
}
|
@ -1,16 +1,9 @@
|
|||||||
import { Route, Routes } from "react-router-dom"
|
import { Suspense } from "react"
|
||||||
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 { useTranslation } from "react-i18next"
|
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 = () => {
|
export const OptionRouting = () => {
|
||||||
const { mode } = useDarkMode()
|
const { mode } = useDarkMode()
|
||||||
@ -21,16 +14,13 @@ export const OptionRouting = () => {
|
|||||||
className={`${mode === "dark" ? "dark" : "light"} ${
|
className={`${mode === "dark" ? "dark" : "light"} ${
|
||||||
i18n.language === "ru" ? "onest" : "inter"
|
i18n.language === "ru" ? "onest" : "inter"
|
||||||
}`}>
|
}`}>
|
||||||
<Routes>
|
<Suspense fallback={<PageAssistLoader />}>
|
||||||
<Route path="/" element={<OptionIndex />} />
|
{import.meta.env.BROWSER === "chrome" ? (
|
||||||
<Route path="/settings" element={<OptionSettings />} />
|
<OptionRoutingChrome />
|
||||||
<Route path="/settings/model" element={<OptionModal />} />
|
) : (
|
||||||
<Route path="/settings/prompt" element={<OptionPrompt />} />
|
<OptionRoutingFirefox />
|
||||||
<Route path="/settings/ollama" element={<OptionOllamaSettings />} />
|
)}
|
||||||
<Route path="/settings/share" element={<OptionShare />} />
|
</Suspense>
|
||||||
<Route path="/settings/knowledge" element={<OptionKnowledgeBase />} />
|
|
||||||
<Route path="/settings/about" element={<OptionAbout />} />
|
|
||||||
</Routes>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -44,10 +34,13 @@ export const SidepanelRouting = () => {
|
|||||||
className={`${mode === "dark" ? "dark" : "light"} ${
|
className={`${mode === "dark" ? "dark" : "light"} ${
|
||||||
i18n.language === "ru" ? "onest" : "inter"
|
i18n.language === "ru" ? "onest" : "inter"
|
||||||
}`}>
|
}`}>
|
||||||
<Routes>
|
<Suspense fallback={<PageAssistLoader />}>
|
||||||
<Route path="/" element={<SidepanelChat />} />
|
{import.meta.env.BROWSER === "chrome" ? (
|
||||||
<Route path="/settings" element={<SidepanelSettings />} />
|
<SidepanelRoutingChrome />
|
||||||
</Routes>
|
) : (
|
||||||
|
<SidepanelRoutingFirefox />
|
||||||
|
)}
|
||||||
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import OptionLayout from "~/components/Layouts/Layout"
|
import OptionLayout from "~/components/Layouts/Layout"
|
||||||
import { Playground } from "~/components/Option/Playground/Playground"
|
import { Playground } from "~/components/Option/Playground/Playground"
|
||||||
|
|
||||||
export const OptionIndex = () => {
|
const OptionIndex = () => {
|
||||||
return (
|
return (
|
||||||
<OptionLayout>
|
<OptionLayout>
|
||||||
<Playground />
|
<Playground />
|
||||||
</OptionLayout>
|
</OptionLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default OptionIndex
|
@ -2,7 +2,7 @@ import { SettingsLayout } from "~/components/Layouts/SettingsOptionLayout"
|
|||||||
import OptionLayout from "~/components/Layouts/Layout"
|
import OptionLayout from "~/components/Layouts/Layout"
|
||||||
import { AboutApp } from "@/components/Option/Settings/about"
|
import { AboutApp } from "@/components/Option/Settings/about"
|
||||||
|
|
||||||
export const OptionAbout = () => {
|
const OptionAbout = () => {
|
||||||
return (
|
return (
|
||||||
<OptionLayout>
|
<OptionLayout>
|
||||||
<SettingsLayout>
|
<SettingsLayout>
|
||||||
@ -11,3 +11,5 @@ export const OptionAbout = () => {
|
|||||||
</OptionLayout>
|
</OptionLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default OptionAbout
|
||||||
|
@ -2,7 +2,7 @@ import { SettingsLayout } from "~/components/Layouts/SettingsOptionLayout"
|
|||||||
import OptionLayout from "~/components/Layouts/Layout"
|
import OptionLayout from "~/components/Layouts/Layout"
|
||||||
import { KnowledgeSettings } from "@/components/Option/Knowledge"
|
import { KnowledgeSettings } from "@/components/Option/Knowledge"
|
||||||
|
|
||||||
export const OptionKnowledgeBase = () => {
|
const OptionKnowledgeBase = () => {
|
||||||
return (
|
return (
|
||||||
<OptionLayout>
|
<OptionLayout>
|
||||||
<SettingsLayout>
|
<SettingsLayout>
|
||||||
@ -11,3 +11,5 @@ export const OptionKnowledgeBase = () => {
|
|||||||
</OptionLayout>
|
</OptionLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default OptionKnowledgeBase
|
@ -2,7 +2,7 @@ import { SettingsLayout } from "~/components/Layouts/SettingsOptionLayout"
|
|||||||
import OptionLayout from "~/components/Layouts/Layout"
|
import OptionLayout from "~/components/Layouts/Layout"
|
||||||
import { ModelsBody } from "~/components/Option/Models"
|
import { ModelsBody } from "~/components/Option/Models"
|
||||||
|
|
||||||
export const OptionModal = () => {
|
const OptionModal = () => {
|
||||||
return (
|
return (
|
||||||
<OptionLayout>
|
<OptionLayout>
|
||||||
<SettingsLayout>
|
<SettingsLayout>
|
||||||
@ -11,3 +11,5 @@ export const OptionModal = () => {
|
|||||||
</OptionLayout>
|
</OptionLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default OptionModal
|
||||||
|
@ -2,7 +2,7 @@ import { SettingsLayout } from "~/components/Layouts/SettingsOptionLayout"
|
|||||||
import OptionLayout from "~/components/Layouts/Layout"
|
import OptionLayout from "~/components/Layouts/Layout"
|
||||||
import { PromptBody } from "~/components/Option/Prompt"
|
import { PromptBody } from "~/components/Option/Prompt"
|
||||||
|
|
||||||
export const OptionPrompt = () => {
|
const OptionPrompt = () => {
|
||||||
return (
|
return (
|
||||||
<OptionLayout>
|
<OptionLayout>
|
||||||
<SettingsLayout>
|
<SettingsLayout>
|
||||||
@ -11,3 +11,5 @@ export const OptionPrompt = () => {
|
|||||||
</OptionLayout>
|
</OptionLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default OptionPrompt
|
@ -2,7 +2,7 @@ import { SettingsLayout } from "~/components/Layouts/SettingsOptionLayout"
|
|||||||
import OptionLayout from "~/components/Layouts/Layout"
|
import OptionLayout from "~/components/Layouts/Layout"
|
||||||
import { OptionShareBody } from "~/components/Option/Share"
|
import { OptionShareBody } from "~/components/Option/Share"
|
||||||
|
|
||||||
export const OptionShare = () => {
|
const OptionShare = () => {
|
||||||
return (
|
return (
|
||||||
<OptionLayout>
|
<OptionLayout>
|
||||||
<SettingsLayout>
|
<SettingsLayout>
|
||||||
@ -11,3 +11,5 @@ export const OptionShare = () => {
|
|||||||
</OptionLayout>
|
</OptionLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default OptionShare
|
@ -2,7 +2,7 @@ import { SettingsLayout } from "~/components/Layouts/SettingsOptionLayout"
|
|||||||
import OptionLayout from "~/components/Layouts/Layout"
|
import OptionLayout from "~/components/Layouts/Layout"
|
||||||
import { SettingOther } from "~/components/Option/Settings/other"
|
import { SettingOther } from "~/components/Option/Settings/other"
|
||||||
|
|
||||||
export const OptionSettings = () => {
|
const OptionSettings = () => {
|
||||||
return (
|
return (
|
||||||
<OptionLayout>
|
<OptionLayout>
|
||||||
<SettingsLayout>
|
<SettingsLayout>
|
||||||
@ -11,3 +11,5 @@ export const OptionSettings = () => {
|
|||||||
</OptionLayout>
|
</OptionLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default OptionSettings
|
@ -2,7 +2,7 @@ import { SettingsLayout } from "~/components/Layouts/SettingsOptionLayout"
|
|||||||
import OptionLayout from "~/components/Layouts/Layout"
|
import OptionLayout from "~/components/Layouts/Layout"
|
||||||
import { SettingsOllama } from "~/components/Option/Settings/ollama"
|
import { SettingsOllama } from "~/components/Option/Settings/ollama"
|
||||||
|
|
||||||
export const OptionOllamaSettings = () => {
|
const OptionOllamaSettings = () => {
|
||||||
return (
|
return (
|
||||||
<OptionLayout>
|
<OptionLayout>
|
||||||
<SettingsLayout>
|
<SettingsLayout>
|
||||||
@ -11,3 +11,5 @@ export const OptionOllamaSettings = () => {
|
|||||||
</OptionLayout>
|
</OptionLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default OptionOllamaSettings
|
@ -4,7 +4,7 @@ import { SidepanelForm } from "~/components/Sidepanel/Chat/form"
|
|||||||
import { SidepanelHeader } from "~/components/Sidepanel/Chat/header"
|
import { SidepanelHeader } from "~/components/Sidepanel/Chat/header"
|
||||||
import { useMessage } from "~/hooks/useMessage"
|
import { useMessage } from "~/hooks/useMessage"
|
||||||
|
|
||||||
export const SidepanelChat = () => {
|
const SidepanelChat = () => {
|
||||||
const drop = React.useRef<HTMLDivElement>(null)
|
const drop = React.useRef<HTMLDivElement>(null)
|
||||||
const [dropedFile, setDropedFile] = React.useState<File | undefined>()
|
const [dropedFile, setDropedFile] = React.useState<File | undefined>()
|
||||||
const [dropState, setDropState] = React.useState<
|
const [dropState, setDropState] = React.useState<
|
||||||
@ -90,3 +90,5 @@ export const SidepanelChat = () => {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default SidepanelChat
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { SettingsBody } from "~/components/Sidepanel/Settings/body"
|
import { SettingsBody } from "~/components/Sidepanel/Settings/body"
|
||||||
import { SidepanelSettingsHeader } from "~/components/Sidepanel/Settings/header"
|
import { SidepanelSettingsHeader } from "~/components/Sidepanel/Settings/header"
|
||||||
|
|
||||||
export const SidepanelSettings = () => {
|
const SidepanelSettings = () => {
|
||||||
return (
|
return (
|
||||||
<div className="flex bg-neutral-50 dark:bg-[#171717] flex-col min-h-screen mx-auto max-w-7xl">
|
<div className="flex bg-neutral-50 dark:bg-[#171717] flex-col min-h-screen mx-auto max-w-7xl">
|
||||||
<div className="sticky bg-white dark:bg-[#171717] top-0 z-10">
|
<div className="sticky bg-white dark:bg-[#171717] top-0 z-10">
|
||||||
@ -11,3 +11,5 @@ export const SidepanelSettings = () => {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default SidepanelSettings
|
||||||
|
36
src/services/app.ts
Normal file
36
src/services/app.ts
Normal file
@ -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<boolean | undefined>("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
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import { Storage } from "@plasmohq/storage"
|
import { Storage } from "@plasmohq/storage"
|
||||||
import { cleanUrl } from "../libs/clean-url"
|
import { cleanUrl } from "../libs/clean-url"
|
||||||
import { chromeRunTime } from "../libs/runtime"
|
import { urlRewriteRuntime } from "../libs/runtime"
|
||||||
|
|
||||||
const storage = new Storage()
|
const storage = new Storage()
|
||||||
|
|
||||||
@ -22,10 +22,10 @@ Search results:
|
|||||||
export const getOllamaURL = async () => {
|
export const getOllamaURL = async () => {
|
||||||
const ollamaURL = await storage.get("ollamaURL")
|
const ollamaURL = await storage.get("ollamaURL")
|
||||||
if (!ollamaURL || ollamaURL.length === 0) {
|
if (!ollamaURL || ollamaURL.length === 0) {
|
||||||
await chromeRunTime(DEFAULT_OLLAMA_URL)
|
await urlRewriteRuntime(DEFAULT_OLLAMA_URL)
|
||||||
return DEFAULT_OLLAMA_URL
|
return DEFAULT_OLLAMA_URL
|
||||||
}
|
}
|
||||||
await chromeRunTime(cleanUrl(ollamaURL))
|
await urlRewriteRuntime(cleanUrl(ollamaURL))
|
||||||
return ollamaURL
|
return ollamaURL
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,7 +163,7 @@ export const setOllamaURL = async (ollamaURL: string) => {
|
|||||||
"http://127.0.0.1:"
|
"http://127.0.0.1:"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
await chromeRunTime(cleanUrl(formattedUrl))
|
await urlRewriteRuntime(cleanUrl(formattedUrl))
|
||||||
await storage.set("ollamaURL", cleanUrl(formattedUrl))
|
await storage.set("ollamaURL", cleanUrl(formattedUrl))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,8 +21,16 @@ export const setTTSProvider = async (ttsProvider: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getBrowserTTSVoices = async () => {
|
export const getBrowserTTSVoices = async () => {
|
||||||
const tts = await chrome.tts.getVoices()
|
if (import.meta.env.BROWSER === "chrome") {
|
||||||
return tts
|
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 () => {
|
export const getVoice = async () => {
|
||||||
|
25
src/utils/action.ts
Normal file
25
src/utils/action.ts
Normal file
@ -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 })
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import { cleanUrl } from "@/libs/clean-url"
|
import { cleanUrl } from "@/libs/clean-url"
|
||||||
import { chromeRunTime } from "@/libs/runtime"
|
import { urlRewriteRuntime } from "@/libs/runtime"
|
||||||
import { PageAssistHtmlLoader } from "@/loader/html"
|
import { PageAssistHtmlLoader } from "@/loader/html"
|
||||||
import {
|
import {
|
||||||
defaultEmbeddingChunkOverlap,
|
defaultEmbeddingChunkOverlap,
|
||||||
@ -18,7 +18,7 @@ import { RecursiveCharacterTextSplitter } from "langchain/text_splitter"
|
|||||||
import { MemoryVectorStore } from "langchain/vectorstores/memory"
|
import { MemoryVectorStore } from "langchain/vectorstores/memory"
|
||||||
|
|
||||||
export const localDuckDuckGoSearch = async (query: string) => {
|
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()
|
const abortController = new AbortController()
|
||||||
setTimeout(() => abortController.abort(), 10000)
|
setTimeout(() => abortController.abort(), 10000)
|
||||||
|
@ -7,7 +7,7 @@ import type { Document } from "@langchain/core/documents"
|
|||||||
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter"
|
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter"
|
||||||
import { MemoryVectorStore } from "langchain/vectorstores/memory"
|
import { MemoryVectorStore } from "langchain/vectorstores/memory"
|
||||||
import { cleanUrl } from "~/libs/clean-url"
|
import { cleanUrl } from "~/libs/clean-url"
|
||||||
import { chromeRunTime } from "~/libs/runtime"
|
import { urlRewriteRuntime } from "~/libs/runtime"
|
||||||
import { PageAssistHtmlLoader } from "~/loader/html"
|
import { PageAssistHtmlLoader } from "~/loader/html"
|
||||||
import {
|
import {
|
||||||
defaultEmbeddingChunkOverlap,
|
defaultEmbeddingChunkOverlap,
|
||||||
@ -18,8 +18,9 @@ import {
|
|||||||
|
|
||||||
|
|
||||||
export const localGoogleSearch = async (query: string) => {
|
export const localGoogleSearch = async (query: string) => {
|
||||||
await chromeRunTime(
|
await urlRewriteRuntime(
|
||||||
cleanUrl("https://www.google.com/search?hl=en&q=" + query)
|
cleanUrl("https://www.google.com/search?hl=en&q=" + query),
|
||||||
|
"google"
|
||||||
)
|
)
|
||||||
const abortController = new AbortController()
|
const abortController = new AbortController()
|
||||||
setTimeout(() => abortController.abort(), 10000)
|
setTimeout(() => abortController.abort(), 10000)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { cleanUrl } from "@/libs/clean-url"
|
import { cleanUrl } from "@/libs/clean-url"
|
||||||
import { chromeRunTime } from "@/libs/runtime"
|
import { urlRewriteRuntime } from "@/libs/runtime"
|
||||||
import { PageAssistHtmlLoader } from "@/loader/html"
|
import { PageAssistHtmlLoader } from "@/loader/html"
|
||||||
import {
|
import {
|
||||||
defaultEmbeddingChunkOverlap,
|
defaultEmbeddingChunkOverlap,
|
||||||
@ -25,7 +25,10 @@ const getCorrectTargeUrl = async (url: string) => {
|
|||||||
return matches?.[1] || ""
|
return matches?.[1] || ""
|
||||||
}
|
}
|
||||||
export const localSogouSearch = async (query: string) => {
|
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()
|
const abortController = new AbortController()
|
||||||
|
|
||||||
|
@ -3,5 +3,5 @@ module.exports = {
|
|||||||
mode: "jit",
|
mode: "jit",
|
||||||
darkMode: "class",
|
darkMode: "class",
|
||||||
content: ["./src/**/*.tsx"],
|
content: ["./src/**/*.tsx"],
|
||||||
plugins: [require("@tailwindcss/forms"), require("@tailwindcss/typography")]
|
plugins: [require("@tailwindcss/forms"), require("@tailwindcss/typography"),]
|
||||||
}
|
}
|
||||||
|
@ -2,35 +2,73 @@ import { defineConfig } from "wxt"
|
|||||||
import react from "@vitejs/plugin-react"
|
import react from "@vitejs/plugin-react"
|
||||||
import topLevelAwait from "vite-plugin-top-level-await"
|
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
|
// See https://wxt.dev/api/config.html
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
vite: () => ({
|
vite: () => ({
|
||||||
plugins: [react(),
|
plugins: [
|
||||||
|
react(),
|
||||||
topLevelAwait({
|
topLevelAwait({
|
||||||
promiseExportName: '__tla',
|
promiseExportName: "__tla",
|
||||||
promiseImportName: i => `__tla_${i}`,
|
promiseImportName: (i) => `__tla_${i}`
|
||||||
}),
|
})
|
||||||
],
|
],
|
||||||
build: {
|
build: {
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
external: [
|
external: ["langchain", "@langchain/community"]
|
||||||
"langchain",
|
|
||||||
"@langchain/community",
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
entrypointsDir: "entries",
|
entrypointsDir: "entries",
|
||||||
srcDir: "src",
|
srcDir: "src",
|
||||||
outDir: "build",
|
outDir: "build",
|
||||||
|
|
||||||
manifest: {
|
manifest: {
|
||||||
version: "1.1.6",
|
version: "1.1.7",
|
||||||
name: '__MSG_extName__',
|
name:
|
||||||
description: '__MSG_extDescription__',
|
process.env.TARGET === "firefox"
|
||||||
default_locale: 'en',
|
? "Page Assist - A Web UI for Local AI Models"
|
||||||
|
: "__MSG_extName__",
|
||||||
|
description: "__MSG_extDescription__",
|
||||||
|
default_locale: "en",
|
||||||
action: {},
|
action: {},
|
||||||
author: "n4ze3m",
|
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: {
|
commands: {
|
||||||
_execute_action: {
|
_execute_action: {
|
||||||
suggested_key: {
|
suggested_key: {
|
||||||
@ -44,16 +82,9 @@ export default defineConfig({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
permissions: [
|
permissions:
|
||||||
"storage",
|
process.env.TARGET === "firefox"
|
||||||
"sidePanel",
|
? firefoxMV2Permissions
|
||||||
"activeTab",
|
: chromeMV3Permissions
|
||||||
"scripting",
|
|
||||||
"declarativeNetRequest",
|
|
||||||
"action",
|
|
||||||
"unlimitedStorage",
|
|
||||||
"contextMenus",
|
|
||||||
"tts"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
Loading…
x
Reference in New Issue
Block a user