Merge pull request #23 from n4ze3m/next

Next
This commit is contained in:
Muhammed Nazeem 2024-03-26 00:23:26 +05:30 committed by GitHub
commit 4f933c9bab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
94 changed files with 4990 additions and 4426 deletions

1
.gitignore vendored
View File

@ -40,3 +40,4 @@ keys.json
# typescript
.tsbuildinfo
.wxt

94
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,94 @@
# Contributing to Page Assist
Thank you for your interest in contributing to Page Assist! We welcome contributions from anyone, whether it's reporting bugs, suggesting improvements, or submitting code changes.
## Getting Started
1. **Fork the repository**
To start contributing, you'll need to fork the [Page Assist repository](https://github.com/n4ze3m/page-assist) by clicking the "Fork" button at the top right of the page.
2. **Clone your forked repository**
Once you have your own fork, clone it to your local machine:
```
git clone https://github.com/YOUR-USERNAME/page-assist.git
```
3. **Install dependencies**
Page Assist uses [Yarn](https://yarnpkg.com/) for dependency management. Install the required dependencies by running the following command in the project root directory:
```
yarn install
```
4. **Start the development server**
To run the extension in development mode, use the following command:
```
yarn dev
```
This will open a browser window with the extension loaded.
5. **Install Ollama locally**
Page Assist requires [Ollama](https://ollama.ai) to be installed locally. Follow the installation instructions provided in the Ollama repository.
## Making Changes
Once you have the project set up locally, you can start making changes. We recommend creating a new branch for your changes:
```
git checkout -b my-feature-branch
```
Make your desired changes, and don't forget to add or update tests if necessary.
## Submitting a Pull Request
1. **Commit your changes**
Once you've made your changes, commit them with a descriptive commit message:
```
git commit -m "Add a brief description of your changes"
```
2. **Push your changes**
Push your changes to your forked repository:
```
git push origin my-feature-branch
```
3. **Open a Pull Request**
Go to the original repository on GitHub and click the "New Pull Request" button. Select your forked repository and the branch you just pushed as the source, and the main repository's `main` branch as the destination.
4. **Describe your changes**
Provide a clear and concise description of the changes you've made, including any relevant issue numbers or other context.
5. **Review and merge**
The maintainers of the project will review your pull request and provide feedback or merge it if everything looks good.
## Code Style and Guidelines
To ensure consistency and maintainability, we follow certain code style guidelines. Please ensure your code adheres to these guidelines before submitting a pull request.
- Use proper indentation and code formatting
- Write clear and concise comments when necessary
- Follow best practices for TypeScript and React development
## Need Help?
If you have any questions or need further assistance, feel free to open an issue or reach out to the maintainers.
Thank you for your contribution!

View File

@ -5,32 +5,40 @@
"description": "Use your locally running AI models to assist you in your web browsing.",
"author": "n4ze3m",
"scripts": {
"dev": "plasmo dev",
"build": "plasmo build",
"package": "plasmo package"
"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",
"compile": "tsc --noEmit",
"postinstall": "wxt prepare"
},
"dependencies": {
"@ant-design/cssinjs": "^1.18.4",
"@headlessui/react": "^1.7.18",
"@heroicons/react": "^2.1.1",
"@langchain/community": "^0.0.21",
"@langchain/core": "^0.1.22",
"@langchain/community": "^0.0.41",
"@mantine/form": "^7.5.0",
"@mantine/hooks": "^7.5.3",
"@plasmohq/storage": "^1.9.0",
"@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.10",
"@tanstack/react-query": "^5.17.19",
"@vitejs/plugin-react": "^4.2.1",
"antd": "^5.13.3",
"axios": "^1.6.7",
"dayjs": "^1.11.10",
"html-to-text": "^9.0.5",
"langchain": "^0.1.9",
"i18next": "^23.10.1",
"i18next-browser-languagedetector": "^7.2.0",
"langchain": "^0.1.28",
"lucide-react": "^0.350.0",
"plasmo": "0.84.1",
"pdfjs-dist": "^4.0.379",
"property-information": "^6.4.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-i18next": "^14.1.0",
"react-markdown": "8.0.0",
"react-router-dom": "6.10.0",
"react-syntax-highlighter": "^15.5.0",
@ -53,34 +61,11 @@
"postcss": "^8.4.33",
"prettier": "3.2.4",
"tailwindcss": "^3.4.1",
"typescript": "5.3.3"
"typescript": "5.3.3",
"vite-plugin-top-level-await": "^1.4.1",
"wxt": "^0.17.7"
},
"manifest": {
"host_permissions": [
"http://*/*",
"https://*/*"
],
"commands": {
"_execute_action": {
"suggested_key": {
"default": "Ctrl+Shift+L"
}
},
"execute_side_panel": {
"description": "Open the side panel",
"suggested_key": {
"default": "Ctrl+Shift+P"
}
}
},
"permissions": [
"storage",
"activeTab",
"scripting",
"declarativeNetRequest",
"action",
"unlimitedStorage",
"contextMenus"
]
"resolutions": {
"@langchain/core": "0.1.45"
}
}

View File

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -0,0 +1,52 @@
{
"pageAssist": "Page Assist",
"selectAModel": "Select a Model",
"save": "Save",
"saved": "Saved",
"cancel": "Cancel",
"retry": "Retry",
"share": {
"tooltip": {
"share": "Share"
},
"modal": {
"title": "Share link to Chat"
},
"form": {
"defaultValue": {
"name": "Anonymous",
"title": "Untitled chat"
},
"title": {
"label": "Chat title",
"placeholder": "Enter Chat title",
"required": "Chat title is required"
},
"name": {
"label": "Your name",
"placeholder": "Enter your name",
"required": "Your name is required"
},
"btn": {
"save": "Generate Link",
"saving": "Generating Link..."
}
},
"notification": {
"successGenerate": "Link copied to clipboard",
"failGenerate": "Failed to generate link"
}
},
"copyToClipboard": "Copy to clipboard",
"webSearch": "Searching the web",
"regenerate": "Regenerate",
"edit": "Edit",
"saveAndSubmit": "Save & Submit",
"editMessage": {
"placeholder": "Type a message..."
},
"submit": "Submit",
"noData": "No data",
"noHistory": "No chat history",
"chatWithCurrentPage": "Chat with current page"
}

View File

@ -0,0 +1,12 @@
{
"newChat": "New Chat",
"selectAPrompt": "Select a Prompt",
"githubRepository": "GitHub Repository",
"settings": "Settings",
"sidebarTitle": "Chat History",
"error": "Error",
"somethingWentWrong": "Something went wrong",
"validationSelectModel": "Please select a model to continue",
"deleteHistoryConfirmation": "Are you sure you want to delete this history?",
"editHistoryTitle": "Enter a new title"
}

View File

@ -0,0 +1,27 @@
{
"ollamaState": {
"searching": "Searching for Your Ollama 🦙",
"running": "Ollama is running 🦙",
"notRunning": "Unable to connect to Ollama 🦙"
},
"formError": {
"noModel": "Please select a model",
"noEmbeddingModel": "Please set an embedding model on the Settings > Ollama page"
},
"form": {
"textarea": {
"placeholder": "Type a message..."
},
"webSearch": {
"on": "On",
"off": "Off"
}
},
"tooltip": {
"searchInternet": "Search Internet",
"speechToText": "Speech to Text",
"uploadImage": "Upload Image",
"stopStreaming": "Stop Streaming"
},
"sendWhenEnter": "Send when Enter pressed"
}

View File

@ -0,0 +1,207 @@
{
"generalSettings": {
"title": "General Settings",
"heading": "Web UI Settings",
"settings": {
"speechRecognitionLang": {
"label": "Speech Recognition Language",
"placeholder": "Select a language"
},
"language": {
"label": "Language",
"placeholder": "Select a language"
},
"darkMode": {
"label": "Change Theme",
"options": {
"light": "Light",
"dark": "Dark"
}
},
"searchMode": {
"label": "Perform Simple Internet Search"
},
"deleteChatHistory": {
"label": "Delete Chat History",
"button": "Delete",
"confirm": "Are you sure you want to delete your chat history? This action cannot be undone."
}
}
},
"manageModels": {
"title": "Manage Models",
"addBtn": "Add New Model",
"columns": {
"name": "Name",
"digest": "Digest",
"modifiedAt": "Modified At",
"size": "Size",
"actions": "Actions"
},
"expandedColumns": {
"parentModel": "Parent Model",
"format": "Format",
"family": "Family",
"parameterSize": "Parameter Size",
"quantizationLevel": "Quantization Level"
},
"tooltip": {
"delete": "Delete Model",
"repull": "Re-Pull Model"
},
"confirm": {
"delete": "Are you sure you want to delete this model?",
"repull": "Are you sure you want to re-pull this model?"
},
"modal": {
"title": "Add New Model",
"placeholder": "Enter Model Name",
"pull": "Pull Model"
},
"notification": {
"pullModel": "Pulling Model",
"pullModelDescription": "Pulling {{modelName}} model. For more details, check the extension icon.",
"success": "Success",
"error": "Error",
"successDescription": "Successfully pulled the model",
"successDeleteDescription": "Successfully deleted the model",
"someError": "Something went wrong. Please try again later"
}
},
"managePrompts": {
"title": "Manage Prompts",
"addBtn": "Add New Prompt",
"option1": "Normal",
"option2": "RAG",
"questionPrompt": "Question Prompt",
"columns": {
"title": "Title",
"prompt": "Prompt",
"type": "Prompt Type",
"actions": "Actions"
},
"systemPrompt": "System Prompt",
"quickPrompt": "Quick Prompt",
"tooltip": {
"delete": "Delete Prompt",
"edit": "Edit Prompt"
},
"confirm": {
"delete": "Are you sure you want to delete this prompt? This action cannot be undone."
},
"modal": {
"addTitle": "Add New Prompt",
"editTitle": "Edit Prompt"
},
"form": {
"title": {
"label": "Title",
"placeholder": "My Awesome Prompt",
"required": "Please enter a title"
},
"prompt": {
"label": "Prompt",
"placeholder": "Enter Prompt",
"required": "Please enter a prompt",
"help": "You can use {key} as variable in your prompt."
},
"isSystem": {
"label": "Is System Prompt"
},
"btnSave": {
"saving": "Adding Prompt...",
"save": "Add Prompt"
},
"btnEdit": {
"saving": "Updating Prompt...",
"save": "Update Prompt"
}
},
"notification": {
"addSuccess": "Prompt Added",
"addSuccessDesc": "Prompt has been added successfully",
"error": "Error",
"someError": "Something went wrong. Please try again later",
"updatedSuccess": "Prompt Updated",
"updatedSuccessDesc": "Prompt has been updated successfully",
"deletedSuccess": "Prompt Deleted",
"deletedSuccessDesc": "Prompt has been deleted successfully"
}
},
"manageShare": {
"title": "Manage Share",
"heading": "Configure Page Share URL",
"form": {
"url": {
"label": "Page Share URL",
"placeholder": "Enter Page Share URL",
"required": "Please input your Page Share URL!",
"help": "For privacy reasons, you can self-host the page share and provide the URL here. <anchor>Learn More</anchor>."
}
},
"webshare": {
"heading": "Web Share",
"columns": {
"title": "Title",
"url": "URL",
"actions": "Actions"
},
"tooltip": {
"delete": "Delete Share"
},
"confirm": {
"delete": "Are you sure you want to delete this share? This action cannot be undone."
}
},
"notification": {
"pageShareSuccess": "Page Share URL updated successfully",
"someError": "Something went wrong. Please try again later",
"webShareDeleteSuccess": "Web Share deleted successfully"
}
},
"ollamaSettings": {
"title": "Ollama Settings",
"heading": "Configure Ollama",
"settings": {
"ollamaUrl": {
"label": "Ollama URL",
"placeholder": "Enter Ollama URL"
},
"ragSettings": {
"label": "RAG Settings",
"model": {
"label": "Embedding Model",
"required": "Please select a model",
"help": "Highly recommended to use embedding models like `nomic-embed-text`.",
"placeholder": "Select a model"
},
"chunkSize": {
"label": "Chunk Size",
"placeholder": "Enter Chunk Size",
"required": "Please enter a chunk size"
},
"chunkOverlap": {
"label": "Chunk Overlap",
"placeholder": "Enter Chunk Overlap",
"required": "Please enter a chunk overlap"
}
},
"prompt": {
"label": "Configure RAG Prompt",
"option1": "Normal",
"option2": "Web",
"alert": "Configuring the system prompt here is deprecated. Please use the Manage Prompts section to add or edit prompts. This section will be removed in a future release",
"systemPrompt": "System Prompt",
"systemPromptPlaceholder": "Enter System Prompt",
"webSearchPrompt": "Web Search Prompt",
"webSearchPromptHelp": "Do not remove `{search_results}` from the prompt.",
"webSearchPromptError": "Please enter a web search prompt",
"webSearchPromptPlaceholder": "Enter Web Search Prompt",
"webSearchFollowUpPrompt": "Web Search Follow Up Prompt",
"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"
}
}
}
}

View File

@ -0,0 +1,5 @@
{
"tooltip": {
"embed": "It may take a few minutes to embed the page. Please wait..."
}
}

View File

@ -0,0 +1,52 @@
{
"pageAssist": "പേജ് ആസിസ്റ്റ്",
"selectAModel": "ഒരു മോഡല്‍ തിരഞ്ഞെടുക്കുക",
"save": "സേവ് ചെയ്യുക",
"saved": "സേവ് ചെയ്തു",
"cancel": "റദ്ദാക്കുക",
"retry": "വീണ്ടും ശ്രമിക്കുക",
"share": {
"tooltip": {
"share": "പങ്കിടുക"
},
"modal": {
"title": "ചാറ്റ് ലിങ്ക് പങ്കിടുക"
},
"form": {
"defaultValue": {
"name": "അജ്ഞാതന്‍",
"title": "പേരില്ലാത്ത ചാറ്റ്"
},
"title": {
"label": "ചാറ്റ് തലക്കെട്ട്",
"placeholder": "ചാറ്റ് തലക്കെട്ട് നല്കുക",
"required": "ചാറ്റ് തലക്കെട്ട് ആവശ്യമാണ്"
},
"name": {
"label": "നിങ്ങളുടെ പേര്",
"placeholder": "നിങ്ങളുടെ പേര് നല്കുക",
"required": "നിങ്ങളുടെ പേര് ആവശ്യമാണ്"
},
"btn": {
"save": "ലിങ്ക് ജനറേറ്റ് ചെയ്യുക",
"saving": "ലിങ്ക് ജനറേറ്റ് ചെയ്യുന്നു..."
}
},
"notification": {
"successGenerate": "ലിങ്ക് ക്ലിപ്ബോര്‍ഡിലേക്ക് പകര്‍ത്തി",
"failGenerate": "ലിങ്ക് ജനറേറ്റ് ചെയ്യുന്നതില്‍ പരാജയപ്പെട്ടു"
}
},
"copyToClipboard": "ക്ലിപ്ബോര്‍ഡിലേക്ക് പകര്‍ത്തുക",
"webSearch": "വെബ് തിരയുന്നു",
"regenerate": "വീണ്ടും ജനറേറ്റ് ചെയ്യുക",
"edit": "എഡിറ്റ് ചെയ്യുക",
"saveAndSubmit": "സേവ് ചെയ്ത് സമര്‍പ്പിക്കുക",
"editMessage": {
"placeholder": "ഒരു സന്ദേശം ടൈപ്പ് ചെയ്യുക..."
},
"submit": "സമർപ്പിക്കുക",
"noData": "ഡാറ്റ ലഭ്യമല്ല",
"noHistory": "ചാറ്റ് ചരിത്രം ലഭ്യമല്ല",
"chatWithCurrentPage": "നിലവിലെ പേജിനുമായി ചാറ്റ് ചെയ്യുക"
}

View File

@ -0,0 +1,12 @@
{
"newChat": "പുതിയ ചാറ്റ്",
"selectAPrompt": "ഒരു പ്രോംപ്റ്റ് തിരഞ്ഞെടുക്കുക",
"githubRepository": "ഗിറ്റ്ഹബ് റെപ്പോസിറ്ററി",
"settings": "ക്രമീകരണങ്ങള്‍",
"sidebarTitle": "ചാറ്റ് ചരിത്രം",
"error": "പിശക്",
"somethingWentWrong": "എന്തോ തെറ്റായി",
"deleteHistoryConfirmation": "നിങ്ങളുടെ ചാറ്റ് ചരിത്രം ഇല്ലാതാക്കണമെന്ന് തീർച്ചയാണോ?",
"editHistoryTitle": "ചാറ്റ് title എഡിറ്റുചെയ്യുക",
"validationSelectModel": "തുടരുന്നതിന് ഒരു മോഡല്‍ തിരഞ്ഞെടുക്കുക"
}

View File

@ -0,0 +1,27 @@
{
"ollamaState": {
"searching": "നിങ്ങളുടെ ഒല്ലാമയ്ക്കായി തിരയുന്നു 🦙",
"running": "ഒല്ലാമ പ്രവര്‍ത്തിക്കുന്നു 🦙",
"notRunning": "ഒല്ലാമയുമായി ബന്ധിപ്പിക്കാന്‍ കഴിയുന്നില്ല 🦙"
},
"formError": {
"noModel": "ദയവായി ഒരു മോഡല്‍ തിരഞ്ഞെടുക്കുക",
"noEmbeddingModel": "ക്രമീകരണങ്ങള്‍ > ഒല്ലാമ പേജിലുള്ള എംബെഡിംഗ് മോഡല്‍ സജ്ജീകരിക്കുക"
},
"form": {
"textarea": {
"placeholder": "ഒരു സന്ദേശം ടൈപ്പ് ചെയ്യുക..."
},
"webSearch": {
"on": "ഓണ്‍",
"off": "ഓഫ്"
}
},
"tooltip": {
"searchInternet": "ഇന്റര്‍നെറ്റ് തിരയുക",
"speechToText": "സംഭാഷണം ടെക്സ്റ്റായി",
"uploadImage": "ഇമേജ് അപ്‌ലോഡ് ചെയ്യുക",
"stopStreaming": "സ്ട്രീമിംഗ് നിർത്തുക"
},
"sendWhenEnter": "എന്റര്‍ അമര്‍ത്തുമ്പോള്‍ അയയ്ക്കുക"
}

View File

@ -0,0 +1,207 @@
{
"generalSettings": {
"title": "പൊതുവായ ക്രമീകരണങ്ങള്‍",
"heading": "വെബ് UI ക്രമീകരണങ്ങള്‍",
"settings": {
"speechRecognitionLang": {
"label": "സംഭാഷണ തിരിച്ചറിയല്‍ ഭാഷ",
"placeholder": "ഒരു ഭാഷ തിരഞ്ഞെടുക്കുക"
},
"language": {
"label": "ഭാഷ",
"placeholder": "ഒരു ഭാഷ തിരഞ്ഞെടുക്കുക"
},
"darkMode": {
"label": "തീം മാറ്റുക",
"options": {
"light": "ലൈറ്റ്",
"dark": "ഡാര്‍ക്ക്"
}
},
"searchMode": {
"label": "സാധാരണ ഇന്റർനെറ്റ് അന്വേഷണം നടത്തുക"
},
"deleteChatHistory": {
"label": "ചാറ്റ് ചരിത്രം ഇല്ലാതാക്കുക",
"button": "ഇല്ലാതാക്കുക",
"confirm": "നിങ്ങളുടെ ചാറ്റ് ചരിത്രം ഇല്ലാതാക്കണമെന്ന് തീർച്ചയാണോ? ഈ പ്രവർത്തനം പിന്നീട് പിൻവലിക്കാനാകില്ല."
}
}
},
"manageModels": {
"title": "മോഡലുകള്‍ കൈകാര്യം ചെയ്യുക",
"addBtn": "പുതിയ മോഡല്‍ ചേര്‍ക്കുക",
"columns": {
"name": "പേര്",
"digest": "ഡൈജസ്റ്റ്",
"modifiedAt": "അവസാനമായി പരിഷ്‌കരിച്ചത്",
"size": "വലുപ്പം",
"actions": "പ്രവർത്തനങ്ങൾ"
},
"expandedColumns": {
"parentModel": "പാരന്റ് മോഡല്‍",
"format": "ഫോര്‍മാറ്റ്",
"family": "കുടുംബം",
"parameterSize": "പാരാമീറ്റര്‍ വലുപ്പം",
"quantizationLevel": "ക്വാണ്ടൈസേഷന്‍ ലെവല്‍"
},
"tooltip": {
"delete": "മോഡല്‍ ഇല്ലാതാക്കുക",
"repull": "മോഡല്‍ വീണ്ടും ലഭ്യമാക്കുക"
},
"confirm": {
"delete": "ഈ മോഡല്‍ ഇല്ലാതാക്കണമെന്ന് തീർച്ചയാണോ?",
"repull": "ഈ മോഡല്‍ വീണ്ടും ലഭ്യമാക്കണമെന്ന് തീർച്ചയാണോ?"
},
"modal": {
"title": "പുതിയ മോഡല്‍ ചേര്‍ക്കുക",
"placeholder": "മോഡല്‍ പേര് നല്‍കുക",
"pull": "മോഡല്‍ ലഭ്യമാക്കുക"
},
"notification": {
"pullModel": "മോഡല്‍ ലഭ്യമാക്കുന്നു",
"pullModelDescription": "{{modelName}} മോഡല്‍ ലഭ്യമാക്കുന്നു. കൂടുതല്‍ വിവരങ്ങള്‍ക്കായി എക്‌സ്റ്റെന്‍ഷന്‍ ഐക്കണ്‍ പരിശോധിക്കുക.",
"success": "വിജയം",
"error": "പിശക്",
"successDescription": "മോഡല്‍ വിജയകരമായി ലഭ്യമാക്കി",
"successDeleteDescription": "മോഡല്‍ വിജയകരമായി ഇല്ലാതാക്കി",
"someError": "എന്തോ തെറ്റായി. ദയവായി പിന്നീട് വീണ്ടും ശ്രമിക്കുക"
}
},
"managePrompts": {
"title": "പ്രോംപ്റ്റുകള്‍ കൈകാര്യം ചെയ്യുക",
"addBtn": "പുതിയ പ്രോംപ്റ്റ് ചേര്‍ക്കുക",
"columns": {
"title": "തലക്കെട്ട്",
"prompt": "പ്രോംപ്റ്റ്",
"type": "പ്രോംപ്റ്റ് തരം",
"actions": "പ്രവർത്തനങ്ങൾ"
},
"option1": "സാധാരണ",
"option2": "RAG",
"questionPrompt": "ചോദ്യ പ്രോംപ്റ്റ്",
"systemPrompt": "സിസ്റ്റം പ്രോംപ്റ്റ്",
"quickPrompt": "വേഗത്തിലുള്ള പ്രോംപ്റ്റ്",
"tooltip": {
"delete": "പ്രോംപ്റ്റ് ഇല്ലാതാക്കുക",
"edit": "പ്രോംപ്റ്റ് എഡിറ്റുചെയ്യുക"
},
"confirm": {
"delete": "ഈ പ്രോംപ്റ്റ് ഇല്ലാതാക്കണമെന്ന് തീർച്ചയാണോ? ഈ പ്രവർത്തനം പിന്നീട് പിൻവലിക്കാനാകില്ല."
},
"modal": {
"addTitle": "പുതിയ പ്രോംപ്റ്റ് ചേര്‍ക്കുക",
"editTitle": "പ്രോംപ്റ്റ് എഡിറ്റുചെയ്യുക"
},
"form": {
"title": {
"label": "തലക്കെട്ട്",
"placeholder": "എന്‍റെ അതുല്യമായ പ്രോംപ്റ്റ്",
"required": "ദയവായി ഒരു തലക്കെട്ട് നല്കുക"
},
"prompt": {
"label": "പ്രോംപ്റ്റ്",
"placeholder": "പ്രോംപ്റ്റ് നല്കുക",
"required": "ദയവായി ഒരു പ്രോംപ്റ്റ് നല്കുക",
"help": "നിങ്ങള്‍ക്ക് {key} എന്ന രീതിയില്‍ പ്രോംപ്റ്റില്‍ വേരിയബിളുകള്‍ ഉപയോഗിക്കാവുന്നതാണ്."
},
"isSystem": {
"label": "സിസ്റ്റം പ്രോംപ്റ്റ് ആണോ"
},
"btnSave": {
"saving": "പ്രോംപ്റ്റ് ചേര്‍ക്കുന്നു...",
"save": "പ്രോംപ്റ്റ് ചേര്‍ക്കുക"
},
"btnEdit": {
"saving": "പ്രോംപ്റ്റ് അപ്ഡേറ്റ് ചെയ്യുന്നു...",
"save": "പ്രോംപ്റ്റ് അപ്ഡേറ്റ് ചെയ്യുക"
}
},
"notification": {
"addSuccess": "പ്രോംപ്റ്റ് ചേർത്തു",
"addSuccessDesc": "പ്രോംപ്റ്റ് വിജയകരമായി ചേർത്തു",
"error": "പിശക്",
"someError": "എന്തോ തെറ്റായി. ദയവായി പിന്നീട് വീണ്ടും ശ്രമിക്കുക",
"updatedSuccess": "പ്രോംപ്റ്റ് അപ്ഡേറ്റ് ചെയ്‌തു",
"updatedSuccessDesc": "പ്രോംപ്റ്റ് വിജയകരമായി അപ്ഡേറ്റ് ചെയ്തു",
"deletedSuccess": "പ്രോംപ്റ്റ് ഇല്ലാതാക്കി",
"deletedSuccessDesc": "പ്രോംപ്റ്റ് വിജയകരമായി ഇല്ലാതാക്കി"
}
},
"manageShare": {
"title": "പങ്കിടുന്നത് കൈകാര്യം ചെയ്യുക",
"heading": "പേജ് പങ്കിടാനുള്ള URL കോൺഫിഗർ ചെയ്യുക",
"form": {
"url": {
"label": "പേജ് പങ്കിടാനുള്ള URL",
"placeholder": "പേജ് പങ്കിടാനുള്ള URL നല്കുക",
"required": "ദയവായി നിങ്ങളുടെ പേജ് പങ്കിടാനുള്ള URL നല്കുക!",
"help": "സ്വകാര്യതക്കായി, നിങ്ങള്‍ക്ക് സ്വന്തമായി പേജ് പങ്കിടുന്ന സൗകര്യം ഹോസ്റ്റ് ചെയ്യാനും അവിടെയുള്ള URL ഇവിടെ നല്കാനും കഴിയും. <anchor>കൂടുതല്‍ അറിയുക</anchor>."
}
},
"webshare": {
"heading": "വെബ് പങ്കിടല്‍",
"columns": {
"title": "തലക്കെട്ട്",
"url": "URL",
"actions": "പ്രവർത്തനങ്ങൾ"
},
"tooltip": {
"delete": "പങ്കിടല്‍ ഇല്ലാതാക്കുക"
},
"confirm": {
"delete": "ഈ പങ്കിടല്‍ ഇല്ലാതാക്കണമെന്ന് തീർച്ചയാണോ? ഈ പ്രവർത്തനം പിന്നീട് പിൻവലിക്കാനാകില്ല."
}
},
"notification": {
"pageShareSuccess": "പേജ് പങ്കിടാനുള്ള URL വിജയകരമായി അപ്ഡേറ്റ് ചെയ്തു",
"someError": "എന്തോ തെറ്റായി. ദയവായി പിന്നീട് വീണ്ടും ശ്രമിക്കുക",
"webShareDeleteSuccess": "വെബ് പങ്കിടല്‍ വിജയകരമായി ഇല്ലാതാക്കി"
}
},
"ollamaSettings": {
"title": "ഒല്ലാമാ ക്രമീകരണങ്ങള്‍",
"heading": "ഒല്ലാമാ കോൺഫിഗർ ചെയ്യുക",
"settings": {
"ollamaUrl": {
"label": "ഒല്ലാമാ URL",
"placeholder": "ഒല്ലാമാ URL നല്കുക"
},
"ragSettings": {
"label": "RAG ക്രമീകരണങ്ങള്‍",
"model": {
"label": "എംബെഡിംഗ് മോഡല്‍",
"required": "ദയവായി ഒരു മോഡല്‍ തിരഞ്ഞെടുക്കുക",
"help": "`nomic-embed-text` പോലുള്ള എംബെഡിംഗ് മോഡലുകള്‍ ഉപയോഗിക്കുന്നത് വളരെ നന്നായിരിക്കും.",
"placeholder": "ഒരു മോഡല്‍ തിരഞ്ഞെടുക്കുക"
},
"chunkSize": {
"label": "ചങ്ക് വലുപ്പം",
"placeholder": "ചങ്ക് വലുപ്പം നല്കുക",
"required": "ദയവായി ചങ്ക് വലുപ്പം നല്കുക"
},
"chunkOverlap": {
"label": "ചങ്ക് ഓവര്‍ലാപ്പ്",
"placeholder": "ചങ്ക് ഓവര്‍ലാപ്പ് നല്കുക",
"required": "ദയവായി ചങ്ക് ഓവര്‍ലാപ്പ് നല്കുക"
}
},
"prompt": {
"label": "RAG പ്രോംപ്റ്റ് കോൺഫിഗർ ചെയ്യുക",
"option1": "സാധാരണ",
"option2": "വെബ്",
"alert": "സിസ്റ്റം പ്രോംപ്റ്റ് ഇവിടെ കോൺഫിഗർ ചെയ്യുന്നത് പഴയൗഖികമായി. ദയവായി പ്രോംപ്റ്റുകള്‍ ചേര്‍ക്കാനോ എഡിറ്റുചെയ്യാനോ മാനേജ് പ്രോംപ്റ്റ്‌സ് സെക്ഷന്‍ ഉപയോഗിക്കുക. ഈ സെക്ഷന്‍ ഭാവിയില്‍ നീക്കം ചെയ്യപ്പെടും.",
"systemPrompt": "സിസ്റ്റം പ്രോംപ്റ്റ്",
"systemPromptPlaceholder": "സിസ്റ്റം പ്രോംപ്റ്റ് നല്കുക",
"webSearchPrompt": "വെബ് തിരയല്‍ പ്രോംപ്റ്റ്",
"webSearchPromptHelp": "പ്രോംപ്റ്റില്‍ നിന്ന് `{search_results}` നീക്കം ചെയ്യരുത്.",
"webSearchPromptError": "ദയവായി ഒരു വെബ് തിരയല്‍ പ്രോംപ്റ്റ് നല്കുക",
"webSearchPromptPlaceholder": "വെബ് തിരയല്‍ പ്രോംപ്റ്റ് നല്കുക",
"webSearchFollowUpPrompt": "വെബ് തിരയല്‍ തുടര്‍പ്രോംപ്റ്റ്",
"webSearchFollowUpPromptHelp": "പ്രോംപ്റ്റില്‍ നിന്ന് `{chat_history}` യും `{question}` യും നീക്കം ചെയ്യരുത്.",
"webSearchFollowUpPromptError": "ദയവായി നിങ്ങളുടെ വെബ് തിരയല്‍ തുടര്‍പ്രോംപ്റ്റ് നല്കുക!",
"webSearchFollowUpPromptPlaceholder": "നിങ്ങളുടെ വെബ് തിരയല്‍ തുടര്‍പ്രോംപ്റ്റ്"
}
}
}
}

View File

@ -0,0 +1,6 @@
{
"tooltip": {
"embed": "പേജ് പ്രോസസ്സ് ചെയ്യുന്നതിന് കുറച്ച് മിനിറ്റുകൾ എടുത്തേക്കാം. കാത്തിരിക്കൂ.."
}
}

View File

@ -1,138 +0,0 @@
import { getOllamaURL, isOllamaRunning } from "~services/ollama"
export {}
const progressHuman = (completed: number, total: number) => {
return ((completed / total) * 100).toFixed(0) + "%"
}
const clearBadge = () => {
chrome.action.setBadgeText({ text: "" })
chrome.action.setTitle({ title: "" })
}
const streamDownload = async (url: string, model: string) => {
url += "/api/pull"
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ model, stream: true })
})
const reader = response.body?.getReader()
const decoder = new TextDecoder()
let isSuccess = true
while (true) {
const { done, value } = await reader.read()
if (done) {
break
}
const text = decoder.decode(value)
try {
const json = JSON.parse(text.trim()) as {
status: string
total?: number
completed?: number
}
if (json.total && json.completed) {
chrome.action.setBadgeText({
text: progressHuman(json.completed, json.total)
})
chrome.action.setBadgeBackgroundColor({ color: "#0000FF" })
} else {
chrome.action.setBadgeText({ text: "🏋️‍♂️" })
chrome.action.setBadgeBackgroundColor({ color: "#FFFFFF" })
}
chrome.action.setTitle({ title: json.status })
if (json.status === "success") {
isSuccess = true
}
} catch (e) {
console.error(e)
}
}
if (isSuccess) {
chrome.action.setBadgeText({ text: "✅" })
chrome.action.setBadgeBackgroundColor({ color: "#00FF00" })
chrome.action.setTitle({ title: "Model pulled successfully" })
} else {
chrome.action.setBadgeText({ text: "❌" })
chrome.action.setBadgeBackgroundColor({ color: "#FF0000" })
chrome.action.setTitle({ title: "Model pull failed" })
}
setTimeout(() => {
clearBadge()
}, 5000)
}
chrome.runtime.onMessage.addListener(async (message) => {
if (message.type === "sidepanel") {
chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => {
const tab = tabs[0]
await chrome.sidePanel.open({
tabId: tab.id
})
})
} 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" })
setTimeout(() => {
clearBadge()
}, 5000)
}
await streamDownload(ollamaURL, message.modelName)
}
})
chrome.action.onClicked.addListener((tab) => {
chrome.tabs.create({ url: chrome.runtime.getURL("options.html") })
})
chrome.commands.onCommand.addListener((command) => {
switch (command) {
case "execute_side_panel":
chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => {
const tab = tabs[0]
await chrome.sidePanel.open({
tabId: tab.id
})
})
break
default:
break
}
})
chrome.contextMenus.create({
id: "open-side-panel-pa",
title: "Open Side Panel to Chat",
contexts: ["all"]
})
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]
await chrome.sidePanel.open({
tabId: tab.id
})
})
}
})

