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 # typescript
.tsbuildinfo .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.", "description": "Use your locally running AI models to assist you in your web browsing.",
"author": "n4ze3m", "author": "n4ze3m",
"scripts": { "scripts": {
"dev": "plasmo dev", "dev": "wxt",
"build": "plasmo build", "dev:firefox": "wxt -b firefox",
"package": "plasmo package" "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": { "dependencies": {
"@ant-design/cssinjs": "^1.18.4", "@ant-design/cssinjs": "^1.18.4",
"@headlessui/react": "^1.7.18", "@headlessui/react": "^1.7.18",
"@heroicons/react": "^2.1.1", "@heroicons/react": "^2.1.1",
"@langchain/community": "^0.0.21", "@langchain/community": "^0.0.41",
"@langchain/core": "^0.1.22",
"@mantine/form": "^7.5.0", "@mantine/form": "^7.5.0",
"@mantine/hooks": "^7.5.3", "@mantine/hooks": "^7.5.3",
"@plasmohq/storage": "^1.9.0", "@plasmohq/storage": "^1.9.0",
"@tailwindcss/forms": "^0.5.7", "@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.10", "@tailwindcss/typography": "^0.5.10",
"@tanstack/react-query": "^5.17.19", "@tanstack/react-query": "^5.17.19",
"@vitejs/plugin-react": "^4.2.1",
"antd": "^5.13.3", "antd": "^5.13.3",
"axios": "^1.6.7", "axios": "^1.6.7",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"html-to-text": "^9.0.5", "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", "lucide-react": "^0.350.0",
"plasmo": "0.84.1", "pdfjs-dist": "^4.0.379",
"property-information": "^6.4.1", "property-information": "^6.4.1",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-i18next": "^14.1.0",
"react-markdown": "8.0.0", "react-markdown": "8.0.0",
"react-router-dom": "6.10.0", "react-router-dom": "6.10.0",
"react-syntax-highlighter": "^15.5.0", "react-syntax-highlighter": "^15.5.0",
@ -53,34 +61,11 @@
"postcss": "^8.4.33", "postcss": "^8.4.33",
"prettier": "3.2.4", "prettier": "3.2.4",
"tailwindcss": "^3.4.1", "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": { "resolutions": {
"host_permissions": [ "@langchain/core": "0.1.45"
"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"
]
} }
} }

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, RunnableMap,
RunnableSequence, RunnableSequence,
} from "langchain/schema/runnable"; } from "langchain/schema/runnable";
import type { ChatHistory } from "~store"; import type { ChatHistory } from "~/store";
type RetrievalChainInput = { type RetrievalChainInput = {
chat_history: string; chat_history: string;
question: string; question: string;

View File

@ -8,17 +8,11 @@ import "property-information"
import React from "react" import React from "react"
import { Tooltip } from "antd" import { Tooltip } from "antd"
import { CheckIcon, ClipboardIcon } from "lucide-react" import { CheckIcon, ClipboardIcon } from "lucide-react"
import { useTranslation } from "react-i18next"
export default function Markdown({ message }: { message: string }) { export default function Markdown({ message }: { message: string }) {
const [isBtnPressed, setIsBtnPressed] = React.useState(false) const [isBtnPressed, setIsBtnPressed] = React.useState(false)
const { t } = useTranslation("common")
React.useEffect(() => {
if (isBtnPressed) {
setTimeout(() => {
setIsBtnPressed(false)
}, 4000)
}
}, [isBtnPressed])
return ( return (
<React.Fragment> <React.Fragment>
@ -37,11 +31,14 @@ export default function Markdown({ message }: { message: string }) {
</span> </span>
<div className="flex items-center"> <div className="flex items-center">
<Tooltip title="Copy to clipboard"> <Tooltip title={t("copyToClipboard")}>
<button <button
onClick={() => { onClick={() => {
navigator.clipboard.writeText(children[0] as string) navigator.clipboard.writeText(children[0] as string)
setIsBtnPressed(true) 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"> 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 ? ( {!isBtnPressed ? (

View File

@ -1,6 +1,7 @@
import { useForm } from "@mantine/form" import { useForm } from "@mantine/form"
import React from "react" import React from "react"
import useDynamicTextareaSize from "~hooks/useDynamicTextareaSize" import { useTranslation } from "react-i18next"
import useDynamicTextareaSize from "~/hooks/useDynamicTextareaSize"
type Props = { type Props = {
value: string value: string
@ -12,6 +13,7 @@ type Props = {
export const EditMessageForm = (props: Props) => { export const EditMessageForm = (props: Props) => {
const [isComposing, setIsComposing] = React.useState(false) const [isComposing, setIsComposing] = React.useState(false)
const textareaRef = React.useRef<HTMLTextAreaElement>(null) const textareaRef = React.useRef<HTMLTextAreaElement>(null)
const { t } = useTranslation("common")
const form = useForm({ const form = useForm({
initialValues: { initialValues: {
@ -40,21 +42,21 @@ export const EditMessageForm = (props: Props) => {
rows={1} rows={1}
style={{ minHeight: "60px" }} style={{ minHeight: "60px" }}
tabIndex={0} tabIndex={0}
placeholder="Type a message..." placeholder={t("editMessage.placeholder")}
ref={textareaRef} 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" 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"> <div className="flex justify-center space-x-2 mt-2">
<button <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"> 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>
<button <button
onClick={props.onClose} 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"> 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> </button>
</div> </div>
</form> </form>

View File

@ -4,6 +4,7 @@ import { Image, Tooltip } from "antd"
import { WebSearch } from "./WebSearch" import { WebSearch } from "./WebSearch"
import { CheckIcon, ClipboardIcon, Pen, RotateCcw } from "lucide-react" import { CheckIcon, ClipboardIcon, Pen, RotateCcw } from "lucide-react"
import { EditMessageForm } from "./EditMessageForm" import { EditMessageForm } from "./EditMessageForm"
import { useTranslation } from "react-i18next"
type Props = { type Props = {
message: string message: string
@ -28,6 +29,8 @@ export const PlaygroundMessage = (props: Props) => {
const [isBtnPressed, setIsBtnPressed] = React.useState(false) const [isBtnPressed, setIsBtnPressed] = React.useState(false)
const [editMode, setEditMode] = React.useState(false) const [editMode, setEditMode] = React.useState(false)
const { t } = useTranslation("common")
return ( return (
<div className="group w-full text-gray-800 dark:text-gray-100"> <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"> <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.isBot && (
<> <>
{!props.hideCopy && ( {!props.hideCopy && (
<Tooltip title="Copy to clipboard"> <Tooltip title={t("copyToClipboard")}
>
<button <button
onClick={() => { onClick={() => {
navigator.clipboard.writeText(props.message) navigator.clipboard.writeText(props.message)
@ -133,7 +137,8 @@ export const PlaygroundMessage = (props: Props) => {
{!props.hideEditAndRegenerate && {!props.hideEditAndRegenerate &&
props.currentMessageIndex === props.totalMessages - 1 && ( props.currentMessageIndex === props.totalMessages - 1 && (
<Tooltip title="Regenerate"> <Tooltip title={t("regenerate")}
>
<button <button
onClick={props.onRengerate} 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"> 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 && ( {!props.hideEditAndRegenerate && (
<Tooltip title="Edit"> <Tooltip title={t("edit")}
>
<button <button
onClick={() => setEditMode(true)} 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"> 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 { Globe } from "lucide-react"
import { useTranslation } from "react-i18next"
export const WebSearch = () => { export const WebSearch = () => {
const {t} = useTranslation('common')
return ( 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="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"> <div className="rounded p-1">
<Globe className="w-6 h-6" /> <Globe className="w-6 h-6" />
</div> </div>
<div className="text-sm font-semibold">Searching the web</div> <div className="text-sm font-semibold">
{t('webSearch')}
</div>
</div> </div>
) )
} }

View File

@ -1,5 +1,6 @@
import { useState } from "react" import { useState } from "react"
import { CheckIcon } from "lucide-react" import { CheckIcon } from "lucide-react"
import { useTranslation } from "react-i18next"
type Props = { type Props = {
onClick?: () => void onClick?: () => void
disabled?: boolean disabled?: boolean
@ -13,11 +14,12 @@ export const SaveButton = ({
onClick, onClick,
disabled, disabled,
className, className,
text = "Save", text = "save",
textOnSave = "Saved", textOnSave = "saved",
btnType = "button" btnType = "button"
}: Props) => { }: Props) => {
const [clickedSave, setClickedSave] = useState(false) const [clickedSave, setClickedSave] = useState(false)
const { t } = useTranslation("common")
return ( return (
<button <button
type={btnType} type={btnType}
@ -33,7 +35,7 @@ export const SaveButton = ({
disabled={disabled} 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}`}> 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 ? <CheckIcon className="w-4 h-4 mr-2" /> : null}
{clickedSave ? textOnSave : text} {clickedSave ? t(textOnSave) : t(text)}
</button> </button>
) )
} }

View File

@ -1,13 +1,14 @@
import { Form, Image, Input, Modal, Tooltip, message } from "antd" import { Form, Image, Input, Modal, Tooltip, message } from "antd"
import { Share } from "lucide-react" import { Share } from "lucide-react"
import { useState } from "react" import { useState } from "react"
import type { Message } from "~store/option" import type { Message } from "~/store/option"
import Markdown from "./Markdown" import Markdown from "./Markdown"
import React from "react" import React from "react"
import { useMutation } from "@tanstack/react-query" import { useMutation } from "@tanstack/react-query"
import { getPageShareUrl } from "~services/ollama" import { getPageShareUrl } from "~/services/ollama"
import { cleanUrl } from "~libs/clean-url" import { cleanUrl } from "~/libs/clean-url"
import { getUserId, saveWebshare } from "~libs/db" import { getUserId, saveWebshare } from "~/libs/db"
import { useTranslation } from "react-i18next"
type Props = { type Props = {
messages: Message[] messages: Message[]
@ -75,6 +76,7 @@ export const PlaygroundMessage = (
} }
export const ShareBtn: React.FC<Props> = ({ messages }) => { export const ShareBtn: React.FC<Props> = ({ messages }) => {
const { t } = useTranslation("common")
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [form] = Form.useForm() const [form] = Form.useForm()
const name = Form.useWatch("name", form) 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() const data = await res.json()
@ -121,18 +123,23 @@ export const ShareBtn: React.FC<Props> = ({ messages }) => {
onSuccess: async (data) => { onSuccess: async (data) => {
const url = data.url const url = data.url
navigator.clipboard.writeText(url) navigator.clipboard.writeText(url)
message.success("Link copied to clipboard") message.success(t("share.notification.successGenerate"))
await saveWebshare({ title: data.title, url, api_url: data.api_url, share_id: data.share_id }) await saveWebshare({
title: data.title,
url,
api_url: data.api_url,
share_id: data.share_id
})
setOpen(false) setOpen(false)
}, },
onError: (error) => { onError: (error) => {
message.error(error?.message || "Failed to create share link") message.error(error?.message || t("share.notification.failGenerate"))
} }
}) })
return ( return (
<> <>
<Tooltip title="Share"> <Tooltip title={t("share.tooltip.share")}>
<button <button
onClick={() => setOpen(true)} onClick={() => setOpen(true)}
className="!text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"> 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> </Tooltip>
<Modal <Modal
title="Share link to Chat" title={t("share.modal.title")}
open={open} open={open}
footer={null} footer={null}
width={600} width={600}
@ -151,20 +158,30 @@ export const ShareBtn: React.FC<Props> = ({ messages }) => {
layout="vertical" layout="vertical"
onFinish={createShareLink} onFinish={createShareLink}
initialValues={{ initialValues={{
title: "Untitled Chat", title: t("share.form.defaultValue.title"),
name: "Anonymous" name: t("share.form.defaultValue.name")
}}> }}>
<Form.Item <Form.Item
name="title" name="title"
label="Chat Title" label={t("share.form.title.label")}
rules={[{ required: true, message: "Please enter chat title" }]}> rules={[
<Input size="large" placeholder="Enter chat title" /> { required: true, message: t("share.form.title.required") }
]}>
<Input
size="large"
placeholder={t("share.form.title.placeholder")}
/>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="name" name="name"
label="Your Name" label={t("share.form.name.label")}
rules={[{ required: true, message: "Please enter your name" }]}> rules={[
<Input size="large" placeholder="Enter your name" /> { required: true, message: t("share.form.name.required") }
]}>
<Input
size="large"
placeholder={t("share.form.name.placeholder")}
/>
</Form.Item> </Form.Item>
<Form.Item> <Form.Item>
@ -182,7 +199,9 @@ export const ShareBtn: React.FC<Props> = ({ messages }) => {
<button <button
type="submit" 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 "> 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> </button>
</div> </div>
</Form.Item> </Form.Item>

View File

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

View File

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

View File

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

View File

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

View File

@ -1,14 +1,16 @@
import { useQuery } from "@tanstack/react-query" import { useQuery } from "@tanstack/react-query"
import { RotateCcw } from "lucide-react" import { RotateCcw } from "lucide-react"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import { useTranslation } from "react-i18next"
import { import {
getOllamaURL, getOllamaURL,
isOllamaRunning, isOllamaRunning,
setOllamaURL as saveOllamaURL setOllamaURL as saveOllamaURL
} from "~services/ollama" } from "~/services/ollama"
export const PlaygroundEmpty = () => { export const PlaygroundEmpty = () => {
const [ollamaURL, setOllamaURL] = useState<string>("") const [ollamaURL, setOllamaURL] = useState<string>("")
const { t } = useTranslation(["playground", "common"])
const { const {
data: ollamaInfo, data: ollamaInfo,
status: ollamaStatus, status: ollamaStatus,
@ -40,7 +42,7 @@ export const PlaygroundEmpty = () => {
<div className="inline-flex items-center space-x-2"> <div className="inline-flex items-center space-x-2">
<div className="w-3 h-3 bg-blue-500 rounded-full animate-bounce"></div> <div className="w-3 h-3 bg-blue-500 rounded-full animate-bounce"></div>
<p className="dark:text-gray-400 text-gray-900"> <p className="dark:text-gray-400 text-gray-900">
Searching for Your Ollama 🦙 {t("ollamaState.searching")}
</p> </p>
</div> </div>
)} )}
@ -49,7 +51,7 @@ export const PlaygroundEmpty = () => {
<div className="inline-flex items-center space-x-2"> <div className="inline-flex items-center space-x-2">
<div className="w-3 h-3 bg-green-500 rounded-full"></div> <div className="w-3 h-3 bg-green-500 rounded-full"></div>
<p className="dark:text-gray-400 text-gray-900"> <p className="dark:text-gray-400 text-gray-900">
Ollama is running 🦙 {t("ollamaState.running")}
</p> </p>
</div> </div>
) : ( ) : (
@ -57,7 +59,7 @@ export const PlaygroundEmpty = () => {
<div className="inline-flex space-x-2"> <div className="inline-flex space-x-2">
<div className="w-3 h-3 bg-red-500 rounded-full"></div> <div className="w-3 h-3 bg-red-500 rounded-full"></div>
<p className="dark:text-gray-400 text-gray-900"> <p className="dark:text-gray-400 text-gray-900">
Unable to connect to Ollama 🦙 {t("ollamaState.notRunning")}
</p> </p>
</div> </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 "> 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" /> <RotateCcw className="h-4 w-4 mr-3" />
Retry {t("common:retry")}
</button> </button>
</div> </div>
) )

View File

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

View File

@ -1,8 +1,10 @@
import { PencilIcon } from "lucide-react" import { PencilIcon } from "lucide-react"
import { useMessage } from "../../../hooks/useMessage" import { useMessage } from "../../../hooks/useMessage"
import { useTranslation } from 'react-i18next';
export const PlaygroundNewChat = () => { export const PlaygroundNewChat = () => {
const { setHistory, setMessages, setHistoryId } = useMessage() const { setHistory, setMessages, setHistoryId } = useMessage()
const { t } = useTranslation('optionChat')
const handleClick = () => { const handleClick = () => {
setHistoryId(null) 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"> 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" /> <PencilIcon className="mx-3 h-5 w-5" aria-hidden="true" />
<span className="inline-flex font-semibol text-white text-sm"> <span className="inline-flex font-semibol text-white text-sm">
New Chat {t('newChat')}
</span> </span>
</button> </button>
) )

View File

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

View File

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

View File

@ -1,11 +1,13 @@
import { useQueryClient } from "@tanstack/react-query" import { useQueryClient } from "@tanstack/react-query"
import { useDarkMode } from "~hooks/useDarkmode" import { useDarkMode } from "~/hooks/useDarkmode"
import { useMessageOption } from "~hooks/useMessageOption" import { useMessageOption } from "~/hooks/useMessageOption"
import { PageAssitDatabase } from "~libs/db" import { PageAssitDatabase } from "~/libs/db"
import { Select } from "antd" 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 { MoonIcon, SunIcon } from "lucide-react"
import { SearchModeSettings } from "./search-mode" import { SearchModeSettings } from "./search-mode"
import { useTranslation } from "react-i18next"
import { useI18n } from "@/hooks/useI18n"
export const SettingOther = () => { export const SettingOther = () => {
const { clearChat, speechToTextLanguage, setSpeechToTextLanguage } = const { clearChat, speechToTextLanguage, setSpeechToTextLanguage } =
@ -14,29 +16,36 @@ export const SettingOther = () => {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const { mode, toggleDarkMode } = useDarkMode() const { mode, toggleDarkMode } = useDarkMode()
const { t } = useTranslation("settings")
const {
changeLocale,
locale,
supportLanguage
}= useI18n()
return ( return (
<dl className="flex flex-col space-y-6"> <dl className="flex flex-col space-y-6 text-sm">
<div> <div>
<h2 className="text-base font-semibold leading-7 text-gray-900 dark:text-white"> <h2 className="text-base font-semibold leading-7 text-gray-900 dark:text-white">
Web UI Settings {t("generalSettings.heading")}
</h2> </h2>
<div className="border border-b border-gray-200 dark:border-gray-600 mt-3"></div> <div className="border border-b border-gray-200 dark:border-gray-600 mt-3"></div>
</div> </div>
<div className="flex flex-row justify-between"> <div className="flex flex-row justify-between">
<span className="text-gray-500 dark:text-neutral-50"> <span className="text-gray-500 dark:text-neutral-50">
Speech Recognition Language {t("generalSettings.settings.speechRecognitionLang.label")}
</span> </span>
<Select <Select
placeholder="Select Language" placeholder={t("generalSettings.settings.speechRecognitionLang.placeholder")}
allowClear allowClear
showSearch showSearch
options={SUPPORTED_LANGUAGES} options={SUPPORTED_LANGUAGES}
value={speechToTextLanguage} value={speechToTextLanguage}
filterOption={(input, option) => filterOption={(input, option) =>
option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 || option!.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 ||
option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0 option!.value.toLowerCase().indexOf(input.toLowerCase()) >= 0
} }
onChange={(value) => { onChange={(value) => {
setSpeechToTextLanguage(value) setSpeechToTextLanguage(value)
@ -44,7 +53,30 @@ export const SettingOther = () => {
/> />
</div> </div>
<div className="flex flex-row justify-between"> <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 <button
onClick={toggleDarkMode} onClick={toggleDarkMode}
@ -54,19 +86,19 @@ export const SettingOther = () => {
) : ( ) : (
<MoonIcon className="w-4 h-4 mr-2" /> <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> </button>
</div> </div>
<SearchModeSettings /> <SearchModeSettings />
<div className="flex flex-row justify-between"> <div className="flex flex-row justify-between">
<span className="text-gray-500 dark:text-neutral-50 "> <span className="text-gray-500 dark:text-neutral-50 ">
Delete Chat History {t("generalSettings.settings.deleteChatHistory.label")}
</span> </span>
<button <button
onClick={async () => { onClick={async () => {
const confirm = window.confirm( 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) { 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"> 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> </button>
</div> </div>
</dl> </dl>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,20 +1,27 @@
import logoImage from "data-base64:~assets/icon.png" import logoImage from "~/assets/icon.png"
import { useMessage } from "~hooks/useMessage" import { useMessage } from "~/hooks/useMessage"
import { Link } from "react-router-dom" import { Link } from "react-router-dom"
import { Tooltip } from "antd" import { Tooltip } from "antd"
import { BoxesIcon, CogIcon, RefreshCcw } from "lucide-react" import { BoxesIcon, CogIcon, RefreshCcw } from "lucide-react"
import { useTranslation } from "react-i18next"
export const SidepanelHeader = () => { export const SidepanelHeader = () => {
const { clearChat, isEmbedding } = useMessage() const { clearChat, isEmbedding } = useMessage()
const { t } = useTranslation(["sidepanel", "common"])
return ( 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="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"> <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" /> <img
<span className="ml-1 text-sm ">Page Assist</span> className="h-6 w-auto"
src={logoImage}
alt={t("common:pageAssist")}
/>
<span className="ml-1 text-sm ">{t("common:pageAssist")}</span>
</div> </div>
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
{isEmbedding ? ( {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" /> <BoxesIcon className="h-5 w-5 text-gray-500 dark:text-gray-400 animate-bounce animate-infinite" />
</Tooltip> </Tooltip>
) : null} ) : null}

View File

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

View File

@ -1,15 +1,18 @@
import logoImage from "data-base64:~assets/icon.png"
import { ChevronLeft } from "lucide-react" import { ChevronLeft } from "lucide-react"
import { useTranslation } from "react-i18next"
import { Link } from "react-router-dom" import { Link } from "react-router-dom"
import logoImage from "~/assets/icon.png"
export const SidepanelSettingsHeader = () => { export const SidepanelSettingsHeader = () => {
const { t } = useTranslation("common")
return ( 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"> <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="/"> <Link to="/">
<ChevronLeft className="h-5 w-5 text-gray-500 dark:text-gray-400" /> <ChevronLeft className="h-5 w-5 text-gray-500 dark:text-gray-400" />
</Link> </Link>
<div className="focus:outline-none focus-visible:ring-2 focus-visible:ring-pink-700 flex items-center dark:text-white"> <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" /> <img className="h-6 w-auto" src={logoImage} alt={t("pageAssist")} />
<span className="ml-1 text-sm ">Page Assist</span> <span className="ml-1 text-sm ">{t("pageAssist")}</span>
</div> </div>
</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 { ToastContainer } from "react-toastify"
import "react-toastify/dist/ReactToastify.css" import "react-toastify/dist/ReactToastify.css"
const queryClient = new QueryClient() const queryClient = new QueryClient()
import "./css/tailwind.css" import { ConfigProvider, Empty, theme } from "antd"
import { ConfigProvider, theme } from "antd"
import { StyleProvider } from "@ant-design/cssinjs" import { StyleProvider } from "@ant-design/cssinjs"
import { useDarkMode } from "~hooks/useDarkmode" import { useDarkMode } from "~/hooks/useDarkmode"
import { OptionRouting } from "~routes" import { OptionRouting } from "~/routes"
import "~/i18n"
import { useTranslation } from "react-i18next"
function IndexOption() { function IndexOption() {
const { mode } = useDarkMode() const { mode } = useDarkMode()
const { t } = useTranslation()
return ( return (
<MemoryRouter> <MemoryRouter>
<ConfigProvider <ConfigProvider
theme={{ theme={{
algorithm: algorithm:
mode === "dark" ? theme.darkAlgorithm : theme.defaultAlgorithm mode === "dark" ? theme.darkAlgorithm : theme.defaultAlgorithm
}}> }}
renderEmpty={() => (
<Empty
imageStyle={{
height: 60
}}
description={t("common:noData")}
/>
)}
>
<StyleProvider hashPriority="high"> <StyleProvider hashPriority="high">
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<OptionRouting /> <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 { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { MemoryRouter } from "react-router-dom" import { MemoryRouter } from "react-router-dom"
import { SidepanelRouting } from "~routes" import { SidepanelRouting } from "~/routes"
import { ToastContainer } from "react-toastify" import { ToastContainer } from "react-toastify"
import "react-toastify/dist/ReactToastify.css" import "react-toastify/dist/ReactToastify.css"
const queryClient = new QueryClient() const queryClient = new QueryClient()
import "./css/tailwind.css" import { ConfigProvider, Empty, theme } from "antd"
import { ConfigProvider, theme } from "antd"
import { StyleProvider } from "@ant-design/cssinjs" 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() { function IndexSidepanel() {
const { mode } = useDarkMode() const { mode } = useDarkMode()
const { t } = useTranslation()
return ( return (
<MemoryRouter> <MemoryRouter>
@ -17,7 +20,16 @@ function IndexSidepanel() {
theme={{ theme={{
algorithm: algorithm:
mode === "dark" ? theme.darkAlgorithm : theme.defaultAlgorithm mode === "dark" ? theme.darkAlgorithm : theme.defaultAlgorithm
}}> }}
renderEmpty={() => (
<Empty
imageStyle={{
height: 60
}}
description={t("common:noData")}
/>
)}
>
<StyleProvider hashPriority="high"> <StyleProvider hashPriority="high">
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<SidepanelRouting /> <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 React from "react"
import { cleanUrl } from "~libs/clean-url" import { cleanUrl } from "~/libs/clean-url"
import { import {
defaultEmbeddingChunkOverlap,
defaultEmbeddingChunkSize,
defaultEmbeddingModelForRag, defaultEmbeddingModelForRag,
getOllamaURL, getOllamaURL,
promptForRag, promptForRag,
systemPromptForNonRag systemPromptForNonRag
} from "~services/ollama" } from "~/services/ollama"
import { useStoreMessage, type ChatHistory, type Message } from "~store" import { useStoreMessage, type Message } from "~/store"
import { ChatOllama } from "@langchain/community/chat_models/ollama" import { ChatOllama } from "@langchain/community/chat_models/ollama"
import { import { HumanMessage, SystemMessage } from "@langchain/core/messages"
HumanMessage, import { getDataFromCurrentTab } from "~/libs/get-html"
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 { OllamaEmbeddings } from "@langchain/community/embeddings/ollama" import { OllamaEmbeddings } from "@langchain/community/embeddings/ollama"
import { import {
createChatWithWebsiteChain, createChatWithWebsiteChain,
groupMessagesByConversation groupMessagesByConversation
} from "~chain/chat-with-website" } from "~/chain/chat-with-website"
import { MemoryVectorStore } from "langchain/vectorstores/memory" import { MemoryVectorStore } from "langchain/vectorstores/memory"
import { chromeRunTime } from "~libs/runtime" import { memoryEmbedding } from "@/utils/memory-embeddings"
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
}
export const useMessage = () => { export const useMessage = () => {
const { const {
@ -129,47 +62,18 @@ export const useMessage = () => {
setStreaming(false) 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) => { const chatWithWebsiteMode = async (message: string) => {
try { try {
let isAlreadyExistEmbedding: MemoryVectorStore 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) { if (messages.length === 0) {
const { html, url } = await getHtmlOfCurrentTab() const { content: html, url, type, pdf } = await getDataFromCurrentTab()
embedHTML = html embedHTML = html
embedURL = url embedURL = url
embedType = type
embedPDF = pdf
setCurrentURL(url) setCurrentURL(url)
isAlreadyExistEmbedding = keepTrackOfEmbedding[currentURL] isAlreadyExistEmbedding = keepTrackOfEmbedding[currentURL]
} else { } else {
@ -212,11 +116,16 @@ export const useMessage = () => {
if (isAlreadyExistEmbedding) { if (isAlreadyExistEmbedding) {
vectorstore = isAlreadyExistEmbedding vectorstore = isAlreadyExistEmbedding
} else { } else {
vectorstore = await memoryEmbedding( vectorstore = await memoryEmbedding({
embedURL, html: embedHTML,
embedHTML, keepTrackOfEmbedding: keepTrackOfEmbedding,
ollamaEmbedding ollamaEmbedding: ollamaEmbedding,
) pdf: embedPDF,
setIsEmbedding: setIsEmbedding,
setKeepTrackOfEmbedding: setKeepTrackOfEmbedding,
type: embedType,
url: embedURL
})
} }
const { ragPrompt: systemPrompt, ragQuestionPrompt: questionPrompt } = const { ragPrompt: systemPrompt, ragQuestionPrompt: questionPrompt } =

View File

@ -1,19 +1,14 @@
import React from "react" import React from "react"
import { cleanUrl } from "~libs/clean-url" import { cleanUrl } from "~/libs/clean-url"
import { import {
geWebSearchFollowUpPrompt, geWebSearchFollowUpPrompt,
getOllamaURL, getOllamaURL,
systemPromptForNonRagOption systemPromptForNonRagOption
} from "~services/ollama" } from "~/services/ollama"
import { type ChatHistory, type Message } from "~store/option" import { type ChatHistory, type Message } from "~/store/option"
import { ChatOllama } from "@langchain/community/chat_models/ollama" import { ChatOllama } from "@langchain/community/chat_models/ollama"
import { import { HumanMessage, SystemMessage } from "@langchain/core/messages"
HumanMessage, import { useStoreMessageOption } from "~/store/option"
AIMessage,
type MessageContent,
SystemMessage
} from "@langchain/core/messages"
import { useStoreMessageOption } from "~store/option"
import { import {
deleteChatForEdit, deleteChatForEdit,
getPromptById, getPromptById,
@ -21,69 +16,12 @@ import {
saveHistory, saveHistory,
saveMessage, saveMessage,
updateMessageByIndex updateMessageByIndex
} from "~libs/db" } from "~/libs/db"
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom"
import { notification } from "antd" import { notification } from "antd"
import { getSystemPromptForWeb } from "~web/web" import { getSystemPromptForWeb } from "~/web/web"
import { generateHistory } from "@/utils/generate-history"
export type BotResponse = { import { useTranslation } from "react-i18next"
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
}
export const useMessageOption = () => { export const useMessageOption = () => {
const { const {
@ -116,7 +54,7 @@ export const useMessageOption = () => {
setSelectedSystemPrompt setSelectedSystemPrompt
} = useStoreMessageOption() } = useStoreMessageOption()
// const { notification } = App.useApp() const { t } = useTranslation("option")
const navigate = useNavigate() const navigate = useNavigate()
const textareaRef = React.useRef<HTMLTextAreaElement>(null) const textareaRef = React.useRef<HTMLTextAreaElement>(null)
@ -150,7 +88,7 @@ export const useMessageOption = () => {
abortControllerRef.current = new AbortController() abortControllerRef.current = new AbortController()
const ollama = new ChatOllama({ const ollama = new ChatOllama({
model: selectedModel, model: selectedModel!,
baseUrl: cleanUrl(url) baseUrl: cleanUrl(url)
}) })
@ -204,7 +142,7 @@ export const useMessageOption = () => {
.replaceAll("{chat_history}", chat_history) .replaceAll("{chat_history}", chat_history)
.replaceAll("{question}", message) .replaceAll("{question}", message)
const questionOllama = new ChatOllama({ const questionOllama = new ChatOllama({
model: selectedModel, model: selectedModel!,
baseUrl: cleanUrl(url) baseUrl: cleanUrl(url)
}) })
const response = await questionOllama.invoke(promptForQuestion) const response = await questionOllama.invoke(promptForQuestion)
@ -308,11 +246,11 @@ export const useMessageOption = () => {
if (historyId) { if (historyId) {
if (!isRegenerate) { if (!isRegenerate) {
await saveMessage(historyId, selectedModel, "user", message, [image]) await saveMessage(historyId, selectedModel!, "user", message, [image])
} }
await saveMessage( await saveMessage(
historyId, historyId,
selectedModel, selectedModel!,
"assistant", "assistant",
newMessage[appendingIndex].message, newMessage[appendingIndex].message,
[], [],
@ -320,12 +258,12 @@ export const useMessageOption = () => {
) )
} else { } else {
const newHistoryId = await saveHistory(message) const newHistoryId = await saveHistory(message)
await saveMessage(newHistoryId.id, selectedModel, "user", message, [ await saveMessage(newHistoryId.id, selectedModel!, "user", message, [
image image
]) ])
await saveMessage( await saveMessage(
newHistoryId.id, newHistoryId.id,
selectedModel, selectedModel!,
"assistant", "assistant",
newMessage[appendingIndex].message, newMessage[appendingIndex].message,
[], [],
@ -337,6 +275,7 @@ export const useMessageOption = () => {
setIsProcessing(false) setIsProcessing(false)
setStreaming(false) setStreaming(false)
} catch (e) { } catch (e) {
//@ts-ignore
if (e?.name === "AbortError") { if (e?.name === "AbortError") {
newMessage[appendingIndex].message = newMessage[ newMessage[appendingIndex].message = newMessage[
appendingIndex appendingIndex
@ -356,22 +295,22 @@ export const useMessageOption = () => {
]) ])
if (historyId) { if (historyId) {
await saveMessage(historyId, selectedModel, "user", message, [image]) await saveMessage(historyId, selectedModel!, "user", message, [image])
await saveMessage( await saveMessage(
historyId, historyId,
selectedModel, selectedModel!,
"assistant", "assistant",
newMessage[appendingIndex].message, newMessage[appendingIndex].message,
[] []
) )
} else { } else {
const newHistoryId = await saveHistory(message) const newHistoryId = await saveHistory(message)
await saveMessage(newHistoryId.id, selectedModel, "user", message, [ await saveMessage(newHistoryId.id, selectedModel!, "user", message, [
image image
]) ])
await saveMessage( await saveMessage(
newHistoryId.id, newHistoryId.id,
selectedModel, selectedModel!,
"assistant", "assistant",
newMessage[appendingIndex].message, newMessage[appendingIndex].message,
[] []
@ -379,9 +318,10 @@ export const useMessageOption = () => {
setHistoryId(newHistoryId.id) setHistoryId(newHistoryId.id)
} }
} else { } else {
//@ts-ignore
notification.error({ notification.error({
message: "Error", message: t("error"),
description: e?.message || "Something went wrong" description: e?.message || t("somethingWentWrong")
}) })
} }
@ -405,7 +345,7 @@ export const useMessageOption = () => {
abortControllerRef.current = new AbortController() abortControllerRef.current = new AbortController()
const ollama = new ChatOllama({ const ollama = new ChatOllama({
model: selectedModel, model: selectedModel!,
baseUrl: cleanUrl(url) baseUrl: cleanUrl(url)
}) })
@ -620,8 +560,8 @@ export const useMessageOption = () => {
} }
} else { } else {
notification.error({ notification.error({
message: "Error", message: t("error"),
description: e?.message || "Something went wrong" description: e?.message || t("somethingWentWrong")
}) })
} }
@ -699,8 +639,8 @@ export const useMessageOption = () => {
const validateBeforeSubmit = () => { const validateBeforeSubmit = () => {
if (!selectedModel || selectedModel?.trim()?.length === 0) { if (!selectedModel || selectedModel?.trim()?.length === 0) {
notification.error({ notification.error({
message: "Error", message: t("error"),
description: "Please select a model to continue" description: t("validationSelectModel")
}) })
return false 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 { import {
type ChatHistory as ChatHistoryType, type ChatHistory as ChatHistoryType,
type Message as MessageType type Message as MessageType
} from "~store/option" } from "~/store/option"
type HistoryInfo = { type HistoryInfo = {
id: string 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 const url = window.location.href
if (document.contentType === "application/pdf") {
return { url, content: "", type: "pdf" }
}
const html = Array.from(document.querySelectorAll("script")).reduce( const html = Array.from(document.querySelectorAll("script")).reduce(
(acc, script) => { (acc, script) => {
return acc.replace(script.outerHTML, "") return acc.replace(script.outerHTML, "")
}, },
document.documentElement.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) => { const result = new Promise((resolve) => {
chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => { chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => {
const tab = tabs[0] const tab = tabs[0]
@ -25,9 +54,48 @@ export const getHtmlOfCurrentTab = async () => {
}) })
}) as Promise<{ }) as Promise<{
url: string 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 { BaseDocumentLoader } from "langchain/document_loaders/base"
import { Document } from "@langchain/core/documents" import { Document } from "@langchain/core/documents"
import { compile } from "html-to-text" import { compile } from "html-to-text"
import { chromeRunTime } from "~libs/runtime" import { chromeRunTime } from "~/libs/runtime"
import { YtTranscript } from "yt-transcript" import { YtTranscript } from "yt-transcript"
const YT_REGEX = 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 { Route, Routes } from "react-router-dom"
import { SidepanelChat } from "./sidepanel-chat" import { SidepanelChat } from "./sidepanel-chat"
import { useDarkMode } from "~hooks/useDarkmode" import { useDarkMode } from "~/hooks/useDarkmode"
import { SidepanelSettings } from "./sidepanel-settings" import { SidepanelSettings } from "./sidepanel-settings"
import { OptionIndex } from "./option-index" import { OptionIndex } from "./option-index"
import { OptionModal } from "./option-settings-model" import { OptionModal } from "./option-settings-model"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { Storage } from "@plasmohq/storage" import { Storage } from "@plasmohq/storage"
import { cleanUrl } from "~libs/clean-url" import { cleanUrl } from "../libs/clean-url"
import { chromeRunTime } from "~libs/runtime" import { chromeRunTime } from "../libs/runtime"
const storage = new Storage() const storage = new Storage()
@ -62,6 +62,7 @@ export const isOllamaRunning = async () => {
} }
export const getAllModels = async ({ returnEmpty = false }: { returnEmpty?: boolean }) => { export const getAllModels = async ({ returnEmpty = false }: { returnEmpty?: boolean }) => {
try {
const baseUrl = await getOllamaURL() const baseUrl = await getOllamaURL()
const response = await fetch(`${cleanUrl(baseUrl)}/api/tags`) const response = await fetch(`${cleanUrl(baseUrl)}/api/tags`)
if (!response.ok) { if (!response.ok) {
@ -87,6 +88,10 @@ export const getAllModels = async ({ returnEmpty = false }: { returnEmpty?: bool
quantization_level: string quantization_level: string
} }
}[] }[]
} catch (e) {
console.error(e)
return []
}
} }
export const deleteModel = async (model: string) => { 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) => { export const verifyPageShareURL = async (url: string) => {
const res = await fetch(`${cleanUrl(url)}/api/v1/ping`) 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 type { Document } from "@langchain/core/documents"
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter" import { RecursiveCharacterTextSplitter } from "langchain/text_splitter"
import { MemoryVectorStore } from "langchain/vectorstores/memory" import { MemoryVectorStore } from "langchain/vectorstores/memory"
import { cleanUrl } from "~libs/clean-url" import { cleanUrl } from "~/libs/clean-url"
import { chromeRunTime } from "~libs/runtime" import { chromeRunTime } from "~/libs/runtime"
import { PageAssistHtmlLoader } from "~loader/html" import { PageAssistHtmlLoader } from "~/loader/html"
import { defaultEmbeddingChunkOverlap, defaultEmbeddingChunkSize, defaultEmbeddingModelForRag, getIsSimpleInternetSearch, getOllamaURL } from "~services/ollama" import { defaultEmbeddingChunkOverlap, defaultEmbeddingChunkSize, defaultEmbeddingModelForRag, getIsSimpleInternetSearch, getOllamaURL } from "~/services/ollama"
const BLOCKED_HOSTS = [ const BLOCKED_HOSTS = [
"google.com", "google.com",

View File

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

View File

@ -3,8 +3,5 @@ module.exports = {
mode: "jit", mode: "jit",
darkMode: "class", darkMode: "class",
content: ["./src/**/*.tsx"], content: ["./src/**/*.tsx"],
plugins: [ plugins: [require("@tailwindcss/forms"), require("@tailwindcss/typography")]
require('@tailwindcss/forms'),
require('@tailwindcss/typography')
]
} }

View File

@ -1,11 +1,14 @@
{ {
"extends": "plasmo/templates/tsconfig.base", "extends": "./.wxt/tsconfig.json",
"exclude": ["node_modules"],
"include": [".plasmo/index.d.ts", "./**/*.ts", "./**/*.tsx"],
"compilerOptions": { "compilerOptions": {
"paths": { "noEmit": true,
"~*": ["./src/*"] "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