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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|