View File

@ -14,7 +14,7 @@ import {
RunnableMap,
RunnableSequence,
} from "langchain/schema/runnable";
import type { ChatHistory } from "~store";
import type { ChatHistory } from "~/store";
type RetrievalChainInput = {
chat_history: string;
question: string;

View File

@ -8,17 +8,11 @@ import "property-information"
import React from "react"
import { Tooltip } from "antd"
import { CheckIcon, ClipboardIcon } from "lucide-react"
import { useTranslation } from "react-i18next"
export default function Markdown({ message }: { message: string }) {
const [isBtnPressed, setIsBtnPressed] = React.useState(false)
React.useEffect(() => {
if (isBtnPressed) {
setTimeout(() => {
setIsBtnPressed(false)
}, 4000)
}
}, [isBtnPressed])
const { t } = useTranslation("common")
return (
<React.Fragment>
@ -37,11 +31,14 @@ export default function Markdown({ message }: { message: string }) {
</span>
<div className="flex items-center">
<Tooltip title="Copy to clipboard">
<Tooltip title={t("copyToClipboard")}>
<button
onClick={() => {
navigator.clipboard.writeText(children[0] as string)
setIsBtnPressed(true)
setTimeout(() => {
setIsBtnPressed(false)
}, 4000)
}}
className="flex gap-1.5 items-center rounded bg-none p-1 text-xs text-gray-200 hover:bg-gray-700 hover:text-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 focus:ring-offset-gray-100">
{!isBtnPressed ? (

View File

@ -1,6 +1,7 @@
import { useForm } from "@mantine/form"
import React from "react"
import useDynamicTextareaSize from "~hooks/useDynamicTextareaSize"
import { useTranslation } from "react-i18next"
import useDynamicTextareaSize from "~/hooks/useDynamicTextareaSize"
type Props = {
value: string
@ -12,6 +13,7 @@ type Props = {
export const EditMessageForm = (props: Props) => {
const [isComposing, setIsComposing] = React.useState(false)
const textareaRef = React.useRef<HTMLTextAreaElement>(null)
const { t } = useTranslation("common")
const form = useForm({
initialValues: {
@ -40,21 +42,21 @@ export const EditMessageForm = (props: Props) => {
rows={1}
style={{ minHeight: "60px" }}
tabIndex={0}
placeholder="Type a message..."
placeholder={t("editMessage.placeholder")}
ref={textareaRef}
className="w-full bg-transparent focus-within:outline-none focus:ring-0 focus-visible:ring-0 ring-0 dark:ring-0 border-0 dark:text-gray-100"
/>
<div className="flex justify-center space-x-2 mt-2">
<button
aria-label="Save"
aria-label={t("save")}
className="bg-white dark:bg-black px-2.5 py-2 rounded-md text-gray-700 dark:text-gray-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-gray-500 hover:bg-gray-100 dark:hover:bg-gray-900">
{props.isBot ? "Save" : "Save & Submit"}
{props.isBot ? t("save") : t("saveAndSubmit")}
</button>
<button
onClick={props.onClose}
aria-label="Cancel"
aria-label={t("cancel")}
className="border dark:border-gray-600 px-2.5 py-2 rounded-md text-gray-700 dark:text-gray-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-gray-500 hover:bg-gray-100 dark:hover:bg-gray-900">
Cancel
{t("cancel")}
</button>
</div>
</form>

View File

@ -4,6 +4,7 @@ import { Image, Tooltip } from "antd"
import { WebSearch } from "./WebSearch"
import { CheckIcon, ClipboardIcon, Pen, RotateCcw } from "lucide-react"
import { EditMessageForm } from "./EditMessageForm"
import { useTranslation } from "react-i18next"
type Props = {
message: string
@ -28,6 +29,8 @@ export const PlaygroundMessage = (props: Props) => {
const [isBtnPressed, setIsBtnPressed] = React.useState(false)
const [editMode, setEditMode] = React.useState(false)
const { t } = useTranslation("common")
return (
<div className="group w-full text-gray-800 dark:text-gray-100">
<div className="text-base md:max-w-2xl lg:max-w-xl xl:max-w-3xl flex lg:px-0 m-auto w-full">
@ -112,7 +115,8 @@ export const PlaygroundMessage = (props: Props) => {
{props.isBot && (
<>
{!props.hideCopy && (
<Tooltip title="Copy to clipboard">
<Tooltip title={t("copyToClipboard")}
>
<button
onClick={() => {
navigator.clipboard.writeText(props.message)
@ -133,7 +137,8 @@ export const PlaygroundMessage = (props: Props) => {
{!props.hideEditAndRegenerate &&
props.currentMessageIndex === props.totalMessages - 1 && (
<Tooltip title="Regenerate">
<Tooltip title={t("regenerate")}
>
<button
onClick={props.onRengerate}
className="flex items-center justify-center w-6 h-6 rounded-full bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500">
@ -144,7 +149,8 @@ export const PlaygroundMessage = (props: Props) => {
</>
)}
{!props.hideEditAndRegenerate && (
<Tooltip title="Edit">
<Tooltip title={t("edit")}
>
<button
onClick={() => setEditMode(true)}
className="flex items-center justify-center w-6 h-6 rounded-full bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500">

View File

@ -1,12 +1,16 @@
import { Globe } from "lucide-react"
import { useTranslation } from "react-i18next"
export const WebSearch = () => {
const {t} = useTranslation('common')
return (
<div className="gradient-border mt-4 flex w-56 items-center gap-4 rounded-lg bg-neutral-100 p-1ccc text-slate-900 dark:bg-neutral-800 dark:text-slate-50">
<div className="rounded p-1">
<Globe className="w-6 h-6" />
</div>
<div className="text-sm font-semibold">Searching the web</div>
<div className="text-sm font-semibold">
{t('webSearch')}
</div>
</div>
)
}

View File

@ -1,5 +1,6 @@
import { useState } from "react"
import { CheckIcon } from "lucide-react"
import { useTranslation } from "react-i18next"
type Props = {
onClick?: () => void
disabled?: boolean
@ -13,11 +14,12 @@ export const SaveButton = ({
onClick,
disabled,
className,
text = "Save",
textOnSave = "Saved",
text = "save",
textOnSave = "saved",
btnType = "button"
}: Props) => {
const [clickedSave, setClickedSave] = useState(false)
const { t } = useTranslation("common")
return (
<button
type={btnType}
@ -33,7 +35,7 @@ export const SaveButton = ({
disabled={disabled}
className={`inline-flex mt-4 items-center rounded-md border border-transparent bg-black px-2 py-2 text-sm font-medium leading-4 text-white shadow-sm dark:bg-white dark:text-gray-800 disabled:opacity-50 ${className}`}>
{clickedSave ? <CheckIcon className="w-4 h-4 mr-2" /> : null}
{clickedSave ? textOnSave : text}
{clickedSave ? t(textOnSave) : t(text)}
</button>
)
}

View File

@ -1,13 +1,14 @@
import { Form, Image, Input, Modal, Tooltip, message } from "antd"
import { Share } from "lucide-react"
import { useState } from "react"
import type { Message } from "~store/option"
import type { Message } from "~/store/option"
import Markdown from "./Markdown"
import React from "react"
import { useMutation } from "@tanstack/react-query"
import { getPageShareUrl } from "~services/ollama"
import { cleanUrl } from "~libs/clean-url"
import { getUserId, saveWebshare } from "~libs/db"
import { getPageShareUrl } from "~/services/ollama"
import { cleanUrl } from "~/libs/clean-url"
import { getUserId, saveWebshare } from "~/libs/db"
import { useTranslation } from "react-i18next"
type Props = {
messages: Message[]
@ -75,6 +76,7 @@ export const PlaygroundMessage = (
}
export const ShareBtn: React.FC<Props> = ({ messages }) => {
const { t } = useTranslation("common")
const [open, setOpen] = useState(false)
const [form] = Form.useForm()
const name = Form.useWatch("name", form)
@ -104,7 +106,7 @@ export const ShareBtn: React.FC<Props> = ({ messages }) => {
})
})
if (!res.ok) throw new Error("Failed to create share link")
if (!res.ok) throw new Error(t("share.notification.failGenerate"))
const data = await res.json()
@ -121,18 +123,23 @@ export const ShareBtn: React.FC<Props> = ({ messages }) => {
onSuccess: async (data) => {
const url = data.url
navigator.clipboard.writeText(url)
message.success("Link copied to clipboard")
await saveWebshare({ title: data.title, url, api_url: data.api_url, share_id: data.share_id })
message.success(t("share.notification.successGenerate"))
await saveWebshare({
title: data.title,
url,
api_url: data.api_url,
share_id: data.share_id
})
setOpen(false)
},
onError: (error) => {
message.error(error?.message || "Failed to create share link")
message.error(error?.message || t("share.notification.failGenerate"))
}
})
return (
<>
<Tooltip title="Share">
<Tooltip title={t("share.tooltip.share")}>
<button
onClick={() => setOpen(true)}
className="!text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
@ -141,7 +148,7 @@ export const ShareBtn: React.FC<Props> = ({ messages }) => {
</Tooltip>
<Modal
title="Share link to Chat"
title={t("share.modal.title")}
open={open}
footer={null}
width={600}
@ -151,20 +158,30 @@ export const ShareBtn: React.FC<Props> = ({ messages }) => {
layout="vertical"
onFinish={createShareLink}
initialValues={{
title: "Untitled Chat",
name: "Anonymous"
title: t("share.form.defaultValue.title"),
name: t("share.form.defaultValue.name")
}}>
<Form.Item
name="title"
label="Chat Title"
rules={[{ required: true, message: "Please enter chat title" }]}>
<Input size="large" placeholder="Enter chat title" />
label={t("share.form.title.label")}
rules={[
{ required: true, message: t("share.form.title.required") }
]}>
<Input
size="large"
placeholder={t("share.form.title.placeholder")}
/>
</Form.Item>
<Form.Item
name="name"
label="Your Name"
rules={[{ required: true, message: "Please enter your name" }]}>
<Input size="large" placeholder="Enter your name" />
label={t("share.form.name.label")}
rules={[
{ required: true, message: t("share.form.name.required") }
]}>
<Input
size="large"
placeholder={t("share.form.name.placeholder")}
/>
</Form.Item>
<Form.Item>
@ -182,7 +199,9 @@ export const ShareBtn: React.FC<Props> = ({ messages }) => {
<button
type="submit"
className="inline-flex items-center rounded-md border border-transparent bg-black px-2 py-2.5 text-md font-medium leading-4 text-white shadow-sm dark:bg-white dark:text-gray-800 disabled:opacity-50 ">
{isPending ? "Generating link..." : "Generate Link"}
{isPending
? t("share.form.btn.saving")
: t("share.form.btn.save")}
</button>
</div>
</Form.Item>

View File

@ -4,8 +4,8 @@ import { useLocation, NavLink } from "react-router-dom"
import { Sidebar } from "../Option/Sidebar"
import { Drawer, Select, Tooltip } from "antd"
import { useQuery } from "@tanstack/react-query"
import { getAllModels } from "~services/ollama"
import { useMessageOption } from "~hooks/useMessageOption"
import { getAllModels } from "~/services/ollama"
import { useMessageOption } from "~/hooks/useMessageOption"
import {
ChevronLeft,
CogIcon,
@ -15,8 +15,9 @@ import {
SquarePen,
ZapIcon
} from "lucide-react"
import { getAllPrompts } from "~libs/db"
import { ShareBtn } from "~components/Common/ShareBtn"
import { getAllPrompts } from "~/libs/db"
import { ShareBtn } from "~/components/Common/ShareBtn"
import { useTranslation } from "react-i18next"
export default function OptionLayout({
children
@ -24,6 +25,8 @@ export default function OptionLayout({
children: React.ReactNode
}) {
const [sidebarOpen, setSidebarOpen] = useState(false)
const { t } = useTranslation(["option", "common"])
const {
selectedModel,
setSelectedModel,
@ -61,8 +64,8 @@ export default function OptionLayout({
if (prompt?.is_system) {
setSelectedSystemPrompt(prompt.id)
} else {
setSelectedQuickPrompt(prompt.content)
setSelectedSystemPrompt(null)
setSelectedQuickPrompt(prompt!.content)
setSelectedSystemPrompt("")
}
}
@ -93,7 +96,7 @@ export default function OptionLayout({
onClick={clearChat}
className="inline-flex items-center rounded-lg border dark:border-gray-700 bg-transparent px-3 py-3 text-sm font-medium leading-4 text-gray-800 dark:text-white disabled:opacity-50 ">
<SquarePen className="h-4 w-4 mr-3" />
New Chat
{t("newChat")}
</button>
</div>
<span className="text-lg font-thin text-zinc-300 dark:text-zinc-600">
@ -106,12 +109,13 @@ export default function OptionLayout({
size="large"
loading={isModelsLoading || isModelsFetching}
filterOption={(input, option) =>
option.label.toLowerCase().indexOf(input.toLowerCase()) >=
option!.label.toLowerCase().indexOf(input.toLowerCase()) >=
0 ||
option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0
option!.value.toLowerCase().indexOf(input.toLowerCase()) >=
0
}
showSearch
placeholder="Select a model"
placeholder={t("common:selectAModel")}
className="w-64 "
options={models?.map((model) => ({
label: model.name,
@ -127,12 +131,13 @@ export default function OptionLayout({
size="large"
loading={isPromptLoading}
showSearch
placeholder="Select a prompt"
placeholder={t("selectAPrompt")}
className="w-60"
allowClear
onChange={handlePromptChange}
value={selectedSystemPrompt}
filterOption={(input, option) =>
//@ts-ignore
option.label.key
.toLowerCase()
.indexOf(input.toLowerCase()) >= 0
@ -161,7 +166,8 @@ export default function OptionLayout({
{pathname === "/" && messages.length > 0 && !streaming && (
<ShareBtn messages={messages} />
)}
<Tooltip title="Github Repository">
<Tooltip title={t("githubRepository")}
>
<a
href="https://github.com/n4ze3m/page-assist"
target="_blank"
@ -169,7 +175,8 @@ export default function OptionLayout({
<GithubIcon className="w-6 h-6" />
</a>
</Tooltip>
<Tooltip title="Manage Ollama Models">
<Tooltip title={t("settings")}
>
<NavLink
to="/settings"
className="!text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
@ -185,14 +192,12 @@ export default function OptionLayout({
</div>
<Drawer
title={"Chat History"}
title={t("sidebarTitle")}
placement="left"
closeIcon={null}
onClose={() => setSidebarOpen(false)}
open={sidebarOpen}>
<Sidebar
onClose={() => setSidebarOpen(false)}
/>
<Sidebar onClose={() => setSidebarOpen(false)} />
</Drawer>
</div>
)

View File

@ -5,6 +5,7 @@ import {
Orbit,
Share
} from "lucide-react"
import { useTranslation } from "react-i18next"
import { Link, useLocation } from "react-router-dom"
function classNames(...classes: string[]) {
@ -25,7 +26,7 @@ const LinkComponent = (item: {
item.current === item.href
? "bg-gray-100 text-gray-600 dark:bg-[#262626] dark:text-white"
: "text-gray-700 hover:text-gray-600 hover:bg-gray-100 dark:text-gray-200 dark:hover:text-white dark:hover:bg-[#262626]",
"group flex gap-x-3 rounded-md py-2 pl-2 pr-3 text-sm leading-6 font-semibold"
"group flex gap-x-3 rounded-md py-2 pl-2 pr-3 text-sm font-semibold"
)}>
<item.icon
className={classNames(
@ -44,6 +45,8 @@ const LinkComponent = (item: {
export const SettingsLayout = ({ children }: { children: React.ReactNode }) => {
const location = useLocation()
const { t } = useTranslation("settings")
return (
<>
<div className="mx-auto max-w-7xl lg:flex lg:gap-x-16 lg:px-8">
@ -54,31 +57,31 @@ export const SettingsLayout = ({ children }: { children: React.ReactNode }) => {
className="flex gap-x-3 gap-y-1 whitespace-nowrap lg:flex-col">
<LinkComponent
href="/settings"
name="General Settings"
name={t("generalSettings.title")}
icon={Orbit}
current={location.pathname}
/>
<LinkComponent
href="/settings/ollama"
name="Ollama Settings"
name={t("ollamaSettings.title")}
icon={CircuitBoardIcon}
current={location.pathname}
/>
<LinkComponent
href="/settings/model"
name="Manage Model"
name={t("manageModels.title")}
current={location.pathname}
icon={BrainCircuit}
/>
<LinkComponent
href="/settings/prompt"
name="Manage Prompt"
name={t("managePrompts.title")}
icon={Book}
current={location.pathname}
/>
<LinkComponent
href="/settings/share"
name="Manage Share"
name={t("manageShare.title")}
icon={Share}
current={location.pathname}
/>

View File

@ -1,18 +1,20 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { Skeleton, Table, Tag, Tooltip, notification, Modal, Input } from "antd"
import { bytePerSecondFormatter } from "~libs/byte-formater"
import { deleteModel, getAllModels } from "~services/ollama"
import { bytePerSecondFormatter } from "~/libs/byte-formater"
import { deleteModel, getAllModels } from "~/services/ollama"
import dayjs from "dayjs"
import relativeTime from "dayjs/plugin/relativeTime"
import { useState } from "react"
import { useForm } from "@mantine/form"
import { Download, RotateCcw, Trash2 } from "lucide-react"
import { useTranslation } from "react-i18next"
dayjs.extend(relativeTime)
export const ModelsBody = () => {
const queryClient = useQueryClient()
const [open, setOpen] = useState(false)
const { t } = useTranslation(["settings", "common"])
const form = useForm({
initialValues: {
@ -32,22 +34,24 @@ export const ModelsBody = () => {
queryKey: ["fetchAllModels"]
})
notification.success({
message: "Model Deleted",
description: "Model has been deleted successfully"
message: t("manageModels.notification.success"),
description: t("manageModels.notification.successDeleteDescription")
})
},
onError: (error) => {
notification.error({
message: "Error",
description: error?.message || "Something went wrong"
description: error?.message || t("manageModels.notification.someError")
})
}
})
const pullModel = async (modelName: string) => {
notification.info({
message: "Pulling Model",
description: `Pulling ${modelName} model. For more details, check the extension icon.`
message: t("manageModels.notification.pullModel"),
description: t("manageModels.notification.pullModelDescription", {
modelName
})
})
setOpen(false)
@ -76,7 +80,7 @@ export const ModelsBody = () => {
<button
onClick={() => setOpen(true)}
className="inline-flex items-center rounded-md border border-transparent bg-black px-2 py-2 text-md font-medium leading-4 text-white shadow-sm hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-white dark:text-gray-800 dark:hover:bg-gray-100 dark:focus:ring-gray-500 dark:focus:ring-offset-gray-100 disabled:opacity-50">
Add New Model
{t("manageModels.addBtn")}
</button>
</div>
</div>
@ -88,12 +92,12 @@ export const ModelsBody = () => {
<Table
columns={[
{
title: "Name",
title: t("manageModels.columns.name"),
dataIndex: "name",
key: "name"
},
{
title: "Digest",
title: t("manageModels.columns.digest"),
dataIndex: "digest",
key: "digest",
render: (text: string) => (
@ -105,28 +109,26 @@ export const ModelsBody = () => {
)
},
{
title: "Modified",
title: t("manageModels.columns.modifiedAt"),
dataIndex: "modified_at",
key: "modified_at",
render: (text: string) => dayjs(text).fromNow(true)
},
{
title: "Size",
title: t("manageModels.columns.size"),
dataIndex: "size",
key: "size",
render: (text: number) => bytePerSecondFormatter(text)
},
{
title: "Action",
title: t("manageModels.columns.actions"),
render: (_, record) => (
<div className="flex gap-4">
<Tooltip title="Delete Model">
<Tooltip title={t("manageModels.tooltip.delete")}>
<button
onClick={() => {
if (
window.confirm(
"Are you sure you want to delete this model?"
)
window.confirm(t("manageModels.confirm.delete"))
) {
deleteOllamaModel(record.model)
}
@ -135,13 +137,11 @@ export const ModelsBody = () => {
<Trash2 className="w-5 h-5" />
</button>
</Tooltip>
<Tooltip title="Re-Pull Model">
<Tooltip title={t("manageModels.tooltip.repull")}>
<button
onClick={() => {
if (
window.confirm(
"Are you sure you want to re-pull this model?"
)
window.confirm(t("manageModels.confirm.repull"))
) {
pullOllamaModel(record.model)
}
@ -160,35 +160,41 @@ export const ModelsBody = () => {
pagination={false}
columns={[
{
title: "Parent Model",
title: t("manageModels.expandedColumns.parentModel"),
key: "parent_model",
dataIndex: "parent_model"
},
{
title: "Format",
title: t("manageModels.expandedColumns.format"),
key: "format",
dataIndex: "format"
},
{
title: "Family",
title: t("manageModels.expandedColumns.family"),
key: "family",
dataIndex: "family"
},
{
title: "Parameter Size",
title: t("manageModels.expandedColumns.parameterSize"),
key: "parameter_size",
dataIndex: "parameter_size"
},
{
title: "Quantization Level",
title: t(
"manageModels.expandedColumns.quantizationLevel"
),
key: "quantization_level",
dataIndex: "quantization_level"
}
]}
dataSource={[record.details]}
locale={{
emptyText: t("common:noData")
}}
/>
),
defaultExpandAllRows: false
defaultExpandAllRows: false,
}}
bordered
dataSource={data}
@ -200,13 +206,13 @@ export const ModelsBody = () => {
<Modal
footer={null}
open={open}
title="Add New Model"
title={t("manageModels.modal.title")}
onCancel={() => setOpen(false)}>
<form
onSubmit={form.onSubmit((values) => pullOllamaModel(values.model))}>
<Input
{...form.getInputProps("model")}
placeholder="Enter model name"
placeholder={t("manageModels.modal.placeholder")}
size="large"
/>
@ -214,7 +220,7 @@ export const ModelsBody = () => {
type="submit"
className="inline-flex justify-center w-full text-center mt-4 items-center rounded-md border border-transparent bg-black px-2 py-2 text-sm font-medium leading-4 text-white shadow-sm hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-white dark:text-gray-800 dark:hover:bg-gray-100 dark:focus:ring-gray-500 dark:focus:ring-offset-gray-100 disabled:opacity-50 ">
<Download className="w-5 h-5 mr-3" />
Pull Model
{t("manageModels.modal.pull")}
</button>
</form>
</Modal>

View File

@ -1,7 +1,7 @@
import React from "react"
import { useMessageOption } from "~hooks/useMessageOption"
import { useMessageOption } from "~/hooks/useMessageOption"
import { PlaygroundEmpty } from "./PlaygroundEmpty"
import { PlaygroundMessage } from "~components/Common/Playground/Message"
import { PlaygroundMessage } from "~/components/Common/Playground/Message"
export const PlaygroundChat = () => {
const {

View File

@ -1,14 +1,16 @@
import { useQuery } from "@tanstack/react-query"
import { RotateCcw } from "lucide-react"
import { useEffect, useState } from "react"
import { useTranslation } from "react-i18next"
import {
getOllamaURL,
isOllamaRunning,
setOllamaURL as saveOllamaURL
} from "~services/ollama"
} from "~/services/ollama"
export const PlaygroundEmpty = () => {
const [ollamaURL, setOllamaURL] = useState<string>("")
const { t } = useTranslation(["playground", "common"])
const {
data: ollamaInfo,
status: ollamaStatus,
@ -40,7 +42,7 @@ export const PlaygroundEmpty = () => {
<div className="inline-flex items-center space-x-2">
<div className="w-3 h-3 bg-blue-500 rounded-full animate-bounce"></div>
<p className="dark:text-gray-400 text-gray-900">
Searching for Your Ollama 🦙
{t("ollamaState.searching")}
</p>
</div>
)}
@ -49,7 +51,7 @@ export const PlaygroundEmpty = () => {
<div className="inline-flex items-center space-x-2">
<div className="w-3 h-3 bg-green-500 rounded-full"></div>
<p className="dark:text-gray-400 text-gray-900">
Ollama is running 🦙
{t("ollamaState.running")}
</p>
</div>
) : (
@ -57,7 +59,7 @@ export const PlaygroundEmpty = () => {
<div className="inline-flex space-x-2">
<div className="w-3 h-3 bg-red-500 rounded-full"></div>
<p className="dark:text-gray-400 text-gray-900">
Unable to connect to Ollama 🦙
{t("ollamaState.notRunning")}
</p>
</div>
@ -75,7 +77,7 @@ export const PlaygroundEmpty = () => {
}}
className="inline-flex mt-4 items-center rounded-md border border-transparent bg-black px-2 py-2 text-sm font-medium leading-4 text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-white dark:text-gray-800 dark:hover:bg-gray-100 dark:focus:ring-gray-500 dark:focus:ring-offset-gray-100 disabled:opacity-50 ">
<RotateCcw className="h-4 w-4 mr-3" />
Retry
{t("common:retry")}
</button>
</div>
)

View File

@ -1,22 +1,24 @@
import { useForm } from "@mantine/form"
import { useMutation, useQueryClient } from "@tanstack/react-query"
import React from "react"
import useDynamicTextareaSize from "~hooks/useDynamicTextareaSize"
import { toBase64 } from "~libs/to-base64"
import { useMessageOption } from "~hooks/useMessageOption"
import useDynamicTextareaSize from "~/hooks/useDynamicTextareaSize"
import { toBase64 } from "~/libs/to-base64"
import { useMessageOption } from "~/hooks/useMessageOption"
import { Checkbox, Dropdown, Switch, Tooltip } from "antd"
import { Image } from "antd"
import { useSpeechRecognition } from "~hooks/useSpeechRecognition"
import { useWebUI } from "~store/webui"
import { defaultEmbeddingModelForRag } from "~services/ollama"
import { useSpeechRecognition } from "~/hooks/useSpeechRecognition"
import { useWebUI } from "~/store/webui"
import { defaultEmbeddingModelForRag } from "~/services/ollama"
import { ImageIcon, MicIcon, StopCircleIcon, X } from "lucide-react"
import { getVariable } from "~utils/select-varaible"
import { getVariable } from "~/utils/select-varaible"
import { useTranslation } from "react-i18next"
type Props = {
dropedFile: File | undefined
}
export const PlaygroundForm = ({ dropedFile }: Props) => {
const { t } = useTranslation(["playground", "common"])
const inputRef = React.useRef<HTMLInputElement>(null)
const [typing, setTyping] = React.useState<boolean>(false)
const {
@ -131,16 +133,13 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
return
}
if (!selectedModel || selectedModel.length === 0) {
form.setFieldError("message", "Please select a model")
form.setFieldError("message", t("formError.noModel"))
return
}
if (webSearch) {
const defaultEM = await defaultEmbeddingModelForRag()
if (!defaultEM) {
form.setFieldError(
"message",
"Please set an embedding model on the Settings > Ollama page"
)
form.setFieldError("message", t("formError.noEmbeddingModel"))
return
}
}
@ -181,16 +180,13 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
<form
onSubmit={form.onSubmit(async (value) => {
if (!selectedModel || selectedModel.length === 0) {
form.setFieldError("message", "Please select a model")
form.setFieldError("message", t("formError.noModel"))
return
}
if (webSearch) {
const defaultEM = await defaultEmbeddingModelForRag()
if (!defaultEM) {
form.setFieldError(
"message",
"Please set an embedding model on the Settings > Ollama page"
)
form.setFieldError("message", t("formError.noEmbeddingModel"))
return
}
}
@ -223,12 +219,12 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
rows={1}
style={{ minHeight: "60px" }}
tabIndex={0}
placeholder="Type a message..."
placeholder={t("form.textarea.placeholder")}
{...form.getInputProps("message")}
/>
<div className="mt-4 flex justify-between items-center">
<div className="flex">
<Tooltip title="Search Internet">
<Tooltip title={t("tooltip.searchInternet")}>
<div className="inline-flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
@ -246,14 +242,14 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
<Switch
value={webSearch}
onChange={(e) => setWebSearch(e)}
checkedChildren="On"
unCheckedChildren="Off"
checkedChildren={t("form.webSearch.on")}
unCheckedChildren={t("form.webSearch.off")}
/>
</div>
</Tooltip>
</div>
<div className="flex !justify-end gap-3">
<Tooltip title="Voice Message">
<Tooltip title={t("tooltip.speechToText")}>
<button
type="button"
onClick={() => {
@ -277,7 +273,7 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
)}
</button>
</Tooltip>
<Tooltip title="Upload Image">
<Tooltip title={t("tooltip.uploadImage")}>
<button
type="button"
onClick={() => {
@ -319,7 +315,7 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
onChange={(e) =>
setSendWhenEnter(e.target.checked)
}>
Send when Enter pressed
{t("sendWhenEnter")}
</Checkbox>
)
}
@ -340,11 +336,11 @@ export const PlaygroundForm = ({ dropedFile }: Props) => {
<path d="M20 4v7a4 4 0 01-4 4H4"></path>
</svg>
) : null}
Submit
{t("common:submit")}
</div>
</Dropdown.Button>
) : (
<Tooltip title="Stop Streaming">
<Tooltip title={t("tooltip.stopStreaming")}>
<button
type="button"
onClick={stopStreamingRequest}

View File

@ -1,8 +1,10 @@
import { PencilIcon } from "lucide-react"
import { useMessage } from "../../../hooks/useMessage"
import { useTranslation } from 'react-i18next';
export const PlaygroundNewChat = () => {
const { setHistory, setMessages, setHistoryId } = useMessage()
const { t } = useTranslation('optionChat')
const handleClick = () => {
setHistoryId(null)
@ -16,7 +18,7 @@ export const PlaygroundNewChat = () => {
className="flex w-full border bg-transparent hover:bg-gray-200 dark:hover:bg-gray-800 text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 focus:ring-offset-gray-100 rounded-md p-2 dark:border-gray-800">
<PencilIcon className="mx-3 h-5 w-5" aria-hidden="true" />
<span className="inline-flex font-semibol text-white text-sm">
New Chat
{t('newChat')}
</span>
</button>
)

View File

@ -7,16 +7,18 @@ import {
Modal,
Input,
Form,
Switch
Switch,
Empty
} from "antd"
import { Trash2, Pen, Computer, Zap } from "lucide-react"
import { useState } from "react"
import { useTranslation } from "react-i18next"
import {
deletePromptById,
getAllPrompts,
savePrompt,
updatePrompt
} from "~libs/db"
} from "~/libs/db"
export const PromptBody = () => {
const queryClient = useQueryClient()
@ -25,6 +27,7 @@ export const PromptBody = () => {
const [editId, setEditId] = useState("")
const [createForm] = Form.useForm()
const [editForm] = Form.useForm()
const { t } = useTranslation("settings")
const { data, status } = useQuery({
queryKey: ["fetchAllPrompts"],
@ -38,14 +41,14 @@ export const PromptBody = () => {
queryKey: ["fetchAllPrompts"]
})
notification.success({
message: "Model Deleted",
description: "Model has been deleted successfully"
message: t("managePrompts.notification.deletedSuccess"),
description: t("managePrompts.notification.deletedSuccessDesc")
})
},
onError: (error) => {
notification.error({
message: "Error",
description: error?.message || "Something went wrong"
message: t("managePrompts.notification.error"),
description: error?.message || t("managePrompts.notification.someError")
})
}
})
@ -60,14 +63,15 @@ export const PromptBody = () => {
setOpen(false)
createForm.resetFields()
notification.success({
message: "Prompt Added",
description: "Prompt has been added successfully"
message: t("managePrompts.notification.addSuccess"),
description: t("managePrompts.notification.addSuccessDesc")
})
},
onError: (error) => {
notification.error({
message: "Error",
description: error?.message || "Something went wrong"
message: t("managePrompts.notification.error"),
description:
error?.message || t("managePrompts.notification.someError")
})
}
})
@ -87,14 +91,15 @@ export const PromptBody = () => {
setOpenEdit(false)
editForm.resetFields()
notification.success({
message: "Prompt Updated",
description: "Prompt has been updated successfully"
message: t("managePrompts.notification.updatedSuccess"),
description: t("managePrompts.notification.updatedSuccessDesc")
})
},
onError: (error) => {
notification.error({
message: "Error",
description: error?.message || "Something went wrong"
message: t("managePrompts.notification.error"),
description:
error?.message || t("managePrompts.notification.someError")
})
}
})
@ -108,7 +113,7 @@ export const PromptBody = () => {
<button
onClick={() => setOpen(true)}
className="inline-flex items-center rounded-md border border-transparent bg-black px-2 py-2 text-md font-medium leading-4 text-white shadow-sm hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-white dark:text-gray-800 dark:hover:bg-gray-100 dark:focus:ring-gray-500 dark:focus:ring-offset-gray-100 disabled:opacity-50">
Add New Prompt
{t("managePrompts.addBtn")}
</button>
</div>
</div>
@ -120,43 +125,41 @@ export const PromptBody = () => {
<Table
columns={[
{
title: "Title",
title: t("managePrompts.columns.title"),
dataIndex: "title",
key: "title"
},
{
title: "Prompt",
title: t("managePrompts.columns.prompt"),
dataIndex: "content",
key: "content"
},
{
title: "Prompt Type",
title: t("managePrompts.columns.type"),
dataIndex: "is_system",
key: "is_system",
render: (is_system) =>
is_system ? (
<span className="flex items-center gap-2">
<Computer className="w-5 h-5 " />
System Prompt
{t("managePrompts.systemPrompt")}
</span>
) : (
<span className="flex items-center gap-2">
<Zap className="w-5 h-5" />
Quick Prompt
{t("managePrompts.quickPrompt")}
</span>
)
},
{
title: "Action",
title: t("managePrompts.columns.actions"),
render: (_, record) => (
<div className="flex gap-4">
<Tooltip title="Delete Prompt">
<Tooltip title={t("managePrompts.tooltip.delete")}>
<button
onClick={() => {
if (
window.confirm(
"Are you sure you want to delete this prompt? This action cannot be undone."
)
window.confirm(t("managePrompts.confirm.delete"))
) {
deletePrompt(record.id)
}
@ -165,7 +168,7 @@ export const PromptBody = () => {
<Trash2 className="w-5 h-5" />
</button>
</Tooltip>
<Tooltip title="Edit Prompt">
<Tooltip title={t("managePrompts.tooltip.edit")}>
<button
onClick={() => {
setEditId(record.id)
@ -188,7 +191,7 @@ export const PromptBody = () => {
</div>
<Modal
title="Add New Prompt"
title={t("managePrompts.modal.addTitle")}
open={open}
onCancel={() => setOpen(false)}
footer={null}>
@ -198,25 +201,35 @@ export const PromptBody = () => {
form={createForm}>
<Form.Item
name="title"
label="Title"
rules={[{ required: true, message: "Title is required" }]}>
<Input placeholder="My Awesome Prompt" />
label={t("managePrompts.form.title.label")}
rules={[
{
required: true,
message: t("managePrompts.form.title.required")
}
]}>
<Input placeholder={t("managePrompts.form.title.placeholder")} />
</Form.Item>
<Form.Item
name="content"
label="Prompt"
rules={[{ required: true, message: "Prompt is required" }]}
help="You can use {key} as variable in your prompt.">
label={t("managePrompts.form.prompt.label")}
rules={[
{
required: true,
message: t("managePrompts.form.prompt.required")
}
]}
help={t("managePrompts.form.prompt.help")}>
<Input.TextArea
placeholder="Your prompt goes here..."
placeholder={t("managePrompts.form.prompt.placeholder")}
autoSize={{ minRows: 3, maxRows: 10 }}
/>
</Form.Item>
<Form.Item
name="is_system"
label="Is System Prompt"
label={t("managePrompts.form.isSystem.label")}
valuePropName="checked">
<Switch />
</Form.Item>
@ -225,14 +238,16 @@ export const PromptBody = () => {
<button
disabled={savePromptLoading}
className="inline-flex justify-center w-full text-center mt-4 items-center rounded-md border border-transparent bg-black px-2 py-2 text-sm font-medium leading-4 text-white shadow-sm hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-white dark:text-gray-800 dark:hover:bg-gray-100 dark:focus:ring-gray-500 dark:focus:ring-offset-gray-100 disabled:opacity-50 ">
{savePromptLoading ? "Adding Prompt..." : "Add Prompt"}
{savePromptLoading
? t("managePrompts.form.btnSave.saving")
: t("managePrompts.form.btnSave.save")}
</button>
</Form.Item>
</Form>
</Modal>
<Modal
title="Update Prompt"
title={t("managePrompts.modal.editTitle")}
open={openEdit}
onCancel={() => setOpenEdit(false)}
footer={null}>
@ -242,25 +257,35 @@ export const PromptBody = () => {
form={editForm}>
<Form.Item
name="title"
label="Title"
rules={[{ required: true, message: "Title is required" }]}>
<Input placeholder="My Awesome Prompt" />
label={t("managePrompts.form.title.label")}
rules={[
{
required: true,
message: t("managePrompts.form.title.required")
}
]}>
<Input placeholder={t("managePrompts.form.title.placeholder")} />
</Form.Item>
<Form.Item
name="content"
label="Prompt"
rules={[{ required: true, message: "Prompt is required" }]}
help="You can use {key} as variable in your prompt.">
label={t("managePrompts.form.prompt.label")}
rules={[
{
required: true,
message: t("managePrompts.form.prompt.required")
}
]}
help={t("managePrompts.form.prompt.help")}>
<Input.TextArea
placeholder="Your prompt goes here..."
placeholder={t("managePrompts.form.prompt.placeholder")}
autoSize={{ minRows: 3, maxRows: 10 }}
/>
</Form.Item>
<Form.Item
name="is_system"
label="Is System Prompt"
label={t("managePrompts.form.isSystem.label")}
valuePropName="checked">
<Switch />
</Form.Item>
@ -269,7 +294,9 @@ export const PromptBody = () => {
<button
disabled={isUpdatingPrompt}
className="inline-flex justify-center w-full text-center mt-4 items-center rounded-md border border-transparent bg-black px-2 py-2 text-sm font-medium leading-4 text-white shadow-sm hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-white dark:text-gray-800 dark:hover:bg-gray-100 dark:focus:ring-gray-500 dark:focus:ring-offset-gray-100 disabled:opacity-50 ">
{isUpdatingPrompt ? "Updating Prompt..." : "Update Prompt"}
{isUpdatingPrompt
? t("managePrompts.form.btnEdit.saving")
: t("managePrompts.form.btnEdit.save")}
</button>
</Form.Item>
</Form>

View File

@ -1,7 +1,7 @@
import { useMutation, useQuery } from "@tanstack/react-query"
import { Form, InputNumber, Select, Skeleton } from "antd"
import { useState } from "react"
import { SaveButton } from "~components/Common/SaveButton"
import { SaveButton } from "~/components/Common/SaveButton"
import {
defaultEmbeddingChunkOverlap,
defaultEmbeddingChunkSize,
@ -10,11 +10,14 @@ import {
getOllamaURL,
saveForRag,
setOllamaURL as saveOllamaURL
} from "~services/ollama"
} from "~/services/ollama"
import { SettingPrompt } from "./prompt"
import { useTranslation } from "react-i18next"
export const SettingsOllama = () => {
const [ollamaURL, setOllamaURL] = useState<string>("")
const { t } = useTranslation("settings")
const { data: ollamaInfo, status } = useQuery({
queryKey: ["fetchOllamURL"],
queryFn: async () => {
@ -54,7 +57,7 @@ export const SettingsOllama = () => {
<div>
<div>
<h2 className="text-base font-semibold leading-7 text-gray-900 dark:text-white">
Configure Ollama
{t("ollamaSettings.heading")}
</h2>
<div className="border border-b border-gray-200 dark:border-gray-600 mt-3 mb-6"></div>
</div>
@ -62,7 +65,7 @@ export const SettingsOllama = () => {
<label
htmlFor="ollamaURL"
className="text-sm font-medium dark:text-gray-200">
Ollama URL
{t("ollamaSettings.settings.ollamaUrl.label")}
</label>
<input
type="url"
@ -71,7 +74,7 @@ export const SettingsOllama = () => {
onChange={(e) => {
setOllamaURL(e.target.value)
}}
placeholder="Your Ollama URL"
placeholder={t("ollamaSettings.settings.ollamaUrl.placeholder")}
className="w-full p-2 border border-gray-300 rounded-md dark:bg-[#262626] dark:text-gray-100"
/>
</div>
@ -88,7 +91,7 @@ export const SettingsOllama = () => {
<div>
<div>
<h2 className="text-base font-semibold leading-7 text-gray-900 dark:text-white">
Configure RAG
{t("ollamaSettings.settings.ragSettings.label")}
</h2>
<div className="border border-b border-gray-200 dark:border-gray-600 mt-3 mb-6"></div>
</div>
@ -108,18 +111,26 @@ export const SettingsOllama = () => {
}}>
<Form.Item
name="defaultEM"
label="Embedding Model"
help="Highly recommended to use embedding models like `nomic-embed-text`."
rules={[{ required: true, message: "Please select a model!" }]}>
label={t("ollamaSettings.settings.ragSettings.model.label")}
help={t("ollamaSettings.settings.ragSettings.model.help")}
rules={[
{
required: true,
message: t(
"ollamaSettings.settings.ragSettings.model.required"
)
}
]}>
<Select
size="large"
filterOption={(input, option) =>
option.label.toLowerCase().indexOf(input.toLowerCase()) >=
option!.label.toLowerCase().indexOf(input.toLowerCase()) >=
0 ||
option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0
option!.value.toLowerCase().indexOf(input.toLowerCase()) >=
0
}
showSearch
placeholder="Select a model"
placeholder={t("ollamaSettings.settings.ragSettings.model.placeholder")}
style={{ width: "100%" }}
className="mt-4"
options={ollamaInfo.models?.map((model) => ({
@ -131,27 +142,28 @@ export const SettingsOllama = () => {
<Form.Item
name="chunkSize"
label="Chunk Size"
label={t("ollamaSettings.settings.ragSettings.chunkSize.label")}
rules={[
{ required: true, message: "Please input your chunk size!" }
]}>
<InputNumber
style={{ width: "100%" }}
placeholder="Chunk Size"
/>
</Form.Item>
<Form.Item
name="chunkOverlap"
label="Chunk Overlap"
rules={[
{
required: true,
message: "Please input your chunk overlap!"
{ required: true, message: t("ollamaSettings.settings.ragSettings.chunkSize.required")
}
]}>
<InputNumber
style={{ width: "100%" }}
placeholder="Chunk Overlap"
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>
@ -164,7 +176,7 @@ export const SettingsOllama = () => {
<div>
<div>
<h2 className="text-base font-semibold leading-7 text-gray-900 dark:text-white">
Configure RAG Prompt
{t("ollamaSettings.settings.prompt.label")}
</h2>
<div className="border border-b border-gray-200 dark:border-gray-600 mt-3 mb-6"></div>
</div>

View File

@ -1,11 +1,13 @@
import { useQueryClient } from "@tanstack/react-query"
import { useDarkMode } from "~hooks/useDarkmode"
import { useMessageOption } from "~hooks/useMessageOption"
import { PageAssitDatabase } from "~libs/db"
import { useDarkMode } from "~/hooks/useDarkmode"
import { useMessageOption } from "~/hooks/useMessageOption"
import { PageAssitDatabase } from "~/libs/db"
import { Select } from "antd"
import { SUPPORTED_LANGUAGES } from "~utils/supporetd-languages"
import { SUPPORTED_LANGUAGES } from "~/utils/supporetd-languages"
import { MoonIcon, SunIcon } from "lucide-react"
import { SearchModeSettings } from "./search-mode"
import { useTranslation } from "react-i18next"
import { useI18n } from "@/hooks/useI18n"
export const SettingOther = () => {
const { clearChat, speechToTextLanguage, setSpeechToTextLanguage } =
@ -14,29 +16,36 @@ export const SettingOther = () => {
const queryClient = useQueryClient()
const { mode, toggleDarkMode } = useDarkMode()
const { t } = useTranslation("settings")
const {
changeLocale,
locale,
supportLanguage
}= useI18n()
return (
<dl className="flex flex-col space-y-6">
<dl className="flex flex-col space-y-6 text-sm">
<div>
<h2 className="text-base font-semibold leading-7 text-gray-900 dark:text-white">
Web UI Settings
{t("generalSettings.heading")}
</h2>
<div className="border border-b border-gray-200 dark:border-gray-600 mt-3"></div>
</div>
<div className="flex flex-row justify-between">
<span className="text-gray-500 dark:text-neutral-50">
Speech Recognition Language
{t("generalSettings.settings.speechRecognitionLang.label")}
</span>
<Select
placeholder="Select Language"
placeholder={t("generalSettings.settings.speechRecognitionLang.placeholder")}
allowClear
showSearch
options={SUPPORTED_LANGUAGES}
value={speechToTextLanguage}
filterOption={(input, option) =>
option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 ||
option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0
option!.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 ||
option!.value.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
onChange={(value) => {
setSpeechToTextLanguage(value)
@ -44,7 +53,30 @@ export const SettingOther = () => {
/>
</div>
<div className="flex flex-row justify-between">
<span className="text-gray-500 dark:text-neutral-50 ">Change Theme</span>
<span className="text-gray-500 dark:text-neutral-50">
{t("generalSettings.settings.language.label")}
</span>
<Select
placeholder={t("generalSettings.settings.language.placeholder")}
allowClear
showSearch
style={{ width: "200px" }}
options={supportLanguage}
value={locale}
filterOption={(input, option) =>
option!.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 ||
option!.value.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
onChange={(value) => {
changeLocale(value)
}}
/>
</div>
<div className="flex flex-row justify-between">
<span className="text-gray-500 dark:text-neutral-50 ">
{t("generalSettings.settings.darkMode.label")}
</span>
<button
onClick={toggleDarkMode}
@ -54,19 +86,19 @@ export const SettingOther = () => {
) : (
<MoonIcon className="w-4 h-4 mr-2" />
)}
{mode === "dark" ? "Light" : "Dark"}
{mode === "dark" ? t("generalSettings.settings.darkMode.options.light") : t("generalSettings.settings.darkMode.options.dark")}
</button>
</div>
<SearchModeSettings />
<div className="flex flex-row justify-between">
<span className="text-gray-500 dark:text-neutral-50 ">
Delete Chat History
{t("generalSettings.settings.deleteChatHistory.label")}
</span>
<button
onClick={async () => {
const confirm = window.confirm(
"Are you sure you want to delete your chat history? This action cannot be undone."
t("generalSettings.settings.deleteChatHistory.confirm")
)
if (confirm) {
@ -79,7 +111,7 @@ export const SettingOther = () => {
}
}}
className="bg-red-500 dark:bg-red-600 text-white dark:text-gray-200 px-4 py-2 rounded-md">
Delete
{t("generalSettings.settings.deleteChatHistory.button")}
</button>
</div>
</dl>

View File

@ -1,16 +1,19 @@
import { useQuery, useQueryClient } from "@tanstack/react-query"
import { Skeleton, Radio, Form, Alert } from "antd"
import React from "react"
import { SaveButton } from "~components/Common/SaveButton"
import { useTranslation } from "react-i18next"
import { SaveButton } from "~/components/Common/SaveButton"
import {
getWebSearchPrompt,
setSystemPromptForNonRagOption,
systemPromptForNonRagOption,
geWebSearchFollowUpPrompt,
setWebPrompts
} from "~services/ollama"
} from "~/services/ollama"
export const SettingPrompt = () => {
const { t } = useTranslation("settings")
const [selectedValue, setSelectedValue] = React.useState<"normal" | "web">(
"web"
)
@ -45,8 +48,12 @@ export const SettingPrompt = () => {
<Radio.Group
defaultValue={selectedValue}
onChange={(e) => setSelectedValue(e.target.value)}>
<Radio.Button value="normal">Normal</Radio.Button>
<Radio.Button value="web">Web</Radio.Button>
<Radio.Button value="normal">
{t("ollamaSettings.settings.prompt.option1")}
</Radio.Button>
<Radio.Button value="web">
{t("ollamaSettings.settings.prompt.option2")}
</Radio.Button>
</Radio.Group>
</div>
@ -64,18 +71,22 @@ export const SettingPrompt = () => {
}}>
<Form.Item>
<Alert
message="Configuring the system prompt here is deprecated. Please use the Manage Prompts section to add or edit prompts. This section will be removed in a future release"
message={t("ollamaSettings.settings.prompt.alert")}
type="warning"
showIcon
closable
/>
</Form.Item>
<Form.Item label="System Prompt" name="prompt">
<Form.Item
label={t("ollamaSettings.settings.prompt.systemPrompt")}
name="prompt">
<textarea
value={data.prompt}
rows={5}
id="ollamaPrompt"
placeholder="Your System Prompt"
placeholder={t(
"ollamaSettings.settings.prompt.systemPromptPlaceholder"
)}
className="w-full p-2 border border-gray-300 rounded-md dark:bg-[#262626] dark:text-gray-100"
/>
</Form.Item>
@ -104,38 +115,42 @@ export const SettingPrompt = () => {
webSearchFollowUpPrompt: data.webSearchFollowUpPrompt
}}>
<Form.Item
label="Web Search Prompt"
label={t("ollamaSettings.settings.prompt.webSearchPrompt")}
name="webSearchPrompt"
help="Do not remove `{search_results}` from the prompt."
help={t("ollamaSettings.settings.prompt.webSearchPromptHelp")}
rules={[
{
required: true,
message: "Please input your Web Search Prompt!"
message: t(
"ollamaSettings.settings.prompt.webSearchPromptError"
)
}
]}>
<textarea
value={data.webSearchPrompt}
rows={5}
id="ollamaWebSearchPrompt"
placeholder="Your Web Search Prompt"
placeholder={t(
"ollamaSettings.settings.prompt.webSearchPromptPlaceholder"
)}
className="w-full p-2 border border-gray-300 rounded-md dark:bg-[#262626] dark:text-gray-100"
/>
</Form.Item>
<Form.Item
label="Web Search Follow Up Prompt"
label={t("ollamaSettings.settings.prompt.webSearchFollowUpPrompt")}
name="webSearchFollowUpPrompt"
help="Do not remove `{chat_history}` and `{question}` from the prompt."
help={t("ollamaSettings.settings.prompt.webSearchFollowUpPromptHelp")}
rules={[
{
required: true,
message: "Please input your Web Search Follow Up Prompt!"
message: t("ollamaSettings.settings.prompt.webSearchFollowUpPromptError")
}
]}>
<textarea
value={data.webSearchFollowUpPrompt}
rows={5}
id="ollamaWebSearchFollowUpPrompt"
placeholder="Your Web Search Follow Up Prompt"
placeholder={t("ollamaSettings.settings.prompt.webSearchFollowUpPromptPlaceholder")}
className="w-full p-2 border border-gray-300 rounded-md dark:bg-[#262626] dark:text-gray-100"
/>
</Form.Item>

View File

@ -1,11 +1,14 @@
import { useQuery, useQueryClient } from "@tanstack/react-query"
import { Skeleton, Switch } from "antd"
import { useTranslation } from "react-i18next"
import {
getIsSimpleInternetSearch,
setIsSimpleInternetSearch
} from "~services/ollama"
} from "~/services/ollama"
export const SearchModeSettings = () => {
const { t } = useTranslation("settings")
const { data, status } = useQuery({
queryKey: ["fetchIsSimpleInternetSearch"],
queryFn: () => getIsSimpleInternetSearch()
@ -20,7 +23,7 @@ export const SearchModeSettings = () => {
return (
<div className="flex flex-row justify-between">
<span className="text-gray-500 dark:text-neutral-50 ">
Perform Simple Internet Search
{t("generalSettings.settings.searchMode.label")}
</span>
<Switch

View File

@ -1,13 +1,16 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { Form, Input, Skeleton, Table, Tooltip, message } from "antd"
import { Trash2 } from "lucide-react"
import { SaveButton } from "~components/Common/SaveButton"
import { deleteWebshare, getAllWebshares, getUserId } from "~libs/db"
import { getPageShareUrl, setPageShareUrl } from "~services/ollama"
import { verifyPageShareURL } from "~utils/verify-page-share"
import { Trans, useTranslation } from "react-i18next"
import { SaveButton } from "~/components/Common/SaveButton"
import { deleteWebshare, getAllWebshares, getUserId } from "~/libs/db"
import { getPageShareUrl, setPageShareUrl } from "~/services/ollama"
import { verifyPageShareURL } from "~/utils/verify-page-share"
export const OptionShareBody = () => {
const queryClient = useQueryClient()
const { t } = useTranslation(["settings"])
const { status, data } = useQuery({
queryKey: ["fetchShareInfo"],
queryFn: async () => {
@ -58,10 +61,10 @@ export const OptionShareBody = () => {
queryClient.invalidateQueries({
queryKey: ["fetchShareInfo"]
})
message.success("Page Share URL updated successfully")
message.success(t("manageShare.notification.pageShareSuccess"))
},
onError: (error) => {
message.error(error?.message || "Failed to update Page Share URL")
message.error(error?.message || t("manageShare.notification.someError"))
}
})
@ -71,10 +74,10 @@ export const OptionShareBody = () => {
queryClient.invalidateQueries({
queryKey: ["fetchShareInfo"]
})
message.success("Webshare deleted successfully")
message.success(t("manageShare.notification.webShareDeleteSuccess"))
},
onError: (error) => {
message.error(error?.message || "Failed to delete Webshare")
message.error(error?.message || t("manageShare.notification.someError"))
}
})
@ -86,7 +89,7 @@ export const OptionShareBody = () => {
<div>
<div>
<h2 className="text-base font-semibold leading-7 text-gray-900 dark:text-white">
Configure Page Share URL
{t("manageShare.heading")}
</h2>
<div className="border border-b border-gray-200 dark:border-gray-600 mt-3 mb-6"></div>
</div>
@ -99,25 +102,29 @@ export const OptionShareBody = () => {
<Form.Item
name="url"
help={
<span>
For privacy reasons, you can self-host the page share and
provide the URL here.{" "}
<Trans
i18nKey="settings:manageShare.form.url.help"
components={{
anchor: (
<a
href="https://github.com/n4ze3m/page-assist/blob/main/page-share.md"
target="__blank"
className="text-blue-600 dark:text-blue-400">
Learn more
</a>
</span>
className="text-blue-600 dark:text-blue-400"></a>
)
}}
/>
}
rules={[
{
required: true,
message: "Please input your Page Share URL!"
message: t("manageShare.form.url.required")
}
]}
label="Page Share URL">
<Input placeholder="Page Share URL" size="large" />
label={t("manageShare.form.url.label")}>
<Input
placeholder={t("manageShare.form.url.placeholder")}
size="large"
/>
</Form.Item>
<Form.Item>
<div className="flex justify-end">
@ -129,7 +136,7 @@ export const OptionShareBody = () => {
<div>
<div>
<h2 className="text-base font-semibold leading-7 text-gray-900 dark:text-white">
Webshares
{t("manageShare.webshare.heading")}
</h2>
<div className="border border-b border-gray-200 dark:border-gray-600 mt-3 mb-6"></div>
</div>
@ -138,12 +145,12 @@ export const OptionShareBody = () => {
dataSource={data.shares}
columns={[
{
title: "Title",
title: t("manageShare.webshare.columns.title"),
dataIndex: "title",
key: "title"
},
{
title: "URL",
title: t("manageShare.webshare.columns.url"),
dataIndex: "url",
key: "url",
render: (url: string) => (
@ -156,14 +163,14 @@ export const OptionShareBody = () => {
)
},
{
title: "Actions",
title: t("manageShare.webshare.columns.actions"),
render: (_, render) => (
<Tooltip title="Delete Share">
<Tooltip title={t("manageShare.webshare.tooltip.delete")}>
<button
onClick={() => {
if (
window.confirm(
"Are you sure you want to delete this webshare?"
t("manageShare.webshare.confirm.delete")
)
) {
deleteMutation({

View File

@ -5,12 +5,12 @@ import {
formatToMessage,
deleteByHistoryId,
updateHistory
} from "~libs/db"
} from "~/libs/db"
import { Empty, Skeleton } from "antd"
import { useMessageOption } from "~hooks/useMessageOption"
import { useState } from "react"
import { useMessageOption } from "~/hooks/useMessageOption"
import { PencilIcon, Trash2 } from "lucide-react"
import { useNavigate } from "react-router-dom"
import { useTranslation } from "react-i18next"
type Props = {
onClose: () => void
@ -19,6 +19,7 @@ type Props = {
export const Sidebar = ({ onClose }: Props) => {
const { setMessages, setHistory, setHistoryId, historyId, clearChat } =
useMessageOption()
const { t } = useTranslation(["option", "common"])
const client = useQueryClient()
const navigate = useNavigate()
@ -60,7 +61,7 @@ export const Sidebar = ({ onClose }: Props) => {
<div className="overflow-y-auto z-99">
{status === "success" && chatHistories.length === 0 && (
<div className="flex justify-center items-center mt-20 overflow-hidden">
<Empty description="No history yet" />
<Empty description={t("common:noHistory")} />
</div>
)}
{status === "pending" && (
@ -95,7 +96,7 @@ export const Sidebar = ({ onClose }: Props) => {
<div className="flex flex-row gap-3">
<button
onClick={() => {
const newTitle = prompt("Enter new title", chat.title)
const newTitle = prompt(t("editHistoryTitle"), chat.title)
if (newTitle) {
editHistory({ id: chat.id, title: newTitle })
@ -107,10 +108,7 @@ export const Sidebar = ({ onClose }: Props) => {
<button
onClick={() => {
if (
!confirm("Are you sure you want to delete this history?")
)
return
if (!confirm(t("deleteHistoryConfirmation"))) return
deleteHistory(chat.id)
}}
className="text-red-500 dark:text-red-400 opacity-80">

View File

@ -1,6 +1,6 @@
import React from "react"
import { PlaygroundMessage } from "~components/Common/Playground/Message"
import { useMessage } from "~hooks/useMessage"
import { PlaygroundMessage } from "~/components/Common/Playground/Message"
import { useMessage } from "~/hooks/useMessage"
import { EmptySidePanel } from "../Chat/empty"
export const SidePanelBody = () => {

View File

@ -2,16 +2,18 @@ import { useQuery } from "@tanstack/react-query"
import { Select } from "antd"
import { RotateCcw } from "lucide-react"
import { useEffect, useState } from "react"
import { useMessage } from "~hooks/useMessage"
import { useTranslation } from "react-i18next"
import { useMessage } from "~/hooks/useMessage"
import {
getAllModels,
getOllamaURL,
isOllamaRunning,
setOllamaURL as saveOllamaURL
} from "~services/ollama"
} from "~/services/ollama"
export const EmptySidePanel = () => {
const [ollamaURL, setOllamaURL] = useState<string>("")
const { t } = useTranslation(["playground", "common"])
const {
data: ollamaInfo,
status: ollamaStatus,
@ -38,7 +40,7 @@ export const EmptySidePanel = () => {
}
}, [ollamaInfo])
const { setSelectedModel, selectedModel, chatMode, setChatMode, } =
const { setSelectedModel, selectedModel, chatMode, setChatMode } =
useMessage()
return (
@ -48,7 +50,7 @@ export const EmptySidePanel = () => {
<div className="inline-flex items-center space-x-2">
<div className="w-3 h-3 bg-blue-500 rounded-full animate-bounce"></div>
<p className="dark:text-gray-400 text-gray-900">
Searching for your Ollama 🦙
{t("ollamaState.searching")}
</p>
</div>
)}
@ -57,7 +59,7 @@ export const EmptySidePanel = () => {
<div className="inline-flex items-center space-x-2">
<div className="w-3 h-3 bg-green-500 rounded-full"></div>
<p className="dark:text-gray-400 text-gray-900">
Ollama is running 🦙
{t("ollamaState.running")}
</p>
</div>
) : (
@ -65,7 +67,7 @@ export const EmptySidePanel = () => {
<div className="inline-flex space-x-2">
<div className="w-3 h-3 bg-red-500 rounded-full"></div>
<p className="dark:text-gray-400 text-gray-900">
We couldn't find your Ollama 🦙
{t("ollamaState.notRunning")}
</p>
</div>
@ -83,7 +85,7 @@ export const EmptySidePanel = () => {
}}
className="inline-flex mt-4 items-center rounded-md border border-transparent bg-black px-2 py-2 text-sm font-medium leading-4 text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:bg-white dark:text-gray-800 dark:hover:bg-gray-100 dark:focus:ring-gray-500 dark:focus:ring-offset-gray-100 disabled:opacity-50 ">
<RotateCcw className="h-4 w-4 mr-3" />
Retry
{t("common:retry")}
</button>
</div>
)
@ -91,8 +93,6 @@ export const EmptySidePanel = () => {
{ollamaStatus === "success" && ollamaInfo.isOk && (
<div className="mt-4">
<p className="dark:text-gray-400 text-gray-900">Models:</p>
<Select
onChange={(e) => {
setSelectedModel(e)
@ -104,7 +104,7 @@ export const EmptySidePanel = () => {
option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
showSearch
placeholder="Select a model"
placeholder={t("common:selectAModel")}
style={{ width: "100%" }}
className="mt-4"
options={ollamaInfo.models?.map((model) => ({
@ -145,7 +145,7 @@ export const EmptySidePanel = () => {
<label
className="mt-px font-light cursor-pointer select-none text-gray-900 dark:text-gray-400"
htmlFor="check">
Chat with Current Page
{t("common:chatWithCurrentPage")}
</label>
</div>
</div>

View File

@ -1,14 +1,15 @@
import { useForm } from "@mantine/form"
import { useMutation } from "@tanstack/react-query"
import React from "react"
import useDynamicTextareaSize from "~hooks/useDynamicTextareaSize"
import { useMessage } from "~hooks/useMessage"
import { toBase64 } from "~libs/to-base64"
import useDynamicTextareaSize from "~/hooks/useDynamicTextareaSize"
import { useMessage } from "~/hooks/useMessage"
import { toBase64 } from "~/libs/to-base64"
import { Checkbox, Dropdown, Image, Tooltip } from "antd"
import { useSpeechRecognition } from "~hooks/useSpeechRecognition"
import { useWebUI } from "~store/webui"
import { defaultEmbeddingModelForRag } from "~services/ollama"
import { useSpeechRecognition } from "~/hooks/useSpeechRecognition"
import { useWebUI } from "~/store/webui"
import { defaultEmbeddingModelForRag } from "~/services/ollama"
import { ImageIcon, MicIcon, X } from "lucide-react"
import { useTranslation } from "react-i18next"
type Props = {
dropedFile: File | undefined
@ -19,6 +20,7 @@ export const SidepanelForm = ({ dropedFile }: Props) => {
const inputRef = React.useRef<HTMLInputElement>(null)
const { sendWhenEnter, setSendWhenEnter } = useWebUI()
const [typing, setTyping] = React.useState<boolean>(false)
const { t } = useTranslation(["playground", "common"])
const textAreaFocus = () => {
if (textareaRef.current) {
@ -88,16 +90,13 @@ export const SidepanelForm = ({ dropedFile }: Props) => {
return
}
if (!selectedModel || selectedModel.length === 0) {
form.setFieldError("message", "Please select a model")
form.setFieldError("message", t("formError.noModel"))
return
}
if (chatMode === "rag") {
const defaultEM = await defaultEmbeddingModelForRag()
if (!defaultEM) {
form.setFieldError(
"message",
"Please set an embedding model on the settings page"
)
form.setFieldError("message", t("formError.noEmbeddingModel"))
return
}
}
@ -139,16 +138,13 @@ export const SidepanelForm = ({ dropedFile }: Props) => {
<form
onSubmit={form.onSubmit(async (value) => {
if (!selectedModel || selectedModel.length === 0) {
form.setFieldError("message", "Please select a model")
form.setFieldError("message", t("formError.noModel"))
return
}
if (chatMode === "rag") {
const defaultEM = await defaultEmbeddingModelForRag()
if (!defaultEM) {
form.setFieldError(
"message",
"Please set an embedding model on the settings page"
)
form.setFieldError("message", t("formError.noEmbeddingModel"))
return
}
}
@ -181,11 +177,11 @@ export const SidepanelForm = ({ dropedFile }: Props) => {
tabIndex={0}
onCompositionStart={() => setTyping(true)}
onCompositionEnd={() => setTyping(false)}
placeholder="Type a message..."
placeholder={t("form.textarea.placeholder")}
{...form.getInputProps("message")}
/>
<div className="flex mt-4 justify-end gap-3">
<Tooltip title="Voice Message">
<Tooltip title={t("tooltip.speechToText")}>
<button
type="button"
onClick={() => {
@ -209,7 +205,7 @@ export const SidepanelForm = ({ dropedFile }: Props) => {
)}
</button>
</Tooltip>
<Tooltip title="Upload Image">
<Tooltip title={t("tooltip.uploadImage")}>
<button
type="button"
onClick={() => {
@ -250,7 +246,7 @@ export const SidepanelForm = ({ dropedFile }: Props) => {
onChange={(e) =>
setSendWhenEnter(e.target.checked)
}>
Send when Enter pressed
{t("sendWhenEnter")}
</Checkbox>
)
}
@ -271,7 +267,7 @@ export const SidepanelForm = ({ dropedFile }: Props) => {
<path d="M20 4v7a4 4 0 01-4 4H4"></path>
</svg>
) : null}
Submit
{t("common:submit")}
</div>
</Dropdown.Button>
</div>

View File

@ -1,20 +1,27 @@
import logoImage from "data-base64:~assets/icon.png"
import { useMessage } from "~hooks/useMessage"
import logoImage from "~/assets/icon.png"
import { useMessage } from "~/hooks/useMessage"
import { Link } from "react-router-dom"
import { Tooltip } from "antd"
import { BoxesIcon, CogIcon, RefreshCcw } from "lucide-react"
import { useTranslation } from "react-i18next"
export const SidepanelHeader = () => {
const { clearChat, isEmbedding } = useMessage()
const { t } = useTranslation(["sidepanel", "common"])
return (
<div className="flex px-3 justify-between bg-white dark:bg-[#171717] border-b border-gray-300 dark:border-gray-700 py-4 items-center">
<div className="focus:outline-none focus-visible:ring-2 focus-visible:ring-pink-700 flex items-center dark:text-white">
<img className="h-6 w-auto" src={logoImage} alt="Page Assist" />
<span className="ml-1 text-sm ">Page Assist</span>
<img
className="h-6 w-auto"
src={logoImage}
alt={t("common:pageAssist")}
/>
<span className="ml-1 text-sm ">{t("common:pageAssist")}</span>
</div>
<div className="flex items-center space-x-3">
{isEmbedding ? (
<Tooltip title="It may take a few minutes to embed the page. Please wait...">
<Tooltip title={t("tooltip.embed")}>
<BoxesIcon className="h-5 w-5 text-gray-500 dark:text-gray-400 animate-bounce animate-infinite" />
</Tooltip>
) : null}

View File

@ -12,16 +12,19 @@ import {
defaultEmbeddingChunkSize,
defaultEmbeddingModelForRag,
saveForRag
} from "~services/ollama"
} from "~/services/ollama"
import { Skeleton, Radio, Select, Form, InputNumber } from "antd"
import { useDarkMode } from "~hooks/useDarkmode"
import { SaveButton } from "~components/Common/SaveButton"
import { SUPPORTED_LANGUAGES } from "~utils/supporetd-languages"
import { useMessage } from "~hooks/useMessage"
import { useDarkMode } from "~/hooks/useDarkmode"
import { SaveButton } from "~/components/Common/SaveButton"
import { SUPPORTED_LANGUAGES } from "~/utils/supporetd-languages"
import { useMessage } from "~/hooks/useMessage"
import { MoonIcon, SunIcon } from "lucide-react"
import { useTranslation } from "react-i18next"
import { useI18n } from "@/hooks/useI18n"
export const SettingsBody = () => {
const { t } = useTranslation("settings")
const [ollamaURL, setOllamaURL] = React.useState<string>("")
const [systemPrompt, setSystemPrompt] = React.useState<string>("")
const [ragPrompt, setRagPrompt] = React.useState<string>("")
@ -33,6 +36,8 @@ export const SettingsBody = () => {
const { speechToTextLanguage, setSpeechToTextLanguage } = useMessage()
const { mode, toggleDarkMode } = useDarkMode()
const { changeLocale, locale, supportLanguage } = useI18n()
const { data, status } = useQuery({
queryKey: ["sidebarSettings"],
queryFn: async () => {
@ -104,20 +109,26 @@ export const SettingsBody = () => {
return (
<div className="flex flex-col gap-4 p-4 max-w-2xl mx-auto lg:max-w-3xl xl:max-w-4xl 2xl:max-w-5xl">
<div className="border border-gray-300 dark:border-gray-700 rounded p-4 bg-white dark:bg-[#171717]">
<h2 className="text-md font-semibold dark:text-white">Prompt</h2>
<h2 className="text-md font-semibold dark:text-white">
{t("managePrompts.title")}
</h2>
<div className="my-3 flex justify-end">
<Radio.Group
defaultValue={selectedValue}
onChange={(e) => setSelectedValue(e.target.value)}>
<Radio.Button value="normal">Normal</Radio.Button>
<Radio.Button value="rag">Rag</Radio.Button>
<Radio.Button value="normal">
{t("managePrompts.option1")}
</Radio.Button>
<Radio.Button value="rag">
{t("managePrompts.option2")}
</Radio.Button>
</Radio.Group>
</div>
{selectedValue === "normal" && (
<div>
<span className="text-md font-thin text-gray-500 dark:text-gray-400">
System Prompt
{t("managePrompts.systemPrompt")}
</span>
<textarea
className="w-full border border-gray-300 dark:border-gray-700 rounded p-2 dark:bg-[#171717] dark:text-white dark:placeholder-gray-400"
@ -138,7 +149,7 @@ export const SettingsBody = () => {
<div>
<div className="mb-3">
<span className="text-md font-thin text-gray-500 dark:text-gray-400">
System Prompt
{t("managePrompts.systemPrompt")}
</span>
<textarea
className="w-full border border-gray-300 dark:border-gray-700 rounded p-2 dark:bg-[#171717] dark:text-white dark:placeholder-gray-400"
@ -148,7 +159,7 @@ export const SettingsBody = () => {
</div>
<div className="mb-3">
<span className="text-md font-thin text-gray-500 dark:text-gray-400">
Question Prompt
{t("managePrompts.questionPrompt")}
</span>
<textarea
className="w-full border border-gray-300 dark:border-gray-700 rounded p-2 dark:bg-[#171717] dark:text-white dark:placeholder-gray-400"
@ -170,14 +181,14 @@ export const SettingsBody = () => {
<div className="border border-gray-300 dark:border-gray-700 rounded p-4 bg-white dark:bg-[#171717]">
<h2 className="text-md mb-4 font-semibold dark:text-white">
Ollama URL
{t("ollamaSettings.heading")}
</h2>
<input
className="w-full border border-gray-300 dark:border-gray-700 rounded p-2 dark:bg-[#171717] dark:text-white dark:placeholder-gray-400"
value={ollamaURL}
type="url"
onChange={(e) => setOllamaURL(e.target.value)}
placeholder="Enter Ollama URL here"
placeholder={t("ollamaSettings.settings.ollamaUrl.placeholder")}
/>
<div className="flex justify-end">
<SaveButton
@ -190,7 +201,7 @@ export const SettingsBody = () => {
<div className="border border-gray-300 dark:border-gray-700 rounded p-4 bg-white dark:bg-[#171717]">
<h2 className="text-md mb-4 font-semibold dark:text-white">
RAG Configuration
{t("ollamaSettings.settings.ragSettings.label")}
</h2>
<Form
onFinish={(data) => {
@ -207,9 +218,14 @@ export const SettingsBody = () => {
}}>
<Form.Item
name="defaultEM"
label="Embedding Model"
help="Highly recommended to use embedding models like `nomic-embed-text`."
rules={[{ required: true, message: "Please select a model!" }]}>
label={t("ollamaSettings.settings.ragSettings.model.label")}
help={t("ollamaSettings.settings.ragSettings.model.help")}
rules={[
{
required: true,
message: t("ollamaSettings.settings.ragSettings.model.required")
}
]}>
<Select
size="large"
filterOption={(input, option) =>
@ -229,21 +245,38 @@ export const SettingsBody = () => {
<Form.Item
name="chunkSize"
label="Chunk Size"
label={t("ollamaSettings.settings.ragSettings.chunkSize.label")}
rules={[
{ required: true, message: "Please input your chunk size!" }
]}>
<InputNumber style={{ width: "100%" }} placeholder="Chunk Size" />
</Form.Item>
<Form.Item
name="chunkOverlap"
label="Chunk Overlap"
rules={[
{ required: true, message: "Please input your chunk overlap!" }
{
required: true,
message: t(
"ollamaSettings.settings.ragSettings.chunkSize.required"
)
}
]}>
<InputNumber
style={{ width: "100%" }}
placeholder="Chunk Overlap"
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>
@ -254,10 +287,33 @@ export const SettingsBody = () => {
</div>
<div className="border border-gray-300 dark:border-gray-700 rounded p-4 bg-white dark:bg-[#171717]">
<h2 className="text-md mb-4 font-semibold dark:text-white">
Speech Recognition Language
{t("generalSettings.settings.language.label")}{" "}
</h2>
<Select
placeholder="Select Language"
placeholder={t("generalSettings.settings.language.placeholder")}
showSearch
options={supportLanguage}
value={locale}
filterOption={(input, option) =>
option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 ||
option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
onChange={(value) => {
changeLocale(value)
}}
style={{
width: "100%"
}}
/>
</div>
<div className="border border-gray-300 dark:border-gray-700 rounded p-4 bg-white dark:bg-[#171717]">
<h2 className="text-md mb-4 font-semibold dark:text-white">
{t("generalSettings.settings.speechRecognitionLang.label")}{" "}
</h2>
<Select
placeholder={t(
"generalSettings.settings.speechRecognitionLang.placeholder"
)}
allowClear
showSearch
options={SUPPORTED_LANGUAGES}
@ -275,20 +331,22 @@ export const SettingsBody = () => {
/>
</div>
<div className="border border-gray-300 dark:border-gray-700 rounded p-4 bg-white dark:bg-[#171717]">
<h2 className="text-md mb-4 font-semibold dark:text-white">Theme</h2>
<h2 className="text-md mb-4 font-semibold dark:text-white">
{t("generalSettings.settings.darkMode.label")}{" "}
</h2>
{mode === "dark" ? (
<button
onClick={toggleDarkMode}
className="select-none inline-flex w-full rounded-lg border border-gray-900 py-3 px-6 text-center align-middle font-sans text-xs font-bold uppercase text-gray-900 transition-all hover:opacity-75 focus:ring focus:ring-gray-300 active:opacity-[0.85] disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none dark:border-gray-100 dark:text-white dark:hover:opacity-75 dark:focus:ring-dark dark:active:opacity-75 dark:disabled:pointer-events-none dark:disabled:opacity-50 dark:disabled:shadow-none">
className="select-none inline-flex text-center w-full rounded-lg border border-gray-900 py-3 px-6 justify-center font-sans text-xs font-bold uppercase text-gray-900 transition-all hover:opacity-75 focus:ring focus:ring-gray-300 active:opacity-[0.85] disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none dark:border-gray-100 dark:text-white dark:hover:opacity-75 dark:focus:ring-dark dark:active:opacity-75 dark:disabled:pointer-events-none dark:disabled:opacity-50 dark:disabled:shadow-none">
<SunIcon className="h-4 w-4 mr-2" />
Light
{t("generalSettings.settings.darkMode.options.light")}
</button>
) : (
<button
onClick={toggleDarkMode}
className="select-none inline-flex w-full rounded-lg border border-gray-900 py-3 px-6 text-center align-middle font-sans text-xs font-bold uppercase text-gray-900 transition-all hover:opacity-75 focus:ring focus:ring-gray-300 active:opacity-[0.85] disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none dark:border-gray-100 dark:text-white dark:hover:opacity-75 dark:focus:ring-dark dark:active:opacity-75 dark:disabled:pointer-events-none dark:disabled:opacity-50 dark:disabled:shadow-none">
className="select-none inline-flex text-center w-full rounded-lg border border-gray-900 py-3 px-6 justify-center font-sans text-xs font-bold uppercase text-gray-900 transition-all hover:opacity-75 focus:ring focus:ring-gray-300 active:opacity-[0.85] disabled:pointer-events-none disabled:opacity-50 disabled:shadow-none dark:border-gray-100 dark:text-white dark:hover:opacity-75 dark:focus:ring-dark dark:active:opacity-75 dark:disabled:pointer-events-none dark:disabled:opacity-50 dark:disabled:shadow-none">
<MoonIcon className="h-4 w-4 mr-2" />
Dark
{t("generalSettings.settings.darkMode.options.dark")}
</button>
)}
</div>

View File

@ -1,15 +1,18 @@
import logoImage from "data-base64:~assets/icon.png"
import { ChevronLeft } from "lucide-react"
import { useTranslation } from "react-i18next"
import { Link } from "react-router-dom"
import logoImage from "~/assets/icon.png"
export const SidepanelSettingsHeader = () => {
const { t } = useTranslation("common")
return (
<div className="flex px-3 justify-start gap-3 bg-white dark:bg-[#171717] border-b border-gray-300 dark:border-gray-700 py-4 items-center">
<Link to="/">
<ChevronLeft className="h-5 w-5 text-gray-500 dark:text-gray-400" />
</Link>
<div className="focus:outline-none focus-visible:ring-2 focus-visible:ring-pink-700 flex items-center dark:text-white">
<img className="h-6 w-auto" src={logoImage} alt="Page Assist" />
<span className="ml-1 text-sm ">Page Assist</span>
<img className="h-6 w-auto" src={logoImage} alt={t("pageAssist")} />
<span className="ml-1 text-sm ">{t("pageAssist")}</span>
</div>
</div>
)

View File

@ -1,54 +0,0 @@
import type { PlasmoCSConfig } from "plasmo"
export const config: PlasmoCSConfig = {
matches: ["*://ollama.com/library/*"],
all_frames: true
}
const downloadModel = async (modelName: string) => {
const ok = confirm(
`[Page Assist Extension] Do you want to pull ${modelName} model? This has nothing to do with Ollama.com website. The model will be pulled locally once you confirm.`
)
if (ok) {
alert(
`[Page Assist Extension] Pulling ${modelName} model. For more details, check the extension icon.`
)
await chrome.runtime.sendMessage({
type: "pull_model",
modelName
})
return true
}
return false
}
const downloadSVG = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="h-5 w-5 pageasssist-icon">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
</svg>
`
const codeDiv = document.querySelectorAll("div.language-none")
for (let i = 0; i < codeDiv.length; i++) {
const button = codeDiv[i].querySelector("button")
const command = codeDiv[i].querySelector("input")
if (button && command) {
const newButton = document.createElement("button")
newButton.innerHTML = downloadSVG
newButton.className = `border-l ${button.className}`
newButton.id = `download-${i}-pageassist`
const modelName = command?.value
.replace("ollama run", "")
.replace("ollama pull", "")
.trim()
newButton.addEventListener("click", () => {
downloadModel(modelName)
})
const span = document.createElement("span")
span.title = "Download model via Page Assist"
span.appendChild(newButton)
button.parentNode.appendChild(span)
}
}

143
src/entries/background.ts Normal file
View File

@ -0,0 +1,143 @@
import { getOllamaURL, isOllamaRunning } from "../services/ollama"
const progressHuman = (completed: number, total: number) => {
return ((completed / total) * 100).toFixed(0) + "%"
}
const clearBadge = () => {
chrome.action.setBadgeText({ text: "" })
chrome.action.setTitle({ title: "" })
}
const streamDownload = async (url: string, model: string) => {
url += "/api/pull"
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ model, stream: true })
})
const reader = response.body?.getReader()
const decoder = new TextDecoder()
let isSuccess = true
while (true) {
if (!reader) {
break
}
const { done, value } = await reader.read()
if (done) {
break
}
const text = decoder.decode(value)
try {
const json = JSON.parse(text.trim()) as {
status: string
total?: number
completed?: number
}
if (json.total && json.completed) {
chrome.action.setBadgeText({
text: progressHuman(json.completed, json.total)
})
chrome.action.setBadgeBackgroundColor({ color: "#0000FF" })
} else {
chrome.action.setBadgeText({ text: "🏋️‍♂️" })
chrome.action.setBadgeBackgroundColor({ color: "#FFFFFF" })
}
chrome.action.setTitle({ title: json.status })
if (json.status === "success") {
isSuccess = true
}
} catch (e) {
console.error(e)
}
}
if (isSuccess) {
chrome.action.setBadgeText({ text: "✅" })
chrome.action.setBadgeBackgroundColor({ color: "#00FF00" })
chrome.action.setTitle({ title: "Model pulled successfully" })
} else {
chrome.action.setBadgeText({ text: "❌" })
chrome.action.setBadgeBackgroundColor({ color: "#FF0000" })
chrome.action.setTitle({ title: "Model pull failed" })
}
setTimeout(() => {
clearBadge()
}, 5000)
}
export default defineBackground({
main() {
chrome.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!
})
})
} 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"
})
setTimeout(() => {
clearBadge()
}, 5000)
}
await streamDownload(ollamaURL, message.modelName)
}
})
chrome.action.onClicked.addListener((tab) => {
chrome.tabs.create({ url: chrome.runtime.getURL("options.html") })
})
chrome.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.create({
id: "open-side-panel-pa",
title: browser.i18n.getMessage("openSidePanelToChat"),
contexts: ["all"]
})
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]
await chrome.sidePanel.open({
tabId: tab.id!
})
})
}
})
},
persistent: true
})

View File

@ -0,0 +1,56 @@
export default defineContentScript({
main(ctx) {
const downloadModel = async (modelName: string) => {
const ok = confirm(
`[Page Assist Extension] Do you want to pull ${modelName} model? This has nothing to do with Ollama.com website. The model will be pulled locally once you confirm.`
)
if (ok) {
alert(
`[Page Assist Extension] Pulling ${modelName} model. For more details, check the extension icon.`
)
await chrome.runtime.sendMessage({
type: "pull_model",
modelName
})
return true
}
return false
}
const downloadSVG = `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="h-5 w-5 pageasssist-icon">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
</svg>
`
const codeDiv = document.querySelectorAll("div.language-none")
for (let i = 0; i < codeDiv.length; i++) {
const button = codeDiv[i].querySelector("button")
const command = codeDiv[i].querySelector("input")
if (button && command) {
const newButton = document.createElement("button")
newButton.innerHTML = downloadSVG
newButton.className = `border-l ${button.className}`
newButton.id = `download-${i}-pageassist`
const modelName = command?.value
.replace("ollama run", "")
.replace("ollama pull", "")
.trim()
newButton.addEventListener("click", () => {
downloadModel(modelName)
})
const span = document.createElement("span")
span.title = "Download model via Page Assist"
span.appendChild(newButton)
if (button.parentNode) {
button.parentNode.appendChild(span)
}
}
}
},
allFrames: true,
matches: ["*://ollama.com/library/*"]
})

View File

@ -3,20 +3,32 @@ import { MemoryRouter } from "react-router-dom"
import { ToastContainer } from "react-toastify"
import "react-toastify/dist/ReactToastify.css"
const queryClient = new QueryClient()
import "./css/tailwind.css"
import { ConfigProvider, theme } from "antd"
import { ConfigProvider, Empty, theme } from "antd"
import { StyleProvider } from "@ant-design/cssinjs"
import { useDarkMode } from "~hooks/useDarkmode"
import { OptionRouting } from "~routes"
import { useDarkMode } from "~/hooks/useDarkmode"
import { OptionRouting } from "~/routes"
import "~/i18n"
import { useTranslation } from "react-i18next"
function IndexOption() {
const { mode } = useDarkMode()
const { t } = useTranslation()
return (
<MemoryRouter>
<ConfigProvider
theme={{
algorithm:
mode === "dark" ? theme.darkAlgorithm : theme.defaultAlgorithm
}}>
}}
renderEmpty={() => (
<Empty
imageStyle={{
height: 60
}}
description={t("common:noData")}
/>
)}
>
<StyleProvider hashPriority="high">
<QueryClientProvider client={queryClient}>
<OptionRouting />

View File

@ -0,0 +1,14 @@
<!doctype html>
<html>
<head>
<title>Page Assist - A Web UI for Local AI Models</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="manifest.type" content="browser_action" />
<link href="~/assets/tailwind.css" rel="stylesheet" />
<meta charset="utf-8" />
</head>
<body class="bg-white dark:bg-[#171717]">
<div id="root"></div>
<script type="module" src="./main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,10 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import IndexOption from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<IndexOption />
</React.StrictMode>,
);

View File

@ -1,15 +1,18 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { MemoryRouter } from "react-router-dom"
import { SidepanelRouting } from "~routes"
import { SidepanelRouting } from "~/routes"
import { ToastContainer } from "react-toastify"
import "react-toastify/dist/ReactToastify.css"
const queryClient = new QueryClient()
import "./css/tailwind.css"
import { ConfigProvider, theme } from "antd"
import { ConfigProvider, Empty, theme } from "antd"
import { StyleProvider } from "@ant-design/cssinjs"
import { useDarkMode } from "~hooks/useDarkmode"
import { useDarkMode } from "~/hooks/useDarkmode"
import "~/i18n"
import { useTranslation } from "react-i18next"
function IndexSidepanel() {
const { mode } = useDarkMode()
const { t } = useTranslation()
return (
<MemoryRouter>
@ -17,7 +20,16 @@ function IndexSidepanel() {
theme={{
algorithm:
mode === "dark" ? theme.darkAlgorithm : theme.defaultAlgorithm
}}>
}}
renderEmpty={() => (
<Empty
imageStyle={{
height: 60
}}
description={t("common:noData")}
/>
)}
>
<StyleProvider hashPriority="high">
<QueryClientProvider client={queryClient}>
<SidepanelRouting />

View File

@ -0,0 +1,14 @@
<!doctype html>
<html>
<head>
<title>Page Assist - A Web UI for Local AI Models</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="manifest.type" content="browser_action" />
<link href="~/assets/tailwind.css" rel="stylesheet" />
<meta charset="utf-8" />
</head>
<body class="bg-white dark:bg-[#171717]">
<div id="root"></div>
<script type="module" src="./main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from "react"
import ReactDOM from "react-dom/client"
import IndexSidepanel from "./App"
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<IndexSidepanel />
</React.StrictMode>
)

17
src/hooks/useI18n.tsx Normal file
View File

@ -0,0 +1,17 @@
import { supportLanguage } from "@/i18n/support-language"
import { useState } from "react"
import { useTranslation } from "react-i18next"
export const useI18n = () => {
const { i18n } = useTranslation()
const [locale, setLocale] = useState<string>(
localStorage.getItem("i18nextLng") || "en"
)
const changeLocale = (lang: string) => {
setLocale(lang)
i18n.changeLanguage(lang)
}
return { locale, changeLocale, supportLanguage }
}

View File

@ -1,89 +1,22 @@
import React from "react"
import { cleanUrl } from "~libs/clean-url"
import { cleanUrl } from "~/libs/clean-url"
import {
defaultEmbeddingChunkOverlap,
defaultEmbeddingChunkSize,
defaultEmbeddingModelForRag,
getOllamaURL,
promptForRag,
systemPromptForNonRag
} from "~services/ollama"
import { useStoreMessage, type ChatHistory, type Message } from "~store"
} from "~/services/ollama"
import { useStoreMessage, type Message } from "~/store"
import { ChatOllama } from "@langchain/community/chat_models/ollama"
import {
HumanMessage,
AIMessage,
type MessageContent,
SystemMessage
} from "@langchain/core/messages"
import { getHtmlOfCurrentTab } from "~libs/get-html"
import { PageAssistHtmlLoader } from "~loader/html"
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter"
import { HumanMessage, SystemMessage } from "@langchain/core/messages"
import { getDataFromCurrentTab } from "~/libs/get-html"
import { OllamaEmbeddings } from "@langchain/community/embeddings/ollama"
import {
createChatWithWebsiteChain,
groupMessagesByConversation
} from "~chain/chat-with-website"
} from "~/chain/chat-with-website"
import { MemoryVectorStore } from "langchain/vectorstores/memory"
import { chromeRunTime } from "~libs/runtime"
export type BotResponse = {
bot: {
text: string
sourceDocuments: any[]
}
history: ChatHistory
history_id: string
}
const generateHistory = (
messages: {
role: "user" | "assistant" | "system"
content: string
image?: string
}[]
) => {
let history = []
for (const message of messages) {
if (message.role === "user") {
let content: MessageContent = [
{
type: "text",
text: message.content
}
]
if (message.image) {
content = [
{
type: "image_url",
image_url: message.image
},
{
type: "text",
text: message.content
}
]
}
history.push(
new HumanMessage({
content: content
})
)
} else if (message.role === "assistant") {
history.push(
new AIMessage({
content: [
{
type: "text",
text: message.content
}
]
})
)
}
}
return history
}
import { memoryEmbedding } from "@/utils/memory-embeddings"
export const useMessage = () => {
const {
@ -129,47 +62,18 @@ export const useMessage = () => {
setStreaming(false)
}
const memoryEmbedding = async (
url: string,
html: string,
ollamaEmbedding: OllamaEmbeddings
) => {
const loader = new PageAssistHtmlLoader({
html,
url
})
const docs = await loader.load()
const chunkSize = await defaultEmbeddingChunkSize()
const chunkOverlap = await defaultEmbeddingChunkOverlap()
const textSplitter = new RecursiveCharacterTextSplitter({
chunkSize,
chunkOverlap
})
const chunks = await textSplitter.splitDocuments(docs)
const store = new MemoryVectorStore(ollamaEmbedding)
setIsEmbedding(true)
await store.addDocuments(chunks)
setKeepTrackOfEmbedding({
...keepTrackOfEmbedding,
[url]: store
})
setIsEmbedding(false)
return store
}
const chatWithWebsiteMode = async (message: string) => {
try {
let isAlreadyExistEmbedding: MemoryVectorStore
let embedURL: string, embedHTML: string
let embedURL: string, embedHTML: string, embedType: string
let embedPDF: { content: string; page: number }[] = []
if (messages.length === 0) {
const { html, url } = await getHtmlOfCurrentTab()
const { content: html, url, type, pdf } = await getDataFromCurrentTab()
embedHTML = html
embedURL = url
embedType = type
embedPDF = pdf
setCurrentURL(url)
isAlreadyExistEmbedding = keepTrackOfEmbedding[currentURL]
} else {
@ -212,11 +116,16 @@ export const useMessage = () => {
if (isAlreadyExistEmbedding) {
vectorstore = isAlreadyExistEmbedding
} else {
vectorstore = await memoryEmbedding(
embedURL,
embedHTML,
ollamaEmbedding
)
vectorstore = await memoryEmbedding({
html: embedHTML,
keepTrackOfEmbedding: keepTrackOfEmbedding,
ollamaEmbedding: ollamaEmbedding,
pdf: embedPDF,
setIsEmbedding: setIsEmbedding,
setKeepTrackOfEmbedding: setKeepTrackOfEmbedding,
type: embedType,
url: embedURL
})
}
const { ragPrompt: systemPrompt, ragQuestionPrompt: questionPrompt } =

View File

@ -1,19 +1,14 @@
import React from "react"
import { cleanUrl } from "~libs/clean-url"
import { cleanUrl } from "~/libs/clean-url"
import {
geWebSearchFollowUpPrompt,
getOllamaURL,
systemPromptForNonRagOption
} from "~services/ollama"
import { type ChatHistory, type Message } from "~store/option"
} from "~/services/ollama"
import { type ChatHistory, type Message } from "~/store/option"
import { ChatOllama } from "@langchain/community/chat_models/ollama"
import {
HumanMessage,
AIMessage,
type MessageContent,
SystemMessage
} from "@langchain/core/messages"
import { useStoreMessageOption } from "~store/option"
import { HumanMessage, SystemMessage } from "@langchain/core/messages"
import { useStoreMessageOption } from "~/store/option"
import {
deleteChatForEdit,
getPromptById,
@ -21,69 +16,12 @@ import {
saveHistory,
saveMessage,
updateMessageByIndex
} from "~libs/db"
} from "~/libs/db"
import { useNavigate } from "react-router-dom"
import { notification } from "antd"
import { getSystemPromptForWeb } from "~web/web"
export type BotResponse = {
bot: {
text: string
sourceDocuments: any[]
}
history: ChatHistory
history_id: string
}
const generateHistory = (
messages: {
role: "user" | "assistant" | "system"
content: string
image?: string
}[]
) => {
let history = []
for (const message of messages) {
if (message.role === "user") {
let content: MessageContent = [
{
type: "text",
text: message.content
}
]
if (message.image) {
content = [
{
type: "image_url",
image_url: message.image
},
{
type: "text",
text: message.content
}
]
}
history.push(
new HumanMessage({
content: content
})
)
} else if (message.role === "assistant") {
history.push(
new AIMessage({
content: [
{
type: "text",
text: message.content
}
]
})
)
}
}
return history
}
import { getSystemPromptForWeb } from "~/web/web"
import { generateHistory } from "@/utils/generate-history"
import { useTranslation } from "react-i18next"
export const useMessageOption = () => {
const {
@ -116,7 +54,7 @@ export const useMessageOption = () => {
setSelectedSystemPrompt
} = useStoreMessageOption()
// const { notification } = App.useApp()
const { t } = useTranslation("option")
const navigate = useNavigate()
const textareaRef = React.useRef<HTMLTextAreaElement>(null)
@ -150,7 +88,7 @@ export const useMessageOption = () => {
abortControllerRef.current = new AbortController()
const ollama = new ChatOllama({
model: selectedModel,
model: selectedModel!,
baseUrl: cleanUrl(url)
})
@ -204,7 +142,7 @@ export const useMessageOption = () => {
.replaceAll("{chat_history}", chat_history)
.replaceAll("{question}", message)
const questionOllama = new ChatOllama({
model: selectedModel,
model: selectedModel!,
baseUrl: cleanUrl(url)
})
const response = await questionOllama.invoke(promptForQuestion)
@ -308,11 +246,11 @@ export const useMessageOption = () => {
if (historyId) {
if (!isRegenerate) {
await saveMessage(historyId, selectedModel, "user", message, [image])
await saveMessage(historyId, selectedModel!, "user", message, [image])
}
await saveMessage(
historyId,
selectedModel,
selectedModel!,
"assistant",
newMessage[appendingIndex].message,
[],
@ -320,12 +258,12 @@ export const useMessageOption = () => {
)
} else {
const newHistoryId = await saveHistory(message)
await saveMessage(newHistoryId.id, selectedModel, "user", message, [
await saveMessage(newHistoryId.id, selectedModel!, "user", message, [
image
])
await saveMessage(
newHistoryId.id,
selectedModel,
selectedModel!,
"assistant",
newMessage[appendingIndex].message,
[],
@ -337,6 +275,7 @@ export const useMessageOption = () => {
setIsProcessing(false)
setStreaming(false)
} catch (e) {
//@ts-ignore
if (e?.name === "AbortError") {
newMessage[appendingIndex].message = newMessage[
appendingIndex
@ -356,22 +295,22 @@ export const useMessageOption = () => {
])
if (historyId) {
await saveMessage(historyId, selectedModel, "user", message, [image])
await saveMessage(historyId, selectedModel!, "user", message, [image])
await saveMessage(
historyId,
selectedModel,
selectedModel!,
"assistant",
newMessage[appendingIndex].message,
[]
)
} else {
const newHistoryId = await saveHistory(message)
await saveMessage(newHistoryId.id, selectedModel, "user", message, [
await saveMessage(newHistoryId.id, selectedModel!, "user", message, [
image
])
await saveMessage(
newHistoryId.id,
selectedModel,
selectedModel!,
"assistant",
newMessage[appendingIndex].message,
[]
@ -379,9 +318,10 @@ export const useMessageOption = () => {
setHistoryId(newHistoryId.id)
}
} else {
//@ts-ignore
notification.error({
message: "Error",
description: e?.message || "Something went wrong"
message: t("error"),
description: e?.message || t("somethingWentWrong")
})
}
@ -405,7 +345,7 @@ export const useMessageOption = () => {
abortControllerRef.current = new AbortController()
const ollama = new ChatOllama({
model: selectedModel,
model: selectedModel!,
baseUrl: cleanUrl(url)
})
@ -620,8 +560,8 @@ export const useMessageOption = () => {
}
} else {
notification.error({
message: "Error",
description: e?.message || "Something went wrong"
message: t("error"),
description: e?.message || t("somethingWentWrong")
})
}
@ -699,8 +639,8 @@ export const useMessageOption = () => {
const validateBeforeSubmit = () => {
if (!selectedModel || selectedModel?.trim()?.length === 0) {
notification.error({
message: "Error",
description: "Please select a model to continue"
message: t("error"),
description: t("validationSelectModel")
})
return false
}

17
src/i18n/index.ts Normal file
View File

@ -0,0 +1,17 @@
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import { en } from "./lang/en";
import { ml } from "./lang/ml";
i18n
.use(initReactI18next)
.init({
resources: {
en: en,
ml: ml
},
fallbackLng: "en",
lng: localStorage.getItem("i18nextLng") || "en",
})
export default i18n;

14
src/i18n/lang/en.ts Normal file
View File

@ -0,0 +1,14 @@
import option from "@/assets/locale/en/option.json";
import playground from "@/assets/locale/en/playground.json";
import common from "@/assets/locale/en/common.json";
import sidepanel from "@/assets/locale/en/sidepanel.json";
import settings from "@/assets/locale/en/settings.json";
export const en = {
option,
playground,
common,
sidepanel,
settings
}

14
src/i18n/lang/ml.ts Normal file
View File

@ -0,0 +1,14 @@
import option from "@/assets/locale/ml/option.json";
import playground from "@/assets/locale/ml/playground.json";
import common from "@/assets/locale/ml/common.json";
import sidepanel from "@/assets/locale/ml/sidepanel.json";
import settings from "@/assets/locale/ml/settings.json";
export const ml = {
option,
playground,
common,
sidepanel,
settings
}

View File

@ -0,0 +1,11 @@
// Please add new language code to supportLanguage array
export const supportLanguage = [
{
label: "English",
value: "en"
},
{
label: "മലയാളം",
value: "ml"
}
]

View File

@ -1,7 +1,7 @@
import {
type ChatHistory as ChatHistoryType,
type Message as MessageType
} from "~store/option"
} from "~/store/option"
type HistoryInfo = {
id: string

View File

@ -1,15 +1,44 @@
import { pdfDist } from "./pdfjs"
const _getHtml = () => {
export const getPdf = async (data: ArrayBuffer) => {
const pdf = pdfDist.getDocument({
data,
useWorkerFetch: false,
isEvalSupported: false,
useSystemFonts: true,
});
pdf.onPassword = (callback: any) => {
const password = prompt("Enter the password: ")
if (!password) {
throw new Error("Password required to open the PDF.");
}
callback(password);
};
const pdfDocument = await pdf.promise;
return pdfDocument
}
const _getHtml = async () => {
const url = window.location.href
if (document.contentType === "application/pdf") {
return { url, content: "", type: "pdf" }
}
const html = Array.from(document.querySelectorAll("script")).reduce(
(acc, script) => {
return acc.replace(script.outerHTML, "")
},
document.documentElement.outerHTML
)
return { url, html }
return { url, content: html, type: "html" }
}
export const getHtmlOfCurrentTab = async () => {
export const getDataFromCurrentTab = async () => {
const result = new Promise((resolve) => {
chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => {
const tab = tabs[0]
@ -25,9 +54,48 @@ export const getHtmlOfCurrentTab = async () => {
})
}) as Promise<{
url: string
html: string
content: string
type: string
}>
return result
const { content, type, url } = await result
if (type === "pdf") {
const res = await fetch(url)
const data = await res.arrayBuffer()
let pdfHtml: {
content: string
page: number
}[] = []
const pdf = await getPdf(data)
for (let i = 1; i <= pdf.numPages; i += 1) {
const page = await pdf.getPage(i);
const content = await page.getTextContent();
if (content?.items.length === 0) {
continue;
}
const text = content?.items.map((item: any) => item.str).join("\n")
.replace(/\x00/g, "").trim();
pdfHtml.push({
content: text,
page: i
})
}
return {
url,
content: "",
pdf: pdfHtml,
type: "pdf"
}
}
return { url, content, type, pdf: [] }
}

8
src/libs/pdfjs.ts Normal file
View File

@ -0,0 +1,8 @@
import * as pdfDist from "pdfjs-dist"
import * as pdfWorker from "pdfjs-dist/build/pdf.worker.mjs";
pdfDist.GlobalWorkerOptions.workerSrc = pdfWorker
export {
pdfDist
}

View File

@ -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 { chromeRunTime } from "~/libs/runtime"
import { YtTranscript } from "yt-transcript"
const YT_REGEX =

37
src/loader/pdf.ts Normal file
View File

@ -0,0 +1,37 @@
import { BaseDocumentLoader } from "langchain/document_loaders/base"
import { Document } from "@langchain/core/documents"
export interface WebLoaderParams {
pdf: { content: string, page: number }[]
url: string
}
export class PageAssistPDFLoader
extends BaseDocumentLoader
implements WebLoaderParams {
pdf: { content: string, page: number }[]
url: string
constructor({ pdf, url }: WebLoaderParams) {
super()
this.pdf = pdf
this.url = url
}
async load(): Promise<Document<Record<string, any>>[]> {
const documents: Document[] = [];
for (const page of this.pdf) {
const metadata = { source: this.url, page: page.page }
documents.push(new Document({ pageContent: page.content, metadata }))
}
return [
new Document({
pageContent: documents.map((doc) => doc.pageContent).join("\n\n"),
metadata: documents.map((doc) => doc.metadata),
}),
];
}
}

View File

@ -1,8 +0,0 @@
<!doctype html>
<html>
<head>
<title>__plasmo_static_index_title__</title>
<meta charset="utf-8" />
</head>
<body class="bg-white dark:bg-[#171717]"></body>
</html>

View File

@ -0,0 +1,11 @@
{
"extName": {
"message": "Page Assist - A Web UI for Local AI Models"
},
"extDescription": {
"message": "Use your locally running AI models to assist you in your web browsing."
},
"openSidePanelToChat": {
"message": "Open Side Panel to Chat"
}
}

View File

@ -0,0 +1,11 @@
{
"extName": {
"message": "പേജ് അസിസ്റ്റ് - ഒള്ളമ മോഡലുകളിക്ക് ഒരു വെബ്യൂഐ"
},
"extDescription": {
"message": "ഇന്റർനെറ്റ് ബ്രൌസ് ചെയ്യുമ്പോൾ സ്ഥലിപ്പായി പ്രവർത്തിക്കുന്ന എയ്‌ മോഡൽ ഉപയോഗിക്കുക."
},
"openSidePanelToChat": {
"message": "ചാറ്റ് ചെയ്യാന്‍ സൈഡ് പാനല്‍ തുറക്കുക"
}
}

BIN
src/public/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
src/public/icon/128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
src/public/icon/16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 B

BIN
src/public/icon/32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 535 B

BIN
src/public/icon/48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 951 B

BIN
src/public/icon/64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1,6 +1,6 @@
import { Route, Routes } from "react-router-dom"
import { SidepanelChat } from "./sidepanel-chat"
import { useDarkMode } from "~hooks/useDarkmode"
import { useDarkMode } from "~/hooks/useDarkmode"
import { SidepanelSettings } from "./sidepanel-settings"
import { OptionIndex } from "./option-index"
import { OptionModal } from "./option-settings-model"

View File

@ -1,5 +1,5 @@
import OptionLayout from "~components/Layouts/Layout"
import { Playground } from "~components/Option/Playground/Playground"
import OptionLayout from "~/components/Layouts/Layout"
import { Playground } from "~/components/Option/Playground/Playground"
export const OptionIndex = () => {
return (

View File

@ -1,6 +1,6 @@
import { SettingsLayout } from "~components/Layouts/SettingsOptionLayout"
import OptionLayout from "~components/Layouts/Layout"
import { ModelsBody } from "~components/Option/Models"
import { SettingsLayout } from "~/components/Layouts/SettingsOptionLayout"
import OptionLayout from "~/components/Layouts/Layout"
import { ModelsBody } from "~/components/Option/Models"
export const OptionModal = () => {
return (

View File

@ -1,6 +1,6 @@
import { SettingsLayout } from "~components/Layouts/SettingsOptionLayout"
import OptionLayout from "~components/Layouts/Layout"
import { PromptBody } from "~components/Option/Prompt"
import { SettingsLayout } from "~/components/Layouts/SettingsOptionLayout"
import OptionLayout from "~/components/Layouts/Layout"
import { PromptBody } from "~/components/Option/Prompt"
export const OptionPrompt = () => {
return (

View File

@ -1,6 +1,6 @@
import { SettingsLayout } from "~components/Layouts/SettingsOptionLayout"
import OptionLayout from "~components/Layouts/Layout"
import { OptionShareBody } from "~components/Option/Share"
import { SettingsLayout } from "~/components/Layouts/SettingsOptionLayout"
import OptionLayout from "~/components/Layouts/Layout"
import { OptionShareBody } from "~/components/Option/Share"
export const OptionShare = () => {
return (

View File

@ -1,6 +1,6 @@
import { SettingsLayout } from "~components/Layouts/SettingsOptionLayout"
import OptionLayout from "~components/Layouts/Layout"
import { SettingOther } from "~components/Option/Settings/other"
import { SettingsLayout } from "~/components/Layouts/SettingsOptionLayout"
import OptionLayout from "~/components/Layouts/Layout"
import { SettingOther } from "~/components/Option/Settings/other"
export const OptionSettings = () => {
return (

View File

@ -1,6 +1,6 @@
import { SettingsLayout } from "~components/Layouts/SettingsOptionLayout"
import OptionLayout from "~components/Layouts/Layout"
import { SettingsOllama } from "~components/Option/Settings/ollama"
import { SettingsLayout } from "~/components/Layouts/SettingsOptionLayout"
import OptionLayout from "~/components/Layouts/Layout"
import { SettingsOllama } from "~/components/Option/Settings/ollama"
export const OptionOllamaSettings = () => {
return (

View File

@ -1,8 +1,8 @@
import React from "react"
import { SidePanelBody } from "~components/Sidepanel/Chat/body"
import { SidepanelForm } from "~components/Sidepanel/Chat/form"
import { SidepanelHeader } from "~components/Sidepanel/Chat/header"
import { useMessage } from "~hooks/useMessage"
import { SidePanelBody } from "~/components/Sidepanel/Chat/body"
import { SidepanelForm } from "~/components/Sidepanel/Chat/form"
import { SidepanelHeader } from "~/components/Sidepanel/Chat/header"
import { useMessage } from "~/hooks/useMessage"
export const SidepanelChat = () => {
const drop = React.useRef<HTMLDivElement>(null)

View File

@ -1,5 +1,5 @@
import { SettingsBody } from "~components/Sidepanel/Settings/body"
import { SidepanelSettingsHeader } from "~components/Sidepanel/Settings/header"
import { SettingsBody } from "~/components/Sidepanel/Settings/body"
import { SidepanelSettingsHeader } from "~/components/Sidepanel/Settings/header"
export const SidepanelSettings = () => {
return (

View File

@ -1,6 +1,6 @@
import { Storage } from "@plasmohq/storage"
import { cleanUrl } from "~libs/clean-url"
import { chromeRunTime } from "~libs/runtime"
import { cleanUrl } from "../libs/clean-url"
import { chromeRunTime } from "../libs/runtime"
const storage = new Storage()
@ -62,6 +62,7 @@ export const isOllamaRunning = async () => {
}
export const getAllModels = async ({ returnEmpty = false }: { returnEmpty?: boolean }) => {
try {
const baseUrl = await getOllamaURL()
const response = await fetch(`${cleanUrl(baseUrl)}/api/tags`)
if (!response.ok) {
@ -87,6 +88,10 @@ export const getAllModels = async ({ returnEmpty = false }: { returnEmpty?: bool
quantization_level: string
}
}[]
} catch (e) {
console.error(e)
return []
}
}
export const deleteModel = async (model: string) => {

10
src/types/index.ts Normal file
View File

@ -0,0 +1,10 @@
import { ChatHistory } from "@/store"
export type BotResponse = {
bot: {
text: string
sourceDocuments: any[]
}
history: ChatHistory
history_id: string
}

View File

@ -0,0 +1,55 @@
import {
HumanMessage,
AIMessage,
type MessageContent,
} from "@langchain/core/messages"
export const generateHistory = (
messages: {
role: "user" | "assistant" | "system"
content: string
image?: string
}[]
) => {
let history = []
for (const message of messages) {
if (message.role === "user") {
let content: MessageContent = [
{
type: "text",
text: message.content
}
]
if (message.image) {
content = [
{
type: "image_url",
image_url: message.image
},
{
type: "text",
text: message.content
}
]
}
history.push(
new HumanMessage({
content: content
})
)
} else if (message.role === "assistant") {
history.push(
new AIMessage({
content: [
{
type: "text",
text: message.content
}
]
})
)
}
}
return history
}

View File

@ -0,0 +1,63 @@
import { PageAssistHtmlLoader } from "~/loader/html"
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter"
import { MemoryVectorStore } from "langchain/vectorstores/memory"
import { OllamaEmbeddings } from "@langchain/community/embeddings/ollama"
import { defaultEmbeddingChunkOverlap, defaultEmbeddingChunkSize } from "@/services/ollama"
import { PageAssistPDFLoader } from "@/loader/pdf"
export const getLoader = ({ html, pdf, type, url }: {
url: string,
html: string,
type: string,
pdf: { content: string, page: number }[]
}) => {
if (type === "pdf") {
return new PageAssistPDFLoader({
pdf,
url
})
} else {
return new PageAssistHtmlLoader({
html,
url
})
}
}
export const memoryEmbedding = async (
{ html,
keepTrackOfEmbedding, ollamaEmbedding, pdf, setIsEmbedding, setKeepTrackOfEmbedding, type, url }: {
url: string,
html: string,
type: string,
pdf: { content: string, page: number }[],
keepTrackOfEmbedding: Record<string, MemoryVectorStore>,
ollamaEmbedding: OllamaEmbeddings,
setIsEmbedding: (value: boolean) => void,
setKeepTrackOfEmbedding: (value: Record<string, MemoryVectorStore>) => void
}
) => {
setIsEmbedding(true)
const loader = getLoader({ html, pdf, type, url })
const docs = await loader.load()
const chunkSize = await defaultEmbeddingChunkSize()
const chunkOverlap = await defaultEmbeddingChunkOverlap()
const textSplitter = new RecursiveCharacterTextSplitter({
chunkSize,
chunkOverlap
})
const chunks = await textSplitter.splitDocuments(docs)
const store = new MemoryVectorStore(ollamaEmbedding)
await store.addDocuments(chunks)
setKeepTrackOfEmbedding({
...keepTrackOfEmbedding,
[url]: store
})
setIsEmbedding(false)
return store
}

View File

@ -1,4 +1,4 @@
import { cleanUrl } from "~libs/clean-url"
import { cleanUrl } from "~/libs/clean-url"
export const verifyPageShareURL = async (url: string) => {
const res = await fetch(`${cleanUrl(url)}/api/v1/ping`)

View File

@ -2,10 +2,10 @@ import { OllamaEmbeddings } from "@langchain/community/embeddings/ollama"
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 { PageAssistHtmlLoader } from "~loader/html"
import { defaultEmbeddingChunkOverlap, defaultEmbeddingChunkSize, defaultEmbeddingModelForRag, getIsSimpleInternetSearch, getOllamaURL } from "~services/ollama"
import { cleanUrl } from "~/libs/clean-url"
import { chromeRunTime } from "~/libs/runtime"
import { PageAssistHtmlLoader } from "~/loader/html"
import { defaultEmbeddingChunkOverlap, defaultEmbeddingChunkSize, defaultEmbeddingModelForRag, getIsSimpleInternetSearch, getOllamaURL } from "~/services/ollama"
const BLOCKED_HOSTS = [
"google.com",

View File

@ -1,4 +1,4 @@
import { getWebSearchPrompt } from "~services/ollama"
import { getWebSearchPrompt } from "~/services/ollama"
import { webSearch } from "./local-google"
const getHostName = (url: string) => {

View File

@ -3,8 +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")]
}

View File

@ -1,11 +1,14 @@
{
"extends": "plasmo/templates/tsconfig.base",
"exclude": ["node_modules"],
"include": [".plasmo/index.d.ts", "./**/*.ts", "./**/*.tsx"],
"extends": "./.wxt/tsconfig.json",
"compilerOptions": {
"paths": {
"~*": ["./src/*"]
"noEmit": true,
"allowImportingTsExtensions": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"jsx": "react-jsx",
"strict": false
},
"baseUrl": "."
}
"exclude": [
"node_modules"
],
}

58
wxt.config.ts Normal file
View File

@ -0,0 +1,58 @@
import { defineConfig } from "wxt"
import react from "@vitejs/plugin-react"
import topLevelAwait from "vite-plugin-top-level-await"
// See https://wxt.dev/api/config.html
export default defineConfig({
vite: () => ({
plugins: [react(),
topLevelAwait({
promiseExportName: '__tla',
promiseImportName: i => `__tla_${i}`,
}),
],
build: {
rollupOptions: {
external: [
"langchain",
"@langchain/community",
]
}
}
}),
entrypointsDir: "entries",
srcDir: "src",
outDir: "build",
manifest: {
version: "1.1.0",
name: '__MSG_extName__',
description: '__MSG_extDescription__',
default_locale: 'en',
action: {},
author: "n4ze3m",
host_permissions: ["http://*/*", "https://*/*", "file://*/*"],
commands: {
_execute_action: {
suggested_key: {
default: "Ctrl+Shift+L"
}
},
execute_side_panel: {
description: "Open the side panel",
suggested_key: {
default: "Ctrl+Shift+P"
}
}
},
permissions: [
"storage",
"sidePanel",
"activeTab",
"scripting",
"declarativeNetRequest",
"action",
"unlimitedStorage",
"contextMenus"
]
}
})

6463
yarn.lock

File diff suppressed because it is too large Load Diff