1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @ -40,3 +40,4 @@ keys.json | |||||||
| 
 | 
 | ||||||
| # typescript | # typescript | ||||||
| .tsbuildinfo | .tsbuildinfo | ||||||
|  | .wxt | ||||||
							
								
								
									
										94
									
								
								CONTRIBUTING.md
									
									
									
									
									
										Normal 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! | ||||||
							
								
								
									
										55
									
								
								package.json
									
									
									
									
									
								
							
							
						
						| @ -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" |  | ||||||
|     ] |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB | 
							
								
								
									
										52
									
								
								src/assets/locale/en/common.json
									
									
									
									
									
										Normal 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" | ||||||
|  | } | ||||||
							
								
								
									
										12
									
								
								src/assets/locale/en/option.json
									
									
									
									
									
										Normal 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" | ||||||
|  | } | ||||||
							
								
								
									
										27
									
								
								src/assets/locale/en/playground.json
									
									
									
									
									
										Normal 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" | ||||||
|  | } | ||||||
							
								
								
									
										207
									
								
								src/assets/locale/en/settings.json
									
									
									
									
									
										Normal 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" | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										5
									
								
								src/assets/locale/en/sidepanel.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,5 @@ | |||||||
|  | { | ||||||
|  |     "tooltip": { | ||||||
|  |         "embed": "It may take a few minutes to embed the page. Please wait..." | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										52
									
								
								src/assets/locale/ml/common.json
									
									
									
									
									
										Normal 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": "നിലവിലെ പേജിനുമായി ചാറ്റ് ചെയ്യുക" | ||||||
|  | } | ||||||
							
								
								
									
										12
									
								
								src/assets/locale/ml/option.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,12 @@ | |||||||
|  | { | ||||||
|  |     "newChat": "പുതിയ ചാറ്റ്", | ||||||
|  |     "selectAPrompt": "ഒരു പ്രോംപ്റ്റ് തിരഞ്ഞെടുക്കുക", | ||||||
|  |     "githubRepository": "ഗിറ്റ്ഹബ് റെപ്പോസിറ്ററി", | ||||||
|  |     "settings": "ക്രമീകരണങ്ങള്", | ||||||
|  |     "sidebarTitle": "ചാറ്റ് ചരിത്രം", | ||||||
|  |     "error": "പിശക്", | ||||||
|  |     "somethingWentWrong": "എന്തോ തെറ്റായി", | ||||||
|  |     "deleteHistoryConfirmation": "നിങ്ങളുടെ ചാറ്റ് ചരിത്രം ഇല്ലാതാക്കണമെന്ന് തീർച്ചയാണോ?", | ||||||
|  |     "editHistoryTitle": "ചാറ്റ് title എഡിറ്റുചെയ്യുക", | ||||||
|  |     "validationSelectModel": "തുടരുന്നതിന് ഒരു മോഡല് തിരഞ്ഞെടുക്കുക" | ||||||
|  | } | ||||||
							
								
								
									
										27
									
								
								src/assets/locale/ml/playground.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,27 @@ | |||||||
|  | { | ||||||
|  |     "ollamaState": { | ||||||
|  |         "searching": "നിങ്ങളുടെ ഒല്ലാമയ്ക്കായി തിരയുന്നു 🦙", | ||||||
|  |         "running": "ഒല്ലാമ പ്രവര്ത്തിക്കുന്നു 🦙", | ||||||
|  |         "notRunning": "ഒല്ലാമയുമായി ബന്ധിപ്പിക്കാന് കഴിയുന്നില്ല 🦙" | ||||||
|  |     }, | ||||||
|  |     "formError": { | ||||||
|  |         "noModel": "ദയവായി ഒരു മോഡല് തിരഞ്ഞെടുക്കുക", | ||||||
|  |         "noEmbeddingModel": "ക്രമീകരണങ്ങള് > ഒല്ലാമ പേജിലുള്ള എംബെഡിംഗ് മോഡല് സജ്ജീകരിക്കുക" | ||||||
|  |     }, | ||||||
|  |     "form": { | ||||||
|  |         "textarea": { | ||||||
|  |             "placeholder": "ഒരു സന്ദേശം ടൈപ്പ് ചെയ്യുക..." | ||||||
|  |         }, | ||||||
|  |         "webSearch": { | ||||||
|  |             "on": "ഓണ്", | ||||||
|  |             "off": "ഓഫ്" | ||||||
|  |         } | ||||||
|  |     }, | ||||||
|  |     "tooltip": { | ||||||
|  |         "searchInternet": "ഇന്റര്നെറ്റ് തിരയുക", | ||||||
|  |         "speechToText": "സംഭാഷണം ടെക്സ്റ്റായി", | ||||||
|  |         "uploadImage": "ഇമേജ് അപ്ലോഡ് ചെയ്യുക", | ||||||
|  |         "stopStreaming": "സ്ട്രീമിംഗ് നിർത്തുക" | ||||||
|  |     }, | ||||||
|  |     "sendWhenEnter": "എന്റര് അമര്ത്തുമ്പോള് അയയ്ക്കുക" | ||||||
|  | } | ||||||
							
								
								
									
										207
									
								
								src/assets/locale/ml/settings.json
									
									
									
									
									
										Normal 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": "നിങ്ങളുടെ വെബ് തിരയല് തുടര്പ്രോംപ്റ്റ്" | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										6
									
								
								src/assets/locale/ml/sidepanel.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,6 @@ | |||||||
|  | { | ||||||
|  |     "tooltip": { | ||||||
|  |         "embed": "പേജ് പ്രോസസ്സ് ചെയ്യുന്നതിന് കുറച്ച് മിനിറ്റുകൾ എടുത്തേക്കാം. കാത്തിരിക്കൂ.." | ||||||
|  |     } | ||||||
|  |      | ||||||
|  | } | ||||||
| @ -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 |  | ||||||
|       }) |  | ||||||
|     }) |  | ||||||
|   } |  | ||||||
| }) |  | ||||||
| @ -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; | ||||||
|  | |||||||
| @ -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 ? ( | ||||||
|  | |||||||
| @ -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> | ||||||
|  | |||||||
| @ -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"> | ||||||
|  | |||||||
| @ -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> | ||||||
|   ) |   ) | ||||||
| } | } | ||||||
|  | |||||||
| @ -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> | ||||||
|   ) |   ) | ||||||
| } | } | ||||||
|  | |||||||
| @ -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> | ||||||
|  | |||||||
| @ -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> | ||||||
|   ) |   ) | ||||||
|  | |||||||
| @ -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} | ||||||
|               /> |               /> | ||||||
|  | |||||||
| @ -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> | ||||||
|  | |||||||
| @ -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 { | ||||||
|  | |||||||
| @ -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> | ||||||
|           ) |           ) | ||||||
|  | |||||||
| @ -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 { | ||||||
| @ -117,7 +119,7 @@ export const PlaygroundForm = ({ dropedFile }: Props) => { | |||||||
|   }) |   }) | ||||||
| 
 | 
 | ||||||
|   const handleKeyDown = (e: React.KeyboardEvent) => { |   const handleKeyDown = (e: React.KeyboardEvent) => { | ||||||
|     if (e.key === "Process" || e.key === "229" ) return |     if (e.key === "Process" || e.key === "229") return | ||||||
|     if ( |     if ( | ||||||
|       !typing && |       !typing && | ||||||
|       e.key === "Enter" && |       e.key === "Enter" && | ||||||
| @ -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} | ||||||
|  | |||||||
| @ -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> | ||||||
|   ) |   ) | ||||||
|  | |||||||
| @ -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> | ||||||
|  | |||||||
| @ -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,18 +10,21 @@ 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 () => { | ||||||
|       const [ollamaURL, allModels, chunkOverlap, chunkSize, defaultEM] = |       const [ollamaURL, allModels, chunkOverlap, chunkSize, defaultEM] = | ||||||
|         await Promise.all([ |         await Promise.all([ | ||||||
|           getOllamaURL(), |           getOllamaURL(), | ||||||
|           getAllModels({returnEmpty: true}), |           getAllModels({ returnEmpty: true }), | ||||||
|           defaultEmbeddingChunkOverlap(), |           defaultEmbeddingChunkOverlap(), | ||||||
|           defaultEmbeddingChunkSize(), |           defaultEmbeddingChunkSize(), | ||||||
|           defaultEmbeddingModelForRag() |           defaultEmbeddingModelForRag() | ||||||
| @ -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 |                 <InputNumber | ||||||
|                   style={{ width: "100%" }} |                   style={{ width: "100%" }} | ||||||
|                   placeholder="Chunk Size" |                   placeholder={t("ollamaSettings.settings.ragSettings.chunkSize.placeholder")} | ||||||
|                 /> |                 /> | ||||||
|               </Form.Item> |               </Form.Item> | ||||||
|               <Form.Item |               <Form.Item | ||||||
|                 name="chunkOverlap" |                 name="chunkOverlap" | ||||||
|                 label="Chunk Overlap" |                 label={t("ollamaSettings.settings.ragSettings.chunkOverlap.label")} | ||||||
|                 rules={[ |                 rules={[ | ||||||
|                   { |                   { | ||||||
|                     required: true, |                     required: true, | ||||||
|                     message: "Please input your chunk overlap!" |                     message: t("ollamaSettings.settings.ragSettings.chunkOverlap.required") | ||||||
|                   } |                   } | ||||||
|                 ]}> |                 ]}> | ||||||
|                 <InputNumber |                 <InputNumber | ||||||
|                   style={{ width: "100%" }} |                   style={{ width: "100%" }} | ||||||
|                   placeholder="Chunk Overlap" |                   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> | ||||||
|  | |||||||
| @ -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> | ||||||
|  | |||||||
| @ -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> | ||||||
|  | |||||||
| @ -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 | ||||||
|  | |||||||
| @ -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={{ | ||||||
|                     <a |                       anchor: ( | ||||||
|                       href="https://github.com/n4ze3m/page-assist/blob/main/page-share.md" |                         <a | ||||||
|                       target="__blank" |                           href="https://github.com/n4ze3m/page-assist/blob/main/page-share.md" | ||||||
|                       className="text-blue-600 dark:text-blue-400"> |                           target="__blank" | ||||||
|                       Learn more |                           className="text-blue-600 dark:text-blue-400"></a> | ||||||
|                     </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({ | ||||||
|  | |||||||
| @ -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"> | ||||||
|  | |||||||
| @ -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 = () => { | ||||||
|  | |||||||
| @ -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> | ||||||
|  | |||||||
| @ -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> | ||||||
|  | |||||||
| @ -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} | ||||||
|  | |||||||
| @ -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> | ||||||
|  | |||||||
| @ -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> | ||||||
|   ) |   ) | ||||||
|  | |||||||
| @ -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
									
								
							
							
						
						| @ -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 | ||||||
|  | }) | ||||||
							
								
								
									
										56
									
								
								src/entries/ollama-pull.content.ts
									
									
									
									
									
										Normal 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/*"] | ||||||
|  | }) | ||||||
| @ -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 /> | ||||||
							
								
								
									
										14
									
								
								src/entries/options/index.html
									
									
									
									
									
										Normal 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> | ||||||
							
								
								
									
										10
									
								
								src/entries/options/main.tsx
									
									
									
									
									
										Normal 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>, | ||||||
|  | ); | ||||||
| @ -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 /> | ||||||
							
								
								
									
										14
									
								
								src/entries/sidepanel/index.html
									
									
									
									
									
										Normal 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> | ||||||
							
								
								
									
										9
									
								
								src/entries/sidepanel/main.tsx
									
									
									
									
									
										Normal 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
									
								
							
							
						
						| @ -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 } | ||||||
|  | } | ||||||
| @ -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 } = | ||||||
|  | |||||||
| @ -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
									
								
							
							
						
						| @ -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
									
								
							
							
						
						| @ -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
									
								
							
							
						
						| @ -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 | ||||||
|  | } | ||||||
							
								
								
									
										11
									
								
								src/i18n/support-language.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,11 @@ | |||||||
|  | // Please add new language code to supportLanguage array
 | ||||||
|  | export const supportLanguage = [ | ||||||
|  |     { | ||||||
|  |         label: "English", | ||||||
|  |         value: "en" | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |         label: "മലയാളം", | ||||||
|  |         value: "ml" | ||||||
|  |     } | ||||||
|  | ] | ||||||
| @ -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 | ||||||
|  | |||||||
| @ -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
									
								
							
							
						
						| @ -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 | ||||||
|  | } | ||||||
| @ -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
									
								
							
							
						
						| @ -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), | ||||||
|  |             }), | ||||||
|  |         ]; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -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> |  | ||||||
							
								
								
									
										11
									
								
								src/public/_locales/en/messages.json
									
									
									
									
									
										Normal 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" | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										11
									
								
								src/public/_locales/ml/messages.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,11 @@ | |||||||
|  | { | ||||||
|  |     "extName": { | ||||||
|  |         "message": "പേജ് അസിസ്റ്റ് - ഒള്ളമ മോഡലുകളിക്ക് ഒരു വെബ്യൂഐ" | ||||||
|  |     }, | ||||||
|  |     "extDescription": { | ||||||
|  |         "message": "ഇന്റർനെറ്റ് ബ്രൌസ് ചെയ്യുമ്പോൾ സ്ഥലിപ്പായി പ്രവർത്തിക്കുന്ന എയ് മോഡൽ ഉപയോഗിക്കുക." | ||||||
|  |     }, | ||||||
|  |     "openSidePanelToChat": { | ||||||
|  |         "message": "ചാറ്റ് ചെയ്യാന് സൈഡ് പാനല് തുറക്കുക" | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										
											BIN
										
									
								
								src/public/icon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.9 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/public/icon/128.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.9 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/public/icon/16.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 345 B | 
							
								
								
									
										
											BIN
										
									
								
								src/public/icon/32.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 535 B | 
							
								
								
									
										
											BIN
										
									
								
								src/public/icon/48.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 951 B | 
							
								
								
									
										
											BIN
										
									
								
								src/public/icon/64.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.3 KiB | 
| @ -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" | ||||||
|  | |||||||
| @ -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 ( | ||||||
|  | |||||||
| @ -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 ( | ||||||
|  | |||||||
| @ -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 ( | ||||||
|  | |||||||
| @ -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 ( | ||||||
|  | |||||||
| @ -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 ( | ||||||
|  | |||||||
| @ -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 ( | ||||||
|  | |||||||
| @ -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) | ||||||
|  | |||||||
| @ -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 ( | ||||||
|  | |||||||
| @ -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,31 +62,36 @@ export const isOllamaRunning = async () => { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export const getAllModels = async ({ returnEmpty = false }: { returnEmpty?: boolean }) => { | export const getAllModels = async ({ returnEmpty = false }: { returnEmpty?: boolean }) => { | ||||||
|   const baseUrl = await getOllamaURL() |   try { | ||||||
|   const response = await fetch(`${cleanUrl(baseUrl)}/api/tags`) |     const baseUrl = await getOllamaURL() | ||||||
|   if (!response.ok) { |     const response = await fetch(`${cleanUrl(baseUrl)}/api/tags`) | ||||||
|     if (returnEmpty) { |     if (!response.ok) { | ||||||
|       return [] |       if (returnEmpty) { | ||||||
|  |         return [] | ||||||
|  |       } | ||||||
|  |       throw new Error(response.statusText) | ||||||
|     } |     } | ||||||
|     throw new Error(response.statusText) |     const json = await response.json() | ||||||
|   } |  | ||||||
|   const json = await response.json() |  | ||||||
| 
 | 
 | ||||||
|   return json.models as { |     return json.models as { | ||||||
|     name: string |       name: string | ||||||
|     model: string |       model: string | ||||||
|     modified_at: string |       modified_at: string | ||||||
|     size: number |       size: number | ||||||
|     digest: string |       digest: string | ||||||
|     details: { |       details: { | ||||||
|       parent_model: string |         parent_model: string | ||||||
|       format: string |         format: string | ||||||
|       family: string |         family: string | ||||||
|       families: string[] |         families: string[] | ||||||
|       parameter_size: string |         parameter_size: string | ||||||
|       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
									
								
							
							
						
						| @ -0,0 +1,10 @@ | |||||||
|  | import { ChatHistory } from "@/store" | ||||||
|  | 
 | ||||||
|  | export type BotResponse = { | ||||||
|  |     bot: { | ||||||
|  |         text: string | ||||||
|  |         sourceDocuments: any[] | ||||||
|  |     } | ||||||
|  |     history: ChatHistory | ||||||
|  |     history_id: string | ||||||
|  | } | ||||||
							
								
								
									
										55
									
								
								src/utils/generate-history.ts
									
									
									
									
									
										Normal 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 | ||||||
|  | } | ||||||
							
								
								
									
										63
									
								
								src/utils/memory-embeddings.ts
									
									
									
									
									
										Normal 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 | ||||||
|  | } | ||||||
| @ -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`) | ||||||
|  | |||||||
| @ -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", | ||||||
|  | |||||||
| @ -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) => { | ||||||
|  | |||||||
| @ -1,10 +1,7 @@ | |||||||
| /** @type {import('tailwindcss').Config} */ | /** @type {import('tailwindcss').Config} */ | ||||||
| module.exports = { | 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') |  | ||||||
|     ] |  | ||||||
|   } |  | ||||||
|  | |||||||
| @ -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, | ||||||
|     "baseUrl": "." |     "esModuleInterop": true, | ||||||
|   } |     "jsx": "react-jsx", | ||||||
|  |     "strict": false | ||||||
|  |   }, | ||||||
|  |   "exclude": [ | ||||||
|  |     "node_modules" | ||||||
|  |   ], | ||||||
| } | } | ||||||
							
								
								
									
										58
									
								
								wxt.config.ts
									
									
									
									
									
										Normal 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" | ||||||
|  |     ] | ||||||
|  |   } | ||||||
|  | }) | ||||||