[
  {
    "path": ".github/FUNDING.yml",
    "content": "github: n4ze3m\nko_fi: n4ze3m"
  },
  {
    "path": ".gitignore",
    "content": "\n# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n# settings\n.vscode\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n#cache\n.turbo\n.next\n.vercel\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\n\n\n# local env files\n.env*\n\nout/\nbuild/\ndist/\n\n# plasmo - https://www.plasmo.com\n.plasmo\n\n# bpp - http://bpp.browser.market/\nkeys.json\n\n# typescript\n.tsbuildinfo\n# WXT\n.wxt\n# WebStorm\n.idea \n\n/docs/.vitepress/cache/\npackage-lock.json"
  },
  {
    "path": ".prettierrc.cjs",
    "content": "/**\n * @type {import('prettier').Options}\n */\nmodule.exports = {\n  printWidth: 80,\n  tabWidth: 2,\n  useTabs: false,\n  semi: false,\n  singleQuote: false,\n  trailingComma: \"none\",\n  bracketSpacing: true,\n  bracketSameLine: true,\n  plugins: [require.resolve(\"@plasmohq/prettier-plugin-sort-imports\")],\n  importOrder: [\"^@plasmohq/(.*)$\", \"^~(.*)$\", \"^[./]\"],\n  importOrderSeparation: true,\n  importOrderSortSpecifiers: true\n}\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to Page Assist\n\nThank 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.\n\n## Getting Started\n\n1. **Fork the repository**\n\n   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.\n\n2. **Clone your forked repository**\n\n   Once you have your own fork, clone it to your local machine:\n\n   ```\n   git clone https://github.com/YOUR-USERNAME/page-assist.git\n   ```\n\n3. **Install dependencies**\n\n   Page Assist uses [Bun](https://bun.sh/) for dependency management. Install the required dependencies by running the following command in the project root directory:\n\n   ```\n   bun install\n   ```\n\n   If you face any issues with Bun, you can use `npm` instead.\n\n4. **Start the development server**\n\n   To run the extension in development mode, use the following command:\n\n   ```\n   bun dev\n   ```\n\n   This will open a  chrome browser window with the extension loaded.\n\n   for firefox:\n\n   ```\n   bun dev:firefox\n   ```\n\n5. **Install Ollama locally**\n\n   Page Assist requires [Ollama](https://ollama.ai) to be installed locally. Follow the installation instructions provided in the Ollama repository.\n\n## Making Changes\n\nOnce you have the project set up locally, you can start making changes. We recommend creating a new branch for your changes:\n\n```\ngit checkout -b my-feature-branch\n```\n\nMake your desired changes, and don't forget to add or update tests if necessary.\n\n## Submitting a Pull Request\n\n1. **Commit your changes**\n\n   Once you've made your changes, commit them with a descriptive commit message:\n\n   ```\n   git commit -m \"Add a brief description of your changes\"\n   ```\n\n2. **Push your changes**\n\n   Push your changes to your forked repository:\n\n   ```\n   git push origin my-feature-branch\n   ```\n\n3. **Open a Pull Request**\n\n   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.\n\n4. **Describe your changes**\n\n   Provide a clear and concise description of the changes you've made, including any relevant issue numbers or other context.\n\n5. **Review and merge**\n\n   The maintainers of the project will review your pull request and provide feedback or merge it if everything looks good.\n\n## Code Style and Guidelines\n\nTo ensure consistency and maintainability, we follow certain code style guidelines. Please ensure your code adheres to these guidelines before submitting a pull request.\n\n- Use proper indentation and code formatting\n- Write clear and concise comments when necessary\n- Follow best practices for TypeScript and React development\n\n## Need Help?\n\nIf you have any questions or need further assistance, feel free to open an issue or reach out to the maintainers.\n\nThank you for your contribution!\n"
  },
  {
    "path": "LICENCE",
    "content": "MIT License\n\nCopyright (c) 2023 Muhammed Nazeem\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "PRIVACY.md",
    "content": "# Privacy Policy\n\n## Data Collection\nPage Assist is committed to user privacy and does not collect any user data. All operations and data storage occur locally within your browser.\n\n## Browser Permissions\nThe extension requires the following permissions to function:\n\n- **Notifications**: To provide system notifications\n- **Website Content Access**: Required for the chat-with-webpage functionality\n- **Unlimited Storage**: Used to store chat history locally in your browser\n- **Active Tab**: To ensure the extension is active on the current tab and get screenshots etc\n- **Scripting**: For the chat-with-webpage functionality\n- **Web Requests**: To modify headers of the local server to avoid CORS issues\n\n## Page Share Feature\nWhen using the Page Share feature:\n\n- Data sharing only occurs when explicitly connecting to external sources\n- Self-hosting option is available for complete data control\n- Shared chats can be permanently deleted from the server at any time\n- No data is retained after deletion\n\n## Data Storage\n- All chat history and settings are stored locally in your browser\n- No data is transmitted to external servers unless explicitly initiated by the user\n- Users maintain full control over their data\n\n## Third-Party Services\nPage Assist does not integrate with any third-party analytics or tracking services.\n\n## Changes to Privacy Policy\nWe reserve the right to update this privacy policy as needed. Users will be notified of any significant changes.\n\n## Contact\nFor privacy-related questions or concerns, please open an issue on our GitHub repository or mail me at me@n4ze3m.com"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n    <h1 align=\"center\">Page Assist</h1>\n</p>\n\n\n<p align=\"center\">\n<a href=\"https://discord.gg/bu54382uBd\" aria-label=\"Join dialoqbase #welcome\"><img src=\"https://img.shields.io/badge/discord-join%20chat-blue.svg\" alt=\"Join dialoqbase #welcome\"></a>  <a href=\"https://twitter.com/page_assist\" aria-label=\"Follow @page_assist on Twitter\"><img src=\"https://img.shields.io/twitter/follow/page_assist?style=social\" alt=\"Follow @page_assist on Twitter\"></a> \n</p>\n\n<p align=\"center\">\n    <a href=\"https://docs.pageassist.xyz\">\n        Documentation\n    </a>\n\n</p>\n\n\nPage Assist is an open-source browser extension that provides a sidebar and web UI for your local AI model. It allows you to interact with your model from any webpage.\n\n## Installation\n\nPage Assist supports Chromium-based browsers like Chrome, Brave, and Edge, as well as Firefox.\n\n[![Chrome Web Store](https://pub-35424b4473484be483c0afa08c69e7da.r2.dev/UV4C4ybeBTsZt43U4xis.png)](https://chromewebstore.google.com/detail/page-assist/jfgfiigpkhlkbnfnbobbkinehhfdhndo)\n[![Firefox Add-on](https://pub-35424b4473484be483c0afa08c69e7da.r2.dev/get-the-addon.png)](https://addons.mozilla.org/en-US/firefox/addon/page-assist/)\n[![Edge Add-on](https://pub-35424b4473484be483c0afa08c69e7da.r2.dev/edge-addon.png)](https://microsoftedge.microsoft.com/addons/detail/page-assist-a-web-ui-fo/ogkogooadflifpmmidmhjedogicnhooa)\n\nCheckout the Demo (v1.0.0):\n\n<div align=\"center\">\n\n[![Page Assist Demo](https://img.youtube.com/vi/8VTjlLGXA4s/0.jpg)](https://www.youtube.com/watch?v=8VTjlLGXA4s)\n\n</div>\n\n## Features\n\n- **Sidebar**: A sidebar that can be opened on any webpage. It allows you to interact with your model and see the results.\n\n- **Web UI**: A web UI that allows you to interact with your model like a ChatGPT Website.\n\n- **Chat With Webpage**: You can chat with the webpage and ask questions about the content.\n\nwant more features? Create an issue and let me know.\n\n### Manual Installation\n\n#### Pre-requisites\n\n- Bun - [Installation Guide](https://bun.sh/)\n- Ollama (Local AI Provider) - [Installation Guide](https://ollama.com)\n- Any OpenAI API Compatible Endpoint (like LM Studio, llamafile etc.)\n\n1. Clone the repository\n\n```bash\ngit clone https://github.com/n4ze3m/page-assist.git\ncd page-assist\n```\n\n2. Install the dependencies\n\n```bash\nbun install\n```\n\n3. Build the extension (by default it will build for Chrome, Edge and Firefox)\n\n```bash\nbun run build\n```\n\n_Note: If you face any issues with Bun, use `npm` instead of `bun`._\n\n4. Load the extension (chrome)\n\n- Open the Extension Management page by navigating to `chrome://extensions`.\n\n- Enable Developer Mode by clicking the toggle switch next to Developer mode.\n\n- Click the `Load unpacked` button and select the `build` directory.\n\n5. Load the extension (firefox)\n\n- Open the Add-ons page by navigating to `about:addons`.\n- Click the `Extensions` tab.\n- Click the `Manage Your Extensions` button.\n- Click the `Load Temporary Add-on` button and select the `manifest.json` file from the `build` directory.\n\n## Usage\n\n### Sidebar\n\nOnce the extension is installed, you can open the sidebar via context menu or keyboard shortcut.\n\nDefault Keyboard Shortcut: `Ctrl+Shift+Y`\n\n### Web UI\n\nYou can open the Web UI by clicking on the extension icon which will open a new tab with the Web UI.\n\nDefault Keyboard Shortcut: `Ctrl+Shift+L`\n\nNote: You can change the keyboard shortcuts from the extension settings on the Chrome Extension Management page.\n\n## Keyboard Shortcuts\n\nPage Assist supports various keyboard shortcuts to enhance your productivity:\n\n### Extension Shortcuts\n\n| Action | Shortcut | Description |\n|--------|----------|-------------|\n| Open Sidebar | `Ctrl+Shift+Y` | Opens the sidebar on any webpage |\n| Open Web UI | `Ctrl+Shift+L` | Opens the Web UI in a new tab |\n\n**Note**: You can customize extension shortcuts from your browser's extension management page .\n\n### Application Shortcuts\n\n| Action | Shortcut | Description |\n|--------|----------|-------------|\n| New Chat | `Ctrl+Shift+O` | Starts a new chat conversation |\n| Toggle Sidebar | `Ctrl+B` | Opens/closes the chat history sidebar |\n| Focus Textarea | `Shift+Esc` | Focuses the message input field |\n| Toggle Chat Mode | `Ctrl+E` | Toggles between normal chat and chat with current page |\n\n\n\n## Development\n\nYou can run the extension in development mode to make changes and test them.\n\n```bash\nbun dev\n```\n\nThis will start a development server and watch for changes in the source files. You can load the extension in your browser and test the changes.\n\n## Browser Support\n\n| Browser     | Sidebar | Chat With Webpage | Web UI |\n| ----------- | ------- | ----------------- | ------ |\n| Chrome      | ✅      | ✅                | ✅     |\n| Brave       | ✅      | ✅                | ✅     |\n| Firefox     | ✅      | ✅                | ✅     |\n| Vivaldi     | ✅      | ✅                | ✅     |\n| Edge        | ✅      | ✅                | ✅     |\n| LibreWolf   | ✅      | ✅                | ✅     |\n| Zen Browser | ✅      | ✅                | ✅     |\n| Opera       | ❌      | ❌                | ✅     |\n| Arc         | ❌      | ❌                | ✅     |\n\n## Local AI Provider\n\n- [Ollama](https://github.com/ollama/ollama)\n\n- Chrome AI (Gemini Nano)\n\n- OpenAI API Compatible endpoints (like LM Studio, llamafile etc.)\n\n## Roadmap\n\n- [x] Firefox Support\n- [x] More Local AI Providers\n- [ ] More Customization Options\n- [ ] Better UI/UX\n\n## Privacy\n\nPage Assist does not collect any personal data. The only time the extension communicates with the server is when you are using the share feature, which can be disabled from the settings.\n\nAll the data is stored locally in the browser storage. You can view the source code and verify it yourself.\n\nYou learn more about the privacy policy [here](PRIVACY.md).\n\n## Contributing\n\nContributions are welcome. If you have any feature requests, bug reports, or questions, feel free to create an issue.\n\n## Support\n\nIf you like the project and want to support it, you can buy me a coffee. It will help me to keep working on the project.\n\n<a href='https://ko-fi.com/M4M3EMCLL' target='_blank'><img height='36' style='border:0px;height:36px;' src='https://storage.ko-fi.com/cdn/kofi2.png?v=3' border='0' alt='Buy Me a Coffee at ko-fi.com' /></a>\n\nor you can sponsor me on GitHub.\n\n## Blogs and Videos About Page Assist\n\nThis are some of the blogs and videos about Page Assist. If you have written a blog or made a video about Page Assist, feel free to create a PR and add it here.\n\n- [OllamaをChromeAddonのPage Assistで簡単操作](https://note.com/lucas_san/n/nf00d01a02c3a) by [LucasChatGPT](https://twitter.com/LucasChatGPT)\n\n- [This Chrome Extension Surprised Me](https://www.youtube.com/watch?v=IvLTlDy9G8c) by [Matt Williams](https://www.youtube.com/@technovangelist)\n\n- [Ollama With 1 Click](https://www.youtube.com/watch?v=61uN5jtj2wo) by [Yaron Been From EcomXFactor](https://www.youtube.com/@ecomxfactor-YaronBeen)\n\n- [Page Assist 介绍合集](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=Mzk2NDUxNDQ3Nw==&action=getalbum&album_id=3845692786608553984#wechat_redirect) by 百工智用公众号\n\n\n- [Eine KI auf dem eigenen Rechner laufen lassen, 10 Minuten Installation](https://www.johannesholstein.de/gsCMS/index.php?id=sonstige-video-tutorials) by [Johannes Holstein](https://www.johannesholstein.de)\n\n## License\n\nMIT\n\n## Last but not least\n\nMade in [Alappuzha](https://en.wikipedia.org/wiki/Alappuzha) with ❤️\n"
  },
  {
    "path": "docs/.vitepress/config.mts",
    "content": "import { defineConfig } from 'vitepress'\n\n// https://vitepress.dev/reference/site-config\nexport default defineConfig({\n  title: \"Page Assist\",\n  description: \"Page Assist is an open-source Chrome Extension that provides a Sidebar and Web UI for your Local AI model. It allows you to interact with your model from any webpage.\",\n  lastUpdated: true,\n  themeConfig: {\n    // https://vitepress.dev/reference/default-theme-config\n    search: {\n      provider: \"local\",\n    },\n    editLink: {\n      pattern: \"https://github.com/n4ze3m/page-assist/edit/main/docs/:path\",\n      text: \"Edit this page on GitHub\"\n    },\n    nav: [\n      { text: 'Home', link: '/' },\n    ],\n\n    sidebar: [\n      {\n        text: 'Guide',\n        items: [\n          { text: 'Welcome to Page Assist', link: '/' },\n          {\n            text: \"Browser Support\",\n            link: \"/browser-support\"\n          },\n          {\n            text: \"Keyboard Shortcuts\",\n            link: \"/shortcuts\"\n          }\n        ],\n      },\n      {\n        text: \"Sidebar\",\n        items: [\n          {\n            text: \"Sidebar Settings\",\n            link: \"/sidebar\"\n          },\n          {\n            text: \"Sidebar Copilot\",\n            link: \"/sidebar/copilot\"\n          },\n          {\n            text: \"Chat With Website\",\n            link: \"/sidebar/chat-with-website\"\n          },\n          {\n            text: \"Sidebar Vision (🧪)\",\n            link: \"/sidebar/vision\"\n          }\n        ],\n      },\n      {\n        text: \"Features\",\n        items: [\n          {\n            text: \"Internet Search\",\n            link: \"/features/internet-search\"\n          },\n          {\n            text: \"Prompts\",\n            link: \"/features/prompts\"\n          },\n          {\n            text: \"Knowledge Base\",\n            link: \"/features/knowledge-base\"\n          },\n          {\n            text: \"Page Share\",\n            link: \"/features/page-share\"\n          },\n          {\n            text: \"MCP\",\n            link: \"/features/mcp\"\n          },\n          {\n            text: \"Ollama\",\n            link: \"/features/ollama\"\n          },\n          {\n            text: \"Other\",\n            link: \"/features/other\"\n          }\n        ]\n      },\n      {\n        text: \"Providers\",\n        collapsed: true,\n        items: [\n          {\n            text: \"Ollama\",\n            link: \"/providers/ollama\"\n          },\n          {\n            text: \"LM Studio\",\n            link: \"/providers/lmstudio\"\n          },\n          {\n            text: \"OpenAI Compatible API\",\n            link: \"/providers/openai\"\n          }\n        ]\n      },\n      {\n        text: \"Troubleshooting\",\n        items: [\n          {\n            text: \"Ollama Connection Issue\",\n            link: \"/connection-issue\"\n          },\n          {\n            text: \"Extensions Causing Issue with Other Websites\",\n            link: \"/extensions-causing-issue-other-websites\"\n          }\n        ]\n      }],\n\n    socialLinks: [\n      { icon: 'github', link: 'https://github.com/n4ze3m/page-assist' },\n      { icon: 'x', link: 'https://x.com/page_assist' },\n      { icon: 'discord', link: 'https://discord.gg/bu54382uBd' },\n    ],\n    footer: {\n      message: \"MIT Licensed Open Source Project\",\n      copyright: \"Copyright © 2025 Muhammed Nazeem  & Page Assist Contributors\",\n    },\n  },\n  ignoreDeadLinks: true\n})\n"
  },
  {
    "path": "docs/browser-support.md",
    "content": "# Browser Support\n\nFor the best experience, we recommend using Page Assist with the latest versions of Google Chrome, Microsoft Edge, or Firefox.\n\n\n## Supported Browsers\n\n| Browser     | Sidebar | Chat With Webpage | Web UI |\n| ----------- | ------- | ----------------- | ------ |\n| Chrome      | ✅      | ✅                | ✅     |\n| Brave       | ✅      | ✅                | ✅     |\n| Firefox     | ✅      | ✅                | ✅     |\n| Vivaldi     | ✅      | ✅                | ✅     |\n| Edge        | ✅      | ✅                | ✅     |\n| LibreWolf   | ✅      | ✅                | ✅     |\n| Zen Browser | ✅      | ✅                | ✅     |\n| Opera       | ❌      | ❌                | ✅     |\n| Arc         | ❌      | ❌                | ✅     |\n"
  },
  {
    "path": "docs/connection-issue.md",
    "content": "# Ollama Connection Issues\n\nConnection issues can be caused by a number of reasons. Here are some common issues and how to resolve them on Page Assist. You will see the following error message if there is a connection issue:\n\n### 1. Direct Connection Error\n![Direct connection error](https://image.pageassist.xyz/Screenshot%202024-05-13%20001742.png)\n\n### 2. `403` Error When Sending a Message\n![403 error when sending a message](https://image.pageassist.xyz/Screenshot%202024-05-13%20001940.png)\n\nThis issue because of CORS (Cross-Origin Resource Sharing) issues. Since Page Assist is a browser extension, it needs to communicate with the server through the browser. However, the browser restricts communication between different origins. To resolve this issue, you can try the following solutions:   \n\n## 1. Solutions \n\nSince Ollama has connection issues when directly accessed from the browser extension, Page Assist rewrites the request headers to make it work. However, automatic rewriting of headers only works on `http://127.0.0.1:*` and `http://localhost:*` URLs. To resolve the connection issue, you can try the following solutions:\n\n1. Go to Page Assist and click on the `Settings` icon.\n\n2. Click on the `Ollama Settings` tab.\n\n3. There you will see the `Advance Ollama URL Configuration` option. You need to expand it.\n\n![Advance Ollama URL Configuration](https://image.pageassist.xyz/Screenshot%202024-05-13%20003123.png)\n\n4. Enable the `Enable or Disable Custom Origin URL` option.\n\n![Enable or Disable Custom Origin URL](https://image.pageassist.xyz/Screenshot%202024-05-13%20003225.png)\n\n:::tip\nIf Ollama is running on a different port, then change the URL in the `Custom Origin URL` field; otherwise, leave it as it is. Do not change the URL to the Ollama server URL like\n:::\n\n5. Make sure click on the `Save` button to save the changes.\n\n_This will resolve the connection issue, and you will be able to use Ollama without any issues on Page Assist_\n\n## 2. Solution\n\nYou can set OLLAMA_ORIGINS=* to allow connections from any origin. Here's how to do it on different operating systems:\n\n### Windows\n1. Open Start menu and search for \"Environment Variables\"\n2. Click \"Edit the system environment variables\"\n3. Click \"Environment Variables\" button\n4. Under \"System Variables\" click \"New\"\n5. Set Variable name: `OLLAMA_ORIGINS` and Variable value: `*`\n6. Click OK to save\n7. Restart Ollama service\n\n\n### MacOS\n\n1. Open Terminal\n2. Run the following command:\n\n```bash\nlaunchctl setenv OLLAMA_ORIGINS \"*\"\n```\n3. Restart Ollama service\n\n### Linux\n1. Open Terminal\n2. Run the following command:\n\n```bash\nexport OLLAMA_ORIGINS=\"*\"\n```\n3. Restart Ollama service\n\n_This will allow connections from any origin. Hopefully, this will resolve the connection issue._\n\n\n\nIf you still face any issues, feel free to contact us [here](https://github.com/n4ze3m/page-assist/issues/new), and we will be happy to help you out.\n"
  },
  {
    "path": "docs/extensions-causing-issue-other-websites.md",
    "content": "# Extensions Causing Issue with Other Websites\n\nSince Page Assist rewrites the Origin header to avoid CORS issues on the Ollama API, this feature causes issues for some users or websites.\n\nCurrent known issues:\n\n- Breaks Intel® Driver & Support Assistant\n- Box Tools Website\n\nFor this reason, we have added a setting to disable the feature.\n\n## How to disable the feature\n\n1. Click on the Page Assist icon in the browser toolbar.\n2. Click on the settings icon.\n3. Click on the \"Ollama Settings\" tab.\n4. Expand the \"Advanced Ollama URL Configuration\"\n5. Turn off the \"Enable or Disable Automatic Ollama CORS Fix\" option.\n6. Click on the \"Save\" button.\n\n![image](https://pub-35424b4473484be483c0afa08c69e7da.r2.dev/Screenshot%202025-02-17%20185214.png)\n\nThis will disable the feature and prevent Page Assist from rewriting the Origin header.\n\nHowever, your Ollama may start throwing 403 errors. To fix that, you need to add the following line to your Ollama config file.\n\n## How to fix 403 error\n\nYou can set OLLAMA_ORIGIN=* to allow connections from any origin. Here's how to do it on different operating systems:\n\n### Windows\n1. Open Start menu and search for \"Environment Variables\"\n2. Click \"Edit the system environment variables\"\n3. Click \"Environment Variables\" button\n4. Under \"System Variables\" click \"New\"\n5. Set Variable name: `OLLAMA_ORIGIN` and Variable value: `*`\n6. Click OK to save\n7. Restart Ollama service\n\n### MacOS\n\n1. Open Terminal\n2. Run the following command:\n\n```bash\nlaunchctl setenv OLLAMA_ORIGIN \"*\"\n```\n\n3. Restart Ollama service\n\n### Linux\n1. Open Terminal\n2. Run the following command:\n\n```bash\nexport OLLAMA_ORIGIN=\"*\"\n```\n\n3. Restart Ollama service\n\nFor Linux systems using systemd, you can also add the environment variable to your service file. Here's an example of a systemd unit file (credit: Axel Schwarzer):\n\n```bash\n[Unit]\nDescription=Ollama Service\nAfter=network-online.target\n\n[Service]\n#  - see docker.serice for an example\n#\n# EnvironmentFile=/etc/sysconfig/ollama\nEnvironment=\"OLLAMA_HOST=192.168.4.67:11434\"\nEnvironment=\"OLLAMA_MAX_LOADED_MODELS=4\"\n# Environment=\"OLLAMA_ORIGINS=*\"\nExecStart=/usr/local/bin/ollama serve\nUser=ollama\nGroup=ollama\nRestart=always\nRestartSec=3\nEnvironment=\"PATH=/usr/local/sbin:/sbin:/usr/sbin:/root/bin:/usr/local/bin:/bin:/usr/bin:\"\n\n[Install]\nWantedBy=default.target\n```\n\nTo use this configuration, uncomment the `Environment=\"OLLAMA_ORIGINS=*\"` line.\n\n_This will allow connections from any origin. Hopefully, this will resolve the connection issue._\n"
  },
  {
    "path": "docs/features/internet-search.md",
    "content": "# Internet Search\n\nPage Assist supports internet search which can be used with your LLM. It works similarly to ChatGPT's internet search.\n\n## Supported Search Engines\n\n- Google (with region support)\n- DuckDuckGo\n- Sogou\n- Baidu\n- Brave\n- Searxng\n- Brave Search API\n- Tavily Search API\n- Bing\n- Stract\n- Startpage\n- Exa\n- Firecrawl\n- Ollama Web search\n- Kagi Search API (Private Beta - requires API access)\n- Perplexity Search API\n\n## How to use Internet Search\n\nBoth Sidebar and Web UI support internet search. You can use it by toggling the switch on the right side with the globe icon.\n\n![Internet Search](https://pub-35424b4473484be483c0afa08c69e7da.r2.dev/Screenshot%202025-02-19%20203546.png)\n\n## Update Search Prompt\n\nYou can update the search prompt by going to Settings > RAG Settings. Scroll down and you will find the option `Configure RAG Prompt`. Select the `Web` tab and update the prompt.\n\n![Update Search Prompt](https://pub-35424b4473484be483c0afa08c69e7da.r2.dev/Screenshot%202025-02-19%20204314.png)\n\n- `{search_results}` - This will be replaced with search results. (do not remove this)\n- `{current_date_time}` - This will be replaced with current date and time.\n- `{query}` - This will be replaced with the search query.\n\n## Visit websites from messages\n\nThis feature is enabled by default. If you want to disable it, you can do it from the settings.\n\n### How it works?\n\nWhen you enable internet search and input a webpage URL into the input box and send it, Page Assist will visit the website and extract the text from it. Then it will send the text to the LLM.\n\n## Deep Search Mode\n\nBy default, `Perform Simple Internet Search` is enabled. If you want to use Deep Search Mode, you need to disable it.\n\nDeep Search Mode will visit the website and extract the text from it. Then it will send the text to the LLM.\n\n::: warning\nThe current Deep Search is not similar to ChatGPT's DeepSearch. It is a very basic implementation.\n:::\n\n\n## Enable Internet Search by Default\n\nYou can enable Internet Search by default by following these steps:\n\n1. Go to Settings\n2. Under the `General Settings` section\n3. Scroll down to `Manage Web Search`\n4. Enable `Internet Search ON by default`\n5. Click on `Save Settings`"
  },
  {
    "path": "docs/features/knowledge-base.md",
    "content": "# Knowledge Base\n\nPage Assist supports Knowledge Base which is useful for chatting with your own data. You can use it to chat with your own data.\n\n::: warning\nUse this feature with caution. Due to no server-side storage, the data will be processed and embeddings will be stored in browser storage. This may cause performance issues.\n:::\n\n## Supported File Types\n\n- PDF\n- Docx\n- Txt\n- CSV\n- MD\n\n## How to use Knowledge Base  \n\nIn order to use knowledge base, you need to set an embedding model from the RAG Settings. We recommend using `nomic-embed-text` or any embedding model that supports text. Do not use text to text model for this. \n\n1. Go to Settings\n2. Go to Manage Knowledge\n![Knowledge Base](https://pub-35424b4473484be483c0afa08c69e7da.r2.dev/Screenshot%202025-02-19%20210054.png)\n3. You can upload your files by clicking `Add New Knowledge`\n\nIt will take some time to process the files. Once it is done, when you check the input box, you will see a block icon which is knowledge base. You can click on it and select the knowledge you want to use.\n\n![Knowledge Base](https://pub-35424b4473484be483c0afa08c69e7da.r2.dev/Screenshot%202025-02-19%20210300.png)"
  },
  {
    "path": "docs/features/mcp.md",
    "content": "# MCP (Model Context Protocol)\n\nPage Assist supports MCP which allows your LLM to use external tools like search, databases, and more. You can connect to any MCP server that supports Streamable HTTP or SSE transport.\n\n## Supported Transport Types\n\n- Streamable HTTP\n\n## Connecting to a Remote MCP Server\n\n1. Go to Settings\n2. Go to `MCP Settings`\n3. Click `Add MCP Server`\n4. Enter a name and the server URL\n5. Select Auth Type (None, Bearer Token, or OAuth 2.1)\n6. Click `Save`\n\nOnce added, the server tools will be automatically fetched and cached.\n\n## Using STDIO MCP Servers\n\nPage Assist is a browser extension, so it can't run STDIO-based MCP servers directly. You can use [supergateway](https://github.com/supercorp-ai/supergateway) to convert any STDIO MCP server to HTTP.\n\nFor example, to use Playwright MCP:\n\n```bash\nnpx -y supergateway --stdio \"npx @playwright/mcp@latest\" --port 8808 --cors --outputTransport streamableHttp\n```\n\nThen add `http://localhost:8808/mcp` as the server URL in MCP Settings.\n\n## Authentication\n\nPage Assist supports two authentication methods for MCP servers.\n\n### API Key / Bearer Token\n\nIf your MCP server requires an API key or bearer token:\n\n1. Go to MCP Settings\n2. Click `Add Custom Server`\n3. Select `Bearer Token` as the auth type\n4. Enter your token\n5. Click `Save`\n\nThe token will be sent as `Authorization: Bearer <token>` with every request.\n\n### OAuth 2.1\n\nSome MCP servers (like Notion) require OAuth 2.1 authorization. Page Assist supports this using your Page Share URL as the OAuth redirect endpoint.\n\n#### Setup\n\n1. Make sure you have a Page Share URL configured (go to Settings > Manage Share)\n2. Go to MCP Settings\n3. Click `Add Custom Server`\n4. Enter the server name and URL (e.g. `https://mcp.notion.com/mcp`)\n5. Select `OAuth 2.1` as the auth type\n6. Click `Save`\n7. Click the key icon in the actions column to start the OAuth flow\n8. Complete the authorization in the browser tab that opens\n\n::: tip\nPage Assist does not log or store any OAuth data on the server. The Page Share app only serves as a redirect endpoint. All tokens are stored locally in your browser.\n:::\n\n#### Self-Hosting Page Share\n\nIf you prefer not to use the default Page Share server for OAuth redirects, you can self-host it. See the [Page Share](/features/page-share) docs for instructions.\n\nOnce deployed, update your Page Share URL in Settings > Manage Share.\n\n## Enable/Disable Servers Per Chat\n\nYou can temporarily enable or disable MCP servers per chat using the MCP icon button in the chat input. This lets you control which tools are available without changing global settings.\n\n## Custom Headers\n\nIf your MCP server requires custom headers, you can add them when creating or editing a server in MCP Settings.\n"
  },
  {
    "path": "docs/features/ollama.md",
    "content": "# Ollama\n\nPage Assist is designed to work with Ollama. Following are a few things you can do with Page Assist for your Ollama instance.\n\n## Manage Ollama Models\n\nYou can manage Ollama models from the `Manage Models` section in the settings.\n\n1. Go to `Settings`\n2. Go to `Manage Models`\n3. You will see all the models you have pulled\n4. You can delete models from there\n\n![Manage Models](https://pub-35424b4473484be483c0afa08c69e7da.r2.dev/Screenshot%202025-02-19%20230330.png)\n\n## Pull Ollama Models\n\nYou can pull models for Ollama without going to terminal using three methods.\n\n### Method 1: From Web UI\n\n1. Go to `Settings`\n2. Go to `Manage Models`\n3. Click on the `Add New Model` button\n4. Add the model name and click on `Pull Model`\n\n![Pull Model](https://pub-35424b4473484be483c0afa08c69e7da.r2.dev/Screenshot%202025-02-19%20225356.png)\n\n### Method 2: From Ollama.com\n\nWhen you are browsing Ollama.com, you can pull models directly from the website.\n\n1. Go to Ollama.com\n2. Go to any model page\n3. There will be a Pull icon beside the copy icon\n4. Once you click on it, it will ask for confirmation\n5. Download progress can be seen in the Page Assist icon\n\n![Pull Model](https://pub-35424b4473484be483c0afa08c69e7da.r2.dev/brave_vczba7pnUo.gif)\n\n### Method 3: From huggingface.com\n\nYou can pull `gguf` models from huggingface.com.\n\n1. Go to huggingface.com\n2. Go to any model page\n3. On the right side there will be `Use this model` and select `Ollama`\n![Pull Model](https://pub-35424b4473484be483c0afa08c69e7da.r2.dev/Screenshot%202025-02-19%20225915.png)\n4. In the Huggingface popup there will be a button `Pull from Page Assist`, click on it\n![Pull Model](https://pub-35424b4473484be483c0afa08c69e7da.r2.dev/Screenshot%202025-02-19%20230049.png)\n5. Rest of the process is same as Method 2\n"
  },
  {
    "path": "docs/features/other.md",
    "content": "# Other Features\n\nThese are small utility features that Page Assist has.\n\n## Wide Mode\n\nFor larger screens, you can enable wide mode from settings.\n\n1. Go to settings\n2. Under the `General Settings` section\n3. Enable `Enable wide screen mode` option\n\n![Wide Mode](https://pub-35424b4473484be483c0afa08c69e7da.r2.dev/Screenshot%202025-02-19%20212707.png)\n\nThanks to [@yz778](https://github.com/yz778) for this feature. \n\n## Restore last used model for previous chats\n\nAs the title says, you can restore the last used model for previous chats. This is useful if you want to use the same model when you switch to an old chat.\n\n1. Go to settings\n2. Under the `General Settings` section\n3. Enable `Restore last used model for previous chats` option\n\n\n## Resume the last chat when opening the Web UI\n\nYou can resume the last chat when opening the Web UI. This is useful if you want to continue the chat from where you left off.\n\n1. Go to settings\n2. Under the `General Settings` section\n3. Enable `Resume the last chat when opening the Web UI` option\n\n## Resume the last chat when opening the SidePanel (Copilot)\n\nYou can resume the last chat when opening the SidePanel (Copilot). This is useful if you want to continue the chat from where you left off.\n\n1. Go to settings\n2. Under the `General Settings` section\n3. Enable `Resume the last chat when opening the SidePanel (Copilot)` option\n\n\n## Generate Title using AI\n\nTitle generation is a feature that generates a title for the chat based on the conversation. This is useful if you want to generate a title for the chat.\n\n1. Go to settings\n2. Under the `General Settings` section\n3. Enable `Generate Title using AI` option\n"
  },
  {
    "path": "docs/features/page-share.md",
    "content": "# Page Share \n\nPage Share is a feature that allows you to share your chat with others like the share feature of ChatGPT. This feature interacts with the internet by default, and you can use the page assist server to share your chat.\n\nBut for privacy, it's better to self-host the page share server. You can do this by following the steps below.\n\n\n## Self-Host\n\nYou can self-host Page Share using two methods:\n\n- Railway\n- Docker\n\n### Railway\n\nClick the button below to deploy the code to Railway.\n\n[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/VbiS2Q?referralCode=olbszX)\n\n### Docker\n\n1. Clone the repository\n\n\ngit clone https://github.com/n4ze3m/page-share-app.git\ncd page-share-app\n\n\n2. Run the server\n\n\ndocker-compose up\n\n\n3. Open the app\n\nNavigate to [http://localhost:3000](http://localhost:3000) in your browser.\n\n\nOnce you have deployed the server, you can change the Page Share by going to the settings and manage share.\n\n![Page Share](https://pub-35424b4473484be483c0afa08c69e7da.r2.dev/Screenshot%202025-02-19%20210635.png)"
  },
  {
    "path": "docs/features/prompts.md",
    "content": "# Prompts\n\nPage Assist offers three types of prompts to enhance your AI interaction experience:\n\n1. **Custom Prompts** - For use within the chat interface\n2. **Copilot Prompts** - Built-in context menu actions (See [Copilot Prompts](/sidebar/copilot.md))\n3. **Custom Copilot Prompts** - Create your own context menu actions (See [Custom Copilot Prompts](/sidebar/copilot.md#custom-copilot-prompts))\n\n## Custom Prompts\n\nCustom Prompts are prompts that you can create and use within the Page Assist chat interface. These are different from Copilot prompts, which appear in the browser's context menu.\n\nYou can create custom prompts by going to `Settings` → `Manage Prompts` → `Custom` tab.\n\n![Custom Prompts](https://pub-35424b4473484be483c0afa08c69e7da.r2.dev/Screenshot%202025-02-19%20205135.png)\n\nThere are two types of custom prompts:\n\n1. System Prompts\n2. Quick Prompts\n\n\n### System Prompts\n\nSystem prompts will be set as the `system` type of prompt in the LLM. This means that the prompt will be sent to the LLM as a system prompt. This is useful for setting the context of the conversation.\n\n#### Supported System Prompt Variables\n\nYou can use the following variables in your system prompts:\n\n- `{current_date_time}` - The current date and time in local format\n- `{current_year}` - The current year\n- `{current_month}` - The current month (0-11)\n- `{current_day}` - The current day of the month\n- `{current_hour}` - The current hour (0-23)\n- `{current_minute}` - The current minute (0-59)\n\nThese variables will be automatically replaced with their respective values when the prompt is sent to the LLM.\n\n### Quick Prompts\n\nQuick prompts are quick prompts that you can use to quickly send a prompt to the LLM. You can use quick prompts by clicking on the `Quick Prompts` button in the input box.\n\nIf you put variables in the `{}` brackets, they will be selected automatically."
  },
  {
    "path": "docs/index.md",
    "content": "# Welcome to Page Assist\n\nWelcome to Page Assist, the browser companion for your Local AI model! With Page Assist, your web browsing experience enters a new dimension of intelligence and efficiency.\n\n## What is Page Assist?\n\nPage Assist makes AI interaction effortless! Simply:\n\n- Chat with your AI from any webpage using our sleek Sidebar\n- Access the powerful Web UI Control Center\n- Connect to your favorite local AI models\n\n## Installation\n\nDownload for your preferred browser:\n\n[![Chrome Web Store](https://pub-35424b4473484be483c0afa08c69e7da.r2.dev/UV4C4ybeBTsZt43U4xis.png)](https://chromewebstore.google.com/detail/page-assist/jfgfiigpkhlkbnfnbobbkinehhfdhndo)\n\n[![Firefox Add-on](https://pub-35424b4473484be483c0afa08c69e7da.r2.dev/get-the-addon.png)](https://addons.mozilla.org/en-US/firefox/addon/page-assist/)\n\n[![Edge Add-on](https://pub-35424b4473484be483c0afa08c69e7da.r2.dev/edge-addon.png)](https://microsoftedge.microsoft.com/addons/detail/page-assist-a-web-ui-fo/ogkogooadflifpmmidmhjedogicnhooa)\n\n## Privacy\n\nCheeck out our [Privacy Policy](/privacy) to understand how we handle your data.\n\n## Sponsors\n\nPage Assist is an open-source project. If you enjoy our work, please consider supporting us by becoming a sponsor. Your contribution will help us maintain and enhance Page Assist for everyone.\n\nYou can sponsor us on GitHub: [https://github.com/sponsors/n4ze3m](https://github.com/sponsors/n4ze3m)\n\nSupport us on Ko-fi: [https://ko-fi.com/n4ze3m](https://ko-fi.com/n4ze3m)\n\n## Team\n\nPage Assist is maintained by [@n4ze3m](https://x.com/n4ze3m) and our wonderful community of contributors.\n\n![Contributors](https://contrib.rocks/image?repo=n4ze3m/page-assist)"
  },
  {
    "path": "docs/markdown-examples.md",
    "content": "# Markdown Extension Examples\n\nThis page demonstrates some of the built-in markdown extensions provided by VitePress.\n\n## Syntax Highlighting\n\nVitePress provides Syntax Highlighting powered by [Shiki](https://github.com/shikijs/shiki), with additional features like line-highlighting:\n\n**Input**\n\n````md\n```js{4}\nexport default {\n  data () {\n    return {\n      msg: 'Highlighted!'\n    }\n  }\n}\n```\n````\n\n**Output**\n\n```js{4}\nexport default {\n  data () {\n    return {\n      msg: 'Highlighted!'\n    }\n  }\n}\n```\n\n## Custom Containers\n\n**Input**\n\n```md\n::: info\nThis is an info box.\n:::\n\n::: tip\nThis is a tip.\n:::\n\n::: warning\nThis is a warning.\n:::\n\n::: danger\nThis is a dangerous warning.\n:::\n\n::: details\nThis is a details block.\n:::\n```\n\n**Output**\n\n::: info\nThis is an info box.\n:::\n\n::: tip\nThis is a tip.\n:::\n\n::: warning\nThis is a warning.\n:::\n\n::: danger\nThis is a dangerous warning.\n:::\n\n::: details\nThis is a details block.\n:::\n\n## More\n\nCheck out the documentation for the [full list of markdown extensions](https://vitepress.dev/guide/markdown).\n"
  },
  {
    "path": "docs/package.json",
    "content": "{\n  \"name\": \"page-assist-docs\",\n  \"private\": true,\n  \"scripts\": {\n    \"build\": \"vitepress build\",\n    \"dev\": \"vitepress dev\",\n    \"preview\": \"vitepress preview\"\n  },\n  \"devDependencies\": {\n    \"vitepress\": \"^1.6.3\"\n  }\n}\n"
  },
  {
    "path": "docs/postcss.config.js",
    "content": "module.exports = {}\n"
  },
  {
    "path": "docs/privacy.md",
    "content": "# Privacy Policy\n\n## Data Collection\nPage Assist is committed to user privacy and does not collect any user data. All operations and data storage occur locally within your browser.\n\n## Browser Permissions\nThe extension requires the following permissions to function:\n\n- **Notifications**: To provide system notifications\n- **Website Content Access**: Required for the chat-with-webpage functionality\n- **Unlimited Storage**: Used to store chat history locally in your browser\n- **Active Tab**: To ensure the extension is active on the current tab and get screenshots etc\n- **Scripting**: For the chat-with-webpage functionality\n- **Web Requests**: To modify headers of the local server to avoid CORS issues\n\n## Page Share Feature\nWhen using the Page Share feature:\n\n- Data sharing only occurs when explicitly connecting to external sources\n- Self-hosting option is available for complete data control\n- Shared chats can be permanently deleted from the server at any time\n- No data is retained after deletion\n\n## Data Storage\n- All chat history and settings are stored locally in your browser\n- No data is transmitted to external servers unless explicitly initiated by the user\n- Users maintain full control over their data\n\n## Third-Party Services\nPage Assist does not integrate with any third-party analytics or tracking services.\n\n## Changes to Privacy Policy\nWe reserve the right to update this privacy policy as needed. Users will be notified of any significant changes.\n\n## Contact\nFor privacy-related questions or concerns, please open an issue on our GitHub repository or mail me at me@n4ze3m.com"
  },
  {
    "path": "docs/prompt.md",
    "content": "# Prompt \n"
  },
  {
    "path": "docs/providers/llamacpp.md",
    "content": "# LLaMA.cpp\n\nPage Assist supports LLaMA.cpp API endpoints. You can use any LLaMA.cpp API endpoint with Page Assist.\n\n## Adding LLaMA.cpp API\n\n1. Click on the Page Assist icon on the browser toolbar.\n\n2. Click on the `Settings` icon.\n\n3. Go to the `OpenAI Compatible API` tab.\n\n4. Click on the `Add Provider` button.\n\n5. Select `LLaMA.cpp` from the dropdown.\n\n6. Enter the `LLaMA.cpp URL`. (by default it is `http://localhost:8080/v1`)\n\n7. Click on the `Save` button.\n\n\n::: info\nYou don't need to add any models since Page Assist will automatically fetch them from the LLaMA.cpp instance you have configured.\n\nThe model must be loaded in LLaMA.cpp before Page Assist can fetch it.\n:::"
  },
  {
    "path": "docs/providers/lmstudio.md",
    "content": "# LM Studio\n\nYou can add LMStudio's API to Page Assist. Here's how you can do it:\n\n1. Click on the Page Assist icon on the browser toolbar.\n\n2. Click on the `Settings` icon.\n\n3. Go to the `OpenAI Compatible API` tab.\n\n4. Click on the `Add Provider` button.\n\n5. Select `LMStudio` from the dropdown.\n\n6. Enter the `LMStudio URL`. (by default it is `http://localhost:1234/v1`)\n\n7. Click on the `Save` button.\n\n\n::: info\nYou don't need to add any models since Page Assist will automatically fetch them from the LMStudio instance you have configured.\n\nThe model must be loaded in LMStudio before Page Assist can fetch it.\n:::"
  },
  {
    "path": "docs/providers/ollama.md",
    "content": "# Ollama \n\nOllama lets you run large language models locally on your machine.\n\nPage Assist supports Ollama by default. You don't need to configure anything if you are using Ollama on `localhost:11434`. Page Assist will automatically detect it.\n\nIf you face any issues with Ollama, please check the [Ollama Connection Issues](/connection-issue.md) guide.\n\n\n## Multiple Ollama Instances\n\nYou can configure multiple Ollama instances by following these steps:\n\n\n1. Click on the Page Assist icon on the browser toolbar.\n\n2. Click on the `Settings` icon.\n\n3. Go to the `OpenAI Compatible API` tab.\n\n4. Click on the `Add Provider` button.\n\n5. Select `Ollama` from the dropdown.\n\n6. Enter the `Ollama URL`.\n\n7. Click on the `Save` button.\n\nYou don't need to add any models since Page Assist will automatically fetch them from the Ollama instance you have configured."
  },
  {
    "path": "docs/providers/openai.md",
    "content": "# OpenAI Compatible API\n\nPage Assist supports OpenAI Compatible API endpoints. You can use any OpenAI Compatible API endpoint with Page Assist.\n\nBy default, Page Assist supports the following OpenAI Compatible API endpoints:\n\n- LLaMA.cpp\n- LM Studio\n- Llamafile\n- Ollama\n- OpenAI\n- DeepSeek\n- Fireworks\n- Novita AI\n- Hugging Face\n- Groq\n- Together\n- OpenRouter\n- Google AI\n- Mistral\n- Infinigence AI\n- SiliconFlow\n- VolcEngine\n- TencentCloud\n- AlibabaCloud\n- vLLM\n- Moonshot\n- xAI\n- Vercel AI Gateway\n- Chutes\n- Anthropic (Claude)\n- CanopyWave\n- BigModel (Zhipu)\n- MiniMax\n\n\n## Adding OpenAI Compatible API\n\n\n1. Click on the Page Assist icon on the browser toolbar.\n\n2. Click on the `Settings` icon.\n\n3. Go to the `OpenAI Compatible API` tab.\n\n4. Click on the `Add Provider` button.\n\n5. Select the API from the dropdown. In case it is not listed in the default list, select `Custom` and enter the API URL and API Key.\n\n6. Add API key if required.\n\n7. Click on the `Save` button.\n\n\n::: info\nFor Ollama, LM Studio, and Llamafile, you don't need to add any models since Page Assist will automatically fetch them from the API.\n:::"
  },
  {
    "path": "docs/shortcuts.md",
    "content": "# Shortcut Keys\n\nPage Assist supports the following shortcut keys:\n\n| Action       | Shortcut       |\n| ------------ | -------------- |\n| Open Sidebar | `Ctrl+Shift+Y` |\n| Open Web UI  | `Ctrl+Shift+L` |\n\nYou can change the keyboard shortcuts from the extension settings on the Chrome Extension Management page.\n\n## Application Shortcuts\n\n| Action | Shortcut | Description |\n|--------|----------|-------------|\n| New Chat | `Ctrl+Shift+O` | Starts a new chat conversation |\n| Toggle Sidebar | `Ctrl+B` | Opens/closes the chat history sidebar |\n| Focus Textarea | `Shift+Esc` | Focuses the message input field |\n| Toggle Chat Mode | `Ctrl+E` | Toggles between normal chat and chat with current page |\n\n## Changing Keyboard Shortcuts (Browser Specific)\n\nTo change the keyboard shortcuts, follow these steps:\n\n### Chrome\n\n1. Go to the Extension Settings page by navigating to `chrome://extensions/shortcuts`.\n\n2. Find the Page Assist extension.\n\n3. Change the shortcut keys for the desired action.\n\n_*Note*: This works with Chromium-based browsers like Edge, Brave, etc._\n\n### Firefox\n\n1. Go to the Add-ons page by navigating to `about:addons`.\n\n2. Click on the `Settings` icon.\n\n3. Click on the `Manage Extension Shortcuts` button.\n\n![Manage Extension Shortcuts](https://pub-35424b4473484be483c0afa08c69e7da.r2.dev/Screenshot%202025-02-15%20114332.png)\n\n4. Find the Page Assist extension.\n\n5. Change the shortcut keys for the desired action.\n\n_*Note*: This works with Firefox-based browsers like Zen, Librewolf, etc._\n"
  },
  {
    "path": "docs/sidebar/chat-with-website.md",
    "content": "# Sidebar Chat with Website\n\nThis is one of the features of Page Assist that allows you to chat with your AI model from any webpage using the sleek Sidebar.\n\n\n## How to chat with the website\n\n1. Open the Sidebar\n2. Enable the `Chat with Website` option\n3. Start chatting with the current website\n\n\n## How to disable RAG for Chat With Website\n\nBy default, Chat With Website needs to have an embedding model. You can disable the RAG model by following the steps below:\n\n1. Open the Sidebar\n2. Click on the `Settings` icon\n3. There will be an option `Copilot Chat With Website Settings`\n4. By default, `Chat with website using vector embeddings` is enabled. Disable it.\n5. You can increase the `Normal mode website content size` to increase the amount of content that will be sent to the AI model.\n\n![Disable RAG](https://pub-35424b4473484be483c0afa08c69e7da.r2.dev/Screenshot%202025-02-19%20104323.png)\n\nThat's it! Now you can chat with your AI model from any webpage without using RAG.\n\n## Enable Chat with Website by Default\n\nYou can enable Chat with Website by default by following these steps:\n\n1. Open the Sidebar\n2. Click on the `Settings` icon\n3. Look for the option `Enable Chat with Website by default (Copilot)`"
  },
  {
    "path": "docs/sidebar/copilot.md",
    "content": "# Copilot\n\nSidebar Copilot is a feature that allows you to select text from any webpage and perform AI-powered actions on it.\n\n## Built-in Copilot Prompts\n\nPage Assist comes with 5 built-in copilot prompts:\n\n- **Summarize** - Get a concise summary of selected text\n- **Rephrase** - Rewrite text with alternative vocabulary\n- **Translate** - Translate text to English\n- **Explain** - Get a detailed explanation of the text\n- **Custom** - Use a custom prompt template\n\n::: warning Deprecation Notice\nThe built-in \"Custom\" prompt will be removed in a future version. Please migrate to **Custom Copilot Prompts** (see below) for better flexibility and multiple custom prompts.\n:::\n\n### Disabling Built-in Prompts\n\nYou can now disable any built-in copilot prompt to reduce context menu clutter:\n\n1. Go to `Settings` → `Manage Prompts` → `Copilot` tab\n2. Toggle the switch next to any prompt to enable/disable it\n3. Disabled prompts will not appear in the context menu\n\n![Built-in Copilot Prompts with Enable/Disable Toggle](https://pub-35424b4473484be483c0afa08c69e7da.r2.dev/Screenshot%202025-02-15%20120628.png)\n\n## Custom Copilot Prompts\n\n::: tip New Feature\nYou can now create unlimited custom copilot prompts! This is the recommended way to create your own prompts instead of using the built-in \"Custom\" prompt.\n:::\n\nCustom Copilot Prompts allow you to create your own AI-powered actions that appear in the context menu when you select text.\n\n### Creating Custom Copilot Prompts\n\n1. Go to `Settings` → `Manage Prompts` → `Custom Copilot` tab\n2. Click the `Add` button\n3. Fill in the form:\n   - **Title**: The name that will appear in the context menu (e.g., \"Simplify Text\", \"Find Grammar Errors\")\n   - **Prompt Template**: Your prompt with `{text}` as a placeholder for selected text\n\n4. Click `Save`\n\n### How to Use Custom Copilot Prompts\n\n1. Select text on any webpage\n2. Right-click on the selected text\n3. Find your custom prompt in the `Page Assist` context menu\n4. Click it to process the selected text\n\n### Writing Good Prompts\n\n::: tip Prompt Writing Best Practices\nThe quality of your results depends heavily on how well you write your prompts. Follow these guidelines:\n:::\n\n#### 1. **Be Specific and Clear**\n```\n❌ Bad: Make this better\n✅ Good: Rewrite the following text to be more professional and formal:\n\n{text}\n```\n\n#### 2. **Provide Context**\n```\n❌ Bad: {text}\n✅ Good: You are a technical writing expert. Review the following text and suggest improvements for clarity and conciseness:\n\n{text}\n```\n\n#### 3. **Specify the Output Format**\n```\n❌ Bad: Check grammar: {text}\n✅ Good: Review the following text for grammar and spelling errors.\nList each error with:\n- Original text\n- Corrected text\n- Explanation\n\nText:\n{text}\n```\n\n#### 4. **Use the {text} Placeholder**\nAlways include `{text}` in your prompt. This will be replaced with the selected text.\n\n```\nExample: Analyze the sentiment of the following text and classify it as Positive, Negative, or Neutral. Provide reasoning for your classification.\n\nText:\n{text}\n```\n\n#### 5. **Set Constraints When Needed**\n```\nSummarize the following article in exactly 3 bullet points. Each bullet point should be no more than 20 words.\n\nArticle:\n{text}\n```\n\n### Example Custom Copilot Prompts\n\nHere are some useful custom prompts you can create:\n\n#### Grammar and Style\n```\nTitle: Fix Grammar\nPrompt: Review the following text for grammar, spelling, and punctuation errors. Provide the corrected version.\n\n{text}\n```\n\n#### Simplification\n```\nTitle: Simplify Text\nPrompt: Rewrite the following text to be understood by a 10-year-old. Use simple words and short sentences.\n\n{text}\n```\n\n#### Code Review\n```\nTitle: Review Code\nPrompt: Review the following code for:\n- Potential bugs\n- Performance issues\n- Best practices\n- Readability improvements\n\nCode:\n{text}\n```\n\n#### Tone Adjustment\n```\nTitle: Make Professional\nPrompt: Rewrite the following text in a professional, formal tone suitable for business communication.\n\n{text}\n```\n\n#### Fact Checking\n```\nTitle: Extract Facts\nPrompt: Extract all factual claims from the following text. List each claim and note if it needs verification.\n\n{text}\n```\n\n### Managing Custom Copilot Prompts\n\n- **Edit**: Click the edit icon to modify the title or prompt\n- **Delete**: Click the delete icon to remove a prompt\n- **Enable/Disable**: Toggle the switch to show/hide a prompt in the context menu\n\nChanges are applied immediately without requiring a browser restart!\n\n## How to Update Built-in Prompts\n\nYou can customize the default built-in prompts:\n\n1. Go to `Settings` → `Manage Prompts` → `Copilot` tab\n2. Click the edit icon next to the prompt you want to change\n3. Modify the prompt template (must include `{text}` placeholder)\n4. Click `Save`\n\n::: warning\nEditing built-in prompts changes them for all future uses. Consider creating a Custom Copilot Prompt instead if you want to keep the original.\n:::"
  },
  {
    "path": "docs/sidebar/index.md",
    "content": "# Page Assist Sidebar \n\nSidebar is a really good feature of the extension. It allows you to chat with your AI model from any webpage. Rather than opening a new tab and navigating to the website, you can simply open the sidebar and chat with your AI model or ask any question about the current page.\n\n\nSome browser not support the sidebar feature. You can check the [Browser Support](/browser-support.md) page to see if your browser is supported.\n\n\n\n## How to open the Sidebar\n\n\nThere are 3 ways to open the sidebar:\n\n### From the browser toolbar\n\n1. Right-click on the Page Assist icon in the browser toolbar and select `Open Sidebar`.\n2. Click on the Page Assist icon in the browser toolbar and select `Open Sidebar`.\n\n![Open Sidebar](https://pub-35424b4473484be483c0afa08c69e7da.r2.dev/Screenshot%202025-02-15%20115612.png)\n\n\n### From the context menu\n\n1. Right-click on any webpage and select `Open Page Assist Sidebar`.\n![Open Sidebar](https://pub-35424b4473484be483c0afa08c69e7da.r2.dev/Screenshot%202025-02-15%20115734.png)\n\n\n### Using the keyboard shortcut\n\nThe default keyboard shortcut to open the sidebar is `Ctrl+Shift+Y`. You can change the keyboard shortcuts from the extension settings on the Chrome Extension Management page. Check the [Shortcut Keys](/shortcuts.md) page for more information."
  },
  {
    "path": "docs/sidebar/vision.md",
    "content": "# Sidebar Vision (🧪)\n\nThe vision feature of the sidebar allows LLM to see the current webpage. This is similar to [Chat with Website](/sidebar/chat-with-website.md) but it is not like RAG.\n\n## How to use Sidebar Vision with LLM with vision capabilities\n\n1. Open the Sidebar\n2. Select a model with vision capabilities\n3. Enable the `Vision` option from the input box eye icon\n4. Start chatting with the current website\n\n![Vision](https://pub-35424b4473484be483c0afa08c69e7da.r2.dev/Screenshot%202025-02-19%20104826.png)\n\n## How to use Sidebar Vision with LLM without vision capabilities\n\nPage Assist converts the current webpage to text using Tesseract OCR. This is a very basic OCR and it is not very accurate, but it is good enough to get the basic idea of the webpage.\n\n1. Open the Sidebar\n2. Select a model \n3. Enable the `Vision` option from the input box eye icon\n4. Expand the `Submit` button dropdown and enable `Extract Text From Image (OCR)`\n5. Start chatting with the current website\n\n![Vision](https://pub-35424b4473484be483c0afa08c69e7da.r2.dev/Screenshot%202025-02-19%20105123.png)"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"pageassist\",\n  \"displayName\": \"Page Assist - A Web UI for Local AI Models\",\n  \"version\": \"1.0.9\",\n  \"description\": \"Use your locally running AI models to assist you in your web browsing.\",\n  \"author\": \"n4ze3m\",\n  \"scripts\": {\n    \"dev\": \"cross-env TARGET=chrome wxt\",\n    \"dev:firefox\": \"cross-env TARGET=firefox wxt -b firefox\",\n    \"dev:edge\": \"cross-env TARGET=chrome wxt -b edge\",\n    \"build\": \"bun build:chrome; bun build:firefox; bun build:edge\",\n    \"build:chrome\": \"cross-env TARGET=chrome wxt build\",\n    \"build:firefox\": \"cross-env TARGET=firefox wxt build -b firefox\",\n    \"build:edge\": \"cross-env TARGET=chrome wxt build -b edge\",\n    \"zip\": \"cross-env TARGET=chrome wxt zip\",\n    \"zip:firefox\": \"cross-env TARGET=firefox wxt zip -b firefox\",\n    \"compile\": \"tsc --noEmit\",\n    \"docs:dev\": \"vitepress dev docs\",\n    \"docs:build\": \"vitepress build docs\",\n    \"docs:preview\": \"vitepress preview docs\",\n    \"postinstall\": \"wxt prepare\"\n  },\n  \"dependencies\": {\n    \"@cfworker/json-schema\": \"^4.1.1\",\n    \"@ant-design/cssinjs\": \"^1.18.4\",\n    \"@headlessui/react\": \"^1.7.18\",\n    \"@heroicons/react\": \"^2.1.1\",\n    \"@langchain/community\": \"^1.1.22\",\n    \"@langchain/core\": \"^1.1.31\",\n    \"@langchain/openai\": \"^1.2.12\",\n    \"@mantine/form\": \"^7.5.0\",\n    \"@mantine/hooks\": \"^7.5.3\",\n    \"@modelcontextprotocol/sdk\": \"^1.27.1\",\n    \"@mozilla/readability\": \"^0.5.0\",\n    \"@plasmohq/storage\": \"^1.9.0\",\n    \"@tailwindcss/forms\": \"^0.5.7\",\n    \"@tailwindcss/typography\": \"^0.5.10\",\n    \"@tanstack/react-query\": \"^5.17.19\",\n    \"@tanstack/react-virtual\": \"^3.13.6\",\n    \"@vitejs/plugin-react\": \"^4.2.1\",\n    \"antd\": \"^5.13.3\",\n    \"axios\": \"^1.6.7\",\n    \"caniuse-lite\": \"^1.0.30001716\",\n    \"cheerio\": \"^1.0.0-rc.12\",\n    \"d3-dsv\": \"2\",\n    \"dayjs\": \"^1.11.10\",\n    \"dexie\": \"^4.0.11\",\n    \"dexie-react-hooks\": \"^1.1.7\",\n    \"html-to-text\": \"^9.0.5\",\n    \"html2canvas\": \"^1.4.1\",\n    \"i18next\": \"^23.10.1\",\n    \"i18next-browser-languagedetector\": \"^7.2.0\",\n    \"langchain\": \"^1.2.30\",\n    \"lucide-react\": \"^0.350.0\",\n    \"mammoth\": \"^1.7.2\",\n    \"marked\": \"^15.0.12\",\n    \"mermaid\": \"^11.4.1\",\n    \"ml-distance\": \"^4.0.1\",\n    \"ollama\": \"^0.5.17\",\n    \"openai\": \"^4.95.1\",\n    \"pa-tesseract.js\": \"^5.1.1\",\n    \"pdfjs-dist\": \"4.0.379\",\n    \"property-information\": \"^6.4.1\",\n    \"pubsub-js\": \"^1.9.4\",\n    \"react\": \"18.2.0\",\n    \"react-dom\": \"18.2.0\",\n    \"react-i18next\": \"^14.1.0\",\n    \"react-icons\": \"^5.2.1\",\n    \"react-markdown\": \"8.0.0\",\n    \"react-router-dom\": \"6.10.0\",\n    \"react-syntax-highlighter\": \"^15.5.0\",\n    \"react-toastify\": \"^10.0.4\",\n    \"rehype-katex\": \"6.0.3\",\n    \"rehype-mathjax\": \"4.0.3\",\n    \"remark-gfm\": \"3.0.1\",\n    \"remark-math\": \"5.1.1\",\n    \"turndown\": \"^7.1.3\",\n    \"unist-util-visit\": \"^5.0.0\",\n    \"yt-transcript\": \"^0.0.2\",\n    \"zustand\": \"^4.5.0\"\n  },\n  \"devDependencies\": {\n    \"@plasmohq/prettier-plugin-sort-imports\": \"4.0.1\",\n    \"@types/chrome\": \"^0.0.280\",\n    \"@types/d3-dsv\": \"^3.0.7\",\n    \"@types/html-to-text\": \"^9.0.4\",\n    \"@types/node\": \"20.11.9\",\n    \"@types/pubsub-js\": \"^1.8.6\",\n    \"@types/react\": \"18.2.48\",\n    \"@types/react-dom\": \"18.2.18\",\n    \"@types/react-speech-recognition\": \"^3.9.5\",\n    \"@types/react-syntax-highlighter\": \"^15.5.11\",\n    \"@types/turndown\": \"^5.0.4\",\n    \"autoprefixer\": \"^10.4.17\",\n    \"cross-env\": \"^7.0.3\",\n    \"postcss\": \"^8.4.33\",\n    \"prettier\": \"3.2.4\",\n    \"tailwindcss\": \"^3.4.1\",\n    \"typescript\": \"5.3.3\",\n    \"vite-plugin-top-level-await\": \"^1.4.1\",\n    \"vitepress\": \"^1.6.3\",\n    \"wxt\": \"^0.19.6\"\n  },\n  \"resolutions\": {\n    \"@langchain/core\": \"^1.1.31\"\n  }\n}\n"
  },
  {
    "path": "page-share.md",
    "content": "# Page Share\n\nPage Share allows you to share the chat publicly, similar to ChatGPT Share. You can self-host Page Share for privacy and security.\n\nThe default Page Share is hosted at [https://pageassist.xyz](https://pageassist.xyz).\n\n## Self-Host\n\nYou can self-host Page Share using two methods:\n\n- Railway\n- Docker\n\n### Railway\n\nClick the button below to deploy the code to Railway.\n\n[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/VbiS2Q?referralCode=olbszX)\n\n### Docker\n\n1. Clone the repository\n\n```bash\ngit clone https://github.com/n4ze3m/page-share-app.git\ncd page-share-app\n```\n\n2. Run the server\n\n```bash\ndocker-compose up\n```\n\n3. Open the app\n\nNavigate to [http://localhost:3000](http://localhost:3000) in your browser.\n"
  },
  {
    "path": "postcss.config.js",
    "content": "/**\n * @type {import('postcss').ProcessOptions}\n */\nmodule.exports = {\n    plugins: {\n      tailwindcss: {},\n      autoprefixer: {}\n    }\n  }"
  },
  {
    "path": "src/assets/locale/ar/chrome.json",
    "content": "{\n    \"heading\": \"إعداد Chrome AI\",\n    \"status\": {\n        \"label\": \"تمكين أو تعطيل دعم Chrome AI في مساعد الصفحة\"\n    },\n    \"error\": {\n        \"browser_not_supported\": \"هذا الإصدار من Chrome غير مدعوم من نموذج Gemini Nano. يرجى التحديث إلى الإصدار 127 أو أحدث\",\n        \"ai_not_supported\": \"الإعداد chrome://flags/#prompt-api-for-gemini-nano غير مفعل. يرجى تفعيله.\",\n        \"ai_not_ready\": \"Gemini Nano غير جاهز بعد؛ تحتاج إلى التحقق مرة أخرى من إعدادات Chrome.\",\n        \"internal_error\": \"حدث خطأ داخلي. يرجى المحاولة مرة أخرى لاحقاً.\"\n    },\n    \"errorDescription\": \"لاستخدام Chrome AI، تحتاج إلى إصدار Chrome 138 أو أحدث. اتبع الخطوات التالية:\\n\\n1. انتقل إلى `chrome://flags/#prompt-api-for-gemini-nano` وقم بتمكين \\\"Prompt API for Gemini Nano\\\".\\n2. أعد تشغيل Chrome لتطبيق الإعداد.\\n3. عد إلى هذه الصفحة وانقر على \\\"تحميل النموذج\\\" — سيؤدي ذلك إلى تنزيل نموذج بحجم 4 جيجابايت لأول مرة.\\n4. بمجرد اكتمال التنزيل، يمكن تمكين Gemini Nano من خلال مساعد الصفحة.\",\n    \"downloadModel\": \"تحميل النموذج\",\n    \"modelDownloadWarning\": \"سيتم تنزيل نموذج بحجم تقريبي يتراوح بين 1.5 جيجابايت و2.4 جيجابايت. تأكد من توفر مساحة كافية على القرص.\"\n}"
  },
  {
    "path": "src/assets/locale/ar/common.json",
    "content": "{\n    \"pageAssist\": \"مساعد الصفحة\",\n    \"selectAModel\": \"اختر نموذجًا\",\n    \"save\": \"حفظ\",\n    \"saved\": \"تم الحفظ\",\n    \"cancel\": \"إلغاء\",\n    \"retry\": \"إعادة المحاولة\",\n    \"share\": {\n        \"tooltip\": {\n            \"share\": \"مشاركة\"\n        },\n        \"modal\": {\n            \"title\": \"مشاركة رابط المحادثة\"\n        },\n        \"form\": {\n            \"defaultValue\": {\n                \"name\": \"مجهول\",\n                \"title\": \"محادثة بدون عنوان\"\n            },\n            \"title\": {\n                \"label\": \"عنوان المحادثة\",\n                \"placeholder\": \"أدخل عنوان المحادثة\",\n                \"required\": \"عنوان المحادثة مطلوب\"\n            },\n            \"name\": {\n                \"label\": \"اسمك\",\n                \"placeholder\": \"أدخل اسمك\",\n                \"required\": \"اسمك مطلوب\"\n            },\n            \"btn\": {\n                \"save\": \"إنشاء رابط\",\n                \"saving\": \"جاري إنشاء الرابط...\"\n            }\n        },\n        \"notification\": {\n            \"successGenerate\": \"تم نسخ الرابط إلى الحافظة\",\n            \"failGenerate\": \"فشل في إنشاء الرابط\"\n        }\n    },\n    \"copyToClipboard\": \"نسخ إلى الحافظة\",\n    \"webSearch\": \"البحث في الويب\",\n    \"regenerate\": \"إعادة التوليد\",\n    \"continue\": \"متابعة الرد\",\n    \"edit\": \"تعديل\",\n    \"delete\": \"حذف\",\n    \"saveAndSubmit\": \"حفظ وإرسال\",\n    \"editMessage\": {\n        \"placeholder\": \"اكتب رسالة...\"\n    },\n    \"submit\": \"إرسال\",\n    \"noData\": \"لا توجد بيانات\",\n    \"noHistory\": \"لا يوجد سجل محادثات\",\n    \"chatWithCurrentPage\": \"الدردشة مع الصفحة الحالية\",\n    \"beta\": \"تجريبي\",\n    \"tts\": \"قراءة بصوت عالٍ\",\n    \"currentChatModelSettings\": \"إعدادات نموذج المحادثة الحالي\",\n    \"modelSettings\": {\n        \"label\": \"إعدادات النموذج\",\n        \"description\": \"تعيين خيارات النموذج عالمياً لجميع المحادثات\",\n        \"form\": {\n            \"keepAlive\": {\n                \"label\": \"الإبقاء نشطاً\",\n                \"help\": \"يتحكم في المدة التي سيظل فيها النموذج محملاً في الذاكرة بعد الطلب (الافتراضي: 5 دقائق)\",\n                \"placeholder\": \"أدخل مدة البقاء نشطاً (مثال: 5م، 10م، 1س)\"\n            },\n            \"temperature\": {\n                \"label\": \"درجة الحرارة\",\n                \"placeholder\": \"أدخل قيمة درجة الحرارة (مثال: 0.7، 1.0)\"\n            },\n            \"numCtx\": {\n                \"label\": \"حجم نافذة السياق (num_ctx)\",\n                \"placeholder\": \"أدخل قيمة حجم نافذة السياق (الافتراضي: 2048)\"\n            },\n            \"numPredict\": {\n                \"label\": \"الحد الأقصى للرموز\",\n                \"placeholder\": \"أدخل قيمة الحد الأقصى للرموز (مثال: 2048، 4096)\"\n            },\n            \"seed\": {\n                \"label\": \"البذرة\",\n                \"placeholder\": \"أدخل قيمة البذرة (مثال: 1234)\",\n                \"help\": \"إمكانية تكرار مخرجات النموذج\"\n            },\n            \"topK\": {\n                \"label\": \"أعلى K\",\n                \"placeholder\": \"أدخل قيمة أعلى K (مثال: 40، 100)\"\n            },\n            \"topP\": {\n                \"label\": \"أعلى P\",\n                \"placeholder\": \"أدخل قيمة أعلى P (مثال: 0.9، 0.95)\"\n            },\n            \"useMMap\": {\n                \"label\": \"استخدام MMap\"\n            },\n            \"numGpu\": {\n                \"label\": \"عدد وحدات معالجة الرسومات\",\n                \"placeholder\": \"أدخل عدد الطبقات لإرسالها إلى وحدة/وحدات معالجة الرسومات\"\n            },\n            \"systemPrompt\": {\n                \"label\": \"موجه النظام المؤقت\",\n                \"placeholder\": \"أدخل موجه النظام\",\n                \"help\": \"هذه طريقة سريعة لتعيين موجه النظام في المحادثة الحالية، والذي سيتجاوز موجه النظام المحدد إذا كان موجوداً.\"\n            }\n        },\n        \"advanced\": \"المزيد من إعدادات النموذج\"\n    },\n    \"copilot\": {\n        \"summary\": \"تلخيص\",\n        \"explain\": \"شرح\",\n        \"rephrase\": \"إعادة صياغة\",\n        \"translate\": \"ترجمة\",\n        \"custom\": \"مخصص\"\n    },\n    \"citations\": \"الاقتباسات\",\n    \"segmented\": {\n        \"ollama\": \"نماذج Ollama\",\n        \"custom\": \"نماذج مخصصة\"\n    },\n    \"downloadCode\": \"تنزيل الكود\",\n    \"date\": {\n        \"pinned\": \"مثبت\",\n        \"today\": \"اليوم\",\n        \"yesterday\": \"الأمس\",\n        \"last7Days\": \"آخر 7 أيام\",\n        \"older\": \"أقدم\"\n    },\n    \"range\": {\n        \"deleteConfirm\": {\n            \"pinned\": \"هل أنت متأكد أنك تريد حذف جميع الرسائل المثبتة؟\",\n            \"today\": \"هل أنت متأكد أنك تريد حذف جميع رسائل اليوم؟\",\n            \"yesterday\": \"هل أنت متأكد أنك تريد حذف جميع رسائل الأمس؟\",\n            \"last7Days\": \"هل أنت متأكد أنك تريد حذف جميع رسائل آخر 7 أيام؟\",\n            \"older\": \"هل أنت متأكد أنك تريد حذف جميع الرسائل القديمة؟\"\n        },\n        \"tooltip\": {\n            \"pinned\": \"حذف جميع الرسائل المثبتة\",\n            \"today\": \"حذف جميع رسائل اليوم\",\n            \"yesterday\": \"حذف جميع رسائل الأمس\",\n            \"last7Days\": \"حذف جميع رسائل آخر 7 أيام\",\n            \"older\": \"حذف جميع الرسائل القديمة\"\n        }\n    },\n    \"pin\": \"تثبيت\",\n    \"unpin\": \"إلغاء التثبيت\",\n    \"generationInfo\": \"معلومات التوليد\",\n    \"sidebarChat\": \"دردشة الشريط الجانبي\",\n    \"reasoning\": {\n        \"thinking\": \"جاري التفكير....\",\n        \"thought\": \"فكر لمدة {{time}}\"\n    },\n    \"embeddingGen\": \"إنشاء تمثيلات، قد يستغرق هذا وقتًا\",\n    \"semanticSearch\": \"جاري البحث الدلالي\",\n    \"downloading\": \"جاري التنزيل\",\n    \"cancelPullingModel\": {\n        \"confirm\": \"هل أنت متأكد أنك تريد إلغاء التنزيل؟ سيؤدي ذلك إلى إيقاف عملية التنزيل. وفقًا لتوثيق Ollama، يمكنك المتابعة من حيث توقفت.\"\n    }\n}"
  },
  {
    "path": "src/assets/locale/ar/knowledge.json",
    "content": "{ \n    \"addBtn\": \"إضافة معرفة جديدة\",\n    \"columns\": {\n        \"title\": \"العنوان\",\n        \"status\": \"الحالة\",\n        \"embeddings\": \"نموذج التضمين\",\n        \"createdAt\": \"تاريخ الإنشاء\",\n        \"action\": \"الإجراءات\"\n    },\n    \"expandedColumns\": {\n        \"name\": \"الاسم\"\n    },\n    \"confirm\": {\n        \"delete\": \"هل أنت متأكد أنك تريد حذف هذه المعرفة؟\"\n    },\n    \"deleteSuccess\": \"تم حذف المعرفة بنجاح\",\n    \"status\": {\n        \"pending\": \"قيد الانتظار\",\n        \"finished\": \"مكتمل\",\n        \"processing\": \"قيد المعالجة\",\n        \"failed\": \"فشل\"\n    },\n    \"addKnowledge\": \"إضافة معرفة\",\n    \"updateKnowledge\": \"تحديث المعرفة\",\n    \"form\": {\n        \"title\": {\n            \"label\": \"عنوان المعرفة\",\n            \"placeholder\": \"أدخل عنوان المعرفة\",\n            \"required\": \"عنوان المعرفة مطلوب\"\n        },\n        \"uploadFile\": {\n            \"label\": \"رفع ملف\",\n            \"uploadText\": \"اسحب وأفلت ملفًا هنا أو انقر للرفع\",\n            \"uploadHint\": \"أنواع الملفات المدعومة: .pdf, .csv, .txt, .md, .docx\",\n            \"required\": \"الملف مطلوب\"\n        },\n        \"submit\": \"إرسال\",\n        \"success\": \"تمت إضافة المعرفة بنجاح\"\n    },\n    \"noEmbeddingModel\": \"يرجى إضافة نموذج تضمين من صفحة إعدادات RAG أولاً\"\n}"
  },
  {
    "path": "src/assets/locale/ar/openai.json",
    "content": "{\n    \"settings\": \"واجهة برمجة التطبيقات المتوافقة مع OpenAI\",\n    \"heading\": \"واجهة برمجة التطبيقات المتوافقة مع OpenAI\",\n    \"subheading\": \"إدارة وتكوين مزودي واجهة برمجة التطبيقات المتوافقة مع OpenAI هنا.\",\n    \"addBtn\": \"إضافة مزود\",\n    \"table\": {\n        \"name\": \"اسم المزود\",\n        \"baseUrl\": \"عنوان URL الأساسي\",\n        \"actions\": \"إجراء\"\n    },\n    \"modal\": {\n        \"titleAdd\": \"إضافة مزود جديد\",\n        \"name\": {\n            \"label\": \"اسم المزود\",\n            \"required\": \"اسم المزود مطلوب.\",\n            \"placeholder\": \"أدخل اسم المزود\"\n        },\n        \"baseUrl\": {\n            \"label\": \"عنوان URL الأساسي\",\n            \"help\": \"عنوان URL الأساسي لمزود واجهة برمجة التطبيقات OpenAI. مثال (http://localhost:1234/v1)\",\n            \"required\": \"عنوان URL الأساسي مطلوب.\",\n            \"placeholder\": \"أدخل عنوان URL الأساسي\"\n        },\n        \"apiKey\": {\n            \"label\": \"مفتاح API\",\n            \"required\": \"مفتاح API مطلوب.\",\n            \"placeholder\": \"أدخل مفتاح API\"\n        },\n        \"submit\": \"حفظ\",\n        \"update\": \"تحديث\",\n        \"deleteConfirm\": \"هل أنت متأكد أنك تريد حذف هذا المزود؟\",\n        \"model\": {\n            \"title\": \"قائمة النماذج\",\n            \"subheading\": \"يرجى تحديد نماذج المحادثة التي تريد استخدامها مع هذا المزود.\",\n            \"success\": \"تمت إضافة نماذج جديدة بنجاح.\"\n        },\n        \"tipLMStudio\": \"سيقوم Page Assist تلقائيًا بجلب النماذج التي قمت بتحميلها على LM Studio. لست بحاجة إلى إضافتها يدويًا.\"\n    },\n    \"addSuccess\": \"تمت إضافة المزود بنجاح.\",\n    \"deleteSuccess\": \"تم حذف المزود بنجاح.\",\n    \"updateSuccess\": \"تم تحديث المزود بنجاح.\",\n    \"delete\": \"حذف\",\n    \"edit\": \"تعديل\",\n    \"newModel\": \"إضافة نماذج للمزود\",\n    \"noNewModel\": \"بالنسبة لـ LMStudio و Ollama و Llamafile، نقوم بالجلب ديناميكيًا. لا حاجة للإضافة اليدوية.\",\n    \"searchModel\": \"بحث عن نموذج\",\n    \"selectAll\": \"تحديد الكل\",\n    \"save\": \"حفظ\",\n    \"saving\": \"جاري الحفظ...\",\n    \"manageModels\": {\n        \"columns\": {\n            \"name\": \"اسم النموذج\",\n            \"model_type\": \"نوع النموذج\",\n            \"model_id\": \"معرف النموذج\",\n            \"provider\": \"اسم المزود\",\n            \"actions\": \"إجراء\",\n            \"nickname\": \"الاسم المستعار للنموذج\"\n        },\n        \"tooltip\": {\n            \"delete\": \"حذف\"\n        },\n        \"confirm\": {\n            \"delete\": \"هل أنت متأكد أنك تريد حذف هذا النموذج؟\"\n        },\n        \"modal\": {\n            \"title\": \"إضافة نموذج مخصص\",\n            \"form\": {\n                \"name\": {\n                    \"label\": \"معرف النموذج\",\n                    \"placeholder\": \"llama3.2\",\n                    \"required\": \"معرف النموذج مطلوب.\"\n                },\n                \"provider\": {\n                    \"label\": \"المزود\",\n                    \"placeholder\": \"اختر المزود\",\n                    \"required\": \"المزود مطلوب.\"\n                },\n                \"type\": {\n                    \"label\": \"نوع النموذج\"\n                }\n            }\n        }\n    },\n    \"noModelFound\": \"لم يتم العثور على نموذج. تأكد من إضافة المزود الصحيح مع عنوان URL الأساسي ومفتاح API.\",\n    \"radio\": {\n        \"chat\": \"نموذج المحادثة\",\n        \"embedding\": \"نموذج التضمين\",\n        \"chatInfo\": \"يستخدم لإكمال المحادثة وتوليد المحادثات\",\n        \"embeddingInfo\": \"يستخدم لـ RAG ومهام البحث الدلالي الأخرى ذات الصلة.\"\n    },\n    \"nicknameModal\": {\n        \"title\": \"إضافة / تعديل الاسم المستعار للنموذج\",\n        \"form\": {\n            \"modelName\": {\n                \"label\": \"اسم النموذج\",\n                \"placeholder\": \"أدخل اسم النموذج\",\n                \"required\": \"اسم النموذج مطلوب.\"\n            },\n            \"modelAvatar\": {\n                \"label\": \"صورة النموذج\",\n                \"placeholder\": \"أدخل صورة النموذج\",\n                \"help\": \"يرجى إدخال رابط صورة النموذج. سيتم عرض هذه الصورة في نافذة المحادثة.\"\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/assets/locale/ar/option.json",
    "content": "{ \n    \"newChat\": \"محادثة جديدة\",\n    \"selectAPrompt\": \"اختر موجهاً\",\n    \"githubRepository\": \"مستودع GitHub\",\n    \"settings\": \"الإعدادات\",\n    \"sidebarTitle\": \"سجل المحادثات\",\n    \"error\": \"خطأ\",\n    \"somethingWentWrong\": \"حدث خطأ ما\",\n    \"validationSelectModel\": \"الرجاء اختيار نموذج للمتابعة\",\n    \"deleteHistoryConfirmation\": \"هل أنت متأكد أنك تريد حذف هذا السجل؟\",\n    \"editHistoryTitle\": \"أدخل عنواناً جديداً\",\n    \"temporaryChat\": \"محادثة مؤقتة\",\n    \"more\": {\n        \"copy\": {\n            \"group\": \"نسخ\",\n            \"asText\": \"نسخ كنص\",\n            \"asMarkdown\": \"نسخ كماركداون\",\n            \"success\": \"تم النسخ إلى الحافظة!\"\n        },\n        \"download\": {\n            \"group\": \"تنزيل\",\n            \"text\": \"ملف نصي (.txt)\",\n            \"markdown\": \"ماركداون (.md)\",\n            \"json\": \"ملف JSON (.json)\"\n        },\n        \"share\": \"مشاركة\"\n    }\n}"
  },
  {
    "path": "src/assets/locale/ar/playground.json",
    "content": "{\n    \"ollamaState\": {\n        \"searching\": \"جارٍ البحث عن Ollama الخاص بك 🦙\",\n        \"running\": \"Ollama يعمل 🦙\",\n        \"notRunning\": \"تعذر الاتصال بـ Ollama 🦙\",\n        \"connectionError\": \"يبدو أنك تواجه خطأ في الاتصال. يرجى الرجوع إلى هذا <anchor>الدليل</anchor> لاستكشاف الأخطاء وإصلاحها.\"\n    },\n    \"formError\": {\n        \"noModel\": \"الرجاء اختيار نموذج\",\n        \"noEmbeddingModel\": \"الرجاء تعيين نموذج التضمين في صفحة الإعدادات > RAG\"\n    },\n    \"form\": {\n        \"textarea\": {\n            \"placeholder\": \"اكتب رسالة...\"\n        },\n        \"webSearch\": {\n            \"on\": \"تشغيل\",\n            \"off\": \"إيقاف\"\n        }\n    },\n    \"tooltip\": {\n        \"searchInternet\": \"البحث في الإنترنت\",\n        \"speechToText\": \"تحويل الكلام إلى نص\",\n        \"uploadImage\": \"تحميل صورة\",\n        \"stopStreaming\": \"إيقاف البث\",\n        \"knowledge\": \"المعرفة\",\n        \"vision\": \"[تجريبي] محادثة الرؤية\",\n        \"clearContext\": \"مسح السياق\"\n    },\n    \"sendWhenEnter\": \"إرسال عند الضغط على Enter\",\n    \"welcome\": \"مرحباً! كيف يمكنني مساعدتك اليوم؟\",\n    \"useOCR\": \"استخراج النص من الصورة (OCR)\"\n}"
  },
  {
    "path": "src/assets/locale/ar/settings.json",
    "content": "{\n  \"generalSettings\": {\n    \"title\": \"الإعدادات العامة\",\n    \"settings\": {\n      \"heading\": \"إعدادات واجهة المستخدم\",\n      \"speechRecognitionLang\": {\n        \"label\": \"لغة التعرف على الكلام\",\n        \"placeholder\": \"اختر لغة\"\n      },\n      \"language\": {\n        \"label\": \"اللغة\",\n        \"placeholder\": \"اختر لغة\"\n      },\n      \"darkMode\": {\n        \"label\": \"تغيير المظهر\",\n        \"options\": {\n          \"light\": \"فاتح\",\n          \"dark\": \"داكن\"\n        }\n      },\n      \"copilotResumeLastChat\": {\n        \"label\": \"استئناف آخر محادثة عند فتح اللوحة الجانبية (كوبيلوت)\"\n      },\n      \"turnOnChatWithWebsite\": {\n        \"label\": \"تمكين الدردشة مع الموقع بشكل افتراضي (كوبيلوت)\"\n      },\n      \"webUIResumeLastChat\": {\n        \"label\": \"استئناف آخر محادثة عند فتح واجهة المستخدم\"\n      },\n      \"hideCurrentChatModelSettings\": {\n        \"label\": \"إخفاء إعدادات نموذج المحادثة الحالي\"\n      },\n      \"restoreLastChatModel\": {\n        \"label\": \"استعادة آخر نموذج مستخدم للمحادثات السابقة\"\n      },\n      \"sendNotificationAfterIndexing\": {\n        \"label\": \"إرسال إشعار بعد الانتهاء من معالجة قاعدة المعرفة\"\n      },\n      \"generateTitle\": {\n        \"label\": \"توليد العنوان باستخدام الذكاء الاصطناعي\"\n      },\n      \"ollamaStatus\": {\n        \"label\": \"تمكين أو تعطيل فحص حالة اتصال أولاما\"\n      },\n      \"wideMode\": {\n        \"label\": \"تمكين وضع الشاشة العريضة\"\n      },\n      \"openReasoning\": {\n        \"label\": \"فتح التفكير المنطقي بشكل افتراضي\"\n      },\n      \"userChatBubble\": {\n        \"label\": \"استخدام فقاعة الدردشة لرسائل المستخدم\"\n      },\n      \"autoCopyResponseToClipboard\": {\n        \"label\": \"نسخ الرد تلقائياً إلى الحافظة\"\n      },\n      \"useMarkdownForUserMessage\": {\n        \"label\": \"تمكين تنسيق ماركداون لرسائل المستخدم\"\n      },\n      \"copyAsFormattedText\": {\n        \"label\": \"نسخ كنص منسق\"\n      },\n      \"tabMentionsEnabled\": {\n        \"label\": \"تمكين إشارات التبويب (@tab)\"\n      },\n      \"pasteLargeTextAsFile\": {\n        \"label\": \"لصق النص الكبير كملف\"\n      },\n      \"sidepanelTemporaryChat\": {\n        \"label\": \"تمكين المحادثة المؤقتة في اللوحة الجانبية بشكل افتراضي\"\n      }\n    },\n    \"sidepanelRag\": {\n      \"heading\": \"إعدادات الاسترجاع\",\n      \"ragEnabled\": {\n        \"label\": \"تمكين التضمين والاسترجاع\"\n      },\n      \"maxWebsiteContext\": {\n        \"label\": \"الحد الأقصى لحجم المحتوى لوضع السياق الكامل\",\n        \"placeholder\": \"حجم المحتوى (الافتراضي 4028)\"\n      }\n    },\n    \"webSearch\": {\n      \"heading\": \"إدارة البحث على الإنترنت\",\n      \"searchMode\": {\n        \"label\": \"إجراء بحث بسيط على الإنترنت\"\n      },\n      \"provider\": {\n        \"label\": \"محرك البحث\",\n        \"placeholder\": \"اختر محرك بحث\"\n      },\n      \"totalSearchResults\": {\n        \"label\": \"إجمالي نتائج البحث\",\n        \"placeholder\": \"أدخل إجمالي نتائج البحث\"\n      },\n      \"visitSpecificWebsite\": {\n        \"label\": \"زيارة الموقع المذكور في الرسالة\"\n      },\n      \"searxng\": {\n        \"url\": {\n          \"label\": \"رابط SearXNG\"\n        }\n      },\n      \"braveApi\": {\n        \"label\": \"مفتاح واجهة برنامج Brave\",\n        \"placeholder\": \"أدخل مفتاح واجهة برنامج Brave\"\n      },\n      \"googleDomain\": {\n        \"label\": \"نطاق جوجل\"\n      },\n      \"searchOnByDefault\": {\n        \"label\": \"تفعيل البحث على الإنترنت بشكل افتراضي\"\n      }\n    },\n    \"system\": {\n      \"heading\": \"إعدادات النظام\",\n      \"storageSyncEnabled\": {\n        \"label\": \"تمكين مزامنة تخزين المتصفح (مزامنة الإعدادات عبر الأجهزة)\"\n      },\n      \"deleteChatHistory\": {\n        \"label\": \"إعادة تعيين النظام\",\n        \"button\": \"إعادة تعيين الكل\",\n        \"confirm\": \"هل أنت متأكد أنك تريد إجراء إعادة تعيين النظام؟ سيؤدي هذا إلى مسح جميع البيانات ولا يمكن التراجع عنه.\"\n      },\n      \"export\": {\n        \"label\": \"تصدير جميع البيانات (سجل الدردشة، قاعدة المعرفة، الطلبات، والإعدادات)\",\n        \"button\": \"تصدير البيانات\",\n        \"success\": \"تم التصدير بنجاح\"\n      },\n      \"import\": {\n        \"label\": \"استيراد جميع البيانات (سجل الدردشة، قاعدة المعرفة، الطلبات، والإعدادات)\",\n        \"button\": \"استيراد البيانات\",\n        \"success\": \"تم الاستيراد بنجاح\",\n        \"error\": \"خطأ في الاستيراد\"\n      }\n    },\n    \"tts\": {\n      \"heading\": \"إعدادات تحويل النص إلى كلام\",\n      \"ttsEnabled\": {\n        \"label\": \"تمكين تحويل النص إلى كلام\"\n      },\n      \"ttsAutoPlay\": {\n        \"label\": \"تشغيل الرد الصوتي تلقائياً بعد الاكتمال\"\n      },\n      \"ttsProvider\": {\n        \"label\": \"مزود خدمة تحويل النص إلى كلام\",\n        \"placeholder\": \"اختر مزود خدمة\"\n      },\n      \"ttsVoice\": {\n        \"label\": \"صوت تحويل النص إلى كلام\",\n        \"placeholder\": \"اختر صوتاً\"\n      },\n      \"ssmlEnabled\": {\n        \"label\": \"تمكين SSML (لغة ترميز توليف الكلام)\"\n      },\n      \"removeReasoningTagTTS\": {\n        \"label\": \"إزالة علامة التفكير من تحويل النص إلى كلام\"\n      }\n    },\n    \"stt\": {\n      \"heading\": \"إعدادات تحويل الكلام إلى نص\",\n      \"autoStopTimeout\": {\n        \"label\": \"مهلة التوقف التلقائي (مللي ثانية)\",\n        \"placeholder\": \"أدخل مهلة التوقف التلقائي بالمللي ثانية\"\n      },\n      \"autoSubmitVoiceMessage\": {\n        \"label\": \"إرسال الرسالة الصوتية تلقائياً\"\n      }\n    }\n  },\n  \"manageModels\": {\n    \"title\": \"إدارة النماذج\",\n    \"addBtn\": \"إضافة نموذج جديد\",\n    \"columns\": {\n      \"name\": \"الاسم\",\n      \"digest\": \"الملخص\",\n      \"modifiedAt\": \"تم التعديل في\",\n      \"size\": \"الحجم\",\n      \"actions\": \"الإجراءات\"\n    },\n    \"expandedColumns\": {\n      \"parentModel\": \"النموذج الأصلي\",\n      \"format\": \"التنسيق\",\n      \"family\": \"العائلة\",\n      \"parameterSize\": \"حجم المعلمة\",\n      \"quantizationLevel\": \"مستوى التكميم\"\n    },\n    \"tooltip\": {\n      \"delete\": \"حذف النموذج\",\n      \"repull\": \"إعادة سحب النموذج\"\n    },\n    \"confirm\": {\n      \"delete\": \"هل أنت متأكد أنك تريد حذف هذا النموذج؟\",\n      \"repull\": \"هل أنت متأكد أنك تريد إعادة سحب هذا النموذج؟\"\n    },\n    \"modal\": {\n      \"title\": \"إضافة نموذج جديد\",\n      \"placeholder\": \"أدخل اسم النموذج\",\n      \"pull\": \"سحب النموذج\"\n    },\n    \"notification\": {\n      \"pullModel\": \"جاري سحب النموذج\",\n      \"pullModelDescription\": \"جاري سحب نموذج {{modelName}}. لمزيد من التفاصيل، تحقق من أيقونة الإضافة.\",\n      \"success\": \"نجاح\",\n      \"error\": \"خطأ\",\n      \"successDescription\": \"تم سحب النموذج بنجاح\",\n      \"successDeleteDescription\": \"تم حذف النموذج بنجاح\",\n      \"someError\": \"حدث خطأ ما. يرجى المحاولة مرة أخرى لاحقاً\"\n    }\n  },\n  \"managePrompts\": {\n    \"title\": \"إدارة الإرشادات\",\n    \"addBtn\": \"إضافة إرشاد جديد\",\n    \"option1\": \"عادي\",\n    \"option2\": \"RAG\",\n    \"questionPrompt\": \"إرشاد السؤال\",\n    \"segmented\": {\n      \"custom\": \"إرشادات مخصصة\",\n      \"copilot\": \"إرشادات كوبيلوت\"\n    },\n    \"columns\": {\n      \"title\": \"العنوان\",\n      \"prompt\": \"الإرشاد\",\n      \"type\": \"نوع الإرشاد\",\n      \"actions\": \"الإجراءات\"\n    },\n    \"systemPrompt\": \"إرشاد النظام\",\n    \"quickPrompt\": \"إرشاد سريع\",\n    \"tooltip\": {\n      \"delete\": \"حذف الإرشاد\",\n      \"edit\": \"تعديل الإرشاد\"\n    },\n    \"confirm\": {\n      \"delete\": \"هل أنت متأكد أنك تريد حذف هذا الإرشاد؟ لا يمكن التراجع عن هذا الإجراء.\"\n    },\n    \"modal\": {\n      \"addTitle\": \"إضافة إرشاد جديد\",\n      \"editTitle\": \"تعديل الإرشاد\"\n    },\n    \"form\": {\n      \"title\": {\n        \"label\": \"العنوان\",\n        \"placeholder\": \"إرشادي الرائع\",\n        \"required\": \"الرجاء إدخال عنوان\"\n      },\n      \"prompt\": {\n        \"label\": \"الإرشاد\",\n        \"placeholder\": \"أدخل الإرشاد\",\n        \"required\": \"الرجاء إدخال إرشاد\",\n        \"help\": \"يمكنك استخدام {key} كمتغير في إرشادك.\",\n        \"missingTextPlaceholder\": \"المتغير {text} مفقود في الإرشاد. الرجاء إضافته.\"\n      },\n      \"isSystem\": {\n        \"label\": \"إرشاد نظام\"\n      },\n      \"btnSave\": {\n        \"saving\": \"جاري إضافة الإرشاد...\",\n        \"save\": \"إضافة الإرشاد\"\n      },\n      \"btnEdit\": {\n        \"saving\": \"جاري تحديث الإرشاد...\",\n        \"save\": \"تحديث الإرشاد\"\n      }\n    },\n    \"notification\": {\n      \"addSuccess\": \"تمت إضافة الإرشاد\",\n      \"addSuccessDesc\": \"تمت إضافة الإرشاد بنجاح\",\n      \"error\": \"خطأ\",\n      \"someError\": \"حدث خطأ ما. يرجى المحاولة مرة أخرى لاحقاً\",\n      \"updatedSuccess\": \"تم تحديث الإرشاد\",\n      \"updatedSuccessDesc\": \"تم تحديث الإرشاد بنجاح\",\n      \"deletedSuccess\": \"تم حذف الإرشاد\",\n      \"deletedSuccessDesc\": \"تم حذف الإرشاد بنجاح\"\n    }\n  },\n  \"manageShare\": {\n    \"title\": \"إدارة المشاركة\",\n    \"heading\": \"تكوين رابط مشاركة الصفحة\",\n    \"form\": {\n      \"url\": {\n        \"label\": \"رابط مشاركة الصفحة\",\n        \"placeholder\": \"أدخل رابط مشاركة الصفحة\",\n        \"required\": \"الرجاء إدخال رابط مشاركة الصفحة!\",\n        \"help\": \"لأسباب تتعلق بالخصوصية، يمكنك استضافة مشاركة الصفحة ذاتياً وتوفير الرابط هنا. <anchor>تعلم المزيد</anchor>.\"\n      }\n    },\n    \"webshare\": {\n      \"heading\": \"مشاركة الويب\",\n      \"columns\": {\n        \"title\": \"العنوان\",\n        \"url\": \"الرابط\",\n        \"actions\": \"الإجراءات\"\n      },\n      \"tooltip\": {\n        \"delete\": \"حذف المشاركة\"\n      },\n      \"confirm\": {\n        \"delete\": \"هل أنت متأكد أنك تريد حذف هذه المشاركة؟ لا يمكن التراجع عن هذا الإجراء.\"\n      },\n      \"label\": \"إدارة مشاركة الصفحة\",\n      \"description\": \"تمكين أو تعطيل ميزة مشاركة الصفحة\"\n    },\n    \"notification\": {\n      \"pageShareSuccess\": \"تم تحديث رابط مشاركة الصفحة بنجاح\",\n      \"someError\": \"حدث خطأ ما. يرجى المحاولة مرة أخرى لاحقاً\",\n      \"webShareDeleteSuccess\": \"تم حذف مشاركة الويب بنجاح\"\n    }\n  },\n  \"ollamaSettings\": {\n    \"title\": \"إعدادات Ollama\",\n    \"heading\": \"تكوين Ollama\",\n    \"settings\": {\n      \"ollamaUrl\": {\n        \"label\": \"رابط Ollama\",\n        \"placeholder\": \"أدخل رابط Ollama\"\n      },\n      \"globalEnable\": {\n        \"label\": \"تمكين أو تعطيل تكامل Ollama عالمياً\",\n        \"warning\": \"عند تعطيل تكامل Ollama عالمياً، لن يقوم مساعد الصفحة بجلب النماذج من Ollama. لا يزال بإمكانك إضافة نسخة Ollama من قسم <anchor>واجهة برمجة التطبيقات المتوافقة مع OpenAI</anchor> والتي ستعمل بشكل جيد.\"\n      },\n      \"advanced\": {\n        \"label\": \"تكوين رابط Ollama المتقدم\",\n        \"urlRewriteEnabled\": {\n          \"label\": \"تمكين أو تعطيل رابط المصدر المخصص\"\n        },\n        \"rewriteUrl\": {\n          \"label\": \"رابط المصدر المخصص\",\n          \"placeholder\": \"أدخل رابط المصدر المخصص\"\n        },\n        \"autoCORSFix\": {\n          \"label\": \"تمكين أو تعطيل إصلاح CORS التلقائي لـ Ollama\"\n        },\n        \"headers\": {\n          \"label\": \"الترويسات المخصصة\",\n          \"add\": \"إضافة ترويسة\",\n          \"key\": {\n            \"label\": \"مفتاح الترويسة\",\n            \"placeholder\": \"التفويض\"\n          },\n          \"value\": {\n            \"label\": \"قيمة الترويسة\",\n            \"placeholder\": \"رمز Bearer\"\n          }\n        },\n        \"help\": \"إذا كنت تواجه مشاكل في الاتصال مع Ollama في مساعد الصفحة، يمكنك تكوين رابط مصدر مخصص. لمعرفة المزيد حول التكوين، <anchor>انقر هنا</anchor>.\"\n      }\n    }\n  },\n  \"manageSearch\": {\n    \"title\": \"إدارة البحث في الويب\",\n    \"heading\": \"تكوين البحث في الويب\"\n  },\n  \"about\": {\n    \"title\": \"حول\",\n    \"heading\": \"حول\",\n    \"chromeVersion\": \"إصدار مساعد الصفحة\",\n    \"ollamaVersion\": \"إصدار Ollama\",\n    \"support\": \"يمكنك دعم مشروع مساعد الصفحة من خلال التبرع أو الرعاية عبر المنصات التالية:\",\n    \"koFi\": \"ادعم على Ko-fi\",\n    \"githubSponsor\": \"كن راعياً على GitHub\",\n    \"githubRepo\": \"مستودع GitHub\"\n  },\n  \"manageKnowledge\": {\n    \"title\": \"إدارة المعرفة\",\n    \"heading\": \"تكوين قاعدة المعرفة\"\n  },\n  \"rag\": {\n    \"title\": \"إعدادات Pipeline \",\n    \"ragSettings\": {\n      \"label\": \"إعدادات RAG\",\n      \"model\": {\n        \"label\": \"نموذج التضمين\",\n        \"required\": \"الرجاء اختيار نموذج\",\n        \"help\": \"يوصى بشدة باستخدام نماذج التضمين مثل `nomic-embed-text`.\",\n        \"placeholder\": \"اختر نموذجاً\"\n      },\n      \"chunkSize\": {\n        \"label\": \"حجم القطعة\",\n        \"placeholder\": \"أدخل حجم القطعة\",\n        \"required\": \"الرجاء إدخال حجم القطعة\"\n      },\n      \"chunkOverlap\": {\n        \"label\": \"تداخل القطع\",\n        \"placeholder\": \"أدخل تداخل القطع\",\n        \"required\": \"الرجاء إدخال تداخل القطع\"\n      },\n      \"totalFilePerKB\": {\n        \"label\": \"حد رفع الملفات الافتراضي لقاعدة المعرفة\",\n        \"placeholder\": \"أدخل حد رفع الملفات الافتراضي (مثال: 10)\",\n        \"required\": \"الرجاء إدخال حد رفع الملفات الافتراضي\"\n      },\n      \"noOfRetrievedDocs\": {\n        \"label\": \"عدد المستندات المسترجعة\",\n        \"placeholder\": \"أدخل عدد المستندات المسترجعة\",\n        \"required\": \"الرجاء إدخال عدد المستندات المسترجعة\"\n      },\n      \"splittingSeparator\": {\n        \"label\": \"الفاصل\",\n        \"placeholder\": \"أدخل الفاصل (مثال: \\\\n\\\\n)\",\n        \"required\": \"الرجاء إدخال الفاصل\"\n      },\n      \"splittingStrategy\": {\n        \"label\": \"مقسم النص\"\n      }\n    },\n    \"prompt\": {\n      \"label\": \"تكوين إرشاد RAG\",\n      \"option1\": \"عادي\",\n      \"option2\": \"ويب\",\n      \"alert\": \"تكوين إرشاد النظام هنا مهمل. يرجى استخدام قسم إدارة الإرشادات لإضافة أو تعديل الإرشادات. سيتم إزالة هذا القسم في إصدار مستقبلي\",\n      \"systemPrompt\": \"إرشاد النظام\",\n      \"systemPromptPlaceholder\": \"أدخل إرشاد النظام\",\n      \"webSearchPrompt\": \"إرشاد بحث الويب\",\n      \"webSearchPromptHelp\": \"لا تقم بإزالة `{search_results}` من الإرشاد.\",\n      \"webSearchPromptError\": \"الرجاء إدخال إرشاد بحث الويب\",\n      \"webSearchPromptPlaceholder\": \"أدخل إرشاد بحث الويب\",\n      \"webSearchFollowUpPrompt\": \"إرشاد متابعة بحث الويب\",\n      \"webSearchFollowUpPromptHelp\": \"لا تقم بإزالة `{chat_history}` و `{question}` من الإرشاد.\",\n      \"webSearchFollowUpPromptError\": \"الرجاء إدخال إرشاد متابعة بحث الويب!\",\n      \"webSearchFollowUpPromptPlaceholder\": \"إرشاد متابعة بحث الويب الخاص بك\"\n    }\n  },\n  \"chromeAiSettings\": {\n    \"title\": \"إعدادات Chrome AI\"\n  }\n}\n"
  },
  {
    "path": "src/assets/locale/ar/sidepanel.json",
    "content": "{\n    \"tooltip\": {\n        \"embed\": \"قد يستغرق تضمين الصفحة بضع دقائق. يرجى الانتظار...\",\n        \"clear\": \"مسح سجل المحادثة\",\n        \"history\": \"سجل المحادثة\",\n        \"openwebui\": \"فتح واجهة الويب\"\n    }\n}"
  },
  {
    "path": "src/assets/locale/da/chrome.json",
    "content": "{\n    \"heading\": \"Konfigurer Chrome AI\",\n    \"status\": {\n        \"label\": \"Tænd eller sluk Chrome AI Support på Page Assist\"\n    },\n    \"error\": {\n        \"browser_not_supported\": \"Denne version af Chrome er ikke understøttet af Gemini Nano modelen. Opdater venligst til version 127 eller nyere\",\n        \"ai_not_supported\": \"Indstillingen chrome://flags/#prompt-api-for-gemini-nano er ikke tændt. Venligst tænd for indstillingen.\",\n        \"ai_not_ready\": \"Gemini Nano er ikke tilgængelig; du er nødt til at double-cheke Chrome indstillingerne.\",\n        \"internal_error\": \"Der opstod en intern fejl. Prøv venligst igen senere.\"\n    },\n    \"errorDescription\": \"For at bruge Chrome AI skal du bruge en browserversion, der er nyere end 138. Følg disse trin:\\n\\n1. Gå til `chrome://flags/#prompt-api-for-gemini-nano` og aktiver \\\"Prompt API for Gemini Nano\\\".\\n2. Genstart Chrome for at anvende flaget.\\n3. Gå tilbage til denne side og klik på \\\"Download Model\\\" — dette downloader en 4GB model for første gang.\\n4. Når den er downloadet, kan Gemini Nano aktiveres gennem Page Assist.\",\n    \"downloadModel\": \"Download model\",\n    \"modelDownloadWarning\": \"Dette vil downloade en model med en omtrentlig downloadstørrelse på mellem 1,5 GB og 2,4 GB. Sørg for, at du har tilstrækkelig diskplads.\"\n}"
  },
  {
    "path": "src/assets/locale/da/common.json",
    "content": "{\n    \"pageAssist\": \"Page Assist\",\n    \"selectAModel\": \"Vælg en Model\",\n    \"save\": \"Gem\",\n    \"saved\": \"Gemt\",\n    \"cancel\": \"Fortryd\",\n    \"retry\": \"Prøv igen\",\n    \"share\": {\n        \"tooltip\": {\n            \"share\": \"Del\"\n        },\n        \"modal\": {\n            \"title\": \"Del link til Chatten\"\n        },\n        \"form\": {\n            \"defaultValue\": {\n                \"name\": \"Anonym\",\n                \"title\": \"Unavngivet chat\"\n            },\n            \"title\": {\n                \"label\": \"Chattitel\",\n                \"placeholder\": \"Indtast chattitel\",\n                \"required\": \"Chattitel er nødvendig\"\n            },\n            \"name\": {\n                \"label\": \"Dit navn\",\n                \"placeholder\": \"Indtast dit navn\",\n                \"required\": \"Dit navn er nødvendig\"\n            },\n            \"btn\": {\n                \"save\": \"Generer et Link\",\n                \"saving\": \"Generering af link...\"\n            }\n        },\n        \"notification\": {\n            \"successGenerate\": \"Link kopied til udklipsholder\",\n            \"failGenerate\": \"Kunne ikke generere link\"\n        }\n    },\n    \"copyToClipboard\": \"Kopier til udklipsholder\",\n    \"webSearch\": \"Søger på internettet\",\n    \"regenerate\": \"Regenerer\",\n    \"continue\": \"Fortsæt svar\",\n    \"edit\": \"Ændre\",\n    \"delete\": \"Slet\",\n    \"saveAndSubmit\": \"Gem & Indsend\",\n    \"editMessage\": {\n        \"placeholder\": \"Skriv en besked...\"\n    },\n    \"submit\": \"Indsend\",\n    \"noData\": \"Igen data\",\n    \"noHistory\": \"Igen chat history\",\n    \"chatWithCurrentPage\": \"Chat med nuværende side\",\n    \"beta\": \"Beta\",\n    \"tts\": \"Læs op\",\n    \"currentChatModelSettings\": \"Nuværende chat model indstillinger\",\n    \"modelSettings\": {\n        \"label\": \"Model Indstillinger\",\n        \"description\": \"Konfigurer model indstillingerne alle chats\",\n        \"form\": {\n            \"keepAlive\": {\n                \"label\": \"Hold i live\",\n                \"help\": \"controls how long the model will stay loaded into memory following the request (standard: 5m)\",\n                \"placeholder\": \"Indtast længden af sessionen (fx. 5m, 10m, 1t)\"\n            },\n            \"temperature\": {\n                \"label\": \"Temperatur\",\n                \"placeholder\": \"Indtast Temperatur værdi (fx. 0.7, 1.0)\"\n            },\n            \"numCtx\": {\n                \"label\": \"Kontekst Vindue Størrelse (num_ctx)\",\n                \"placeholder\": \"Indtast Kontekst Vindue Størrelse værdi (standard: 2048)\"\n            },\n            \"numPredict\": {\n                \"label\": \"Maks Tokens (num_predict)\",\n                \"placeholder\": \"Indtast Maks Tokens værdi (fx. 2048, 4096)\"\n            },\n            \"seed\": {\n                \"label\": \"Seed\",\n                \"placeholder\": \"Indtast Seed værdi (fx. 1234)\",\n                \"help\": \"Reproducerbarhed af modeloutput\"\n            },\n            \"topK\": {\n                \"label\": \"Top K\",\n                \"placeholder\": \"Indtast Top K værdi (fx. 40, 100)\"\n            },\n            \"topP\": {\n                \"label\": \"Top P\",\n                \"placeholder\": \"Indtast Top P value (fx. 0.9, 0.95)\"\n            },\n            \"numGpu\": {\n                \"label\": \"Antal GPU'er\",\n                \"placeholder\": \"Indtast antallet af lag, som sendes til GPU('er)\"\n            },\n            \"systemPrompt\": {\n                \"label\": \"Midlertidige System Prompt\",\n                \"placeholder\": \"Indtast System Prompt\",\n                \"help\": \"Dette er en hurtig måde at indstille systemprompten i den aktuelle chat, som vil tilsidesætte den valgte systemprompt, hvis den findes.\"\n            }\n        },\n        \"advanced\": \"Flere Model Indstillinger\"\n    },\n    \"copilot\": {\n        \"summary\": \"Opsummer\",\n        \"explain\": \"Forklar\",\n        \"rephrase\": \"Omskriv\",\n        \"translate\": \"Oversæt\",\n        \"custom\": \"Brugerdefineret\"\n    },\n    \"citations\": \"Citater\",\n    \"downloadCode\": \"Download Kode\",\n    \"date\": {\n        \"pinned\": \"Fastgjort\",\n        \"today\": \"I dag\",\n        \"yesterday\": \"I går\",\n        \"last7Days\": \"Sidste 7 dage\",\n        \"older\": \"Ældre\"\n    },\n    \"range\": {\n        \"deleteConfirm\": {\n            \"pinned\": \"Er du sikker på, at du vil slette alle fastgjorte beskeder?\",\n            \"today\": \"Er du sikker på, at du vil slette alle beskeder fra i dag?\",\n            \"yesterday\": \"Er du sikker på, at du vil slette alle beskeder fra i går?\",\n            \"last7Days\": \"Er du sikker på, at du vil slette alle beskeder fra de sidste 7 dage?\",\n            \"older\": \"Er du sikker på, at du vil slette alle ældre beskeder?\"\n        },\n        \"tooltip\": {\n            \"pinned\": \"Slet alle fastgjorte beskeder\",\n            \"today\": \"Slet alle beskeder fra i dag\",\n            \"yesterday\": \"Slet alle beskeder fra i går\",\n            \"last7Days\": \"Slet alle beskeder fra de sidste 7 dage\",\n            \"older\": \"Slet alle ældre beskeder\"\n        }\n    },\n    \"pin\": \"Fastgør\",\n    \"unpin\": \"Frigør\",\n    \"generationInfo\": \"Genererings Info\",\n    \"sidebarChat\": \"Sidepanel Chat\",\n    \"reasoning\": {\n        \"thinking\": \"Tænker....\",\n        \"thought\": \"Tænkte i {{time}}\"\n    },\n    \"embeddingGen\": \"Opretter embeddings, dette kan tage en stund\",\n    \"semanticSearch\": \"Udfører semantisk søgning\",\n    \"downloading\": \"Downloader\",\n    \"cancelPullingModel\": {\n        \"confirm\": \"Er du sikker på, at du vil annullere downloadet? Dette stopper downloadprocessen. Ifølge Ollama-dokumentationen kan du genoptage, hvor du slap.\"\n    }\n}"
  },
  {
    "path": "src/assets/locale/da/knowledge.json",
    "content": "{\n    \"addBtn\": \"Tilføj Ny Viden\",\n    \"columns\": {\n        \"title\": \"Titel\",\n        \"status\": \"Status\",\n        \"embeddings\": \"Embedding Model\",\n        \"createdAt\": \"Oprettet At\",\n        \"action\": \"Handlinger\"\n    },\n    \"expandedColumns\": {\n        \"name\": \"Navn\"\n    },\n    \"confirm\": {\n        \"delete\": \"Er du sikker på du vil slette denne viden?\"\n    },\n    \"deleteSuccess\": \"Viden slettet med success\",\n    \"status\": {\n        \"pending\": \"Venter\",\n        \"finished\": \"Færdig\",\n        \"processing\": \"Processerer\",\n        \"failed\": \"Fejlet\"\n    },\n    \"addKnowledge\": \"Tilføj Viden\",\n    \"updateKnowledge\": \"Tilføj Kilde\",\n    \"form\": {\n        \"title\": {\n            \"label\": \"Viden Titel\",\n            \"placeholder\": \"Indtast viden titel\",\n            \"required\": \"Viden titel er nødvendig\"\n        },\n        \"uploadFile\": {\n            \"label\": \"Upload Filer\",\n            \"uploadText\": \"Træk og slip denne fil here og klik upload\",\n            \"uploadHint\": \"Understøttet filtyper: .pdf, .csv, .txt, .md, .docx\",\n            \"required\": \"En fil er nødvendig\"\n        },\n        \"submit\": \"Indsend\",\n        \"success\": \"Viden tilføjet med success\"\n    },\n    \"noEmbeddingModel\": \"Tilføj venligst en embedding model fra RAG indstillingerne først\"\n}"
  },
  {
    "path": "src/assets/locale/da/openai.json",
    "content": "{\n    \"settings\": \"OpenAI Kompatibel API\",\n    \"heading\": \"OpenAI kompatibel API\",\n    \"subheading\": \"Administrer og konfigurer dine OpenAI API-kompatible udbydere her.\",\n    \"addBtn\": \"Tilføj Udbyder\",\n    \"table\": {\n        \"name\": \"Udbyders Navn\",\n        \"baseUrl\": \"Basis URL\",\n        \"actions\": \"Handling\"\n    },\n    \"modal\": {\n        \"titleAdd\": \"Tilføj Ny Udbyder\",\n        \"name\": {\n            \"label\": \"Udbyders Navn\",\n            \"required\": \"Udbyders navn er påkrævet.\",\n            \"placeholder\": \"Indtast udbyders navn\"\n        },\n        \"baseUrl\": {\n            \"label\": \"Basis URL\",\n            \"help\": \"Basis URL'en for OpenAI API-udbyderen. f.eks. (http://localhost:1234/v1)\",\n            \"required\": \"Basis URL er påkrævet.\",\n            \"placeholder\": \"Indtast basis URL\"\n        },\n        \"apiKey\": {\n            \"label\": \"API Nøgle\",\n            \"required\": \"API Nøgle er påkrævet.\",\n            \"placeholder\": \"Indtast API Nøgle\"\n        },\n        \"submit\": \"Gem\",\n        \"update\": \"Opdater\",\n        \"deleteConfirm\": \"Er du sikker på, at du vil slette denne udbyder?\",\n        \"model\": {\n            \"title\": \"Modelliste\",\n            \"subheading\": \"Vælg venligst de chatmodeller, du ønsker at bruge med denne udbyder.\",\n            \"success\": \"Nye modeller tilføjet med succes.\"\n        },\n        \"tipLMStudio\": \"Page Assist vil automatisk hente de modeller, du har indlæst på LM Studio. Du behøver ikke at tilføje dem manuelt.\"\n    },\n    \"addSuccess\": \"Udbyder tilføjet med succes.\",\n    \"deleteSuccess\": \"Udbyder slettet med succes.\",\n    \"updateSuccess\": \"Udbyder opdateret med succes.\",\n    \"delete\": \"Slet\",\n    \"edit\": \"Rediger\",\n    \"newModel\": \"Tilføj Modeller til Udbyder\",\n    \"noNewModel\": \"For LMStudio, Ollama, Llamafile, henter vi dynamisk. Ingen manuel tilføjelse nødvendig.\",\n    \"searchModel\": \"Søg Model\",\n    \"selectAll\": \"Vælg Alle\",\n    \"save\": \"Gem\",\n    \"saving\": \"Gemmer...\",\n    \"manageModels\": {\n        \"columns\": {\n            \"name\": \"Modelnavn\",\n            \"model_type\": \"Modeltype\",\n            \"model_id\": \"Model-ID\",\n            \"provider\": \"Udbyders Navn\",\n            \"actions\": \"Handling\",\n            \"nickname\": \"Model Kaldenavn\"\n        },\n        \"tooltip\": {\n            \"delete\": \"Slet\"\n        },\n        \"confirm\": {\n            \"delete\": \"Er du sikker på, at du vil slette denne model?\"\n        },\n        \"modal\": {\n            \"title\": \"Tilføj Brugerdefineret Model\",\n            \"form\": {\n                \"name\": {\n                    \"label\": \"Model-ID\",\n                    \"placeholder\": \"llama3.2\",\n                    \"required\": \"Model-ID er påkrævet.\"\n                },\n                \"provider\": {\n                    \"label\": \"Udbyder\",\n                    \"placeholder\": \"Vælg udbyder\",\n                    \"required\": \"Udbyder er påkrævet.\"\n                },\n                \"type\": {\n                    \"label\": \"Modeltype\"\n                }\n            }\n        }\n    },\n    \"noModelFound\": \"Ingen model fundet. Sørg for, at du har tilføjet korrekt udbyder med basis URL og API-nøgle.\",\n    \"radio\": {\n        \"chat\": \"Chatmodel\",\n        \"embedding\": \"Indlejringsmodel\",\n        \"chatInfo\": \"bruges til chatfuldførelse og samtalegeneration\",\n        \"embeddingInfo\": \"bruges til RAG og andre semantiske søgerelaterede opgaver.\"\n    },\n    \"nicknameModal\": {\n        \"title\": \"Tilføj / Rediger Model Kaldenavn\",\n        \"form\": {\n            \"modelName\": {\n                \"label\": \"Modelnavn\",\n                \"placeholder\": \"Indtast modelnavn\",\n                \"required\": \"Modelnavn er påkrævet.\"\n            },\n            \"modelAvatar\": {\n                \"label\": \"Model Avatar\",\n                \"placeholder\": \"Indtast model avatar\",\n                \"help\": \"Indtast venligst URL'en til model avataren. Dette billede vil blive vist i chatvinduet.\"\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/assets/locale/da/option.json",
    "content": "{\n    \"newChat\": \"Ny Chat\",\n    \"selectAPrompt\": \"Vælg en Prompt\",\n    \"githubRepository\": \"GitHub Repository\",\n    \"settings\": \"Indstillinger\",\n    \"sidebarTitle\": \"Chathistorik\",\n    \"error\": \"Fejl\",\n    \"somethingWentWrong\": \"Noget gik galt\",\n    \"validationSelectModel\": \"Venligst vælg en model for at forsæætte\",\n    \"deleteHistoryConfirmation\": \"Er du sikker på at du vil slette denne historik?\",\n    \"editHistoryTitle\": \"Indtast en ny titel\",\n    \"temporaryChat\": \"Midlertidig Chat\",\n    \"more\": {\n        \"copy\": {\n            \"group\": \"Kopier\",\n            \"asText\": \"Kopier som tekst\",\n            \"asMarkdown\": \"Kopier som Markdown\",\n            \"success\": \"Kopieret til udklipsholder!\"\n        },\n        \"download\": {\n            \"group\": \"Download\",\n            \"text\": \"Tekstfil (.txt)\",\n            \"markdown\": \"Markdown (.md)\",\n            \"json\": \"JSON-fil (.json)\"\n        },\n        \"share\": \"Del\"\n    }\n}"
  },
  {
    "path": "src/assets/locale/da/playground.json",
    "content": "{\n    \"ollamaState\": {\n        \"searching\": \"Søger efter din Ollama 🦙\",\n        \"running\": \"Ollama kør 🦙\",\n        \"notRunning\": \"Kan ikke oprette forbindelse til Ollama 🦙\",\n        \"connectionError\": \"Det lader til, at du har en forbindelsesfejl. Se venligst denne <anchor>dokumentation</anchor> for fejlfinding.\"\n    },\n    \"formError\": {\n        \"noModel\": \"Vælg venligst en model\",\n        \"noEmbeddingModel\": \"Vælg venligst en embedding model under indstillinger > RAG side\"\n    },\n    \"form\": {\n        \"textarea\": {\n            \"placeholder\": \"Skriv en besked...\"\n        },\n        \"webSearch\": {\n            \"on\": \"Til\",\n            \"off\": \"Fra\"\n        }\n    },\n    \"tooltip\": {\n        \"searchInternet\": \"Søg Internettet\",\n        \"speechToText\": \"Tal til Tekst\",\n        \"uploadImage\": \"Upload Billed\",\n        \"stopStreaming\": \"Stop Streaming\",\n        \"knowledge\": \"Viden\",\n        \"clearContext\": \"Ryd Kontekst\"\n    },\n    \"sendWhenEnter\": \"Søg, når Indtast trykkes\",\n    \"welcome\": \"Hej! Hvordan kan jeg hjælpe dig i dag?\",\n    \"useOCR\": \"Udtræk tekst fra billede (OCR)\"\n}"
  },
  {
    "path": "src/assets/locale/da/settings.json",
    "content": "{\n  \"generalSettings\": {\n    \"title\": \"Generelle Indstillinger\",\n    \"settings\": {\n      \"heading\": \"Web UI Indstillinger\",\n      \"speechRecognitionLang\": {\n        \"label\": \"Talegenkendelse Sprog\",\n        \"placeholder\": \"Vælg et sprog\"\n      },\n      \"language\": {\n        \"label\": \"Sprog\",\n        \"placeholder\": \"Vælg et sprog\"\n      },\n      \"darkMode\": {\n        \"label\": \"Ændre Tema\",\n        \"options\": {\n          \"light\": \"Lyst\",\n          \"dark\": \"Mørkt\"\n        }\n      },\n      \"copilotResumeLastChat\": {\n        \"label\": \"Genoptag den sidste chat, når du åbner SidePanel (copilot)\"\n      },\n      \"webUIResumeLastChat\": {\n        \"label\": \"Genoptag den sidste chat, når du åbner Web UI'en\"\n      },\n      \"hideCurrentChatModelSettings\": {\n        \"label\": \"Skjul nuværende chat model indstillinger\"\n      },\n      \"restoreLastChatModel\": {\n        \"label\": \"Gendan sidste brugte chatmodel fremtidigt\"\n      },\n      \"sendNotificationAfterIndexing\": {\n        \"label\": \"Send besked efter færdigbehandling af vidensbasen\"\n      },\n      \"generateTitle\": {\n        \"label\": \"Generer titel med AI\"\n      },\n      \"ollamaStatus\": {\n        \"label\": \"Aktivér eller deaktivér Ollama forbindelsesstatus kontrol\"\n      },\n      \"wideMode\": {\n        \"label\": \"Aktivér bredskærm tilstand\"\n      },\n      \"openReasoning\": {\n        \"label\": \"Åbn Ræsonnement Sammenfoldet som standard\"\n      },\n      \"userChatBubble\": {\n        \"label\": \"Brug chatboble til brugermeddelelser\"\n      },\n      \"autoCopyResponseToClipboard\": {\n        \"label\": \"Kopiér automatisk svar til udklipsholder\"\n      },\n      \"useMarkdownForUserMessage\": {\n        \"label\": \"Aktivér Markdown formatering for brugermeddelelser\"\n      },\n      \"copyAsFormattedText\": {\n        \"label\": \"Kopiér som formateret tekst\"\n      },\n      \"tabMentionsEnabled\": {\n        \"label\": \"Aktivér Tab Omtaler (@tab)\"\n      },\n      \"pasteLargeTextAsFile\": {\n        \"label\": \"Indsæt stor tekst som fil\"\n      },\n      \"sidepanelTemporaryChat\": {\n        \"label\": \"Aktivér midlertidig chat i SidePanel som standard\"\n      }\n    },\n    \"sidepanelRag\": {\n      \"heading\": \"Vidensbase Indstillinger\",\n      \"ragEnabled\": {\n        \"label\": \"Aktivér Indeksering og Genfinding\"\n      },\n      \"maxWebsiteContext\": {\n        \"label\": \"Maksimal Indholdsstørrelse for Fuld Kontekst Tilstand\",\n        \"placeholder\": \"Indholdsstørrelse (standard 4028)\"\n      }\n    },\n    \"webSearch\": {\n      \"heading\": \"Administrer Web Søgning\",\n      \"searchMode\": {\n        \"label\": \"Søge Tilstand\"\n      },\n      \"provider\": {\n        \"label\": \"Søgemaskine\",\n        \"placeholder\": \"Vælg en søgemaskine\"\n      },\n      \"totalSearchResults\": {\n        \"label\": \"Antal søgeresultater\",\n        \"placeholder\": \"Indtast antal Søgeresultater\"\n      },\n      \"visitSpecificWebsite\": {\n        \"label\": \"Besøg websitet nævnt i samtalen\"\n      },\n      \"searxng\": {\n        \"url\": {\n          \"label\": \"SearXNG URL\"\n        }\n      },\n      \"braveApi\": {\n        \"label\": \"Brave API Nøgle\",\n        \"placeholder\": \"Indtast din Brave API nøgle\"\n      },\n      \"searchOnByDefault\": {\n        \"label\": \"Internet Søgning TIL som standard\"\n      }\n    },\n    \"system\": {\n      \"heading\": \"Systemindstillinger\",\n      \"storageSyncEnabled\": {\n        \"label\": \"Aktivér browser lagringssynkronisering (synkroniser indstillinger på tværs af enheder)\"\n      },\n      \"deleteChatHistory\": {\n        \"label\": \"System Nulstilling\",\n        \"button\": \"Nulstil Alt\",\n        \"confirm\": \"Er du sikker på, at du vil udføre en systemnulstilling? Dette vil slette alle data og kan ikke fortrydes.\"\n      },\n      \"export\": {\n        \"label\": \"Eksporter alle data (chat-historik, vidensbase, prompts og indstillinger)\",\n        \"button\": \"Eksporter data\",\n        \"success\": \"Eksport fuldført\"\n      },\n      \"import\": {\n        \"label\": \"Importer alle data (chat-historik, vidensbase, prompts og indstillinger)\",\n        \"button\": \"Importer data\",\n        \"success\": \"Import fuldført\",\n        \"error\": \"Importfejl\"\n      }\n    },\n    \"tts\": {\n      \"heading\": \"Tekst-til-tale Indstillinger\",\n      \"ttsEnabled\": {\n        \"label\": \"Tilføj Teskt-til-Tale\"\n      },\n      \"ttsAutoPlay\": {\n        \"label\": \"Automatisk afspilning af stemmerespons efter færdiggørelse\"\n      },\n      \"ttsProvider\": {\n        \"label\": \"Tekst-til-Tale Udbyder\",\n        \"placeholder\": \"Vælg en udbyder\"\n      },\n      \"ttsVoice\": {\n        \"label\": \"Tekst-til-Tale Stemme\",\n        \"placeholder\": \"Vælg en stemme\"\n      },\n      \"ssmlEnabled\": {\n        \"label\": \"Aktiver SSML (Speech Synthesis Markup Language)\"\n      },\n      \"removeReasoningTagTTS\": {\n        \"label\": \"Fjern Ræsonnement Tag fra TTS\"\n      }\n    },\n    \"stt\": {\n      \"heading\": \"Tale-til-Tekst Indstillinger\",\n      \"autoStopTimeout\": {\n        \"label\": \"Auto Stop Timeout (ms)\",\n        \"placeholder\": \"Indtast auto-stop timeout i millisekunder\"\n      },\n      \"autoSubmitVoiceMessage\": {\n        \"label\": \"Auto Send Stemmebesked\"\n      }\n    }\n  },\n  \"manageModels\": {\n    \"title\": \"Administer Modeller\",\n    \"addBtn\": \"Tilføj ny Model\",\n    \"columns\": {\n      \"name\": \"Navn\",\n      \"digest\": \"Digest\",\n      \"modifiedAt\": \"Ændret den\",\n      \"size\": \"Størrelse\",\n      \"actions\": \"Handlinger\"\n    },\n    \"expandedColumns\": {\n      \"parentModel\": \"Forælder model\",\n      \"format\": \"Format\",\n      \"family\": \"Familie\",\n      \"parameterSize\": \"Parameterstørrelse\",\n      \"quantizationLevel\": \"kvantificeringsniveau\"\n    },\n    \"tooltip\": {\n      \"delete\": \"Slet Model\",\n      \"repull\": \"Hent Model Igen\"\n    },\n    \"confirm\": {\n      \"delete\": \"Er du sikker på, at du vil slette denne model?\",\n      \"repull\": \"Er du sikker på, at du vil hente denne model igen?\"\n    },\n    \"modal\": {\n      \"title\": \"Tilføj Ny Model\",\n      \"placeholder\": \"Indtast Modelnavn\",\n      \"pull\": \"Hent Model\"\n    },\n    \"notification\": {\n      \"pullModel\": \"Henter Model\",\n      \"pullModelDescription\": \"Henter {{modelName}} model. For flere detaljer, tjek udvidelsesikonet.\",\n      \"success\": \"Det virkede\",\n      \"error\": \"Fejl\",\n      \"successDescription\": \"Det lykkedes at hente modellen\",\n      \"successDeleteDescription\": \"Det lykkedes at slette modellen\",\n      \"someError\": \"Noget gik galt. Venligst prøv igen senere\"\n    }\n  },\n  \"managePrompts\": {\n    \"title\": \"Administrer Prompts\",\n    \"addBtn\": \"Tilføj Ny Prompt\",\n    \"option1\": \"Normal\",\n    \"option2\": \"RAG\",\n    \"questionPrompt\": \"Spørgsmålsprompt\",\n    \"segmented\": {\n      \"custom\": \"Brugerdefinerede Prompts\",\n      \"copilot\": \"Copilot Prompts\"\n    },\n    \"columns\": {\n      \"title\": \"Titel\",\n      \"prompt\": \"Prompt\",\n      \"type\": \"Prompttype\",\n      \"actions\": \"Handlinger\"\n    },\n    \"systemPrompt\": \"Systemprompt\",\n    \"quickPrompt\": \"Hurtig Prompt\",\n    \"tooltip\": {\n      \"delete\": \"Slet Prompt\",\n      \"edit\": \"Ændre Prompt\"\n    },\n    \"confirm\": {\n      \"delete\": \"Er du sikker på, at du vil slette denne prompt? Denne handling kan ikke fortrydes.\"\n    },\n    \"modal\": {\n      \"addTitle\": \"Tilføj ny Prompt\",\n      \"editTitle\": \"Ændre Prompt\"\n    },\n    \"form\": {\n      \"title\": {\n        \"label\": \"Titel\",\n        \"placeholder\": \"Min Seje Prompt\",\n        \"required\": \"Indtast venligst en titel\"\n      },\n      \"prompt\": {\n        \"label\": \"Prompt\",\n        \"placeholder\": \"Indtast Prompt\",\n        \"required\": \"Venligst indtast en prompt\",\n        \"help\": \"Du kan bruge {key} som variabel i din prompt.\",\n        \"missingTextPlaceholder\": \"Variablen {text} mangler i prompten. Tilføj venligst dette.\"\n      },\n      \"isSystem\": {\n        \"label\": \"Er Systemprompt\"\n      },\n      \"btnSave\": {\n        \"saving\": \"Tilføjer Prompt...\",\n        \"save\": \"Tilføj Prompt\"\n      },\n      \"btnEdit\": {\n        \"saving\": \"Opdaterer Prompt...\",\n        \"save\": \"Opdater Prompt\"\n      }\n    },\n    \"notification\": {\n      \"addSuccess\": \"Prompt Tilføjet\",\n      \"addSuccessDesc\": \"Prompt blev tilføjet med succes\",\n      \"error\": \"Fejl\",\n      \"someError\": \"Noget gik galt. Prøv venligst igen senere\",\n      \"updatedSuccess\": \"Prompt Opdateret\",\n      \"updatedSuccessDesc\": \"Prompt blev opdateret med succes\",\n      \"deletedSuccess\": \"Prompt Slettet\",\n      \"deletedSuccessDesc\": \"Prompt blev slettet med succes\"\n    }\n  },\n  \"manageShare\": {\n    \"title\": \"Administrer Deling\",\n    \"heading\": \"Konfigurerer Page deling URL\",\n    \"form\": {\n      \"url\": {\n        \"label\": \"Page Deling URL\",\n        \"placeholder\": \"Indtast websted deling URL\",\n        \"required\": \"Venligst indstast din Page deling URL!\",\n        \"help\": \"Af hensyn til privatliv kan du selv hoste side delingen og angive URL'en her. <anchor>Lær Mere</anchor>.\"\n      }\n    },\n    \"webshare\": {\n      \"heading\": \"Web Deling\",\n      \"columns\": {\n        \"title\": \"Titel\",\n        \"url\": \"URL\",\n        \"actions\": \"Handlinger\"\n      },\n      \"tooltip\": {\n        \"delete\": \"Slet Deling\"\n      },\n      \"confirm\": {\n        \"delete\": \"Er du sikker på du vil slette denne deling? Dette kan ikke fortrydes.\"\n      },\n      \"label\": \"Administrer Page Deling\",\n      \"description\": \"Tilføj eller disable the page share feature\"\n    },\n    \"notification\": {\n      \"pageShareSuccess\": \"Page Deling URL Updateret korrekt\",\n      \"someError\": \"Noget gik galt. Prøv venligst igen senere\",\n      \"webShareDeleteSuccess\": \"Webdeling er slettet korrekt\"\n    }\n  },\n  \"ollamaSettings\": {\n    \"title\": \"Ollama Indstillinger\",\n    \"heading\": \"Konfigurerer Ollama\",\n    \"settings\": {\n      \"ollamaUrl\": {\n        \"label\": \"Ollama URL\",\n        \"placeholder\": \"Indtast Ollama URL\"\n      },\n      \"globalEnable\": {\n        \"label\": \"Aktiver eller Deaktiver Ollama Integration Globalt\",\n        \"warning\": \"Ved at deaktivere Ollama-integration globalt vil Page Assist ikke hente modeller fra Ollama. Du kan stadig tilføje Ollama-instans fra <anchor>OpenAI-kompatibel API</anchor> sektionen, som vil fungere fint.\"\n      },\n      \"advanced\": {\n        \"label\": \"Avanceret Ollama URL Konfiguration\",\n        \"urlRewriteEnabled\": {\n          \"label\": \"Aktiver eller Deaktiver Tilpasset Oprindelses-URL\"\n        },\n        \"rewriteUrl\": {\n          \"label\": \"Tilpasset Oprindelses URL\",\n          \"placeholder\": \"Indtast tilpasset oprindelses URL\"\n        },\n        \"autoCORSFix\": {\n          \"label\": \"Aktiver eller Deaktiver Automatisk Ollama CORS Fix\"\n        },\n        \"headers\": {\n          \"label\": \"Tilpas Headers\",\n          \"Tilføj\": \"Tilføj Header\",\n          \"key\": {\n            \"label\": \"Header Værdi\",\n            \"placeholder\": \"Autorisation\"\n          },\n          \"value\": {\n            \"label\": \"Header Value\",\n            \"placeholder\": \"Bearer token\"\n          }\n        },\n        \"help\": \"Hvis du har forbindelsesproblemer med Ollama på Page Assist, kan du konfigurere en brugerdefineret oprindelses-URL. For mere information om konfigurationen, <anchor>klik her</anchor>.\"\n      }\n    }\n  },\n  \"manageSearch\": {\n    \"title\": \"Administrer Web Search\",\n    \"heading\": \"Konfigurerer Web Search\"\n  },\n  \"about\": {\n    \"title\": \"Om\",\n    \"heading\": \"Om\",\n    \"chromeVersion\": \"Page Assist Version\",\n    \"ollamaVersion\": \"Ollama Version\",\n    \"support\": \"Du kan støtte Page Assist-projektet ved at donere eller sponsorere via følgende platforme:\",\n    \"koFi\": \"Støt på Ko-fi\",\n    \"githubSponsor\": \"Sponsor på GitHub\",\n    \"githubRepo\": \"GitHub Repository\"\n  },\n  \"manageKnowledge\": {\n    \"title\": \"Administrer Viden\",\n    \"heading\": \"konfigurer Videnbase\"\n  },\n  \"rag\": {\n    \"title\": \"Pipeline Indstillinger\",\n    \"ragSettings\": {\n      \"label\": \"RAG Indstillinger\",\n      \"model\": {\n        \"label\": \"Embedding Model\",\n        \"required\": \"Vælg venligst en model\",\n        \"help\": \"Det anbefales stærkt at bruge indlejringsmodeller som `nomic-embed-text`.\",\n        \"placeholder\": \"Vælg a model\"\n      },\n      \"chunkSize\": {\n        \"label\": \"Chunk Størrelse\",\n        \"placeholder\": \"Indtast Chunk Størrelse\",\n        \"required\": \"Venligst indtast en chunk størrelse\"\n      },\n      \"chunkOverlap\": {\n        \"label\": \"Chunk Overlap\",\n        \"placeholder\": \"Indtast Chunk Overlap\",\n        \"required\": \"Indtast venligst chunk overlap\"\n      },\n      \"totalFilePerKB\": {\n        \"label\": \"Videnbase Standard Fil Upload Grænse\",\n        \"placeholder\": \"Indtast standard fil upload grænse (f.eks. 10)\",\n        \"required\": \"Indtast venligst standard fil upload grænsen\"\n      },\n      \"noOfRetrievedDocs\": {\n        \"label\": \"Antal Hentede Dokumenter\",\n        \"placeholder\": \"Indtast Number of Retrieved Documents\",\n        \"required\": \"Venligst indtast the number of retrieved documents\"\n      },\n      \"splittingSeparator\": {\n        \"label\": \"Separator\",\n        \"placeholder\": \"Indtast Separator (f.eks. \\\\n\\\\n)\",\n        \"required\": \"Indtast venligst en separator\"\n      },\n      \"splittingStrategy\": {\n        \"label\": \"Tekst Splitter\"\n      }\n    },\n    \"prompt\": {\n      \"label\": \"Konfigurer RAG Prompt\",\n      \"option1\": \"Normal\",\n      \"option2\": \"Web\",\n      \"alert\": \"Konfigurering af systemprompt her er forældet. Venligst brug Administrer Prompts sektionen til, at tilføje eller ændre prompts. Denne sektion vil blive fjernet i fremtidige versioner.\",\n      \"systemPrompt\": \"System Prompt\",\n      \"systemPromptPlaceholder\": \"Indtast System Prompt\",\n      \"webSearchPrompt\": \"Websøgningsprompt\",\n      \"webSearchPromptHelp\": \"Fjern ikke `{search_results}` fra prompten.\",\n      \"webSearchPromptError\": \"Venligst indtast a web search prompt\",\n      \"webSearchPromptPlaceholder\": \"Indtast Websøgningsprompt\",\n      \"webSearchFollowUpPrompt\": \"Web Search Follow Up Prompt\",\n      \"webSearchFollowUpPromptHelp\": \"Do not remove `{chat_history}` og `{question}` from the prompt.\",\n      \"webSearchFollowUpPromptError\": \"Indtast venligst din websøgning opfølgende prompt!\",\n      \"webSearchFollowUpPromptPlaceholder\": \"Din Websøgnings opfølgende Prompt\"\n    }\n  },\n  \"chromeAiSettings\": {\n    \"title\": \"Chrome AI Indstillinger\"\n  }\n}\n"
  },
  {
    "path": "src/assets/locale/da/sidepanel.json",
    "content": "{\n    \"tooltip\": {\n        \"embed\": \"Det kan tage et par minutter at indlejre siden. Vent venligst...\",\n        \"clear\": \"Slet chat historiken\",\n        \"history\": \"Chat historik\",\n        \"openwebui\": \"Åbn WebUI\"\n    }\n}"
  },
  {
    "path": "src/assets/locale/de/chrome.json",
    "content": "{\n    \"heading\": \"Chrome AI konfigurieren\",\n    \"status\": {\n        \"label\": \"Chrome AI-Unterstützung für Page Assist aktivieren oder deaktivieren\"\n    },\n    \"error\": {\n        \"browser_not_supported\": \"Diese Version von Chrome wird vom Gemini Nano-Modell nicht unterstützt. Bitte aktualisieren Sie auf Version 127 oder höher\",\n        \"ai_not_supported\": \"Die Einstellung chrome://flags/#prompt-api-for-gemini-nano ist nicht aktiviert. Bitte aktivieren Sie sie.\",\n        \"ai_not_ready\": \"Gemini Nano ist noch nicht bereit; Sie müssen die Chrome-Einstellungen überprüfen.\",\n        \"internal_error\": \"Ein interner Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.\"\n    },\n    \"errorDescription\": \"Um Chrome AI zu verwenden, benötigen Sie Chrome Version 138 oder höher. Befolgen Sie diese Schritte:\\n\\n1. Gehen Sie zu `chrome://flags/#prompt-api-for-gemini-nano` und aktivieren Sie \\\"Prompt API for Gemini Nano\\\".\\n2. Starten Sie Chrome neu, um das Flag zu übernehmen.\\n3. Kehren Sie zu dieser Seite zurück und klicken Sie auf \\\"Modell herunterladen\\\" — dies lädt beim ersten Mal ein 4-GB-Modell herunter.\\n4. Sobald das Modell heruntergeladen wurde, kann Gemini Nano über Page Assist aktiviert werden.\",\n    \"downloadModel\": \"Modell herunterladen\",\n    \"modelDownloadWarning\": \"Dies wird ein Modell mit einer ungefähren Downloadgröße von 1,5 GB bis 2,4 GB herunterladen. Stellen Sie sicher, dass Sie über ausreichend Speicherplatz verfügen.\"\n}"
  },
  {
    "path": "src/assets/locale/de/common.json",
    "content": "{\n    \"pageAssist\": \"Page Assist\",\n    \"selectAModel\": \"Modell auswählen\",\n    \"save\": \"Speichern\",\n    \"saved\": \"Gespeichert\",\n    \"cancel\": \"Abbrechen\",\n    \"retry\": \"Erneut versuchen\",\n    \"share\": {\n        \"tooltip\": {\n            \"share\": \"Teilen\"\n        },\n        \"modal\": {\n            \"title\": \"Link zum Chat teilen\"\n        },\n        \"form\": {\n            \"defaultValue\": {\n                \"name\": \"Anonym\",\n                \"title\": \"Unbenannter Chat\"\n            },\n            \"title\": {\n                \"label\": \"Chat-Titel\",\n                \"placeholder\": \"Chat-Titel eingeben\",\n                \"required\": \"Chat-Titel ist erforderlich\"\n            },\n            \"name\": {\n                \"label\": \"Ihr Name\",\n                \"placeholder\": \"Geben Sie Ihren Namen ein\",\n                \"required\": \"Ihr Name ist erforderlich\"\n            },\n            \"btn\": {\n                \"save\": \"Link generieren\",\n                \"saving\": \"Link wird generiert...\"\n            }\n        },\n        \"notification\": {\n            \"successGenerate\": \"Link in die Zwischenablage kopiert\",\n            \"failGenerate\": \"Link konnte nicht generiert werden\"\n        }\n    },\n    \"copyToClipboard\": \"In die Zwischenablage kopieren\",\n    \"webSearch\": \"Web durchsuchen\",\n    \"regenerate\": \"Neu generieren\",\n    \"edit\": \"Bearbeiten\",\n    \"delete\": \"Löschen\",\n    \"continue\": \"Antwort fortsetzen\",\n    \"saveAndSubmit\": \"Speichern & Absenden\",\n    \"editMessage\": {\n        \"placeholder\": \"Nachricht eingeben...\"\n    },\n    \"submit\": \"Absenden\",\n    \"noData\": \"Keine Daten\",\n    \"noHistory\": \"Kein Chat-Verlauf\",\n    \"chatWithCurrentPage\": \"Mit aktueller Seite chatten\",\n    \"beta\": \"Beta\",\n    \"tts\": \"Vorlesen\",\n    \"currentChatModelSettings\": \"Aktuelle Chat-Modell-Einstellungen\",\n    \"modelSettings\": {\n        \"label\": \"Modell-Einstellungen\",\n        \"description\": \"Legen Sie die Modelloptionen global für alle Chats fest\",\n        \"form\": {\n            \"keepAlive\": {\n                \"label\": \"Aktiv halten\",\n                \"help\": \"Steuert, wie lange das Modell nach der Anfrage im Speicher geladen bleibt (Standard: 5m)\",\n                \"placeholder\": \"Geben Sie die Dauer für Aktiv halten ein (z.B. 5m, 10m, 1h)\"\n            },\n            \"temperature\": {\n                \"label\": \"Temperatur\",\n                \"placeholder\": \"Geben Sie den Temperaturwert ein (z.B. 0.7, 1.0)\"\n            },\n            \"numCtx\": {\n                \"label\": \"Kontextfenstergröße (num_ctx)\",\n                \"placeholder\": \"Geben Sie die Kontextfenstergröße ein (Standard: 2048)\"\n            },\n            \"numPredict\": {\n                \"label\": \"Max Tokens (num_predict)\",\n                \"placeholder\": \"Geben Sie den Max-Tokens-Wert ein (z.B. 2048, 4096)\"\n            },\n            \"seed\": {\n                \"label\": \"Seed\",\n                \"placeholder\": \"Geben Sie den Seed-Wert ein (z.B. 1234)\",\n                \"help\": \"Reproduzierbarkeit der Modellausgabe\"\n            },\n            \"topK\": {\n                \"label\": \"Top K\",\n                \"placeholder\": \"Geben Sie den Top-K-Wert ein (z.B. 40, 100)\"\n            },\n            \"topP\": {\n                \"label\": \"Top P\",\n                \"placeholder\": \"Geben Sie den Top-P-Wert ein (z.B. 0.9, 0.95)\"\n            },\n            \"numGpu\": {\n                \"label\": \"Anzahl GPUs\",\n                \"placeholder\": \"Geben Sie die Anzahl der Ebenen ein, die an GPU(s) gesendet werden sollen\"\n            },\n            \"systemPrompt\": {\n                \"label\": \"Temporärer System-Prompt\",\n                \"placeholder\": \"System-Prompt eingeben\",\n                \"help\": \"Dies ist eine schnelle Möglichkeit, den System-Prompt im aktuellen Chat festzulegen, der den ausgewählten System-Prompt überschreibt, falls vorhanden.\"\n            }\n        },\n        \"advanced\": \"Weitere Modell-Einstellungen\"\n    },\n    \"copilot\": {\n        \"summary\": \"Zusammenfassen\",\n        \"explain\": \"Erklären\",\n        \"rephrase\": \"Umformulieren\",\n        \"translate\": \"Übersetzen\",\n        \"custom\": \"Benutzerdefiniert\"\n    },\n    \"citations\": \"Zitate\",\n    \"downloadCode\": \"Code herunterladen\",\n    \"date\": {\n        \"pinned\": \"Angepinnt\",\n        \"today\": \"Heute\",\n        \"yesterday\": \"Gestern\",\n        \"last7Days\": \"Letzte 7 Tage\",\n        \"older\": \"Älter\"\n    },\n    \"range\": {\n        \"deleteConfirm\": {\n            \"pinned\": \"Sind Sie sicher, dass Sie alle angepinnten Nachrichten löschen möchten?\",\n            \"today\": \"Sind Sie sicher, dass Sie alle Nachrichten von heute löschen möchten?\",\n            \"yesterday\": \"Sind Sie sicher, dass Sie alle Nachrichten von gestern löschen möchten?\",\n            \"last7Days\": \"Sind Sie sicher, dass Sie alle Nachrichten der letzten 7 Tage löschen möchten?\",\n            \"older\": \"Sind Sie sicher, dass Sie alle älteren Nachrichten löschen möchten?\"\n        },\n        \"tooltip\": {\n            \"pinned\": \"Alle angepinnten Nachrichten löschen\",\n            \"today\": \"Alle Nachrichten von heute löschen\",\n            \"yesterday\": \"Alle Nachrichten von gestern löschen\",\n            \"last7Days\": \"Alle Nachrichten der letzten 7 Tage löschen\",\n            \"older\": \"Alle älteren Nachrichten löschen\"\n        }\n    },\n    \"pin\": \"Anheften\",\n    \"unpin\": \"Losheften\",\n    \"generationInfo\": \"Generierungsinformationen\",\n    \"sidebarChat\": \"Seitenleisten-Chat\",\n    \"reasoning\": {\n        \"thinking\": \"Denke nach....\",\n        \"thought\": \"Gedanke für {{time}}\"\n    },\n    \"embeddingGen\": \"Erstelle Embeddings, dies kann eine Weile dauern\",\n    \"semanticSearch\": \"Führe semantische Suche durch\",\n    \"downloading\": \"Herunterladen\",\n    \"cancelPullingModel\": {\n        \"confirm\": \"Sind Sie sicher, dass Sie den Download abbrechen möchten? Dadurch wird der Downloadvorgang gestoppt. Laut der Ollama-Dokumentation können Sie dort weitermachen, wo Sie aufgehört haben.\"\n    }\n}"
  },
  {
    "path": "src/assets/locale/de/knowledge.json",
    "content": "{\n    \"addBtn\": \"Neues Wissen hinzufügen\",\n    \"columns\": {\n        \"title\": \"Titel\",\n        \"status\": \"Status\",\n        \"embeddings\": \"Einbettungsmodell\",\n        \"createdAt\": \"Erstellt am\",\n        \"action\": \"Aktionen\"\n    },\n    \"expandedColumns\": {\n        \"name\": \"Name\"\n    },\n    \"confirm\": {\n        \"delete\": \"Sind Sie sicher, dass Sie dieses Wissen löschen möchten?\"\n    },\n    \"deleteSuccess\": \"Wissen erfolgreich gelöscht\",\n    \"status\": {\n        \"pending\": \"Ausstehend\",\n        \"finished\": \"Abgeschlossen\",\n        \"processing\": \"In Bearbeitung\",\n        \"failed\": \"Fehlgeschlagen\"\n    },\n    \"addKnowledge\": \"Wissen hinzufügen\",\n    \"updateKnowledge\": \"Quelle hinzufügen\",\n    \"form\": {\n        \"title\": {\n            \"label\": \"Wissenstitel\",\n            \"placeholder\": \"Wissenstitel eingeben\",\n            \"required\": \"Wissenstitel ist erforderlich\"\n        },\n        \"uploadFile\": {\n            \"label\": \"Datei hochladen\",\n            \"uploadText\": \"Ziehen Sie eine Datei hierher oder klicken Sie zum Hochladen\",\n            \"uploadHint\": \"Unterstützte Dateitypen: .pdf, .csv, .txt, .md, .docx\",\n            \"required\": \"Datei ist erforderlich\"\n        },\n        \"submit\": \"Absenden\",\n        \"success\": \"Wissen erfolgreich hinzugefügt\"\n    },\n    \"noEmbeddingModel\": \"Bitte fügen Sie zuerst ein Einbettungsmodell von der RAG-Einstellungsseite hinzu\"\n}"
  },
  {
    "path": "src/assets/locale/de/openai.json",
    "content": "{\n    \"settings\": \"OpenAI-kompatible API\",\n    \"heading\": \"OpenAI-kompatible API\",\n    \"subheading\": \"Verwalten und konfigurieren Sie hier Ihre OpenAI-API-kompatiblen Anbieter.\",\n    \"addBtn\": \"Anbieter hinzufügen\",\n    \"table\": {\n        \"name\": \"Anbietername\",\n        \"baseUrl\": \"Basis-URL\",\n        \"actions\": \"Aktion\"\n    },\n    \"modal\": {\n        \"titleAdd\": \"Neuen Anbieter hinzufügen\",\n        \"name\": {\n            \"label\": \"Anbietername\",\n            \"required\": \"Anbietername ist erforderlich.\",\n            \"placeholder\": \"Anbieternamen eingeben\"\n        },\n        \"baseUrl\": {\n            \"label\": \"Basis-URL\",\n            \"help\": \"Die Basis-URL des OpenAI-API-Anbieters. z.B. (http://localhost:1234/v1)\",\n            \"required\": \"Basis-URL ist erforderlich.\",\n            \"placeholder\": \"Basis-URL eingeben\"\n        },\n        \"apiKey\": {\n            \"label\": \"API-Schlüssel\",\n            \"required\": \"API-Schlüssel ist erforderlich.\",\n            \"placeholder\": \"API-Schlüssel eingeben\"\n        },\n        \"submit\": \"Speichern\",\n        \"update\": \"Aktualisieren\",\n        \"deleteConfirm\": \"Sind Sie sicher, dass Sie diesen Anbieter löschen möchten?\",\n        \"model\": {\n            \"title\": \"Modellliste\",\n            \"subheading\": \"Bitte wählen Sie die Chat-Modelle aus, die Sie mit diesem Anbieter verwenden möchten.\",\n            \"success\": \"Neue Modelle erfolgreich hinzugefügt.\"\n        },\n        \"tipLMStudio\": \"Page Assist wird automatisch die Modelle abrufen, die Sie in LM Studio geladen haben. Sie müssen sie nicht manuell hinzufügen.\"\n    },\n    \"addSuccess\": \"Anbieter erfolgreich hinzugefügt.\",\n    \"deleteSuccess\": \"Anbieter erfolgreich gelöscht.\",\n    \"updateSuccess\": \"Anbieter erfolgreich aktualisiert.\",\n    \"delete\": \"Löschen\",\n    \"edit\": \"Bearbeiten\",\n    \"newModel\": \"Modelle zum Anbieter hinzufügen\",\n    \"noNewModel\": \"Für LMStudio, Ollama, Llamafile, holen wir die Daten dynamisch. Keine manuelle Hinzufügung erforderlich.\",\n    \"searchModel\": \"Modell suchen\",\n    \"selectAll\": \"Alle auswählen\",\n    \"save\": \"Speichern\",\n    \"saving\": \"Speichern...\",\n    \"manageModels\": {\n        \"columns\": {\n            \"name\": \"Modellname\",\n            \"model_type\": \"Modelltyp\",\n            \"model_id\": \"Modell-ID\",\n            \"provider\": \"Anbietername\",\n            \"actions\": \"Aktion\",\n            \"nickname\": \"Modell-Spitzname\"\n        },\n        \"tooltip\": {\n            \"delete\": \"Löschen\"\n        },\n        \"confirm\": {\n            \"delete\": \"Sind Sie sicher, dass Sie dieses Modell löschen möchten?\"\n        },\n        \"modal\": {\n            \"title\": \"Benutzerdefiniertes Modell hinzufügen\",\n            \"form\": {\n                \"name\": {\n                    \"label\": \"Modell-ID\",\n                    \"placeholder\": \"llama3.2\",\n                    \"required\": \"Modell-ID ist erforderlich.\"\n                },\n                \"provider\": {\n                    \"label\": \"Anbieter\",\n                    \"placeholder\": \"Anbieter auswählen\",\n                    \"required\": \"Anbieter ist erforderlich.\"\n                },\n                \"type\": {\n                    \"label\": \"Modelltyp\"\n                }\n            }\n        }\n    },\n    \"noModelFound\": \"Kein Modell gefunden. Stellen Sie sicher, dass Sie den korrekten Anbieter mit Basis-URL und API-Schlüssel hinzugefügt haben.\",\n    \"radio\": {\n        \"chat\": \"Chat-Modell\",\n        \"embedding\": \"Embedding-Modell\",\n        \"chatInfo\": \"wird für Chat-Vervollständigung und Gesprächsgenerierung verwendet\",\n        \"embeddingInfo\": \"wird für RAG und andere semantische suchbezogene Aufgaben verwendet.\"\n    },\n    \"nicknameModal\": {\n        \"title\": \"Modell-Spitznamen hinzufügen / bearbeiten\",\n        \"form\": {\n            \"modelName\": {\n                \"label\": \"Modellname\",\n                \"placeholder\": \"Modellname eingeben\",\n                \"required\": \"Modellname ist erforderlich.\"\n            },\n            \"modelAvatar\": {\n                \"label\": \"Modell-Avatar\",\n                \"placeholder\": \"Modell-Avatar eingeben\",\n                \"help\": \"Bitte geben Sie die URL des Modell-Avatars ein. Dieses Bild wird im Chat-Fenster angezeigt.\"\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/assets/locale/de/option.json",
    "content": "{\n    \"newChat\": \"Neuer Chat\",\n    \"selectAPrompt\": \"Wähle eine Eingabeaufforderung\",\n    \"githubRepository\": \"GitHub-Repository\",\n    \"settings\": \"Einstellungen\",\n    \"sidebarTitle\": \"Chat-Verlauf\",\n    \"error\": \"Fehler\",\n    \"somethingWentWrong\": \"Etwas ist schiefgelaufen\",\n    \"validationSelectModel\": \"Bitte wähle ein Modell aus, um fortzufahren\",\n    \"deleteHistoryConfirmation\": \"Bist du sicher, dass du diesen Verlauf löschen möchtest?\",\n    \"editHistoryTitle\": \"Gib einen neuen Titel ein\",\n    \"temporaryChat\": \"Temporärer Chat\",\n    \"more\": {\n        \"copy\": {\n            \"group\": \"Kopieren\",\n            \"asText\": \"Als Text kopieren\",\n            \"asMarkdown\": \"Als Markdown kopieren\",\n            \"success\": \"In die Zwischenablage kopiert!\"\n        },\n        \"download\": {\n            \"group\": \"Herunterladen\",\n            \"text\": \"Textdatei (.txt)\",\n            \"markdown\": \"Markdown (.md)\",\n            \"json\": \"JSON-Datei (.json)\"\n        },\n        \"share\": \"Teilen\"\n    }\n}"
  },
  {
    "path": "src/assets/locale/de/playground.json",
    "content": "{\n    \"ollamaState\": {\n        \"searching\": \"Suche nach Ihrem Ollama 🦙\",\n        \"running\": \"Ollama läuft 🦙\",\n        \"notRunning\": \"Verbindung zu Ollama nicht möglich 🦙\",\n        \"connectionError\": \"Es scheint, dass Sie ein Verbindungsproblem haben. Bitte beachten Sie diese <anchor>Dokumentation</anchor> zur Fehlerbehebung.\"\n    },\n    \"formError\": {\n        \"noModel\": \"Bitte wählen Sie ein Modell aus\",\n        \"noEmbeddingModel\": \"Bitte legen Sie ein Embedding-Modell auf der Seite Einstellungen > RAG fest\"\n    },\n    \"form\": {\n        \"textarea\": {\n            \"placeholder\": \"Nachricht eingeben...\"\n        },\n        \"webSearch\": {\n            \"on\": \"An\",\n            \"off\": \"Aus\"\n        }\n    },\n    \"tooltip\": {\n        \"searchInternet\": \"Internet durchsuchen\",\n        \"speechToText\": \"Sprache zu Text\",\n        \"uploadImage\": \"Bild hochladen\",\n        \"stopStreaming\": \"Streaming stoppen\",\n        \"knowledge\": \"Wissen\",\n        \"clearContext\": \"Kontext löschen\"\n    },\n    \"sendWhenEnter\": \"Senden bei Drücken der Eingabetaste\",\n    \"welcome\": \"Hallo! Wie kann ich Ihnen heute helfen?\",\n    \"useOCR\": \"Text aus Bild extrahieren (OCR)\"\n}"
  },
  {
    "path": "src/assets/locale/de/settings.json",
    "content": "{\n  \"generalSettings\": {\n    \"title\": \"Allgemeine Einstellungen\",\n    \"settings\": {\n      \"heading\": \"Web-UI-Einstellungen\",\n      \"speechRecognitionLang\": {\n        \"label\": \"Spracherkennungssprache\",\n        \"placeholder\": \"Sprache auswählen\"\n      },\n      \"language\": {\n        \"label\": \"Sprache\",\n        \"placeholder\": \"Sprache auswählen\"\n      },\n      \"darkMode\": {\n        \"label\": \"Design ändern\",\n        \"options\": {\n          \"light\": \"Hell\",\n          \"dark\": \"Dunkel\"\n        }\n      },\n      \"defaultCopilotPrompt\": {\n        \"label\": \"Standardabfrage für Seitenpanel (Copilot)\",\n        \"placeholder\": \"Wählen Sie eine Aufforderung\"\n      },\n      \"defaultWebUIPrompt\": {\n        \"label\": \"Standardabfrage für Web-UI\",\n        \"placeholder\": \"Wählen Sie eine Aufforderung\"\n      },\n      \"copilotResumeLastChat\": {\n        \"label\": \"Letzten Chat beim Öffnen des Seitenpanels fortsetzen (Copilot)\"\n      },\n      \"turnOnChatWithWebsite\": {\n        \"label\": \"Chat mit Website standardmäßig aktivieren (Copilot)\"\n      },\n      \"webUIResumeLastChat\": {\n        \"label\": \"Letzten Chat beim Öffnen der Web-UI fortsetzen\"\n      },\n      \"hideCurrentChatModelSettings\": {\n        \"label\": \"Aktuelle Chat-Modell-Einstellungen ausblenden\"\n      },\n      \"restoreLastChatModel\": {\n        \"label\": \"Zuletzt verwendetes Modell für vorherige Chats wiederherstellen\"\n      },\n      \"sendNotificationAfterIndexing\": {\n        \"label\": \"Benachrichtigung nach Abschluss der Wissensbasis-Verarbeitung senden\"\n      },\n      \"generateTitle\": {\n        \"label\": \"Titel mit KI generieren\"\n      },\n      \"ollamaStatus\": {\n        \"label\": \"Ollama-Verbindungsstatus-Überprüfung aktivieren oder deaktivieren\"\n      },\n      \"wideMode\": {\n        \"label\": \"Breitbildmodus aktivieren\"\n      },\n      \"openReasoning\": {\n        \"label\": \"Offene Argumentation standardmäßig eingeklappt\"\n      },\n      \"userChatBubble\": {\n        \"label\": \"Chat-Blase für Benutzernachrichten verwenden\"\n      }\n    },\n    \"sidepanelRag\": {\n      \"heading\": \"Abruf-Einstellungen\",\n      \"ragEnabled\": {\n        \"label\": \"Einbettung und Abruf aktivieren\"\n      },\n      \"maxWebsiteContext\": {\n        \"label\": \"Maximale Inhaltsgröße für Vollkontext-Modus\",\n        \"placeholder\": \"Inhaltsgröße (Standard 4028)\"\n      }\n    },\n    \"webSearch\": {\n      \"heading\": \"Websuche verwalten\",\n      \"searchMode\": {\n        \"label\": \"Einfache Internetsuche durchführen\"\n      },\n      \"provider\": {\n        \"label\": \"Suchmaschine\",\n        \"placeholder\": \"Suchmaschine auswählen\"\n      },\n      \"totalSearchResults\": {\n        \"label\": \"Gesamtanzahl der Suchergebnisse\",\n        \"placeholder\": \"Gesamtanzahl der Suchergebnisse eingeben\"\n      },\n      \"visitSpecificWebsite\": {\n        \"label\": \"Die in der Nachricht erwähnte Website besuchen\"\n      },\n      \"searxng\": {\n        \"url\": {\n          \"label\": \"SearXNG-URL\"\n        }\n      },\n      \"braveApi\": {\n        \"label\": \"Brave API-Schlüssel\",\n        \"placeholder\": \"Geben Sie Ihren Brave API-Schlüssel ein\"\n      },\n      \"searchOnByDefault\": {\n        \"label\": \"Internetsuche standardmäßig aktiviert\"\n      }\n    },\n    \"system\": {\n      \"heading\": \"Systemeinstellungen\",\n      \"storageSyncEnabled\": {\n        \"label\": \"Browser-Speichersynchronisierung aktivieren (Einstellungen geräteübergreifend synchronisieren)\"\n      },\n      \"deleteChatHistory\": {\n        \"label\": \"System zurücksetzen\",\n        \"button\": \"Alles zurücksetzen\",\n        \"confirm\": \"Sind Sie sicher, dass Sie einen Systemreset durchführen möchten? Dies löscht alle Daten und kann nicht rückgängig gemacht werden.\"\n      },\n      \"export\": {\n        \"label\": \"Alle Daten exportieren (Chatverlauf, Wissensdatenbank, Prompts und Einstellungen)\",\n        \"button\": \"Daten exportieren\",\n        \"success\": \"Export erfolgreich\"\n      },\n      \"import\": {\n        \"label\": \"Alle Daten importieren (Chatverlauf, Wissensdatenbank, Prompts und Einstellungen)\",\n        \"button\": \"Daten importieren\",\n        \"success\": \"Import erfolgreich\",\n        \"error\": \"Importfehler\"\n      }\n    },\n    \"tts\": {\n      \"heading\": \"Text-zu-Sprache-Einstellungen\",\n      \"ttsEnabled\": {\n        \"label\": \"Text-zu-Sprache aktivieren\"\n      },\n      \"ttsAutoPlay\": {\n        \"label\": \"Sprachantwort nach Fertigstellung automatisch abspielen\"\n      },\n      \"ttsProvider\": {\n        \"label\": \"Text-zu-Sprache-Anbieter\",\n        \"placeholder\": \"Anbieter auswählen\"\n      },\n      \"ttsVoice\": {\n        \"label\": \"Text-zu-Sprache-Stimme\",\n        \"placeholder\": \"Stimme auswählen\"\n      },\n      \"ssmlEnabled\": {\n        \"label\": \"SSML (Speech Synthesis Markup Language) aktivieren\"\n      },\n      \"removeReasoningTagTTS\": {\n        \"label\": \"Reasoning-Tag aus Text-zu-Sprache entfernen\"\n      }\n    },\n    \"stt\": {\n      \"heading\": \"Sprache-zu-Text-Einstellungen\",\n      \"autoStopTimeout\": {\n        \"label\": \"Automatische Stopp-Zeitüberschreitung (ms)\",\n        \"placeholder\": \"Geben Sie die automatische Stopp-Zeitüberschreitung in Millisekunden ein\"\n      },\n      \"autoSubmitVoiceMessage\": {\n        \"label\": \"Sprachnachricht automatisch senden\"\n      }\n    }\n  },\n  \"manageModels\": {\n    \"title\": \"Modelle verwalten\",\n    \"addBtn\": \"Neues Modell hinzufügen\",\n    \"columns\": {\n      \"name\": \"Name\",\n      \"digest\": \"Digest\",\n      \"modifiedAt\": \"Zuletzt geändert\",\n      \"size\": \"Größe\",\n      \"actions\": \"Aktionen\"\n    },\n    \"expandedColumns\": {\n      \"parentModel\": \"Übergeordnetes Modell\",\n      \"format\": \"Format\",\n      \"family\": \"Familie\",\n      \"parameterSize\": \"Parametergröße\",\n      \"quantizationLevel\": \"Quantisierungsstufe\"\n    },\n    \"tooltip\": {\n      \"delete\": \"Modell löschen\",\n      \"repull\": \"Modell erneut herunterladen\"\n    },\n    \"confirm\": {\n      \"delete\": \"Sind Sie sicher, dass Sie dieses Modell löschen möchten?\",\n      \"repull\": \"Sind Sie sicher, dass Sie dieses Modell erneut herunterladen möchten?\"\n    },\n    \"modal\": {\n      \"title\": \"Neues Modell hinzufügen\",\n      \"placeholder\": \"Modellnamen eingeben\",\n      \"pull\": \"Modell herunterladen\"\n    },\n    \"notification\": {\n      \"pullModel\": \"Modell wird heruntergeladen\",\n      \"pullModelDescription\": \"Das Modell {{modelName}} wird heruntergeladen. Weitere Details finden Sie im Erweiterungssymbol.\",\n      \"success\": \"Erfolgreich\",\n      \"error\": \"Fehler\",\n      \"successDescription\": \"Das Modell wurde erfolgreich heruntergeladen\",\n      \"successDeleteDescription\": \"Das Modell wurde erfolgreich gelöscht\",\n      \"someError\": \"Etwas ist schiefgelaufen. Bitte versuchen Sie es später erneut\"\n    }\n  },\n  \"managePrompts\": {\n    \"title\": \"Prompts verwalten\",\n    \"addBtn\": \"Neuen Prompt hinzufügen\",\n    \"option1\": \"Normal\",\n    \"option2\": \"RAG\",\n    \"questionPrompt\": \"Frage-Prompt\",\n    \"segmented\": {\n      \"custom\": \"Benutzerdefinierte Prompts\",\n      \"copilot\": \"Copilot-Prompts\"\n    },\n    \"columns\": {\n      \"title\": \"Titel\",\n      \"prompt\": \"Prompt\",\n      \"type\": \"Prompt-Typ\",\n      \"actions\": \"Aktionen\"\n    },\n    \"systemPrompt\": \"System-Prompt\",\n    \"quickPrompt\": \"Schnell-Prompt\",\n    \"tooltip\": {\n      \"delete\": \"Prompt löschen\",\n      \"edit\": \"Prompt bearbeiten\"\n    },\n    \"confirm\": {\n      \"delete\": \"Sind Sie sicher, dass Sie diesen Prompt löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.\"\n    },\n    \"modal\": {\n      \"addTitle\": \"Neuen Prompt hinzufügen\",\n      \"editTitle\": \"Prompt bearbeiten\"\n    },\n    \"form\": {\n      \"title\": {\n        \"label\": \"Titel\",\n        \"placeholder\": \"Mein toller Prompt\",\n        \"required\": \"Bitte geben Sie einen Titel ein\"\n      },\n      \"prompt\": {\n        \"label\": \"Prompt\",\n        \"placeholder\": \"Prompt eingeben\",\n        \"required\": \"Bitte geben Sie einen Prompt ein\",\n        \"help\": \"Sie können {key} als Variable in Ihrem Prompt verwenden.\",\n        \"missingTextPlaceholder\": \"Die Variable {text} fehlt im Prompt. Bitte fügen Sie sie hinzu.\"\n      },\n      \"isSystem\": {\n        \"label\": \"Ist System-Prompt\"\n      },\n      \"btnSave\": {\n        \"saving\": \"Prompt wird hinzugefügt...\",\n        \"save\": \"Prompt hinzufügen\"\n      },\n      \"btnEdit\": {\n        \"saving\": \"Prompt wird aktualisiert...\",\n        \"save\": \"Prompt aktualisieren\"\n      }\n    },\n    \"notification\": {\n      \"addSuccess\": \"Prompt hinzugefügt\",\n      \"addSuccessDesc\": \"Prompt wurde erfolgreich hinzugefügt\",\n      \"error\": \"Fehler\",\n      \"someError\": \"Etwas ist schiefgelaufen. Bitte versuchen Sie es später erneut\",\n      \"updatedSuccess\": \"Prompt aktualisiert\",\n      \"updatedSuccessDesc\": \"Prompt wurde erfolgreich aktualisiert\",\n      \"deletedSuccess\": \"Prompt gelöscht\",\n      \"deletedSuccessDesc\": \"Prompt wurde erfolgreich gelöscht\"\n    }\n  },\n  \"manageShare\": {\n    \"title\": \"Freigabe verwalten\",\n    \"heading\": \"Seiten-Freigabe-URL konfigurieren\",\n    \"form\": {\n      \"url\": {\n        \"label\": \"Seiten-Freigabe-URL\",\n        \"placeholder\": \"Seiten-Freigabe-URL eingeben\",\n        \"required\": \"Bitte geben Sie Ihre Seiten-Freigabe-URL ein!\",\n        \"help\": \"Aus Datenschutzgründen können Sie die Seitenfreigabe selbst hosten und die URL hier angeben. <anchor>Mehr erfahren</anchor>.\"\n      }\n    },\n    \"webshare\": {\n      \"heading\": \"Web-Freigabe\",\n      \"columns\": {\n        \"title\": \"Titel\",\n        \"url\": \"URL\",\n        \"actions\": \"Aktionen\"\n      },\n      \"tooltip\": {\n        \"delete\": \"Freigabe löschen\"\n      },\n      \"confirm\": {\n        \"delete\": \"Sind Sie sicher, dass Sie diese Freigabe löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.\"\n      },\n      \"label\": \"Seitenfreigabe verwalten\",\n      \"description\": \"Seitenfreigabe-Funktion aktivieren oder deaktivieren\"\n    },\n    \"notification\": {\n      \"pageShareSuccess\": \"Seiten-Freigabe-URL erfolgreich aktualisiert\",\n      \"someError\": \"Etwas ist schiefgelaufen. Bitte versuchen Sie es später erneut\",\n      \"webShareDeleteSuccess\": \"Web-Freigabe erfolgreich gelöscht\"\n    }\n  },\n  \"ollamaSettings\": {\n    \"title\": \"Ollama-Einstellungen\",\n    \"heading\": \"Ollama konfigurieren\",\n    \"settings\": {\n      \"ollamaUrl\": {\n        \"label\": \"Ollama-URL\",\n        \"placeholder\": \"Ollama-URL eingeben\"\n      },\n      \"globalEnable\": {\n        \"label\": \"Ollama-Integration global aktivieren oder deaktivieren\",\n        \"warning\": \"Wenn Sie die Ollama-Integration global deaktivieren, wird Page Assist keine Modelle von Ollama abrufen. Sie können Ollama-Instanzen weiterhin über den Bereich <anchor>OpenAI-kompatible API</anchor> hinzufügen, was einwandfrei funktioniert.\"\n      },\n      \"advanced\": {\n        \"label\": \"Erweiterte Ollama-URL-Konfiguration\",\n        \"urlRewriteEnabled\": {\n          \"label\": \"Benutzerdefinierte Ursprungs-URL aktivieren oder deaktivieren\"\n        },\n        \"rewriteUrl\": {\n          \"label\": \"Benutzerdefinierte Ursprungs-URL\",\n          \"placeholder\": \"Benutzerdefinierte Ursprungs-URL eingeben\"\n        },\n        \"autoCORSFix\": {\n          \"label\": \"Automatische Ollama-CORS-Korrektur aktivieren oder deaktivieren\"\n        },\n        \"headers\": {\n          \"label\": \"Benutzerdefinierte Header\",\n          \"add\": \"Header hinzufügen\",\n          \"key\": {\n            \"label\": \"Header-Schlüssel\",\n            \"placeholder\": \"Autorisierung\"\n          },\n          \"value\": {\n            \"label\": \"Header-Wert\",\n            \"placeholder\": \"Bearer-Token\"\n          }\n        },\n        \"help\": \"Wenn Sie Verbindungsprobleme mit Ollama auf Page Assist haben, können Sie eine benutzerdefinierte Ursprungs-URL konfigurieren. Um mehr über die Konfiguration zu erfahren, <anchor>klicken Sie hier</anchor>.\"\n      }\n    }\n  },\n  \"manageSearch\": {\n    \"title\": \"Web-Suche verwalten\",\n    \"heading\": \"Web-Suche konfigurieren\"\n  },\n  \"about\": {\n    \"title\": \"Über\",\n    \"heading\": \"Über\",\n    \"chromeVersion\": \"Page Assist Version\",\n    \"ollamaVersion\": \"Ollama Version\",\n    \"support\": \"Sie können das Page Assist-Projekt durch Spenden oder Sponsoring über die folgenden Plattformen unterstützen:\",\n    \"koFi\": \"Unterstützen Sie uns auf Ko-fi\",\n    \"githubSponsor\": \"Sponsern Sie uns auf GitHub\",\n    \"githubRepo\": \"GitHub-Repository\"\n  },\n  \"manageKnowledge\": {\n    \"title\": \"Wissen verwalten\",\n    \"heading\": \"Wissensbasis konfigurieren\"\n  },\n  \"rag\": {\n    \"title\": \"Pipeline-Einstellungen\",\n    \"ragSettings\": {\n      \"label\": \"RAG-Einstellungen\",\n      \"model\": {\n        \"label\": \"Embedding-Modell\",\n        \"required\": \"Bitte wählen Sie ein Modell aus\",\n        \"help\": \"Es wird dringend empfohlen, Embedding-Modelle wie `nomic-embed-text` zu verwenden.\",\n        \"placeholder\": \"Wählen Sie ein Modell aus\"\n      },\n      \"chunkSize\": {\n        \"label\": \"Chunk-Größe\",\n        \"placeholder\": \"Chunk-Größe eingeben\",\n        \"required\": \"Bitte geben Sie eine Chunk-Größe ein\"\n      },\n      \"chunkOverlap\": {\n        \"label\": \"Chunk-Überlappung\",\n        \"placeholder\": \"Chunk-Überlappung eingeben\",\n        \"required\": \"Bitte geben Sie eine Chunk-Überlappung ein\"\n      },\n      \"totalFilePerKB\": {\n        \"label\": \"Standard-Datei-Upload-Limit für die Wissensbasis\",\n        \"placeholder\": \"Geben Sie das Standard-Datei-Upload-Limit ein (z.B. 10)\",\n        \"required\": \"Bitte geben Sie das Standard-Datei-Upload-Limit ein\"\n      },\n      \"noOfRetrievedDocs\": {\n        \"label\": \"Anzahl der abgerufenen Dokumente\",\n        \"placeholder\": \"Anzahl der abgerufenen Dokumente eingeben\",\n        \"required\": \"Bitte geben Sie die Anzahl der abgerufenen Dokumente ein\"\n      },\n      \"splittingSeparator\": {\n        \"label\": \"Separator\",\n        \"placeholder\": \"Separator eingeben (z.B. \\\\n\\\\n)\",\n        \"required\": \"Bitte geben Sie einen Separator ein\"\n      },\n      \"splittingStrategy\": {\n        \"label\": \"Text-Splitter\"\n      }\n    },\n    \"prompt\": {\n      \"label\": \"RAG-Prompt konfigurieren\",\n      \"option1\": \"Normal\",\n      \"option2\": \"Web\",\n      \"alert\": \"Die Konfiguration des System-Prompts hier ist veraltet. Bitte verwenden Sie den Abschnitt 'Prompts verwalten', um Prompts hinzuzufügen oder zu bearbeiten. Dieser Abschnitt wird in einer zukünftigen Version entfernt\",\n      \"systemPrompt\": \"System-Prompt\",\n      \"systemPromptPlaceholder\": \"System-Prompt eingeben\",\n      \"webSearchPrompt\": \"Web-Suche-Prompt\",\n      \"webSearchPromptHelp\": \"Entfernen Sie `{search_results}` nicht aus dem Prompt.\",\n      \"webSearchPromptError\": \"Bitte geben Sie einen Web-Suche-Prompt ein\",\n      \"webSearchPromptPlaceholder\": \"Web-Suche-Prompt eingeben\",\n      \"webSearchFollowUpPrompt\": \"Web-Suche-Folgeprompt\",\n      \"webSearchFollowUpPromptHelp\": \"Entfernen Sie `{chat_history}` und `{question}` nicht aus dem Prompt.\",\n      \"webSearchFollowUpPromptError\": \"Bitte geben Sie Ihren Web-Suche-Folgeprompt ein!\",\n      \"webSearchFollowUpPromptPlaceholder\": \"Ihr Web-Suche-Folgeprompt\"\n    }\n  },\n  \"chromeAiSettings\": {\n    \"title\": \"Chrome AI-Einstellungen\"\n  }\n}\n"
  },
  {
    "path": "src/assets/locale/de/sidepanel.json",
    "content": "{\n    \"tooltip\": {\n        \"embed\": \"Es kann einige Minuten dauern, die Seite einzubetten. Bitte warten Sie...\",\n        \"clear\": \"Chatverlauf löschen\",\n        \"history\": \"Chatverlauf\",\n        \"openwebui\": \"WebUI öffnen\"\n    }\n}"
  },
  {
    "path": "src/assets/locale/en/chrome.json",
    "content": "{\n    \"heading\": \"Configure Chrome AI\",\n    \"status\": {\n        \"label\": \"Enable or Disable Chrome AI Support on Page Assist\"\n    },\n    \"error\": {\n        \"browser_not_supported\": \"This version of Chrome is not supported by the Gemini Nano model. Please update to version 138 or later.\",\n        \"ai_not_supported\": \"The setting chrome://flags/#prompt-api-for-gemini-nano is not enabled. Please enable it.\",\n        \"ai_not_ready\": \"Gemini Nano is not ready yet; you need to double-check Chrome settings.\",\n        \"internal_error\": \"An internal error occurred. Please try again later.\"\n    },\n    \"errorDescription\": \"To use Chrome AI, you need Chrome version 138 or later. Follow these steps:\\n\\n1. Go to `chrome://flags/#prompt-api-for-gemini-nano` and enable \\\"Prompt API for Gemini Nano\\\".\\n2. Restart Chrome to apply the flag.\\n3. Return to this page and click \\\"Download Model\\\" — this will download a 4GB model for the first time.\\n4. Once downloaded, Gemini Nano can be enabled through Page Assist.\",\n    \"downloadModel\": \"Download Model\",\n    \"modelDownloadWarning\": \"This will download a model with an approximate download size ranging from 1.5 GB to 2.4 GB. Ensure you have sufficient disk space.\",\n    \"downloadModal\": {\n        \"title\": \"Download Gemini Nano Model\",\n        \"warning\": \"Large Download Required\",\n        \"warningDescription\": \"This will download approximately 4GB of data. Ensure you have sufficient disk space and a stable internet connection.\",\n        \"confirm\": \"Download Now\",\n        \"cancel\": \"Cancel\",\n        \"downloading\": \"Downloading Model\",\n        \"downloadingDescription\": \"Downloading Gemini Nano model. This may take several minutes depending on your internet connection.\",\n        \"pleaseWait\": \"Please do not close this window while downloading...\"\n    },\n    \"downloadSuccess\": \"Model downloaded successfully!\",\n    \"downloadError\": \"Failed to download model. Please try again.\"\n}"
  },
  {
    "path": "src/assets/locale/en/common.json",
    "content": "{\n    \"pageAssist\": \"Page Assist\",\n    \"selectAModel\": \"Select a Model\",\n    \"save\": \"Save\",\n    \"saved\": \"Saved\",\n    \"cancel\": \"Cancel\",\n    \"retry\": \"Retry\",\n    \"loadMore\": \"Load More...\",\n    \"share\": {\n        \"tooltip\": {\n            \"share\": \"Share\"\n        },\n        \"modal\": {\n            \"title\": \"Share link to Chat\"\n        },\n        \"form\": {\n            \"defaultValue\": {\n                \"name\": \"Anonymous\",\n                \"title\": \"Untitled chat\"\n            },\n            \"title\": {\n                \"label\": \"Chat title\",\n                \"placeholder\": \"Enter Chat title\",\n                \"required\": \"Chat title is required\"\n            },\n            \"name\": {\n                \"label\": \"Your name\",\n                \"placeholder\": \"Enter your name\",\n                \"required\": \"Your name is required\"\n            },\n            \"btn\": {\n                \"save\": \"Generate Link\",\n                \"saving\": \"Generating Link...\"\n            }\n        },\n        \"notification\": {\n            \"successGenerate\": \"Link copied to clipboard\",\n            \"failGenerate\": \"Failed to generate link\"\n        }\n    },\n    \"copyToClipboard\": \"Copy to clipboard\",\n    \"webSearch\": \"Searching the web\",\n    \"regenerate\": \"Regenerate\",\n    \"edit\": \"Edit\",\n    \"delete\": \"Delete\",\n    \"continue\": \"Continue Response\",\n    \"saveAndSubmit\": \"Save & Submit\",\n    \"editMessage\": {\n        \"placeholder\": \"Type a message...\"\n    },\n    \"submit\": \"Submit\",\n    \"noData\": \"No data\",\n    \"noHistory\": \"No chat history\",\n    \"chatWithCurrentPage\": \"Chat with current page\",\n    \"beta\": \"Beta\",\n    \"tts\": \"Read aloud\",\n    \"currentChatModelSettings\": \"Current Chat Model Settings\",\n    \"modelSettings\": {\n        \"label\": \"Model Settings\",\n        \"description\": \"Set the model options globally for all chats\",\n        \"form\": {\n            \"keepAlive\": {\n                \"label\": \"Keep Alive\",\n                \"help\": \"controls how long the model will stay loaded into memory following the request (default: 5m)\",\n                \"placeholder\": \"e.g. 5m, 10m, 1h\"\n            },\n            \"temperature\": {\n                \"label\": \"Temperature\",\n                \"placeholder\": \"e.g. 0.7, 1.0\"\n            },\n            \"numCtx\": {\n                \"label\": \"Context Window Size (num_ctx)\",\n                \"placeholder\": \"Enter Context Window Size value (default: 2048)\"\n            },\n            \"numPredict\": {\n                \"label\": \"Max Tokens (num_predict)\",\n                \"placeholder\": \"e.g. 2048, 4096\"\n            },\n            \"thinking\": {\n                \"label\": \"Thinking Mode (Ollama)\",\n                \"levels\": {\n                    \"off\": \"Off\",\n                    \"on\": \"On\",\n                    \"low\": \"Low reasoning effort\",\n                    \"medium\": \"Medium reasoning effort\",\n                    \"high\": \"High reasoning effort\"\n                }\n            },\n            \"seed\": {\n                \"label\": \"Seed\",\n                \"placeholder\": \"e.g. 1234\",\n                \"help\": \"Reproducibility of the model output\"\n            },\n            \"topK\": {\n                \"label\": \"Top K\",\n                \"placeholder\": \"e.g. 40, 100\"\n            },\n            \"topP\": {\n                \"label\": \"Top P\",\n                \"placeholder\": \"e.g. 0.9, 0.95\"\n            },\n            \"useMMap\": {\n                \"label\": \"useMmap\"\n            },\n            \"tfsZ\": {\n                \"label\": \"TFS-Z\",\n                \"placeholder\": \"e.g. 1.0, 1.1\"\n            },\n            \"numKeep\": {\n                \"label\": \"Num Keep\",\n                \"placeholder\": \"e.g. 256, 512\"\n            },\n            \"numThread\": {\n                \"label\": \"Num Thread\",\n                \"placeholder\": \"e.g. 8, 16\"\n            },\n            \"useMlock\": {\n                \"label\": \"useMlock\"\n            },\n            \"reasoningEffort\": {\n                \"label\": \"Reasoning Effort\",\n                \"placeholder\": \"low, medium, high\"\n            },\n            \"minP\": {\n                \"label\": \"Min P\",\n                \"placeholder\": \"e.g. 0.05\"\n            },\n            \"repeatPenalty\": {\n                \"label\": \"Repeat Penalty\",\n                \"placeholder\": \"e.g. 1.1, 1.2\"\n            },\n            \"repeatLastN\": {\n                \"label\": \"Repeat Last N\",\n                \"placeholder\": \"e.g. 64, 128\"\n            },\n            \"numGpu\": {\n                \"label\": \"Num GPU\",\n                \"placeholder\": \"Enter number of layers to send to GPU(s)\"\n            },\n            \"systemPrompt\": {\n                \"label\": \"Temporary System Prompt\",\n                \"placeholder\": \"Enter System Prompt\",\n                \"help\": \"This is a quick way to set the system prompt in the current chat, which will override the selected system prompt if it exists.\"\n            }\n        },\n        \"advanced\": \"More Model Settings\"\n    },\n    \"copilot\": {\n        \"summary\": \"Summarize\",\n        \"explain\": \"Explain\",\n        \"rephrase\": \"Rephrase\",\n        \"translate\": \"Translate\",\n        \"custom\": \"Custom\"\n    },\n    \"citations\": \"Citations\",\n    \"segmented\": {\n        \"ollama\": \"Ollama Models\",\n        \"custom\": \"Custom Models\"\n    },\n    \"downloadCode\": \"Download Code\",\n    \"date\": {\n        \"pinned\": \"Pinned\",\n        \"today\": \"Today\",\n        \"yesterday\": \"Yesterday\",\n        \"last7Days\": \"Last 7 Days\",\n        \"older\": \"Older\"\n    },\n    \"range\": {\n        \"deleteConfirm\": {\n            \"pinned\": \"Are you sure you want to delete all pinned messages?\",\n            \"today\": \"Are you sure you want to delete all messages from today?\",\n            \"yesterday\": \"Are you sure you want to delete all messages from yesterday?\",\n            \"last7Days\": \"Are you sure you want to delete all messages from the last 7 days?\",\n            \"older\": \"Are you sure you want to delete all older messages?\"\n        },\n        \"tooltip\": {\n            \"pinned\": \"Delete all pinned messages\",\n            \"today\": \"Delete all messages from today\",\n            \"yesterday\": \"Delete all messages from yesterday\",\n            \"last7Days\": \"Delete all messages from the last 7 days\",\n            \"older\": \"Delete all older messages\"\n        }\n    },\n    \"historiesDeleted\": \"{{count}} Histories Deleted\",\n    \"deleteHistoriesError\": \"Error deleting histories\",\n    \"pin\": \"Pin\",\n    \"unpin\": \"Unpin\",\n    \"generationInfo\": \"Generation Info\",\n    \"sidebarChat\": \"Sidebar Chat\",\n    \"reasoning\": {\n        \"thinking\": \"Thinking....\",\n        \"thought\": \"Thought for {{time}}\",\n        \"title\": \"Reasoning Process\",\n        \"expand\": \"Show reasoning\",\n        \"collapse\": \"Hide reasoning\"\n    },\n    \"mermaid\": \"Mermaid\",\n    \"search\": \"Search\",\n    \"searchResults\": \"Search Results\",\n    \"embeddingGen\": \"Creating embeddings, this may take a while\",\n    \"semanticSearch\": \"Performing semantic search\",\n    \"newBranch\": \"New Branch\",\n    \"downloading\": \"Downloading\" ,\n    \"mcp\": {\n        \"tool\": \"tool\",\n        \"server\": \"server\",\n        \"toolRequestTitle\": \"Tools requested\",\n        \"toolResultTitle\": \"Tool result\",\n        \"arguments\": \"Arguments\",\n        \"output\": \"Output\",\n        \"noOutput\": \"No output returned.\",\n        \"status\": {\n            \"success\": \"Success\",\n            \"error\": \"Error\"\n        },\n        \"action\": {\n            \"connecting\": \"Connecting to MCP servers\",\n            \"loading_tools\": \"Loading MCP tools\",\n            \"calling_tool\": \"Calling {{tool}} on {{server}}\",\n            \"waiting_result\": \"Waiting for {{tool}} on {{server}}\"\n        }\n    },\n    \"cancelPullingModel\": {\n        \"confirm\": \"Are you sure you want to cancel the download? This will stop the download process. According to the Ollama documentation, you can restart from where you left off.\"\n    },\n    \"saveChat\": \"Save Chat\"\n}\n"
  },
  {
    "path": "src/assets/locale/en/knowledge.json",
    "content": "{\n    \"addBtn\": \"Add New Knowledge\",\n    \"columns\": {\n        \"title\": \"Title\",\n        \"status\": \"Status\",\n        \"embeddings\": \"Embedding Model\",\n        \"createdAt\": \"Created At\",\n        \"action\": \"Actions\"\n    },\n    \"expandedColumns\": {\n        \"name\": \"Name\"\n    },\n    \"confirm\": {\n        \"delete\": \"Are you sure you want to delete this knowledge?\",\n        \"deleteSource\": \"Are you sure you want to delete this source?\"\n    },\n    \"deleteSuccess\": \"Knowledge deleted successfully\",\n    \"status\": {\n        \"pending\": \"Pending\",\n        \"finished\": \"Finished\",\n        \"processing\": \"Processing\",\n        \"failed\": \"Failed\"\n    },\n    \"addKnowledge\": \"Add Knowledge\",\n    \"updateKnowledge\": \"Add Source\",\n    \"form\": {\n        \"tabs\": {\n            \"upload\": \"Upload File\",\n            \"text\": \"Text Input\"\n        },\n        \"title\": {\n            \"label\": \"Knowledge Title (optional)\",\n            \"placeholder\": \"Enter knowledge title\",\n            \"placeholderOptional\": \"Optional title (defaults to first 50 characters)\",\n            \"required\": \"Knowledge title is required\"\n        },\n        \"uploadFile\": {\n            \"label\": \"Upload File\",\n            \"uploadText\": \"Drag and drop a file here or click to upload\",\n            \"uploadHint\": \"Supported file types: .pdf, .csv, .txt, .md, .docx\",\n            \"required\": \"File is required\",\n            \"uploadError\": \"Unsupported file type\"\n        },\n        \"textInput\": {\n            \"typeLabel\": \"Type\",\n            \"type\": {\n                \"plain\": \"Plain Text\",\n                \"markdown\": \"Markdown\",\n                \"code\": \"Code\"\n            },\n            \"contentLabel\": \"Content\",\n            \"placeholder\": \"Paste or type your text here...\",\n            \"required\": \"Text content is required\",\n            \"tooLarge\": \"Content is too large. Please keep it under 500k characters.\",\n            \"defaultTitle\": \"Untitled Text\"\n        },\n        \"submit\": \"Submit\",\n        \"success\": \"Knowledge added successfully\"\n    },\n    \"noEmbeddingModel\": \"Please add an embedding model from the RAG settings page first\",\n    \"newSource\": \"New Source\",\n    \"editSettings\": {\n        \"title\": \"Edit Knowledge Settings\",\n        \"success\": \"Knowledge settings updated successfully\",\n        \"variableInfo\": {\n            \"title\": \"Available Variables\",\n            \"description\": \"You can use the following variables in your prompts:\",\n            \"context\": \"The retrieved context from your knowledge base\",\n            \"query\": \"The user's question or query\"\n        },\n        \"form\": {\n            \"title\": {\n                \"label\": \"Knowledge Title\",\n                \"placeholder\": \"Enter knowledge title\",\n                \"required\": \"Knowledge title is required\"\n            },\n            \"systemPrompt\": {\n                \"label\": \"System Prompt\",\n                \"help\": \"Customize how the AI responds using your knowledge base. Use {context} for retrieved information and {question} for the user's question. Note: {question} is required in the prompt, otherwise character won't get question context.\",\n                \"placeholder\": \"Enter your custom system prompt...\",\n                \"prefillButton\": \"Use Default Prompt\"\n            },\n            \"followupPrompt\": {\n                \"label\": \"Follow-up Question Prompt\",\n                \"help\": \"Customize how follow-up questions are processed. This prompt rephrases follow-up questions into standalone questions.\",\n                \"placeholder\": \"Enter your custom follow-up prompt...\",\n                \"prefillButton\": \"Use Default Prompt\"\n            }\n        },\n        \"tooltip\": \"Edit knowledge settings\"\n    }\n}"
  },
  {
    "path": "src/assets/locale/en/openai.json",
    "content": "{\n    \"settings\": \"OpenAI Compatible API\",\n    \"heading\": \"OpenAI compatible API\",\n    \"subheading\": \"Manage and configure your OpenAI API Compatible providers here.\",\n    \"addBtn\": \"Add Provider\",\n    \"table\": {\n        \"name\": \"Provider Name\",\n        \"baseUrl\": \"Base URL\",\n        \"actions\": \"Action\"\n    },\n    \"modal\": {\n        \"titleAdd\": \"Add New Provider\",\n        \"titleEdit\": \"Edit Provider\",\n        \"name\": {\n            \"label\": \"Provider Name\",\n            \"required\": \"Provider name is required.\",\n            \"placeholder\": \"Enter provider name\"\n        },\n        \"baseUrl\": {\n            \"label\": \"Base URL\",\n            \"help\": \"The base URL of the OpenAI API provider. eg (http://localhost:1234/v1)\",\n            \"required\": \"Base URL is required.\",\n            \"placeholder\": \"Enter base URL\"\n        },\n        \"apiKey\": {\n            \"label\": \"API Key\",\n            \"required\": \"API Key is required.\",\n            \"placeholder\": \"Enter API Key\"\n        },\n        \"submit\": \"Save\",\n        \"update\": \"Update\",\n        \"deleteConfirm\": \"Are you sure you want to delete this provider?\",\n        \"model\": {\n            \"title\": \"Model List\",\n            \"subheading\": \"Please select the chat models you want to use with this provider.\",\n            \"success\": \"Successfully added new models.\"\n        },\n        \"tipLMStudio\": \"Page Assist will automatically fetch the models you loaded on LM Studio. You don't need to add them manually.\"\n    },\n    \"addSuccess\": \"Provider added successfully.\",\n    \"deleteSuccess\": \"Provider deleted successfully.\",\n    \"updateSuccess\": \"Provider updated successfully.\",\n    \"delete\": \"Delete\",\n    \"edit\": \"Edit\",\n    \"newModel\": \"Add Models to Provider\",\n    \"noNewModel\": \"For LMStudio, Ollama, Llamafile, we fetch dynamically. No manual addition needed.\",\n    \"searchModel\": \"Search Model\",\n    \"selectAll\": \"Select All\",\n    \"save\": \"Save\",\n    \"saving\": \"Saving...\",\n    \"manageModels\": {\n        \"columns\": {\n            \"name\": \"Model Name\",\n            \"model_type\": \"Model Type\",\n            \"model_id\": \"Model ID\",\n            \"provider\": \"Provider Name\",\n            \"actions\": \"Action\",\n            \"nickname\": \"Model Nickname\"\n        },\n        \"tooltip\": {\n            \"delete\": \"Delete\"\n        },\n        \"confirm\": {\n            \"delete\": \"Are you sure you want to delete this model?\"\n        },\n        \"modal\": {\n            \"title\": \"Add Custom Model\",\n            \"titleEdit\": \"Edit Custom Model\",\n            \"form\": {\n                \"name\": {\n                    \"label\": \"Model ID\",\n                    \"placeholder\": \"deepseek-v3.1\",\n                    \"required\": \"Model ID is required.\"\n                },\n                \"provider\": {\n                    \"label\": \"Provider\",\n                    \"placeholder\": \"Select provider\",\n                    \"required\": \"Provider is required.\"\n                },\n                \"type\": {\n                    \"label\": \"Model Type\"\n                }\n            }\n        }\n    },\n    \"noModelFound\": \"No model found. Make sure you have added correct provider with base URL and API key.\",\n    \"radio\": {\n        \"chat\": \"Chat Model\",\n        \"embedding\": \"Embedding Model\",\n        \"chatInfo\": \"is used for chat completion and conversation generation\",\n        \"embeddingInfo\": \"is used for RAG and other semantic search related tasks.\"\n    },\n    \"nicknameModal\": {\n        \"title\": \"Add / Edit Model Nickname\",\n        \"form\": {\n            \"modelName\": {\n                \"label\": \"Model Name\",\n                \"placeholder\": \"Enter model name\",\n                \"required\": \"Model name is required.\"\n            },\n            \"modelAvatar\": {\n                \"label\": \"Model Avatar\",\n                \"placeholder\": \"Enter model avatar\",\n                \"help\": \"Please enter the URL of the model avatar. This image will be displayed in the chat window.\"\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/assets/locale/en/option.json",
    "content": "{\n    \"newChat\": \"New Chat\",\n    \"selectAPrompt\": \"Select a Prompt\",\n    \"githubRepository\": \"GitHub Repository\",\n    \"settings\": \"Settings\",\n    \"sidebarTitle\": \"Chat History\",\n    \"error\": \"Error\",\n    \"somethingWentWrong\": \"Something went wrong\",\n    \"validationSelectModel\": \"Please select a model to continue\",\n    \"deleteHistoryConfirmation\": \"Are you sure you want to delete this history?\",\n    \"editHistoryTitle\": \"Enter a new title\",\n    \"temporaryChat\": \"Temporary Chat\",\n    \"more\": {\n        \"copy\": {\n            \"group\": \"Copy\",\n            \"asText\": \"Copy as Text\",\n            \"asMarkdown\": \"Copy as Markdown\",\n            \"success\": \"Copied to clipboard!\"\n        },\n        \"download\": {\n            \"group\": \"Download\",\n            \"text\": \"Text File (.txt)\",\n            \"markdown\": \"Markdown (.md)\",\n            \"json\": \"JSON File (.json)\",\n            \"image\": \"Image (.png)\"\n        },\n        \"share\": \"Share\"\n    },\n    \"chatSaved\": \"Chat Saved\",\n    \"temporaryChatSavedSuccessfully\": \"Your temporary chat has been saved successfully\",\n    \"failedToSaveTemporaryChat\": \"Failed to save temporary chat. Please try again.\"\n}"
  },
  {
    "path": "src/assets/locale/en/playground.json",
    "content": "{\n    \"ollamaState\": {\n        \"searching\": \"Searching for Your Ollama 🦙\",\n        \"running\": \"Ollama is running 🦙\",\n        \"notRunning\": \"Unable to connect to Ollama 🦙\",\n        \"connectionError\": \"It seems like you are having a connection error. Please refer to this <anchor>documentation</anchor> for troubleshooting.\"\n    },\n    \"formError\": {\n        \"noModel\": \"Please select a model\",\n        \"noEmbeddingModel\": \"Please set an embedding model on the Settings > RAG page\"\n    },\n    \"form\": {\n        \"textarea\": {\n            \"placeholder\": \"Type a message...\"\n        },\n        \"webSearch\": {\n            \"on\": \"On\",\n            \"off\": \"Off\"\n        },\n        \"thinking\": {\n            \"on\": \"On\",\n            \"off\": \"Off\",\n            \"level\": \"Level\",\n            \"levels\": {\n                \"low\": \"Low\",\n                \"medium\": \"Medium\",\n                \"high\": \"High\"\n            }\n        }\n    },\n    \"tooltip\": {\n        \"searchInternet\": \"Search Internet\",\n        \"thinking\": \"Enable reasoning mode to see the model's thinking process\",\n        \"speechToText\": \"Speech to Text\",\n        \"uploadImage\": \"Upload Image\",\n        \"stopStreaming\": \"Stop Streaming\",\n        \"knowledge\": \"Knowledge\",\n        \"vision\": \"[Experimental] Vision Chat\",\n        \"clearContext\": \"Clear Context\",\n        \"uploadDocuments\": \"Upload Documents (beta)\",\n        \"mcpServers\": \"MCP Servers\",\n        \"mcpEmpty\": \"No MCP servers configured\",\n        \"mcpEmptyDesc\": \"Connect MCP servers to use tools in chat.\",\n        \"mcpAddServer\": \"Add MCP Server\"\n    },\n    \"sendWhenEnter\": \"Send when Enter pressed\",\n    \"welcome\": \"Hello! How can I help you today?\",\n    \"useOCR\": \"Extract text from image (OCR)\",\n    \"fileRetrievalEnabled\": \"Enable RAG for Documents\"\n}"
  },
  {
    "path": "src/assets/locale/en/settings.json",
    "content": "{\n  \"generalSettings\": {\n    \"title\": \"General Settings\",\n    \"settings\": {\n      \"heading\": \"Web UI Settings\",\n      \"speechRecognitionLang\": {\n        \"label\": \"Speech Recognition Language\",\n        \"placeholder\": \"Select a language\"\n      },\n      \"language\": {\n        \"label\": \"Language\",\n        \"placeholder\": \"Select a language\"\n      },\n      \"darkMode\": {\n        \"label\": \"Change Theme\",\n        \"options\": {\n          \"light\": \"Light\",\n          \"dark\": \"Dark\"\n        }\n      },\n      \"defaultCopilotPrompt\": {\n        \"label\": \"Default Prompt for SidePanel (Copilot)\",\n        \"placeholder\": \"Select a prompt\"\n      },\n      \"defaultWebUIPrompt\": {\n        \"label\": \"Default Prompt for Web UI\",\n        \"placeholder\": \"Select a prompt\"\n      },\n      \"copilotResumeLastChat\": {\n        \"label\": \"Resume the last chat when opening the SidePanel (Copilot)\"\n      },\n      \"turnOnChatWithWebsite\": {\n        \"label\": \"Enable Chat with Website by default (Copilot)\"\n      },\n      \"webUIResumeLastChat\": {\n        \"label\": \"Resume the last chat when opening the Web UI\"\n      },\n      \"hideCurrentChatModelSettings\": {\n        \"label\": \"Hide the current Chat Model Settings\"\n      },\n      \"restoreLastChatModel\": {\n        \"label\": \"Restore last used model for previous chats\"\n      },\n      \"sendNotificationAfterIndexing\": {\n        \"label\": \"Send Notification After Finishing Processing the Knowledge Base\"\n      },\n      \"generateTitle\": {\n        \"label\": \"Generate Title using AI\"\n      },\n      \"ollamaStatus\": {\n        \"label\": \"Enable or disable Ollama connection status check\"\n      },\n      \"wideMode\": {\n        \"label\": \"Enable wide screen mode\"\n      },\n      \"openReasoning\": {\n        \"label\": \"Open Reasoning Collapse by default\"\n      },\n      \"defaultThinkingMode\": {\n        \"label\": \"Show Thinking Mode State in Forms\"\n      },\n      \"userChatBubble\": {\n        \"label\": \"Use Chat Bubble for User Messages\"\n      },\n      \"autoCopyResponseToClipboard\": {\n        \"label\": \"Automatically Copy Response to Clipboard\"\n      },\n      \"useMarkdownForUserMessage\": {\n        \"label\": \"Enable Markdown formatting for User messages\"\n      },\n      \"copyAsFormattedText\": {\n        \"label\": \"Copy as Formatted Text\"\n      },\n      \"tabMentionsEnabled\": {\n        \"label\": \"Enable Tab Mentions (@tab)\"\n      },\n      \"pasteLargeTextAsFile\": {\n        \"label\": \"Paste Large Text as File\"\n      },\n      \"ocrLanguage\": {\n        \"label\": \"Default OCR Language\",\n        \"placeholder\": \"Select an OCR language\"\n      },\n      \"sidepanelTemporaryChat\": {\n        \"label\": \"Enable Temporary Chat in SidePanel by default\"\n      },\n      \"webuiTemporaryChat\": {\n        \"label\": \"Enable Temporary Chat in Web UI by default\"\n      },\n      \"removeReasoningTagFromCopy\": {\n        \"label\": \"Remove Reasoning Tag from Copied Text\"\n      },\n      \"youtubeAutoSummarize\": {\n        \"label\": \"Show the 'Summarize' button on YouTube videos.\"\n      },\n      \"hideReasoningWidget\": {\n        \"label\": \"Hide Reasoning Widget from AI Messages\"\n      },\n      \"persistChatInput\": {\n        \"label\": \"Persist Chat Input (Save unsent messages)\"\n      },\n      \"enableMessageQueue\": {\n        \"label\": \"Enable Message Queue While Streaming\"\n      },\n      \"optimizeQueueForSmallScreen\": {\n        \"label\": \"Optimize Chat UI for Small Screens\"\n      },\n      \"tableTextWrap\": {\n        \"label\": \"Enable Text Wrapping in Markdown Tables\"\n      },\n      \"showMoreForLargeMessage\": {\n        \"label\": \"Show 'Show more' for large human messages\"\n      },\n      \"sidebarPosition\": {\n        \"label\": \"Sidebar Position\",\n        \"options\": {\n          \"left\": \"Left\",\n          \"right\": \"Right\"\n        }\n      },\n      \"showMcpServersInChat\": {\n        \"label\": \"Show MCP Servers Toggle in Chat\"\n      }\n    },\n    \"sidepanelRag\": {\n      \"heading\": \"Retrieval Settings\",\n      \"ragEnabled\": {\n        \"label\": \"Enable Embedding and Retrieval\"\n      },\n      \"maxWebsiteContext\": {\n        \"label\": \"Maximum Content Size for Full Context Mode\",\n        \"placeholder\": \"Content size (default 4028)\"\n      }\n    },\n    \"webSearch\": {\n      \"heading\": \"Manage Web Search\",\n      \"searchMode\": {\n        \"label\": \"Perform Simple Internet Search\"\n      },\n      \"provider\": {\n        \"label\": \"Search Engine\",\n        \"placeholder\": \"Select a search engine\"\n      },\n      \"totalSearchResults\": {\n        \"label\": \"Total Search Results\",\n        \"placeholder\": \"Enter Total Search Results\"\n      },\n      \"visitSpecificWebsite\": {\n        \"label\": \"Visit the website mentioned in the message\"\n      },\n      \"searxng\": {\n        \"url\": {\n          \"label\": \"SearXNG URL\"\n        }\n      },\n      \"braveApi\": {\n        \"label\": \"Brave API Key\",\n        \"placeholder\": \"Enter your Brave API key\"\n      },\n      \"tavilyApi\": {\n        \"label\": \"Tavily API Key \",\n        \"placeholder\": \"Enter your Tavily API key\"\n      },\n      \"exa\": {\n        \"label\": \"Exa API Key\",\n        \"placeholder\": \"Enter your Exa API key\"\n      },\n      \"googleDomain\": {\n        \"label\": \"Google Domain\"\n      },\n      \"searchOnByDefault\": {\n        \"label\": \"Internet Search ON by default\"\n      },\n      \"firecrawlAPIKey\": {\n        \"label\": \"Firecrawl API Key\",\n        \"placeholder\": \"Enter your Firecrawl API key\"\n      },\n      \"domainFilter\": {\n        \"label\": \"Domain Filter List\",\n        \"description\": \"Only show results from these domains\",\n        \"placeholder\": \"e.g., example.com\"\n      },\n      \"blockedDomains\": {\n        \"label\": \"Blocked Domains\",\n        \"description\": \"Exclude results from these domains\",\n        \"placeholder\": \"e.g., spam.com\"\n      }\n    },\n    \"system\": {\n      \"heading\": \"System Settings\",\n      \"deleteChatHistory\": {\n        \"label\": \"System Reset\",\n        \"button\": \"Reset All\",\n        \"confirm\": \"Are you sure you want to perform a system reset? This will clear all data and cannot be undone.\"\n      },\n      \"export\": {\n        \"label\": \"Export All Data (Chat History, Knowledge Base, Prompts, and Settings)\",\n        \"button\": \"Export Data\",\n        \"success\": \"Export Success\"\n      },\n      \"import\": {\n        \"label\": \"Import All Data (Chat History, Knowledge Base, Prompts, and Settings)\",\n        \"button\": \"Import Data\",\n        \"success\": \"Import Success\",\n        \"error\": \"Import Error\"\n      },\n      \"actionIcon\": {\n        \"label\": \"Set Default Action for Extension Icon Clicks\"\n      },\n      \"contextMenu\": {\n        \"label\": \"Set Default action for Context Menu\"\n      },\n      \"fontSize\": {\n        \"label\": \"Font Size\"\n      },\n      \"webuiBtnSidePanel\": {\n        \"label\": \"Show Web UI Button in Side Panel\"\n      },\n      \"chatBackgroundImage\": {\n        \"label\": \"Chat Background Image\"\n      },\n      \"storageSyncEnabled\": {\n        \"label\": \"Enable Browser Storage Sync (Sync settings across devices)\"\n      }\n    },\n    \"tts\": {\n      \"heading\": \"Text-to-Speech Settings\",\n      \"ttsEnabled\": {\n        \"label\": \"Enable Text-to-Speech\"\n      },\n      \"ttsAutoPlay\": {\n        \"label\": \"Auto play voice response after completion\"\n      },\n      \"ttsProvider\": {\n        \"label\": \"Text-to-Speech Provider\",\n        \"placeholder\": \"Select a provider\"\n      },\n      \"ttsVoice\": {\n        \"label\": \"Text-to-Speech Voice\",\n        \"placeholder\": \"Select a voice\"\n      },\n      \"ssmlEnabled\": {\n        \"label\": \"Enable SSML (Speech Synthesis Markup Language)\"\n      },\n      \"responseSplitting\": {\n        \"label\": \"Response Splitting\"\n      },\n      \"removeReasoningTagTTS\": {\n        \"label\": \"Remove Reasoning Tag from TTS\"\n      }\n    },\n    \"stt\": {\n      \"heading\": \"Speech-to-Text Settings\",\n      \"autoStopTimeout\": {\n        \"label\": \"Auto Stop Timeout (ms)\",\n        \"placeholder\": \"Enter auto-stop timeout in milliseconds\"\n      },\n      \"autoSubmitVoiceMessage\": {\n        \"label\": \"Auto Submit Voice Message\"\n      }\n    }\n  },\n  \"manageModels\": {\n    \"title\": \"Manage Models\",\n    \"addBtn\": \"Add New Model\",\n    \"columns\": {\n      \"name\": \"Name\",\n      \"digest\": \"Digest\",\n      \"nickname\": \"Nickname\",\n      \"modifiedAt\": \"Modified At\",\n      \"size\": \"Size\",\n      \"actions\": \"Actions\"\n    },\n    \"expandedColumns\": {\n      \"parentModel\": \"Parent Model\",\n      \"format\": \"Format\",\n      \"family\": \"Family\",\n      \"parameterSize\": \"Parameter Size\",\n      \"quantizationLevel\": \"Quantization Level\"\n    },\n    \"tooltip\": {\n      \"delete\": \"Delete Model\",\n      \"repull\": \"Re-Pull Model\",\n      \"editNickname\": \"Edit Nickname\"\n    },\n    \"confirm\": {\n      \"delete\": \"Are you sure you want to delete this model?\",\n      \"repull\": \"Are you sure you want to re-pull this model?\"\n    },\n    \"modal\": {\n      \"title\": \"Add New Model\",\n      \"placeholder\": \"Enter Model Name\",\n      \"pull\": \"Pull Model\"\n    },\n    \"notification\": {\n      \"pullModel\": \"Pulling Model\",\n      \"pullModelDescription\": \"Pulling {{modelName}} model. For more details, check the extension icon.\",\n      \"cancellingDownload\": \"Cancelling Download\",\n      \"cancellingDownloadDescription\": \"Model download is being cancelled...\",\n      \"success\": \"Success\",\n      \"error\": \"Error\",\n      \"successDescription\": \"Successfully pulled the model\",\n      \"successDeleteDescription\": \"Successfully deleted the model\",\n      \"someError\": \"Something went wrong. Please try again later\"\n    }\n  },\n  \"managePrompts\": {\n    \"title\": \"Manage Prompts\",\n    \"addBtn\": \"Add New Prompt\",\n    \"option1\": \"Normal\",\n    \"option2\": \"RAG\",\n    \"questionPrompt\": \"Question Prompt\",\n    \"segmented\": {\n      \"custom\": \"Custom Prompts\",\n      \"copilot\": \"Copilot Prompts\"\n    },\n    \"columns\": {\n      \"title\": \"Title\",\n      \"prompt\": \"Prompt\",\n      \"type\": \"Prompt Type\",\n      \"actions\": \"Actions\"\n    },\n    \"systemPrompt\": \"System Prompt\",\n    \"quickPrompt\": \"Quick Prompt\",\n    \"tooltip\": {\n      \"delete\": \"Delete Prompt\",\n      \"edit\": \"Edit Prompt\"\n    },\n    \"confirm\": {\n      \"delete\": \"Are you sure you want to delete this prompt? This action cannot be undone.\"\n    },\n    \"modal\": {\n      \"addTitle\": \"Add New Prompt\",\n      \"editTitle\": \"Edit Prompt\"\n    },\n    \"form\": {\n      \"title\": {\n        \"label\": \"Title\",\n        \"placeholder\": \"My Awesome Prompt\",\n        \"required\": \"Please enter a title\"\n      },\n      \"prompt\": {\n        \"label\": \"Prompt\",\n        \"placeholder\": \"Enter Prompt\",\n        \"required\": \"Please enter a prompt\",\n        \"help\": \"You can use {key} as variable in your prompt.\",\n        \"missingTextPlaceholder\": \"The {text} variable is missing in the prompt. Please add it.\"\n      },\n      \"isSystem\": {\n        \"label\": \"Is System Prompt\"\n      },\n      \"btnSave\": {\n        \"saving\": \"Adding Prompt...\",\n        \"save\": \"Add Prompt\"\n      },\n      \"btnEdit\": {\n        \"saving\": \"Updating Prompt...\",\n        \"save\": \"Update Prompt\"\n      }\n    },\n    \"notification\": {\n      \"addSuccess\": \"Prompt Added\",\n      \"addSuccessDesc\": \"Prompt has been added successfully\",\n      \"error\": \"Error\",\n      \"someError\": \"Something went wrong. Please try again later\",\n      \"updatedSuccess\": \"Prompt Updated\",\n      \"updatedSuccessDesc\": \"Prompt has been updated successfully\",\n      \"deletedSuccess\": \"Prompt Deleted\",\n      \"deletedSuccessDesc\": \"Prompt has been deleted successfully\"\n    }\n  },\n  \"manageShare\": {\n    \"title\": \"Manage Share\",\n    \"heading\": \"Configure Page Share URL\",\n    \"form\": {\n      \"url\": {\n        \"label\": \"Page Share URL\",\n        \"placeholder\": \"Enter Page Share URL\",\n        \"required\": \"Please input your Page Share URL!\",\n        \"help\": \"For privacy reasons, you can self-host the page share and provide the URL here. <anchor>Learn More</anchor>.\"\n      }\n    },\n    \"webshare\": {\n      \"heading\": \"Web Share\",\n      \"columns\": {\n        \"title\": \"Title\",\n        \"url\": \"URL\",\n        \"actions\": \"Actions\"\n      },\n      \"tooltip\": {\n        \"delete\": \"Delete Share\"\n      },\n      \"confirm\": {\n        \"delete\": \"Are you sure you want to delete this share? This action cannot be undone.\"\n      },\n      \"label\": \"Manage Page Share\",\n      \"description\": \"Enable or disable the page share feature\"\n    },\n    \"notification\": {\n      \"pageShareSuccess\": \"Page Share URL updated successfully\",\n      \"someError\": \"Something went wrong. Please try again later\",\n      \"webShareDeleteSuccess\": \"Web Share deleted successfully\"\n    }\n  },\n  \"ollamaSettings\": {\n    \"title\": \"Ollama Settings\",\n    \"heading\": \"Configure Ollama\",\n    \"settings\": {\n      \"ollamaUrl\": {\n        \"label\": \"Ollama URL\",\n        \"placeholder\": \"Enter Ollama URL\"\n      },\n      \"globalEnable\": {\n        \"label\": \"Enable or Disable Ollama Integration Globally\",\n        \"warning\": \"By disabling Ollama integration globally, Page Assist won't fetch models from Ollama. You can still add Ollama instance from the <anchor>OpenAI compatible API</anchor> section which will work fine.\"\n      },\n      \"advanced\": {\n        \"label\": \"Advance Ollama URL Configuration\",\n        \"urlRewriteEnabled\": {\n          \"label\": \"Enable or Disable Custom Origin URL\"\n        },\n        \"rewriteUrl\": {\n          \"label\": \"Custom Origin URL\",\n          \"placeholder\": \"Enter Custom Origin URL\"\n        },\n        \"autoCORSFix\": {\n          \"label\": \"Enable or Disable Automatic Ollama CORS Fix\"\n        },\n        \"headers\": {\n          \"label\": \"Custom Headers\",\n          \"add\": \"Add Header\",\n          \"key\": {\n            \"label\": \"Header Key\",\n            \"placeholder\": \"Authorization\"\n          },\n          \"value\": {\n            \"label\": \"Header Value\",\n            \"placeholder\": \"Bearer token\"\n          }\n        },\n        \"help\": \"If you have connection issues with Ollama on Page Assist, you can configure a custom origin URL. To learn more about the configuration, <anchor>click here</anchor>.\"\n      }\n    }\n  },\n  \"manageSearch\": {\n    \"title\": \"Manage Web Search\",\n    \"heading\": \"Configure Web Search\"\n  },\n  \"mcpSettings\": {\n    \"title\": \"MCP Servers\",\n    \"heading\": \"Manage MCP Servers\",\n    \"subheading\": \"Add HTTP MCP servers for normal chat, including bearer auth and custom headers.\",\n    \"addBtn\": \"Add MCP Server\",\n    \"table\": {\n      \"name\": \"Name\",\n      \"url\": \"URL\",\n      \"tools\": \"Cached Tools\",\n      \"status\": \"Validation\",\n      \"notValidated\": \"Not validated yet\",\n      \"toolsUnavailable\": \"No cached tools\",\n      \"auth\": \"Auth\",\n      \"enabled\": \"Enabled\",\n      \"actions\": \"Actions\"\n    },\n    \"actions\": {\n      \"refreshTools\": \"Refresh tools\",\n      \"enable\": \"Enable\",\n      \"disable\": \"Disable\"\n    },\n    \"status\": {\n      \"ready\": \"{{count}} tools available\",\n      \"failed\": \"Validation failed\",\n      \"notValidated\": \"Not validated\",\n      \"lastChecked\": \"Last checked: {{value}}\"\n    },\n    \"auth\": {\n      \"none\": \"No auth\",\n      \"bearer\": \"Bearer token\"\n    },\n    \"modal\": {\n      \"titleAdd\": \"Add MCP Server\",\n      \"titleEdit\": \"Edit MCP Server\",\n      \"deleteConfirm\": \"Delete {{name}}?\",\n      \"name\": {\n        \"label\": \"Server Name\",\n        \"placeholder\": \"My MCP Server\",\n        \"required\": \"Please enter a server name\"\n      },\n      \"url\": {\n        \"label\": \"Server URL\",\n        \"placeholder\": \"https://example.com/mcp\",\n        \"help\": \"Only Streamable HTTP MCP endpoints over HTTP or HTTPS are supported right now.\",\n        \"required\": \"Please enter a server URL\",\n        \"invalid\": \"Please enter a valid HTTP or HTTPS URL\"\n      },\n      \"transportNotice\": {\n        \"title\": \"Transport support\",\n        \"description\": \"Page Assist supports Streamable HTTP MCP servers only. Legacy SSE endpoints such as /sse or /messages?sessionId=... are not supported.\"\n      },\n      \"auth\": {\n        \"label\": \"Authentication\"\n      },\n      \"bearerToken\": {\n        \"label\": \"Bearer Token\",\n        \"placeholder\": \"Enter bearer token\",\n        \"required\": \"Please enter a bearer token\"\n      },\n      \"enabled\": {\n        \"label\": \"Global Enable\"\n      },\n      \"headers\": {\n        \"label\": \"Custom Headers\",\n        \"add\": \"Add Header\",\n        \"key\": {\n          \"label\": \"Header Key\",\n          \"placeholder\": \"X-Request-ID\"\n        },\n        \"value\": {\n          \"label\": \"Header Value\",\n          \"placeholder\": \"value\"\n        }\n      },\n      \"validation\": {\n        \"title\": \"Validate Tools\",\n        \"help\": \"Connect to the MCP server, fetch available tools, and cache them before saving. Validation expects a Streamable HTTP MCP endpoint.\",\n        \"button\": \"Validate and Load Tools\",\n        \"availableTools\": \"Available tools\",\n        \"idle\": \"Validate this server to confirm the connection and load its available tools.\",\n        \"stale\": \"Connection details changed. Validate again to refresh the cached tools.\",\n        \"failed\": \"Could not validate this MCP server.\",\n        \"success\": \"Validated successfully. {{count}} tools are available.\",\n        \"syncedAt\": \"Last synced: {{value}}\",\n        \"saveHint\": \"Saving will validate the current configuration if the cached tool list is stale or missing.\"\n      },\n      \"submit\": \"Save MCP Server\",\n      \"update\": \"Update MCP Server\"\n    },\n    \"notification\": {\n      \"added\": \"MCP server added\",\n      \"updated\": \"MCP server updated\",\n      \"deleted\": \"MCP server deleted\",\n      \"validated\": \"Loaded {{count}} MCP tools\",\n      \"toolsRefreshed\": \"Refreshed tools for {{name}}\",\n      \"validationFailedTitle\": \"MCP validation failed\",\n      \"storageBlockedTitle\": \"Page Assist can't save data\",\n      \"storageBlockedDescription\": \"Firefox Private Mode does not support saving data to IndexedDB. Please manage MCP servers from a normal window.\"\n    }\n  },\n  \"about\": {\n    \"title\": \"About\",\n    \"heading\": \"About\",\n    \"chromeVersion\": \"Page Assist Version\",\n    \"ollamaVersion\": \"Ollama Version\",\n    \"support\": \"You can support the Page Assist project by donating or sponsoring through the following platforms:\",\n    \"koFi\": \"Support on Ko-fi\",\n    \"githubSponsor\": \"Sponsor on GitHub\",\n    \"githubRepo\": \"GitHub Repository\"\n  },\n  \"manageKnowledge\": {\n    \"title\": \"Manage Knowledge\",\n    \"heading\": \"Configure Knowledge Base\"\n  },\n  \"rag\": {\n    \"title\": \"Pipeline Settings\",\n    \"ragSettings\": {\n      \"label\": \"RAG Settings\",\n      \"model\": {\n        \"label\": \"Embedding Model\",\n        \"required\": \"Please select a model\",\n        \"help\": \"Highly recommended to use embedding models like `nomic-embed-text`.\",\n        \"placeholder\": \"Select a model\"\n      },\n      \"chunkSize\": {\n        \"label\": \"Chunk Size\",\n        \"placeholder\": \"Enter Chunk Size\",\n        \"required\": \"Please enter a chunk size\"\n      },\n      \"chunkOverlap\": {\n        \"label\": \"Chunk Overlap\",\n        \"placeholder\": \"Enter Chunk Overlap\",\n        \"required\": \"Please enter a chunk overlap\"\n      },\n      \"totalFilePerKB\": {\n        \"label\": \"Knowledge Base Default File Upload Limit\",\n        \"placeholder\": \"Enter default file upload limit (e.g., 10)\",\n        \"required\": \"Please enter the default file upload limit\"\n      },\n      \"noOfRetrievedDocs\": {\n        \"label\": \"Number of Retrieved Documents\",\n        \"placeholder\": \"Enter Number of Retrieved Documents\",\n        \"required\": \"Please enter the number of retrieved documents\"\n      },\n      \"splittingSeparator\": {\n        \"label\": \"Separator\",\n        \"placeholder\": \"Enter Separator (e.g., \\\\n\\\\n)\",\n        \"required\": \"Please enter a separator\"\n      },\n      \"splittingStrategy\": {\n        \"label\": \"Text Splitter\"\n      }\n    },\n    \"prompt\": {\n      \"label\": \"Configure RAG Prompt\",\n      \"option1\": \"Normal\",\n      \"option2\": \"Web\",\n      \"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\",\n      \"systemPrompt\": \"System Prompt\",\n      \"systemPromptPlaceholder\": \"Enter System Prompt\",\n      \"webSearchPrompt\": \"Web Search Prompt\",\n      \"webSearchPromptHelp\": \"Do not remove `{search_results}` from the prompt.\",\n      \"webSearchPromptError\": \"Please enter a web search prompt\",\n      \"webSearchPromptPlaceholder\": \"Enter Web Search Prompt\",\n      \"webSearchFollowUpPrompt\": \"Web Search Follow Up Prompt\",\n      \"webSearchFollowUpPromptHelp\": \"Do not remove `{chat_history}` and `{question}` from the prompt.\",\n      \"webSearchFollowUpPromptError\": \"Please input your Web Search Follow Up Prompt!\",\n      \"webSearchFollowUpPromptPlaceholder\": \"Your Web Search Follow Up Prompt\"\n    }\n  },\n  \"chromeAiSettings\": {\n    \"title\": \"Chrome AI Settings\"\n  },\n  \"mermaid\": \"Mermaid\"\n}\n"
  },
  {
    "path": "src/assets/locale/en/sidepanel.json",
    "content": "{\n    \"tooltip\": {\n        \"embed\": \"It may take a few minutes to embed the page. Please wait...\",\n        \"clear\": \"Erase chat history\",\n        \"history\": \"Chat history\", \n        \"openwebui\": \"Open WebUI\"\n    }\n}"
  },
  {
    "path": "src/assets/locale/es/chrome.json",
    "content": "{\n    \"heading\": \"Configurar Chrome AI\",\n    \"status\": {\n        \"label\": \"Habilitar o deshabilitar el soporte de Chrome AI en Page Assist\"\n    },\n    \"error\": {\n        \"browser_not_supported\": \"Esta versión de Chrome no es compatible con el modelo Gemini Nano. Por favor, actualice a la versión 127 o posterior.\",\n        \"ai_not_supported\": \"La configuración chrome://flags/#prompt-api-for-gemini-nano no está habilitada. Por favor, habilítela.\",\n        \"ai_not_ready\": \"Gemini Nano aún no está listo; necesita verificar la configuración de Chrome.\",\n        \"internal_error\": \"Ocurrió un error interno. Por favor, inténtelo de nuevo más tarde.\"\n    },\n    \"errorDescription\": \"Para usar Chrome AI, necesita la versión 138 o posterior de Chrome. Siga estos pasos:\\n\\n1. Vaya a `chrome://flags/#prompt-api-for-gemini-nano` y habilite \\\"Prompt API for Gemini Nano\\\".\\n2. Reinicie Chrome para aplicar el cambio.\\n3. Regrese a esta página y haga clic en \\\"Descargar Modelo\\\" — esto descargará un modelo de 4GB por primera vez.\\n4. Una vez descargado, Gemini Nano se puede habilitar a través de Page Assist.\",\n    \"downloadModel\": \"Descargar Modelo\",\n    \"modelDownloadWarning\": \"Esto descargará un modelo con un tamaño de descarga aproximado que varía entre 1.5 GB y 2.4 GB. Asegúrese de tener suficiente espacio en disco.\"\n}"
  },
  {
    "path": "src/assets/locale/es/common.json",
    "content": "{\n    \"pageAssist\": \"Page Assist\",\n    \"selectAModel\": \"Selecione un Modelo\",\n    \"save\": \"Guardar\",\n    \"saved\": \"Guardado\",\n    \"cancel\": \"Cancelar\",\n    \"retry\": \"Reintentar\",\n    \"share\": {\n        \"tooltip\": {\n            \"share\": \"Compartir\"\n        },\n        \"modal\": {\n            \"title\": \"Compartir enlace para chat\"\n        },\n        \"form\": {\n            \"defaultValue\": {\n                \"name\": \"Anónimo\",\n                \"title\": \"Chat sin título\"\n            },\n            \"title\": {\n                \"label\": \"Título del Chat\",\n                \"placeholder\": \"Ingresar el título del Chat\",\n                \"required\": \"El título del Chat es obligatorio\"\n            },\n            \"name\": {\n                \"label\": \"Tu nombre\",\n                \"placeholder\": \"Ingresar tu nombre\",\n                \"required\": \"Tu nombre es obligatorio\"\n            },\n            \"btn\": {\n                \"save\": \"Generar enlace\",\n                \"saving\": \"Generando enlace...\"\n            }\n        },\n        \"notification\": {\n            \"successGenerate\": \"Enlace copiado al Clipboard\",\n            \"failGenerate\": \"Fallo al generar el enlace\"\n        }\n    },\n    \"copyToClipboard\": \"Copiar al clipboard\",\n    \"webSearch\": \"Buscando en la web\",\n    \"regenerate\": \"Regenerar\",\n    \"continue\": \"Continuar Respuesta\",\n    \"edit\": \"Editar\",\n    \"delete\": \"Borrar\",\n    \"saveAndSubmit\": \"Guardar y Enviar\",\n    \"editMessage\": {\n        \"placeholder\": \"Ingresar un mensaje...\"\n    },\n    \"submit\": \"Enviar\",\n    \"noData\": \"Sin datos\",\n    \"noHistory\": \"Chat sin histórico\",\n    \"chatWithCurrentPage\": \"Conversar con la página actual\",\n    \"beta\": \"Beta\",\n    \"tts\": \"Leer en voz alta\",\n    \"currentChatModelSettings\": \"Configuraciones del Modelo de Chat Actual\",\n    \"modelSettings\": {\n        \"label\": \"Configuraciones del Modelo\",\n        \"description\": \"Definir las opciones del modelo globalmente para todos los chats\",\n        \"form\": {\n            \"keepAlive\": {\n                \"label\": \"Mantener vivo\",\n                \"help\": \"controlar cuanto tiempo el modelo permanecera cargado en la memoria luego de su utilización (por defecto: 5m)\",\n                \"placeholder\": \"Ingresar duración para mantenerlo vivo (ej: 5m, 10m, 1h)\"\n            },\n            \"temperature\": {\n                \"label\": \"Temperatura\",\n                \"placeholder\": \"Ingresar valor de la Temperatura (ej: 0.7, 1.0)\"\n            },\n            \"numCtx\": {\n                \"label\": \"Tamaño de la Ventana de Contexto (num_ctx)\",\n                \"placeholder\": \"Ingrese el valor del tamaño de la ventana de contexto (por defecto: 2048)\"\n            },\n            \"numPredict\": {\n                \"label\": \"Máximo de Tokens (num_predict)\",\n                \"placeholder\": \"Ingrese el valor máximo de Tokens (ej: 2048, 4096)\"\n            },\n            \"seed\": {\n                \"label\": \"Semilla\",\n                \"placeholder\": \"Ingresar el valor de la semilla (ej: 1234)\",\n                \"help\": \"Reproductibilidad de la salida del modelo\"\n            },\n            \"topK\": {\n                \"label\": \"Top K\",\n                \"placeholder\": \"Ingresar el valor de Top K (ej: 40, 100)\"\n            },\n            \"topP\": {\n                \"label\": \"Top P\",\n                \"placeholder\": \"Ingresar el valor de Top P (ej: 0.9, 0.95)\"\n            },\n            \"numGpu\": {\n                \"label\": \"Num GPU\",\n                \"placeholder\": \"Ingrese el número de capas para enviar a la(s) GPU(s)\"\n            },\n            \"systemPrompt\": {\n                \"label\": \"Prompt de Sistema Temporal\",\n                \"placeholder\": \"Ingrese el Prompt de Sistema\",\n                \"help\": \"Esta es una forma rápida de establecer el prompt de sistema en el chat actual, que anulará el prompt de sistema seleccionado si existe.\"\n            }\n        },\n        \"advanced\": \"Más Configuraciones del Modelo\"\n    },\n    \"copilot\": {\n        \"summary\": \"Resumir\",\n        \"explain\": \"Explicar\",\n        \"rephrase\": \"Reformular\",\n        \"translate\": \"Traducir\"\n    },\n    \"citations\": \"Citas\",\n    \"downloadCode\": \"Descargar Código\",\n    \"date\": {\n        \"pinned\": \"Fijado\",\n        \"today\": \"Hoy\",\n        \"yesterday\": \"Ayer\",\n        \"last7Days\": \"Últimos 7 días\",\n        \"older\": \"Más antiguo\"\n    },\n    \"range\": {\n        \"deleteConfirm\": {\n            \"pinned\": \"¿Está seguro de que desea eliminar todos los mensajes fijados?\",\n            \"today\": \"¿Está seguro de que desea eliminar todos los mensajes de hoy?\",\n            \"yesterday\": \"¿Está seguro de que desea eliminar todos los mensajes de ayer?\",\n            \"last7Days\": \"¿Está seguro de que desea eliminar todos los mensajes de los últimos 7 días?\",\n            \"older\": \"¿Está seguro de que desea eliminar todos los mensajes más antiguos?\"\n        },\n        \"tooltip\": {\n            \"pinned\": \"Eliminar todos los mensajes fijados\",\n            \"today\": \"Eliminar todos los mensajes de hoy\",\n            \"yesterday\": \"Eliminar todos los mensajes de ayer\",\n            \"last7Days\": \"Eliminar todos los mensajes de los últimos 7 días\",\n            \"older\": \"Eliminar todos los mensajes más antiguos\"\n        }\n    },\n    \"pin\": \"Fijar\",\n    \"unpin\": \"Desfijar\",\n    \"generationInfo\": \"Información de Generación\",\n    \"sidebarChat\": \"Chat lateral\",\n    \"reasoning\": {\n        \"thinking\": \"Pensando....\",\n        \"thought\": \"Pensamiento por {{time}}\"\n    },\n    \"embeddingGen\": \"Creando embeddings, esto puede tardar un poco\",\n    \"semanticSearch\": \"Realizando búsqueda semántica\",\n    \"downloading\": \"Descargando\",\n    \"cancelPullingModel\": {\n        \"confirm\": \"¿Está seguro de que desea cancelar la descarga? Esto detendrá el proceso de descarga. Según la documentación de Ollama, puede reiniciar desde donde lo dejó.\"\n    }\n}"
  },
  {
    "path": "src/assets/locale/es/knowledge.json",
    "content": "{\n    \"addBtn\": \"Agregar Nuevo Conocimiento\",\n    \"columns\": {\n        \"title\": \"Título\",\n        \"status\": \"Estado\",\n        \"embeddings\": \"Modelo de Embedding\",\n        \"createdAt\": \"Creado\",\n        \"action\": \"Acciones\"\n    },\n    \"expandedColumns\": {\n        \"name\": \"Nombre\"\n    },\n    \"confirm\": {\n        \"delete\": \"¿Esta seguro que desea borrar este conocimiento?\"\n    },\n    \"deleteSuccess\": \"Conocimiento borrado\",\n    \"status\": {\n        \"pending\": \"Pendiente\",\n        \"finished\": \"Finalizado\",\n        \"processing\": \"Procesando\",\n        \"failed\": \"Fallido\"\n    },\n    \"addKnowledge\": \"Agregar Conocimiento\",\n    \"updateKnowledge\": \"Actualizar Conocimiento\",\n    \"form\": {\n        \"title\": {\n            \"label\": \"Título del Conocimiento\",\n            \"placeholder\": \"Ingresar un título de conocimiento\",\n            \"required\": \"El Título de conocimiento es obligatorio\"\n        },\n        \"uploadFile\": {\n            \"label\": \"Subir un Archivo\",\n            \"uploadText\": \"Arraste y suelte un archivo aquí o haga click para subirlo\",\n            \"uploadHint\": \"Tipos de archivo soportados: .pdf, .csv, .txt, .md, .docx\",\n            \"required\": \"El archivo es obligatorio\"\n        },\n        \"submit\": \"Enviar\",\n        \"success\": \"Conocimiento agregado exitosamente\"\n    },\n    \"noEmbeddingModel\": \"Por favor, agregue un modelo de embedding de la página de configuraciones de RAG primero\"\n}\n"
  },
  {
    "path": "src/assets/locale/es/openai.json",
    "content": "{\n    \"settings\": \"API compatible con OpenAI\",\n    \"heading\": \"API compatible con OpenAI\",\n    \"subheading\": \"Gestiona y configura tus proveedores compatibles con la API de OpenAI aquí.\",\n    \"addBtn\": \"Añadir Proveedor\",\n    \"table\": {\n        \"name\": \"Nombre del Proveedor\",\n        \"baseUrl\": \"URL Base\",\n        \"actions\": \"Acción\"\n    },\n    \"modal\": {\n        \"titleAdd\": \"Añadir Nuevo Proveedor\",\n        \"name\": {\n            \"label\": \"Nombre del Proveedor\",\n            \"required\": \"El nombre del proveedor es obligatorio.\",\n            \"placeholder\": \"Introduce el nombre del proveedor\"\n        },\n        \"baseUrl\": {\n            \"label\": \"URL Base\",\n            \"help\": \"La URL base del proveedor de la API de OpenAI. ej. (http://localhost:1234/v1)\",\n            \"required\": \"La URL base es obligatoria.\",\n            \"placeholder\": \"Introduce la URL base\"\n        },\n        \"apiKey\": {\n            \"label\": \"Clave API\",\n            \"required\": \"La clave API es obligatoria.\",\n            \"placeholder\": \"Introduce la clave API\"\n        },\n        \"submit\": \"Guardar\",\n        \"update\": \"Actualizar\",\n        \"deleteConfirm\": \"¿Estás seguro de que quieres eliminar este proveedor?\",\n        \"model\": {\n            \"title\": \"Lista de Modelos\",\n            \"subheading\": \"Por favor, selecciona los modelos de chat que quieres usar con este proveedor.\",\n            \"success\": \"Nuevos modelos añadidos con éxito.\"\n        },\n        \"tipLMStudio\": \"Page Assist obtendrá automáticamente los modelos que hayas cargado en LM Studio. No necesitas añadirlos manualmente.\"\n    },\n    \"addSuccess\": \"Proveedor añadido con éxito.\",\n    \"deleteSuccess\": \"Proveedor eliminado con éxito.\",\n    \"updateSuccess\": \"Proveedor actualizado con éxito.\",\n    \"delete\": \"Eliminar\",\n    \"edit\": \"Editar\",\n    \"newModel\": \"Añadir Modelos al Proveedor\",\n    \"noNewModel\": \"Para LMStudio, Ollama, Llamafile, obtenemos dinámicamente. No se necesita adición manual.\",\n    \"searchModel\": \"Buscar Modelo\",\n    \"selectAll\": \"Seleccionar Todo\",\n    \"save\": \"Guardar\",\n    \"saving\": \"Guardando...\",\n    \"manageModels\": {\n        \"columns\": {\n            \"name\": \"Nombre del Modelo\",\n            \"model_type\": \"Tipo de Modelo\",\n            \"model_id\": \"ID del Modelo\",\n            \"provider\": \"Nombre del Proveedor\",\n            \"actions\": \"Acción\",\n            \"nickname\": \"Apodo del Modelo\"\n        },\n        \"tooltip\": {\n            \"delete\": \"Eliminar\"\n        },\n        \"confirm\": {\n            \"delete\": \"¿Estás seguro de que quieres eliminar este modelo?\"\n        },\n        \"modal\": {\n            \"title\": \"Añadir Modelo Personalizado\",\n            \"form\": {\n                \"name\": {\n                    \"label\": \"ID del Modelo\",\n                    \"placeholder\": \"llama3.2\",\n                    \"required\": \"El ID del modelo es obligatorio.\"\n                },\n                \"provider\": {\n                    \"label\": \"Proveedor\",\n                    \"placeholder\": \"Seleccionar proveedor\",\n                    \"required\": \"El proveedor es obligatorio.\"\n                },\n                \"type\": {\n                    \"label\": \"Tipo de Modelo\"\n                }\n            }\n        }\n    },\n    \"noModelFound\": \"No se encontró ningún modelo. Asegúrate de haber añadido el proveedor correcto con la URL base y la clave API.\",\n    \"radio\": {\n        \"chat\": \"Modelo de Chat\",\n        \"embedding\": \"Modelo de Incrustación\",\n        \"chatInfo\": \"se utiliza para la finalización de chat y la generación de conversaciones\",\n        \"embeddingInfo\": \"se utiliza para RAG y otras tareas relacionadas con la búsqueda semántica.\"\n    },\n    \"nicknameModal\": {\n        \"title\": \"Añadir / Editar Apodo del Modelo\",\n        \"form\": {\n            \"modelName\": {\n                \"label\": \"Nombre del Modelo\",\n                \"placeholder\": \"Ingrese el nombre del modelo\",\n                \"required\": \"El nombre del modelo es obligatorio.\"\n            },\n            \"modelAvatar\": {\n                \"label\": \"Avatar del Modelo\",\n                \"placeholder\": \"Ingrese el avatar del modelo\",\n                \"help\": \"Por favor ingrese la URL del avatar del modelo. Esta imagen se mostrará en la ventana de chat.\"\n            }\n        }\n    }}"
  },
  {
    "path": "src/assets/locale/es/option.json",
    "content": "{\n    \"newChat\": \"Nuevo Chat\",\n    \"selectAPrompt\": \"Selecione un Prompt\",\n    \"githubRepository\": \"Repositorio de GitHub\",\n    \"settings\": \"Configuraciones\",\n    \"sidebarTitle\": \"Histórico del Chat\",\n    \"error\": \"Error\",\n    \"somethingWentWrong\": \"Hubo un error\",\n    \"validationSelectModel\": \"Selecione un modelo para continuar\",\n    \"deleteHistoryConfirmation\": \"¿Esta seguro que quiere borrar éste histórico?\",\n    \"editHistoryTitle\": \"Ingrese un nuevo título\",\n    \"temporaryChat\": \"Chat Temporal\",\n    \"more\": {\n        \"copy\": {\n            \"group\": \"Copiar\",\n            \"asText\": \"Copiar como Texto\",\n            \"asMarkdown\": \"Copiar como Markdown\",\n            \"success\": \"¡Copiado al portapapeles!\"\n        },\n        \"download\": {\n            \"group\": \"Descargar\",\n            \"text\": \"Archivo de Texto (.txt)\",\n            \"markdown\": \"Markdown (.md)\",\n            \"json\": \"Archivo JSON (.json)\"\n        },\n        \"share\": \"Compartir\"\n    }\n}"
  },
  {
    "path": "src/assets/locale/es/playground.json",
    "content": "{\n    \"ollamaState\": {\n        \"searching\": \"Buscando tu Ollama 🦙\",\n        \"running\": \"Ollama está funcionando 🦙\",\n        \"notRunning\": \"No fue posible conectar con Ollama 🦙\",\n        \"connectionError\": \"Hubo un error de conexión. Por favor, consulte la <anchor>documentación</anchor> para solucionar el problema.\"\n    },\n    \"formError\": {\n        \"noModel\": \"Por favor, selecione un modelo\",\n        \"noEmbeddingModel\": \"Por favor, defina un modelo de embedding para la página de configuraciones > RAG\"\n    },\n    \"form\": {\n        \"textarea\": {\n            \"placeholder\": \"Ingrese un mensaje...\"\n        },\n        \"webSearch\": {\n            \"on\": \"On\",\n            \"off\": \"Off\"\n        }\n    },\n    \"tooltip\": {\n        \"searchInternet\": \"Buscar en Internet\",\n        \"speechToText\": \"Voz a Texto\",\n        \"uploadImage\": \"Subir Imagén\",\n        \"stopStreaming\": \"Parar Transmisión\",\n        \"knowledge\": \"Conocimiento\",\n        \"clearContext\": \"Limpiar Contexto\"\n    },\n    \"sendWhenEnter\": \"Enviar cuando presione Enter\",\n    \"welcome\": \"¡Hola! ¿Cómo puedo ayudarte hoy?\",\n    \"useOCR\": \"Extraer texto de imagen (OCR)\"\n}"
  },
  {
    "path": "src/assets/locale/es/settings.json",
    "content": "{\n  \"generalSettings\": {\n    \"title\": \"Configuraciones Generales\",\n    \"settings\": {\n      \"heading\": \"Configuraciones de la Interfaz Web\",\n      \"speechRecognitionLang\": {\n        \"label\": \"Idioma de Reconocimiento de Voz\",\n        \"placeholder\": \"Selecione un idioma\"\n      },\n      \"language\": {\n        \"label\": \"Idioma\",\n        \"placeholder\": \"Selecione un idioma\"\n      },\n      \"darkMode\": {\n        \"label\": \"Cambiar Tema\",\n        \"options\": {\n          \"light\": \"Claro\",\n          \"dark\": \"Oscuro\"\n        }\n      },\n      \"defaultCopilotPrompt\": {\n        \"label\": \"Prompt predeterminado para el Panel Lateral (Copilot)\",\n        \"placeholder\": \"Seleccionar un prompt\"\n      },\n      \"defaultWebUIPrompt\": {\n        \"label\": \"Prompt predeterminado para la Interfaz Web\",\n        \"placeholder\": \"Seleccionar un prompt\"\n      },\n      \"copilotResumeLastChat\": {\n        \"label\": \"Retomar el último chat al abrir el Panel Lateral (Copilot)\"\n      },\n      \"turnOnChatWithWebsite\": {\n        \"label\": \"Habilitar Chat con Sitio Web por defecto (Copilot)\"\n      },\n      \"webUIResumeLastChat\": {\n        \"label\": \"Retomar el último chat al abrir la Interfaz Web\"\n      },\n      \"hideCurrentChatModelSettings\": {\n        \"label\": \"Ocultar Configuraciones del Modelo de Chat Actual\"\n      },\n      \"restoreLastChatModel\": {\n        \"label\": \"Restaurar el último modelo utilizado para chats anteriores\"\n      },\n      \"sendNotificationAfterIndexing\": {\n        \"label\": \"Enviar notificación después de terminar el procesamiento de la base de conocimientos\"\n      },\n      \"generateTitle\": {\n        \"label\": \"Generar título usando IA\"\n      },\n      \"ollamaStatus\": {\n        \"label\": \"Habilitar o deshabilitar la verificación del estado de conexión de Ollama\"\n      },\n      \"wideMode\": {\n        \"label\": \"Habilitar modo pantalla ancha\"\n      },\n      \"openReasoning\": {\n        \"label\": \"Abrir el Razonamiento Colapsado por defecto\"\n      },\n      \"defaultThinkingMode\": {\n        \"label\": \"Mostrar Estado del Modo de Pensamiento en Formularios\"\n      },\n      \"userChatBubble\": {\n        \"label\": \"Usar Burbuja de Chat para Mensajes del Usuario\"\n      },\n      \"autoCopyResponseToClipboard\": {\n        \"label\": \"Copiar automáticamente la respuesta al portapapeles\"\n      },\n      \"useMarkdownForUserMessage\": {\n        \"label\": \"Habilitar formato Markdown para mensajes del usuario\"\n      },\n      \"copyAsFormattedText\": {\n        \"label\": \"Copiar como Texto Formateado\"\n      },\n      \"tabMentionsEnabled\": {\n        \"label\": \"Habilitar Menciones de Pestañas (@tab)\"\n      },\n      \"pasteLargeTextAsFile\": {\n        \"label\": \"Pegar Texto Largo como Archivo\"\n      },\n      \"ocrLanguage\": {\n        \"label\": \"Idioma OCR predeterminado\",\n        \"placeholder\": \"Seleccionar un idioma OCR\"\n      },\n      \"sidepanelTemporaryChat\": {\n        \"label\": \"Habilitar Chat Temporal en el Panel Lateral por defecto\"\n      },\n      \"webuiTemporaryChat\": {\n        \"label\": \"Habilitar Chat Temporal en la Interfaz Web por defecto\"\n      },\n      \"removeReasoningTagFromCopy\": {\n        \"label\": \"Eliminar Etiqueta de Razonamiento del Texto Copiado\"\n      },\n      \"youtubeAutoSummarize\": {\n        \"label\": \"Mostrar el botón 'Resumir' en videos de YouTube\"\n      },\n      \"hideReasoningWidget\": {\n        \"label\": \"Ocultar Widget de Razonamiento de los Mensajes de IA\"\n      },\n      \"persistChatInput\": {\n        \"label\": \"Persistir Entrada de Chat (Guardar mensajes no enviados)\"\n      },\n      \"enableMessageQueue\": {\n        \"label\": \"Habilitar Cola de Mensajes durante la Transmisión\"\n      },\n      \"optimizeQueueForSmallScreen\": {\n        \"label\": \"Optimizar la Interfaz de Chat para Pantallas Pequeñas\"\n      },\n      \"tableTextWrap\": {\n        \"label\": \"Habilitar Ajuste de Texto en Tablas Markdown\"\n      },\n      \"showMoreForLargeMessage\": {\n        \"label\": \"Mostrar 'Ver más' para mensajes largos del usuario\"\n      },\n      \"sidebarPosition\": {\n        \"label\": \"Posición de la Barra Lateral\",\n        \"options\": {\n          \"left\": \"Izquierda\",\n          \"right\": \"Derecha\"\n        }\n      }\n    },\n    \"sidepanelRag\": {\n      \"heading\": \"Configuración de Recuperación\",\n      \"ragEnabled\": {\n        \"label\": \"Habilitar Incrustación y Recuperación\"\n      },\n      \"maxWebsiteContext\": {\n        \"label\": \"Tamaño Máximo de Contenido para Modo de Contexto Completo\",\n        \"placeholder\": \"Tamaño del contenido (predeterminado 4028)\"\n      }\n    },\n    \"webSearch\": {\n      \"heading\": \"Manejo de la busqueda Web\",\n      \"searchMode\": {\n        \"label\": \"Realizar busquedas Simples en Internet\"\n      },\n      \"provider\": {\n        \"label\": \"Motor de Busqueda\",\n        \"placeholder\": \"Selecione un motor de busqueda\"\n      },\n      \"totalSearchResults\": {\n        \"label\": \"Resultados totales de la busqueda\",\n        \"placeholder\": \"Ingresar el total de Resultados de la busqueda\"\n      },\n      \"visitSpecificWebsite\": {\n        \"label\": \"Visita el sitio web mencionado en el mensaje\"\n      },\n      \"searxng\": {\n        \"url\": {\n          \"label\": \"URL de SearXNG\"\n        }\n      },\n      \"braveApi\": {\n        \"label\": \"Clave API de Brave\",\n        \"placeholder\": \"Ingrese su clave API de Brave\"\n      },\n      \"tavilyApi\": {\n        \"label\": \"Clave API de Tavily\",\n        \"placeholder\": \"Ingrese su clave API de Tavily\"\n      },\n      \"exa\": {\n        \"label\": \"Clave API de Exa\",\n        \"placeholder\": \"Ingrese su clave API de Exa\"\n      },\n      \"googleDomain\": {\n        \"label\": \"Dominio de Google\"\n      },\n      \"searchOnByDefault\": {\n        \"label\": \"Búsqueda en Internet activada por defecto\"\n      },\n      \"firecrawlAPIKey\": {\n        \"label\": \"Clave API de Firecrawl\",\n        \"placeholder\": \"Ingrese su clave API de Firecrawl\"\n      },\n      \"domainFilter\": {\n        \"label\": \"Lista de Filtro de Dominio\",\n        \"description\": \"Solo mostrar resultados de estos dominios\",\n        \"placeholder\": \"ej., example.com\"\n      },\n      \"blockedDomains\": {\n        \"label\": \"Dominios Bloqueados\",\n        \"description\": \"Excluir resultados de estos dominios\",\n        \"placeholder\": \"ej., spam.com\"\n      }\n    },\n    \"system\": {\n      \"heading\": \"Configuraciones del Sistema\",\n      \"deleteChatHistory\": {\n        \"label\": \"Reinicio del Sistema\",\n        \"button\": \"Reiniciar Todo\",\n        \"confirm\": \"¿Está seguro de que desea realizar un reinicio del sistema? Esto borrará todos los datos y no se puede deshacer.\"\n      },\n      \"export\": {\n        \"label\": \"Exportar todos los datos (historial de chat, base de conocimiento, prompts y configuraciones)\",\n        \"button\": \"Exportar datos\",\n        \"success\": \"Exportación exitosa\"\n      },\n      \"import\": {\n        \"label\": \"Importar todos los datos (historial de chat, base de conocimiento, prompts y configuraciones)\",\n        \"button\": \"Importar datos\",\n        \"success\": \"Importación exitosa\",\n        \"error\": \"Error de importación\"\n      },\n      \"actionIcon\": {\n        \"label\": \"Establecer Acción Predeterminada para Clics en el Ícono de la Extensión\"\n      },\n      \"contextMenu\": {\n        \"label\": \"Establecer Acción Predeterminada para el Menú Contextual\"\n      },\n      \"fontSize\": {\n        \"label\": \"Tamaño de Fuente\"\n      },\n      \"webuiBtnSidePanel\": {\n        \"label\": \"Mostrar Botón de Interfaz Web en el Panel Lateral\"\n      },\n      \"chatBackgroundImage\": {\n        \"label\": \"Imagen de Fondo del Chat\"\n      },\n      \"storageSyncEnabled\": {\n        \"label\": \"Habilitar sincronización de almacenamiento del navegador (sincronizar configuraciones entre dispositivos)\"\n      }\n    },\n    \"tts\": {\n      \"heading\": \"Configuraciones de Text-to-speech\",\n      \"ttsEnabled\": {\n        \"label\": \"Habilitar Texto-a-Voz\"\n      },\n      \"ttsAutoPlay\": {\n        \"label\": \"Reproducir automáticamente la respuesta de voz después de completar\"\n      },\n      \"ttsProvider\": {\n        \"label\": \"Proveedor de Text-to-speech\",\n        \"placeholder\": \"Selecione un proveedor\"\n      },\n      \"ttsVoice\": {\n        \"label\": \"Voz de Text-to-speech\",\n        \"placeholder\": \"Selecione una voz\"\n      },\n      \"ssmlEnabled\": {\n        \"label\": \"Habilitar SSML (Speech Synthesis Markup Language)\"\n      },\n      \"responseSplitting\": {\n        \"label\": \"División de Respuesta\"\n      },\n      \"removeReasoningTagTTS\": {\n        \"label\": \"Eliminar Etiqueta de Razonamiento del TTS\"\n      }\n    },\n    \"stt\": {\n      \"heading\": \"Configuraciones de Voz-a-Texto\",\n      \"autoStopTimeout\": {\n        \"label\": \"Tiempo de Auto-Detención (ms)\",\n        \"placeholder\": \"Ingrese el tiempo de auto-detención en milisegundos\"\n      },\n      \"autoSubmitVoiceMessage\": {\n        \"label\": \"Auto-Enviar Mensaje de Voz\"\n      }\n    }\n  },\n  \"manageModels\": {\n    \"title\": \"Administar de Modelos\",\n    \"addBtn\": \"Agregar Nuevo Modelo\",\n    \"columns\": {\n      \"name\": \"Nombre\",\n      \"digest\": \"Resumen\",\n      \"nickname\": \"Apodo\",\n      \"modifiedAt\": \"Modificado\",\n      \"size\": \"Tamaño\",\n      \"actions\": \"Acciones\"\n    },\n    \"expandedColumns\": {\n      \"parentModel\": \"Modelo Padre\",\n      \"format\": \"Formato\",\n      \"family\": \"Familia\",\n      \"parameterSize\": \"Tamaño de Parametros\",\n      \"quantizationLevel\": \"Nível de Quantización\"\n    },\n    \"tooltip\": {\n      \"delete\": \"Borrar Modelo\",\n      \"repull\": \"Traer nuevamente el Modelo\",\n      \"editNickname\": \"Editar Apodo\"\n    },\n    \"confirm\": {\n      \"delete\": \"¿Esta seguro que desea borrar este modelos?\",\n      \"repull\": \"¿Esta seguro que desea traer nuevamente este modelo?\"\n    },\n    \"modal\": {\n      \"title\": \"Traer Nuevo Modelo\",\n      \"placeholder\": \"Ingresar el nombre del modelo\",\n      \"pull\": \"Traer Modelo\"\n    },\n    \"notification\": {\n      \"pullModel\": \"Trayendo Modelo\",\n      \"pullModelDescription\": \"Trayendo modelo {{modelName}}. Para más detalles, verifique el ícono de la extensión.\",\n      \"cancellingDownload\": \"Cancelando Descarga\",\n      \"cancellingDownloadDescription\": \"La descarga del modelo está siendo cancelada...\",\n      \"success\": \"Exito\",\n      \"error\": \"Error\",\n      \"successDescription\": \"Modelo traido exitosamente\",\n      \"successDeleteDescription\": \"Modelo borrado exitosamente\",\n      \"someError\": \"Hubo un error. Intente nuevamente más tarde\"\n    }\n  },\n  \"managePrompts\": {\n    \"title\": \"Administrar de Prompts\",\n    \"addBtn\": \"Agregar Nuevo Prompt\",\n    \"option1\": \"Normal\",\n    \"option2\": \"RAG\",\n    \"questionPrompt\": \"Prompt de Pregunta\",\n    \"columns\": {\n      \"title\": \"Título\",\n      \"prompt\": \"Prompt\",\n      \"type\": \"Tipo de Prompt\",\n      \"actions\": \"Acciones\"\n    },\n    \"segmented\": {\n      \"custom\": \"Invites personnalisées\",\n      \"copilot\": \"Invites Copilot\"\n    },\n    \"systemPrompt\": \"Prompt del Sistema\",\n    \"quickPrompt\": \"Prompt Rápido\",\n    \"tooltip\": {\n      \"delete\": \"Borrar Prompt\",\n      \"edit\": \"Editar Prompt\"\n    },\n    \"confirm\": {\n      \"delete\": \"¿Esta seguro que desea borrar este prompt? Esta acción no tiene vuelta a atrás.\"\n    },\n    \"modal\": {\n      \"addTitle\": \"Agregar Nuevo Prompt\",\n      \"editTitle\": \"Editar Prompt\"\n    },\n    \"form\": {\n      \"title\": {\n        \"label\": \"Título\",\n        \"placeholder\": \"Mi Prompt genial\",\n        \"required\": \"Por favor, ingrese un título\"\n      },\n      \"prompt\": {\n        \"label\": \"Prompt\",\n        \"placeholder\": \"Ingrese un prompt\",\n        \"required\": \"Por favor, ingrese un prompt\",\n        \"help\": \"Puede usar {key} como variable en su prompt.\",\n        \"missingTextPlaceholder\": \"Falta la variable {text} en el mensaje. Por favor, añádela.\"\n      },\n      \"isSystem\": {\n        \"label\": \"Es un Prompt del Sistema\"\n      },\n      \"btnSave\": {\n        \"saving\": \"Agregando un Prompt...\",\n        \"save\": \"Agregar Prompt\"\n      },\n      \"btnEdit\": {\n        \"saving\": \"Actualizando Prompt...\",\n        \"save\": \"Actualizar Prompt\"\n      }\n    },\n    \"notification\": {\n      \"addSuccess\": \"Prompt Agregado\",\n      \"addSuccessDesc\": \"Prompt agregado exitosamente\",\n      \"error\": \"Error\",\n      \"someError\": \"Hubo un error. Intente nuevamente más tarde\",\n      \"updatedSuccess\": \"Prompt Actualizado\",\n      \"updatedSuccessDesc\": \"Prompt actualizado exitosamente\",\n      \"deletedSuccess\": \"Prompt Borrado\",\n      \"deletedSuccessDesc\": \"Prompt borrado exitosamente\"\n    }\n  },\n  \"manageShare\": {\n    \"title\": \"Administrar los recursos compartidos\",\n    \"heading\": \"Configurar URL de Página Compartida\",\n    \"form\": {\n      \"url\": {\n        \"label\": \"URL de Página compartida\",\n        \"placeholder\": \"Ingresar URL de Página compartida\",\n        \"required\": \"Por favor, ingrese URL de Página compartida\",\n        \"help\": \"Por motivos de privacidad, podes hacer self-host de la página compartida y proveer una URL aqui. <anchor>Aprende más</anchor>.\"\n      }\n    },\n    \"webshare\": {\n      \"heading\": \"Compartir una Web\",\n      \"columns\": {\n        \"title\": \"Título\",\n        \"url\": \"URL\",\n        \"actions\": \"Acciones\"\n      },\n      \"tooltip\": {\n        \"delete\": \"Borrar lo compartido\"\n      },\n      \"confirm\": {\n        \"delete\": \"¿Esta seguro de desear borrar esta web compartida? Esta acción no tiene vuelta a atrás.\"\n      },\n      \"label\": \"Administrar páginas compartidas\",\n      \"description\": \"Habilitar o deshabilitar el recurso de páginas compartidas\"\n    },\n    \"notification\": {\n      \"pageShareSuccess\": \"URL compartida actualizada exitosamente\",\n      \"someError\": \"Hubo un error. Intente nuevamente más tarde\",\n      \"webShareDeleteSuccess\": \"Web compartida borrada exitosamente com sucesso\"\n    }\n  },\n  \"ollamaSettings\": {\n    \"title\": \"Configuraciones de Ollama\",\n    \"heading\": \"Configurar Ollama\",\n    \"settings\": {\n      \"ollamaUrl\": {\n        \"label\": \"URL de Ollama\",\n        \"placeholder\": \"Ingrese la URL de Ollama\"\n      },\n      \"globalEnable\": {\n        \"label\": \"Habilitar o Deshabilitar la Integración de Ollama Globalmente\",\n        \"warning\": \"Al deshabilitar la integración de Ollama globalmente, Page Assist no obtendrá modelos de Ollama. Aún puedes agregar una instancia de Ollama desde la sección de <anchor>API compatible con OpenAI</anchor> que funcionará correctamente.\"\n      },\n      \"advanced\": {\n        \"label\": \"Configuración avanzada de URL de Ollama\",\n        \"urlRewriteEnabled\": {\n          \"label\": \"Habilitar o Deshabilitar URL Personalizada\"\n        },\n        \"rewriteUrl\": {\n          \"label\": \"URL Personalizada\",\n          \"placeholder\": \"Ingresar URL Personalizada\"\n        },\n        \"autoCORSFix\": {\n          \"label\": \"Habilitar o Deshabilitar Corrección Automática de CORS de Ollama\"\n        },\n        \"headers\": {\n          \"label\": \"Encabezados Personalizados\",\n          \"add\": \"Agregar Encabezado\",\n          \"key\": {\n            \"label\": \"Clave del Encabezado\",\n            \"placeholder\": \"Autorización\"\n          },\n          \"value\": {\n            \"label\": \"Valor del Encabezado\",\n            \"placeholder\": \"Token Bearer\"\n          }\n        },\n        \"help\": \"Si tenes problemas de conexión con Ollama en Page Assist, podes configurar una URL de personalizada. Para saber más sobre la configuración, <anchor>click aqui</anchor>.\"\n      }\n    }\n  },\n  \"manageSearch\": {\n    \"title\": \"Administrar Busqueda Web\",\n    \"heading\": \"Configurar Busqueda Web\"\n  },\n  \"about\": {\n    \"title\": \"Sobre\",\n    \"heading\": \"Sobre\",\n    \"chromeVersion\": \"Versión de Page Assist\",\n    \"ollamaVersion\": \"Versión de Ollama\",\n    \"support\": \"Podes apoyar el proyecto Page Assist haciendo donaciones o patrocinarnos a través de las seguientes plataformas:\",\n    \"koFi\": \"Apoyar en Ko-fi\",\n    \"githubSponsor\": \"Patrocinarnos en GitHub\",\n    \"githubRepo\": \"Repositorio de GitHub\"\n  },\n  \"manageKnowledge\": {\n    \"title\": \"Administrar Conocimiento\",\n    \"heading\": \"Configurar Bases de Conocimiento\"\n  },\n  \"rag\": {\n    \"title\": \"Configuraciones de Pipeline\",\n    \"ragSettings\": {\n      \"label\": \"Configuraciones de RAG\",\n      \"model\": {\n        \"label\": \"Modelo de embeddings\",\n        \"required\": \"Por favor, selecione un modelo\",\n        \"help\": \"Es recomendable usar modelos de embeddings como `nomic-embed-text`.\",\n        \"placeholder\": \"Selecione un modelo\"\n      },\n      \"chunkSize\": {\n        \"label\": \"Tamaño del Chunk\",\n        \"placeholder\": \"Ingresar el tamaño del chunk\",\n        \"required\": \"Por favor, ingrese el tamaño del chunk\"\n      },\n      \"chunkOverlap\": {\n        \"label\": \"Solapamiento del Chunk\",\n        \"placeholder\": \"Ingrese el solapamiento del chunk\",\n        \"required\": \"Por favor, ingresar el solapamiento del chunk\"\n      },\n      \"totalFilePerKB\": {\n        \"label\": \"Límite predeterminado de carga de archivos para la Base de Conocimientos\",\n        \"placeholder\": \"Ingrese el límite predeterminado de carga de archivos (ej., 10)\",\n        \"required\": \"Por favor, ingrese el límite predeterminado de carga de archivos\"\n      },\n      \"noOfRetrievedDocs\": {\n        \"label\": \"Número de Documentos Recuperados\",\n        \"placeholder\": \"Ingrese el Número de Documentos Recuperados\",\n        \"required\": \"Por favor, ingrese el número de documentos recuperados\"\n      },\n      \"splittingSeparator\": {\n        \"label\": \"Separador\",\n        \"placeholder\": \"Ingrese el separador (ej., \\\\n\\\\n)\",\n        \"required\": \"Por favor, ingrese un separador\"\n      },\n      \"splittingStrategy\": {\n        \"label\": \"Divisor de Texto\"\n      }\n    },\n    \"prompt\": {\n      \"label\": \"Configurar el Prompt del RAG\",\n      \"option1\": \"Normal\",\n      \"option2\": \"Web\",\n      \"alert\": \"Es obsoleto configurar aquí el prompt del sistema. Por favor, use la sección de Administrar Prompts para agregar o editar prompts. Esta sección se quitará en una versión futura\",\n      \"systemPrompt\": \"Prompt del Sistema\",\n      \"systemPromptPlaceholder\": \"Ingresar el prompt del sistema\",\n      \"webSearchPrompt\": \"Prompt de la busqueda Web\",\n      \"webSearchPromptHelp\": \"No borre `{search_results}` del prompt.\",\n      \"webSearchPromptError\": \"Por favor, ingresar un prompt de busqueda web\",\n      \"webSearchPromptPlaceholder\": \"Ingrese un prompt de busqueda web\",\n      \"webSearchFollowUpPrompt\": \"Prompt de Seguimiento de busqueda Web\",\n      \"webSearchFollowUpPromptHelp\": \"No borre `{chat_history}` y `{question}` del prompt.\",\n      \"webSearchFollowUpPromptError\": \"Por favor, ingrese el prompt de seguimiento de la busqueda web\",\n      \"webSearchFollowUpPromptPlaceholder\": \"Su prompt de seguimiento de busqueda web\"\n    }\n  },\n  \"chromeAiSettings\": {\n    \"title\": \"Configuración de IA de Chrome\"\n  },\n  \"mermaid\": \"Mermaid\"\n}\n"
  },
  {
    "path": "src/assets/locale/es/sidepanel.json",
    "content": "{\n    \"tooltip\": {\n        \"embed\": \"Puede demorar algunos minutos para incluir la página. Por favor, aguarde...\",\n        \"clear\": \"Borrar el histórico de conversación\",\n        \"history\": \"Histórico de la conversación\",\n        \"openwebui\": \"Abrir WebUI\"\n    }\n}"
  },
  {
    "path": "src/assets/locale/fa/chrome.json",
    "content": "{\n    \"heading\": \"تنظیمات AI کروم\",\n    \"status\": {\n        \"label\": \"فعال یا غیر فعال کردن پشتیبانی از AI کروم\"\n    },\n    \"error\": {\n        \"browser_not_supported\": \"این نسخه از کروم توسط مدل Gemini Nano پشتیبانی نمی شود. لطفا به نسخه ۱۲۷ یا بالاتر به روزرسانی کنید.\",\n        \"ai_not_supported\": \"تنظیم chrome://flags/#prompt-api-for-gemini-nano فعال نشده است. لطفا فعالش کنید..\",\n        \"ai_not_ready\": \"Gemini Nano هنوز آماده نیست; تنظیمات کروم را مجددا بررسی کنید.\",\n        \"internal_error\": \"یک خطای داخلی رخ داد. لطفا بعدا مجددا تلاش نمایید.\"\n    },\n    \"errorDescription\": \"برای استفاده از AI کروم، به نسخه ۱۳۸ یا بالاتر از کروم نیاز دارید. مراحل زیر را دنبال کنید:\\n\\n۱. به `chrome://flags/#prompt-api-for-gemini-nano` بروید و \\\"Prompt API for Gemini Nano\\\" را فعال کنید.\\n۲. کروم را برای اعمال تغییرات مجددا راه اندازی کنید.\\n۳. به این صفحه بازگردید و روی \\\"دانلود مدل\\\" کلیک کنید — این کار برای اولین بار یک مدل ۴ گیگابایتی دانلود خواهد کرد.\\n۴. پس از دانلود، Gemini Nano می تواند از طریق Page Assist فعال شود.\",\n    \"downloadModel\": \"دانلود مدل\",\n    \"modelDownloadWarning\": \"این کار یک مدل با اندازه دانلود تقریبی بین ۱.۵ گیگابایت و ۲.۴ گیگابایت دانلود خواهد کرد. اطمینان حاصل کنید که فضای دیسک کافی دارید.\"\n}"
  },
  {
    "path": "src/assets/locale/fa/common.json",
    "content": "{\n    \"pageAssist\": \"دستیار صفحه\",\n    \"selectAModel\": \"یک مدل انتخاب کنید\",\n    \"save\": \"ذخیره\",\n    \"saved\": \"ذخیره شد\",\n    \"cancel\": \"لغو\",\n    \"retry\": \"تلاش مجدد\",\n    \"share\": {\n        \"tooltip\": {\n            \"share\": \"اشتراک گذاری\"\n        },\n        \"modal\": {\n            \"title\": \"اشتراک گذاری لینک به گپ\"\n        },\n        \"form\": {\n            \"defaultValue\": {\n                \"name\": \"ناشناس\",\n                \"title\": \"گپ بی نام\"\n            },\n            \"title\": {\n                \"label\": \"عنوان گپ\",\n                \"placeholder\": \"عنوان گپ را وارد کنید\",\n                \"required\": \"وارد کردن عنوان چت الزامی است\"\n            },\n            \"name\": {\n                \"label\": \"نام شما\",\n                \"placeholder\": \"نام خود را وارد کنید\",\n                \"required\": \"وارد کردن نام الزامی است\"\n            },\n            \"btn\": {\n                \"save\": \"ایجاد لینک\",\n                \"saving\": \"در حال ایجاد لینک...\"\n            }\n        },\n        \"notification\": {\n            \"successGenerate\": \"پیوند در کلیپ بورد کپی شد\",\n            \"failGenerate\": \"پیوند ایجاد نشد\"\n        }\n    },\n    \"copyToClipboard\": \"کپی به کلیپ بورد\",\n    \"webSearch\": \"جستجوی وب\",\n    \"regenerate\": \"ایجاد مجدد\",\n    \"continue\": \"ادامه پاسخ\",\n    \"edit\": \"ویرایش\",\n    \"delete\": \"حذف\",\n    \"saveAndSubmit\": \"ذخیره و ارسال\",\n    \"editMessage\": {\n        \"placeholder\": \"یک پیام وارد کنید...\"\n    },\n    \"submit\": \"ارسال\",\n    \"noData\": \"اطلاعاتی وجود ندارد\",\n    \"noHistory\": \"تاریخچه گپ وجود ندارد\",\n    \"chatWithCurrentPage\": \"گپ زدن با این صفحه\",\n    \"beta\": \"بتا\",\n    \"tts\": \"بلند بخوان\",\n    \"currentChatModelSettings\": \"تنظیمات مدل گپ فعلی\",\n    \"modelSettings\": {\n        \"label\": \"تنظیمات مدل\",\n        \"description\": \"گزینه های مدل را به صورت کلی برای همه گپ ها تنظیم کنید\",\n        \"form\": {\n            \"keepAlive\": {\n                \"label\": \"Keep Alive\",\n                \"help\": \"کنترل می کند که چه مدت مدل پس از درخواست در حافظه باقی می ماند (پیش فرض: 5 دقیقه)\",\n                \"placeholder\": \"مدت زمان Keep Alive را وارد کنید (مثلا 5m, 10m, 1h)\"\n            },\n            \"temperature\": {\n                \"label\": \"Temperature\",\n                \"placeholder\": \"مقدار Temperature را وارد کنید (مثلا 0.7, 1.0)\"\n            },\n            \"numCtx\": {\n                \"label\": \"اندازه پنجره متن (num_ctx)\",\n                \"placeholder\": \"مقدار اندازه پنجره متن را وارد کنید (پیش فرض: 2048)\"\n            },\n            \"numPredict\": {\n                \"label\": \"حداکثر توکن‌ها (num_predict)\",\n                \"placeholder\": \"مقدار حداکثر توکن‌ها را وارد کنید (مثلا 2048، 4096)\"\n            },\n            \"seed\": {\n                \"label\": \"Seed\",\n                \"placeholder\": \"مقدار Seed را وارد کنید (e.g. 1234)\",\n                \"help\": \"تکرارپذیری خروجی مدل\"\n            },\n            \"topK\": {\n                \"label\": \"Top K\",\n                \"placeholder\": \"مقدار Top K را وارد کنید (مثلا 40, 100)\"\n            },\n            \"topP\": {\n                \"label\": \"Top P\",\n                \"placeholder\": \"مقدار Top P را وارد کنید (مثلا 0.9, 0.95)\"\n            },\n            \"numGpu\": {\n                \"label\": \"Num GPU\",\n                \"placeholder\": \"تعداد لایه‌هایی که به GPU(ها) ارسال می‌شود را وارد کنید\"\n            },\n            \"systemPrompt\": {\n                \"label\": \"پرامپت سیستم موقت\",\n                \"placeholder\": \"پرامپت سیستم را وارد کنید\",\n                \"help\": \"این یک روش سریع برای تنظیم پرامپت سیستم در گفتگوی فعلی است که در صورت وجود، پرامپت سیستم انتخاب شده را لغو خواهد کرد.\"\n            }\n        },\n        \"advanced\": \"تنظیمات بیشتر مدل\"\n    },\n    \"citations\": \"منابع\",\n    \"downloadCode\": \"دانلود کد\",\n    \"date\": {\n        \"pinned\": \"پین شده\",\n        \"today\": \"امروز\",\n        \"yesterday\": \"دیروز\",\n        \"last7Days\": \"۷ روز گذشته\",\n        \"older\": \"قدیمی‌تر\"\n    },\n    \"range\": {\n        \"deleteConfirm\": {\n            \"pinned\": \"آیا مطمئن هستید که می‌خواهید تمام پیام‌های پین شده را حذف کنید؟\",\n            \"today\": \"آیا مطمئن هستید که می‌خواهید تمام پیام‌های امروز را حذف کنید؟\",\n            \"yesterday\": \"آیا مطمئن هستید که می‌خواهید تمام پیام‌های دیروز را حذف کنید؟\",\n            \"last7Days\": \"آیا مطمئن هستید که می‌خواهید تمام پیام‌های ۷ روز گذشته را حذف کنید؟\",\n            \"older\": \"آیا مطمئن هستید که می‌خواهید تمام پیام‌های قدیمی‌تر را حذف کنید؟\"\n        },\n        \"tooltip\": {\n            \"pinned\": \"حذف تمام پیام‌های پین شده\",\n            \"today\": \"حذف تمام پیام‌های امروز\",\n            \"yesterday\": \"حذف تمام پیام‌های دیروز\",\n            \"last7Days\": \"حذف تمام پیام‌های ۷ روز گذشته\",\n            \"older\": \"حذف تمام پیام‌های قدیمی‌تر\"\n        }\n    },\n    \"pin\": \"پین کردن\",\n    \"unpin\": \"حذف پین\",\n    \"generationInfo\": \"اطلاعات تولید\",\n    \"sidebarChat\": \"چت کناری\",\n    \"reasoning\": {\n        \"thinking\": \"در حال فکر کردن....\",\n        \"thought\": \"فکر کردن برای {{time}}\"\n    },\n    \"embeddingGen\": \"در حال ایجاد تمثیلات، این ممکن است چند لحظه طول بکشد\",\n    \"semanticSearch\": \"در حال جستجوی معنایی\",\n    \"downloading\": \"در حال دانلود\",\n    \"cancelPullingModel\": {\n        \"confirm\": \"آیا مطمئن هستید که می‌خواهید دانلود را لغو کنید؟ این کار فرآیند دانلود را متوقف می‌کند. طبق مستندات Ollama، می‌توانید دانلود را از همان جایی که متوقف شده ادامه دهید.\"\n    }\n}"
  },
  {
    "path": "src/assets/locale/fa/knowledge.json",
    "content": "{\n    \"addBtn\": \"افزودن دانش جدید\",\n    \"columns\": {\n        \"title\": \"عنوان\",\n        \"status\": \"وضعیت\",\n        \"embeddings\": \"مدل جاسازی\",\n        \"createdAt\": \"ایجاد شده در\",\n        \"action\": \"اقدامات\"\n    },\n    \"expandedColumns\": {\n        \"name\": \"نام\"\n    },\n    \"confirm\": {\n        \"delete\": \"آیا مطمئن هستید که می خواهید این دانش را حذف کنید؟\"\n    },\n    \"deleteSuccess\": \"دانش با موفقیت حذف شد\",\n    \"status\": {\n        \"pending\": \"انتظار\",\n        \"finished\": \"تمام\",\n        \"processing\": \"در حال پردازش\",\n        \"failed\": \"ناموفق\"\n    },\n    \"addKnowledge\": \"دانش را اضافه کنید\",\n    \"updateKnowledge\": \"به روز رسانی دانش\",\n    \"form\": {\n        \"title\": {\n            \"label\": \"عنوان دانش\",\n            \"placeholder\": \"عنوان دانش را وارد کنید\",\n            \"required\": \"وارد کردن عنوان دانش الزامی است\"\n        },\n        \"uploadFile\": {\n            \"label\": \"آپلود فایل\",\n            \"uploadText\": \"یک فایل را در اینجا بکشید و رها کنید یا برای آپلود کلیک کنید\",\n            \"uploadHint\": \"انواع فایل های پشتیبانی شده: .pdf, .csv, .txt, .md, .docx\",\n            \"required\": \"وارد کردن فایل مورد نیاز است\"\n        },\n        \"submit\": \"ارسال\",\n        \"success\": \"دانش با موفقیت اضافه شد\"\n    },\n    \"noEmbeddingModel\": \"لطفا ابتدا یک مدل جاسازی را از صفحه تنظیمات RAG اضافه کنید\"\n}"
  },
  {
    "path": "src/assets/locale/fa/openai.json",
    "content": "{\n    \"settings\": \"API سازگار با OpenAI\",\n    \"heading\": \"API سازگار با OpenAI\",\n    \"subheading\": \"ارائه‌دهندگان API سازگار با OpenAI خود را در اینجا مدیریت و پیکربندی کنید.\",\n    \"addBtn\": \"افزودن ارائه‌دهنده\",\n    \"table\": {\n        \"name\": \"نام ارائه‌دهنده\",\n        \"baseUrl\": \"آدرس پایه\",\n        \"actions\": \"عملیات\"\n    },\n    \"modal\": {\n        \"titleAdd\": \"افزودن ارائه‌دهنده جدید\",\n        \"name\": {\n            \"label\": \"نام ارائه‌دهنده\",\n            \"required\": \"نام ارائه‌دهنده الزامی است.\",\n            \"placeholder\": \"نام ارائه‌دهنده را وارد کنید\"\n        },\n        \"baseUrl\": {\n            \"label\": \"آدرس پایه\",\n            \"help\": \"آدرس پایه ارائه‌دهنده API OpenAI. مثال (http://localhost:1234/v1)\",\n            \"required\": \"آدرس پایه الزامی است.\",\n            \"placeholder\": \"آدرس پایه را وارد کنید\"\n        },\n        \"apiKey\": {\n            \"label\": \"کلید API\",\n            \"required\": \"کلید API الزامی است.\",\n            \"placeholder\": \"کلید API را وارد کنید\"\n        },\n        \"submit\": \"ذخیره\",\n        \"update\": \"به‌روزرسانی\",\n        \"deleteConfirm\": \"آیا مطمئن هستید که می‌خواهید این ارائه‌دهنده را حذف کنید؟\",\n        \"model\": {\n            \"title\": \"لیست مدل‌ها\",\n            \"subheading\": \"لطفاً مدل‌های گفتگویی که می‌خواهید با این ارائه‌دهنده استفاده کنید را انتخاب کنید.\",\n            \"success\": \"مدل‌های جدید با موفقیت اضافه شدند.\"\n        },\n        \"tipLMStudio\": \"Page Assist به طور خودکار مدل‌هایی را که در LM Studio بارگذاری کرده‌اید، دریافت می‌کند. نیازی به افزودن دستی آنها نیست.\"\n    },\n    \"addSuccess\": \"ارائه‌دهنده با موفقیت اضافه شد.\",\n    \"deleteSuccess\": \"ارائه‌دهنده با موفقیت حذف شد.\",\n    \"updateSuccess\": \"ارائه‌دهنده با موفقیت به‌روزرسانی شد.\",\n    \"delete\": \"حذف\",\n    \"edit\": \"ویرایش\",\n    \"newModel\": \"افزودن مدل‌ها به ارائه‌دهنده\",\n    \"noNewModel\": \"برای LMStudio, Ollama, Llamafile, ما به صورت پویا دریافت می‌کنیم. نیازی به افزودن دستی نیست.\",\n    \"searchModel\": \"جستجوی مدل\",\n    \"selectAll\": \"انتخاب همه\",\n    \"save\": \"ذخیره\",\n    \"saving\": \"در حال ذخیره...\",\n    \"manageModels\": {\n        \"columns\": {\n            \"name\": \"نام مدل\",\n            \"model_type\": \"نوع مدل\",\n            \"model_id\": \"شناسه مدل\",\n            \"provider\": \"نام ارائه‌دهنده\",\n            \"actions\": \"عملیات\",\n            \"nickname\": \"نام مستعار مدل\"\n        },\n        \"tooltip\": {\n            \"delete\": \"حذف\"\n        },\n        \"confirm\": {\n            \"delete\": \"آیا مطمئن هستید که می‌خواهید این مدل را حذف کنید؟\"\n        },\n        \"modal\": {\n            \"title\": \"افزودن مدل سفارشی\",\n            \"form\": {\n                \"name\": {\n                    \"label\": \"شناسه مدل\",\n                    \"placeholder\": \"llama3.2\",\n                    \"required\": \"شناسه مدل الزامی است.\"\n                },\n                \"provider\": {\n                    \"label\": \"ارائه‌دهنده\",\n                    \"placeholder\": \"ارائه‌دهنده را انتخاب کنید\",\n                    \"required\": \"ارائه‌دهنده الزامی است.\"\n                },\n                \"type\": {\n                    \"label\": \"نوع مدل\"\n                }\n            }\n        }\n    },\n    \"noModelFound\": \"هیچ مدلی یافت نشد. اطمینان حاصل کنید که ارائه‌دهنده صحیح را با آدرس پایه و کلید API اضافه کرده‌اید.\",\n    \"radio\": {\n        \"chat\": \"مدل گفتگو\",\n        \"embedding\": \"مدل تعبیه\",\n        \"chatInfo\": \"برای تکمیل گفتگو و تولید مکالمه استفاده می‌شود\",\n        \"embeddingInfo\": \"برای RAG و سایر وظایف مرتبط با جستجوی معنایی استفاده می‌شود.\"\n    },\n    \"nicknameModal\": {\n        \"title\": \"افزودن / ویرایش نام مستعار مدل\",\n        \"form\": {\n            \"modelName\": {\n                \"label\": \"نام مدل\",\n                \"placeholder\": \"نام مدل را وارد کنید\",\n                \"required\": \"نام مدل الزامی است.\"\n            },\n            \"modelAvatar\": {\n                \"label\": \"آواتار مدل\",\n                \"placeholder\": \"آواتار مدل را وارد کنید\",\n                \"help\": \"لطفاً آدرس URL آواتار مدل را وارد کنید. این تصویر در پنجره گفتگو نمایش داده خواهد شد.\"\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/assets/locale/fa/option.json",
    "content": "{\n    \"newChat\": \"گپ جدید\",\n    \"selectAPrompt\": \"یک پرامپت را انتخاب کنید\",\n    \"githubRepository\": \"مخزن GitHub\",\n    \"settings\": \"تنظیمات\",\n    \"sidebarTitle\": \"تاریخچه گپ\",\n    \"error\": \"خطا\",\n    \"somethingWentWrong\": \"مشکلی پیش آمد\",\n    \"validationSelectModel\": \"لطفا یک مدل را برای ادامه انتخاب کنید\",\n    \"deleteHistoryConfirmation\": \"آیا مطمئن هستید که می خواهید این تاریخچه را حذف کنید؟\",\n    \"editHistoryTitle\": \"یک عنوان جدید وارد کنید\",\n    \"temporaryChat\": \"گپ موقت\",\n    \"more\": {\n        \"copy\": {\n            \"group\": \"کپی\",\n            \"asText\": \"کپی به صورت متن\",\n            \"asMarkdown\": \"کپی به صورت مارک‌داون\",\n            \"success\": \"در کلیپ‌بورد کپی شد!\"\n        },\n        \"download\": {\n            \"group\": \"دانلود\",\n            \"text\": \"فایل متنی (.txt)\",\n            \"markdown\": \"مارک‌داون (.md)\",\n            \"json\": \"فایل JSON (.json)\"\n        },\n        \"share\": \"اشتراک‌گذاری\"\n    }\n}"
  },
  {
    "path": "src/assets/locale/fa/playground.json",
    "content": "{\n    \"ollamaState\": {\n        \"searching\": \"در حال جستجوی Ollama شما 🦙\",\n        \"running\": \"Ollama در حال اجرا است 🦙\",\n        \"notRunning\": \"امکان اتصال به Ollama وجود ندارد 🦙\",\n        \"connectionError\": \"به نظر می رسد که خطای اتصال دارید. لطفا برای عیب یابی به این <anchor>مستندات</anchor> مراجعه کنید.\"\n    },\n    \"formError\": {\n        \"noModel\": \"لطفا یک مدل را انتخاب کنید\",\n        \"noEmbeddingModel\": \"لطفا یک مدل جاسازی در صفحه تنظیمات > RAG تنظیم کنید\"\n    },\n    \"form\": {\n        \"textarea\": {\n            \"placeholder\": \"یک پیام تایپ کنید...\"\n        },\n        \"webSearch\": {\n            \"on\": \"روشن\",\n            \"off\": \"خاموش\"\n        }\n    },\n    \"tooltip\": {\n        \"searchInternet\": \"جستجوی اینترنت\",\n        \"speechToText\": \"گفتار به متن\",\n        \"uploadImage\": \"آپلود تصویر\",\n        \"stopStreaming\": \"توقف Streaming\",\n        \"knowledge\": \"دانش\",\n        \"clearContext\": \"پاک کردن متن\"\n    },\n    \"sendWhenEnter\": \"با فشار دادن Enter ارسال شود\",\n    \"welcome\": \"سلام! امروز چطور می‌توانم به شما کمک کنم؟\",\n    \"useOCR\": \"استخراج متن از تصویر (OCR)\"\n}"
  },
  {
    "path": "src/assets/locale/fa/settings.json",
    "content": "{\n  \"generalSettings\": {\n    \"title\": \"تنظیمات عمومی\",\n    \"settings\": {\n      \"heading\": \"تنظیمات رابط کاربری وب\",\n      \"speechRecognitionLang\": {\n        \"label\": \"زبان تشخیص گفتار\",\n        \"placeholder\": \"یک زبان را انتخاب کنید\"\n      },\n      \"language\": {\n        \"label\": \"زبان\",\n        \"placeholder\": \"یک زبان را انتخاب کنید\"\n      },\n      \"darkMode\": {\n        \"label\": \"تغییر تم\",\n        \"options\": {\n          \"light\": \"روشن\",\n          \"dark\": \"تیره\"\n        }\n      },\n      \"defaultCopilotPrompt\": {\n        \"label\": \"پرامپت پیش‌فرض برای پنل جانبی (کوپایلوت)\",\n        \"placeholder\": \"یک پرامپت را انتخاب کنید\"\n      },\n      \"defaultWebUIPrompt\": {\n        \"label\": \"پرامپت پیش‌فرض برای رابط کاربری وب\",\n        \"placeholder\": \"یک پرامپت را انتخاب کنید\"\n      },\n      \"copilotResumeLastChat\": {\n        \"label\": \"آخرین گفتگو را هنگام باز کردن SidePanel (Copilot) از سر بگیر\"\n      },\n      \"turnOnChatWithWebsite\": {\n        \"label\": \"فعال کردن گپ با وب سایت به صورت پیش‌فرض (کوپایلوت)\"\n      },\n      \"webUIResumeLastChat\": {\n        \"label\": \"آخرین گفتگو را هنگام باز کردن رابط کاربری وب از سر بگیر\"\n      },\n      \"hideCurrentChatModelSettings\": {\n        \"label\": \"مخفی کردن تنظیمات مدل گپ فعلی را\"\n      },\n      \"restoreLastChatModel\": {\n        \"label\": \"بازیابی آخرین مدل استفاده شده برای گپ‌های قبلی\"\n      },\n      \"sendNotificationAfterIndexing\": {\n        \"label\": \"ارسال نوتیفیکیشن پس از اتمام پردازش پایگاه دانش\"\n      },\n      \"generateTitle\": {\n        \"label\": \"تولید عنوان با استفاده از هوش مصنوعی\"\n      },\n      \"ollamaStatus\": {\n        \"label\": \"فعال یا غیرفعال کردن بررسی وضعیت اتصال Ollama\"\n      },\n      \"wideMode\": {\n        \"label\": \"فعال کردن حالت صفحه عریض\"\n      },\n      \"openReasoning\": {\n        \"label\": \"باز کردن استدلال به صورت پیش فرض\"\n      },\n      \"defaultThinkingMode\": {\n        \"label\": \"نمایش وضعیت حالت تفکر در فرم‌ها\"\n      },\n      \"userChatBubble\": {\n        \"label\": \"استفاده از حباب گفتگو برای پیام‌های کاربر\"\n      },\n      \"autoCopyResponseToClipboard\": {\n        \"label\": \"کپی خودکار پاسخ در کلیپ بورد\"\n      },\n      \"useMarkdownForUserMessage\": {\n        \"label\": \"فعال کردن قالب‌بندی مارک‌داون برای پیام‌های کاربر\"\n      },\n      \"copyAsFormattedText\": {\n        \"label\": \"کپی به عنوان متن قالب‌بندی شده\"\n      },\n      \"tabMentionsEnabled\": {\n        \"label\": \"فعال‌سازی منشن تب (@tab)\"\n      },\n      \"pasteLargeTextAsFile\": {\n        \"label\": \"الصاق متن طولانی به عنوان فایل\"\n      },\n      \"ocrLanguage\": {\n        \"label\": \"زبان پیش‌فرض OCR\",\n        \"placeholder\": \"یک زبان OCR انتخاب کنید\"\n      },\n      \"sidepanelTemporaryChat\": {\n        \"label\": \"فعال کردن چت موقت در پنل جانبی به صورت پیش‌فرض\"\n      },\n      \"webuiTemporaryChat\": {\n        \"label\": \"فعال کردن چت موقت در رابط کاربری وب به صورت پیش‌فرض\"\n      },\n      \"removeReasoningTagFromCopy\": {\n        \"label\": \"حذف برچسب استدلال از متن کپی شده\"\n      },\n      \"youtubeAutoSummarize\": {\n        \"label\": \"نمایش دکمه 'خلاصه‌سازی' در ویدیوهای یوتیوب\"\n      },\n      \"hideReasoningWidget\": {\n        \"label\": \"مخفی کردن ابزارک استدلال از پیام‌های هوش مصنوعی\"\n      },\n      \"persistChatInput\": {\n        \"label\": \"ذخیره ورودی چت (ذخیره پیام‌های ارسال نشده)\"\n      },\n      \"enableMessageQueue\": {\n        \"label\": \"فعال کردن صف پیام در حین استریم\"\n      },\n      \"optimizeQueueForSmallScreen\": {\n        \"label\": \"بهینه‌سازی رابط کاربری چت برای صفحات کوچک\"\n      },\n      \"tableTextWrap\": {\n        \"label\": \"فعال کردن شکستن متن در جداول مارک‌داون\"\n      },\n      \"showMoreForLargeMessage\": {\n        \"label\": \"نمایش 'بیشتر نشان بده' برای پیام‌های طولانی کاربر\"\n      },\n      \"sidebarPosition\": {\n        \"label\": \"موقعیت نوار کناری\",\n        \"options\": {\n          \"left\": \"چپ\",\n          \"right\": \"راست\"\n        }\n      }\n    },\n    \"sidepanelRag\": {\n      \"heading\": \"تنظیمات بازیابی\",\n      \"ragEnabled\": {\n        \"label\": \"فعال‌سازی جاسازی و بازیابی\"\n      },\n      \"maxWebsiteContext\": {\n        \"label\": \"حداکثر اندازه محتوا برای حالت متن کامل\",\n        \"placeholder\": \"اندازه محتوا (پیش‌فرض ۴۰۲۸)\"\n      }\n    },\n    \"webSearch\": {\n      \"heading\": \"مدیریت جستجوی وب\",\n      \"searchMode\": {\n        \"label\": \"انجام جستجوی ساده اینترنتی\"\n      },\n      \"provider\": {\n        \"label\": \"موتور جستجو\",\n        \"placeholder\": \"یک موتور جستجو را انتخاب کنید\"\n      },\n      \"totalSearchResults\": {\n        \"label\": \"مجموع نتایج جستجو\",\n        \"placeholder\": \"کل نتایج جستجو را وارد کنید\"\n      },\n      \"visitSpecificWebsite\": {\n        \"label\": \"مراجعه به وب سایت ذکر شده در پیام\"\n      },\n      \"searxng\": {\n        \"url\": {\n          \"label\": \"آدرس SearXNG\"\n        }\n      },\n      \"braveApi\": {\n        \"label\": \"کلید API بریو\",\n        \"placeholder\": \"کلید API بریو خود را وارد کنید\"\n      },\n      \"tavilyApi\": {\n        \"label\": \"کلید API تاویلی\",\n        \"placeholder\": \"کلید API تاویلی خود را وارد کنید\"\n      },\n      \"exa\": {\n        \"label\": \"کلید API اکسا\",\n        \"placeholder\": \"کلید API اکسا خود را وارد کنید\"\n      },\n      \"googleDomain\": {\n        \"label\": \"دامنه گوگل\"\n      },\n      \"searchOnByDefault\": {\n        \"label\": \"جستجوی اینترنتی به صورت پیش‌فرض فعال باشد\"\n      },\n      \"firecrawlAPIKey\": {\n        \"label\": \"کلید API فایرکرال\",\n        \"placeholder\": \"کلید API فایرکرال خود را وارد کنید\"\n      },\n      \"domainFilter\": {\n        \"label\": \"لیست فیلتر دامنه\",\n        \"description\": \"فقط نتایج از این دامنه‌ها نشان داده شوند\",\n        \"placeholder\": \"مثال: example.com\"\n      },\n      \"blockedDomains\": {\n        \"label\": \"دامنه‌های مسدود شده\",\n        \"description\": \"نتایج از این دامنه‌ها را حذف کنید\",\n        \"placeholder\": \"مثال: spam.com\"\n      }\n    },\n    \"system\": {\n      \"heading\": \"تنظیمات سیستم\",\n      \"deleteChatHistory\": {\n        \"label\": \"بازنشانی سیستم\",\n        \"button\": \"بازنشانی همه\",\n        \"confirm\": \"آیا مطمئن هستید که می‌خواهید بازنشانی سیستم را انجام دهید؟ این کار تمام داده‌ها را پاک می‌کند و غیرقابل برگشت است.\"\n      },\n      \"export\": {\n        \"label\": \"صدور تمام داده‌ها (تاریخچه چت، پایگاه دانش، پرامپت‌ها و تنظیمات)\",\n        \"button\": \"صدور داده‌ها\",\n        \"success\": \"صدور با موفقیت انجام شد\"\n      },\n      \"import\": {\n        \"label\": \"وارد کردن تمام داده‌ها (تاریخچه چت، پایگاه دانش، پرامپت‌ها و تنظیمات)\",\n        \"button\": \"وارد کردن داده‌ها\",\n        \"success\": \"واردسازی با موفقیت انجام شد\",\n        \"error\": \"خطا در واردسازی\"\n      },\n      \"actionIcon\": {\n        \"label\": \"تنظیم عملکرد پیش‌فرض برای کلیک روی آیکون افزونه\"\n      },\n      \"contextMenu\": {\n        \"label\": \"تنظیم عملکرد پیش‌فرض برای منوی زمینه\"\n      },\n      \"fontSize\": {\n        \"label\": \"اندازه فونت\"\n      },\n      \"webuiBtnSidePanel\": {\n        \"label\": \"نمایش دکمه رابط کاربری وب در پنل جانبی\"\n      },\n      \"chatBackgroundImage\": {\n        \"label\": \"تصویر پس‌زمینه چت\"\n      },\n      \"storageSyncEnabled\": {\n        \"label\": \"فعال کردن همگام‌سازی ذخیره‌سازی مرورگر (همگام‌سازی تنظیمات در بین دستگاه‌ها)\"\n      }\n    },\n    \"tts\": {\n      \"heading\": \"تنظیمات تبدیل متن به گفتار\",\n      \"ttsEnabled\": {\n        \"label\": \"فعال کردن تبدیل متن به گفتار\"\n      },\n      \"ttsAutoPlay\": {\n        \"label\": \"پخش خودکار پاسخ صوتی پس از تکمیل\"\n      },\n      \"ttsProvider\": {\n        \"label\": \"ارائه دهنده تبدیل متن به گفتار\",\n        \"placeholder\": \"یک ارائه دهنده را انتخاب کنید\"\n      },\n      \"ttsVoice\": {\n        \"label\": \"صدای تبدیل متن به گفتار\",\n        \"placeholder\": \"صدا را انتخاب کنید\"\n      },\n      \"ssmlEnabled\": {\n        \"label\": \"فعال کردن SSML (Speech Synthesis Markup Language)\"\n      },\n      \"responseSplitting\": {\n        \"label\": \"تقسیم پاسخ\"\n      },\n      \"removeReasoningTagTTS\": {\n        \"label\": \"حذف برچسب استدلال از تبدیل متن به گفتار\"\n      }\n    },\n    \"stt\": {\n      \"heading\": \"تنظیمات تبدیل گفتار به متن\",\n      \"autoStopTimeout\": {\n        \"label\": \"زمان توقف خودکار (میلی‌ثانیه)\",\n        \"placeholder\": \"زمان توقف خودکار را به میلی‌ثانیه وارد کنید\"\n      },\n      \"autoSubmitVoiceMessage\": {\n        \"label\": \"ارسال خودکار پیام صوتی\"\n      }\n    }\n  },\n  \"manageModels\": {\n    \"title\": \"مدیریت مدل‌ها\",\n    \"addBtn\": \"افزودن مدل جدید\",\n    \"columns\": {\n      \"name\": \"نام\",\n      \"digest\": \"هضم\",\n      \"nickname\": \"نام مستعار\",\n      \"modifiedAt\": \"اصلاح شده در\",\n      \"size\": \"اندازه\",\n      \"actions\": \"اقدامات\"\n    },\n    \"expandedColumns\": {\n      \"parentModel\": \"مدل والد\",\n      \"format\": \"فرمت\",\n      \"family\": \"خانواده\",\n      \"parameterSize\": \"اندازه پارامترها\",\n      \"quantizationLevel\": \"سطح کوانتیزاسیون\"\n    },\n    \"tooltip\": {\n      \"delete\": \"حذف مدل\",\n      \"repull\": \"دریافت دوباره مدل\",\n      \"editNickname\": \"ویرایش نام مستعار\"\n    },\n    \"confirm\": {\n      \"delete\": \"آیا مطمئن هستید که می خواهید این مدل را حذف کنید؟\",\n      \"repull\": \"آیا مطمئن هستید که می خواهید این مدل را دوباره دریافت کنید؟\"\n    },\n    \"modal\": {\n      \"title\": \"افزودن مدل جدید\",\n      \"placeholder\": \"نام مدل را وارد کنید\",\n      \"pull\": \"دریافت مدل\"\n    },\n    \"notification\": {\n      \"pullModel\": \"در حال دریافت مدل\",\n      \"pullModelDescription\": \"در حال دریافت مدل {{modelName}}. برای جزئیات بیشتر، آیکون افزونه را بررسی کنید.\",\n      \"cancellingDownload\": \"لغو دانلود\",\n      \"cancellingDownloadDescription\": \"دانلود مدل در حال لغو شدن است...\",\n      \"success\": \"موفقیت\",\n      \"error\": \"خطا\",\n      \"successDescription\": \"مدل با موفقیت دریافت شد\",\n      \"successDeleteDescription\": \"مدل با موفقیت حذف شد\",\n      \"someError\": \"مشکلی پیش آمد. لطفا بعدا دوباره امتحان کنید\"\n    }\n  },\n  \"managePrompts\": {\n    \"title\": \"مدیریت پرامپت‌ها\",\n    \"addBtn\": \"اضافه کردن پرامپت جدید\",\n    \"option1\": \"عادی\",\n    \"option2\": \"RAG\",\n    \"questionPrompt\": \"سوال پرامپت\",\n    \"columns\": {\n      \"title\": \"عنولن\",\n      \"prompt\": \"پرامپت\",\n      \"type\": \"نوع پرامپت\",\n      \"actions\": \"اقدامات\"\n    },\n    \"segmented\": {\n      \"custom\": \"پرامپت‌های سفارشی\",\n      \"copilot\": \"پرامپت‌های کوپایلوت\"\n    },\n    \"systemPrompt\": \"پرامپت سیستم \",\n    \"quickPrompt\": \"پرامپت سریع\",\n    \"tooltip\": {\n      \"delete\": \"پاک کردن پرامپت\",\n      \"edit\": \"ویرایش پرامپت\"\n    },\n    \"confirm\": {\n      \"delete\": \"آیا مطمئن هستید که می خواهید این پرامپت را حذف کنید؟ این عمل قابل برگشت نیست.\"\n    },\n    \"modal\": {\n      \"addTitle\": \"اضافه کردن پرامپت جدید\",\n      \"editTitle\": \"ویرایش پرامپت\"\n    },\n    \"form\": {\n      \"title\": {\n        \"label\": \"عنوان\",\n        \"placeholder\": \"پرامپت عالی من\",\n        \"required\": \"لطفا یک عنوان وارد کنید\"\n      },\n      \"prompt\": {\n        \"label\": \"پرامپت\",\n        \"placeholder\": \"پرامپت وارد کنید\",\n        \"required\": \"لطفا یک پرامپت وارد کنید\",\n        \"help\": \"می توانید از {key} به عنوان متغیر در درخواست خود استفاده کنید.\",\n        \"missingTextPlaceholder\": \"متغیر {text} در پرامپت وجود ندارد. لطفاً آن را اضافه کنید.\"\n      },\n      \"isSystem\": {\n        \"label\": \"یک پرامپت سیستمی باشد\"\n      },\n      \"btnSave\": {\n        \"saving\": \"در حال اضافه کردن پرامپت...\",\n        \"save\": \"اضافه کردن پرامپت\"\n      },\n      \"btnEdit\": {\n        \"saving\": \"در حال به روزرسانی پرامپت...\",\n        \"save\": \"به روزرسانی پرامپت\"\n      }\n    },\n    \"notification\": {\n      \"addSuccess\": \"پرامپت اضافه شد\",\n      \"addSuccessDesc\": \"پرامپت با موفقیت اضافه شد\",\n      \"error\": \"خطا\",\n      \"someError\": \"مشکلی پیش آمد. لطفا بعدا دوباره امتحان کنید\",\n      \"updatedSuccess\": \"پرامپت به روز شد\",\n      \"updatedSuccessDesc\": \"پرامپت با موفقیت به روز شد\",\n      \"deletedSuccess\": \"پرامپت حذف شد\",\n      \"deletedSuccessDesc\": \"پرامپت با موفقیت حذف شد\"\n    }\n  },\n  \"manageShare\": {\n    \"title\": \"مدیریت اشتراک گذاری\",\n    \"heading\": \"پیکربندی URL اشتراک گذاری صفحه\",\n    \"form\": {\n      \"url\": {\n        \"label\": \"آدرس اشتراک گذاری صفحه\",\n        \"placeholder\": \"URL اشتراک گذاری صفحه را وارد کنید\",\n        \"required\": \"لطفا URL اشتراک گذاری صفحه خود را وارد کنید!\",\n        \"help\": \"به دلایل حفظ حریم خصوصی، می توانید اشتراک گذاری صفحه را خودتان میزبانی کنید و URL را در اینجا ارائه دهید. <anchor>بیشتر بدانید</anchor>.\"\n      }\n    },\n    \"webshare\": {\n      \"heading\": \"اشتراک گذاری وب\",\n      \"columns\": {\n        \"title\": \"عنوان\",\n        \"url\": \"URL\",\n        \"actions\": \"اقدامات\"\n      },\n      \"tooltip\": {\n        \"delete\": \"حذف اشتراک گذاری\"\n      },\n      \"confirm\": {\n        \"delete\": \"آیا مطمئن هستید که می خواهید این اشتراک گذاری را حذف کنید؟ این عمل قابل برگشت نیست.\"\n      },\n      \"label\": \"مدیریت اشتراک گذاری صفحه\",\n      \"description\": \"ویژگی اشتراک گذاری صفحه را فعال یا غیرفعال کنید\"\n    },\n    \"notification\": {\n      \"pageShareSuccess\": \"URL اشتراک گذاری صفحه با موفقیت به روز شد\",\n      \"someError\": \"مشکلی پیش آمد. لطفا بعدا دوباره امتحان کنید\",\n      \"webShareDeleteSuccess\": \"اشتراک گذاری وب با موفقیت حذف شد\"\n    }\n  },\n  \"ollamaSettings\": {\n    \"title\": \"تنظیمات Ollama\",\n    \"heading\": \"پیکربندی Ollama\",\n    \"settings\": {\n      \"ollamaUrl\": {\n        \"label\": \"آدرس Ollama\",\n        \"placeholder\": \"URL Ollama را وارد کنید\"\n      },\n      \"globalEnable\": {\n        \"label\": \"فعال یا غیرفعال کردن یکپارچه سازی Ollama به صورت سراسری\",\n        \"warning\": \"با غیرفعال کردن یکپارچه سازی Ollama به صورت سراسری، Page Assist مدل ها را از Ollama دریافت نخواهد کرد. شما همچنان می توانید نمونه Ollama را از بخش <anchor>API سازگار با OpenAI</anchor> اضافه کنید که به درستی کار خواهد کرد.\"\n      },\n      \"advanced\": {\n        \"label\": \"پیکربندی پیشرفته URL Ollama\",\n        \"urlRewriteEnabled\": {\n          \"label\": \"URL مبدا سفارشی را فعال یا غیرفعال کنید\"\n        },\n        \"rewriteUrl\": {\n          \"label\": \"URL مبدا سفارشی\",\n          \"placeholder\": \"URL مبدا سفارشی را وارد کنید\"\n        },\n        \"autoCORSFix\": {\n          \"label\": \"فعال یا غیرفعال کردن رفع خودکار CORS در Ollama\"\n        },\n        \"headers\": {\n          \"label\": \"هدرهای سفارشی\",\n          \"add\": \"افزودن هدر\",\n          \"key\": {\n            \"label\": \"کلید هدر\",\n            \"placeholder\": \"Authorization\"\n          },\n          \"value\": {\n            \"label\": \"مقدار هدر\",\n            \"placeholder\": \"Bearer token\"\n          }\n        },\n        \"help\": \"اگر با Ollama در Page Assist مشکل اتصال دارید، می توانید یک URL اصلی سفارشی پیکربندی کنید. برای کسب اطلاعات بیشتر در مورد پیکربندی، <anchor>اینجا را کلیک</anchor> کنید.\"\n      }\n    }\n  },\n  \"manageSearch\": {\n    \"title\": \"مدیریت جستجوی وب\",\n    \"heading\": \"پیکربندی جستجوی وب\"\n  },\n  \"about\": {\n    \"title\": \"درباره\",\n    \"heading\": \"درباره\",\n    \"chromeVersion\": \"نسخه دستیار صفحه\",\n    \"ollamaVersion\": \"نسخه Ollama\",\n    \"support\": \"شما می توانید با کمک مالی یا حمایت مالی از طریق پلتفرم های زیر از پروژه Page Assist حمایت کنید:\",\n    \"koFi\": \"پشتیبانی در Ko-fi\",\n    \"githubSponsor\": \"حمایت مالی در GitHub\",\n    \"githubRepo\": \"مخزن GitHub\"\n  },\n  \"manageKnowledge\": {\n    \"title\": \"مدیریت دانش\",\n    \"heading\": \"پیکربندی پایگاه دانش\"\n  },\n  \"rag\": {\n    \"title\": \"تنظیمات Pipelineel\",\n    \"ragSettings\": {\n      \"label\": \"تنظیمات RAG\",\n      \"model\": {\n        \"label\": \"مدل جاسازی\",\n        \"required\": \"لطفا یک مدل را انتخاب کنید\",\n        \"help\": \"به شدت توصیه می شود از مدل های جاسازی مانند `nomic-embed-text` استفاده کنید.\",\n        \"placeholder\": \"یک مدل را انتخاب کنید\"\n      },\n      \"chunkSize\": {\n        \"label\": \"اندازه تکه\",\n        \"placeholder\": \"اندازه تکه را وارد کنید\",\n        \"required\": \"لطفا اندازه تکه را وارد کنید\"\n      },\n      \"chunkOverlap\": {\n        \"label\": \"همپوشانی تکه\",\n        \"placeholder\": \"داخل تکه همپوشانی شوید\",\n        \"required\": \"لطفا یک همپوشانی تکه ای وارد کنید\"\n      },\n      \"totalFilePerKB\": {\n        \"label\": \"محدودیت پیش‌فرض آپلود فایل پایگاه دانش\",\n        \"placeholder\": \"محدودیت پیش‌فرض آپلود فایل را وارد کنید (مثلاً 10)\",\n        \"required\": \"لطفاً محدودیت پیش‌فرض آپلود فایل را وارد کنید\"\n      },\n      \"noOfRetrievedDocs\": {\n        \"label\": \"تعداد اسناد بازیابی شده\",\n        \"placeholder\": \"تعداد اسناد بازیابی شده را وارد کنید\",\n        \"required\": \"لطفاً تعداد اسناد بازیابی شده را وارد کنید\"\n      },\n      \"splittingSeparator\": {\n        \"label\": \"جداکننده\",\n        \"placeholder\": \"جداکننده را وارد کنید (مثلاً \\\\n\\\\n)\",\n        \"required\": \"لطفاً یک جداکننده وارد کنید\"\n      },\n      \"splittingStrategy\": {\n        \"label\": \"تقسیم‌کننده متن\"\n      }\n    },\n    \"prompt\": {\n      \"label\": \"پیکربندی پرامپت RAG\",\n      \"option1\": \"عادی\",\n      \"option2\": \"وب\",\n      \"alert\": \"پیکربندی اعلان سیستم در اینجا منسوخ شده است. لطفا از بخش مدیریت پرامپت‌ها برای افزودن یا ویرایش درخواست‌ها استفاده کنید. این بخش در نسخه بعدی حذف خواهد شد\",\n      \"systemPrompt\": \"پرامپت سیستم\",\n      \"systemPromptPlaceholder\": \"پرامپت سیستم را وارد کنید\",\n      \"webSearchPrompt\": \"پرامپت جستجوی وب\",\n      \"webSearchPromptHelp\": \"«{search_results}» را از پرامپت حذف نکنید.\",\n      \"webSearchPromptError\": \"لطفا یک پرامپت جستجوی وب وارد کنید\",\n      \"webSearchPromptPlaceholder\": \"پرامپت جستجوی وب را وارد کنید\",\n      \"webSearchFollowUpPrompt\": \"پرامپت پیگیری جستجوی وب\",\n      \"webSearchFollowUpPromptHelp\": \"«{chat_history}» و «{question}» را از پرامپت حذف نکنید.\",\n      \"webSearchFollowUpPromptError\": \"لطفا پرامپت پیگیری جستجوی وب خود را وارد کنید!\",\n      \"webSearchFollowUpPromptPlaceholder\": \"پرامپت پیگیری جستجوی وب شما\"\n    }\n  },\n  \"chromeAiSettings\": {\n    \"title\": \"تنظیمات هوش مصنوعی کروم\"\n  },\n  \"mermaid\": \"مرماید\"\n}\n"
  },
  {
    "path": "src/assets/locale/fa/sidepanel.json",
    "content": "{\n    \"tooltip\": {\n        \"embed\": \"ممکن است چند دقیقه طول بکشد تا صفحه جاسازی شود. لطفاً منتظر بمانید...\",\n        \"clear\": \"پاک کردن تاریخچه گپ\",\n        \"history\": \"تاریخچه گپ\",\n        \"openwebui\": \"باز کردن رابط کاربری وب\"\n    }\n}"
  },
  {
    "path": "src/assets/locale/fr/chrome.json",
    "content": "{\n    \"heading\": \"Configurer Chrome AI\",\n    \"status\": {\n        \"label\": \"Activer ou désactiver la prise en charge de Chrome AI sur Page Assist\"\n    },\n    \"error\": {\n        \"browser_not_supported\": \"Cette version de Chrome n'est pas prise en charge par le modèle Gemini Nano. Veuillez mettre à jour vers la version 127 ou ultérieure.\",\n        \"ai_not_supported\": \"Le paramètre chrome://flags/#prompt-api-for-gemini-nano n'est pas activé. Veuillez l'activer.\",\n        \"ai_not_ready\": \"Gemini Nano n'est pas encore prêt; vous devez vérifier les paramètres de Chrome.\",\n        \"internal_error\": \"Une erreur interne est survenue. Veuillez réessayer plus tard.\"\n    },\n    \"errorDescription\": \"Pour utiliser Chrome AI, vous avez besoin d'une version du navigateur supérieure à 138. Suivez ces étapes :\\n\\n1. Allez à `chrome://flags/#prompt-api-for-gemini-nano` et activez \\\"Prompt API for Gemini Nano\\\".\\n2. Redémarrez Chrome pour appliquer le changement.\\n3. Revenez à cette page et cliquez sur \\\"Télécharger le modèle\\\" — cela téléchargera un modèle de 4 Go pour la première fois.\\n4. Une fois téléchargé, Gemini Nano peut être activé via Page Assist.\",\n    \"downloadModel\": \"Télécharger le modèle\",\n    \"modelDownloadWarning\": \"Cela téléchargera un modèle d'une taille de téléchargement approximative allant de 1,5 Go à 2,4 Go. Assurez-vous d'avoir suffisamment d'espace disque.\"\n}"
  },
  {
    "path": "src/assets/locale/fr/common.json",
    "content": "{\n    \"pageAssist\": \"Page Assist\",\n    \"selectAModel\": \"Sélectionner un modèle\",\n    \"save\": \"Enregistrer\",\n    \"saved\": \"Enregistré\",\n    \"cancel\": \"Annuler\",\n    \"retry\": \"Réessayer\",\n    \"share\": {\n        \"tooltip\": {\n            \"share\": \"Partager\"\n        },\n        \"modal\": {\n            \"title\": \"Partagez le lien vers le chat\"\n        },\n        \"form\": {\n            \"defaultValue\": {\n                \"name\": \"Anonyme\",\n                \"title\": \"Chat sans titre\"\n            },\n            \"title\": {\n                \"label\": \"Titre du chat\",\n                \"placeholder\": \"Entrer le titre du chat\",\n                \"required\": \"Le titre du chat est nécessaire\"\n            },\n            \"name\": {\n                \"label\": \"Votre nom\",\n                \"placeholder\": \"Entrer votre nom\",\n                \"required\": \"Votre nom est nécessaire\"\n            },\n            \"btn\": {\n                \"save\": \"Générer le lien\",\n                \"saving\": \"Génération du lien...\"\n            }\n        },\n        \"notification\": {\n            \"successGenerate\": \"Lien copié dans le presse-papiers\",\n            \"failGenerate\": \"Échec de la génération du lien\"\n        }\n    },\n    \"copyToClipboard\": \"Copier dans le presse-papier\",\n    \"webSearch\": \"Recherche sur le Web\",\n    \"regenerate\": \"Régénérer\",\n    \"continue\": \"Continuer la réponse\",\n    \"edit\": \"Modifier\",\n    \"delete\": \"Supprimer\",\n    \"saveAndSubmit\": \"Enregistrer et soumettre\",\n    \"editMessage\": {\n        \"placeholder\": \"Tapez un message...\"\n    },\n    \"submit\": \"Soumettre\",\n    \"noData\": \"Aucune donnée\",\n    \"noHistory\": \"Pas d'historique de chat\",\n    \"chatWithCurrentPage\": \"Discuter avec la page actuelle\",\n    \"beta\": \"Bêta\",\n    \"tts\": \"Synthèse vocale\",\n    \"currentChatModelSettings\": \"Paramètres actuels du modèle de chat\",\n    \"modelSettings\": {\n        \"label\": \"Paramètres du modèle\",\n        \"description\": \"Définissez les options globales du modèle pour tous les chats\",\n        \"form\": {\n            \"keepAlive\": {\n                \"label\": \"Maintenir en mémoire\",\n                \"help\": \"contrôle la durée pendant laquelle le modèle reste chargé en mémoire après la demande (par défaut : 5m)\",\n                \"placeholder\": \"Par exemple : 5m, 10m, 1h\"\n            },\n            \"temperature\": {\n                \"label\": \"Température\",\n                \"placeholder\": \"Par exemple : 0.7, 1.0\"\n            },\n            \"numCtx\": {\n                \"label\": \"Taille de la fenêtre de contexte (num_ctx)\",\n                \"placeholder\": \"Entrer la taille de la fenêtre de contexte (par défaut : 2048)\"\n            },\n            \"numPredict\": {\n                \"label\": \"Nombre maximum de jetons (num_predict)\",\n                \"placeholder\": \"Par exemple : 2048, 4096\"\n            },\n            \"seed\": {\n                \"label\": \"Graine\",\n                \"placeholder\": \"Par exemple : 1234\",\n                \"help\": \"Reproductibilité de la sortie du modèle\"\n            },\n            \"topK\": {\n                \"label\": \"Top K\",\n                \"placeholder\": \"Par exemple : 40, 100\"\n            },\n            \"topP\": {\n                \"label\": \"Top P\",\n                \"placeholder\": \"Par exemple : 0.9, 0.95\"\n            },\n            \"useMMap\": {\n                \"label\": \"Utiliser MMap\"\n            },\n            \"tfsZ\": {\n                \"label\": \"TFS-Z\",\n                \"placeholder\": \"Par exemple : 1.0, 1.1\"\n            },\n            \"numKeep\": {\n                \"label\": \"Nombre de conservation\",\n                \"placeholder\": \"Par exemple : 256, 512\"\n            },\n            \"numThread\": {\n                \"label\": \"Nombre de threads\",\n                \"placeholder\": \"Par exemple : 8, 16\"\n            },\n            \"useMlock\": {\n                \"label\": \"Utiliser Mlock\"\n            },\n            \"minP\": {\n                \"label\": \"Min P\",\n                \"placeholder\": \"Par exemple : 0.05\"\n            },\n            \"repeatPenalty\": {\n                \"label\": \"Pénalité de répétition\",\n                \"placeholder\": \"Par exemple : 1.1, 1.2\"\n            },\n            \"repeatLastN\": {\n                \"label\": \"Répéter les N derniers éléments\",\n                \"placeholder\": \"Par exemple : 64, 128\"\n            },\n            \"numGpu\": {\n                \"label\": \"Nombre de GPU\",\n                \"placeholder\": \"Entrer le nombre de couches à envoyer aux GPU\"\n            },\n            \"systemPrompt\": {\n                \"label\": \"Prompt système temporaire\",\n                \"placeholder\": \"Entrer le prompt système\",\n                \"help\": \"C'est un moyen rapide de définir le prompt système dans le chat actuel, qui remplacera le prompt système sélectionné si il existe.\"\n            }\n        },\n        \"advanced\": \"Plus de paramètres du modèle\"\n    },\n    \"copilot\": {\n        \"summary\": \"Résumer\",\n        \"explain\": \"Expliquer\",\n        \"rephrase\": \"Reformuler\",\n        \"translate\": \"Traduire\",\n        \"custom\": \"Personnalisé\"\n    },\n    \"citations\": \"Citations\",\n    \"segmented\": {\n        \"ollama\": \"Modèles Ollama\",\n        \"custom\": \"Modèles personnalisés\"\n    },\n    \"downloadCode\": \"Télécharger le code\",\n    \"date\": {\n        \"pinned\": \"Épinglé\",\n        \"today\": \"Aujourd'hui\",\n        \"yesterday\": \"Hier\",\n        \"last7Days\": \"7 derniers jours\",\n        \"older\": \"Plus ancien\"\n    },\n    \"range\": {\n        \"deleteConfirm\": {\n            \"pinned\": \"Êtes-vous sûr de vouloir supprimer tous les messages épinglés ?\",\n            \"today\": \"Êtes-vous sûr de vouloir supprimer tous les messages d'aujourd'hui ?\",\n            \"yesterday\": \"Êtes-vous sûr de vouloir supprimer tous les messages d'hier ?\",\n            \"last7Days\": \"Êtes-vous sûr de vouloir supprimer tous les messages des 7 derniers jours ?\",\n            \"older\": \"Êtes-vous sûr de vouloir supprimer tous les messages plus anciens ?\"\n        },\n        \"tooltip\": {\n            \"pinned\": \"Supprimer tous les messages épinglés\",\n            \"today\": \"Supprimer tous les messages d'aujourd'hui\",\n            \"yesterday\": \"Supprimer tous les messages d'hier\",\n            \"last7Days\": \"Supprimer tous les messages des 7 derniers jours\",\n            \"older\": \"Supprimer tous les messages plus anciens\"\n        }\n    },\n    \"pin\": \"Épingler\",\n    \"unpin\": \"Désépingler\",\n    \"generationInfo\": \"Informations de génération\",\n    \"sidebarChat\": \"Chat dans le panneau latérale\",\n    \"reasoning\": {\n        \"thinking\": \"Réflexion....\",\n        \"thought\": \"Réflexion pendant {{time}}\"\n    },\n    \"mermaid\": \"Mermaid\",\n    \"embeddingGen\": \"Création d'embeddings, cela peut prendre un certain temps\",\n    \"semanticSearch\": \"Recherche sémantique en cours\",\n    \"downloading\": \"Téléchargement en cours\",\n    \"cancelPullingModel\": {\n        \"confirm\": \"Êtes-vous sûr de vouloir annuler le téléchargement ? Cela arrêtera le processus de téléchargement. Selon la documentation Ollama, vous pouvez reprendre là où vous vous êtes arrêté.\"\n    }\n}"
  },
  {
    "path": "src/assets/locale/fr/knowledge.json",
    "content": "{\n    \"addBtn\": \"Ajouter de nouvelles connaissances\",\n    \"columns\": {\n        \"title\": \"Titre\",\n        \"status\": \"Statut\",\n        \"embeddings\": \"Modèle d'embedding\",\n        \"createdAt\": \"Créé le\",\n        \"action\": \"Actions\"\n    },\n    \"expandedColumns\": {\n        \"name\": \"Nom\"\n    },\n    \"confirm\": {\n        \"delete\": \"Êtes-vous sûr de vouloir supprimer cette connaissance ?\"\n    },\n    \"deleteSuccess\": \"Connaissance supprimée avec succès\",\n    \"status\": {\n        \"pending\": \"En attente\",\n        \"finished\": \"Terminé\",\n        \"processing\": \"En cours\",\n        \"failed\": \"Échoué\"\n    },\n    \"addKnowledge\": \"Ajouter une connaissance\",\n    \"updateKnowledge\": \"Ajouter une source\",\n    \"form\": {\n        \"title\": {\n            \"label\": \"Titre de la connaissance\",\n            \"placeholder\": \"Entrer le titre de la connaissance\",\n            \"required\": \"Le titre de la connaissance est nécessaire\"\n        },\n        \"uploadFile\": {\n            \"label\": \"Charger un fichier\",\n            \"uploadText\": \"Faites glisser et déposez un fichier ici ou cliquez pour charger\",\n            \"uploadHint\": \"Types de fichiers pris en charge: .pdf, .csv, .txt, .md, .docx\",\n            \"required\": \"Un fichier est nécessaire\"\n        },\n        \"submit\": \"Soumettre\",\n        \"success\": \"Connaissance ajoutée avec succès\"\n    },\n    \"noEmbeddingModel\": \"Veuillez d'abord ajouter un modèle d'embedding depuis la page des paramètres RAG\"\n}"
  },
  {
    "path": "src/assets/locale/fr/openai.json",
    "content": "{\n    \"settings\": \"API compatible OpenAI\",\n    \"heading\": \"API compatible OpenAI\",\n    \"subheading\": \"Gérez et configurez ici vos fournisseurs d'API compatibles OpenAI.\",\n    \"addBtn\": \"Ajouter un fournisseur\",\n    \"table\": {\n        \"name\": \"Nom du fournisseur\",\n        \"baseUrl\": \"URL de base\",\n        \"actions\": \"Actions\"\n    },\n    \"modal\": {\n        \"titleAdd\": \"Ajouter un nouveau fournisseur\",\n        \"titleEdit\": \"Modifier le fournisseur\",\n        \"name\": {\n            \"label\": \"Nom du fournisseur\",\n            \"required\": \"Le nom du fournisseur est nécessaire.\",\n            \"placeholder\": \"Entrer le nom du fournisseur\"\n        },\n        \"baseUrl\": {\n            \"label\": \"URL de base\",\n            \"help\": \"L'URL de base du fournisseur d'API OpenAI. ex: (http://localhost:1234/v1)\",\n            \"required\": \"L'URL de base est nécessaire.\",\n            \"placeholder\": \"Entrer l'URL de base\"\n        },\n        \"apiKey\": {\n            \"label\": \"Clé API\",\n            \"required\": \"La clé API est nécessaire.\",\n            \"placeholder\": \"Entrer la clé API\"\n        },\n        \"submit\": \"Enregistrer\",\n        \"update\": \"Mettre à jour\",\n        \"deleteConfirm\": \"Êtes-vous sûr de vouloir supprimer ce fournisseur ?\",\n        \"model\": {\n            \"title\": \"Liste des modèles\",\n            \"subheading\": \"Veuillez sélectionner les modèles de chat que vous souhaitez utiliser avec ce fournisseur.\",\n            \"success\": \"Nouveaux modèles ajoutés avec succès.\"\n        },\n        \"tipLMStudio\": \"Page Assist récupérera automatiquement les modèles que vous avez chargés sur LM Studio. Vous n'avez pas besoin de les ajouter manuellement.\"\n    },\n    \"addSuccess\": \"Fournisseur ajouté avec succès.\",\n    \"deleteSuccess\": \"Fournisseur supprimé avec succès.\",\n    \"updateSuccess\": \"Fournisseur mis à jour avec succès.\",\n    \"delete\": \"Supprimer\",\n    \"edit\": \"Modifier\",\n    \"newModel\": \"Ajouter des modèles au fournisseur\",\n    \"noNewModel\": \"Page Assist récupérera automatiquement les modèles que vous avez chargés sur LM Studio. Vous n'avez pas besoin de les ajouter manuellement.\",\n    \"searchModel\": \"Rechercher un modèle\",\n    \"selectAll\": \"Sélectionner tout\",\n    \"save\": \"Enregistrer\",\n    \"saving\": \"Enregistrement...\",\n    \"manageModels\": {\n        \"columns\": {\n            \"name\": \"Nom du modèle\",\n            \"model_type\": \"Type de modèle\",\n            \"model_id\": \"ID du modèle\",\n            \"provider\": \"Nom du fournisseur\",\n            \"actions\": \"Action\",\n            \"nickname\": \"Alias du modèle\"\n        },\n        \"tooltip\": {\n            \"delete\": \"Supprimer\"\n        },\n        \"confirm\": {\n            \"delete\": \"Êtes-vous sûr de vouloir supprimer ce modèle ?\"\n        },\n        \"modal\": {\n            \"title\": \"Ajouter un modèle personnalisé\",\n            \"titleEdit\": \"Modifier un modèle personnalisé\",\n            \"form\": {\n                \"name\": {\n                    \"label\": \"ID du modèle\",\n                    \"placeholder\": \"llama3.2\",\n                    \"required\": \"L'ID du modèle est nécessaire.\"\n                },\n                \"provider\": {\n                    \"label\": \"Fournisseur\",\n                    \"placeholder\": \"Sélectionner un fournisseur\",\n                    \"required\": \"Le fournisseur est nécessaire.\"\n                },\n                \"type\": {\n                    \"label\": \"Type de modèle\"\n                }\n            }\n        }\n    },\n    \"noModelFound\": \"Aucun modèle trouvé. Assurez-vous d'avoir ajouté le bon fournisseur avec l'URL de base et la clé API.\",\n    \"radio\": {\n        \"chat\": \"Modèle de chat\",\n        \"embedding\": \"Modèle d'embedding\",\n        \"chatInfo\": \"est utilisé pour la complétion de chat et la génération de conversation\",\n        \"embeddingInfo\": \"est utilisé pour le RAG et d'autres tâches liées à la recherche sémantique.\"\n    },\n    \"nicknameModal\": {\n        \"title\": \"Ajouter / Modifier l'alias du modèle\",\n        \"form\": {\n            \"modelName\": {\n                \"label\": \"Nom du modèle\",\n                \"placeholder\": \"Entrer le nom du modèle\",\n                \"required\": \"Le nom du modèle est nécessaire.\"\n            },\n            \"modelAvatar\": {\n                \"label\": \"Avatar du modèle\",\n                \"placeholder\": \"Entrer l'avatar du modèle\",\n                \"help\": \"Veuillez entrer l'URL de l'avatar du modèle. Cette image sera affichée dans la fenêtre de chat.\"\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/assets/locale/fr/option.json",
    "content": "{\n    \"newChat\": \"Nouveau chat\",\n    \"selectAPrompt\": \"Sélectionner un prompt\",\n    \"githubRepository\": \"Dépôt GitHub\",\n    \"settings\": \"Paramètres\",\n    \"sidebarTitle\": \"Historique de chat\",\n    \"error\": \"Erreur\",\n    \"somethingWentWrong\": \"Une erreur s'est produite\",\n    \"validationSelectModel\": \"Veuillez sélectionner un modèle pour continuer\",\n    \"deleteHistoryConfirmation\": \"Êtes-vous sûr de vouloir supprimer cet historique ?\",\n    \"editHistoryTitle\": \"Entrer un nouveau titre\",\n    \"temporaryChat\": \"Chat temporaire\",\n    \"more\": {\n        \"copy\": {\n            \"group\": \"Copier\",\n            \"asText\": \"Copier comme texte\",\n            \"asMarkdown\": \"Copier comme Markdown\",\n            \"success\": \"Copié dans le presse-papiers !\"\n        },\n        \"download\": {\n            \"group\": \"Télécharger\",\n            \"text\": \"Fichier texte (.txt)\",\n            \"markdown\": \"Markdown (.md)\",\n            \"json\": \"Fichier JSON (.json)\",\n            \"image\": \"Image (.png)\"\n        },\n        \"share\": \"Partager\"\n    }\n}"
  },
  {
    "path": "src/assets/locale/fr/playground.json",
    "content": "{\n    \"ollamaState\": {\n        \"searching\": \"Recherche de votre Ollama 🦙\",\n        \"running\": \"Ollama est en cours d'exécution 🦙\",\n        \"notRunning\": \"Impossible de se connecter à Ollama 🦙\",\n        \"connectionError\": \"Il semble que vous rencontriez une erreur de connexion. Veuillez consulter cette <anchor>documentation</anchor> pour résoudre le problème.\"\n    },\n    \"formError\": {\n        \"noModel\": \"Veuillez sélectionner un modèle\",\n        \"noEmbeddingModel\": \"Veuillez définir un modèle d'intégration sur la page Paramètres > RAG\"\n    },\n    \"form\": {\n        \"textarea\": {\n            \"placeholder\": \"Saisir un message...\"\n        },\n        \"webSearch\": {\n            \"on\": \"Activé\",\n            \"off\": \"Désactivé\"\n        }\n    },\n    \"tooltip\": {\n        \"searchInternet\": \"Rechercher sur Internet\",\n        \"speechToText\": \"Reconnaissance vocale\",\n        \"uploadImage\": \"Uploader une image\",\n        \"stopStreaming\": \"Arrêter la diffusion\",\n        \"knowledge\": \"Connaissances\",\n        \"vision\": \"Discussion visuelle [Expérimental]\",\n        \"clearContext\": \"Effacer le contexte\"\n    },\n    \"sendWhenEnter\": \"Envoyer en appuyant sur Entrée\",\n    \"welcome\": \"Bonjour ! Comment puis-je vous aider aujourd'hui ?\",\n    \"useOCR\": \"Extraire le texte de l'image (OCR)\"\n}"
  },
  {
    "path": "src/assets/locale/fr/settings.json",
    "content": "{\n  \"generalSettings\": {\n    \"title\": \"Paramètres généraux\",\n    \"settings\": {\n      \"heading\": \"Paramètres de l'interface Web\",\n      \"speechRecognitionLang\": {\n        \"label\": \"Langue de la reconnaissance vocale\",\n        \"placeholder\": \"Sélectionner une langue\"\n      },\n      \"language\": {\n        \"label\": \"Langue\",\n        \"placeholder\": \"Sélectionner une langue\"\n      },\n      \"darkMode\": {\n        \"label\": \"Changer de thème\",\n        \"options\": {\n          \"light\": \"Clair\",\n          \"dark\": \"Sombre\"\n        }\n      },\n      \"copilotResumeLastChat\": {\n        \"label\": \"Reprendre la dernière conversation lors de l'ouverture du panneau latéral (Copilot)\"\n      },\n      \"turnOnChatWithWebsite\": {\n        \"label\": \"Activer le chat avec le site Web par défaut (Copilot)\"\n      },\n      \"webUIResumeLastChat\": {\n        \"label\": \"Reprendre la dernière conversation lors de l'ouverture de l'interface Web\"\n      },\n      \"hideCurrentChatModelSettings\": {\n        \"label\": \"Masquer les paramètres actuels du modèle de chat\"\n      },\n      \"restoreLastChatModel\": {\n        \"label\": \"Restaurer le dernier modèle utilisé pour les conversations précédentes\"\n      },\n      \"sendNotificationAfterIndexing\": {\n        \"label\": \"Envoyer une notification après avoir terminé le traitement de la base de connaissances\"\n      },\n      \"generateTitle\": {\n        \"label\": \"Générer le titre en utilisant l'IA\"\n      },\n      \"ollamaStatus\": {\n        \"label\": \"Activer ou désactiver la vérification de l'état de la connexion Ollama\"\n      },\n      \"wideMode\": {\n        \"label\": \"Activer le mode écran large\"\n      },\n      \"openReasoning\": {\n        \"label\": \"Déplier par défaut le raisonnement\"\n      },\n      \"userChatBubble\": {\n        \"label\": \"Utiliser une bulle de discussion pour les messages de l'utilisateur\"\n      },\n      \"autoCopyResponseToClipboard\": {\n        \"label\": \"Copier automatiquement la réponse dans le presse-papiers\"\n      },\n      \"useMarkdownForUserMessage\": {\n        \"label\": \"Activer le formatage Markdown pour les messages de l'utilisateur\"\n      },\n      \"copyAsFormattedText\": {\n        \"label\": \"Copier en tant que texte formaté\"\n      },\n      \"tabMentionsEnabled\": {\n        \"label\": \"Activer les mentions d'onglets (@tab)\"\n      },\n      \"pasteLargeTextAsFile\": {\n        \"label\": \"Coller le texte volumineux en tant que fichier\"\n      },\n      \"sidepanelTemporaryChat\": {\n        \"label\": \"Activer le chat temporaire dans le panneau latéral par défaut\"\n      },\n      \"defaultCopilotPrompt\": {\n        \"label\": \"Prompt par défaut pour le panneau latéral (Copilot)\",\n        \"placeholder\": \"Sélectionner un prompt\"\n      },\n      \"defaultThinkingMode\": {\n        \"label\": \"Afficher l'état du mode de réflexion dans les formulaires\"\n      },\n      \"defaultWebUIPrompt\": {\n        \"label\": \"Prompt par défaut pour l'interface Web\",\n        \"placeholder\": \"Sélectionner un prompt\"\n      },\n      \"enableMessageQueue\": {\n        \"label\": \"Activer la file d'attente des messages pendant le streaming\"\n      },\n      \"hideReasoningWidget\": {\n        \"label\": \"Masquer le widget de raisonnement des messages IA\"\n      },\n      \"ocrLanguage\": {\n        \"label\": \"Langue OCR par défaut\",\n        \"placeholder\": \"Sélectionner une langue OCR\"\n      },\n      \"optimizeQueueForSmallScreen\": {\n        \"label\": \"Optimiser l'interface de chat pour les petits écrans\"\n      },\n      \"persistChatInput\": {\n        \"label\": \"Conserver la saisie du chat (enregistrer les messages non envoyés)\"\n      },\n      \"removeReasoningTagFromCopy\": {\n        \"label\": \"Supprimer la balise de raisonnement du texte copié\"\n      },\n      \"showMoreForLargeMessage\": {\n        \"label\": \"Afficher « Voir plus » pour les longs messages utilisateur\"\n      },\n      \"sidebarPosition\": {\n        \"label\": \"Position de la barre latérale\",\n        \"options\": {\n          \"left\": \"Gauche\",\n          \"right\": \"Droite\"\n        }\n      },\n      \"tableTextWrap\": {\n        \"label\": \"Activer le retour à la ligne dans les tableaux Markdown\"\n      },\n      \"webuiTemporaryChat\": {\n        \"label\": \"Activer le chat temporaire dans l'interface Web par défaut\"\n      },\n      \"youtubeAutoSummarize\": {\n        \"label\": \"Afficher le bouton « Résumer » sur les vidéos YouTube.\"\n      }\n    },\n    \"sidepanelRag\": {\n      \"heading\": \"Paramètres de récupération\",\n      \"ragEnabled\": {\n        \"label\": \"Activer l'intégration et la récupération\"\n      },\n      \"maxWebsiteContext\": {\n        \"label\": \"Taille maximale du contenu pour le mode contexte complet\",\n        \"placeholder\": \"Taille du contenu (par défaut 4028)\"\n      }\n    },\n    \"webSearch\": {\n      \"heading\": \"Gérer la recherche Web\",\n      \"searchMode\": {\n        \"label\": \"Effectuer une simple recherche sur Internet\"\n      },\n      \"provider\": {\n        \"label\": \"Moteur de recherche\",\n        \"placeholder\": \"Sélectionner un moteur de recherche\"\n      },\n      \"totalSearchResults\": {\n        \"label\": \"Nombre total de résultats de recherche\",\n        \"placeholder\": \"Entrer le nombre total de résultats de recherche\"\n      },\n      \"visitSpecificWebsite\": {\n        \"label\": \"Visiter le site Web mentionné dans le message\"\n      },\n      \"searxng\": {\n        \"url\": {\n          \"label\": \"URL SearXNG\"\n        }\n      },\n      \"braveApi\": {\n        \"label\": \"Clé API Brave\",\n        \"placeholder\": \"Entrer votre clé API Brave\"\n      },\n      \"googleDomain\": {\n        \"label\": \"Domaine Google\"\n      },\n      \"searchOnByDefault\": {\n        \"label\": \"Recherche Internet activée par défaut\"\n      },\n      \"blockedDomains\": {\n        \"description\": \"Exclure les résultats provenant de ces domaines\",\n        \"label\": \"Domaines bloqués\",\n        \"placeholder\": \"ex. spam.com\"\n      },\n      \"domainFilter\": {\n        \"description\": \"Afficher uniquement les résultats provenant de ces domaines\",\n        \"label\": \"Liste de filtres de domaines\",\n        \"placeholder\": \"ex. example.com\"\n      },\n      \"exa\": {\n        \"label\": \"Clé API Exa\",\n        \"placeholder\": \"Entrer votre clé API Exa\"\n      },\n      \"firecrawlAPIKey\": {\n        \"label\": \"Clé API Firecrawl\",\n        \"placeholder\": \"Entrer votre clé API Firecrawl\"\n      },\n      \"tavilyApi\": {\n        \"label\": \"Clé API Tavily\",\n        \"placeholder\": \"Entrer votre clé API Tavily\"\n      }\n    },\n    \"system\": {\n      \"heading\": \"Paramètres système\",\n      \"storageSyncEnabled\": {\n        \"label\": \"Activer la synchronisation du stockage du navigateur (synchroniser les paramètres entre les appareils)\"\n      },\n      \"deleteChatHistory\": {\n        \"label\": \"Réinitialisation du système\",\n        \"button\": \"Tout réinitialiser\",\n        \"confirm\": \"Êtes-vous sûr de vouloir effectuer une réinitialisation du système ? Cela effacera toutes les données et ne pourra pas être annulé.\"\n      },\n      \"export\": {\n        \"label\": \"Exporter toutes les données (historique de chat, base de connaissances, prompts et paramètres)\",\n        \"button\": \"Exporter les données\",\n        \"success\": \"Exportation réussie\"\n      },\n      \"import\": {\n        \"label\": \"Importer toutes les données (historique de chat, base de connaissances, prompts et paramètres)\",\n        \"button\": \"Importer les données\",\n        \"success\": \"Importation réussie\",\n        \"error\": \"Erreur d’importation\"\n      },\n      \"actionIcon\": {\n        \"label\": \"Définir l'action par défaut pour les clics sur l'icône de l'extension\"\n      },\n      \"chatBackgroundImage\": {\n        \"label\": \"Image d'arrière-plan du chat\"\n      },\n      \"contextMenu\": {\n        \"label\": \"Définir l'action par défaut pour le menu contextuel\"\n      },\n      \"fontSize\": {\n        \"label\": \"Taille de police\"\n      },\n      \"webuiBtnSidePanel\": {\n        \"label\": \"Afficher le bouton Web UI dans le panneau latéral\"\n      }\n    },\n    \"tts\": {\n      \"heading\": \"Paramètres de synthèse vocale\",\n      \"ttsEnabled\": {\n        \"label\": \"Activer la synthèse vocale\"\n      },\n      \"ttsAutoPlay\": {\n        \"label\": \"Lecture automatique de la réponse vocale après la fin\"\n      },\n      \"ttsProvider\": {\n        \"label\": \"Fournisseur de synthèse vocale\",\n        \"placeholder\": \"Sélectionnez un fournisseur\"\n      },\n      \"ttsVoice\": {\n        \"label\": \"Voix de synthèse vocale\",\n        \"placeholder\": \"Sélectionner une voix\"\n      },\n      \"ssmlEnabled\": {\n        \"label\": \"Activer SSML (langage de balisage de synthèse vocale)\"\n      },\n      \"responseSplitting\": {\n        \"label\": \"Fractionnement de la réponse\"\n      },\n      \"removeReasoningTagTTS\": {\n        \"label\": \"Supprimer la balise de raisonnement de la synthèse vocale\"\n      }\n    },\n    \"stt\": {\n      \"heading\": \"Paramètres de reconnaissance vocale\",\n      \"autoStopTimeout\": {\n        \"label\": \"Délai d'arrêt automatique (ms)\",\n        \"placeholder\": \"Entrez le délai d'arrêt automatique en millisecondes\"\n      },\n      \"autoSubmitVoiceMessage\": {\n        \"label\": \"Envoi automatique du message vocal\"\n      }\n    }\n  },\n  \"manageModels\": {\n    \"title\": \"Gérer les modèles\",\n    \"addBtn\": \"Ajouter un nouveau modèle\",\n    \"columns\": {\n      \"name\": \"Nom\",\n      \"digest\": \"Empreinte\",\n      \"nickname\": \"Alias\",\n      \"modifiedAt\": \"Modifié\",\n      \"size\": \"Taille\",\n      \"actions\": \"Actions\"\n    },\n    \"expandedColumns\": {\n      \"parentModel\": \"Modèle parent\",\n      \"format\": \"Format\",\n      \"family\": \"Famille\",\n      \"parameterSize\": \"Taille des paramètres\",\n      \"quantizationLevel\": \"Niveau de quantification\"\n    },\n    \"tooltip\": {\n      \"delete\": \"Supprimer le modèle\",\n      \"repull\": \"Recharger le modèle\",\n      \"editNickname\": \"Renommer\"\n    },\n    \"confirm\": {\n      \"delete\": \"Êtes-vous sûr de vouloir supprimer ce modèle ?\",\n      \"repull\": \"Êtes-vous sûr de vouloir recharger ce modèle ?\"\n    },\n    \"modal\": {\n      \"title\": \"Ajouter un nouveau modèle\",\n      \"placeholder\": \"Entrer le nom du modèle\",\n      \"pull\": \"Récupérer le Modèle\"\n    },\n    \"notification\": {\n      \"pullModel\": \"Récupération du modèle\",\n      \"pullModelDescription\": \"Récupération du modèle {{modelName}}. Pour plus de détails, vérifiez l'icône de l'extension.\",\n      \"success\": \"Succès\",\n      \"error\": \"Erreur\",\n      \"successDescription\": \"Modèle récupéré avec succès\",\n      \"successDeleteDescription\": \"Modèle supprimé avec succès\",\n      \"someError\": \"Une erreur s'est produite. Veuillez réessayer plus tard\",\n      \"cancellingDownload\": \"Annulation du téléchargement\",\n      \"cancellingDownloadDescription\": \"Le téléchargement du modèle est en cours d'annulation...\"\n    }\n  },\n  \"managePrompts\": {\n    \"title\": \"Gérer les prompts\",\n    \"addBtn\": \"Ajouter un nouveau prompt\",\n    \"option1\": \"Normale\",\n    \"option2\": \"RAG\",\n    \"questionPrompt\": \"Prompt de question\",\n    \"segmented\": {\n      \"custom\": \"Prompts personnalisées\",\n      \"copilot\": \"Prompts Copilot\"\n    },\n    \"columns\": {\n      \"title\": \"Titre\",\n      \"prompt\": \"Prompt\",\n      \"type\": \"Type de prompt\",\n      \"actions\": \"Actions\"\n    },\n    \"systemPrompt\": \"Prompt système\",\n    \"quickPrompt\": \"Prompt rapide\",\n    \"tooltip\": {\n      \"delete\": \"Supprimer le prompt\",\n      \"edit\": \"Modifier le prompt\"\n    },\n    \"confirm\": {\n      \"delete\": \"Êtes-vous sûr de vouloir supprimer ce prompt ? Cette action ne peut pas être annulée.\"\n    },\n    \"modal\": {\n      \"addTitle\": \"Ajouter un nouveau prompt\",\n      \"editTitle\": \"Modifier le prompt\"\n    },\n    \"form\": {\n      \"title\": {\n        \"label\": \"Titre\",\n        \"placeholder\": \"Mon super prompt\",\n        \"required\": \"Veuillez entrer un titre\"\n      },\n      \"prompt\": {\n        \"label\": \"Prompt\",\n        \"placeholder\": \"Entrer le prompt\",\n        \"required\": \"Veuillez entrer un prompt\",\n        \"help\": \"Vous pouvez utiliser {key} comme variable dans votre prompt.\",\n        \"missingTextPlaceholder\": \"La variable {text} est manquante dans le message. Veuillez l'ajouter.\"\n      },\n      \"isSystem\": {\n        \"label\": \"Est un prompt système\"\n      },\n      \"btnSave\": {\n        \"saving\": \"Ajout du prompt...\",\n        \"save\": \"Ajouter un prompt\"\n      },\n      \"btnEdit\": {\n        \"saving\": \"Mise à jour du prompt...\",\n        \"save\": \"Modifier le prompt\"\n      }\n    },\n    \"notification\": {\n      \"addSuccess\": \"Prompt ajouté\",\n      \"addSuccessDesc\": \"Le prompt a été ajouté avec succès\",\n      \"error\": \"Erreur\",\n      \"someError\": \"Une erreur s'est produite. Veuillez réessayer plus tard\",\n      \"updatedSuccess\": \"Prompt mis à jour\",\n      \"updatedSuccessDesc\": \"Le prompt a été mis à jour avec succès\",\n      \"deletedSuccess\": \"Prompt supprimé\",\n      \"deletedSuccessDesc\": \"Le prompt a été supprimé avec succès\"\n    }\n  },\n  \"manageShare\": {\n    \"title\": \"Gérer le partage\",\n    \"heading\": \"Configurer l'URL de partage de la page\",\n    \"form\": {\n      \"url\": {\n        \"label\": \"URL de partage de page\",\n        \"placeholder\": \"Entrer l'URL de partage de la page\",\n        \"required\": \"Veuillez saisir URL de partage de votre page!\",\n        \"help\": \"Pour des raisons de confidentialité, vous pouvez auto-héberger le partage de la page et fournir l'URL ici.<anchor>En savoir plus</anchor>.\"\n      }\n    },\n    \"webshare\": {\n      \"heading\": \"Partage Web\",\n      \"columns\": {\n        \"title\": \"Titre\",\n        \"url\": \"URL\",\n        \"actions\": \"Actions\"\n      },\n      \"tooltip\": {\n        \"delete\": \"Supprimer le partage\"\n      },\n      \"confirm\": {\n        \"delete\": \"Êtes-vous sûr de vouloir supprimer ce partage ? Cette action ne peut pas être annulée.\"\n      },\n      \"label\": \"Gérer le partage de pages\",\n      \"description\": \"Activer ou désactiver la fonction de partage de page\"\n    },\n    \"notification\": {\n      \"pageShareSuccess\": \"URL de partage de page mise à jour avec succès\",\n      \"someError\": \"Une erreur s'est produite. Veuillez réessayer plus tard\",\n      \"webShareDeleteSuccess\": \"Partage Web supprimé avec succès\"\n    }\n  },\n  \"ollamaSettings\": {\n    \"title\": \"Paramètres d'Ollama\",\n    \"heading\": \"Configurer Ollama\",\n    \"settings\": {\n      \"ollamaUrl\": {\n        \"label\": \"URL d'Ollama\",\n        \"placeholder\": \"Entrer l'URL d'Ollama\"\n      },\n      \"globalEnable\": {\n        \"label\": \"Activer ou désactiver l'intégration Ollama globalement\",\n        \"warning\": \"En désactivant l'intégration Ollama globalement, Page Assist ne récupérera pas les modèles d'Ollama. Vous pouvez toujours ajouter une instance Ollama depuis la section <anchor>API compatible OpenAI</anchor> qui fonctionnera correctement.\"\n      },\n      \"advanced\": {\n        \"label\": \"Configuration avancée de l'URL d'Ollama\",\n        \"urlRewriteEnabled\": {\n          \"label\": \"Activer ou désactiver l'URL d'origine personnalisée\"\n        },\n        \"rewriteUrl\": {\n          \"label\": \"URL d'origine personnalisée\",\n          \"placeholder\": \"Entrer l'URL d'origine personnalisée\"\n        },\n        \"autoCORSFix\": {\n          \"label\": \"Activer ou désactiver la correction automatique CORS d'Ollama\"\n        },\n        \"headers\": {\n          \"label\": \"En-têtes personnalisés\",\n          \"add\": \"Ajouter un en-tête\",\n          \"key\": {\n            \"label\": \"Clé de l'en-tête\",\n            \"placeholder\": \"Autorisation\"\n          },\n          \"value\": {\n            \"label\": \"Valeur de l'en-tête\",\n            \"placeholder\": \"Jeton Bearer\"\n          }\n        },\n        \"help\": \"Si vous rencontrez des problèmes de connexion avec Ollama sur Page Assist, vous pouvez configurer une URL d'origine personnalisée. Pour en savoir plus sur la configuration, <anchor>cliquez ici</anchor>.\"\n      }\n    }\n  },\n  \"manageSearch\": {\n    \"title\": \"Gérer la recherche Web\",\n    \"heading\": \"Configurer la recherche Web\"\n  },\n  \"about\": {\n    \"title\": \"À propos\",\n    \"heading\": \"À propos\",\n    \"chromeVersion\": \"Version de Page Assist\",\n    \"ollamaVersion\": \"Version d'Ollama\",\n    \"support\": \"Vous pouvez soutenir le projet Page Assist en faisant un don ou en parrainant via les plateformes suivantes :\",\n    \"koFi\": \"Soutenir sur Ko-fi\",\n    \"githubSponsor\": \"Parrainer sur GitHub\",\n    \"githubRepo\": \"Dépôt GitHub\"\n  },\n  \"manageKnowledge\": {\n    \"title\": \"Gérer les connaissances\",\n    \"heading\": \"Configurer la base de connaissances\"\n  },\n  \"rag\": {\n    \"title\": \"Paramètres Pipeline\",\n    \"ragSettings\": {\n      \"label\": \"Paramètres RAG\",\n      \"model\": {\n        \"label\": \"Modèle d'embedding\",\n        \"required\": \"Veuillez sélectionner un modèle\",\n        \"help\": \"Fortement recommandé d'utiliser des modèles d'embedding comme «momic-embed-text».\",\n        \"placeholder\": \"Sélectionner un modèle\"\n      },\n      \"chunkSize\": {\n        \"label\": \"Taille des Segments\",\n        \"placeholder\": \"Entrer la taille des segments\",\n        \"required\": \"Veuillez entrer une taille de segment\"\n      },\n      \"chunkOverlap\": {\n        \"label\": \"Chevauchement des segments\",\n        \"placeholder\": \"Entrer le chevauchement des segments\",\n        \"required\": \"Veuillez entrer un chevauchement de segment\"\n      },\n      \"totalFilePerKB\": {\n        \"label\": \"Limite de téléchargement de fichiers par défaut de la base de connaissances\",\n        \"placeholder\": \"Entrer la limite de téléchargement de fichiers par défaut (par exemple, 10)\",\n        \"required\": \"Veuillez entrer la limite de téléchargement de fichiers par défaut\"\n      },\n      \"noOfRetrievedDocs\": {\n        \"label\": \"Nombre de documents récupérés\",\n        \"placeholder\": \"Entrer le nombre de documents récupérés\",\n        \"required\": \"Veuillez entrer le nombre de documents récupérés\"\n      },\n      \"splittingSeparator\": {\n        \"label\": \"Séparateur\",\n        \"placeholder\": \"Entrer le séparateur (par exemple, \\\\n\\\\n)\",\n        \"required\": \"Veuillez entrer un séparateur\"\n      },\n      \"splittingStrategy\": {\n        \"label\": \"Diviseur de texte\"\n      }\n    },\n    \"prompt\": {\n      \"label\": \"Configurer le prompt RAG\",\n      \"option1\": \"Normal\",\n      \"option2\": \"Web\",\n      \"alert\": \"La configuration du prompt système ici est obsolète. Veuillez utiliser la section Gérer les prompts pour ajouter ou modifier des prompts. Cette section sera supprimée dans une prochaine version\",\n      \"systemPrompt\": \"Prompt système\",\n      \"systemPromptPlaceholder\": \"Entrer le prompt système\",\n      \"webSearchPrompt\": \"Prompt de recherche Web\",\n      \"webSearchPromptHelp\": \"Ne supprimez pas `{search_results}` du prompt.\",\n      \"webSearchPromptError\": \"Veuillez entrer un prompt de recherche Web\",\n      \"webSearchPromptPlaceholder\": \"Entrer le prompt de recherche Web\",\n      \"webSearchFollowUpPrompt\": \"Prompt de suivi de recherche Web\",\n      \"webSearchFollowUpPromptHelp\": \"Ne supprimez pas `{chat_history}` et `{question}` du prompt.\",\n      \"webSearchFollowUpPromptError\": \"Veuillez saisir votre prompt de suivi de recherche Web !\",\n      \"webSearchFollowUpPromptPlaceholder\": \"Votre prompt de suivi de recherche Web\"\n    }\n  },\n  \"chromeAiSettings\": {\n    \"title\": \"Paramètres Chrome AI\"\n  },\n  \"mermaid\": \"Mermaid\"\n}\n"
  },
  {
    "path": "src/assets/locale/fr/sidepanel.json",
    "content": "{\n    \"tooltip\": {\n        \"embed\": \"L'intégration de la page peut prendre quelques minutes. Veuillez patienter...\",\n        \"clear\": \"Effacer l'historique du chat\",\n        \"history\": \"Historique du chat\",\n        \"openwebui\": \"Ouvrir WebUI\"\n    }\n}"
  },
  {
    "path": "src/assets/locale/it/chrome.json",
    "content": "{\n    \"heading\": \"Configura Chrome AI\",\n    \"status\": {\n        \"label\": \"Abilita o disabilita il supporto di Chrome AI su Page Assist\"\n    },\n    \"error\": {\n        \"browser_not_supported\": \"Questa versione di Chrome non è supportata dal modello Gemini Nano. Si prega di aggiornare alla versione 127 o successiva.\",\n        \"ai_not_supported\": \"L'impostazione chrome://flags/#prompt-api-for-gemini-nano non è abilitata. Si prega di abilitarla.\",\n        \"ai_not_ready\": \"Gemini Nano non è ancora pronto; è necessario verificare le impostazioni di Chrome.\",\n        \"internal_error\": \"Si è verificato un errore interno. Si prega di riprovare più tardi.\"\n    },\n    \"errorDescription\": \"Per utilizzare Chrome AI, è necessaria la versione 138 o successiva di Chrome. Segui questi passaggi:\\n\\n1. Vai a `chrome://flags/#prompt-api-for-gemini-nano` e abilita \\\"Prompt API for Gemini Nano\\\".\\n2. Riavvia Chrome per applicare la flag.\\n3. Torna su questa pagina e clicca su \\\"Scarica Modello\\\" — questo scaricherà un modello da 4GB la prima volta.\\n4. Una volta scaricato, Gemini Nano può essere abilitato tramite Page Assist.\",\n    \"downloadModel\": \"Scarica Modello\",\n    \"modelDownloadWarning\": \"Questo scaricherà un modello con una dimensione approssimativa tra 1,5 GB e 2,4 GB. Assicurati di avere spazio sufficiente sul disco.\"\n}\n"
  },
  {
    "path": "src/assets/locale/it/common.json",
    "content": "{\n    \"pageAssist\": \"Page Assist\",\n    \"selectAModel\": \"Seleziona un Modello\",\n    \"save\": \"Salva\",\n    \"saved\": \"Salvato\",\n    \"cancel\": \"Annulla\",\n    \"retry\": \"Riprova\",\n    \"share\": {\n        \"tooltip\": {\n            \"share\": \"Condividi\"\n        },\n        \"modal\": {\n            \"title\": \"Condividi Collegamento alla Chat\"\n        },\n        \"form\": {\n            \"defaultValue\": {\n                \"name\": \"Anonimo\",\n                \"title\": \"Chat Senza Titolo\"\n            },\n            \"title\": {\n                \"label\": \"Titolo della Chat\",\n                \"placeholder\": \"Inserisci il Titolo della Chat\",\n                \"required\": \"Titolo della Chat obbligatorio\"\n            },\n            \"name\": {\n                \"label\": \"Il tuo Nome\",\n                \"placeholder\": \"Inserisci il tuo Nome\",\n                \"required\": \"Nome obbligatorio\"\n            },\n            \"btn\": {\n                \"save\": \"Genera Link\",\n                \"saving\": \"Sto generando il Link...\"\n            }\n        },\n        \"notification\": {\n            \"successGenerate\": \"Link copiato negli appunti\",\n            \"failGenerate\": \"Impossibile generare il link\"\n        }\n    },\n    \"copyToClipboard\": \"Copia negli Appunti\",\n    \"webSearch\": \"Ricerca nel Web\",\n    \"regenerate\": \"Rigenera\",\n    \"continue\": \"Continua Risposta\",\n    \"edit\": \"Modifica\",\n    \"delete\": \"Elimina\",\n    \"saveAndSubmit\": \"Salva e Invia\",\n    \"editMessage\": {\n        \"placeholder\": \"Scrivi un messaggio...\"\n    },\n    \"submit\": \"Invia\",\n    \"noData\": \"Nessun Dato\",\n    \"noHistory\": \"Nessuna Cronologia Chat\",\n    \"chatWithCurrentPage\": \"Chatta con la Pagina Corrente\",\n    \"beta\": \"Beta\",\n    \"tts\": \"Leggi ad Alta Voce\",\n    \"currentChatModelSettings\": \"Impostazioni del Modello Corrente\",\n    \"modelSettings\": {\n        \"label\": \"Impostazioni del Modello\",\n        \"description\": \"Imposta le opzioni del modello globalmente per tutte le chat\",\n        \"form\": {\n            \"keepAlive\": {\n                \"label\": \"Keep Alive\",\n                \"help\": \"Imposta il tempo per cui il modello deve rimanere caricato in memoria (default: 5m)\",\n                \"placeholder\": \"Inserisci la durata del Keep Alive (e.g. 5m, 10m, 1h)\"\n            },\n            \"temperature\": {\n                \"label\": \"Temperatura\",\n                \"placeholder\": \"Inserisci la Temperatura (e.g. 0.7, 1.0)\"\n            },\n            \"numCtx\": {\n                \"label\": \"Dimensione della Finestra di Contesto (num_ctx)\",\n                \"placeholder\": \"Inserisci il valore della Finestra di Contesto (default: 2048)\"\n            },\n            \"numPredict\": {\n                \"label\": \"Token Massimi (num_predict)\",\n                \"placeholder\": \"Inserisci il valore dei Token Massimi (es. 2048, 4096)\"\n            },\n            \"seed\": {\n                \"label\": \"Seed\",\n                \"placeholder\": \"Inserisci il Valore Seed (e.g. 1234)\",\n                \"help\": \"Riproducibilità dell'output del modello\"\n            },\n            \"topK\": {\n                \"label\": \"Top K\",\n                \"placeholder\": \"Inserisci il Valore Top K (e.g. 40, 100)\"\n            },\n            \"topP\": {\n                \"label\": \"Top P\",\n                \"placeholder\": \"Inserisci il Valore Top P (e.g. 0.9, 0.95)\"\n            },\n            \"numGpu\": {\n                \"label\": \"Num GPU\",\n                \"placeholder\": \"Inserisci il numero di layer da inviare alla/e GPU\"\n            },\n            \"systemPrompt\": {\n                \"label\": \"Prompt di Sistema Temporaneo\",\n                \"placeholder\": \"Inserisci il Prompt di Sistema\",\n                \"help\": \"Questo è un modo rapido per impostare il prompt di sistema nella chat corrente, che sovrascriverà il prompt di sistema selezionato se esiste.\"\n            }\n        },\n        \"advanced\": \"Altre Impostazioni del Modello\"\n    },\n    \"copilot\": {\n        \"summary\": \"Riassumere\",\n        \"explain\": \"Spiegare\",\n        \"rephrase\": \"Riformulare\",\n        \"translate\": \"Tradurre\"\n    },\n    \"citations\": \"Citazioni\",\n    \"downloadCode\": \"Scarica Codice\",\n    \"date\": {\n        \"pinned\": \"Fissato\",\n        \"today\": \"Oggi\",\n        \"yesterday\": \"Ieri\",\n        \"last7Days\": \"Ultimi 7 Giorni\",\n        \"older\": \"Più Vecchi\"\n    },\n    \"range\": {\n        \"deleteConfirm\": {\n            \"pinned\": \"Sei sicuro di voler eliminare tutti i messaggi fissati?\",\n            \"today\": \"Sei sicuro di voler eliminare tutti i messaggi di oggi?\",\n            \"yesterday\": \"Sei sicuro di voler eliminare tutti i messaggi di ieri?\",\n            \"last7Days\": \"Sei sicuro di voler eliminare tutti i messaggi degli ultimi 7 giorni?\",\n            \"older\": \"Sei sicuro di voler eliminare tutti i messaggi più vecchi?\"\n        },\n        \"tooltip\": {\n            \"pinned\": \"Elimina tutti i messaggi fissati\",\n            \"today\": \"Elimina tutti i messaggi di oggi\",\n            \"yesterday\": \"Elimina tutti i messaggi di ieri\",\n            \"last7Days\": \"Elimina tutti i messaggi degli ultimi 7 giorni\",\n            \"older\": \"Elimina tutti i messaggi più vecchi\"\n        }\n    },\n    \"pin\": \"Fissa\",\n    \"unpin\": \"Rimuovi\",\n    \"generationInfo\": \"Informazioni sulla Generazione\",\n    \"sidebarChat\": \"Chat Laterale\",\n    \"reasoning\": {\n        \"thinking\": \"Pensando....\",\n        \"thought\": \"Pensato per {{time}}\"\n    },\n    \"mermaid\": \"Mermaid\",\n    \"embeddingGen\": \"Creazione di embeddings, questo può richiedere un po' di tempo\",\n    \"semanticSearch\": \"Ricerca semantica in corso\",\n    \"downloading\": \"Download in corso\",\n    \"cancelPullingModel\": {\n        \"confirm\": \"Sei sicuro di voler annullare il download? Questo interromperà il processo di download. Secondo la documentazione di Ollama, puoi riprendere da dove hai interrotto.\"\n    }\n}"
  },
  {
    "path": "src/assets/locale/it/knowledge.json",
    "content": "{\n    \"addBtn\": \"Aggiungi nuova Knowledge Base\",\n    \"columns\": {\n        \"title\": \"Titolo\",\n        \"status\": \"Stato\",\n        \"embeddings\": \"Modello di Embedding\",\n        \"createdAt\": \"Creato da\",\n        \"action\": \"Azioni\"\n    },\n    \"expandedColumns\": {\n        \"name\": \"Nome\"\n    },\n    \"confirm\": {\n        \"delete\": \"Sei sicuro di voler eliminare questa Knowledge Base?\"\n    },\n    \"deleteSuccess\": \"Knowledge Base eliminata correttamente\",\n    \"status\": {\n        \"pending\": \"In attesa\",\n        \"finished\": \"Completato\",\n        \"processing\": \"In corso\",\n        \"failed\": \"Fallito\"\n    },\n    \"addKnowledge\": \"Aggiungi Knowledge Base\",\n    \"updateKnowledge\": \"Aggiungi Fonte\",\n    \"form\": {\n        \"title\": {\n            \"label\": \"Titolo Knowledge Base\",\n            \"placeholder\": \"Inserisci il titolo della Knowledge Base\",\n            \"required\": \"Il Titolo è obbligatorio\"\n        },\n        \"uploadFile\": {\n            \"label\": \"Carica File\",\n            \"uploadText\": \"Trascina un file qui or scegli upload\",\n            \"uploadHint\": \"Tipi di file supportati: .pdf, .csv, .txt, .md, .docx\",\n            \"required\": \"File è obbligatorio\"\n        },\n        \"submit\": \"Invia\",\n        \"success\": \"Knowledge Base aggiunta correttamente\"\n    },\n    \"noEmbeddingModel\": \"Aggiungi prima un modello dalla pagina di impostazione di RAG\"\n}"
  },
  {
    "path": "src/assets/locale/it/openai.json",
    "content": "{\n    \"settings\": \"API compatibile con OpenAI\",\n    \"heading\": \"API compatibile con OpenAI\",\n    \"subheading\": \"Gestisci e configura qui i tuoi provider API compatibili con OpenAI.\",\n    \"addBtn\": \"Aggiungi Provider\",\n    \"table\": {\n        \"name\": \"Nome Provider\",\n        \"baseUrl\": \"URL di Base\",\n        \"actions\": \"Azione\"\n    },\n    \"modal\": {\n        \"titleAdd\": \"Aggiungi Nuovo Provider\",\n        \"name\": {\n            \"label\": \"Nome Provider\",\n            \"required\": \"Il nome del provider è obbligatorio.\",\n            \"placeholder\": \"Inserisci il nome del provider\"\n        },\n        \"baseUrl\": {\n            \"label\": \"URL di Base\",\n            \"help\": \"L'URL di base del provider API OpenAI. es. (http://localhost:1234/v1)\",\n            \"required\": \"L'URL di base è obbligatorio.\",\n            \"placeholder\": \"Inserisci l'URL di base\"\n        },\n        \"apiKey\": {\n            \"label\": \"Chiave API\",\n            \"required\": \"La chiave API è obbligatoria.\",\n            \"placeholder\": \"Inserisci la chiave API\"\n        },\n        \"submit\": \"Salva\",\n        \"update\": \"Aggiorna\",\n        \"deleteConfirm\": \"Sei sicuro di voler eliminare questo provider?\",\n        \"model\": {\n            \"title\": \"Lista Modelli\",\n            \"subheading\": \"Seleziona i modelli di chat che desideri utilizzare con questo provider.\",\n            \"success\": \"Nuovi modelli aggiunti con successo.\"\n        },\n        \"tipLMStudio\": \"Page Assist recupererà automaticamente i modelli caricati su LM Studio. Non è necessario aggiungerli manualmente.\"\n    },\n    \"addSuccess\": \"Provider aggiunto con successo.\",\n    \"deleteSuccess\": \"Provider eliminato con successo.\",\n    \"updateSuccess\": \"Provider aggiornato con successo.\",\n    \"delete\": \"Elimina\",\n    \"edit\": \"Modifica\",\n    \"newModel\": \"Aggiungi Modelli al Provider\",\n    \"noNewModel\": \"Per LMStudio, Ollama, Llamafile, recuperiamo dinamicamente. Non è necessaria l'aggiunta manuale.\",\n    \"searchModel\": \"Cerca Modello\",\n    \"selectAll\": \"Seleziona Tutto\",\n    \"save\": \"Salva\",\n    \"saving\": \"Salvataggio in corso...\",\n    \"manageModels\": {\n        \"columns\": {\n            \"name\": \"Nome Modello\",\n            \"model_type\": \"Tipo di Modello\",\n            \"model_id\": \"ID Modello\",\n            \"provider\": \"Nome Provider\",\n            \"actions\": \"Azione\",\n            \"nickname\": \"Soprannome Modello\"\n        },\n        \"tooltip\": {\n            \"delete\": \"Elimina\"\n        },\n        \"confirm\": {\n            \"delete\": \"Sei sicuro di voler eliminare questo modello?\"\n        },\n        \"modal\": {\n            \"title\": \"Aggiungi Modello Personalizzato\",\n            \"form\": {\n                \"name\": {\n                    \"label\": \"ID Modello\",\n                    \"placeholder\": \"llama3.2\",\n                    \"required\": \"L'ID del modello è obbligatorio.\"\n                },\n                \"provider\": {\n                    \"label\": \"Provider\",\n                    \"placeholder\": \"Seleziona provider\",\n                    \"required\": \"Il provider è obbligatorio.\"\n                },\n                \"type\": {\n                    \"label\": \"Tipo di Modello\"\n                }\n            }\n        }\n    },\n    \"noModelFound\": \"Nessun modello trovato. Assicurati di aver aggiunto il provider corretto con l'URL di base e la chiave API.\",\n    \"radio\": {\n        \"chat\": \"Modello di Chat\",\n        \"embedding\": \"Modello di Embedding\",\n        \"chatInfo\": \"è utilizzato per il completamento della chat e la generazione di conversazioni\",\n        \"embeddingInfo\": \"è utilizzato per RAG e altri compiti correlati alla ricerca semantica.\"\n    },\n    \"nicknameModal\": {\n        \"title\": \"Aggiungi / Modifica Soprannome Modello\",\n        \"form\": {\n            \"modelName\": {\n                \"label\": \"Nome Modello\",\n                \"placeholder\": \"Inserisci nome modello\",\n                \"required\": \"Il nome del modello è obbligatorio.\"\n            },\n            \"modelAvatar\": {\n                \"label\": \"Avatar Modello\",\n                \"placeholder\": \"Inserisci avatar modello\",\n                \"help\": \"Inserisci l'URL dell'avatar del modello. Questa immagine verrà visualizzata nella finestra di chat.\"\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/assets/locale/it/option.json",
    "content": "{\n    \"newChat\": \"Nuova Chat\",\n    \"selectAPrompt\": \"Scegli un Prompt\",\n    \"githubRepository\": \"GitHub Repository\",\n    \"settings\": \"Impsotazioni\",\n    \"sidebarTitle\": \"Cronologia Chat\",\n    \"error\": \"Errore\",\n    \"somethingWentWrong\": \"Qualcosa è andato storto\",\n    \"validationSelectModel\": \"Scegliere un modello per continuare\",\n    \"deleteHistoryConfirmation\": \"Sei sicuro che vuoi eliminare la cronologia?\",\n    \"editHistoryTitle\": \"Inserisci un nuovo titolo\",\n    \"temporaryChat\": \"Chat Temporanea\",\n    \"more\": {\n        \"copy\": {\n            \"group\": \"Copia\",\n            \"asText\": \"Copia come Testo\",\n            \"asMarkdown\": \"Copia come Markdown\",\n            \"success\": \"Copiato negli appunti!\"\n        },\n        \"download\": {\n            \"group\": \"Scarica\",\n            \"text\": \"File di Testo (.txt)\",\n            \"markdown\": \"Markdown (.md)\",\n            \"json\": \"File JSON (.json)\"\n        },\n        \"share\": \"Condividi\"\n    }\n}"
  },
  {
    "path": "src/assets/locale/it/playground.json",
    "content": "{\n    \"ollamaState\": {\n        \"searching\": \"Sto cercando Ollama 🦙\",\n        \"running\": \"Ollama è attivo 🦙\",\n        \"notRunning\": \"Impossibile connettersi a Ollama 🦙\",\n        \"connectionError\": \"C'è stato un problema di connessione. Controlla la <anchor>documentazione</anchor> per investigare.\"\n    },\n    \"formError\": {\n        \"noModel\": \"Seleziona un modello\",\n        \"noEmbeddingModel\": \"Imposta un modello di embedding da Impostazioni > RAG\"\n    },\n    \"form\": {\n        \"textarea\": {\n            \"placeholder\": \"Scrivi un messaggio...\"\n        },\n        \"webSearch\": {\n            \"on\": \"Attivo\",\n            \"off\": \"Disattivato\"\n        }\n    },\n    \"tooltip\": {\n        \"searchInternet\": \"Cerca su Internet\",\n        \"speechToText\": \"Speech to Text\",\n        \"uploadImage\": \"Carica immagine\",\n        \"stopStreaming\": \"Ferma lo Streaming\",\n        \"knowledge\": \"Conoscenza\",\n        \"clearContext\": \"Cancella Contesto\"\n    },\n    \"sendWhenEnter\": \"Invia subito dopo Enter\",\n    \"welcome\": \"Ciao! Come posso aiutarti oggi?\",\n    \"useOCR\": \"Estrai testo dall'immagine (OCR)\"\n}"
  },
  {
    "path": "src/assets/locale/it/settings.json",
    "content": "{\n  \"generalSettings\": {\n    \"title\": \"Impostazioni Generali\",\n    \"settings\": {\n      \"heading\": \"Impostazioni Web UI\",\n      \"speechRecognitionLang\": {\n        \"label\": \"Lingua per il riconoscimento vocale\",\n        \"placeholder\": \"Scegli una lingua\"\n      },\n      \"language\": {\n        \"label\": \"Lingua\",\n        \"placeholder\": \"Scegli una lingua\"\n      },\n      \"darkMode\": {\n        \"label\": \"Cambia il Tema\",\n        \"options\": {\n          \"light\": \"Chiaro\",\n          \"dark\": \"Scuro\"\n        }\n      },\n      \"copilotResumeLastChat\": {\n        \"label\": \"Riprendi l'ultima chat quando apri il Pannello Laterale  (Copilot)\"\n      },\n      \"turnOnChatWithWebsite\": {\n        \"label\": \"Abilita Chat con il Sito Web per impostazione predefinita (Copilot)\"\n      },\n      \"webUIResumeLastChat\": {\n        \"label\": \"Riprendi l'ultima chat quando apri l'interfaccia Web\"\n      },\n      \"hideCurrentChatModelSettings\": {\n        \"label\": \"Nascondi le impostazioni correnti del modello Chat\"\n      },\n      \"restoreLastChatModel\": {\n        \"label\": \"Ripristina l'ultimo modello utilizzato per le chat precedenti\"\n      },\n      \"sendNotificationAfterIndexing\": {\n        \"label\": \"Inviare notifica dopo aver terminato l'elaborazione della base di conoscenza\"\n      },\n      \"generateTitle\": {\n        \"label\": \"Genera titolo utilizzando l'IA\"\n      },\n      \"ollamaStatus\": {\n        \"label\": \"Abilita o disabilita il controllo dello stato della connessione Ollama\"\n      },\n      \"wideMode\": {\n        \"label\": \"Abilita modalità schermo largo\"\n      },\n      \"openReasoning\": {\n        \"label\": \"Apri il Ragionamento Compresso per impostazione predefinita\"\n      },\n      \"userChatBubble\": {\n        \"label\": \"Usa fumetto chat per i messaggi utente\"\n      },\n      \"autoCopyResponseToClipboard\": {\n        \"label\": \"Copia automaticamente la risposta negli appunti\"\n      },\n      \"useMarkdownForUserMessage\": {\n        \"label\": \"Abilita la formattazione Markdown per i messaggi dell'utente\"\n      },\n      \"copyAsFormattedText\": {\n        \"label\": \"Copia come Testo Formattato\"\n      },\n      \"tabMentionsEnabled\": {\n        \"label\": \"Abilita Menzioni Scheda (@tab)\"\n      },\n      \"pasteLargeTextAsFile\": {\n        \"label\": \"Incolla Testo Lungo come File\"\n      },\n      \"sidepanelTemporaryChat\": {\n        \"label\": \"Abilita Chat Temporanea nel Pannello Laterale per impostazione predefinita\"\n      },\n      \"defaultCopilotPrompt\": {\n        \"label\": \"Prompt predefinito per il Pannello Laterale (Copilot)\",\n        \"placeholder\": \"Seleziona un prompt\"\n      },\n      \"defaultThinkingMode\": {\n        \"label\": \"Mostra lo stato della Modalità Pensiero nei moduli\"\n      },\n      \"defaultWebUIPrompt\": {\n        \"label\": \"Prompt predefinito per la Web UI\",\n        \"placeholder\": \"Seleziona un prompt\"\n      },\n      \"enableMessageQueue\": {\n        \"label\": \"Abilita la coda dei messaggi durante lo streaming\"\n      },\n      \"hideReasoningWidget\": {\n        \"label\": \"Nascondi il widget di ragionamento dai messaggi AI\"\n      },\n      \"ocrLanguage\": {\n        \"label\": \"Lingua OCR predefinita\",\n        \"placeholder\": \"Seleziona una lingua OCR\"\n      },\n      \"optimizeQueueForSmallScreen\": {\n        \"label\": \"Ottimizza l'interfaccia chat per schermi piccoli\"\n      },\n      \"persistChatInput\": {\n        \"label\": \"Mantieni l'input della chat (salva i messaggi non inviati)\"\n      },\n      \"removeReasoningTagFromCopy\": {\n        \"label\": \"Rimuovi il tag di ragionamento dal testo copiato\"\n      },\n      \"showMoreForLargeMessage\": {\n        \"label\": \"Mostra \\\"Mostra altro\\\" per i messaggi utente lunghi\"\n      },\n      \"sidebarPosition\": {\n        \"label\": \"Posizione della barra laterale\",\n        \"options\": {\n          \"left\": \"Sinistra\",\n          \"right\": \"Destra\"\n        }\n      },\n      \"tableTextWrap\": {\n        \"label\": \"Abilita l'andata a capo del testo nelle tabelle Markdown\"\n      },\n      \"webuiTemporaryChat\": {\n        \"label\": \"Abilita la chat temporanea nella Web UI per impostazione predefinita\"\n      },\n      \"youtubeAutoSummarize\": {\n        \"label\": \"Mostra il pulsante \\\"Riassumi\\\" sui video di YouTube.\"\n      }\n    },\n    \"sidepanelRag\": {\n      \"heading\": \"Impostazioni di Recupero\",\n      \"ragEnabled\": {\n        \"label\": \"Abilita Embedding e Recupero\"\n      },\n      \"maxWebsiteContext\": {\n        \"label\": \"Dimensione Massima del Contenuto per la Modalità Contesto Completo\",\n        \"placeholder\": \"Dimensione contenuto (predefinito 4028)\"\n      }\n    },\n    \"webSearch\": {\n      \"heading\": \"Gestione ricerca Web\",\n      \"searchMode\": {\n        \"label\": \"Effettua ricerca web Internet semplice\"\n      },\n      \"provider\": {\n        \"label\": \"Motori di ricerca\",\n        \"placeholder\": \"Scegli un motore di ricerca\"\n      },\n      \"totalSearchResults\": {\n        \"label\": \"Risultati della ricerca\",\n        \"placeholder\": \"Inserisci il totale delle ricerche\"\n      },\n      \"visitSpecificWebsite\": {\n        \"label\": \"Visita il sito web menzionato nel messaggio\"\n      },\n      \"searxng\": {\n        \"url\": {\n          \"label\": \"URL SearXNG\"\n        }\n      },\n      \"braveApi\": {\n        \"label\": \"Chiave API Brave\",\n        \"placeholder\": \"Inserisci la tua chiave API Brave\"\n      },\n      \"searchOnByDefault\": {\n        \"label\": \"Ricerca Internet attiva per impostazione predefinita\"\n      },\n      \"blockedDomains\": {\n        \"description\": \"Escludi i risultati provenienti da questi domini\",\n        \"label\": \"Domini bloccati\",\n        \"placeholder\": \"es. spam.com\"\n      },\n      \"domainFilter\": {\n        \"description\": \"Mostra solo i risultati provenienti da questi domini\",\n        \"label\": \"Elenco filtro domini\",\n        \"placeholder\": \"es. example.com\"\n      },\n      \"exa\": {\n        \"label\": \"Chiave API Exa\",\n        \"placeholder\": \"Inserisci la tua chiave API Exa\"\n      },\n      \"firecrawlAPIKey\": {\n        \"label\": \"Chiave API Firecrawl\",\n        \"placeholder\": \"Inserisci la tua chiave API Firecrawl\"\n      },\n      \"googleDomain\": {\n        \"label\": \"Dominio Google\"\n      },\n      \"tavilyApi\": {\n        \"label\": \"Chiave API Tavily\",\n        \"placeholder\": \"Inserisci la tua chiave API Tavily\"\n      }\n    },\n    \"system\": {\n      \"heading\": \"Impostazioni di Sistema\",\n      \"storageSyncEnabled\": {\n        \"label\": \"Abilita sincronizzazione archiviazione browser (sincronizza impostazioni tra dispositivi)\"\n      },\n      \"deleteChatHistory\": {\n        \"label\": \"Reset del Sistema\",\n        \"button\": \"Reset Totale\",\n        \"confirm\": \"Sei sicuro di voler eseguire un reset del sistema? Questa operazione cancellerà tutti i dati e non può essere annullata.\"\n      },\n      \"export\": {\n        \"label\": \"Esporta tutti i dati (cronologia chat, knowledge base, prompt e impostazioni)\",\n        \"button\": \"Esporta dati\",\n        \"success\": \"Esportazione riuscita\"\n      },\n      \"import\": {\n        \"label\": \"Importa tutti i dati (cronologia chat, knowledge base, prompt e impostazioni)\",\n        \"button\": \"Importa dati\",\n        \"success\": \"Importazione riuscita\",\n        \"error\": \"Errore di importazione\"\n      },\n      \"actionIcon\": {\n        \"label\": \"Imposta l'azione predefinita per i clic sull'icona dell'estensione\"\n      },\n      \"chatBackgroundImage\": {\n        \"label\": \"Immagine di sfondo della chat\"\n      },\n      \"contextMenu\": {\n        \"label\": \"Imposta l'azione predefinita per il menu contestuale\"\n      },\n      \"fontSize\": {\n        \"label\": \"Dimensione carattere\"\n      },\n      \"webuiBtnSidePanel\": {\n        \"label\": \"Mostra il pulsante Web UI nel pannello laterale\"\n      }\n    },\n    \"tts\": {\n      \"heading\": \"Impostazioni Text-to-Speech\",\n      \"ttsEnabled\": {\n        \"label\": \"Abilita Text-to-Speech\"\n      },\n      \"ttsAutoPlay\": {\n        \"label\": \"Riproduci automaticamente la risposta vocale dopo il completamento\"\n      },\n      \"ttsProvider\": {\n        \"label\": \"Text-to-Speech Provider\",\n        \"placeholder\": \"Seleziona un provider\"\n      },\n      \"ttsVoice\": {\n        \"label\": \"Text-to-Speech Voce\",\n        \"placeholder\": \"Seleziona una voce\"\n      },\n      \"ssmlEnabled\": {\n        \"label\": \"Abilita SSML (Speech Synthesis Markup Language)\"\n      },\n      \"removeReasoningTagTTS\": {\n        \"label\": \"Rimuovi Tag di Ragionamento dal TTS\"\n      },\n      \"responseSplitting\": {\n        \"label\": \"Suddivisione della risposta\"\n      }\n    },\n    \"stt\": {\n      \"heading\": \"Impostazioni Speech-to-Text\",\n      \"autoStopTimeout\": {\n        \"label\": \"Timeout Arresto Automatico (ms)\",\n        \"placeholder\": \"Inserisci il timeout di arresto automatico in millisecondi\"\n      },\n      \"autoSubmitVoiceMessage\": {\n        \"label\": \"Invio Automatico Messaggio Vocale\"\n      }\n    }\n  },\n  \"manageModels\": {\n    \"title\": \"Gestione Modelli\",\n    \"addBtn\": \"Aggiungi un nuovo Modello\",\n    \"columns\": {\n      \"name\": \"Nome\",\n      \"digest\": \"Digest\",\n      \"modifiedAt\": \"Modificato il\",\n      \"size\": \"Dimensioni\",\n      \"actions\": \"Azioni\",\n      \"nickname\": \"Soprannome\"\n    },\n    \"expandedColumns\": {\n      \"parentModel\": \"Modello Padre\",\n      \"format\": \"Formato\",\n      \"family\": \"Famiglia\",\n      \"parameterSize\": \"Numero di Parametri\",\n      \"quantizationLevel\": \"Livello di Quantizzazione\"\n    },\n    \"tooltip\": {\n      \"delete\": \"Elimina Modello\",\n      \"repull\": \"Ri-Scarica Modello\",\n      \"editNickname\": \"Modifica soprannome\"\n    },\n    \"confirm\": {\n      \"delete\": \"Sei sicuro di voler eliminare questo modello?\",\n      \"repull\": \"Se sicuro che vuoi ri-scaricare questo modello?\"\n    },\n    \"modal\": {\n      \"title\": \"Aggiungi Nuovo Modello\",\n      \"placeholder\": \"Inserisci il Nome Modello\",\n      \"pull\": \"Scarico del Modello\"\n    },\n    \"notification\": {\n      \"pullModel\": \"Scarico del Modello\",\n      \"pullModelDescription\": \"Scaricando il modello {{modelName}}. Per ulteriori dettagli visualizza l'icona dell'estensione.\",\n      \"success\": \"Completato\",\n      \"error\": \"Errore\",\n      \"successDescription\": \"Scarico del modello completato\",\n      \"successDeleteDescription\": \"Eliminazione del modello completato\",\n      \"someError\": \"Qualcosa è andato storto. Riprova più tardi\",\n      \"cancellingDownload\": \"Annullamento del download\",\n      \"cancellingDownloadDescription\": \"Il download del modello è in fase di annullamento...\"\n    }\n  },\n  \"managePrompts\": {\n    \"title\": \"Gestisci Prompts\",\n    \"addBtn\": \"Aggiungi nuovo Prompt\",\n    \"option1\": \"Normale\",\n    \"option2\": \"RAG\",\n    \"questionPrompt\": \"Question Prompt\",\n    \"columns\": {\n      \"title\": \"Titolo\",\n      \"prompt\": \"Prompt\",\n      \"type\": \"Tipo di Prompt\",\n      \"actions\": \"Azioni\"\n    },\n    \"systemPrompt\": \"Prompt di Sistema\",\n    \"quickPrompt\": \"Prompt Veloce\",\n    \"tooltip\": {\n      \"delete\": \"Elimina Prompt\",\n      \"edit\": \"Modifica Prompt\"\n    },\n    \"confirm\": {\n      \"delete\": \"Sei sicuro di voler eliminare questo prompt? L'azione non può essere annullata.\"\n    },\n    \"modal\": {\n      \"addTitle\": \"Aggiungi Nuovo Prompt\",\n      \"editTitle\": \"Modifica Prompt\"\n    },\n    \"segmented\": {\n      \"custom\": \"Prompt personalizzati\",\n      \"copilot\": \"Prompt Copilot\"\n    },\n    \"form\": {\n      \"title\": {\n        \"label\": \"Titolo\",\n        \"placeholder\": \"I Miei Prompt\",\n        \"required\": \"Inserisci il Titolo\"\n      },\n      \"prompt\": {\n        \"label\": \"Prompt\",\n        \"placeholder\": \"Inserisci Prompt\",\n        \"required\": \"Scrivi il prompt\",\n        \"help\": \"Puoi usare {key} come variabile nel tuo prompt.\",\n        \"missingTextPlaceholder\": \"La variabile {text} manca nel prompt. Per favore, aggiungila.\"\n      },\n      \"isSystem\": {\n        \"label\": \"Prompt di Sistema\"\n      },\n      \"btnSave\": {\n        \"saving\": \"Aggiungendo Prompt...\",\n        \"save\": \"Aggiungi Prompt\"\n      },\n      \"btnEdit\": {\n        \"saving\": \"Aggiornando Prompt...\",\n        \"save\": \"Aggiorna Prompt\"\n      }\n    },\n    \"notification\": {\n      \"addSuccess\": \"Prompt Aggiunto\",\n      \"addSuccessDesc\": \"Il Prompt è stato aggiunto correttamente\",\n      \"error\": \"Errore\",\n      \"someError\": \"Qualcosa è andato storto. Riprova più tardi\",\n      \"updatedSuccess\": \"Prompt Aggiornato\",\n      \"updatedSuccessDesc\": \"Il Prompt è stato aggiornato correttmante\",\n      \"deletedSuccess\": \"Prompt Eliminato\",\n      \"deletedSuccessDesc\": \"Il Prompt è stato eliminato correttamente\"\n    }\n  },\n  \"manageShare\": {\n    \"title\": \"Gestione Condivisioni\",\n    \"heading\": \"Configura l'URL della Pagina di Condivisione\",\n    \"form\": {\n      \"url\": {\n        \"label\": \"URL Pagina di Condivisione\",\n        \"placeholder\": \"Inserisci URL Pagina di Condivisione\",\n        \"required\": \"Inserisci l'url della pagina di condivisione!\",\n        \"help\": \"Per ragioni di  privacy, tu puoi ospitare in self-host la paginacon il seguente URL. <anchor>Leggi altro</anchor>.\"\n      }\n    },\n    \"webshare\": {\n      \"heading\": \"Condivisioni Web\",\n      \"columns\": {\n        \"title\": \"Titolo\",\n        \"url\": \"URL\",\n        \"actions\": \"Azioni\"\n      },\n      \"tooltip\": {\n        \"delete\": \"Elimina Condivisione\"\n      },\n      \"confirm\": {\n        \"delete\": \"Sei sicuro che vuoi eliminare questa condivisione? L'azione non può essere annullata.\"\n      },\n      \"label\": \"Gestione Condivisioni\",\n      \"description\": \"Abilita o Disattiva la funzionalità di condivisione\"\n    },\n    \"notification\": {\n      \"pageShareSuccess\": \" URL di condivisione aggiornato correttamente\",\n      \"someError\": \"Qualcosa è andato storto. Riprova più tardi\",\n      \"webShareDeleteSuccess\": \"Condivisione eliminata correttamente\"\n    }\n  },\n  \"ollamaSettings\": {\n    \"title\": \"Impostazioni Ollama\",\n    \"heading\": \"Configura Ollama\",\n    \"settings\": {\n      \"ollamaUrl\": {\n        \"label\": \"Ollama URL\",\n        \"placeholder\": \"Inserici l'URL di Ollama\"\n      },\n      \"globalEnable\": {\n        \"label\": \"Abilita o Disabilita l'Integrazione di Ollama Globalmente\",\n        \"warning\": \"Disabilitando l'integrazione globale di Ollama, Page Assist non recupererà i modelli da Ollama. Puoi comunque aggiungere un'istanza di Ollama dalla sezione <anchor>API compatibile con OpenAI</anchor> che funzionerà correttamente.\"\n      },\n      \"advanced\": {\n        \"label\": \"Configurazione Avanzata Ollama URL\",\n        \"urlRewriteEnabled\": {\n          \"label\": \"Abilita o Disabilita l'URL di Origine Personalizzato\"\n        },\n        \"rewriteUrl\": {\n          \"label\": \"URL di Origine Personalizzato\",\n          \"placeholder\": \"Inserisci URL di Origine Personalizzato\"\n        },\n        \"autoCORSFix\": {\n          \"label\": \"Abilita o Disabilita la Correzione Automatica CORS di Ollama\"\n        },\n        \"headers\": {\n          \"label\": \"Intestazioni Personalizzate\",\n          \"add\": \"Aggiungi Intestazione\",\n          \"key\": {\n            \"label\": \"Chiave dell'Intestazione\",\n            \"placeholder\": \"Autorizzazione\"\n          },\n          \"value\": {\n            \"label\": \"Valore dell'Intestazione\",\n            \"placeholder\": \"Token Bearer\"\n          }\n        },\n        \"help\": \"Se hai problemi di connessione con Ollama su Page Assist, puoi configurare un URL di origine personalizzato. Per saperne di più sulla configurazione, <anchor>clicca qui</anchor>.\"\n      }\n    }\n  },\n  \"manageSearch\": {\n    \"title\": \"Gestisci Ricerca Web\",\n    \"heading\": \"Configura Ricerca Web\"\n  },\n  \"about\": {\n    \"title\": \"Informazioni\",\n    \"heading\": \"Informazioni\",\n    \"chromeVersion\": \"Versione di Page Assist\",\n    \"ollamaVersion\": \"Versione di Ollama\",\n    \"support\": \"Puoi supportare il progetto Page Assist donando o sponsorizzando attraverso le seguenti piattaforme:\",\n    \"koFi\": \"Supporta su Ko-fi\",\n    \"githubSponsor\": \"Sponsorizza su GitHub\",\n    \"githubRepo\": \"Repository GitHub\"\n  },\n  \"manageKnowledge\": {\n    \"title\": \"Gestisci Conoscenza\",\n    \"heading\": \"Configura Base di Conoscenza\"\n  },\n  \"rag\": {\n    \"title\": \"Impostazioni Pipeline\",\n    \"ragSettings\": {\n      \"label\": \"Impostazioni RAG\",\n      \"model\": {\n        \"label\": \"Modello di Embedding\",\n        \"required\": \"Scegliere il modello\",\n        \"help\": \"E' raccomandato l'uso di modelli come `nomic-embed-text`.\",\n        \"placeholder\": \"Seleziona un modello\"\n      },\n      \"chunkSize\": {\n        \"label\": \"Dimensione del Blocco (Chunk Size)\",\n        \"placeholder\": \"Inserisci la Dimensione del Blocco (Chunk Size)\",\n        \"required\": \"Inserisci la Dimensione del Blocco (chunk size)\"\n      },\n      \"chunkOverlap\": {\n        \"label\": \"Sovrapposizione del Blocco (Chunk Overlap)\",\n        \"placeholder\": \"Inserisci la Sovrapposizione del Blocco (Chunk Overlap)\",\n        \"required\": \"Inserisci la Sovrapposizione del Blocco\"\n      },\n      \"totalFilePerKB\": {\n        \"label\": \"Limite Predefinito di Caricamento File per la Base di Conoscenza\",\n        \"placeholder\": \"Inserisci il limite predefinito di caricamento file (es. 10)\",\n        \"required\": \"Inserisci il limite predefinito di caricamento file\"\n      },\n      \"noOfRetrievedDocs\": {\n        \"label\": \"Numero di Documenti Recuperati\",\n        \"placeholder\": \"Inserisci il Numero di Documenti Recuperati\",\n        \"required\": \"Inserisci il numero di documenti recuperati\"\n      },\n      \"splittingSeparator\": {\n        \"label\": \"Separatore\",\n        \"placeholder\": \"Inserisci il Separatore (es. \\\\n\\\\n)\",\n        \"required\": \"Inserisci un separatore\"\n      },\n      \"splittingStrategy\": {\n        \"label\": \"Divisore di Testo\"\n      }\n    },\n    \"prompt\": {\n      \"label\": \"Configura il Prompt RAG\",\n      \"option1\": \"Normale\",\n      \"option2\": \"Web\",\n      \"alert\": \"La configurazione del prompt di sistema qui è deprecato. Usa la sezione Gestione Prompt per aggiungere o modificare i prompts.Questa sezione sarà eliminata nelle prossime release\",\n      \"systemPrompt\": \"Prompt di Sistema\",\n      \"systemPromptPlaceholder\": \"Inserisci il Prompt di Sistema\",\n      \"webSearchPrompt\": \"Prompt per la Ricerca Web\",\n      \"webSearchPromptHelp\": \"Non rimuovere `{search_results}` dal prompt.\",\n      \"webSearchPromptError\": \"Inserisci il prompt per la ricerca web\",\n      \"webSearchPromptPlaceholder\": \"Imserosco il Prompt per la Ricerca Web\",\n      \"webSearchFollowUpPrompt\": \"Prompt di Follow Up sulla Ricerca Web\",\n      \"webSearchFollowUpPromptHelp\": \"Non rimuovere `{chat_history}` e `{question}` dal prompt.\",\n      \"webSearchFollowUpPromptError\": \"Inserisci il Prompt di Follow Up della Ricerca Web!\",\n      \"webSearchFollowUpPromptPlaceholder\": \"I tuoi Prompt di Follow Up delle Ricerche Web\"\n    }\n  },\n  \"chromeAiSettings\": {\n    \"title\": \"Impostazioni IA di Chrome\"\n  },\n  \"mermaid\": \"Mermaid\"\n}\n"
  },
  {
    "path": "src/assets/locale/it/sidepanel.json",
    "content": "{\n    \"tooltip\": {\n        \"embed\": \"L'inserimento della pagina potrebbe richiedere alcuni minuti. Attendere prego...\",\n        \"clear\": \"Cancella la cronologia della chat\",\n        \"history\": \"Cronologia della chat\",\n        \"openwebui\": \"Apri WebUI\"\n    }\n}"
  },
  {
    "path": "src/assets/locale/ja-JP/chrome.json",
    "content": "{\n    \"heading\": \"Chrome AIの設定\",\n    \"status\": {\n        \"label\": \"Page AssistでChrome AIサポートを有効または無効にする\"\n    },\n    \"error\": {\n        \"browser_not_supported\": \"このバージョンのChromeはGemini Nanoモデルに対応していません。バージョン127以降に更新してください。\",\n        \"ai_not_supported\": \"設定chrome://flags/#prompt-api-for-gemini-nanoが有効になっていません。有効にしてください。\",\n        \"ai_not_ready\": \"Gemini Nanoはまだ準備ができていません。Chromeの設定を再確認する必要があります。\",\n        \"internal_error\": \"内部エラーが発生しました。後でもう一度お試しください。\"\n    },\n    \"errorDescription\": \"Chrome AIを使用するには、Chromeバージョン138以降が必要です。以下の手順に従ってください:\\n\\n1. `chrome://flags/#prompt-api-for-gemini-nano`にアクセスし、「Prompt API for Gemini Nano」を有効にします。\\n2. フラグを適用するためにChromeを再起動します。\\n3. このページに戻り、「モデルをダウンロード」をクリックします — 初回は4GBのモデルがダウンロードされます。\\n4. ダウンロードが完了すると、Gemini NanoをPage Assistを通じて有効にできます。\",\n    \"downloadModel\": \"モデルをダウンロード\",\n    \"modelDownloadWarning\": \"これは、約1.5GBから2.4GBの範囲のダウンロードサイズを持つモデルをダウンロードします。十分なディスクスペースがあることを確認してください。\"\n}"
  },
  {
    "path": "src/assets/locale/ja-JP/common.json",
    "content": "{\n  \"pageAssist\": \"ページアシスト\",\n  \"selectAModel\": \"モデルを選択\",\n  \"save\": \"保存\",\n  \"saved\": \"保存済み\",\n  \"cancel\": \"キャンセル\",\n  \"retry\": \"再試行\",\n  \"share\": {\n    \"tooltip\": {\n      \"share\": \"共有\"\n    },\n    \"modal\": {\n      \"title\": \"チャットリンクを共有\"\n    },\n    \"form\": {\n      \"defaultValue\": {\n        \"name\": \"匿名\",\n        \"title\": \"無題のチャット\"\n      },\n      \"title\": {\n        \"label\": \"チャットタイトル\",\n        \"placeholder\": \"チャットタイトルを入力\",\n        \"required\": \"チャットタイトルは必須です\"\n      },\n      \"name\": {\n        \"label\": \"あなたの名前\",\n        \"placeholder\": \"名前を入力\",\n        \"required\": \"名前は必須です\"\n      },\n      \"btn\": {\n        \"save\": \"リンクを生成\",\n        \"saving\": \"リンクを生成中...\"\n      }\n    },\n    \"notification\": {\n      \"successGenerate\": \"リンクがクリップボードにコピーされました\",\n      \"failGenerate\": \"リンクの生成に失敗しました\"\n    }\n  },\n  \"copyToClipboard\": \"クリップボードにコピー\",\n  \"webSearch\": \"ウェブを検索中\",\n  \"regenerate\": \"再生成\",\n  \"continue\": \"続ける\",\n  \"edit\": \"編集\",\n  \"delete\": \"削除\",\n  \"saveAndSubmit\": \"保存して送信\",\n  \"editMessage\": {\n    \"placeholder\": \"メッセージを入力...\"\n  },\n  \"submit\": \"送信\",\n  \"noData\": \"データがありません\",\n  \"noHistory\": \"チャット履歴がありません\",\n  \"chatWithCurrentPage\": \"現在のページでチャット\",\n  \"beta\": \"ベータ\",\n  \"tts\": \"読み上げ\",\n  \"currentChatModelSettings\": \"現在のチャットモデル設定\",\n  \"modelSettings\": {\n    \"label\": \"モデル設定\",\n    \"description\": \"すべてのチャットに対してモデルオプションをグローバルに設定します\",\n    \"form\": {\n      \"keepAlive\": {\n        \"label\": \"キープアライブ\",\n        \"help\": \"リクエスト後にモデルがメモリに保持される時間をコントロールします（デフォルト: 5 分）\",\n        \"placeholder\": \"キープアライブの期間を入力してください（例：5分、10分、1時間）\"\n      },\n      \"temperature\": {\n        \"label\": \"温度\",\n        \"placeholder\": \"温度値を入力してください（例：0.7、1.0）\"\n      },\n      \"numCtx\": {\n        \"label\": \"コンテキストウィンドウサイズ (num_ctx)\",\n        \"placeholder\": \"コンテキストウィンドウサイズを入力してください（デフォルト：2048）\"\n      },\n      \"numPredict\": {\n        \"label\": \"最大トークン数 (num_predict)\",\n        \"placeholder\": \"最大トークン数を入力してください（例：2048、4096）\"\n      },\n      \"seed\": {\n        \"label\": \"シード\",\n        \"placeholder\": \"シード値を入力してください（例：1234）\",\n        \"help\": \"モデル出力の再現性\"\n      },\n      \"topK\": {\n        \"label\": \"Top K\",\n        \"placeholder\": \"Top K値を入力してください（例：40、100）\"\n      },\n      \"topP\": {\n        \"label\": \"Top P\",\n        \"placeholder\": \"Top P値を入力してください（例：0.9、0.95）\"\n      },\n      \"numGpu\": {\n        \"label\": \"Num GPU\",\n        \"placeholder\": \"GPU(s)に送信するレイヤー数を入力してください\"\n      },\n      \"systemPrompt\": {\n        \"label\": \"一時的なシステムプロンプト\",\n        \"placeholder\": \"システムプロンプトを入力\",\n        \"help\": \"これは現在のチャットでシステムプロンプトを素早く設定する方法で、選択されたシステムプロンプトが存在する場合はそれを上書きします。\"\n      }\n    },\n    \"advanced\": \"その他のモデル設定\"\n  },\n  \"copilot\": {\n    \"summary\": \"要約\",\n    \"explain\": \"説明\",\n    \"rephrase\": \"言い換え\",\n    \"translate\": \"翻訳\"\n  },\n  \"citations\": \"引用\",\n  \"downloadCode\": \"コードをダウンロード\",\n  \"date\": {\n    \"pinned\": \"固定\",\n    \"today\": \"今日\",\n    \"yesterday\": \"昨日\",\n    \"last7Days\": \"過去7日間\",\n    \"older\": \"それ以前\"\n  },\n  \"range\": {\n    \"deleteConfirm\": {\n      \"pinned\": \"固定されたメッセージをすべて削除してもよろしいですか？\",\n      \"today\": \"今日のメッセージをすべて削除してもよろしいですか？\",\n      \"yesterday\": \"昨日のメッセージをすべて削除してもよろしいですか？\",\n      \"last7Days\": \"過去7日間のメッセージをすべて削除してもよろしいですか？\",\n      \"older\": \"それ以前のメッセージをすべて削除してもよろしいですか？\"\n    },\n    \"tooltip\": {\n      \"pinned\": \"固定されたメッセージをすべて削除\",\n      \"today\": \"今日のメッセージをすべて削除\",\n      \"yesterday\": \"昨日のメッセージをすべて削除\",\n      \"last7Days\": \"過去7日間のメッセージをすべて削除\",\n      \"older\": \"それ以前のメッセージをすべて削除\"\n    }\n  },\n  \"pin\": \"固定\",\n  \"unpin\": \"固定解除\",\n  \"generationInfo\": \"生成情報\",\n  \"sidebarChat\": \"サイドバーチャット\",\n  \"reasoning\": {\n    \"thinking\": \"考え中....\",\n    \"thought\": \"{{time}}の思考\"\n  },\n  \"embeddingGen\": \"埋め込みを作成中です。時間がかかる場合があります\",\n  \"semanticSearch\": \"意味検索を実行中\",\n  \"downloading\": \"ダウンロード中\",\n  \"cancelPullingModel\": {\n    \"confirm\": \"ダウンロードをキャンセルしてもよろしいですか？これによりダウンロードプロセスが停止します。Ollamaのドキュメントによると、中断した場所から再開できます。\"\n  }\n}"
  },
  {
    "path": "src/assets/locale/ja-JP/knowledge.json",
    "content": "{\n    \"addBtn\": \"新しい知識を追加\",\n    \"columns\": {\n        \"title\": \"タイトル\",\n        \"status\": \"ステータス\",\n        \"embeddings\": \"埋め込みモデル\",\n        \"createdAt\": \"作成日\",\n        \"action\": \"アクション\"\n    },\n    \"expandedColumns\": {\n        \"name\": \"名前\"\n    },\n    \"confirm\": {\n        \"delete\": \"この知識を削除してもよろしいですか?\"\n    },\n    \"deleteSuccess\": \"知識が正常に削除されました\",\n    \"status\": {\n        \"pending\": \"保留中\",\n        \"finished\": \"完了\",\n        \"processing\": \"処理中\",\n        \"failed\": \"失敗\"\n    },\n    \"addKnowledge\": \"知識を追加\",\n    \"updateKnowledge\": \"ソースを追加\",\n    \"form\": {\n        \"title\": {\n            \"label\": \"知識タイトル\",\n            \"placeholder\": \"知識のタイトルを入力してください\",\n            \"required\": \"知識のタイトルは必須です\"\n        },\n        \"uploadFile\": {\n            \"label\": \"ファイルをアップロード\",\n            \"uploadText\": \"ファイルをここにドラッグアンドドロップするか、クリックしてアップロード\",\n            \"uploadHint\": \"サポートされているファイルタイプ: .pdf、.csv、.txt\",\n            \"required\": \"ファイルは必須です\"\n        },\n        \"submit\": \"送信\",\n        \"success\": \"知識が正常に追加されました\"\n    },\n    \"noEmbeddingModel\": \"最初にRAGの設定ページから埋め込みモデルを追加してください\"\n}"
  },
  {
    "path": "src/assets/locale/ja-JP/openai.json",
    "content": "{\n    \"settings\": \"OpenAI互換API\",\n    \"heading\": \"OpenAI互換API\",\n    \"subheading\": \"OpenAI API互換プロバイダーの管理と設定はこちらで行います。\",\n    \"addBtn\": \"プロバイダーを追加\",\n    \"table\": {\n        \"name\": \"プロバイダー名\",\n        \"baseUrl\": \"ベースURL\",\n        \"actions\": \"アクション\"\n    },\n    \"modal\": {\n        \"titleAdd\": \"新規プロバイダーを追加\",\n        \"name\": {\n            \"label\": \"プロバイダー名\",\n            \"required\": \"プロバイダー名は必須です。\",\n            \"placeholder\": \"プロバイダー名を入力\"\n        },\n        \"baseUrl\": {\n            \"label\": \"ベースURL\",\n            \"help\": \"OpenAI APIプロバイダーのベースURL。例：(http://localhost:1234/v1)\",\n            \"required\": \"ベースURLは必須です。\",\n            \"placeholder\": \"ベースURLを入力\"\n        },\n        \"apiKey\": {\n            \"label\": \"APIキー\",\n            \"required\": \"APIキーは必須です。\",\n            \"placeholder\": \"APIキーを入力\"\n        },\n        \"submit\": \"保存\",\n        \"update\": \"更新\",\n        \"deleteConfirm\": \"このプロバイダーを削除してもよろしいですか？\",\n        \"model\": {\n            \"title\": \"モデルリスト\",\n            \"subheading\": \"このプロバイダーで使用したいチャットモデルを選択してください。\",\n            \"success\": \"新しいモデルが正常に追加されました。\"\n        },\n        \"tipLMStudio\": \"Page AssistはLM Studioにロードしたモデルを自動的に取得します。手動で追加する必要はありません。\"\n    },\n    \"addSuccess\": \"プロバイダーが正常に追加されました。\",\n    \"deleteSuccess\": \"プロバイダーが正常に削除されました。\",\n    \"updateSuccess\": \"プロバイダーが正常に更新されました。\",\n    \"delete\": \"削除\",\n    \"edit\": \"編集\",\n    \"newModel\": \"プロバイダーにモデルを追加\",\n    \"noNewModel\": \"LMStudio, Ollama, Llamafile,の場合、動的に取得します。手動での追加は不要です。\",\n    \"searchModel\": \"モデルを検索\",\n    \"selectAll\": \"すべて選択\",\n    \"save\": \"保存\",\n    \"saving\": \"保存中...\",\n    \"manageModels\": {\n        \"columns\": {\n            \"name\": \"モデル名\",\n            \"model_type\": \"モデルタイプ\",\n            \"model_id\": \"モデルID\",\n            \"provider\": \"プロバイダー名\",\n            \"actions\": \"アクション\",\n            \"nickname\": \"モデルのニックネーム\"\n        },\n        \"tooltip\": {\n            \"delete\": \"削除\"\n        },\n        \"confirm\": {\n            \"delete\": \"このモデルを削除してもよろしいですか？\"\n        },\n        \"modal\": {\n            \"title\": \"カスタムモデルを追加\",\n            \"form\": {\n                \"name\": {\n                    \"label\": \"モデルID\",\n                    \"placeholder\": \"llama3.2\",\n                    \"required\": \"モデルIDは必須です。\"\n                },\n                \"provider\": {\n                    \"label\": \"プロバイダー\",\n                    \"placeholder\": \"プロバイダーを選択\",\n                    \"required\": \"プロバイダーは必須です。\"\n                },\n                \"type\": {\n                    \"label\": \"モデルタイプ\"\n                }\n            }\n        }\n    },\n    \"noModelFound\": \"モデルが見つかりません。正しいベースURLとAPIキーを持つプロバイダーを追加したことを確認してください。\",\n    \"radio\": {\n        \"chat\": \"チャットモデル\",\n        \"embedding\": \"埋め込みモデル\",\n        \"chatInfo\": \"はチャット完了と会話生成に使用されます\",\n        \"embeddingInfo\": \"はRAGやその他の意味検索関連タスクに使用されます。\"\n    },\n    \"nicknameModal\": {\n        \"title\": \"モデルのニックネームを追加/編集\",\n        \"form\": {\n            \"modelName\": {\n                \"label\": \"モデル名\",\n                \"placeholder\": \"モデル名を入力\",\n                \"required\": \"モデル名は必須です。\"\n            },\n            \"modelAvatar\": {\n                \"label\": \"モデルのアバター\",\n                \"placeholder\": \"モデルのアバターを入力\",\n                \"help\": \"モデルのアバターのURLを入力してください。この画像はチャットウィンドウに表示されます。\"\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/assets/locale/ja-JP/option.json",
    "content": "{\n  \"newChat\": \"新しいチャット\",\n  \"selectAPrompt\": \"プロンプトを選択\",\n  \"githubRepository\": \"GitHubリポジトリ\",\n  \"settings\": \"設定\",\n  \"sidebarTitle\": \"チャット履歴\",\n  \"error\": \"エラー\",\n  \"somethingWentWrong\": \"何かが間違っています\",\n  \"validationSelectModel\": \"続行するにはモデルを選択してください\",\n  \"deleteHistoryConfirmation\": \"この履歴を削除しますか?\",\n  \"editHistoryTitle\": \"新しいタイトルを入力\",\n  \"temporaryChat\": \"一時的なチャット\",\n  \"more\": {\n    \"copy\": {\n      \"group\": \"コピー\",\n      \"asText\": \"テキストとしてコピー\",\n      \"asMarkdown\": \"Markdownとしてコピー\",\n      \"success\": \"クリップボードにコピーしました！\"\n    },\n    \"download\": {\n      \"group\": \"ダウンロード\",\n      \"text\": \"テキストファイル (.txt)\",\n      \"markdown\": \"Markdownファイル (.md)\",\n      \"json\": \"JSONファイル (.json)\"\n    },\n    \"share\": \"共有\"\n  }\n}"
  },
  {
    "path": "src/assets/locale/ja-JP/playground.json",
    "content": "{\n    \"ollamaState\": {\n        \"searching\": \"Ollamaを検索中 🦙\",\n        \"running\": \"Ollamaが実行中 🦙\",\n        \"notRunning\": \"Ollamaに接続できません 🦙\",\n        \"connectionError\": \"接続エラーが発生しているようです。トラブルシューティングについては<anchor>ドキュメント</anchor>をご覧ください。\"\n    },\n    \"formError\": {\n        \"noModel\": \"モデルを選択してください\",\n        \"noEmbeddingModel\": \"設定 > RAGページでembeddingモデルを設定してください\"\n    },\n    \"form\": {\n        \"textarea\": {\n            \"placeholder\": \"メッセージを入力...\"\n        },\n        \"webSearch\": {\n            \"on\": \"オン\",\n            \"off\": \"オフ\"\n        }\n    },\n    \"tooltip\": {\n        \"searchInternet\": \"インターネットを検索\",\n        \"speechToText\": \"音声入力\",\n        \"uploadImage\": \"画像をアップロード\",\n        \"stopStreaming\": \"ストリーミングを停止\",\n        \"knowledge\": \"知識\",\n        \"clearContext\": \"コンテキストをクリア\"\n    },\n    \"sendWhenEnter\": \"Enterキーを押すと送信\",\n    \"welcome\": \"こんにちは！本日はどのようなお手伝いができますか？\",\n    \"useOCR\": \"画像からテキストを抽出（OCR）\"\n}"
  },
  {
    "path": "src/assets/locale/ja-JP/settings.json",
    "content": "{\n  \"generalSettings\": {\n    \"title\": \"一般設定\",\n    \"settings\": {\n      \"heading\": \"Web UIの設定\",\n      \"speechRecognitionLang\": {\n        \"label\": \"音声認識の言語\",\n        \"placeholder\": \"言語を選択\"\n      },\n      \"language\": {\n        \"label\": \"言語\",\n        \"placeholder\": \"言語を選択\"\n      },\n      \"darkMode\": {\n        \"label\": \"テーマを変更\",\n        \"options\": {\n          \"light\": \"ライト\",\n          \"dark\": \"ダーク\"\n        }\n      },\n      \"searchMode\": {\n        \"label\": \"簡易インターネット検索を実行\"\n      },\n      \"copilotResumeLastChat\": {\n        \"label\": \"サイドパネルを開いたときに最後のチャットを再開 (Copilot)\"\n      },\n      \"turnOnChatWithWebsite\": {\n        \"label\": \"デフォルトでウェブサイトとのチャットを有効にする (Copilot)\"\n      },\n      \"webUIResumeLastChat\": {\n        \"label\": \"Web UIを開いたときに最後のチャットを再開\"\n      },\n      \"hideCurrentChatModelSettings\": {\n        \"label\": \"現在のチャットモデル設定を非表示\"\n      },\n      \"restoreLastChatModel\": {\n        \"label\": \"以前のチャットで最後に使用したモデルを復元する\"\n      },\n      \"sendNotificationAfterIndexing\": {\n        \"label\": \"ナレッジベースの処理完了後に通知を送信\"\n      },\n      \"generateTitle\": {\n        \"label\": \"AIを使用してタイトルを生成\"\n      },\n      \"ollamaStatus\": {\n        \"label\": \"Ollamaの接続状態チェックを有効または無効にする\"\n      },\n      \"wideMode\": {\n        \"label\": \"ワイドスクリーンモードを有効にする\"\n      },\n      \"openReasoning\": {\n        \"label\": \"デフォルトで推論を展開する\"\n      },\n      \"userChatBubble\": {\n        \"label\": \"ユーザーメッセージにチャットバブルを使用\"\n      },\n      \"autoCopyResponseToClipboard\": {\n        \"label\": \"応答を自動的にクリップボードにコピー\"\n      },\n      \"useMarkdownForUserMessage\": {\n        \"label\": \"ユーザーメッセージのMarkdown形式を有効にする\"\n      },\n      \"copyAsFormattedText\": {\n        \"label\": \"書式付きテキストとしてコピー\"\n      },\n      \"tabMentionsEnabled\": {\n        \"label\": \"タブメンション (@tab) を有効にする\"\n      },\n      \"pasteLargeTextAsFile\": {\n        \"label\": \"大きなテキストをファイルとして貼り付け\"\n      },\n      \"sidepanelTemporaryChat\": {\n        \"label\": \"サイドパネルでの一時チャットをデフォルトで有効にする\"\n      },\n      \"defaultCopilotPrompt\": {\n        \"label\": \"サイドパネル (Copilot) のデフォルトプロンプト\",\n        \"placeholder\": \"プロンプトを選択\"\n      },\n      \"defaultThinkingMode\": {\n        \"label\": \"フォームに思考モードの状態を表示\"\n      },\n      \"defaultWebUIPrompt\": {\n        \"label\": \"Web UI のデフォルトプロンプト\",\n        \"placeholder\": \"プロンプトを選択\"\n      },\n      \"enableMessageQueue\": {\n        \"label\": \"ストリーミング中にメッセージキューを有効にする\"\n      },\n      \"hideReasoningWidget\": {\n        \"label\": \"AIメッセージから推論ウィジェットを非表示にする\"\n      },\n      \"ocrLanguage\": {\n        \"label\": \"デフォルトOCR言語\",\n        \"placeholder\": \"OCR言語を選択\"\n      },\n      \"optimizeQueueForSmallScreen\": {\n        \"label\": \"小さな画面向けにチャットUIを最適化する\"\n      },\n      \"persistChatInput\": {\n        \"label\": \"チャット入力を保持する（未送信メッセージを保存）\"\n      },\n      \"removeReasoningTagFromCopy\": {\n        \"label\": \"コピーしたテキストから推論タグを削除\"\n      },\n      \"showMoreForLargeMessage\": {\n        \"label\": \"長いユーザーメッセージに「もっと見る」を表示\"\n      },\n      \"sidebarPosition\": {\n        \"label\": \"サイドバーの位置\",\n        \"options\": {\n          \"left\": \"左\",\n          \"right\": \"右\"\n        }\n      },\n      \"tableTextWrap\": {\n        \"label\": \"Markdown テーブルでテキストの折り返しを有効にする\"\n      },\n      \"webuiTemporaryChat\": {\n        \"label\": \"Web UIで一時チャットをデフォルトで有効にする\"\n      },\n      \"youtubeAutoSummarize\": {\n        \"label\": \"YouTube動画に「要約」ボタンを表示する。\"\n      }\n    },\n    \"sidepanelRag\": {\n      \"heading\": \"検索設定\",\n      \"ragEnabled\": {\n        \"label\": \"埋め込みと検索を有効にする\"\n      },\n      \"maxWebsiteContext\": {\n        \"label\": \"フルコンテキストモードの最大コンテンツサイズ\",\n        \"placeholder\": \"コンテンツサイズ（デフォルト4028）\"\n      }\n    },\n    \"webSearch\": {\n      \"heading\": \"ウェブ検索を管理する\",\n      \"searchMode\": {\n        \"label\": \"簡単なインターネット検索を実行する\"\n      },\n      \"provider\": {\n        \"label\": \"検索エンジン\",\n        \"placeholder\": \"検索エンジンを選択する\"\n      },\n      \"totalSearchResults\": {\n        \"label\": \"合計検索結果\",\n        \"placeholder\": \"合計検索結果を入力する\"\n      },\n      \"visitSpecificWebsite\": {\n        \"label\": \"メッセージに記載されたウェブサイトを訪問してください\"\n      },\n      \"searxng\": {\n        \"url\": {\n          \"label\": \"SearXNG URL\"\n        }\n      },\n      \"braveApi\": {\n        \"label\": \"Brave APIキー\",\n        \"placeholder\": \"Brave APIキーを入力してください\"\n      },\n      \"searchOnByDefault\": {\n        \"label\": \"デフォルトでインターネット検索をオンにする\"\n      },\n      \"blockedDomains\": {\n        \"description\": \"これらのドメインの結果を除外\",\n        \"label\": \"ブロックするドメイン\",\n        \"placeholder\": \"例: spam.com\"\n      },\n      \"domainFilter\": {\n        \"description\": \"これらのドメインの結果のみ表示\",\n        \"label\": \"ドメインフィルター一覧\",\n        \"placeholder\": \"例: example.com\"\n      },\n      \"exa\": {\n        \"label\": \"Exa APIキー\",\n        \"placeholder\": \"Exa APIキーを入力してください\"\n      },\n      \"firecrawlAPIKey\": {\n        \"label\": \"Firecrawl APIキー\",\n        \"placeholder\": \"Firecrawl APIキーを入力してください\"\n      },\n      \"googleDomain\": {\n        \"label\": \"Google ドメイン\"\n      },\n      \"tavilyApi\": {\n        \"label\": \"Tavily APIキー\",\n        \"placeholder\": \"Tavily APIキーを入力してください\"\n      }\n    },\n    \"system\": {\n      \"heading\": \"システム設定\",\n      \"storageSyncEnabled\": {\n        \"label\": \"ブラウザストレージ同期を有効にする（デバイス間で設定を同期）\"\n      },\n      \"deleteChatHistory\": {\n        \"label\": \"システムリセット\",\n        \"button\": \"すべてリセット\",\n        \"confirm\": \"システムリセットを実行してもよろしいですか？すべてのデータが消去され、元に戻すことはできません。\"\n      },\n      \"export\": {\n        \"label\": \"すべてのデータをエクスポート (チャット履歴、ナレッジベース、プロンプト、設定)\",\n        \"button\": \"データをエクスポート\",\n        \"success\": \"エクスポート成功\"\n      },\n      \"import\": {\n        \"label\": \"すべてのデータをインポート (チャット履歴、ナレッジベース、プロンプト、設定)\",\n        \"button\": \"データをインポート\",\n        \"success\": \"インポート成功\",\n        \"error\": \"インポートエラー\"\n      },\n      \"actionIcon\": {\n        \"label\": \"拡張機能アイコンクリック時のデフォルト動作を設定\"\n      },\n      \"chatBackgroundImage\": {\n        \"label\": \"チャット背景画像\"\n      },\n      \"contextMenu\": {\n        \"label\": \"コンテキストメニューのデフォルト動作を設定\"\n      },\n      \"fontSize\": {\n        \"label\": \"フォントサイズ\"\n      },\n      \"webuiBtnSidePanel\": {\n        \"label\": \"サイドパネルに Web UI ボタンを表示\"\n      }\n    },\n    \"tts\": {\n      \"heading\": \"テキスト読み上げ設定\",\n      \"ttsEnabled\": {\n        \"label\": \"テキスト読み上げを有効にする\"\n      },\n      \"ttsAutoPlay\": {\n        \"label\": \"応答完了後に音声を自動再生\"\n      },\n      \"ttsProvider\": {\n        \"label\": \"テキスト読み上げプロバイダー\",\n        \"placeholder\": \"プロバイダーを選択\"\n      },\n      \"ttsVoice\": {\n        \"label\": \"テキスト読み上げの音声\",\n        \"placeholder\": \"音声を選択\"\n      },\n      \"ssmlEnabled\": {\n        \"label\": \"SSML (Speech Synthesis Markup Language) を有効にする\"\n      },\n      \"removeReasoningTagTTS\": {\n        \"label\": \"テキスト読み上げから推論タグを削除\"\n      },\n      \"responseSplitting\": {\n        \"label\": \"応答の分割\"\n      }\n    },\n    \"stt\": {\n      \"heading\": \"音声認識設定\",\n      \"autoStopTimeout\": {\n        \"label\": \"自動停止タイムアウト (ミリ秒)\",\n        \"placeholder\": \"自動停止タイムアウトをミリ秒単位で入力\"\n      },\n      \"autoSubmitVoiceMessage\": {\n        \"label\": \"音声メッセージを自動送信\"\n      }\n    }\n  },\n  \"manageModels\": {\n    \"title\": \"モデルを管理\",\n    \"addBtn\": \"新しいモデルを追加\",\n    \"columns\": {\n      \"name\": \"名前\",\n      \"digest\": \"ダイジェスト\",\n      \"modifiedAt\": \"修正日時\",\n      \"size\": \"サイズ\",\n      \"actions\": \"アクション\",\n      \"nickname\": \"ニックネーム\"\n    },\n    \"expandedColumns\": {\n      \"parentModel\": \"親モデル\",\n      \"format\": \"フォーマット\",\n      \"family\": \"ファミリー\",\n      \"parameterSize\": \"パラメータサイズ\",\n      \"quantizationLevel\": \"量子化レベル\"\n    },\n    \"tooltip\": {\n      \"delete\": \"モデルを削除\",\n      \"repull\": \"モデルを再取得\",\n      \"editNickname\": \"ニックネームを編集\"\n    },\n    \"confirm\": {\n      \"delete\": \"本当にこのモデルを削除しますか？\",\n      \"repull\": \"本当にこのモデルを再取得しますか？\"\n    },\n    \"modal\": {\n      \"title\": \"新しいモデルを追加\",\n      \"placeholder\": \"モデル名を入力\",\n      \"pull\": \"モデルを取得\"\n    },\n    \"notification\": {\n      \"pullModel\": \"モデルを取得中\",\n      \"pullModelDescription\": \"{{modelName}}モデルを取得中。詳細は拡張機能のアイコンをご確認ください。\",\n      \"success\": \"成功\",\n      \"error\": \"エラー\",\n      \"successDescription\": \"モデルの取得が完了しました\",\n      \"successDeleteDescription\": \"モデルの削除が完了しました\",\n      \"someError\": \"問題が発生しました。後ほど再度お試しください。\",\n      \"cancellingDownload\": \"ダウンロードをキャンセル中\",\n      \"cancellingDownloadDescription\": \"モデルのダウンロードをキャンセルしています...\"\n    }\n  },\n  \"managePrompts\": {\n    \"title\": \"プロンプトを管理\",\n    \"addBtn\": \"新しいプロンプトを追加\",\n    \"option1\": \"通常\",\n    \"option2\": \"RAG\",\n    \"questionPrompt\": \"質問プロンプト\",\n    \"columns\": {\n      \"title\": \"タイトル\",\n      \"prompt\": \"プロンプト\",\n      \"type\": \"プロンプトタイプ\",\n      \"actions\": \"アクション\"\n    },\n    \"systemPrompt\": \"システムプロンプト\",\n    \"quickPrompt\": \"クイックプロンプト\",\n    \"tooltip\": {\n      \"delete\": \"プロンプトを削除\",\n      \"edit\": \"プロンプトを編集\"\n    },\n    \"confirm\": {\n      \"delete\": \"本当にこのプロンプトを削除しますか？この操作は元に戻せません。\"\n    },\n    \"modal\": {\n      \"addTitle\": \"新しいプロンプトを追加\",\n      \"editTitle\": \"プロンプトを編集\"\n    },\n    \"segmented\": {\n      \"custom\": \"カスタムプロンプト\",\n      \"copilot\": \"Copilotプロンプト\"\n    },\n    \"form\": {\n      \"title\": {\n        \"label\": \"タイトル\",\n        \"placeholder\": \"素晴らしいプロンプト\",\n        \"required\": \"タイトルを入力してください\"\n      },\n      \"prompt\": {\n        \"label\": \"プロンプト\",\n        \"placeholder\": \"プロンプトを入力\",\n        \"required\": \"プロンプトを入力してください\",\n        \"help\": \"プロンプト内で{key}を変数として使用できます。\",\n        \"missingTextPlaceholder\": \"プロンプトに{text}変数がありません。追加してください。\"\n      },\n      \"isSystem\": {\n        \"label\": \"システムプロンプト\"\n      },\n      \"btnSave\": {\n        \"saving\": \"プロンプトを追加中...\",\n        \"save\": \"プロンプトを追加\"\n      },\n      \"btnEdit\": {\n        \"saving\": \"プロンプトを更新中...\",\n        \"save\": \"プロンプトを更新\"\n      }\n    },\n    \"notification\": {\n      \"addSuccess\": \"プロンプトが追加されました\",\n      \"addSuccessDesc\": \"プロンプトが正常に追加されました\",\n      \"error\": \"エラー\",\n      \"someError\": \"問題が発生しました。後ほど再度お試しください。\",\n      \"updatedSuccess\": \"プロンプトが更新されました\",\n      \"updatedSuccessDesc\": \"プロンプトが正常に更新されました\",\n      \"deletedSuccess\": \"プロンプトが削除されました\",\n      \"deletedSuccessDesc\": \"プロンプトが正常に削除されました\"\n    }\n  },\n  \"manageShare\": {\n    \"title\": \"共有を管理\",\n    \"heading\": \"ページ共有URLを設定\",\n    \"form\": {\n      \"url\": {\n        \"label\": \"ページ共有URL\",\n        \"placeholder\": \"ページ共有URLを入力\",\n        \"required\": \"ページ共有URLを入力してください！\",\n        \"help\": \"プライバシー保護のため、ページ共有を自身でホストし、そのURLをここに入力することができます。<anchor>詳細</anchor>\"\n      }\n    },\n    \"webshare\": {\n      \"heading\": \"ウェブ共有\",\n      \"columns\": {\n        \"title\": \"タイトル\",\n        \"url\": \"URL\",\n        \"actions\": \"アクション\"\n      },\n      \"tooltip\": {\n        \"delete\": \"共有を削除\"\n      },\n      \"confirm\": {\n        \"delete\": \"本当にこの共有を削除しますか？この操作は元に戻せません。\"\n      },\n      \"label\": \"ページ共有を管理する\",\n      \"description\": \"ページ共有機能を有効または無効にする\"\n    },\n    \"notification\": {\n      \"pageShareSuccess\": \"ページ共有URLが正常に更新されました\",\n      \"someError\": \"問題が発生しました。後ほど再度お試しください。\",\n      \"webShareDeleteSuccess\": \"ウェブ共有が正常に削除されました\"\n    }\n  },\n  \"ollamaSettings\": {\n    \"title\": \"Ollamaの設定\",\n    \"heading\": \"Ollamaを設定\",\n    \"settings\": {\n      \"ollamaUrl\": {\n        \"label\": \"OllamaのURL\",\n        \"placeholder\": \"OllamaのURLを入力\"\n      },\n      \"globalEnable\": {\n        \"label\": \"Ollamaの統合を全体的に有効または無効にする\",\n        \"warning\": \"Ollamaの統合を全体的に無効にすると、Page AssistはOllamaからモデルを取得しなくなります。<anchor>OpenAI互換API</anchor>セクションからOllamaインスタンスを追加することは可能で、正常に動作します。\"\n      },\n      \"advanced\": {\n        \"label\": \"Ollama URL の高度な設定\",\n        \"urlRewriteEnabled\": {\n          \"label\": \"カスタムOriginのURLを有効化または無効化する\"\n        },\n        \"rewriteUrl\": {\n          \"label\": \"カスタムOriginのURL\",\n          \"placeholder\": \"カスタムOriginのURLを入力\"\n        },\n        \"autoCORSFix\": {\n          \"label\": \"自動Ollama CORSフィックスを有効または無効にする\"\n        },\n        \"headers\": {\n          \"label\": \"カスタムヘッダー\",\n          \"add\": \"ヘッダーを追加\",\n          \"key\": {\n            \"label\": \"ヘッダーキー\",\n            \"placeholder\": \"認証\"\n          },\n          \"value\": {\n            \"label\": \"ヘッダー値\",\n            \"placeholder\": \"ベアラートークン\"\n          }\n        },\n        \"help\": \"PageAssistでOllamaに接続の問題がある場合は、カスタムOriginのURLを設定できます。設定の詳細については、<anchor>ここをクリック</anchor>してください。\"\n      }\n    }\n  },\n  \"manageSearch\": {\n    \"title\": \"Web検索の管理\",\n    \"heading\": \"Web検索を設定する\"\n  },\n  \"about\": {\n    \"title\": \"About\",\n    \"heading\": \"About\",\n    \"chromeVersion\": \"Page Assistのバージョン\",\n    \"ollamaVersion\": \"Ollamaのバージョン\",\n    \"support\": \"Page Assistプロジェクトは、以下のプラットフォームで寄付やスポンサーシップをすることで支援できます:\",\n    \"koFi\": \"Ko-fiで支援する\",\n    \"githubSponsor\": \"GitHubでスポンサーする\",\n    \"githubRepo\": \"GitHubリポジトリ\"\n  },\n  \"manageKnowledge\": {\n    \"title\": \"知識を管理する\",\n    \"heading\": \"知識ベースを構成する\"\n  },\n  \"rag\": {\n    \"title\": \"Pipelineの設定\",\n    \"ragSettings\": {\n      \"label\": \"RAGの設定\",\n      \"model\": {\n        \"label\": \"エンベディングモデル\",\n        \"required\": \"モデルを選択してください\",\n        \"help\": \"`nomic-embed-text`などのエンベディングモデルの使用を強くおすすめします。\",\n        \"placeholder\": \"モデルを選択\"\n      },\n      \"chunkSize\": {\n        \"label\": \"チャンクサイズ\",\n        \"placeholder\": \"チャンクサイズを入力\",\n        \"required\": \"チャンクサイズを入力してください\"\n      },\n      \"chunkOverlap\": {\n        \"label\": \"チャンクオーバーラップ\",\n        \"placeholder\": \"チャンクオーバーラップを入力\",\n        \"required\": \"チャンクオーバーラップを入力してください\"\n      },\n      \"totalFilePerKB\": {\n        \"label\": \"知識ベースのデフォルトファイルアップロード制限\",\n        \"placeholder\": \"デフォルトのファイルアップロード制限を入力してください（例：10）\",\n        \"required\": \"デフォルトのファイルアップロード制限を入力してください\"\n      },\n      \"noOfRetrievedDocs\": {\n        \"label\": \"取得ドキュメント数\",\n        \"placeholder\": \"取得ドキュメント数を入力\",\n        \"required\": \"取得ドキュメント数を入力してください\"\n      },\n      \"splittingSeparator\": {\n        \"label\": \"セパレーター\",\n        \"placeholder\": \"セパレーターを入力（例：\\\\n\\\\n）\",\n        \"required\": \"セパレーターを入力してください\"\n      },\n      \"splittingStrategy\": {\n        \"label\": \"テキスト分割方式\"\n      }\n    },\n    \"prompt\": {\n      \"label\": \"RAGプロンプトを設定\",\n      \"option1\": \"通常\",\n      \"option2\": \"Web\",\n      \"alert\": \"ここでシステムプロンプトを設定することは非推奨となりました。プロンプトの追加や編集には「プロンプトを管理」セクションをご利用ください。このセクションは今後のリリースで削除される予定です。\",\n      \"systemPrompt\": \"システムプロンプト\",\n      \"systemPromptPlaceholder\": \"システムプロンプトを入力\",\n      \"webSearchPrompt\": \"Web検索プロンプト\",\n      \"webSearchPromptHelp\": \"プロンプトから`{search_results}`を削除しないでください。\",\n      \"webSearchPromptError\": \"Web検索プロンプトを入力してください\",\n      \"webSearchPromptPlaceholder\": \"Web検索プロンプトを入力\",\n      \"webSearchFollowUpPrompt\": \"Web検索フォローアッププロンプト\",\n      \"webSearchFollowUpPromptHelp\": \"プロンプトから`{chat_history}`と`{question}`を削除しないでください。\",\n      \"webSearchFollowUpPromptError\": \"Web検索フォローアッププロンプトを入力してください！\",\n      \"webSearchFollowUpPromptPlaceholder\": \"Web検索フォローアッププロンプト\"\n    }\n  },\n  \"chromeAiSettings\": {\n    \"title\": \"Chrome AI設定\"\n  },\n  \"mermaid\": \"Mermaid\"\n}\n"
  },
  {
    "path": "src/assets/locale/ja-JP/sidepanel.json",
    "content": "{\n  \"tooltip\": {\n    \"embed\": \"ページを埋め込むのに数分かかる場合があります。しばらくお待ちください...\",\n    \"openwebui\": \"WebUIを開く\"\n  }\n}"
  },
  {
    "path": "src/assets/locale/ko/chrome.json",
    "content": "{\n    \"heading\": \"Chrome AI 설정\",\n    \"status\": {\n        \"label\": \"Page Assist에서 Chrome AI 지원을 활성화하거나 비활성화하기\"\n    },\n    \"error\": {\n        \"browser_not_supported\": \"이 Chrome 버전은 Gemini Nano 모델을 지원하지 않습니다. 버전을 127 이상으로 업데이트해 주세요.\",\n        \"ai_not_supported\": \"설정 `chrome://flags/#prompt-api-for-gemini-nano`가 활성화되지 않았습니다. 활성화해 주세요.\",\n        \"ai_not_ready\": \"Gemini Nano가 아직 준비되지 않았습니다. Chrome 설정을 다시 확인해 주세요.\",\n        \"internal_error\": \"내부 오류가 발생했습니다. 나중에 다시 시도해 주세요.\"\n    },\n    \"errorDescription\": \"Chrome AI를 사용하려면 Chrome 버전 138 이상이 필요합니다. 다음 단계를 따르세요:\\n\\n1. `chrome://flags/#prompt-api-for-gemini-nano`로 이동하여 \\\"Prompt API for Gemini Nano\\\"를 활성화하세요.\\n2. Chrome을 다시 시작하여 플래그를 적용하세요.\\n3. 이 페이지로 돌아와 \\\"모델 다운로드\\\"를 클릭하세요 — 처음으로 4GB 모델이 다운로드됩니다.\\n4. 다운로드가 완료되면 Page Assist를 통해 Gemini Nano를 활성화할 수 있습니다.\",\n    \"downloadModel\": \"모델 다운로드\",\n    \"modelDownloadWarning\": \"이는 약 1.5GB에서 2.4GB 범위의 다운로드 크기를 가진 모델을 다운로드합니다. 충분한 디스크 공간이 있는지 확인하세요.\"\n}\n"
  },
  {
    "path": "src/assets/locale/ko/common.json",
    "content": "{\n  \"pageAssist\": \"페이지 어시스트\",\n  \"selectAModel\": \"모델 선택\",\n  \"save\": \"저장\",\n  \"saved\": \"저장됨\",\n  \"cancel\": \"취소\",\n  \"retry\": \"재시도\",\n  \"share\": {\n    \"tooltip\": {\n      \"share\": \"공유\"\n    },\n    \"modal\": {\n      \"title\": \"채팅 링크 공유\"\n    },\n    \"form\": {\n      \"defaultValue\": {\n        \"name\": \"익명\",\n        \"title\": \"제목 없는 채팅\"\n      },\n      \"title\": {\n        \"label\": \"채팅 제목\",\n        \"placeholder\": \"채팅 제목을 입력하세요\",\n        \"required\": \"채팅 제목은 필수 항목입니다\"\n      },\n      \"name\": {\n        \"label\": \"이름\",\n        \"placeholder\": \"이름을 입력하세요\",\n        \"required\": \"이름은 필수 항목입니다\"\n      },\n      \"btn\": {\n        \"save\": \"링크 생성\",\n        \"saving\": \"링크 생성 중...\"\n      }\n    },\n    \"notification\": {\n      \"successGenerate\": \"링크가 클립보드에 복사되었습니다\",\n      \"failGenerate\": \"링크 생성에 실패했습니다\"\n    }\n  },\n  \"copyToClipboard\": \"클립보드에 복사\",\n  \"webSearch\": \"웹 검색 중\",\n  \"regenerate\": \"재생성\",\n  \"continue\": \"응답 계속하기\",\n  \"edit\": \"편집\",\n  \"delete\": \"삭제\",\n  \"saveAndSubmit\": \"저장하고 제출\",\n  \"editMessage\": {\n    \"placeholder\": \"메시지를 입력하세요...\"\n  },\n  \"submit\": \"제출\",\n  \"noData\": \"데이터가 없습니다\",\n  \"noHistory\": \"채팅 기록이 없습니다\",\n  \"chatWithCurrentPage\": \"현재 페이지에서 채팅\",\n  \"beta\": \"베타\",\n  \"tts\": \"TTS\",\n  \"currentChatModelSettings\": \"현재 채팅 모델 설정\",\n  \"modelSettings\": {\n    \"label\": \"모델 설정\",\n    \"description\": \"모든 채팅에 대해 글로벌 모델 옵션을 설정합니다\",\n    \"form\": {\n      \"keepAlive\": {\n        \"label\": \"Keep Alive\",\n        \"help\": \"요청 후 모델이 메모리에 유지되는 시간을 설정합니다 (기본값: 5분)\",\n        \"placeholder\": \"Keep Alive 기간을 입력하세요 (예: 5분, 10분, 1시간)\"\n      },\n      \"temperature\": {\n        \"label\": \"온도\",\n        \"placeholder\": \"온도 값을 입력하세요 (예: 0.7, 1.0)\"\n      },\n      \"numCtx\": {\n        \"label\": \"컨텍스트 윈도우 크기 (num_ctx)\",\n        \"placeholder\": \"컨텍스트 윈도우 크기를 입력하세요 (기본값: 2048)\"\n      },\n      \"numPredict\": {\n        \"label\": \"최대 토큰 수 (num_predict)\",\n        \"placeholder\": \"최대 토큰 수를 입력하세요 (예: 2048, 4096)\"\n      },\n      \"seed\": {\n        \"label\": \"시드\",\n        \"placeholder\": \"시드 값을 입력하세요 (예: 1234)\",\n        \"help\": \"모델 출력의 재현성\"\n      },\n      \"topK\": {\n        \"label\": \"Top K\",\n        \"placeholder\": \"Top K 값을 입력하세요 (예: 40, 100)\"\n      },\n      \"topP\": {\n        \"label\": \"Top P\",\n        \"placeholder\": \"Top P 값을 입력하세요 (예: 0.9, 0.95)\"\n      },\n      \"numGpu\": {\n        \"label\": \"GPU 수\",\n        \"placeholder\": \"GPU에 할당할 레이어 수를 입력하세요\"\n      },\n      \"systemPrompt\": {\n        \"label\": \"임시 시스템 프롬프트\",\n        \"placeholder\": \"시스템 프롬프트를 입력하세요\",\n        \"help\": \"현재 채팅에서 시스템 프롬프트를 빠르게 설정하는 방법이며, 선택된 시스템 프롬프트가 있을 경우 이를 덮어씁니다.\"\n      }\n    },\n    \"advanced\": \"기타 모델 설정\"\n  },\n  \"copilot\": {\n    \"summary\": \"요약\",\n    \"explain\": \"설명\",\n    \"rephrase\": \"다르게 표현\",\n    \"translate\": \"번역\"\n  },\n  \"citations\": \"인용\",\n  \"downloadCode\": \"코드 다운로드\",\n  \"date\": {\n    \"pinned\": \"고정됨\",\n    \"today\": \"오늘\",\n    \"yesterday\": \"어제\",\n    \"last7Days\": \"지난 7일\",\n    \"older\": \"그 이전\"\n  },\n  \"range\": {\n    \"deleteConfirm\": {\n      \"pinned\": \"고정된 모든 메시지를 삭제하시겠습니까?\",\n      \"today\": \"오늘의 모든 메시지를 삭제하시겠습니까?\",\n      \"yesterday\": \"어제의 모든 메시지를 삭제하시겠습니까?\",\n      \"last7Days\": \"지난 7일간의 모든 메시지를 삭제하시겠습니까?\",\n      \"older\": \"이전의 모든 메시지를 삭제하시겠습니까?\"\n    },\n    \"tooltip\": {\n      \"pinned\": \"고정된 모든 메시지 삭제\",\n      \"today\": \"오늘의 모든 메시지 삭제\",\n      \"yesterday\": \"어제의 모든 메시지 삭제\",\n      \"last7Days\": \"지난 7일간의 모든 메시지 삭제\",\n      \"older\": \"이전의 모든 메시지 삭제\"\n    }\n  },\n  \"pin\": \"고정\",\n  \"unpin\": \"고정 해제\",\n  \"generationInfo\": \"생성 정보\",\n  \"sidebarChat\": \"사이드바 채팅\",\n  \"reasoning\": {\n    \"thinking\": \"생각 중....\",\n    \"thought\": \"{{time}} 동안 생각함\"\n  },\n  \"embeddingGen\": \"임베딩을 생성하는 중입니다. 시간이 걸릴 수 있습니다\",\n  \"semanticSearch\": \"의미론적 검색을 수행하는 중\",\n  \"downloading\": \"다운로드 중\",\n  \"cancelPullingModel\": {\n    \"confirm\": \"다운로드를 취소하시겠습니까? 이 작업은 다운로드 과정을 중단합니다. Ollama 문서에 따르면, 중단된 위치에서 다시 시작할 수 있습니다.\"\n  }\n}"
  },
  {
    "path": "src/assets/locale/ko/knowledge.json",
    "content": "{\n    \"addBtn\": \"새 지식 추가\",\n    \"columns\": {\n        \"title\": \"제목\",\n        \"status\": \"상태\",\n        \"embeddings\": \"임베딩 모델\",\n        \"createdAt\": \"생성일\",\n        \"action\": \"작업\"\n    },\n    \"expandedColumns\": {\n        \"name\": \"이름\"\n    },\n    \"confirm\": {\n        \"delete\": \"이 지식을 삭제하시겠습니까?\"\n    },\n    \"deleteSuccess\": \"지식이 정상적으로 삭제되었습니다\",\n    \"status\": {\n        \"pending\": \"대기 중\",\n        \"finished\": \"완료\",\n        \"processing\": \"처리 중\",\n        \"failed\": \"실패\"\n    },\n    \"addKnowledge\": \"지식 추가\",\n    \"updateKnowledge\": \"소스 추가\",\n    \"form\": {\n        \"title\": {\n            \"label\": \"지식 제목\",\n            \"placeholder\": \"지식 제목을 입력하세요\",\n            \"required\": \"지식 제목은 필수 항목입니다\"\n        },\n        \"uploadFile\": {\n            \"label\": \"파일 업로드\",\n            \"uploadText\": \"파일을 여기에 드래그 앤 드롭하거나 클릭하여 업로드하세요\",\n            \"uploadHint\": \"지원되는 파일 형식: .pdf, .csv, .txt\",\n            \"required\": \"파일은 필수 항목입니다\"\n        },\n        \"submit\": \"제출\",\n        \"success\": \"지식이 정상적으로 추가되었습니다\"\n    },\n    \"noEmbeddingModel\": \"먼저 RAG 설정 페이지에서 임베딩 모델을 추가해 주세요\"\n}\n"
  },
  {
    "path": "src/assets/locale/ko/openai.json",
    "content": "{\n    \"settings\": \"OpenAI 호환 API\",\n    \"heading\": \"OpenAI 호환 API\",\n    \"subheading\": \"여기에서 OpenAI API 호환 공급자를 관리하고 설정할 수 있습니다.\",\n    \"addBtn\": \"공급자 추가\",\n    \"table\": {\n        \"name\": \"공급자 이름\",\n        \"baseUrl\": \"기본 URL\",\n        \"actions\": \"작업\"\n    },\n    \"modal\": {\n        \"titleAdd\": \"새 공급자 추가\",\n        \"name\": {\n            \"label\": \"공급자 이름\",\n            \"required\": \"공급자 이름은 필수 항목입니다.\",\n            \"placeholder\": \"공급자 이름 입력\"\n        },\n        \"baseUrl\": {\n            \"label\": \"기본 URL\",\n            \"help\": \"OpenAI API 공급자의 기본 URL 예시: (http://localhost:1234/v1)\",\n            \"required\": \"기본 URL은 필수 항목입니다.\",\n            \"placeholder\": \"기본 URL 입력\"\n        },\n        \"apiKey\": {\n            \"label\": \"API 키\",\n            \"required\": \"API 키는 필수 항목입니다.\",\n            \"placeholder\": \"API 키 입력\"\n        },\n        \"submit\": \"저장\",\n        \"update\": \"업데이트\",\n        \"deleteConfirm\": \"이 공급자를 삭제하시겠습니까?\",\n        \"model\": {\n            \"title\": \"모델 목록\",\n            \"subheading\": \"이 공급자에서 사용하고자 하는 챗 모델을 선택하세요.\",\n            \"success\": \"새로운 모델이 정상적으로 추가되었습니다.\"\n        },\n        \"tipLMStudio\": \"Page Assist는 LM Studio에 로드된 모델을 자동으로 가져옵니다. 수동 추가가 필요하지 않습니다.\"\n    },\n    \"addSuccess\": \"공급자가 정상적으로 추가되었습니다.\",\n    \"deleteSuccess\": \"공급자가 정상적으로 삭제되었습니다.\",\n    \"updateSuccess\": \"공급자가 정상적으로 업데이트되었습니다.\",\n    \"delete\": \"삭제\",\n    \"edit\": \"편집\",\n    \"newModel\": \"공급자에 모델 추가\",\n    \"noNewModel\": \"LMStudio, Ollama, Llamafile,의 경우 동적으로 가져옵니다. 수동 추가는 필요하지 않습니다.\",\n    \"searchModel\": \"모델 검색\",\n    \"selectAll\": \"모두 선택\",\n    \"save\": \"저장\",\n    \"saving\": \"저장 중...\",\n    \"manageModels\": {\n        \"columns\": {\n            \"name\": \"모델 이름\",\n            \"model_type\": \"모델 타입\",\n            \"model_id\": \"모델 ID\",\n            \"provider\": \"공급자 이름\",\n            \"actions\": \"작업\",\n            \"nickname\": \"모델 별칭\"\n        },\n        \"tooltip\": {\n            \"delete\": \"삭제\"\n        },\n        \"confirm\": {\n            \"delete\": \"이 모델을 삭제하시겠습니까?\"\n        },\n        \"modal\": {\n            \"title\": \"사용자 정의 모델 추가\",\n            \"form\": {\n                \"name\": {\n                    \"label\": \"모델 ID\",\n                    \"placeholder\": \"llama3.2\",\n                    \"required\": \"모델 ID는 필수 항목입니다.\"\n                },\n                \"provider\": {\n                    \"label\": \"공급자\",\n                    \"placeholder\": \"공급자 선택\",\n                    \"required\": \"공급자는 필수 항목입니다.\"\n                },\n                \"type\": {\n                    \"label\": \"모델 타입\"\n                }\n            }\n        }\n    },\n    \"noModelFound\": \"모델을 찾을 수 없습니다. 올바른 기본 URL과 API 키를 가진 공급자가 추가되었는지 확인하세요.\",\n    \"radio\": {\n        \"chat\": \"챗 모델\",\n        \"embedding\": \"임베딩 모델\",\n        \"chatInfo\": \"는 챗 완료 및 대화 생성에 사용됩니다\",\n        \"embeddingInfo\": \"는 RAG 및 기타 의미 검색 관련 작업에 사용됩니다.\"\n    },\n    \"nicknameModal\": {\n        \"title\": \"모델 별칭 추가 / 편집\",\n        \"form\": {\n            \"modelName\": {\n                \"label\": \"모델 이름\",\n                \"placeholder\": \"모델 이름 입력\",\n                \"required\": \"모델 이름은 필수 항목입니다.\"\n            },\n            \"modelAvatar\": {\n                \"label\": \"모델 아바타\",\n                \"placeholder\": \"모델 아바타 입력\",\n                \"help\": \"모델 아바타의 URL을 입력해주세요. 이 이미지는 채팅 창에 표시됩니다.\"\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/assets/locale/ko/option.json",
    "content": "{\n    \"newChat\": \"새 채팅\",\n    \"selectAPrompt\": \"프롬프트 선택\",\n    \"githubRepository\": \"GitHub 리포지토리\",\n    \"settings\": \"설정\",\n    \"sidebarTitle\": \"채팅 기록\",\n    \"error\": \"오류\",\n    \"somethingWentWrong\": \"문제가 발생했습니다\",\n    \"validationSelectModel\": \"계속하려면 모델을 선택하세요\",\n    \"deleteHistoryConfirmation\": \"이 기록을 삭제하시겠습니까?\",\n    \"editHistoryTitle\": \"새 제목 입력\",\n    \"temporaryChat\": \"임시 채팅\",\n    \"more\": {\n        \"copy\": {\n            \"group\": \"복사\",\n            \"asText\": \"텍스트로 복사\",\n            \"asMarkdown\": \"마크다운으로 복사\",\n            \"success\": \"클립보드에 복사되었습니다!\"\n        },\n        \"download\": {\n            \"group\": \"다운로드\",\n            \"text\": \"텍스트 파일 (.txt)\",\n            \"markdown\": \"마크다운 (.md)\",\n            \"json\": \"JSON 파일 (.json)\"\n        },\n        \"share\": \"공유\"\n    }\n}"
  },
  {
    "path": "src/assets/locale/ko/playground.json",
    "content": "{\n    \"ollamaState\": {\n        \"searching\": \"Ollama 검색 중 🦙\",\n        \"running\": \"Ollama 실행 중 🦙\",\n        \"notRunning\": \"Ollama에 연결할 수 없습니다 🦙\",\n        \"connectionError\": \"연결 오류가 발생한 것 같습니다. 문제 해결에 대한 자세한 내용은 <anchor>문서</anchor>를 참조하세요.\"\n    },\n    \"formError\": {\n        \"noModel\": \"모델을 선택하세요\",\n        \"noEmbeddingModel\": \"설정 > RAG 페이지에서 임베딩 모델을 설정하세요\"\n    },\n    \"form\": {\n        \"textarea\": {\n            \"placeholder\": \"메시지를 입력하세요...\"\n        },\n        \"webSearch\": {\n            \"on\": \"켜짐\",\n            \"off\": \"꺼짐\"\n        }\n    },\n    \"tooltip\": {\n        \"searchInternet\": \"인터넷 검색\",\n        \"speechToText\": \"음성 입력\",\n        \"uploadImage\": \"이미지 업로드\",\n        \"stopStreaming\": \"스트리밍 중지\",\n        \"knowledge\": \"지식\",\n        \"clearContext\": \"컨텍스트 지우기\"\n    },\n    \"sendWhenEnter\": \"Enter 키를 누르면 전송\",\n    \"welcome\": \"안녕하세요! 오늘 어떻게 도와드릴까요?\",\n    \"useOCR\": \"이미지에서 텍스트 추출 (OCR)\"\n}"
  },
  {
    "path": "src/assets/locale/ko/settings.json",
    "content": "{\n  \"generalSettings\": {\n    \"title\": \"일반 설정\",\n    \"settings\": {\n      \"heading\": \"웹 UI 설정\",\n      \"speechRecognitionLang\": {\n        \"label\": \"음성 인식 언어\",\n        \"placeholder\": \"언어 선택\"\n      },\n      \"language\": {\n        \"label\": \"언어\",\n        \"placeholder\": \"언어 선택\"\n      },\n      \"darkMode\": {\n        \"label\": \"테마 변경\",\n        \"options\": {\n          \"light\": \"라이트\",\n          \"dark\": \"다크\"\n        }\n      },\n      \"defaultCopilotPrompt\": {\n        \"label\": \"사이드 패널 기본 프롬프트 (Copilot)\",\n        \"placeholder\": \"프롬프트 선택\"\n      },\n      \"defaultWebUIPrompt\": {\n        \"label\": \"웹 UI 기본 프롬프트\",\n        \"placeholder\": \"프롬프트 선택\"\n      },\n      \"searchMode\": {\n        \"label\": \"간편 인터넷 검색 실행\"\n      },\n      \"copilotResumeLastChat\": {\n        \"label\": \"사이드 패널을 열 때 마지막 채팅 재개 (Copilot)\"\n      },\n      \"turnOnChatWithWebsite\": {\n        \"label\": \"웹사이트와의 채팅 기본 활성화 (Copilot)\"\n      },\n      \"webUIResumeLastChat\": {\n        \"label\": \"웹 UI를 열 때 마지막 채팅 재개\"\n      },\n      \"hideCurrentChatModelSettings\": {\n        \"label\": \"현재 채팅 모델 설정 숨기기\"\n      },\n      \"restoreLastChatModel\": {\n        \"label\": \"이전 채팅에서 마지막 사용한 모델 복원\"\n      },\n      \"sendNotificationAfterIndexing\": {\n        \"label\": \"지식 베이스 처리 완료 후 알림 전송\"\n      },\n      \"generateTitle\": {\n        \"label\": \"AI로 제목 생성\"\n      },\n      \"ollamaStatus\": {\n        \"label\": \"Ollama 연결 상태 확인 활성화 또는 비활성화\"\n      },\n      \"wideMode\": {\n        \"label\": \"와이드 스크린 모드 활성화\"\n      },\n      \"openReasoning\": {\n        \"label\": \"추론 섹션을 기본적으로 열기\"\n      },\n      \"defaultThinkingMode\": {\n        \"label\": \"폼에서 사고 모드 상태 표시\"\n      },\n      \"userChatBubble\": {\n        \"label\": \"사용자 메시지에 채팅 버블 사용\"\n      },\n      \"autoCopyResponseToClipboard\": {\n        \"label\": \"응답 자동 클립보드 복사\"\n      },\n      \"useMarkdownForUserMessage\": {\n        \"label\": \"사용자 메시지에 마크다운 형식 사용\"\n      },\n      \"copyAsFormattedText\": {\n        \"label\": \"서식이 있는 텍스트로 복사\"\n      },\n      \"tabMentionsEnabled\": {\n        \"label\": \"탭 멘션 (@tab) 활성화\"\n      },\n      \"pasteLargeTextAsFile\": {\n        \"label\": \"큰 텍스트를 파일로 붙여넣기\"\n      },\n      \"ocrLanguage\": {\n        \"label\": \"기본 OCR 언어\",\n        \"placeholder\": \"OCR 언어 선택\"\n      },\n      \"sidepanelTemporaryChat\": {\n        \"label\": \"사이드 패널에서 임시 채팅 기본 활성화\"\n      },\n      \"webuiTemporaryChat\": {\n        \"label\": \"웹 UI에서 임시 채팅 기본 활성화\"\n      },\n      \"removeReasoningTagFromCopy\": {\n        \"label\": \"복사된 텍스트에서 추론 태그 제거\"\n      },\n      \"youtubeAutoSummarize\": {\n        \"label\": \"YouTube 동영상에 '요약' 버튼 표시\"\n      },\n      \"hideReasoningWidget\": {\n        \"label\": \"AI 메시지에서 추론 위젯 숨기기\"\n      },\n      \"persistChatInput\": {\n        \"label\": \"채팅 입력 유지 (미전송 메시지 저장)\"\n      },\n      \"enableMessageQueue\": {\n        \"label\": \"스트리밍 중 메시지 큐 활성화\"\n      },\n      \"optimizeQueueForSmallScreen\": {\n        \"label\": \"소형 화면을 위한 채팅 UI 최적화\"\n      },\n      \"tableTextWrap\": {\n        \"label\": \"마크다운 테이블에서 텍스트 줄 바꿈 활성화\"\n      },\n      \"showMoreForLargeMessage\": {\n        \"label\": \"긴 사용자 메시지에 '더 보기' 표시\"\n      },\n      \"sidebarPosition\": {\n        \"label\": \"사이드바 위치\",\n        \"options\": {\n          \"left\": \"왼쪽\",\n          \"right\": \"오른쪽\"\n        }\n      }\n    },\n    \"sidepanelRag\": {\n      \"heading\": \"검색 설정\",\n      \"ragEnabled\": {\n        \"label\": \"임베딩 및 검색 활성화\"\n      },\n      \"maxWebsiteContext\": {\n        \"label\": \"전체 컨텍스트 모드의 최대 콘텐츠 크기\",\n        \"placeholder\": \"콘텐츠 크기 (기본값 4028)\"\n      }\n    },\n    \"webSearch\": {\n      \"heading\": \"웹 검색 관리\",\n      \"searchMode\": {\n        \"label\": \"간편한 인터넷 검색 실행\"\n      },\n      \"provider\": {\n        \"label\": \"검색 엔진\",\n        \"placeholder\": \"검색 엔진 선택\"\n      },\n      \"totalSearchResults\": {\n        \"label\": \"총 검색 결과\",\n        \"placeholder\": \"총 검색 결과 입력\"\n      },\n      \"visitSpecificWebsite\": {\n        \"label\": \"메시지에 언급된 웹사이트 방문\"\n      },\n      \"searxng\": {\n        \"url\": {\n          \"label\": \"SearXNG URL\"\n        }\n      },\n      \"braveApi\": {\n        \"label\": \"Brave API 키\",\n        \"placeholder\": \"Brave API 키를 입력하세요\"\n      },\n      \"tavilyApi\": {\n        \"label\": \"Tavily API 키\",\n        \"placeholder\": \"Tavily API 키를 입력하세요\"\n      },\n      \"exa\": {\n        \"label\": \"Exa API 키\",\n        \"placeholder\": \"Exa API 키를 입력하세요\"\n      },\n      \"googleDomain\": {\n        \"label\": \"Google 도메인\"\n      },\n      \"searchOnByDefault\": {\n        \"label\": \"기본적으로 인터넷 검색 켜기\"\n      },\n      \"firecrawlAPIKey\": {\n        \"label\": \"Firecrawl API 키\",\n        \"placeholder\": \"Firecrawl API 키를 입력하세요\"\n      },\n      \"domainFilter\": {\n        \"label\": \"도메인 필터 목록\",\n        \"description\": \"이 도메인의 결과만 표시\",\n        \"placeholder\": \"예: example.com\"\n      },\n      \"blockedDomains\": {\n        \"label\": \"차단된 도메인\",\n        \"description\": \"이 도메인의 결과 제외\",\n        \"placeholder\": \"예: spam.com\"\n      }\n    },\n    \"system\": {\n      \"heading\": \"시스템 설정\",\n      \"deleteChatHistory\": {\n        \"label\": \"시스템 초기화\",\n        \"button\": \"전체 초기화\",\n        \"confirm\": \"시스템을 초기화하시겠습니까? 모든 데이터가 삭제되며 되돌릴 수 없습니다.\"\n      },\n      \"export\": {\n        \"label\": \"모든 데이터 내보내기 (채팅 기록, 지식 베이스, 프롬프트, 설정)\",\n        \"button\": \"데이터 내보내기\",\n        \"success\": \"내보내기 성공\"\n      },\n      \"import\": {\n        \"label\": \"모든 데이터 가져오기 (채팅 기록, 지식 베이스, 프롬프트, 설정)\",\n        \"button\": \"데이터 가져오기\",\n        \"success\": \"가져오기 성공\",\n        \"error\": \"가져오기 오류\"\n      },\n      \"actionIcon\": {\n        \"label\": \"확장 아이콘 클릭 기본 동작 설정\"\n      },\n      \"contextMenu\": {\n        \"label\": \"컨텍스트 메뉴 기본 동작 설정\"\n      },\n      \"fontSize\": {\n        \"label\": \"글꼴 크기\"\n      },\n      \"webuiBtnSidePanel\": {\n        \"label\": \"사이드 패널에 웹 UI 버튼 표시\"\n      },\n      \"chatBackgroundImage\": {\n        \"label\": \"채팅 배경 이미지\"\n      },\n      \"storageSyncEnabled\": {\n        \"label\": \"브라우저 저장소 동기화 활성화 (기기 간 설정 동기화)\"\n      }\n    },\n    \"tts\": {\n      \"heading\": \"텍스트 음성 변환 설정\",\n      \"ttsEnabled\": {\n        \"label\": \"텍스트 음성 변환 활성화\"\n      },\n      \"ttsAutoPlay\": {\n        \"label\": \"응답 완료 후 자동 음성 재생\"\n      },\n      \"ttsProvider\": {\n        \"label\": \"텍스트 음성 변환 제공자\",\n        \"placeholder\": \"제공자 선택\"\n      },\n      \"ttsVoice\": {\n        \"label\": \"텍스트 음성 변환 음성\",\n        \"placeholder\": \"음성 선택\"\n      },\n      \"ssmlEnabled\": {\n        \"label\": \"SSML (Speech Synthesis Markup Language) 활성화\"\n      },\n      \"responseSplitting\": {\n        \"label\": \"응답 분할\"\n      },\n      \"removeReasoningTagTTS\": {\n        \"label\": \"TTS에서 추론 태그 제거\"\n      }\n    },\n    \"stt\": {\n      \"heading\": \"음성 인식 설정\",\n      \"autoStopTimeout\": {\n        \"label\": \"자동 중지 시간 (ms)\",\n        \"placeholder\": \"자동 중지 시간을 밀리초 단위로 입력하세요\"\n      },\n      \"autoSubmitVoiceMessage\": {\n        \"label\": \"음성 메시지 자동 전송\"\n      }\n    }\n  },\n  \"manageModels\": {\n    \"title\": \"모델 관리\",\n    \"addBtn\": \"새 모델 추가\",\n    \"columns\": {\n      \"name\": \"이름\",\n      \"digest\": \"다이제스트\",\n      \"nickname\": \"닉네임\",\n      \"modifiedAt\": \"수정 일시\",\n      \"size\": \"크기\",\n      \"actions\": \"동작\"\n    },\n    \"expandedColumns\": {\n      \"parentModel\": \"상위 모델\",\n      \"format\": \"형식\",\n      \"family\": \"패밀리\",\n      \"parameterSize\": \"파라미터 크기\",\n      \"quantizationLevel\": \"양자화 수준\"\n    },\n    \"tooltip\": {\n      \"delete\": \"모델 삭제\",\n      \"repull\": \"모델 다시 가져오기\",\n      \"editNickname\": \"닉네임 수정\"\n    },\n    \"confirm\": {\n      \"delete\": \"이 모델을 정말 삭제하시겠습니까?\",\n      \"repull\": \"이 모델을 정말 다시 가져오시겠습니까?\"\n    },\n    \"modal\": {\n      \"title\": \"새 모델 추가\",\n      \"placeholder\": \"모델 이름 입력\",\n      \"pull\": \"모델 가져오기\"\n    },\n    \"notification\": {\n      \"pullModel\": \"모델 가져오는 중\",\n      \"pullModelDescription\": \"{{modelName}} 모델을 가져오는 중입니다. 자세한 내용은 확장 기능 아이콘을 확인하세요.\",\n      \"cancellingDownload\": \"다운로드 취소 중\",\n      \"cancellingDownloadDescription\": \"모델 다운로드가 취소되고 있습니다...\",\n      \"success\": \"성공\",\n      \"error\": \"오류\",\n      \"successDescription\": \"모델 가져오기가 완료되었습니다\",\n      \"successDeleteDescription\": \"모델 삭제가 완료되었습니다\",\n      \"someError\": \"문제가 발생했습니다. 나중에 다시 시도해 주세요.\"\n    }\n  },\n  \"managePrompts\": {\n    \"title\": \"프롬프트 관리\",\n    \"addBtn\": \"새 프롬프트 추가\",\n    \"option1\": \"일반\",\n    \"option2\": \"RAG\",\n    \"questionPrompt\": \"질문 프롬프트\",\n    \"columns\": {\n      \"title\": \"제목\",\n      \"prompt\": \"프롬프트\",\n      \"type\": \"프롬프트 유형\",\n      \"actions\": \"동작\"\n    },\n    \"systemPrompt\": \"시스템 프롬프트\",\n    \"quickPrompt\": \"퀵 프롬프트\",\n    \"tooltip\": {\n      \"delete\": \"프롬프트 삭제\",\n      \"edit\": \"프롬프트 수정\"\n    },\n    \"confirm\": {\n      \"delete\": \"이 프롬프트를 정말 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.\"\n    },\n    \"modal\": {\n      \"addTitle\": \"새 프롬프트 추가\",\n      \"editTitle\": \"프롬프트 수정\"\n    },\n    \"segmented\": {\n      \"custom\": \"커스텀 프롬프트\",\n      \"copilot\": \"Copilot 프롬프트\"\n    },\n    \"form\": {\n      \"title\": {\n        \"label\": \"제목\",\n        \"placeholder\": \"훌륭한 프롬프트\",\n        \"required\": \"제목을 입력하세요\"\n      },\n      \"prompt\": {\n        \"label\": \"프롬프트\",\n        \"placeholder\": \"프롬프트 입력\",\n        \"required\": \"프롬프트를 입력하세요\",\n        \"help\": \"프롬프트 내에서 {key}를 변수로 사용할 수 있습니다.\",\n        \"missingTextPlaceholder\": \"프롬프트에 {text} 변수가 없습니다. 추가해 주세요.\"\n      },\n      \"isSystem\": {\n        \"label\": \"시스템 프롬프트\"\n      },\n      \"btnSave\": {\n        \"saving\": \"프롬프트 추가 중...\",\n        \"save\": \"프롬프트 추가\"\n      },\n      \"btnEdit\": {\n        \"saving\": \"프롬프트 업데이트 중...\",\n        \"save\": \"프롬프트 업데이트\"\n      }\n    },\n    \"notification\": {\n      \"addSuccess\": \"프롬프트가 추가되었습니다\",\n      \"addSuccessDesc\": \"프롬프트가 정상적으로 추가되었습니다\",\n      \"error\": \"오류\",\n      \"someError\": \"문제가 발생했습니다. 나중에 다시 시도해 주세요.\",\n      \"updatedSuccess\": \"프롬프트가 업데이트되었습니다\",\n      \"updatedSuccessDesc\": \"프롬프트가 정상적으로 업데이트되었습니다\",\n      \"deletedSuccess\": \"프롬프트가 삭제되었습니다\",\n      \"deletedSuccessDesc\": \"프롬프트가 정상적으로 삭제되었습니다\"\n    }\n  },\n  \"manageShare\": {\n    \"title\": \"공유 관리\",\n    \"heading\": \"페이지 공유 URL 설정\",\n    \"form\": {\n      \"url\": {\n        \"label\": \"페이지 공유 URL\",\n        \"placeholder\": \"페이지 공유 URL 입력\",\n        \"required\": \"페이지 공유 URL을 입력해 주세요!\",\n        \"help\": \"개인정보 보호를 위해 페이지 공유를 자체 호스팅하고, 해당 URL을 여기에 입력할 수 있습니다. <anchor>자세히 보기</anchor>\"\n      }\n    },\n    \"webshare\": {\n      \"heading\": \"웹 공유\",\n      \"columns\": {\n        \"title\": \"제목\",\n        \"url\": \"URL\",\n        \"actions\": \"동작\"\n      },\n      \"tooltip\": {\n        \"delete\": \"공유 삭제\"\n      },\n      \"confirm\": {\n        \"delete\": \"이 공유를 정말 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.\"\n      },\n      \"label\": \"페이지 공유 관리\",\n      \"description\": \"페이지 공유 기능을 활성화 또는 비활성화\"\n    },\n    \"notification\": {\n      \"pageShareSuccess\": \"페이지 공유 URL이 정상적으로 업데이트되었습니다\",\n      \"someError\": \"문제가 발생했습니다. 나중에 다시 시도해 주세요.\",\n      \"webShareDeleteSuccess\": \"웹 공유가 정상적으로 삭제되었습니다\"\n    }\n  },\n  \"ollamaSettings\": {\n    \"title\": \"Ollama 설정\",\n    \"heading\": \"Ollama 설정하기\",\n    \"settings\": {\n      \"ollamaUrl\": {\n        \"label\": \"Ollama URL\",\n        \"placeholder\": \"Ollama URL 입력\"\n      },\n      \"globalEnable\": {\n        \"label\": \"Ollama 통합 기능 전역 활성화 또는 비활성화\",\n        \"warning\": \"Ollama 통합을 전역적으로 비활성화하면 Page Assist가 Ollama에서 모델을 가져오지 않습니다. <anchor>OpenAI 호환 API</anchor> 섹션에서 Ollama 인스턴스를 추가할 수 있으며 정상적으로 작동합니다.\"\n      },\n      \"advanced\": {\n        \"label\": \"Ollama URL 고급 설정\",\n        \"urlRewriteEnabled\": {\n          \"label\": \"사용자 지정 Origin URL 활성화 또는 비활성화\"\n        },\n        \"rewriteUrl\": {\n          \"label\": \"사용자 지정 Origin URL\",\n          \"placeholder\": \"사용자 지정 Origin URL 입력\"\n        },\n        \"autoCORSFix\": {\n          \"label\": \"자동 Ollama CORS 수정 활성화 또는 비활성화\"\n        },\n        \"headers\": {\n          \"label\": \"사용자 지정 헤더\",\n          \"add\": \"헤더 추가\",\n          \"key\": {\n            \"label\": \"헤더 키\",\n            \"placeholder\": \"인증\"\n          },\n          \"value\": {\n            \"label\": \"헤더 값\",\n            \"placeholder\": \"베어러 토큰\"\n          }\n        },\n        \"help\": \"Page Assist에서 Ollama 연결에 문제가 있는 경우 사용자 지정 Origin URL을 설정할 수 있습니다. 설정에 대한 자세한 내용은 <anchor>여기를 클릭</anchor>하세요.\"\n      }\n    }\n  },\n  \"manageSearch\": {\n    \"title\": \"웹 검색 관리\",\n    \"heading\": \"웹 검색 설정하기\"\n  },\n  \"about\": {\n    \"title\": \"소개\",\n    \"heading\": \"소개\",\n    \"chromeVersion\": \"Page Assist 버전\",\n    \"ollamaVersion\": \"Ollama 버전\",\n    \"support\": \"Page Assist 프로젝트는 다음 플랫폼에서 기부나 후원을 통해 지원할 수 있습니다:\",\n    \"koFi\": \"Ko-fi로 후원하기\",\n    \"githubSponsor\": \"GitHub에서 후원하기\",\n    \"githubRepo\": \"GitHub 저장소\"\n  },\n  \"manageKnowledge\": {\n    \"title\": \"지식 관리\",\n    \"heading\": \"지식 베이스 구성하기\"\n  },\n  \"rag\": {\n    \"title\": \"Pipeline 설정\",\n    \"ragSettings\": {\n      \"label\": \"RAG 설정\",\n      \"model\": {\n        \"label\": \"임베딩 모델\",\n        \"required\": \"모델을 선택해주세요\",\n        \"help\": \"`nomic-embed-text`와 같은 임베딩 모델 사용을 강력히 권장합니다.\",\n        \"placeholder\": \"모델 선택\"\n      },\n      \"chunkSize\": {\n        \"label\": \"청크 크기\",\n        \"placeholder\": \"청크 크기 입력\",\n        \"required\": \"청크 크기를 입력해주세요\"\n      },\n      \"chunkOverlap\": {\n        \"label\": \"청크 오버랩\",\n        \"placeholder\": \"청크 오버랩 입력\",\n        \"required\": \"청크 오버랩을 입력해주세요\"\n      },\n      \"totalFilePerKB\": {\n        \"label\": \"지식 베이스 기본 파일 업로드 제한\",\n        \"placeholder\": \"기본 파일 업로드 제한 입력 (예: 10)\",\n        \"required\": \"기본 파일 업로드 제한을 입력해주세요\"\n      },\n      \"noOfRetrievedDocs\": {\n        \"label\": \"검색 문서 수\",\n        \"placeholder\": \"검색 문서 수 입력\",\n        \"required\": \"검색 문서 수를 입력해주세요\"\n      },\n      \"splittingSeparator\": {\n        \"label\": \"구분자\",\n        \"placeholder\": \"구분자 입력 (예: \\\\n\\\\n)\",\n        \"required\": \"구분자를 입력해주세요\"\n      },\n      \"splittingStrategy\": {\n        \"label\": \"텍스트 분할기\"\n      }\n    },\n    \"prompt\": {\n      \"label\": \"RAG 프롬프트 설정\",\n      \"option1\": \"일반\",\n      \"option2\": \"웹\",\n      \"alert\": \"여기서 시스템 프롬프트를 설정하는 것은 더 이상 권장되지 않습니다. 프롬프트 추가 및 편집은 '프롬프트 관리' 섹션을 이용해주세요. 이 섹션은 향후 릴리스에서 제거될 예정입니다.\",\n      \"systemPrompt\": \"시스템 프롬프트\",\n      \"systemPromptPlaceholder\": \"시스템 프롬프트 입력\",\n      \"webSearchPrompt\": \"웹 검색 프롬프트\",\n      \"webSearchPromptHelp\": \"프롬프트에서 `{search_results}`를 제거하지 마세요.\",\n      \"webSearchPromptError\": \"웹 검색 프롬프트를 입력해주세요\",\n      \"webSearchPromptPlaceholder\": \"웹 검색 프롬프트 입력\",\n      \"webSearchFollowUpPrompt\": \"웹 검색 후속 프롬프트\",\n      \"webSearchFollowUpPromptHelp\": \"프롬프트에서 `{chat_history}`와 `{question}`를 제거하지 마세요.\",\n      \"webSearchFollowUpPromptError\": \"웹 검색 후속 프롬프트를 입력해주세요!\",\n      \"webSearchFollowUpPromptPlaceholder\": \"웹 검색 후속 프롬프트\"\n    }\n  },\n  \"chromeAiSettings\": {\n    \"title\": \"Chrome AI 설정\"\n  },\n  \"mermaid\": \"Mermaid\"\n}\n"
  },
  {
    "path": "src/assets/locale/ko/sidepanel.json",
    "content": "{\n  \"tooltip\": {\n    \"embed\": \"페이지를 임베드하는 데 몇 분이 걸릴 수 있습니다. 잠시만 기다려 주세요...\",\n    \"openwebui\": \"웹 UI 열기\"\n  }\n}"
  },
  {
    "path": "src/assets/locale/ml/chrome.json",
    "content": "{\n    \"heading\": \"ക്രോം എഐ കോൺഫിഗർ ചെയ്യുക\",\n    \"status\": {\n        \"label\": \"പേജ് അസിസ്റ്റിൽ ക്രോം എഐ പിന്തുണ സജ്ജമാക്കുക അല്ലെങ്കിൽ ഡിസബിൾ ചെയ്യുക\"\n    },\n    \"error\": {\n        \"browser_not_supported\": \"ക്രോത്തിന്റെ ഈ പതിപ്പ് ജെമിനി നാനോ മോഡലിനെ പിന്തുണയ്ക്കുന്നില്ല. പതിപ്പ് 127 അല്ലെങ്കിൽ അതിനുശേഷം അപ്ഡേറ്റ് ചെയ്യുക.\",\n        \"ai_not_supported\": \"ക്രമീകരണം chrome://flags/#prompt-api-for-gemini-nano സജ്ജമാക്കപ്പെട്ടിട്ടില്ല. ദയവായി അതിനെ സജ്ജമാക്കുക.\",\n        \"ai_not_ready\": \"ജെമിനി നാനോ ഇപ്പോഴും സജ്ജമല്ല; നിങ്ങൾ ക്രോം ക്രമീകരണങ്ങൾ രണ്ടുതവണ പരിശോധിക്കണം.\",\n        \"internal_error\": \"ഒരു ആന്തരിക പിശക് സംഭവിച്ചു. ദയവായി കുറച്ചുദിവസം ശേഷം വീണ്ടും ശ്രമിക്കുക.\"\n    },\n    \"errorDescription\": \"ക്രോം എഐ ഉപയോഗിക്കാൻ, നിങ്ങൾക്ക് ക്രോം പതിപ്പ് 138 അല്ലെങ്കിൽ അതിനുശേഷം വേണം. ഈ ചുവടുപടികൾ പിന്തുടരുക:\\n\\n1. `chrome://flags/#prompt-api-for-gemini-nano` എന്നതിലേക്ക് പോകുക, \\\"Prompt API for Gemini Nano\\\" സജ്ജമാക്കുക.\\n2. ഫ്ലാഗ് പ്രയോഗിക്കാൻ ക്രോം പുനരാരംഭിക്കുക.\\n3. ഈ പേജിലേക്ക് മടങ്ങി \\\"മോഡൽ ഡൗൺലോഡ് ചെയ്യുക\\\" ക്ലിക്ക് ചെയ്യുക — ഇത് ആദ്യമായി 4GB മോഡൽ ഡൗൺലോഡ് ചെയ്യും.\\n4. ഡൗൺലോഡ് ചെയ്ത ശേഷം, ജെമിനി നാനോ പേജ് അസിസ്റ്റ് വഴി സജ്ജമാക്കാം.\",\n    \"downloadModel\": \"മോഡൽ ഡൗൺലോഡ് ചെയ്യുക\",\n    \"modelDownloadWarning\": \"ഇത് ഏകദേശം 1.5 GB മുതൽ 2.4 GB വരെ ഡൗൺലോഡ് വലുപ്പമുള്ള മോഡൽ ഡൗൺലോഡ് ചെയ്യും. നിങ്ങൾക്ക് മതിയായ ഡിസ്‌ക് സ്പേസ് ഉണ്ടെന്ന് ഉറപ്പാക്കുക.\"\n}\n"
  },
  {
    "path": "src/assets/locale/ml/common.json",
    "content": "{\n    \"pageAssist\": \"പേജ് ആസിസ്റ്റ്\",\n    \"selectAModel\": \"ഒരു മോഡല്‍ തിരഞ്ഞെടുക്കുക\",\n    \"save\": \"സേവ് ചെയ്യുക\",\n    \"saved\": \"സേവ് ചെയ്തു\",\n    \"cancel\": \"റദ്ദാക്കുക\",\n    \"retry\": \"വീണ്ടും ശ്രമിക്കുക\",\n    \"share\": {\n        \"tooltip\": {\n            \"share\": \"പങ്കിടുക\"\n        },\n        \"modal\": {\n            \"title\": \"ചാറ്റ് ലിങ്ക് പങ്കിടുക\"\n        },\n        \"form\": {\n            \"defaultValue\": {\n                \"name\": \"അജ്ഞാതന്‍\",\n                \"title\": \"പേരില്ലാത്ത ചാറ്റ്\"\n            },\n            \"title\": {\n                \"label\": \"ചാറ്റ് തലക്കെട്ട്\",\n                \"placeholder\": \"ചാറ്റ് തലക്കെട്ട് നല്കുക\",\n                \"required\": \"ചാറ്റ് തലക്കെട്ട് ആവശ്യമാണ്\"\n            },\n            \"name\": {\n                \"label\": \"നിങ്ങളുടെ പേര്\",\n                \"placeholder\": \"നിങ്ങളുടെ പേര് നല്കുക\",\n                \"required\": \"നിങ്ങളുടെ പേര് ആവശ്യമാണ്\"\n            },\n            \"btn\": {\n                \"save\": \"ലിങ്ക് ജനറേറ്റ് ചെയ്യുക\",\n                \"saving\": \"ലിങ്ക് ജനറേറ്റ് ചെയ്യുന്നു...\"\n            }\n        },\n        \"notification\": {\n            \"successGenerate\": \"ലിങ്ക് ക്ലിപ്ബോര്‍ഡിലേക്ക് പകര്‍ത്തി\",\n            \"failGenerate\": \"ലിങ്ക് ജനറേറ്റ് ചെയ്യുന്നതില്‍ പരാജയപ്പെട്ടു\"\n        }\n    },\n    \"copyToClipboard\": \"ക്ലിപ്ബോര്‍ഡിലേക്ക് പകര്‍ത്തുക\",\n    \"webSearch\": \"വെബ് തിരയുന്നു\",\n    \"regenerate\": \"വീണ്ടും ജനറേറ്റ് ചെയ്യുക\",\n    \"continue\": \"പ്രതികരണം തുടരുക\",\n    \"edit\": \"എഡിറ്റ് ചെയ്യുക\",\n    \"delete\": \"ഇല്ലാതാക്കുക\",\n    \"saveAndSubmit\": \"സേവ് ചെയ്ത് സമര്‍പ്പിക്കുക\",\n    \"editMessage\": {\n        \"placeholder\": \"ഒരു സന്ദേശം ടൈപ്പ് ചെയ്യുക...\"\n    },\n    \"submit\": \"സമർപ്പിക്കുക\",\n    \"noData\": \"ഡാറ്റ ലഭ്യമല്ല\",\n    \"noHistory\": \"ചാറ്റ് ചരിത്രം ലഭ്യമല്ല\",\n    \"chatWithCurrentPage\": \"നിലവിലെ പേജിനുമായി ചാറ്റ് ചെയ്യുക\",\n    \"beta\": \"ബീറ്റ\",\n    \"currentChatModelSettings\": \"നിലവിലെ ചാറ്റ് മോഡൽ ക്രമീകരണങ്ങൾ\",\n    \"modelSettings\": {\n        \"label\": \"മോഡൽ ക്രമീകരണങ്ങൾ\",\n        \"description\": \"എല്ലാ ചാറ്റുകൾക്കും മോഡൽ ഓപ്ഷനുകൾ ഗ്ലോബലായി സജ്ജമാക്കുക\",\n        \"form\": {\n            \"keepAlive\": {\n                \"label\": \"കീപ്പ് ആലൈവ്\",\n                \"help\": \"അഭ്യർത്ഥനയെ തുടർന്ന് മോഡൽ മെമ്മറിയിൽ എത്രനേരം നിലനിൽക്കുമെന്ന് നിയന്ത്രിക്കുന്നു (സ്ഥിരം: 5 മിനിറ്റ്)\",\n                \"placeholder\": \"കീപ്പ് ആലൈവ് കാലയളവ് നൽകുക (ഉദാ: 5 മിനിറ്റ്, 10 മിനിറ്റ്, 1 മണിക്കൂർ)\"\n            },\n            \"temperature\": {\n                \"label\": \"താപനില\",\n                \"placeholder\": \"താപനില മൂല്യം നൽകുക (ഉദാ: 0.7, 1.0)\"\n            },\n            \"numCtx\": {\n                \"label\": \"കോൺടെക്സ്റ്റ് വിൻഡോ വലുപ്പം (num_ctx)\",\n                \"placeholder\": \"കോൺടെക്സ്റ്റ് വിൻഡോ വലുപ്പ മൂല്യം നൽകുക (സ്ഥിരം: 2048)\"\n            },\n            \"numPredict\": {\n                \"label\": \"പരമാവധി ടോക്കണുകൾ (num_predict)\",\n                \"placeholder\": \"പരമാവധി ടോക്കൺ മൂല്യം നൽകുക (ഉദാ: 2048, 4096)\"\n            },\n            \"seed\": {\n                \"label\": \"സീഡ്\",\n                \"placeholder\": \"സീഡ് വില്യമ നൽകുക (ഉദാ: 1234)\",\n                \"help\": \"മോഡൽ ഔട്ട്പുട്ടിന്റെ പുനഃസൃഷ്ടിയോഗ്യത\"\n            },\n            \"topK\": {\n                \"label\": \"ടോപ് K\",\n                \"placeholder\": \"ടോപ് K മൂല്യം നൽകുക (ഉദാ: 40, 100)\"\n            },\n            \"topP\": {\n                \"label\": \"ടോപ് P\",\n                \"placeholder\": \"ടോപ് P മൂല്യം നൽകുക (ഉദാ: 0.9, 0.95)\"\n            },\n            \"numGpu\": {\n                \"label\": \"ജിപിയു എണ്ണം\",\n                \"placeholder\": \"ജിപിയു(കൾ)ക്ക് അയക്കേണ്ട ലേയറുകളുടെ എണ്ണം നൽകുക\"\n            },\n            \"systemPrompt\": {\n                \"label\": \"താൽക്കാലിക സിസ്റ്റം പ്രോംപ്റ്റ്\",\n                \"placeholder\": \"സിസ്റ്റം പ്രോംപ്റ്റ് നൽകുക\",\n                \"help\": \"നിലവിലുള്ള ചാറ്റിൽ സിസ്റ്റം പ്രോംപ്റ്റ് സെറ്റ് ചെയ്യാനുള്ള വേഗത്തിലുള്ള മാർഗമാണിത്, ഇത് തിരഞ്ഞെടുത്ത സിസ്റ്റം പ്രോംപ്റ്റ് നിലവിലുണ്ടെങ്കിൽ അതിനെ മറികടക്കും.\"\n            }\n        },\n        \"advanced\": \"കൂടുതൽ മോഡൽ ക്രമീകരണങ്ങൾ\"\n    },\n    \"copilot\": {\n        \"summary\": \"സംഗ്രഹിക്കുക\",\n        \"explain\": \"വിശദീകരിക്കുക\",\n        \"rephrase\": \"പുനഃരൂപീകരിക്കുക\",\n        \"translate\": \"വിവർത്തനം ചെയ്യുക\"\n    },\n    \"citations\": \"ഉദ്ധരണികൾ\",\n    \"downloadCode\": \"കോഡ് ഡൗൺലോഡ് ചെയ്യുക\",\n    \"date\": {\n        \"pinned\": \"പിൻ ചെയ്തത്\",\n        \"today\": \"ഇന്ന്\",\n        \"yesterday\": \"ഇന്നലെ\",\n        \"last7Days\": \"കഴിഞ്ഞ 7 ദിവസം\",\n        \"older\": \"പഴയത്\"\n    },\n    \"range\": {\n        \"deleteConfirm\": {\n            \"pinned\": \"പിൻ ചെയ്ത എല്ലാ സന്ദേശങ്ങളും ഇല്ലാതാക്കണമെന്ന് തീർച്ചയാണോ?\",\n            \"today\": \"ഇന്നത്തെ എല്ലാ സന്ദേശങ്ങളും ഇല്ലാതാക്കണമെന്ന് തീർച്ചയാണോ?\",\n            \"yesterday\": \"ഇന്നലത്തെ എല്ലാ സന്ദേശങ്ങളും ഇല്ലാതാക്കണമെന്ന് തീർച്ചയാണോ?\",\n            \"last7Days\": \"കഴിഞ്ഞ 7 ദിവസത്തെ എല്ലാ സന്ദേശങ്ങളും ഇല്ലാതാക്കണമെന്ന് തീർച്ചയാണോ?\",\n            \"older\": \"പഴയ എല്ലാ സന്ദേശങ്ങളും ഇല്ലാതാക്കണമെന്ന് തീർച്ചയാണോ?\"\n        },\n        \"tooltip\": {\n            \"pinned\": \"പിൻ ചെയ്ത എല്ലാ സന്ദേശങ്ങളും ഇല്ലാതാക്കുക\",\n            \"today\": \"ഇന്നത്തെ എല്ലാ സന്ദേശങ്ങളും ഇല്ലാതാക്കുക\",\n            \"yesterday\": \"ഇന്നലത്തെ എല്ലാ സന്ദേശങ്ങളും ഇല്ലാതാക്കുക\",\n            \"last7Days\": \"കഴിഞ്ഞ 7 ദിവസത്തെ എല്ലാ സന്ദേശങ്ങളും ഇല്ലാതാക്കുക\",\n            \"older\": \"പഴയ എല്ലാ സന്ദേശങ്ങളും ഇല്ലാതാക്കുക\"\n        }\n    },\n    \"pin\": \"പിൻ ചെയ്യുക\",\n    \"unpin\": \"അൺപിൻ ചെയ്യുക\",\n    \"generationInfo\": \"ജനറേഷൻ വിവരങ്ങൾ\",\n    \"sidebarChat\": \"സൈഡ്ബാർ ചാറ്റ്\",\n    \"reasoning\": {\n        \"thinking\": \"ചിന്തിക്കുന്നു....\",\n        \"thought\": \"{{time}} നേരത്തെ ചിന്ത\"\n    },\n    \"embeddingGen\": \"എംബെഡിങ്ങുകൾ സൃഷ്ടിക്കുന്നു, ഇതിന് കുറച്ച് സമയമെടുക്കും\",\n    \"semanticSearch\": \"സെമാന്റിക് തിരയൽ നടത്തുന്നു\",\n    \"downloading\": \"ഡൗൺലോഡ് ചെയ്യുന്നു\",\n    \"cancelPullingModel\": {\n        \"confirm\": \"ഡൗൺലോഡ് റദ്ദാക്കണമെന്നുറപ്പാണോ? ഇത് ഡൗൺലോഡ് പ്രക്രിയ നിർത്തും. ഒല്ലാമ ഡോക്യുമെന്റേഷൻ പ്രകാരം, നിങ്ങൾ തുടർന്നിടത്തുനിന്ന് വീണ്ടും ആരംഭിക്കാം.\"\n    }\n}"
  },
  {
    "path": "src/assets/locale/ml/knowledge.json",
    "content": "{\n    \"addBtn\": \"പുതിയ വിജ്ഞാനം ചേര്‍ക്കുക\",\n    \"columns\": {\n        \"title\": \"തലക്കെട്ട്\",\n        \"status\": \"സ്ഥിതി\",\n        \"embeddings\": \"എംബെഡിംഗ് മോഡല്‍\",\n        \"createdAt\": \"സൃഷ്ടിച്ചത്\",\n        \"action\": \"പ്രവർത്തനങ്ങൾ\"\n    },\n    \"expandedColumns\": {\n        \"name\": \"നാമം\"\n    },\n    \"confirm\": {\n        \"delete\": \"നിങ്ങൾക്ക് ഈ വിജ്ഞാനം ഇല്ലാതാക്കണമെന്ന് ഉറപ്പാണോ?\"\n    },\n    \"deleteSuccess\": \"വിജ്ഞാനം വിജയകരമായി ഇല്ലാതാക്കി\",\n    \"status\": {\n        \"pending\": \"തീരുമാനിക്കാനുണ്ട്\",\n        \"finished\": \"പൂർത്തീകരിച്ചു\",\n        \"processing\": \"പ്രോസസ്സിംഗ്\",\n        \"failed\": \"പരാജയപ്പെട്ടു\"\n    },\n    \"addKnowledge\": \"വിജ്ഞാനം ചേര്‍ക്കുക\",\n    \"updateKnowledge\": \"ഉറവിടം ചേർക്കുക\",\n    \"form\": {\n        \"title\": {\n            \"label\": \"വിജ്ഞാനത്തിന്റെ തലക്കെട്ട്\",\n            \"placeholder\": \"വിജ്ഞാനത്തിന്റെ തലക്കെട്ട് നല്‍കുക\",\n            \"required\": \"വിജ്ഞാനത്തിന്റെ തലക്കെട്ട് ആവശ്യമാണ്\"\n        },\n        \"uploadFile\": {\n            \"label\": \"ഫയല്‍ അപ്‌ലോഡ് ചെയ്യുക\",\n            \"uploadText\": \"ഇവിടെ ഒരു ഫയല്‍ എടുത്തിടുക അല്ലെങ്കില്‍ അപ്‌ലോഡ് ചെയ്യാന്‍ ക്ലിക്ക് ചെയ്യുക\",\n            \"uploadHint\": \"പിന്തുണയുള്ള ഫയല്‍ തരങ്ങള്‍: .pdf, .csv, .txt, .md,.docx\",\n            \"required\": \"ഫയല്‍ ആവശ്യമാണ്\"\n        },\n        \"submit\": \"സമര്‍പ്പിക്കുക\",\n        \"success\": \"വിജ്ഞാനം വിജയകരമായി ചേര്‍ത്തു\"\n    },\n    \"noEmbeddingModel\": \"ദയവായി ആദ്യം RAG ക്രമീകരണ പേജില്‍ നിന്ന് ഒരു എംബെഡിംഗ് മോഡല്‍ ചേര്‍ക്കുക\"\n}"
  },
  {
    "path": "src/assets/locale/ml/openai.json",
    "content": "{\n    \"settings\": \"OpenAI അനുയോജ്യമായ API\",\n    \"heading\": \"OpenAI അനുയോജ്യമായ API\",\n    \"subheading\": \"നിങ്ങളുടെ OpenAI API അനുയോജ്യമായ ദാതാക്കളെ ഇവിടെ നിയന്ത്രിക്കുകയും കോൺഫിഗർ ചെയ്യുകയും ചെയ്യുക.\",\n    \"addBtn\": \"ദാതാവിനെ ചേർക്കുക\",\n    \"table\": {\n        \"name\": \"ദാതാവിന്റെ പേര്\",\n        \"baseUrl\": \"അടിസ്ഥാന URL\",\n        \"actions\": \"പ്രവർത്തനം\"\n    },\n    \"modal\": {\n        \"titleAdd\": \"പുതിയ ദാതാവിനെ ചേർക്കുക\",\n        \"name\": {\n            \"label\": \"ദാതാവിന്റെ പേര്\",\n            \"required\": \"ദാതാവിന്റെ പേര് ആവശ്യമാണ്.\",\n            \"placeholder\": \"ദാതാവിന്റെ പേര് നൽകുക\"\n        },\n        \"baseUrl\": {\n            \"label\": \"അടിസ്ഥാന URL\",\n            \"help\": \"OpenAI API ദാതാവിന്റെ അടിസ്ഥാന URL. ഉദാ. (http://localhost:1234/v1)\",\n            \"required\": \"അടിസ്ഥാന URL ആവശ്യമാണ്.\",\n            \"placeholder\": \"അടിസ്ഥാന URL നൽകുക\"\n        },\n        \"apiKey\": {\n            \"label\": \"API കീ\",\n            \"required\": \"API കീ ആവശ്യമാണ്.\",\n            \"placeholder\": \"API കീ നൽകുക\"\n        },\n        \"submit\": \"സംരക്ഷിക്കുക\",\n        \"update\": \"അപ്ഡേറ്റ് ചെയ്യുക\",\n        \"deleteConfirm\": \"ഈ ദാതാവിനെ ഇല്ലാതാക്കണമെന്ന് തീർച്ചയാണോ?\",\n        \"model\": {\n            \"title\": \"മോഡൽ പട്ടിക\",\n            \"subheading\": \"ഈ ദാതാവിനോടൊപ്പം ഉപയോഗിക്കാൻ നിങ്ങൾ ആഗ്രഹിക്കുന്ന ചാറ്റ് മോഡലുകൾ തിരഞ്ഞെടുക്കുക.\",\n            \"success\": \"പുതിയ മോഡലുകൾ വിജയകരമായി ചേർത്തു.\"\n        },\n        \"tipLMStudio\": \"LM Studio-യിൽ നിങ്ങൾ ലോഡ് ചെയ്ത മോഡലുകൾ Page Assist സ്വയമേവ ലഭ്യമാക്കും. അവ മാനുവലായി ചേർക്കേണ്ടതില്ല.\"\n    },\n    \"addSuccess\": \"ദാതാവിനെ വിജയകരമായി ചേർത്തു.\",\n    \"deleteSuccess\": \"ദാതാവിനെ വിജയകരമായി ഇല്ലാതാക്കി.\",\n    \"updateSuccess\": \"ദാതാവിനെ വിജയകരമായി അപ്ഡേറ്റ് ചെയ്തു.\",\n    \"delete\": \"ഇല്ലാതാക്കുക\",\n    \"edit\": \"തിരുത്തുക\",\n    \"newModel\": \"ദാതാവിലേക്ക് മോഡലുകൾ ചേർക്കുക\",\n    \"noNewModel\": \"LMStudio, Ollama, Llamafile-യ്ക്കായി, ഞങ്ങൾ ഡൈനാമിക്കായി ലഭ്യമാക്കുന്നു. മാനുവലായി ചേർക്കേണ്ടതില്ല.\",\n    \"searchModel\": \"മോഡൽ തിരയുക\",\n    \"selectAll\": \"എല്ലാം തിരഞ്ഞെടുക്കുക\",\n    \"save\": \"സംരക്ഷിക്കുക\",\n    \"saving\": \"സംരക്ഷിക്കുന്നു...\",\n    \"manageModels\": {\n        \"columns\": {\n            \"name\": \"മോഡൽ പേര്\",\n            \"model_type\": \"മോഡൽ തരം\",\n            \"model_id\": \"മോഡൽ ID\",\n            \"provider\": \"ദാതാവിന്റെ പേര്\",\n            \"actions\": \"പ്രവർത്തനം\",\n            \"nickname\": \"മോഡൽ വിളിപ്പേര്\"\n        },\n        \"tooltip\": {\n            \"delete\": \"ഇല്ലാതാക്കുക\"\n        },\n        \"confirm\": {\n            \"delete\": \"ഈ മോഡൽ ഇല്ലാതാക്കണമെന്ന് തീർച്ചയാണോ?\"\n        },\n        \"modal\": {\n            \"title\": \"ഇഷ്ടാനുസൃത മോഡൽ ചേർക്കുക\",\n            \"form\": {\n                \"name\": {\n                    \"label\": \"മോഡൽ ID\",\n                    \"placeholder\": \"llama3.2\",\n                    \"required\": \"മോഡൽ ID ആവശ്യമാണ്.\"\n                },\n                \"provider\": {\n                    \"label\": \"ദാതാവ്\",\n                    \"placeholder\": \"ദാതാവിനെ തിരഞ്ഞെടുക്കുക\",\n                    \"required\": \"ദാതാവ് ആവശ്യമാണ്.\"\n                },\n                \"type\": {\n                    \"label\": \"മോഡൽ തരം\"\n                }\n            }\n        }\n    },\n    \"noModelFound\": \"മോഡൽ കണ്ടെത്തിയില്ല. അടിസ്ഥാന URL-ഉം API കീയും ഉള്ള ശരിയായ ദാതാവിനെ നിങ്ങൾ ചേർത്തിട്ടുണ്ടെന്ന് ഉറപ്പാക്കുക.\",\n    \"radio\": {\n        \"chat\": \"ചാറ്റ് മോഡൽ\",\n        \"embedding\": \"എംബെഡിംഗ് മോഡൽ\",\n        \"chatInfo\": \"ചാറ്റ് പൂർത്തീകരണത്തിനും സംഭാഷണ നിർമ്മാണത്തിനും ഉപയോഗിക്കുന്നു\",\n        \"embeddingInfo\": \"RAG-നും മറ്റ് സെമാന്റിക് തിരയൽ അനുബന്ധ ടാസ്കുകൾക്കും ഉപയോഗിക്കുന്നു.\"\n    },\n    \"nicknameModal\": {\n        \"title\": \"മോഡൽ വിളിപ്പേര് ചേർക്കുക / എഡിറ്റ് ചെയ്യുക\",\n        \"form\": {\n            \"modelName\": {\n                \"label\": \"മോഡൽ പേര്\",\n                \"placeholder\": \"മോഡൽ പേര് നൽകുക\",\n                \"required\": \"മോഡൽ പേര് ആവശ്യമാണ്.\"\n            },\n            \"modelAvatar\": {\n                \"label\": \"മോഡൽ അവതാർ\",\n                \"placeholder\": \"മോഡൽ അവതാർ നൽകുക\",\n                \"help\": \"മോഡൽ അവതാറിന്റെ URL നൽകുക. ഈ ചിത്രം ചാറ്റ് വിൻഡോയിൽ പ്രദർശിപ്പിക്കപ്പെടും.\"\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/assets/locale/ml/option.json",
    "content": "{\n    \"newChat\": \"പുതിയ ചാറ്റ്\",\n    \"selectAPrompt\": \"ഒരു പ്രോംപ്റ്റ് തിരഞ്ഞെടുക്കുക\",\n    \"githubRepository\": \"ഗിറ്റ്ഹബ് റെപ്പോസിറ്ററി\",\n    \"settings\": \"സെറ്റിംഗുകൾ\",\n    \"sidebarTitle\": \"ചാറ്റ് ചരിത്രം\",\n    \"error\": \"പിശക്\",\n    \"somethingWentWrong\": \"എന്തോ തെറ്റായി\",\n    \"deleteHistoryConfirmation\": \"നിങ്ങളുടെ ചാറ്റ് ചരിത്രം ഇല്ലാതാക്കണമെന്ന് തീർച്ചയാണോ?\",\n    \"editHistoryTitle\": \"ചാറ്റ് title എഡിറ്റുചെയ്യുക\",\n    \"validationSelectModel\": \"തുടരുന്നതിന് ഒരു മോഡല്‍ തിരഞ്ഞെടുക്കുക\",\n    \"temporaryChat\": \"താൽക്കാലിക ചാറ്റ്\",\n    \"more\": {\n        \"copy\": {\n            \"group\": \"പകർത്തുക\",\n            \"asText\": \"ടെക്സ്റ്റായി പകർത്തുക\",\n            \"asMarkdown\": \"മാർക്ക്ഡൗൺ ആയി പകർത്തുക\",\n            \"success\": \"ക്ലിപ്പ്ബോർഡിലേക്ക് പകർത്തി!\"\n        },\n        \"download\": {\n            \"group\": \"ഡൗൺലോഡ്\",\n            \"text\": \"ടെക്സ്റ്റ് ഫയൽ (.txt)\",\n            \"markdown\": \"മാർക്ക്ഡൗൺ (.md)\",\n            \"json\": \"JSON ഫയൽ (.json)\"\n        },\n        \"share\": \"പങ്കുവയ്ക്കുക\"\n    }\n}"
  },
  {
    "path": "src/assets/locale/ml/playground.json",
    "content": "{\n    \"ollamaState\": {\n        \"searching\": \"നിങ്ങളുടെ ഒല്ലാമയ്ക്കായി തിരയുന്നു 🦙\",\n        \"running\": \"ഒല്ലാമ പ്രവര്‍ത്തിക്കുന്നു 🦙\",\n        \"notRunning\": \"ഒല്ലാമയുമായി ബന്ധിപ്പിക്കാന്‍ കഴിയുന്നില്ല 🦙\",\n        \"connectionError\": \"നിങ്ങൾക്ക് കണക്ഷൻ പ്രശ്നം ഉണ്ടെന്നു കാണുന്നു. ഈ <anchor>ഡോക്യുമെന്റേഷൻ</anchor> പരിശോധിക്കാൻ കൂടുതൽ സഹായത്തിനായി.\"\n    },\n    \"formError\": {\n        \"noModel\": \"ദയവായി ഒരു മോഡല്‍ തിരഞ്ഞെടുക്കുക\",\n        \"noEmbeddingModel\": \"സെറ്റിംഗുകൾ > RAG പേജിലുള്ള എംബെഡിംഗ് മോഡല്‍ സജ്ജീകരിക്കുക\"\n    },\n    \"form\": {\n        \"textarea\": {\n            \"placeholder\": \"ഒരു സന്ദേശം ടൈപ്പ് ചെയ്യുക...\"\n        },\n        \"webSearch\": {\n            \"on\": \"ഓണ്‍\",\n            \"off\": \"ഓഫ്\"\n        }\n    },\n    \"tooltip\": {\n        \"searchInternet\": \"ഇന്റര്‍നെറ്റ് തിരയുക\",\n        \"speechToText\": \"സംഭാഷണം ടെക്സ്റ്റായി\",\n        \"uploadImage\": \"ഇമേജ് അപ്‌ലോഡ് ചെയ്യുക\",\n        \"stopStreaming\": \"സ്ട്രീമിംഗ് നിർത്തുക\",\n        \"knowledge\": \"അറിവ്\",\n        \"clearContext\": \"സന്ദർഭം മായ്ക്കുക\"\n    },\n    \"sendWhenEnter\": \"എന്റര്‍ അമര്‍ത്തുമ്പോള്‍ അയയ്ക്കുക\",\n    \"welcome\": \"നമസ്കാരം! ഇന്ന് എനിക്ക് നിങ്ങളെ എങ്ങനെ സഹായിക്കാൻ കഴിയും?\",\n    \"useOCR\": \"ചിത്രത്തിൽ നിന്ന് ടെക്സ്റ്റ് എടുക്കുക (OCR)\"\n}"
  },
  {
    "path": "src/assets/locale/ml/settings.json",
    "content": "{\n  \"generalSettings\": {\n    \"title\": \"പൊതുവായ സെറ്റിംഗുകൾ\",\n    \"settings\": {\n      \"heading\": \"വെബ് UI സെറ്റിംഗുകൾ\",\n      \"speechRecognitionLang\": {\n        \"label\": \"സംഭാഷണ തിരിച്ചറിയല്‍ ഭാഷ\",\n        \"placeholder\": \"ഒരു ഭാഷ തിരഞ്ഞെടുക്കുക\"\n      },\n      \"language\": {\n        \"label\": \"ഭാഷ\",\n        \"placeholder\": \"ഒരു ഭാഷ തിരഞ്ഞെടുക്കുക\"\n      },\n      \"darkMode\": {\n        \"label\": \"തീം മാറ്റുക\",\n        \"options\": {\n          \"light\": \"ലൈറ്റ്\",\n          \"dark\": \"ഡാര്‍ക്ക്\"\n        }\n      },\n      \"searchMode\": {\n        \"label\": \"സാധാരണ ഇന്റർനെറ്റ് അന്വേഷണം നടത്തുക\"\n      },\n      \"copilotResumeLastChat\": {\n        \"label\": \"സൈഡ്പാനൽ തുറക്കുമ്പോൾ അവസാനത്തെ ചാറ്റ് പുനരാരംഭിക്കുക (Copilot)\"\n      },\n      \"turnOnChatWithWebsite\": {\n        \"label\": \"സ്ഥിരസ്ഥിതിയായി വെബ്സൈറ്റുമായുള്ള ചാറ്റ് പ്രവർത്തനക്ഷമമാക്കുക (കോപൈലറ്റ്)\"\n      },\n      \"webUIResumeLastChat\": {\n        \"label\": \"വെബ് UI തുറക്കുമ്പോൾ അവസാനത്തെ ചാറ്റ് പുനരാരംഭിക്കുക\"\n      },\n      \"hideCurrentChatModelSettings\": {\n        \"label\": \"നിലവിലുള്ള ചാറ്റ് മോഡൽ ക്രമീകരണങ്ങൾ മറയ്ക്കുക\"\n      },\n      \"restoreLastChatModel\": {\n        \"label\": \"മുൻപത്തെ ചാറ്റുകൾക്കായി അവസാനം ഉപയോഗിച്ച മോഡൽ പുനഃസ്ഥാപിക്കുക\"\n      },\n      \"sendNotificationAfterIndexing\": {\n        \"label\": \"അറിവ് ശേഖരം പ്രോസസ്സ് ചെയ്ത് കഴിഞ്ഞതിന് ശേഷം അറിയിപ്പ് അയയ്ക്കുക\"\n      },\n      \"generateTitle\": {\n        \"label\": \"എഐ ഉപയോഗിച്ച് ശീർഷകം സൃഷ്ടിക്കുക\"\n      },\n      \"ollamaStatus\": {\n        \"label\": \"ഒല്ലാമ കണക്ഷൻ സ്റ്റാറ്റസ് പരിശോധന പ്രവർത്തനക്ഷമമാക്കുകയോ പ്രവർത്തനരഹിതമാക്കുകയോ ചെയ്യുക\"\n      },\n      \"wideMode\": {\n        \"label\": \"വിശാലമായ സ്ക്രീൻ മോഡ് പ്രവർത്തനക്ഷമമാക്കുക\"\n      },\n      \"openReasoning\": {\n        \"label\": \"സ്ഥിരസ്ഥിതിയായി റീസണിംഗ് കൊളാപ്സ് തുറക്കുക\"\n      },\n      \"userChatBubble\": {\n        \"label\": \"ഉപയോക്തൃ സന്ദേശങ്ങൾക്കായി ചാറ്റ് ബബിൾ ഉപയോഗിക്കുക\"\n      },\n      \"autoCopyResponseToClipboard\": {\n        \"label\": \"പ്രതികരണം സ്വയമേവ ക്ലിപ്പ്ബോർഡിലേക്ക് പകർത്തുക\"\n      },\n      \"useMarkdownForUserMessage\": {\n        \"label\": \"ഉപയോക്തൃ സന്ദേശങ്ങൾക്കായി മാർക്ക്ഡൗൺ ഫോർമാറ്റിംഗ് പ്രവർത്തനക്ഷമമാക്കുക\"\n      },\n      \"copyAsFormattedText\": {\n        \"label\": \"ഫോർമാറ്റ് ചെയ്ത ടെക്സ്റ്റായി പകർത്തുക\"\n      },\n      \"tabMentionsEnabled\": {\n        \"label\": \"ടാബ് മെൻഷനുകൾ (@tab) പ്രവർത്തനക്ഷമമാക്കുക\"\n      },\n      \"pasteLargeTextAsFile\": {\n        \"label\": \"വലിയ ടെക്സ്റ്റ് ഫയലായി പേസ്റ്റ് ചെയ്യുക\"\n      },\n      \"sidepanelTemporaryChat\": {\n        \"label\": \"സൈഡ്പാനലിൽ താൽക്കാലിക ചാറ്റ് സ്ഥിരസ്ഥിതിയായി പ്രവർത്തനക്ഷമമാക്കുക\"\n      }\n    },\n    \"sidepanelRag\": {\n      \"heading\": \"വീണ്ടെടുക്കൽ ക്രമീകരണങ്ങൾ\",\n      \"ragEnabled\": {\n        \"label\": \"എംബെഡിംഗും വീണ്ടെടുക്കലും പ്രവർത്തനക്ഷമമാക്കുക\"\n      },\n      \"maxWebsiteContext\": {\n        \"label\": \"പൂർണ്ണ സന്ദർഭ മോഡിനായുള്ള പരമാവധി ഉള്ളടക്ക വലുപ്പം\",\n        \"placeholder\": \"ഉള്ളടക്ക വലുപ്പം (സ്ഥിരസ്ഥിതി 4028)\"\n      }\n    },\n    \"webSearch\": {\n      \"heading\": \"വെബ്ബ് തിരച്ചിൽ നിയന്ത്രിക്കുക\",\n      \"searchMode\": {\n        \"label\": \"സരളമായ ഇന്റർനെറ്റ് തിരച്ചിൽ നടത്തുക\"\n      },\n      \"provider\": {\n        \"label\": \"തിരച്ചിൽ എഞ്ചിൻ\",\n        \"placeholder\": \"തിരച്ചിൽ എഞ്ചിൻ തിരഞ്ഞെടുക്കുക\"\n      },\n      \"totalSearchResults\": {\n        \"label\": \"ആകെ തിരച്ചിൽ ഫലങ്ങൾ\",\n        \"placeholder\": \"ആകെ തിരച്ചിൽ ഫലങ്ങളുടെ എണ്ണം നൽകുക\"\n      },\n      \"visitSpecificWebsite\": {\n        \"label\": \"സന്ദേശത്തിൽ പറയുന്ന വെബ്സൈറ്റ് സന്ദർശിക്കുക.\"\n      },\n      \"searxng\": {\n        \"url\": {\n          \"label\": \"SearXNG URL\"\n        }\n      },\n      \"braveApi\": {\n        \"label\": \"ബ്രേവ് API കീ\",\n        \"placeholder\": \"നിങ്ങളുടെ ബ്രേവ് API കീ നൽകുക\"\n      },\n      \"searchOnByDefault\": {\n        \"label\": \"സ്ഥിരസ്ഥിതിയായി ഇന്റർനെറ്റ് തിരച്ചിൽ പ്രവർത്തനക്ഷമമാക്കുക\"\n      }\n    },\n    \"system\": {\n      \"heading\": \"സിസ്റ്റം ക്രമീകരണങ്ങൾ\",\n      \"storageSyncEnabled\": {\n        \"label\": \"ബ്രൗസർ സ്റ്റോറേജ് സമന്വയം പ്രവർത്തനക്ഷമമാക്കുക (ഉപകരണങ്ങളിലുടനീളം ക്രമീകരണങ്ങൾ സമന്വയിപ്പിക്കുക)\"\n      },\n      \"deleteChatHistory\": {\n        \"label\": \"സിസ്റ്റം റീസെറ്റ്\",\n        \"button\": \"എല്ലാം റീസെറ്റ് ചെയ്യുക\",\n        \"confirm\": \"നിങ്ങൾക്ക് സിസ്റ്റം റീസെറ്റ് നടത്താൻ തീർച്ചയാണോ? ഇത് എല്ലാ ഡാറ്റയും മായ്ക്കും, പിന്നീട് പുനഃസ്ഥാപിക്കാൻ കഴിയില്ല.\"\n      },\n      \"export\": {\n        \"label\": \"എല്ലാ ഡാറ്റയും എക്സ്പോർട്ട് ചെയ്യുക (ചാറ്റ് ഹിസ്റ്ററി, നോളഡ്ജ് ബേസ്, പ്രോമ്പ്റ്റുകൾ, സജ്ജീകരണങ്ങൾ)\",\n        \"button\": \"ഡാറ്റ എക്സ്പോർട്ട് ചെയ്യുക\",\n        \"success\": \"എക്സ്പോർട്ട് വിജയകരം\"\n      },\n      \"import\": {\n        \"label\": \"എല്ലാ ഡാറ്റയും ഇമ്പോർട്ട് ചെയ്യുക (ചാറ്റ് ഹിസ്റ്ററി, നോളഡ്ജ് ബേസ്, പ്രോമ്പ്റ്റുകൾ, സജ്ജീകരണങ്ങൾ)\",\n        \"button\": \"ഡാറ്റ ഇമ്പോർട്ട് ചെയ്യുക\",\n        \"success\": \"ഇമ്പോർട്ട് വിജയകരം\",\n        \"error\": \"ഇമ്പോർട്ട് പിശക്\"\n      }\n    },\n    \"tts\": {\n      \"heading\": \"ടെക്സ്റ്റ്-ടു-സ്പീച്ച് ക്രമീകരണങ്ങൾ\",\n      \"ttsEnabled\": {\n        \"label\": \"ടെക്സ്റ്റ്-ടു-സ്പീച്ച് പ്രവർത്തനക്ഷമമാക്കുക\"\n      },\n      \"ttsAutoPlay\": {\n        \"label\": \"പൂർത്തിയാക്കിയ ശേഷം വോയ്സ് പ്രതികരണം സ്വയമേവ പ്ലേ ചെയ്യുക\"\n      },\n      \"ttsProvider\": {\n        \"label\": \"ടെക്സ്റ്റ്-ടു-സ്പീച്ച് പ്രോവൈഡർ\",\n        \"placeholder\": \"ഒരു പ്രോവൈഡർ തിരഞ്ഞെടുക്കുക\"\n      },\n      \"ttsVoice\": {\n        \"label\": \"ടെക്സ്റ്റ്-ടു-സ്പീച്ച് വോയ്സ്\",\n        \"placeholder\": \"ഒരു വോയ്സ് തിരഞ്ഞെടുക്കുക\"\n      },\n      \"ssmlEnabled\": {\n        \"label\": \"SSML (സ്പീച്ച് സിന്തസിസ് മാർക്കപ്പ് ലാംഗ്വേജ്) പ്രവർത്തനക്ഷമമാക്കുക\"\n      },\n      \"removeReasoningTagTTS\": {\n        \"label\": \"ടിടിഎസിൽ നിന്ന് റീസണിംഗ് ടാഗ് നീക്കം ചെയ്യുക\"\n      }\n    },\n    \"stt\": {\n      \"heading\": \"സ്പീച്ച്-ടു-ടെക്സ്റ്റ് ക്രമീകരണങ്ങൾ\",\n      \"autoStopTimeout\": {\n        \"label\": \"സ്വയം നിർത്തൽ സമയപരിധി (മില്ലിസെക്കൻഡ്)\",\n        \"placeholder\": \"സ്വയം നിർത്തൽ സമയപരിധി മില്ലിസെക്കൻഡിൽ നൽകുക\"\n      },\n      \"autoSubmitVoiceMessage\": {\n        \"label\": \"വോയ്സ് സന്ദേശം സ്വയമേവ സമർപ്പിക്കുക\"\n      }\n    }\n  },\n  \"manageModels\": {\n    \"title\": \"മോഡലുകള്‍ കൈകാര്യം ചെയ്യുക\",\n    \"addBtn\": \"പുതിയ മോഡല്‍ ചേര്‍ക്കുക\",\n    \"columns\": {\n      \"name\": \"പേര്\",\n      \"digest\": \"ഡൈജസ്റ്റ്\",\n      \"modifiedAt\": \"അവസാനമായി പരിഷ്‌കരിച്ചത്\",\n      \"size\": \"വലുപ്പം\",\n      \"actions\": \"പ്രവർത്തനങ്ങൾ\"\n    },\n    \"expandedColumns\": {\n      \"parentModel\": \"പാരന്റ് മോഡല്‍\",\n      \"format\": \"ഫോര്‍മാറ്റ്\",\n      \"family\": \"കുടുംബം\",\n      \"parameterSize\": \"പാരാമീറ്റര്‍ വലുപ്പം\",\n      \"quantizationLevel\": \"ക്വാണ്ടൈസേഷന്‍ ലെവല്‍\"\n    },\n    \"tooltip\": {\n      \"delete\": \"മോഡല്‍ ഇല്ലാതാക്കുക\",\n      \"repull\": \"മോഡല്‍ വീണ്ടും ലഭ്യമാക്കുക\"\n    },\n    \"confirm\": {\n      \"delete\": \"ഈ മോഡല്‍ ഇല്ലാതാക്കണമെന്ന് തീർച്ചയാണോ?\",\n      \"repull\": \"ഈ മോഡല്‍ വീണ്ടും ലഭ്യമാക്കണമെന്ന് തീർച്ചയാണോ?\"\n    },\n    \"modal\": {\n      \"title\": \"പുതിയ മോഡല്‍ ചേര്‍ക്കുക\",\n      \"placeholder\": \"മോഡല്‍ പേര് നല്‍കുക\",\n      \"pull\": \"മോഡല്‍ ലഭ്യമാക്കുക\"\n    },\n    \"notification\": {\n      \"pullModel\": \"മോഡല്‍ ലഭ്യമാക്കുന്നു\",\n      \"pullModelDescription\": \"{{modelName}} മോഡല്‍ ലഭ്യമാക്കുന്നു. കൂടുതല്‍ വിവരങ്ങള്‍ക്കായി എക്‌സ്റ്റെന്‍ഷന്‍ ഐക്കണ്‍ പരിശോധിക്കുക.\",\n      \"success\": \"വിജയം\",\n      \"error\": \"പിശക്\",\n      \"successDescription\": \"മോഡല്‍ വിജയകരമായി ലഭ്യമാക്കി\",\n      \"successDeleteDescription\": \"മോഡല്‍ വിജയകരമായി ഇല്ലാതാക്കി\",\n      \"someError\": \"എന്തോ തെറ്റായി. ദയവായി പിന്നീട് വീണ്ടും ശ്രമിക്കുക\"\n    }\n  },\n  \"managePrompts\": {\n    \"title\": \"പ്രോംപ്റ്റുകള്‍ കൈകാര്യം ചെയ്യുക\",\n    \"addBtn\": \"പുതിയ പ്രോംപ്റ്റ് ചേര്‍ക്കുക\",\n    \"columns\": {\n      \"title\": \"തലക്കെട്ട്\",\n      \"prompt\": \"പ്രോംപ്റ്റ്\",\n      \"type\": \"പ്രോംപ്റ്റ് തരം\",\n      \"actions\": \"പ്രവർത്തനങ്ങൾ\"\n    },\n    \"option1\": \"സാധാരണ\",\n    \"option2\": \"RAG\",\n    \"questionPrompt\": \"ചോദ്യ പ്രോംപ്റ്റ്\",\n    \"systemPrompt\": \"സിസ്റ്റം പ്രോംപ്റ്റ്\",\n    \"quickPrompt\": \"വേഗത്തിലുള്ള പ്രോംപ്റ്റ്\",\n    \"tooltip\": {\n      \"delete\": \"പ്രോംപ്റ്റ് ഇല്ലാതാക്കുക\",\n      \"edit\": \"പ്രോംപ്റ്റ് എഡിറ്റുചെയ്യുക\"\n    },\n    \"confirm\": {\n      \"delete\": \"ഈ പ്രോംപ്റ്റ് ഇല്ലാതാക്കണമെന്ന് തീർച്ചയാണോ? ഈ പ്രവർത്തനം പിന്നീട് പിൻവലിക്കാനാകില്ല.\"\n    },\n    \"modal\": {\n      \"addTitle\": \"പുതിയ പ്രോംപ്റ്റ് ചേര്‍ക്കുക\",\n      \"editTitle\": \"പ്രോംപ്റ്റ് എഡിറ്റുചെയ്യുക\"\n    },\n    \"segmented\": {\n      \"custom\": \"കസ്റ്റം പ്രോംപ്റ്റുകൾ\",\n      \"copilot\": \"കോപൈലറ്റ് പ്രോംപ്റ്റുകൾ\"\n    },\n    \"form\": {\n      \"title\": {\n        \"label\": \"തലക്കെട്ട്\",\n        \"placeholder\": \"എന്‍റെ അതുല്യമായ പ്രോംപ്റ്റ്\",\n        \"required\": \"ദയവായി ഒരു തലക്കെട്ട് നല്കുക\"\n      },\n      \"prompt\": {\n        \"label\": \"പ്രോംപ്റ്റ്\",\n        \"placeholder\": \"പ്രോംപ്റ്റ് നല്കുക\",\n        \"required\": \"ദയവായി ഒരു പ്രോംപ്റ്റ് നല്കുക\",\n        \"help\": \"നിങ്ങള്‍ക്ക് {key} എന്ന രീതിയില്‍ പ്രോംപ്റ്റില്‍ വേരിയബിളുകള്‍ ഉപയോഗിക്കാവുന്നതാണ്.\",\n        \"missingTextPlaceholder\": \"പ്രോംപ്റ്റിൽ {text} വേരിയബിൾ കാണുന്നില്ല. ദയവായി അത് ചേർക്കുക.\"\n      },\n      \"isSystem\": {\n        \"label\": \"സിസ്റ്റം പ്രോംപ്റ്റ് ആണോ\"\n      },\n      \"btnSave\": {\n        \"saving\": \"പ്രോംപ്റ്റ് ചേര്‍ക്കുന്നു...\",\n        \"save\": \"പ്രോംപ്റ്റ് ചേര്‍ക്കുക\"\n      },\n      \"btnEdit\": {\n        \"saving\": \"പ്രോംപ്റ്റ് അപ്ഡേറ്റ് ചെയ്യുന്നു...\",\n        \"save\": \"പ്രോംപ്റ്റ് അപ്ഡേറ്റ് ചെയ്യുക\"\n      }\n    },\n    \"notification\": {\n      \"addSuccess\": \"പ്രോംപ്റ്റ് ചേർത്തു\",\n      \"addSuccessDesc\": \"പ്രോംപ്റ്റ് വിജയകരമായി ചേർത്തു\",\n      \"error\": \"പിശക്\",\n      \"someError\": \"എന്തോ തെറ്റായി. ദയവായി പിന്നീട് വീണ്ടും ശ്രമിക്കുക\",\n      \"updatedSuccess\": \"പ്രോംപ്റ്റ് അപ്ഡേറ്റ് ചെയ്‌തു\",\n      \"updatedSuccessDesc\": \"പ്രോംപ്റ്റ് വിജയകരമായി അപ്ഡേറ്റ് ചെയ്തു\",\n      \"deletedSuccess\": \"പ്രോംപ്റ്റ് ഇല്ലാതാക്കി\",\n      \"deletedSuccessDesc\": \"പ്രോംപ്റ്റ് വിജയകരമായി ഇല്ലാതാക്കി\"\n    }\n  },\n  \"manageShare\": {\n    \"title\": \"പങ്കിടുന്നത് കൈകാര്യം ചെയ്യുക\",\n    \"heading\": \"പേജ് പങ്കിടാനുള്ള URL കോൺഫിഗർ ചെയ്യുക\",\n    \"form\": {\n      \"url\": {\n        \"label\": \"പേജ് പങ്കിടാനുള്ള URL\",\n        \"placeholder\": \"പേജ് പങ്കിടാനുള്ള URL നല്കുക\",\n        \"required\": \"ദയവായി നിങ്ങളുടെ പേജ് പങ്കിടാനുള്ള URL നല്കുക!\",\n        \"help\": \"സ്വകാര്യതക്കായി, നിങ്ങള്‍ക്ക് സ്വന്തമായി പേജ് പങ്കിടുന്ന സൗകര്യം ഹോസ്റ്റ് ചെയ്യാനും അവിടെയുള്ള URL ഇവിടെ നല്കാനും കഴിയും. <anchor>കൂടുതല്‍ അറിയുക</anchor>.\"\n      }\n    },\n    \"webshare\": {\n      \"heading\": \"വെബ് പങ്കിടല്‍\",\n      \"columns\": {\n        \"title\": \"തലക്കെട്ട്\",\n        \"url\": \"URL\",\n        \"actions\": \"പ്രവർത്തനങ്ങൾ\"\n      },\n      \"tooltip\": {\n        \"delete\": \"പങ്കിടല്‍ ഇല്ലാതാക്കുക\"\n      },\n      \"confirm\": {\n        \"delete\": \"ഈ പങ്കിടല്‍ ഇല്ലാതാക്കണമെന്ന് തീർച്ചയാണോ? ഈ പ്രവർത്തനം പിന്നീട് പിൻവലിക്കാനാകില്ല.\"\n      },\n      \"label\": \"പേജ് ഷെയർ നിയന്ത്രിക്കുക\",\n      \"description\": \"പേജ് ഷെയർ സാങ്കേതികത സജ്ജീകരിക്കുക അല്ലെങ്കിൽ നിലവിളിക്കുക .\"\n    },\n    \"notification\": {\n      \"pageShareSuccess\": \"പേജ് പങ്കിടാനുള്ള URL വിജയകരമായി അപ്ഡേറ്റ് ചെയ്തു\",\n      \"someError\": \"എന്തോ തെറ്റായി. ദയവായി പിന്നീട് വീണ്ടും ശ്രമിക്കുക\",\n      \"webShareDeleteSuccess\": \"വെബ് പങ്കിടല്‍ വിജയകരമായി ഇല്ലാതാക്കി\"\n    }\n  },\n  \"ollamaSettings\": {\n    \"title\": \"Ollama സെറ്റിംഗുകൾ\",\n    \"heading\": \"Ollama കോൺഫിഗർ ചെയ്യുക\",\n    \"settings\": {\n      \"ollamaUrl\": {\n        \"label\": \"Ollama URL\",\n        \"placeholder\": \"Ollama URL നല്കുക\"\n      },\n      \"globalEnable\": {\n        \"label\": \"ഓളാമ ഇന്റഗ്രേഷൻ ആഗോളമായി പ്രവർത്തനക്ഷമമാക്കുക അല്ലെങ്കിൽ നിർജ്ജീവമാക്കുക\",\n        \"warning\": \"ഓളാമ ഇന്റഗ്രേഷൻ ആഗോളമായി നിർജ്ജീവമാക്കുന്നതിലൂടെ, പേജ് അസിസ്റ്റ് ഓളാമയിൽ നിന്ന് മോഡലുകൾ ലഭ്യമാക്കില്ല. നിങ്ങൾക്ക് <anchor>OpenAI അനുയോജ്യമായ API</anchor> വിഭാഗത്തിൽ നിന്ന് ഓളാമ ഇൻസ്റ്റൻസ് ചേർക്കാൻ കഴിയും, അത് നന്നായി പ്രവർത്തിക്കും.\"\n      },\n      \"advanced\": {\n        \"label\": \"Advance Ollama URL Configuration\",\n        \"urlRewriteEnabled\": {\n          \"label\": \"Enable or Disable Custom Origin URL\"\n        },\n        \"rewriteUrl\": {\n          \"label\": \"Custom Origin URL\",\n          \"placeholder\": \"Enter Custom Origin URL\"\n        },\n        \"autoCORSFix\": {\n          \"label\": \"സ്വയമേവ Ollama CORS പരിഹാരം പ്രവർത്തനക്ഷമമാക്കുക അല്ലെങ്കിൽ നിർജ്ജീവമാക്കുക\"\n        },\n        \"headers\": {\n          \"label\": \"കസ്റ്റം തലക്കെട്ടുകൾ\",\n          \"add\": \"തലക്കെട്ട് ചേർക്കുക\",\n          \"key\": {\n            \"label\": \"തലക്കെട്ട് കീ\",\n            \"placeholder\": \"അധികൃതപെടുത്തൽ\"\n          },\n          \"value\": {\n            \"label\": \"തലക്കെട്ട് മൂല്യം\",\n            \"placeholder\": \"ബെയറർ ടോക്കൺ\"\n          }\n        },\n        \"help\": \"ഏജ് അസിസ്റ്റന്റിൽ Ollama-യുമായി ബന്ധപ്പെടുമ്പോൾ ബന്ധതടസ്സം ഉണ്ടെങ്കിൽ, നിങ്ങൾക്ക് ഒരു വ്യക്തിഗത അസ്ഥിരത്വം URL കോൺഫിഗർ ചെയ്യാം. കോൺഫിഗറേഷനെക്കുറിച്ച് കൂടുതലറിയാൻ, <anchor>ഇവിടെ ക്ലിക്കുചെയ്യുക</anchor>.\"\n      }\n    }\n  },\n  \"manageSearch\": {\n    \"heading\": \"Web തിരയൽ സജ്ജമാക്കുക\",\n    \"title\": \"Web തിരയൽ നിയന്ത്രിക്കുക\"\n  },\n  \"about\": {\n    \"title\": \"വിവരങ്ങൾ\",\n    \"heading\": \"വിവരങ്ങൾ\",\n    \"chromeVersion\": \"പേജ് അസിസ്റ്റ് വേർഷൻ\",\n    \"ollamaVersion\": \"ഓളാമ വേർഷൻ\",\n    \"support\": \"താഴെ പറയുന്ന പ്ലാറ്റ്ഫോമുകളിലൂടെ ദാനം ചെയ്യുകയോ സ്പോൺസർ ചെയ്യുകയോ ചെയ്ത് പേജ് അസിസ്റ്റ് പ്രോജക്റ്റിനെ പിന്തുണയ്ക്കാവുന്നതാണ്:\",\n    \"koFi\": \"കോഫിയിൽ പിന്തുണയ്ക്കുക\",\n    \"githubSponsor\": \"ഗിറ്റ്ഹബ്ബിൽ സ്പോൺസർ ചെയ്യുക\",\n    \"githubRepo\": \"ഗിറ്റ്ഹബ്ബ് റെപ്പോസിറ്ററി\"\n  },\n  \"manageKnowledge\": {\n    \"title\": \"വിജ്ഞാനം നിര്‍വ്വഹിക്കുക\",\n    \"heading\": \"വിജ്ഞാനാധാരം കോണ്‍ഫിഗര്‍ ചെയ്യുക\"\n  },\n  \"rag\": {\n    \"title\": \"Pipeline സെറ്റിംഗുകൾ\",\n    \"ragSettings\": {\n      \"label\": \"RAG സെറ്റിംഗുകൾ\",\n      \"model\": {\n        \"label\": \"എംബെഡിംഗ് മോഡല്‍\",\n        \"required\": \"ദയവായി ഒരു മോഡല്‍ തിരഞ്ഞെടുക്കുക\",\n        \"help\": \"`nomic-embed-text` പോലുള്ള എംബെഡിംഗ് മോഡലുകള്‍ ഉപയോഗിക്കുന്നത് വളരെ നന്നായിരിക്കും.\",\n        \"placeholder\": \"ഒരു മോഡല്‍ തിരഞ്ഞെടുക്കുക\"\n      },\n      \"chunkSize\": {\n        \"label\": \"ചങ്ക് വലുപ്പം\",\n        \"placeholder\": \"ചങ്ക് വലുപ്പം നല്കുക\",\n        \"required\": \"ദയവായി ചങ്ക് വലുപ്പം നല്കുക\"\n      },\n      \"chunkOverlap\": {\n        \"label\": \"ചങ്ക് ഓവര്‍ലാപ്പ്\",\n        \"placeholder\": \"ചങ്ക് ഓവര്‍ലാപ്പ് നല്കുക\",\n        \"required\": \"ദയവായി ചങ്ക് ഓവര്‍ലാപ്പ് നല്കുക\"\n      },\n      \"totalFilePerKB\": {\n        \"label\": \"വിജ്ഞാനാധാരത്തിന്റെ സ്ഥിര ഫയൽ അപ്‌ലോഡ് പരിധി\",\n        \"placeholder\": \"സ്ഥിര ഫയൽ അപ്‌ലോഡ് പരിധി നൽകുക (ഉദാ: 10)\",\n        \"required\": \"ദയവായി സ്ഥിര ഫയൽ അപ്‌ലോഡ് പരിധി നൽകുക\"\n      },\n      \"noOfRetrievedDocs\": {\n        \"label\": \"വീണ്ടെടുത്ത രേഖകളുടെ എണ്ണം\",\n        \"placeholder\": \"വീണ്ടെടുത്ത രേഖകളുടെ എണ്ണം നൽകുക\",\n        \"required\": \"ദയവായി വീണ്ടെടുത്ത രേഖകളുടെ എണ്ണം നൽകുക\"\n      },\n      \"splittingSeparator\": {\n        \"label\": \"വിഭജന ചിഹ്നം\",\n        \"placeholder\": \"വിഭജന ചിഹ്നം നൽകുക (ഉദാ: \\\\n\\\\n)\",\n        \"required\": \"ദയവായി ഒരു വിഭജന ചിഹ്നം നൽകുക\"\n      },\n      \"splittingStrategy\": {\n        \"label\": \"ടെക്സ്റ്റ് സ്പ്ലിറ്റർ\"\n      }\n    },\n    \"prompt\": {\n      \"label\": \"RAG പ്രോംപ്റ്റ് കോൺഫിഗർ ചെയ്യുക\",\n      \"option1\": \"സാധാരണ\",\n      \"option2\": \"വെബ്\",\n      \"alert\": \"സിസ്റ്റം പ്രോംപ്റ്റ് ഇവിടെ കോൺഫിഗർ ചെയ്യുന്നത് പഴയൗഖികമായി. ദയവായി പ്രോംപ്റ്റുകള്‍ ചേര്‍ക്കാനോ എഡിറ്റുചെയ്യാനോ മാനേജ് പ്രോംപ്റ്റ്‌സ് സെക്ഷന്‍ ഉപയോഗിക്കുക. ഈ സെക്ഷന്‍ ഭാവിയില്‍ നീക്കം ചെയ്യപ്പെടും.\",\n      \"systemPrompt\": \"സിസ്റ്റം പ്രോംപ്റ്റ്\",\n      \"systemPromptPlaceholder\": \"സിസ്റ്റം പ്രോംപ്റ്റ് നല്കുക\",\n      \"webSearchPrompt\": \"വെബ് തിരയല്‍ പ്രോംപ്റ്റ്\",\n      \"webSearchPromptHelp\": \"പ്രോംപ്റ്റില്‍ നിന്ന് `{search_results}` നീക്കം ചെയ്യരുത്.\",\n      \"webSearchPromptError\": \"ദയവായി ഒരു വെബ് തിരയല്‍ പ്രോംപ്റ്റ് നല്കുക\",\n      \"webSearchPromptPlaceholder\": \"വെബ് തിരയല്‍ പ്രോംപ്റ്റ് നല്കുക\",\n      \"webSearchFollowUpPrompt\": \"വെബ് തിരയല്‍ തുടര്‍പ്രോംപ്റ്റ്\",\n      \"webSearchFollowUpPromptHelp\": \"പ്രോംപ്റ്റില്‍ നിന്ന് `{chat_history}` യും `{question}` യും നീക്കം ചെയ്യരുത്.\",\n      \"webSearchFollowUpPromptError\": \"ദയവായി നിങ്ങളുടെ വെബ് തിരയല്‍ തുടര്‍പ്രോംപ്റ്റ് നല്കുക!\",\n      \"webSearchFollowUpPromptPlaceholder\": \"നിങ്ങളുടെ വെബ് തിരയല്‍ തുടര്‍പ്രോംപ്റ്റ്\"\n    }\n  },\n  \"chromeAiSettings\": {\n    \"title\": \"ക്രോം AI ക്രമീകരണങ്ങൾ\"\n  }\n}\n"
  },
  {
    "path": "src/assets/locale/ml/sidepanel.json",
    "content": "{\n    \"tooltip\": {\n        \"embed\": \"പേജ് പ്രോസസ്സ് ചെയ്യുന്നതിന് കുറച്ച് മിനിറ്റുകൾ എടുത്തേക്കാം. കാത്തിരിക്കൂ..\",\n        \"openwebui\": \"വെബ് യുഐ തുറക്കുക\"\n    }\n}"
  },
  {
    "path": "src/assets/locale/no/chrome.json",
    "content": "{\n    \"heading\": \"Konfigurer Chrome AI\",\n    \"status\": {\n        \"label\": \"Slå Chrome AI Support på eller av på Page Assist\"\n    },\n    \"error\": {\n        \"browser_not_supported\": \"Denne versjonen av Chrome støttes ikke av Gemini Nano-modellen. Vennligst oppdater til versjon 127 eller nyere\",\n        \"ai_not_supported\": \"Innstillingen chrome://flags/#prompt-api-for-gemini-nano er ikke tændt. Slå på innstillingen..\",\n        \"ai_not_ready\": \"Gemini Nano er ikke tilgjengelig; du må dobbeltsjekke Chrome-innstillingene.\",\n        \"internal_error\": \"Det oppsto en intern feil. Vennligst prøv på nytt senere.\"\n    },\n    \"errorDescription\": \"For å bruke Chrome AI, trenger du Chrome versjon 138 eller nyere. Følg disse trinnene:\\n\\n1. Gå til `chrome://flags/#prompt-api-for-gemini-nano` og aktiver \\\"Prompt API for Gemini Nano\\\".\\n2. Start Chrome på nytt for å bruke innstillingen.\\n3. Gå tilbake til denne siden og klikk \\\"Last ned modell\\\" — dette vil laste ned en modell på 4 GB for første gang.\\n4. Når nedlastingen er fullført, kan Gemini Nano aktiveres gjennom Page Assist.\",\n    \"downloadModel\": \"Last ned modell\",\n    \"modelDownloadWarning\": \"Dette vil laste ned en modell med en estimert nedlastingsstørrelse på mellom 1,5 GB og 2,4 GB. Sørg for at du har tilstrekkelig diskplass.\"\n}"
  },
  {
    "path": "src/assets/locale/no/common.json",
    "content": "{\n    \"pageAssist\": \"Sideassistent\",\n    \"selectAModel\": \"Velg en modell\",\n    \"save\": \"Lagre\",\n    \"saved\": \"Lagret\",\n    \"cancel\": \"Avbryt\",\n    \"retry\": \"Prøv igjen\",\n    \"share\": {\n        \"tooltip\": {\n            \"share\": \"Del\"\n        },\n        \"modal\": {\n            \"title\": \"Del lenke til chatten\"\n        },\n        \"form\": {\n            \"defaultValue\": {\n                \"name\": \"Anonym\",\n                \"title\": \"Navnløs chat\"\n            },\n            \"title\": {\n                \"label\": \"Chattittel\",\n                \"placeholder\": \"Skriv inn chattittel\",\n                \"required\": \"Chattittel er nødvendig\"\n            },\n            \"name\": {\n                \"label\": \"Ditt navn\",\n                \"placeholder\": \"Skriv inn ditt navn\",\n                \"required\": \"Ditt navn er nødvendig\"\n            },\n            \"btn\": {\n                \"save\": \"Generer en lenke\",\n                \"saving\": \"Genererer lenke...\"\n            }\n        },\n        \"notification\": {\n            \"successGenerate\": \"Lenke kopiert til utklippstavle\",\n            \"failGenerate\": \"Kunne ikke generere lenke\"\n        }\n    },\n    \"copyToClipboard\": \"Kopier til utklippstavle\",\n    \"webSearch\": \"Søker på internett\",\n    \"regenerate\": \"Regenerer\",\n    \"continue\": \"Continue Response\",\n    \"edit\": \"Endre\",\n    \"delete\": \"Slett\",\n    \"saveAndSubmit\": \"Lagre & Send inn\",\n    \"editMessage\": {\n        \"placeholder\": \"Skriv en melding...\"\n    },\n    \"submit\": \"Send inn\",\n    \"noData\": \"Ingen data\",\n    \"noHistory\": \"Ingen chathistorikk\",\n    \"chatWithCurrentPage\": \"Chat med nåværende side\",\n    \"beta\": \"Beta\",\n    \"tts\": \"Les opp\",\n    \"currentChatModelSettings\": \"Nåværende chatmodellinnstillinger\",\n    \"modelSettings\": {\n        \"label\": \"Modellinnstillinger\",\n        \"description\": \"Konfigurer modellinnstillingene for alle chatter\",\n        \"form\": {\n            \"keepAlive\": {\n                \"label\": \"Hold i live\",\n                \"help\": \"kontrollerer hvor lenge modellen vil forbli lastet i minnet etter forespørselen (standard: 5m)\",\n                \"placeholder\": \"Skriv inn lengden på økten (f.eks. 5m, 10m, 1t)\"\n            },\n            \"temperature\": {\n                \"label\": \"Temperatur\",\n                \"placeholder\": \"Skriv inn temperaturverdi (f.eks. 0.7, 1.0)\"\n            },\n            \"numCtx\": {\n                \"label\": \"Kontekstvindustørrelse (num_ctx)\",\n                \"placeholder\": \"Skriv inn kontekstvindustørrelse-verdi (standard: 2048)\"\n            },\n            \"numPredict\": {\n                \"label\": \"Maks Tokens (num_predict)\",\n                \"placeholder\": \"Skriv inn Maks Tokens-verdi (f.eks. 2048, 4096)\"\n            },\n            \"seed\": {\n                \"label\": \"Seed\",\n                \"placeholder\": \"Skriv inn seedverdi (f.eks. 1234)\",\n                \"help\": \"Reproduserbarhet av modellutdata\"\n            },\n            \"topK\": {\n                \"label\": \"Topp K\",\n                \"placeholder\": \"Skriv inn Topp K-verdi (f.eks. 40, 100)\"\n            },\n            \"topP\": {\n                \"label\": \"Topp P\",\n                \"placeholder\": \"Skriv inn Topp P-verdi (f.eks. 0.9, 0.95)\"\n            },\n            \"numGpu\": {\n                \"label\": \"Antall GPUer\",\n                \"placeholder\": \"Skriv inn antall lag som sendes til GPU(er)\"\n            },\n            \"systemPrompt\": {\n                \"label\": \"Midlertidig systemprompt\",\n                \"placeholder\": \"Skriv inn systemprompt\",\n                \"help\": \"Dette er en rask måte å sette systemprompt i den nåværende chatten, som vil overstyre den valgte systemprompt hvis den finnes.\"\n            }\n        },\n        \"advanced\": \"Flere modellinnstillinger\"\n    },\n    \"copilot\": {\n        \"summary\": \"Oppsummer\",\n        \"explain\": \"Forklar\",\n        \"rephrase\": \"Omformulér\",\n        \"translate\": \"Oversett\",\n        \"custom\": \"Egendefinert\"\n    },\n    \"citations\": \"Sitater\",\n    \"downloadCode\": \"Last ned kode\",\n    \"date\": {\n        \"pinned\": \"Festet\",\n        \"today\": \"I dag\",\n        \"yesterday\": \"I går\",\n        \"last7Days\": \"Siste 7 dager\",\n        \"older\": \"Eldre\"\n    },\n    \"range\": {\n        \"deleteConfirm\": {\n            \"pinned\": \"Er du sikker på at du vil slette alle festede meldinger?\",\n            \"today\": \"Er du sikker på at du vil slette alle meldinger fra i dag?\",\n            \"yesterday\": \"Er du sikker på at du vil slette alle meldinger fra i går?\",\n            \"last7Days\": \"Er du sikker på at du vil slette alle meldinger fra de siste 7 dagene?\",\n            \"older\": \"Er du sikker på at du vil slette alle eldre meldinger?\"\n        },\n        \"tooltip\": {\n            \"pinned\": \"Slett alle festede meldinger\",\n            \"today\": \"Slett alle meldinger fra i dag\",\n            \"yesterday\": \"Slett alle meldinger fra i går\",\n            \"last7Days\": \"Slett alle meldinger fra de siste 7 dagene\",\n            \"older\": \"Slett alle eldre meldinger\"\n        }\n    },    \"pin\": \"Fest\",\n    \"unpin\": \"Løsne\",\n    \"generationInfo\": \"Generasjonsinformasjon\",\n    \"sidebarChat\": \"Sidepanel-chat\",\n    \"reasoning\": {\n        \"thinking\": \"Tenker....\",\n        \"thought\": \"Tenkte i {{time}}\"\n    },\n    \"embeddingGen\": \"Oppretter embeddings, dette kan ta litt tid\",\n    \"semanticSearch\": \"Utfører semantisk søk\",\n    \"downloading\": \"Laster ned\",\n    \"cancelPullingModel\": {\n        \"confirm\": \"Er du sikker på at du vil avbryte nedlastingen? Dette vil stoppe nedlastingsprosessen. Ifølge Ollama-dokumentasjonen kan du starte på nytt fra der du slapp.\"\n    }\n}"
  },
  {
    "path": "src/assets/locale/no/knowledge.json",
    "content": "{\n    \"addBtn\": \"Legg Til Ny Kunnskap\",\n    \"columns\": {\n        \"title\": \"Tittel\",\n        \"status\": \"Status\",\n        \"embeddings\": \"Embedding Modell\",\n        \"createdAt\": \"Opprettet På\",\n        \"action\": \"Handlinger\"\n    },\n    \"expandedColumns\": {\n        \"name\": \"Navn\"\n    },\n    \"confirm\": {\n        \"delete\": \"Er du sikker på at du vil slette denne kunnskapen?\"\n    },\n    \"deleteSuccess\": \"Kunnskap slettet med suksess\",\n    \"status\": {\n        \"pending\": \"Venter\",\n        \"finished\": \"Ferdig\",\n        \"processing\": \"Behandler\",\n        \"failed\": \"Mislyktes\"\n    },\n    \"addKnowledge\": \"Legg Til Kunnskap\",\n    \"updateKnowledge\": \"Legg Til Kilde\",\n    \"form\": {\n        \"title\": {\n            \"label\": \"Kunnskapstittel\",\n            \"placeholder\": \"Skriv inn kunnskapstittel\",\n            \"required\": \"Kunnskapstittel er nødvendig\"\n        },\n        \"uploadFile\": {\n            \"label\": \"Last Opp Filer\",\n            \"uploadText\": \"Dra og slipp filen her og klikk for å laste opp\",\n            \"uploadHint\": \"Støttede filtyper: .pdf, .csv, .txt, .md, .docx\",\n            \"required\": \"En fil er nødvendig\"\n        },\n        \"submit\": \"Send Inn\",\n        \"success\": \"Kunnskap lagt til med suksess\"\n    },\n    \"noEmbeddingModel\": \"Vennligst legg til en embedding-modell fra RAG-innstillingene først\"\n}"
  },
  {
    "path": "src/assets/locale/no/openai.json",
    "content": "{\n    \"settings\": \"OpenAI-kompatibel API\",\n    \"heading\": \"OpenAI-kompatibel API\",\n    \"subheading\": \"Administrer og konfigurer dine OpenAI API-kompatible leverandører her.\",\n    \"addBtn\": \"Legg til leverandør\",\n    \"table\": {\n        \"name\": \"Leverandørnavn\",\n        \"baseUrl\": \"Base-URL\",\n        \"actions\": \"Handling\"\n    },\n    \"modal\": {\n        \"titleAdd\": \"Legg til ny leverandør\",\n        \"name\": {\n            \"label\": \"Leverandørnavn\",\n            \"required\": \"Leverandørnavn er påkrevd.\",\n            \"placeholder\": \"Skriv inn leverandørnavn\"\n        },\n        \"baseUrl\": {\n            \"label\": \"Base-URL\",\n            \"help\": \"Base-URL-en til OpenAI API-leverandøren. f.eks. (http://localhost:1234/v1)\",\n            \"required\": \"Base-URL er påkrevd.\",\n            \"placeholder\": \"Skriv inn base-URL\"\n        },\n        \"apiKey\": {\n            \"label\": \"API-nøkkel\",\n            \"required\": \"API-nøkkel er påkrevd.\",\n            \"placeholder\": \"Skriv inn API-nøkkel\"\n        },\n        \"submit\": \"Lagre\",\n        \"update\": \"Oppdater\",\n        \"deleteConfirm\": \"Er du sikker på at du vil slette denne leverandøren?\",\n        \"model\": {\n            \"title\": \"Modelliste\",\n            \"subheading\": \"Vennligst velg chatmodellene du ønsker å bruke med denne leverandøren.\",\n            \"success\": \"Nye modeller ble lagt til.\"\n        },\n        \"tipLMStudio\": \"Page Assist vil automatisk hente modellene du lastet inn i LM Studio. Du trenger ikke å legge dem til manuelt.\"\n    },\n    \"addSuccess\": \"Leverandør ble lagt til.\",\n    \"deleteSuccess\": \"Leverandør ble slettet.\",\n    \"updateSuccess\": \"Leverandør ble oppdatert.\",\n    \"delete\": \"Slett\",\n    \"edit\": \"Rediger\",\n    \"newModel\": \"Legg til modeller for leverandør\",\n    \"noNewModel\": \"For LMStudio, Ollama, Llamafile, henter vi dynamisk. Ingen manuell tillegging nødvendig.\",\n    \"searchModel\": \"Søk etter modell\",\n    \"selectAll\": \"Velg alle\",\n    \"save\": \"Lagre\",\n    \"saving\": \"Lagrer...\",\n    \"manageModels\": {\n        \"columns\": {\n            \"name\": \"Modellnavn\",\n            \"model_type\": \"Modelltype\",\n            \"model_id\": \"Modell-ID\",\n            \"provider\": \"Leverandørnavn\",\n            \"actions\": \"Handling\",\n            \"nickname\": \"Modellkallenavn\"\n        },\n        \"tooltip\": {\n            \"delete\": \"Slett\"\n        },\n        \"confirm\": {\n            \"delete\": \"Er du sikker på at du vil slette denne modellen?\"\n        },\n        \"modal\": {\n            \"title\": \"Legg til tilpasset modell\",\n            \"form\": {\n                \"name\": {\n                    \"label\": \"Modell-ID\",\n                    \"placeholder\": \"llama3.2\",\n                    \"required\": \"Modell-ID er påkrevd.\"\n                },\n                \"provider\": {\n                    \"label\": \"Leverandør\",\n                    \"placeholder\": \"Velg leverandør\",\n                    \"required\": \"Leverandør er påkrevd.\"\n                },\n                \"type\": {\n                    \"label\": \"Modelltype\"\n                }\n            }\n        }\n    },\n    \"noModelFound\": \"Ingen modell funnet. Sørg for at du har lagt til riktig leverandør med base-URL og API-nøkkel.\",\n    \"radio\": {\n        \"chat\": \"Chatmodell\",\n        \"embedding\": \"Innbyggingsmodell\",\n        \"chatInfo\": \"brukes for chatfullføring og samtalegenering\",\n        \"embeddingInfo\": \"brukes for RAG og andre semantiske søkerelaterte oppgaver.\"\n    },\n    \"nicknameModal\": {\n        \"title\": \"Legg til / Rediger modellkallenavn\",\n        \"form\": {\n            \"modelName\": {\n                \"label\": \"Modellnavn\",\n                \"placeholder\": \"Skriv inn modellnavn\",\n                \"required\": \"Modellnavn er påkrevd.\"\n            },\n            \"modelAvatar\": {\n                \"label\": \"Modellavatar\",\n                \"placeholder\": \"Skriv inn modellavatar\",\n                \"help\": \"Vennligst skriv inn URL-en til modellavataren. Dette bildet vil vises i chatvinduet.\"\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/assets/locale/no/option.json",
    "content": "{\n    \"newChat\": \"Ny Chat\",\n    \"selectAPrompt\": \"Velg en Prompt\",\n    \"githubRepository\": \"GitHub Repository\",\n    \"settings\": \"Innstillinger\",\n    \"sidebarTitle\": \"Chathistorikk\",\n    \"error\": \"Feil\",\n    \"somethingWentWrong\": \"Noe gikk galt\",\n    \"validationSelectModel\": \"Vennligst velg en modell for å fortsette\",\n    \"deleteHistoryConfirmation\": \"Er du sikker på at du vil slette denne historikken?\",\n    \"editHistoryTitle\": \"Skriv inn en ny tittel\",\n    \"temporaryChat\": \"Midlertidig Chat\",\n    \"more\": {\n        \"copy\": {\n            \"group\": \"Kopier\",\n            \"asText\": \"Kopier som tekst\",\n            \"asMarkdown\": \"Kopier som Markdown\",\n            \"success\": \"Kopiert til utklippstavlen!\"\n        },\n        \"download\": {\n            \"group\": \"Last ned\",\n            \"text\": \"Tekstfil (.txt)\",\n            \"markdown\": \"Markdown (.md)\",\n            \"json\": \"JSON-fil (.json)\"\n        },\n        \"share\": \"Del\"\n    }\n}"
  },
  {
    "path": "src/assets/locale/no/playground.json",
    "content": "{\n    \"ollamaState\": {\n        \"searching\": \"Søker etter din Ollama 🦙\",\n        \"running\": \"Ollama kjører 🦙\",\n        \"notRunning\": \"Kan ikke koble til Ollama 🦙\",\n        \"connectionError\": \"Det ser ut til at du har en tilkoblingsfeil. Vennligst se denne <anchor>dokumentasjonen</anchor> for feilsøking.\"\n    },\n    \"formError\": {\n        \"noModel\": \"Vennligst velg en modell\",\n        \"noEmbeddingModel\": \"Vennligst velg en embedding-modell under innstillinger > RAG-siden\"\n    },\n    \"form\": {\n        \"textarea\": {\n            \"placeholder\": \"Skriv en melding...\"\n        },\n        \"webSearch\": {\n            \"on\": \"På\",\n            \"off\": \"Av\"\n        }\n    },\n    \"tooltip\": {\n        \"searchInternet\": \"Søk på Internett\",\n        \"speechToText\": \"Tale til Tekst\",\n        \"uploadImage\": \"Last opp Bilde\",\n        \"stopStreaming\": \"Stopp Streaming\",\n        \"knowledge\": \"Kunnskap\",\n        \"clearContext\": \"Tøm Kontekst\"\n    },\n    \"sendWhenEnter\": \"Søk når Enter trykkes\",\n    \"welcome\": \"Hei! Hvordan kan jeg hjelpe deg i dag?\",\n    \"useOCR\": \"Trekk ut tekst fra bilde (OCR)\"\n}"
  },
  {
    "path": "src/assets/locale/no/settings.json",
    "content": "{\n  \"generalSettings\": {\n    \"title\": \"Generelle Innstillinger\",\n    \"settings\": {\n      \"heading\": \"Web UI Innstillinger\",\n      \"speechRecognitionLang\": {\n        \"label\": \"Talegjenkjenning Språk\",\n        \"placeholder\": \"Velg et språk\"\n      },\n      \"language\": {\n        \"label\": \"Språk\",\n        \"placeholder\": \"Velg et språk\"\n      },\n      \"darkMode\": {\n        \"label\": \"Endre Tema\",\n        \"options\": {\n          \"light\": \"Lyst\",\n          \"dark\": \"Mørkt\"\n        }\n      },\n      \"copilotResumeLastChat\": {\n        \"label\": \"Gjenoppta siste chat ved åpning av SidePanel (copilot)\"\n      },\n      \"turnOnChatWithWebsite\": {\n        \"label\": \"Aktiver Chat med Nettsted som standard (Copilot)\"\n      },\n      \"webUIResumeLastChat\": {\n        \"label\": \"Gjenoppta siste chat når Web UI åpnes\"\n      },\n      \"hideCurrentChatModelSettings\": {\n        \"label\": \"Skjul gjeldende chat modell innstillinger\"\n      },\n      \"restoreLastChatModel\": {\n        \"label\": \"Gjenopprett sist brukte chatmodell for fremtidig bruk\"\n      },\n      \"sendNotificationAfterIndexing\": {\n        \"label\": \"Send varsel etter ferdigbehandling av kunnskapsbasen\"\n      },\n      \"generateTitle\": {\n        \"label\": \"Generer tittel med AI\"\n      },\n      \"ollamaStatus\": {\n        \"label\": \"Aktiver eller deaktiver Ollama tilkoblingsstatussjekk\"\n      },\n      \"wideMode\": {\n        \"label\": \"Aktiver bredskjerm-modus\"\n      },\n      \"openReasoning\": {\n        \"label\": \"Åpne Resonnement Sammenfoldet som standard\"\n      },\n      \"userChatBubble\": {\n        \"label\": \"Bruk chatboble for bruker meldinger\"\n      },\n      \"autoCopyResponseToClipboard\": {\n        \"label\": \"Automatisk kopier svar til utklippstavle\"\n      },\n      \"useMarkdownForUserMessage\": {\n        \"label\": \"Aktiver Markdown-formatering for brukermeldinger\"\n      },\n      \"copyAsFormattedText\": {\n        \"label\": \"Kopier som formatert tekst\"\n      },\n      \"tabMentionsEnabled\": {\n        \"label\": \"Aktiver Fane Henvisninger (@tab)\"\n      },\n      \"pasteLargeTextAsFile\": {\n        \"label\": \"Lim inn stor tekst som fil\"\n      },\n      \"sidepanelTemporaryChat\": {\n        \"label\": \"Aktiver midlertidig chat i sidepanelet som standard\"\n      }\n    },\n    \"sidepanelRag\": {\n      \"heading\": \"Gjenfinningsinnstillinger\",\n      \"ragEnabled\": {\n        \"label\": \"Aktiver Innbygging og Gjenfinning\"\n      },\n      \"maxWebsiteContext\": {\n        \"label\": \"Maksimal Innholdsstørrelse for Full Kontekst Modus\",\n        \"placeholder\": \"Innholdsstørrelse (standard 4028)\"\n      }\n    },\n    \"webSearch\": {\n      \"heading\": \"Administrer Web Søk\",\n      \"searchMode\": {\n        \"label\": \"Søkemodus\"\n      },\n      \"provider\": {\n        \"label\": \"Søkemotor\",\n        \"placeholder\": \"Velg en søkemotor\"\n      },\n      \"totalSearchResults\": {\n        \"label\": \"Antall søkeresultater\",\n        \"placeholder\": \"Skriv inn antall søkeresultater\"\n      },\n      \"visitSpecificWebsite\": {\n        \"label\": \"Besøk nettstedet nevnt i samtalen\"\n      },\n      \"searxng\": {\n        \"url\": {\n          \"label\": \"SearXNG URL\"\n        }\n      },\n      \"braveApi\": {\n        \"label\": \"Brave API Nøkkel\",\n        \"placeholder\": \"Skriv inn din Brave API nøkkel\"\n      },\n      \"searchOnByDefault\": {\n        \"label\": \"Internett-søk PÅ som standard\"\n      }\n    },\n    \"system\": {\n      \"heading\": \"Systeminnstillinger\",\n      \"storageSyncEnabled\": {\n        \"label\": \"Aktiver nettleser lagringssynkronisering (synkroniser innstillinger på tvers av enheter)\"\n      },\n      \"deleteChatHistory\": {\n        \"label\": \"System Tilbakestilling\",\n        \"button\": \"Tilbakestill Alt\",\n        \"confirm\": \"Er du sikker på at du vil utføre en system tilbakestilling? Dette vil slette alle data og kan ikke angres.\"\n      },\n      \"export\": {\n        \"label\": \"Eksporter all data (chathistorikk, kunnskapsbase, forespørsler og innstillinger)\",\n        \"button\": \"Eksporter data\",\n        \"success\": \"Eksport fullført\"\n      },\n      \"import\": {\n        \"label\": \"Importer all data (chathistorikk, kunnskapsbase, forespørsler og innstillinger)\",\n        \"button\": \"Importer data\",\n        \"success\": \"Import fullført\",\n        \"error\": \"Importfeil\"\n      }\n    },\n    \"tts\": {\n      \"heading\": \"Tekst-til-tale Innstillinger\",\n      \"ttsEnabled\": {\n        \"label\": \"Legg til Tekst-til-Tale\"\n      },\n      \"ttsAutoPlay\": {\n        \"label\": \"Automatisk avspilling av stemmerespons etter fullføring\"\n      },\n      \"ttsProvider\": {\n        \"label\": \"Tekst-til-Tale Tilbyder\",\n        \"placeholder\": \"Velg en tilbyder\"\n      },\n      \"ttsVoice\": {\n        \"label\": \"Tekst-til-Tale Stemme\",\n        \"placeholder\": \"Velg en stemme\"\n      },\n      \"ssmlEnabled\": {\n        \"label\": \"Aktiver SSML (Speech Synthesis Markup Language)\"\n      },\n      \"removeReasoningTagTTS\": {\n        \"label\": \"Fjern Resonneringsmerke fra TTS\"\n      }\n    },\n    \"stt\": {\n      \"heading\": \"Tale-til-tekst Innstillinger\",\n      \"autoStopTimeout\": {\n        \"label\": \"Auto-stopp Tidsavbrudd (ms)\",\n        \"placeholder\": \"Skriv inn auto-stopp tidsavbrudd i millisekunder\"\n      },\n      \"autoSubmitVoiceMessage\": {\n        \"label\": \"Auto-send Talemelding\"\n      }\n    }\n  },\n  \"manageModels\": {\n    \"title\": \"Administrer Modeller\",\n    \"addBtn\": \"Legg til ny Modell\",\n    \"columns\": {\n      \"name\": \"Navn\",\n      \"digest\": \"Digest\",\n      \"modifiedAt\": \"Endret den\",\n      \"size\": \"Størrelse\",\n      \"actions\": \"Handlinger\"\n    },\n    \"expandedColumns\": {\n      \"parentModel\": \"Overordnet modell\",\n      \"format\": \"Format\",\n      \"family\": \"Familie\",\n      \"parameterSize\": \"Parameterstørrelse\",\n      \"quantizationLevel\": \"Kvantifiseringsnivå\"\n    },\n    \"tooltip\": {\n      \"delete\": \"Slett Modell\",\n      \"repull\": \"Hent Modell På Nytt\"\n    },\n    \"confirm\": {\n      \"delete\": \"Er du sikker på at du vil slette denne modellen?\",\n      \"repull\": \"Er du sikker på at du vil hente denne modellen på nytt?\"\n    },\n    \"modal\": {\n      \"title\": \"Legg til Ny Modell\",\n      \"placeholder\": \"Skriv inn Modellnavn\",\n      \"pull\": \"Hent Modell\"\n    },\n    \"notification\": {\n      \"pullModel\": \"Henter Modell\",\n      \"pullModelDescription\": \"Henter {{modelName}} modell. For flere detaljer, sjekk utvidelsesikonet.\",\n      \"success\": \"Suksess\",\n      \"error\": \"Feil\",\n      \"successDescription\": \"Modellen ble hentet vellykket\",\n      \"successDeleteDescription\": \"Modellen ble slettet vellykket\",\n      \"someError\": \"Noe gikk galt. Vennligst prøv igjen senere\"\n    }\n  },\n  \"managePrompts\": {\n    \"title\": \"Administrer Prompts\",\n    \"addBtn\": \"Legg til Ny Prompt\",\n    \"option1\": \"Normal\",\n    \"option2\": \"RAG\",\n    \"questionPrompt\": \"Spørsmålsprompt\",\n    \"segmented\": {\n      \"custom\": \"Tilpassede Prompts\",\n      \"copilot\": \"Copilot Prompts\"\n    },\n    \"columns\": {\n      \"title\": \"Tittel\",\n      \"prompt\": \"Prompt\",\n      \"type\": \"Prompttype\",\n      \"actions\": \"Handlinger\"\n    },\n    \"systemPrompt\": \"Systemprompt\",\n    \"quickPrompt\": \"Hurtigprompt\",\n    \"tooltip\": {\n      \"delete\": \"Slett Prompt\",\n      \"edit\": \"Endre Prompt\"\n    },\n    \"confirm\": {\n      \"delete\": \"Er du sikker på at du vil slette denne prompten? Denne handlingen kan ikke angres.\"\n    },\n    \"modal\": {\n      \"addTitle\": \"Legg til ny Prompt\",\n      \"editTitle\": \"Endre Prompt\"\n    },\n    \"form\": {\n      \"title\": {\n        \"label\": \"Tittel\",\n        \"placeholder\": \"Min Kule Prompt\",\n        \"required\": \"Vennligst skriv inn en tittel\"\n      },\n      \"prompt\": {\n        \"label\": \"Prompt\",\n        \"placeholder\": \"Skriv inn Prompt\",\n        \"required\": \"Vennligst skriv inn en prompt\",\n        \"help\": \"Du kan bruke {key} som variabel i din prompt.\",\n        \"missingTextPlaceholder\": \"Variabelen {text} mangler i prompten. Vennligst legg til dette.\"\n      },\n      \"isSystem\": {\n        \"label\": \"Er Systemprompt\"\n      },\n      \"btnSave\": {\n        \"saving\": \"Legger til Prompt...\",\n        \"save\": \"Legg til Prompt\"\n      },\n      \"btnEdit\": {\n        \"saving\": \"Oppdaterer Prompt...\",\n        \"save\": \"Oppdater Prompt\"\n      }\n    },\n    \"notification\": {\n      \"addSuccess\": \"Prompt Lagt Til\",\n      \"addSuccessDesc\": \"Prompt ble lagt til vellykket\",\n      \"error\": \"Feil\",\n      \"someError\": \"Noe gikk galt. Vennligst prøv igjen senere\",\n      \"updatedSuccess\": \"Prompt Oppdatert\",\n      \"updatedSuccessDesc\": \"Prompt ble oppdatert vellykket\",\n      \"deletedSuccess\": \"Prompt Slettet\",\n      \"deletedSuccessDesc\": \"Prompt ble slettet vellykket\"\n    }\n  },\n  \"manageShare\": {\n    \"title\": \"Administrer Deling\",\n    \"heading\": \"Konfigurer Side deling URL\",\n    \"form\": {\n      \"url\": {\n        \"label\": \"Side Deling URL\",\n        \"placeholder\": \"Skriv inn side deling URL\",\n        \"required\": \"Vennligst skriv inn din Side deling URL!\",\n        \"help\": \"For personvern kan du selv hoste side delingen og angi URL-en her. <anchor>Lær Mer</anchor>.\"\n      }\n    },\n    \"webshare\": {\n      \"heading\": \"Web Deling\",\n      \"columns\": {\n        \"title\": \"Tittel\",\n        \"url\": \"URL\",\n        \"actions\": \"Handlinger\"\n      },\n      \"tooltip\": {\n        \"delete\": \"Slett Deling\"\n      },\n      \"confirm\": {\n        \"delete\": \"Er du sikker på at du vil slette denne delingen? Dette kan ikke angres.\"\n      },\n      \"label\": \"Administrer Side Deling\",\n      \"description\": \"Legg til eller deaktiver side delingsfunksjonen\"\n    },\n    \"notification\": {\n      \"pageShareSuccess\": \"Side Deling URL oppdatert vellykket\",\n      \"someError\": \"Noe gikk galt. Vennligst prøv igjen senere\",\n      \"webShareDeleteSuccess\": \"Webdeling ble slettet vellykket\"\n    }\n  },\n  \"ollamaSettings\": {\n    \"title\": \"Ollama Innstillinger\",\n    \"heading\": \"Konfigurer Ollama\",\n    \"settings\": {\n      \"ollamaUrl\": {\n        \"label\": \"Ollama URL\",\n        \"placeholder\": \"Skriv inn Ollama URL\"\n      },\n      \"globalEnable\": {\n        \"label\": \"Aktiver eller Deaktiver Ollama-integrasjon Globalt\",\n        \"warning\": \"Ved å deaktivere Ollama-integrasjon globalt, vil ikke Page Assist hente modeller fra Ollama. Du kan fortsatt legge til Ollama-instans fra <anchor>OpenAI-kompatibel API</anchor>-seksjonen som vil fungere fint.\"\n      },\n      \"advanced\": {\n        \"label\": \"Avansert Ollama URL-konfigurasjon\",\n        \"urlRewriteEnabled\": {\n          \"label\": \"Aktiver eller deaktiver tilpasset opprinnelses-URL\"\n        },\n        \"rewriteUrl\": {\n          \"label\": \"Tilpasset opprinnelses-URL\",\n          \"placeholder\": \"Skriv inn tilpasset opprinnelses-URL\"\n        },\n        \"autoCORSFix\": {\n          \"label\": \"Aktiver eller deaktiver automatisk Ollama CORS-fiks\"\n        },\n        \"headers\": {\n          \"label\": \"Tilpass Headers\",\n          \"LeggTil\": \"Legg til Header\",\n          \"key\": {\n            \"label\": \"Header Nøkkel\",\n            \"placeholder\": \"Autorisasjon\"\n          },\n          \"value\": {\n            \"label\": \"Header Verdi\",\n            \"placeholder\": \"Bearer token\"\n          }\n        },\n        \"help\": \"Hvis du har forbindelsesproblemer med Ollama på Page Assist, kan du konfigurere en brukerdefinert opprinnelses-URL. For mer informasjon om konfigurasjonen, <anchor>klikk her</anchor>.\"\n      }\n    }\n  },\n  \"manageSearch\": {\n    \"title\": \"Administrer Web Søk\",\n    \"heading\": \"Konfigurer Web Søk\"\n  },\n  \"about\": {\n    \"title\": \"Om\",\n    \"heading\": \"Om\",\n    \"chromeVersion\": \"Page Assist Versjon\",\n    \"ollamaVersion\": \"Ollama Versjon\",\n    \"support\": \"Du kan støtte Page Assist-prosjektet ved å donere eller sponse via følgende plattformer:\",\n    \"koFi\": \"Støtt på Ko-fi\",\n    \"githubSponsor\": \"Spons på GitHub\",\n    \"githubRepo\": \"GitHub Repository\"\n  },\n  \"manageKnowledge\": {\n    \"title\": \"Administrer Kunnskap\",\n    \"heading\": \"Konfigurer Kunnskapsbase\"\n  },\n  \"rag\": {\n    \"title\": \"Pipeline Innstillinger\",\n    \"ragSettings\": {\n      \"label\": \"RAG Innstillinger\",\n      \"model\": {\n        \"label\": \"Embedding Modell\",\n        \"required\": \"Vennligst velg en modell\",\n        \"help\": \"Det anbefales sterkt å bruke embeddingsmodeller som `nomic-embed-text`.\",\n        \"placeholder\": \"Velg en modell\"\n      },\n      \"chunkSize\": {\n        \"label\": \"Delstørrelse\",\n        \"placeholder\": \"Skriv inn delstørrelse\",\n        \"required\": \"Vennligst skriv inn en delstørrelse\"\n      },\n      \"chunkOverlap\": {\n        \"label\": \"Deloverlapp\",\n        \"placeholder\": \"Skriv inn deloverlapp\",\n        \"required\": \"Vennligst skriv inn deloverlapp\"\n      },\n      \"totalFilePerKB\": {\n        \"label\": \"Kunnskapsbase standard filopplastingsgrense\",\n        \"placeholder\": \"Skriv inn standard filopplastingsgrense (f.eks. 10)\",\n        \"required\": \"Vennligst skriv inn standard filopplastingsgrense\"\n      },\n      \"noOfRetrievedDocs\": {\n        \"label\": \"Antall hentede dokumenter\",\n        \"placeholder\": \"Skriv inn antall hentede dokumenter\",\n        \"required\": \"Vennligst skriv inn antall hentede dokumenter\"\n      },\n      \"splittingSeparator\": {\n        \"label\": \"Separator\",\n        \"placeholder\": \"Skriv inn separator (f.eks. \\\\n\\\\n)\",\n        \"required\": \"Vennligst skriv inn en separator\"\n      },\n      \"splittingStrategy\": {\n        \"label\": \"Tekstdeler\"\n      }\n    },\n    \"prompt\": {\n      \"label\": \"Konfigurer RAG Prompt\",\n      \"option1\": \"Normal\",\n      \"option2\": \"Web\",\n      \"alert\": \"Konfigurering av systemprompt her er foreldet. Vennligst bruk Administrer Prompts-seksjonen for å legge til eller endre prompts. Denne seksjonen vil bli fjernet i fremtidige versjoner.\",\n      \"systemPrompt\": \"Systemprompt\",\n      \"systemPromptPlaceholder\": \"Skriv inn systemprompt\",\n      \"webSearchPrompt\": \"Websøke-prompt\",\n      \"webSearchPromptHelp\": \"Ikke fjern `{search_results}` fra prompten.\",\n      \"webSearchPromptError\": \"Vennligst skriv inn en websøke-prompt\",\n      \"webSearchPromptPlaceholder\": \"Skriv inn websøke-prompt\",\n      \"webSearchFollowUpPrompt\": \"Oppfølgingsprompt for websøking\",\n      \"webSearchFollowUpPromptHelp\": \"Ikke fjern `{chat_history}` og `{question}` fra prompten.\",\n      \"webSearchFollowUpPromptError\": \"Vennligst skriv inn din oppfølgingsprompt for websøking!\",\n      \"webSearchFollowUpPromptPlaceholder\": \"Din oppfølgingsprompt for websøking\"\n    }\n  },\n  \"chromeAiSettings\": {\n    \"title\": \"Chrome AI Innstillinger\"\n  }\n}\n"
  },
  {
    "path": "src/assets/locale/no/sidepanel.json",
    "content": "{\n    \"tooltip\": {\n        \"embed\": \"Det kan ta noen minutter å bygge din siden. Vennligst vent...\",\n        \"clear\": \"Slett chathistorikken\",\n        \"history\": \"Chathistorikk\",\n        \"openwebui\": \"Åpne WebUI\"\n    }\n}"
  },
  {
    "path": "src/assets/locale/pt-BR/chrome.json",
    "content": "{\n    \"heading\": \"Configurar Chrome AI\",\n    \"status\": {\n        \"label\": \"Ativar ou Desativar o Suporte do Chrome AI no Page Assist\"\n    },\n    \"error\": {\n        \"browser_not_supported\": \"Esta versão do Chrome não é compatível com o modelo Gemini Nano. Atualize para a versão 127 ou posterior.\",\n        \"ai_not_supported\": \"A configuração chrome://flags/#prompt-api-for-gemini-nano não está habilitada. Por favor, ative-a.\",\n        \"ai_not_ready\": \"O Gemini Nano ainda não está pronto; verifique as configurações do Chrome.\",\n        \"internal_error\": \"Ocorreu um erro interno. Tente novamente mais tarde.\"\n    },\n    \"errorDescription\": \"Para usar o Chrome AI, você precisa da versão 138 ou posterior do Chrome. Siga estas etapas:\\n\\n1. Acesse `chrome://flags/#prompt-api-for-gemini-nano` e ative \\\"Prompt API for Gemini Nano\\\".\\n2. Reinicie o Chrome para aplicar a flag.\\n3. Retorne a esta página e clique em \\\"Baixar Modelo\\\" — isso fará o download de um modelo de 4GB pela primeira vez.\\n4. Após o download, o Gemini Nano pode ser ativado através do Page Assist.\",\n    \"downloadModel\": \"Baixar Modelo\",\n    \"modelDownloadWarning\": \"Isso fará o download de um modelo com um tamanho aproximado de 1,5 GB a 2,4 GB. Certifique-se de ter espaço suficiente em disco.\"\n}"
  },
  {
    "path": "src/assets/locale/pt-BR/common.json",
    "content": "{\n  \"pageAssist\": \"Page Assist\",\n  \"selectAModel\": \"Selecionar um Modelo\",\n  \"save\": \"Salvar\",\n  \"saved\": \"Salvo\",\n  \"cancel\": \"Cancelar\",\n  \"retry\": \"Tentar Novamente\",\n  \"share\": {\n    \"tooltip\": {\n      \"share\": \"Compartilhar\"\n    },\n    \"modal\": {\n      \"title\": \"Compartilhar Link para o Chat\"\n    },\n    \"form\": {\n      \"defaultValue\": {\n        \"name\": \"Anônimo\",\n        \"title\": \"Chat sem título\"\n      },\n      \"title\": {\n        \"label\": \"Título do Chat\",\n        \"placeholder\": \"Digite o Título do Chat\",\n        \"required\": \"O Título do Chat é obrigatório\"\n      },\n      \"name\": {\n        \"label\": \"Seu Nome\",\n        \"placeholder\": \"Digite seu Nome\",\n        \"required\": \"Seu Nome é obrigatório\"\n      },\n      \"btn\": {\n        \"save\": \"Gerar Link\",\n        \"saving\": \"Gerando Link...\"\n      }\n    },\n    \"notification\": {\n      \"successGenerate\": \"Link copiado para a área de transferência\",\n      \"failGenerate\": \"Falha ao gerar link\"\n    }\n  },\n  \"copyToClipboard\": \"Copiar para a área de transferência\",\n  \"webSearch\": \"Pesquisando na web\",\n  \"regenerate\": \"Gerar Novamente\",\n  \"continue\": \"Continuar Resposta\",\n  \"edit\": \"Editar\",\n  \"delete\": \"Excluir\",\n  \"saveAndSubmit\": \"Salvar & Enviar\",\n  \"editMessage\": {\n    \"placeholder\": \"Digite uma mensagem...\"\n  },\n  \"submit\": \"Enviar\",\n  \"noData\": \"Sem dados\",\n  \"noHistory\": \"Sem histórico de chat\",\n  \"chatWithCurrentPage\": \"Conversar com a página atual\",\n  \"beta\": \"Beta\",\n  \"tts\": \"Ler em voz alta\",\n  \"currentChatModelSettings\": \"Configurações Atuais do Modelo de Chat\",\n  \"modelSettings\": {\n    \"label\": \"Configurações do Modelo\",\n    \"description\": \"Defina as opções do modelo globalmente para todos os chats\",\n    \"form\": {\n      \"keepAlive\": {\n        \"label\": \"Manter Ativo\",\n        \"help\": \"controla por quanto tempo o modelo permanecerá carregado na memória após a solicitação (padrão: 5m)\",\n        \"placeholder\": \"Digite a duração do Manter Ativo (ex: 5m, 10m, 1h)\"\n      },\n      \"temperature\": {\n        \"label\": \"Temperatura\",\n        \"placeholder\": \"Digite o valor da Temperatura (ex: 0.7, 1.0)\"\n      },\n      \"numCtx\": {\n        \"label\": \"Tamanho da Janela de Contexto (num_ctx)\",\n        \"placeholder\": \"Digite o valor do Tamanho da Janela de Contexto (padrão: 2048)\"\n      },\n      \"numPredict\": {\n        \"label\": \"Máximo de Tokens (num_predict)\",\n        \"placeholder\": \"Digite o valor do Máximo de Tokens (ex: 2048, 4096)\"\n      },\n      \"seed\": {\n        \"label\": \"Semente\",\n        \"placeholder\": \"Digite o valor da Semente (ex: 1234)\",\n        \"help\": \"Reprodutibilidade da saída do modelo\"\n      },\n      \"topK\": {\n        \"label\": \"Top K\",\n        \"placeholder\": \"Digite o valor do Top K (ex: 40, 100)\"\n      },\n      \"topP\": {\n        \"label\": \"Top P\",\n        \"placeholder\": \"Digite o valor do Top P (ex: 0.9, 0.95)\"\n      },\n      \"numGpu\": {\n        \"label\": \"Num GPUs\",\n        \"placeholder\": \"Digite o número de camadas para enviar para a(s) GPU(s)\"\n      },\n      \"systemPrompt\": {\n        \"label\": \"Prompt do Sistema Temporário\",\n        \"placeholder\": \"Digite o Prompt do Sistema\",\n        \"help\": \"Esta é uma maneira rápida de definir o prompt do sistema no chat atual, que substituirá o prompt do sistema selecionado, se existir.\"\n      }\n    },\n    \"advanced\": \"Mais Configurações do Modelo\"\n  },\n  \"copilot\": {\n    \"summary\": \"Resumir\",\n    \"explain\": \"Explicar\",\n    \"rephrase\": \"Reformular\",\n    \"translate\": \"Traduzir\"\n  },\n  \"citations\": \"Citações\",\n  \"downloadCode\": \"Baixar Código\",\n  \"date\": {\n    \"pinned\": \"Fixado\",\n    \"today\": \"Hoje\",\n    \"yesterday\": \"Ontem\",\n    \"last7Days\": \"Últimos 7 Dias\",\n    \"older\": \"Mais Antigos\"\n  },\n  \"range\": {\n    \"deleteConfirm\": {\n      \"pinned\": \"Tem certeza que deseja excluir todas as mensagens fixadas?\",\n      \"today\": \"Tem certeza que deseja excluir todas as mensagens de hoje?\",\n      \"yesterday\": \"Tem certeza que deseja excluir todas as mensagens de ontem?\",\n      \"last7Days\": \"Tem certeza que deseja excluir todas as mensagens dos últimos 7 dias?\",\n      \"older\": \"Tem certeza que deseja excluir todas as mensagens mais antigas?\"\n    },\n    \"tooltip\": {\n      \"pinned\": \"Excluir todas as mensagens fixadas\",\n      \"today\": \"Excluir todas as mensagens de hoje\",\n      \"yesterday\": \"Excluir todas as mensagens de ontem\",\n      \"last7Days\": \"Excluir todas as mensagens dos últimos 7 dias\",\n      \"older\": \"Excluir todas as mensagens mais antigas\"\n    }\n  },\n  \"pin\": \"Fixar\",\n  \"unpin\": \"Desafixar\",\n  \"generationInfo\": \"Informações de Geração\",\n  \"sidebarChat\": \"Chat Lateral\",\n  \"reasoning\": {\n    \"thinking\": \"Pensando....\",\n    \"thought\": \"Pensou por {{time}}\"\n  },\n  \"embeddingGen\": \"Criando embeddings, isso pode levar algum tempo\",\n  \"semanticSearch\": \"Realizando busca semântica\",\n  \"downloading\": \"Baixando\",\n  \"cancelPullingModel\": {\n    \"confirm\": \"Tem certeza de que deseja cancelar o download? Isso interromperá o processo de download. De acordo com a documentação do Ollama, você pode reiniciar de onde parou.\"\n  }\n}\n"
  },
  {
    "path": "src/assets/locale/pt-BR/knowledge.json",
    "content": "{\n    \"addBtn\": \"Adicionar Novo Conhecimento\",\n    \"columns\": {\n        \"title\": \"Título\",\n        \"status\": \"Status\",\n        \"embeddings\": \"Modelo de Incorporação\",\n        \"createdAt\": \"Criado Em\",\n        \"action\": \"Ações\"\n    },\n    \"expandedColumns\": {\n        \"name\": \"Nome\"\n    },\n    \"confirm\": {\n        \"delete\": \"Tem certeza de que deseja excluir este conhecimento?\"\n    },\n    \"deleteSuccess\": \"Conhecimento excluído com sucesso\",\n    \"status\": {\n        \"pending\": \"Pendente\",\n        \"finished\": \"Concluído\",\n        \"processing\": \"Processando\",\n        \"failed\": \"Falhou\"\n    },\n    \"addKnowledge\": \"Adicionar Conhecimento\",\n    \"updateKnowledge\": \"Adicionar Fonte\",\n    \"form\": {\n        \"title\": {\n            \"label\": \"Título do Conhecimento\",\n            \"placeholder\": \"Digite o título do conhecimento\",\n            \"required\": \"O título do conhecimento é obrigatório\"\n        },\n        \"uploadFile\": {\n            \"label\": \"Carregar Arquivo\",\n            \"uploadText\": \"Arraste e solte um arquivo aqui ou clique para carregar\",\n            \"uploadHint\": \"Tipos de arquivo suportados: .pdf, .csv, .txt, .md, .docx\",\n            \"required\": \"O arquivo é obrigatório\"\n        },\n        \"submit\": \"Enviar\",\n        \"success\": \"Conhecimento adicionado com sucesso\"\n    },\n    \"noEmbeddingModel\": \"Por favor, adicione um modelo de incorporação na página de configurações RAG primeiro\"\n}"
  },
  {
    "path": "src/assets/locale/pt-BR/openai.json",
    "content": "{\n    \"settings\": \"API Compatível com OpenAI\",\n    \"heading\": \"API compatível com OpenAI\",\n    \"subheading\": \"Gerencie e configure seus provedores compatíveis com a API OpenAI aqui.\",\n    \"addBtn\": \"Adicionar Provedor\",\n    \"table\": {\n        \"name\": \"Nome do Provedor\",\n        \"baseUrl\": \"URL Base\",\n        \"actions\": \"Ação\"\n    },\n    \"modal\": {\n        \"titleAdd\": \"Adicionar Novo Provedor\",\n        \"name\": {\n            \"label\": \"Nome do Provedor\",\n            \"required\": \"O nome do provedor é obrigatório.\",\n            \"placeholder\": \"Digite o nome do provedor\"\n        },\n        \"baseUrl\": {\n            \"label\": \"URL Base\",\n            \"help\": \"A URL base do provedor da API OpenAI. ex. (http://localhost:1234/v1)\",\n            \"required\": \"A URL base é obrigatória.\",\n            \"placeholder\": \"Digite a URL base\"\n        },\n        \"apiKey\": {\n            \"label\": \"Chave da API\",\n            \"required\": \"A chave da API é obrigatória.\",\n            \"placeholder\": \"Digite a chave da API\"\n        },\n        \"submit\": \"Salvar\",\n        \"update\": \"Atualizar\",\n        \"deleteConfirm\": \"Tem certeza de que deseja excluir este provedor?\",\n        \"model\": {\n            \"title\": \"Lista de Modelos\",\n            \"subheading\": \"Por favor, selecione os modelos de chat que você deseja usar com este provedor.\",\n            \"success\": \"Novos modelos adicionados com sucesso.\"\n        },\n        \"tipLMStudio\": \"O Page Assist buscará automaticamente os modelos que você carregou no LM Studio. Você não precisa adicioná-los manualmente.\"\n    },\n    \"addSuccess\": \"Provedor adicionado com sucesso.\",\n    \"deleteSuccess\": \"Provedor excluído com sucesso.\",\n    \"updateSuccess\": \"Provedor atualizado com sucesso.\",\n    \"delete\": \"Excluir\",\n    \"edit\": \"Editar\",\n    \"newModel\": \"Adicionar Modelos ao Provedor\",\n    \"noNewModel\": \"Para o LMStudio, buscamos dinamicamente. Não é necessária adição manual.\",\n    \"searchModel\": \"Pesquisar Modelo\",\n    \"selectAll\": \"Selecionar Tudo\",\n    \"save\": \"Salvar\",\n    \"saving\": \"Salvando...\",\n    \"manageModels\": {\n        \"columns\": {\n            \"name\": \"Nome do Modelo\",\n            \"model_type\": \"Tipo de Modelo\",\n            \"model_id\": \"ID do Modelo\",\n            \"provider\": \"Nome do Provedor\",\n            \"actions\": \"Ação\",\n            \"nickname\": \"Apelido do Modelo\"\n        },\n        \"tooltip\": {\n            \"delete\": \"Excluir\"\n        },\n        \"confirm\": {\n            \"delete\": \"Tem certeza de que deseja excluir este modelo?\"\n        },\n        \"modal\": {\n            \"title\": \"Adicionar Modelo Personalizado\",\n            \"form\": {\n                \"name\": {\n                    \"label\": \"ID do Modelo\",\n                    \"placeholder\": \"llama3.2\",\n                    \"required\": \"O ID do modelo é obrigatório.\"\n                },\n                \"provider\": {\n                    \"label\": \"Provedor\",\n                    \"placeholder\": \"Selecione o provedor\",\n                    \"required\": \"O provedor é obrigatório.\"\n                },\n                \"type\": {\n                    \"label\": \"Tipo de Modelo\"\n                }\n            }\n        }\n    },\n    \"noModelFound\": \"Nenhum modelo encontrado. Certifique-se de ter adicionado o provedor correto com URL base e chave da API.\",\n    \"radio\": {\n        \"chat\": \"Modelo de Chat\",\n        \"embedding\": \"Modelo de Incorporação\",\n        \"chatInfo\": \"é usado para conclusão de chat e geração de conversas\",\n        \"embeddingInfo\": \"é usado para RAG e outras tarefas relacionadas à busca semântica.\"\n    },\n    \"nicknameModal\": {\n        \"title\": \"Adicionar / Editar Apelido do Modelo\",\n        \"form\": {\n            \"modelName\": {\n                \"label\": \"Nome do Modelo\",\n                \"placeholder\": \"Digite o nome do modelo\",\n                \"required\": \"O nome do modelo é obrigatório.\"\n            },\n            \"modelAvatar\": {\n                \"label\": \"Avatar do Modelo\",\n                \"placeholder\": \"Digite o avatar do modelo\",\n                \"help\": \"Por favor, insira a URL do avatar do modelo. Esta imagem será exibida na janela de chat.\"\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/assets/locale/pt-BR/option.json",
    "content": "{\n    \"newChat\": \"Novo Chat\",\n    \"selectAPrompt\": \"Selecionar um Prompt\",\n    \"githubRepository\": \"Repositório GitHub\",\n    \"settings\": \"Configurações\",\n    \"sidebarTitle\": \"Histórico de Chat\",\n    \"error\": \"Erro\",\n    \"somethingWentWrong\": \"Algo deu errado\",\n    \"validationSelectModel\": \"Por favor, selecione um modelo para continuar\",\n    \"deleteHistoryConfirmation\": \"Tem certeza de que deseja excluir este histórico?\",\n    \"editHistoryTitle\": \"Digite um novo título\",\n    \"temporaryChat\": \"Chat Temporário\",\n    \"more\": {\n        \"copy\": {\n            \"group\": \"Copiar\",\n            \"asText\": \"Copiar como Texto\",\n            \"asMarkdown\": \"Copiar como Markdown\",\n            \"success\": \"Copiado para área de transferência!\"\n        },\n        \"download\": {\n            \"group\": \"Baixar\",\n            \"text\": \"Arquivo de Texto (.txt)\",\n            \"markdown\": \"Markdown (.md)\",\n            \"json\": \"Arquivo JSON (.json)\"\n        },\n        \"share\": \"Compartilhar\"\n    }\n}"
  },
  {
    "path": "src/assets/locale/pt-BR/playground.json",
    "content": "{\n    \"ollamaState\": {\n        \"searching\": \"Procurando por seu Ollama 🦙\",\n        \"running\": \"Ollama está em execução 🦙\",\n        \"notRunning\": \"Não foi possível conectar ao Ollama 🦙\",\n        \"connectionError\": \"Parece que você está tendo um erro de conexão. Consulte esta <anchor>documentação</anchor> para solucionar o problema.\"\n    },\n    \"formError\": {\n        \"noModel\": \"Por favor, selecione um modelo\",\n        \"noEmbeddingModel\": \"Por favor, configure um modelo de incorporação na página Configurações > RAG\"\n    },\n    \"form\": {\n        \"textarea\": {\n            \"placeholder\": \"Digite uma mensagem...\"\n        },\n        \"webSearch\": {\n            \"on\": \"Ligado\",\n            \"off\": \"Desligado\"\n        }\n    },\n    \"tooltip\": {\n        \"searchInternet\": \"Pesquisar na Internet\",\n        \"speechToText\": \"Fala para Texto\",\n        \"uploadImage\": \"Carregar Imagem\",\n        \"stopStreaming\": \"Parar Streaming\",\n        \"knowledge\": \"Conhecimento\",\n        \"clearContext\": \"Limpar Contexto\"\n    },\n    \"sendWhenEnter\": \"Enviar ao pressionar Enter\",\n    \"welcome\": \"Olá! Como posso ajudar você hoje?\",\n    \"useOCR\": \"Extrair texto da imagem (OCR)\"\n}"
  },
  {
    "path": "src/assets/locale/pt-BR/settings.json",
    "content": "{\n  \"generalSettings\": {\n    \"title\": \"Configurações Gerais\",\n    \"settings\": {\n      \"heading\": \"Configurações da Interface Web\",\n      \"speechRecognitionLang\": {\n        \"label\": \"Idioma de Reconhecimento de Fala\",\n        \"placeholder\": \"Selecione um idioma\"\n      },\n      \"language\": {\n        \"label\": \"Idioma\",\n        \"placeholder\": \"Selecione um idioma\"\n      },\n      \"darkMode\": {\n        \"label\": \"Alterar Tema\",\n        \"options\": {\n          \"light\": \"Claro\",\n          \"dark\": \"Escuro\"\n        }\n      },\n      \"copilotResumeLastChat\": {\n        \"label\": \"Retomar o último chat ao abrir o Painel Lateral (Copilot)\"\n      },\n      \"turnOnChatWithWebsite\": {\n        \"label\": \"Ativar Chat com o Site por padrão (Copilot)\"\n      },\n      \"webUIResumeLastChat\": {\n        \"label\": \"Retomar o último chat ao abrir a Interface Web\"\n      },\n      \"hideCurrentChatModelSettings\": {\n        \"label\": \"Ocultar as Configurações Atuais do Modelo de Chat\"\n      },\n      \"restoreLastChatModel\": {\n        \"label\": \"Restaurar o último modelo usado para conversas anteriores\"\n      },\n      \"sendNotificationAfterIndexing\": {\n        \"label\": \"Enviar notificação após concluir o processamento da base de conhecimento\"\n      },\n      \"generateTitle\": {\n        \"label\": \"Gerar título usando IA\"\n      },\n      \"ollamaStatus\": {\n        \"label\": \"Ativar ou desativar verificação de status da conexão Ollama\"\n      },\n      \"wideMode\": {\n        \"label\": \"Ativar modo tela larga\"\n      },\n      \"openReasoning\": {\n        \"label\": \"Abrir Raciocínio Recolhido por padrão\"\n      },\n      \"userChatBubble\": {\n        \"label\": \"Usar Balão de Chat para Mensagens do Usuário\"\n      },\n      \"autoCopyResponseToClipboard\": {\n        \"label\": \"Copiar Resposta Automaticamente para a Área de Transferência\"\n      },\n      \"useMarkdownForUserMessage\": {\n        \"label\": \"Ativar formatação Markdown para mensagens do Usuário\"\n      },\n      \"copyAsFormattedText\": {\n        \"label\": \"Copiar como Texto Formatado\"\n      },\n      \"tabMentionsEnabled\": {\n        \"label\": \"Ativar Menções de Abas (@tab)\"\n      },\n      \"pasteLargeTextAsFile\": {\n        \"label\": \"Colar Texto Grande como Arquivo\"\n      },\n      \"sidepanelTemporaryChat\": {\n        \"label\": \"Ativar Chat Temporário no Painel Lateral por padrão\"\n      }\n    },\n    \"sidepanelRag\": {\n      \"heading\": \"Configurações de Recuperação\",\n      \"ragEnabled\": {\n        \"label\": \"Ativar Incorporação e Recuperação\"\n      },\n      \"maxWebsiteContext\": {\n        \"label\": \"Tamanho Máximo de Conteúdo para Modo de Contexto Completo\",\n        \"placeholder\": \"Tamanho do conteúdo (padrão 4028)\"\n      }\n    },\n    \"webSearch\": {\n      \"heading\": \"Gerenciar Pesquisa na Web\",\n      \"searchMode\": {\n        \"label\": \"Realizar Pesquisa Simples na Internet\"\n      },\n      \"provider\": {\n        \"label\": \"Mecanismo de Pesquisa\",\n        \"placeholder\": \"Selecione um mecanismo de pesquisa\"\n      },\n      \"totalSearchResults\": {\n        \"label\": \"Total de Resultados da Pesquisa\",\n        \"placeholder\": \"Digite o Total de Resultados da Pesquisa\"\n      },\n      \"visitSpecificWebsite\": {\n        \"label\": \"Visitar o site mencionado na mensagem\"\n      },\n      \"searxng\": {\n        \"url\": {\n          \"label\": \"URL do SearXNG\"\n        }\n      },\n      \"braveApi\": {\n        \"label\": \"Chave da API do Brave\",\n        \"placeholder\": \"Digite sua chave da API do Brave\"\n      },\n      \"tavilyApi\": {\n        \"label\": \"Chave da API do Tavily\",\n        \"placeholder\": \"Digite sua chave da API do Tavily\"\n      },\n      \"searchOnByDefault\": {\n        \"label\": \"Pesquisa na Internet ativada por padrão\"\n      }\n    },\n    \"system\": {\n      \"heading\": \"Configurações do Sistema\",\n      \"storageSyncEnabled\": {\n        \"label\": \"Ativar sincronização de armazenamento do navegador (sincronizar configurações entre dispositivos)\"\n      },\n      \"deleteChatHistory\": {\n        \"label\": \"Reiniciar Sistema\",\n        \"button\": \"Reiniciar Tudo\",\n        \"confirm\": \"Tem certeza que deseja realizar um reinício do sistema? Isso irá apagar todos os dados e não poderá ser desfeito.\"\n      },\n      \"export\": {\n        \"label\": \"Exportar todos os dados (histórico de chat, base de conhecimento, prompts e configurações)\",\n        \"button\": \"Exportar dados\",\n        \"success\": \"Exportação bem-sucedida\"\n      },\n      \"import\": {\n        \"label\": \"Importar todos os dados (histórico de chat, base de conhecimento, prompts e configurações)\",\n        \"button\": \"Importar dados\",\n        \"success\": \"Importação bem-sucedida\",\n        \"error\": \"Erro na importação\"\n      }\n    },\n    \"tts\": {\n      \"heading\": \"Configurações de Texto para Fala\",\n      \"ttsEnabled\": {\n        \"label\": \"Ativar Texto para Fala\"\n      },\n      \"ttsAutoPlay\": {\n        \"label\": \"Reproduzir resposta de voz automaticamente após conclusão\"\n      },\n      \"ttsProvider\": {\n        \"label\": \"Provedor de Texto para Fala\",\n        \"placeholder\": \"Selecione um provedor\"\n      },\n      \"ttsVoice\": {\n        \"label\": \"Voz de Texto para Fala\",\n        \"placeholder\": \"Selecione uma voz\"\n      },\n      \"ssmlEnabled\": {\n        \"label\": \"Ativar SSML (Linguagem de Marcação de Síntese de Fala)\"\n      },\n      \"removeReasoningTagTTS\": {\n        \"label\": \"Remover Tag de Raciocínio do TTS\"\n      }\n    },\n    \"stt\": {\n      \"heading\": \"Configurações de Fala para Texto\",\n      \"autoStopTimeout\": {\n        \"label\": \"Tempo Limite de Parada Automática (ms)\",\n        \"placeholder\": \"Digite o tempo limite de parada automática em milissegundos\"\n      },\n      \"autoSubmitVoiceMessage\": {\n        \"label\": \"Envio Automático de Mensagem de Voz\"\n      }\n    }\n  },\n  \"manageModels\": {\n    \"title\": \"Gerenciar Modelos\",\n    \"addBtn\": \"Adicionar Novo Modelo\",\n    \"columns\": {\n      \"name\": \"Nome\",\n      \"digest\": \"Resumo\",\n      \"modifiedAt\": \"Modificado em\",\n      \"size\": \"Tamanho\",\n      \"actions\": \"Ações\"\n    },\n    \"expandedColumns\": {\n      \"parentModel\": \"Modelo Pai\",\n      \"format\": \"Formato\",\n      \"family\": \"Família\",\n      \"parameterSize\": \"Tamanho do Parâmetro\",\n      \"quantizationLevel\": \"Nível de Quantização\"\n    },\n    \"tooltip\": {\n      \"delete\": \"Excluir Modelo\",\n      \"repull\": \"Baixar Novamente o Modelo\"\n    },\n    \"confirm\": {\n      \"delete\": \"Tem certeza de que deseja excluir este modelo?\",\n      \"repull\": \"Tem certeza de que deseja baixar este modelo novamente?\"\n    },\n    \"modal\": {\n      \"title\": \"Adicionar Novo Modelo\",\n      \"placeholder\": \"Digite o Nome do Modelo\",\n      \"pull\": \"Baixar Modelo\"\n    },\n    \"notification\": {\n      \"pullModel\": \"Baixando Modelo\",\n      \"pullModelDescription\": \"Baixando o modelo {{modelName}}. Para mais detalhes, verifique o ícone da extensão.\",\n      \"success\": \"Sucesso\",\n      \"error\": \"Erro\",\n      \"successDescription\": \"Modelo baixado com sucesso\",\n      \"successDeleteDescription\": \"Modelo excluído com sucesso\",\n      \"someError\": \"Algo deu errado. Por favor, tente novamente mais tarde\"\n    }\n  },\n  \"managePrompts\": {\n    \"title\": \"Gerenciar Prompts\",\n    \"addBtn\": \"Adicionar Novo Prompt\",\n    \"option1\": \"Normal\",\n    \"option2\": \"RAG\",\n    \"questionPrompt\": \"Prompt de Pergunta\",\n    \"columns\": {\n      \"title\": \"Título\",\n      \"prompt\": \"Prompt\",\n      \"type\": \"Tipo de Prompt\",\n      \"actions\": \"Ações\"\n    },\n    \"systemPrompt\": \"Prompt do Sistema\",\n    \"quickPrompt\": \"Prompt Rápido\",\n    \"tooltip\": {\n      \"delete\": \"Excluir Prompt\",\n      \"edit\": \"Editar Prompt\"\n    },\n    \"confirm\": {\n      \"delete\": \"Tem certeza que deseja excluir este prompt? Esta ação não pode ser desfeita.\"\n    },\n    \"modal\": {\n      \"addTitle\": \"Adicionar Novo Prompt\",\n      \"editTitle\": \"Editar Prompt\"\n    },\n    \"segmented\": {\n      \"custom\": \"Prompts personalizados\",\n      \"copilot\": \"Prompts do Copilot\"\n    },\n    \"form\": {\n      \"title\": {\n        \"label\": \"Título\",\n        \"placeholder\": \"Meu Prompt Incrível\",\n        \"required\": \"Por favor, insira um título\"\n      },\n      \"prompt\": {\n        \"label\": \"Prompt\",\n        \"placeholder\": \"Digite o Prompt\",\n        \"required\": \"Por favor, insira um prompt\",\n        \"help\": \"Você pode usar {key} como variável em seu prompt.\",\n        \"missingTextPlaceholder\": \"A variável {text} está faltando no prompt. Por favor, adicione-a.\"\n      },\n      \"isSystem\": {\n        \"label\": \"É um Prompt do Sistema\"\n      },\n      \"btnSave\": {\n        \"saving\": \"Adicionando Prompt...\",\n        \"save\": \"Adicionar Prompt\"\n      },\n      \"btnEdit\": {\n        \"saving\": \"Atualizando Prompt...\",\n        \"save\": \"Atualizar Prompt\"\n      }\n    },\n    \"notification\": {\n      \"addSuccess\": \"Prompt Adicionado\",\n      \"addSuccessDesc\": \"O prompt foi adicionado com sucesso\",\n      \"error\": \"Erro\",\n      \"someError\": \"Algo deu errado. Por favor, tente novamente mais tarde\",\n      \"updatedSuccess\": \"Prompt Atualizado\",\n      \"updatedSuccessDesc\": \"O prompt foi atualizado com sucesso\",\n      \"deletedSuccess\": \"Prompt Excluído\",\n      \"deletedSuccessDesc\": \"O prompt foi excluído com sucesso\"\n    }\n  },\n  \"manageShare\": {\n    \"title\": \"Gerenciar Compartilhamento\",\n    \"heading\": \"Configurar URL de Compartilhamento de Página\",\n    \"form\": {\n      \"url\": {\n        \"label\": \"URL de Compartilhamento de Página\",\n        \"placeholder\": \"Digite a URL de Compartilhamento de Página\",\n        \"required\": \"Por favor, insira sua URL de Compartilhamento de Página!\",\n        \"help\": \"Por motivos de privacidade, você pode hospedar por conta própria o compartilhamento de página e fornecer a URL aqui. <anchor>Saiba Mais</anchor>.\"\n      }\n    },\n    \"webshare\": {\n      \"heading\": \"Compartilhamento Web\",\n      \"columns\": {\n        \"title\": \"Título\",\n        \"url\": \"URL\",\n        \"actions\": \"Ações\"\n      },\n      \"tooltip\": {\n        \"delete\": \"Excluir Compartilhamento\"\n      },\n      \"confirm\": {\n        \"delete\": \"Tem certeza de que deseja excluir este compartilhamento? Esta ação não pode ser desfeita.\"\n      },\n      \"label\": \"Gerenciar Compartilhamento de Página\",\n      \"description\": \"Ativar ou desativar o recurso de compartilhamento de página\"\n    },\n    \"notification\": {\n      \"pageShareSuccess\": \"URL de Compartilhamento de Página atualizada com sucesso\",\n      \"someError\": \"Algo deu errado. Por favor, tente novamente mais tarde\",\n      \"webShareDeleteSuccess\": \"Compartilhamento Web excluído com sucesso\"\n    }\n  },\n  \"ollamaSettings\": {\n    \"title\": \"Configurações do Ollama\",\n    \"heading\": \"Configurar Ollama\",\n    \"settings\": {\n      \"ollamaUrl\": {\n        \"label\": \"URL do Ollama\",\n        \"placeholder\": \"Digite a URL do Ollama\"\n      },\n      \"globalEnable\": {\n        \"label\": \"Ativar ou Desativar Integração com Ollama Globalmente\",\n        \"warning\": \"Ao desativar a integração com Ollama globalmente, o Page Assist não buscará modelos do Ollama. Você ainda pode adicionar uma instância do Ollama pela seção de <anchor>API compatível com OpenAI</anchor> que funcionará normalmente.\"\n      },\n      \"advanced\": {\n        \"label\": \"Configuração Avançada da URL do Ollama\",\n        \"urlRewriteEnabled\": {\n          \"label\": \"Ativar ou Desativar URL de Origem Personalizada\"\n        },\n        \"rewriteUrl\": {\n          \"label\": \"URL de Origem Personalizada\",\n          \"placeholder\": \"Digite a URL de Origem Personalizada\"\n        },\n        \"autoCORSFix\": {\n          \"label\": \"Ativar ou Desativar Correção Automática de CORS do Ollama\"\n        },\n        \"headers\": {\n          \"label\": \"Cabeçalhos Personalizados\",\n          \"add\": \"Adicionar Cabeçalho\",\n          \"key\": {\n            \"label\": \"Chave do Cabeçalho\",\n            \"placeholder\": \"Autorização\"\n          },\n          \"value\": {\n            \"label\": \"Valor do Cabeçalho\",\n            \"placeholder\": \"Token Bearer\"\n          }\n        },\n        \"help\": \"Se você tiver problemas de conexão com o Ollama no Page Assist, você pode configurar uma URL de origem personalizada. Para saber mais sobre a configuração, <anchor>clique aqui</anchor>.\"\n      }\n    }\n  },\n  \"manageSearch\": {\n    \"title\": \"Gerenciar Pesquisa na Web\",\n    \"heading\": \"Configurar Pesquisa na Web\"\n  },\n  \"about\": {\n    \"title\": \"Sobre\",\n    \"heading\": \"Sobre\",\n    \"chromeVersion\": \"Versão do Page Assist\",\n    \"ollamaVersion\": \"Versão do Ollama\",\n    \"support\": \"Você pode apoiar o projeto Page Assist doando ou patrocinando através das seguintes plataformas:\",\n    \"koFi\": \"Apoiar no Ko-fi\",\n    \"githubSponsor\": \"Patrocinar no GitHub\",\n    \"githubRepo\": \"Repositório GitHub\"\n  },\n  \"manageKnowledge\": {\n    \"title\": \"Gerenciar Conhecimento\",\n    \"heading\": \"Configurar Base de Conhecimento\"\n  },\n  \"rag\": {\n    \"title\": \"Configurações Pipeline\",\n    \"ragSettings\": {\n      \"label\": \"Configurações RAG\",\n      \"model\": {\n        \"label\": \"Modelo de Incorporação\",\n        \"required\": \"Por favor, selecione um modelo\",\n        \"help\": \"Altamente recomendado o uso de modelos de incorporação como `nomic-embed-text`.\",\n        \"placeholder\": \"Selecione um modelo\"\n      },\n      \"chunkSize\": {\n        \"label\": \"Tamanho do Pedaço\",\n        \"placeholder\": \"Digite o Tamanho do Pedaço\",\n        \"required\": \"Por favor, insira um tamanho de pedaço\"\n      },\n      \"chunkOverlap\": {\n        \"label\": \"Sobreposição do Pedaço\",\n        \"placeholder\": \"Digite a Sobreposição do Pedaço\",\n        \"required\": \"Por favor, insira uma sobreposição de pedaço\"\n      },\n      \"totalFilePerKB\": {\n        \"label\": \"Limite Padrão de Upload de Arquivos da Base de Conhecimento\",\n        \"placeholder\": \"Digite o limite padrão de upload de arquivos (ex: 10)\",\n        \"required\": \"Por favor, insira o limite padrão de upload de arquivos\"\n      },\n      \"noOfRetrievedDocs\": {\n        \"label\": \"Número de Documentos Recuperados\",\n        \"placeholder\": \"Digite o Número de Documentos Recuperados\",\n        \"required\": \"Por favor, insira o número de documentos recuperados\"\n      },\n      \"splittingSeparator\": {\n        \"label\": \"Separador\",\n        \"placeholder\": \"Digite o Separador (ex: \\\\n\\\\n)\",\n        \"required\": \"Por favor, insira um separador\"\n      },\n      \"splittingStrategy\": {\n        \"label\": \"Divisor de Texto\"\n      }\n    },\n    \"prompt\": {\n      \"label\": \"Configurar Prompt RAG\",\n      \"option1\": \"Normal\",\n      \"option2\": \"Web\",\n      \"alert\": \"A configuração do prompt do sistema aqui está obsoleta. Por favor, use a seção Gerenciar Prompts para adicionar ou editar prompts. Esta seção será removida em uma versão futura\",\n      \"systemPrompt\": \"Prompt do Sistema\",\n      \"systemPromptPlaceholder\": \"Digite o Prompt do Sistema\",\n      \"webSearchPrompt\": \"Prompt de Pesquisa na Web\",\n      \"webSearchPromptHelp\": \"Não remova `{search_results}` do prompt.\",\n      \"webSearchPromptError\": \"Por favor, insira um prompt de pesquisa na web\",\n      \"webSearchPromptPlaceholder\": \"Digite o Prompt de Pesquisa na Web\",\n      \"webSearchFollowUpPrompt\": \"Prompt de Acompanhamento da Pesquisa na Web\",\n      \"webSearchFollowUpPromptHelp\": \"Não remova `{chat_history}` e `{question}` do prompt.\",\n      \"webSearchFollowUpPromptError\": \"Por favor, insira seu Prompt de Acompanhamento da Pesquisa na Web!\",\n      \"webSearchFollowUpPromptPlaceholder\": \"Seu Prompt de Acompanhamento da Pesquisa na Web\"\n    }\n  },\n  \"chromeAiSettings\": {\n    \"title\": \"Configurações do Chrome AI\"\n  }\n}\n"
  },
  {
    "path": "src/assets/locale/pt-BR/sidepanel.json",
    "content": "{\n    \"tooltip\": {\n        \"embed\": \"A incorporação da página pode levar alguns minutos. Por favor, aguarde...\",\n        \"clear\": \"Apagar histórico de chat\",\n        \"history\": \"Histórico de chat\",\n        \"openwebui\": \"Abrir WebUI\"\n    }\n}"
  },
  {
    "path": "src/assets/locale/ru/chrome.json",
    "content": "{\n    \"heading\": \"Настройка Chrome AI\",\n    \"status\": {\n      \"label\": \"Включить или отключить поддержку Chrome AI в помощнике страницы\"\n    },\n    \"error\": {\n      \"browser_not_supported\": \"Эта версия Chrome не поддерживается моделью Gemini Nano. Пожалуйста, обновите до версии 127 или выше\",\n      \"ai_not_supported\": \"Настройка chrome://flags/#prompt-api-for-gemini-nano не включена. Пожалуйста, включите её.\",\n      \"ai_not_ready\": \"Gemini Nano ещё не готов; вам нужно перепроверить настройки Chrome.\",\n      \"internal_error\": \"Произошла внутренняя ошибка. Пожалуйста, повторите попытку позже.\"\n    },\n    \"errorDescription\": \"Чтобы использовать Chrome AI, вам нужна версия Chrome 138 или выше. Выполните следующие шаги:\\n\\n1. Перейдите на `chrome://flags/#prompt-api-for-gemini-nano` и включите \\\"Prompt API for Gemini Nano\\\".\\n2. Перезапустите Chrome, чтобы применить флаг.\\n3. Вернитесь на эту страницу и нажмите \\\"Скачать модель\\\" — это загрузит модель размером 4 ГБ в первый раз.\\n4. После загрузки Gemini Nano можно будет включить через Page Assist.\",\n    \"downloadModel\": \"Скачать модель\",\n    \"modelDownloadWarning\": \"Это загрузит модель размером от 1.5 ГБ до 2.4 ГБ. Убедитесь, что у вас достаточно места на диске.\"\n}"
  },
  {
    "path": "src/assets/locale/ru/common.json",
    "content": "{\n    \"pageAssist\": \"Помощник страницы\",\n    \"selectAModel\": \"Выберите модель\",\n    \"save\": \"Сохранить\",\n    \"saved\": \"Сохранено\",\n    \"cancel\": \"Отмена\",\n    \"retry\": \"Повторить\",\n    \"share\": {\n        \"tooltip\": {\n            \"share\": \"Поделиться\"\n        },\n        \"modal\": {\n            \"title\": \"Поделиться ссылкой на чат\"\n        },\n        \"form\": {\n            \"defaultValue\": {\n                \"name\": \"Аноним\",\n                \"title\": \"Безымянный чат\"\n            },\n            \"title\": {\n                \"label\": \"Название чата\",\n                \"placeholder\": \"Введите название чата\",\n                \"required\": \"Название чата обязательно\"\n            },\n            \"name\": {\n                \"label\": \"Ваше имя\",\n                \"placeholder\": \"Введите ваше имя\",\n                \"required\": \"Ваше имя обязательно\"\n            },\n            \"btn\": {\n                \"save\": \"Создать ссылку\",\n                \"saving\": \"Создание ссылки...\"\n            }\n        },\n        \"notification\": {\n            \"successGenerate\": \"Ссылка скопирована в буфер обмена\",\n            \"failGenerate\": \"Не удалось создать ссылку\"\n        }\n    },\n    \"copyToClipboard\": \"Копировать в буфер обмена\",\n    \"webSearch\": \"Поиск в интернете\",\n    \"regenerate\": \"Пересоздать\",\n    \"continue\": \"Продолжить ответ\",\n    \"edit\": \"Редактировать\",\n    \"delete\": \"Удалить\",\n    \"saveAndSubmit\": \"Сохранить и отправить\",\n    \"editMessage\": {\n        \"placeholder\": \"Введите сообщение...\"\n    },\n    \"submit\": \"Отправить\",\n    \"noData\": \"Нет данных\",\n    \"noHistory\": \"Нет истории чата\",\n    \"chatWithCurrentPage\": \"Чат с текущей страницей\",\n    \"beta\": \"Бета\",\n    \"tts\": \"Прочитать вслух\",\n    \"currentChatModelSettings\": \"Текущие настройки модели чата\",\n    \"modelSettings\": {\n        \"label\": \"Настройки модели\",\n        \"description\": \"Устанавливайте параметры модели глобально для всех чатов\",\n        \"form\": {\n            \"keepAlive\": {\n                \"label\": \"Время жизни\",\n                \"help\": \"Контролирует, как долго модель будет оставаться в памяти после запроса (по умолчанию: 5 минут)\",\n                \"placeholder\": \"Введите продолжительность времени жизни (например, 5м, 10м, 1ч)\"\n            },\n            \"temperature\": {\n                \"label\": \"Температура\",\n                \"placeholder\": \"Введите значение температуры (например, 0.7, 1.0)\"\n            },\n            \"numCtx\": {\n                \"label\": \"Размер окна контекста (num_ctx)\",\n                \"placeholder\": \"Введите значение размера окна контекста (по умолчанию: 2048)\"\n            },\n            \"numPredict\": {\n                \"label\": \"Максимальное количество токенов (num_predict)\",\n                \"placeholder\": \"Введите значение максимального количества токенов (например, 2048, 4096)\"\n            },\n            \"seed\": {\n                \"label\": \"Сид\",\n                \"placeholder\": \"Введите значение сида (например, 1234)\",\n                \"help\": \"Воспроизводимость вывода модели\"\n            },\n            \"topK\": {\n                \"label\": \"Top K\",\n                \"placeholder\": \"Введите значение Top K (например, 40, 100)\"\n            },\n            \"topP\": {\n                \"label\": \"Top P\",\n                \"placeholder\": \"Введите значение Top P (например, 0.9, 0.95)\"\n            },\n            \"numGpu\": {\n                \"label\": \"Num GPU\",\n                \"placeholder\": \"Введите количество слоев для отправки на GPU\"\n            },\n            \"systemPrompt\": {\n                \"label\": \"Временный системный запрос\",\n                \"placeholder\": \"Введите системный запрос\",\n                \"help\": \"Это быстрый способ установить системный запрос в текущем чате, который переопределит выбранный системный запрос, если он существует.\"\n            }\n        },\n        \"advanced\": \"Больше настроек модели\"\n    },\n    \"copilot\": {\n        \"summary\": \"Обобщить\",\n        \"explain\": \"Объяснить\",\n        \"rephrase\": \"Перефразировать\",\n        \"translate\": \"Перевести\"\n    },\n    \"citations\": \"Цитаты\",\n    \"downloadCode\": \"Скачать код\",\n    \"date\": {\n        \"pinned\": \"Закреплено\",\n        \"today\": \"Сегодня\",\n        \"yesterday\": \"Вчера\",\n        \"last7Days\": \"Последние 7 дней\",\n        \"older\": \"Ранее\"\n    },\n    \"range\": {\n        \"deleteConfirm\": {\n            \"pinned\": \"Вы уверены, что хотите удалить все закрепленные сообщения?\",\n            \"today\": \"Вы уверены, что хотите удалить все сообщения за сегодня?\",\n            \"yesterday\": \"Вы уверены, что хотите удалить все сообщения за вчера?\",\n            \"last7Days\": \"Вы уверены, что хотите удалить все сообщения за последние 7 дней?\",\n            \"older\": \"Вы уверены, что хотите удалить все старые сообщения?\"\n        },\n        \"tooltip\": {\n            \"pinned\": \"Удалить все закрепленные сообщения\",\n            \"today\": \"Удалить все сообщения за сегодня\",\n            \"yesterday\": \"Удалить все сообщения за вчера\",\n            \"last7Days\": \"Удалить все сообщения за последние 7 дней\",\n            \"older\": \"Удалить все старые сообщения\"\n        }\n    },\n    \"pin\": \"Закрепить\",\n    \"unpin\": \"Открепить\",\n    \"generationInfo\": \"Информация о генерации\",\n    \"sidebarChat\": \"Боковой чат\",\n    \"reasoning\": {\n        \"thinking\": \"Размышляю...\",\n        \"thought\": \"Размышлял {{time}}\"\n    },\n    \"embeddingGen\": \"Создание вложений, это может занять некоторое время\",\n    \"semanticSearch\": \"Выполнение семантического поиска\",\n    \"downloading\": \"Загрузка\",\n    \"cancelPullingModel\": {\n        \"confirm\": \"Вы уверены, что хотите отменить загрузку? Это остановит процесс загрузки. Согласно документации Ollama, вы можете возобновить загрузку с того места, на котором остановились.\"\n    }\n}"
  },
  {
    "path": "src/assets/locale/ru/knowledge.json",
    "content": "{\n    \"addBtn\": \"Добавить новое знание\",\n    \"columns\": {\n        \"title\": \"Название\",\n        \"status\": \"Статус\",\n        \"embeddings\": \"Модель вложения\",\n        \"createdAt\": \"Создано\",\n        \"action\": \"Действия\"\n    },\n    \"expandedColumns\": {\n        \"name\": \"Имя\"\n    },\n    \"confirm\": {\n        \"delete\": \"Вы уверены, что хотите удалить это знание?\"\n    },\n    \"deleteSuccess\": \"Знание успешно удалено\",\n    \"status\": {\n        \"pending\": \"Ожидание\",\n        \"finished\": \"Завершено\",\n        \"processing\": \"Обработка\",\n        \"failed\": \"Не удалось\"\n    },\n    \"addKnowledge\": \"Добавить знание\",\n    \"updateKnowledge\": \"Добавить источник\",\n    \"form\": {\n        \"title\": {\n            \"label\": \"Название знания\",\n            \"placeholder\": \"Введите название знания\",\n            \"required\": \"Название знания обязательно\"\n        },\n        \"uploadFile\": {\n            \"label\": \"Загрузить файл\",\n            \"uploadText\": \"Перетащите файл сюда или нажмите, чтобы загрузить\",\n            \"uploadHint\": \"Поддерживаемые типы файлов: .pdf, .csv, .txt, .md,.docx\",\n            \"required\": \"Файл обязателен\"\n        },\n        \"submit\": \"Отправить\",\n        \"success\": \"Знание успешно добавлено\"\n    },\n    \"noEmbeddingModel\": \"Пожалуйста, сначала добавьте модель вложения на странице настроек RAG\"\n}\n"
  },
  {
    "path": "src/assets/locale/ru/openai.json",
    "content": "{\n    \"settings\": \"API, совместимый с OpenAI\",\n    \"heading\": \"API, совместимый с OpenAI\",\n    \"subheading\": \"Управляйте и настраивайте ваши провайдеры, совместимые с API OpenAI, здесь.\",\n    \"addBtn\": \"Добавить провайдера\",\n    \"table\": {\n        \"name\": \"Имя провайдера\",\n        \"baseUrl\": \"Базовый URL\",\n        \"actions\": \"Действие\"\n    },\n    \"modal\": {\n        \"titleAdd\": \"Добавить нового провайдера\",\n        \"name\": {\n            \"label\": \"Имя провайдера\",\n            \"required\": \"Имя провайдера обязательно.\",\n            \"placeholder\": \"Введите имя провайдера\"\n        },\n        \"baseUrl\": {\n            \"label\": \"Базовый URL\",\n            \"help\": \"Базовый URL провайдера API OpenAI. например (http://localhost:1234/v1)\",\n            \"required\": \"Базовый URL обязателен.\",\n            \"placeholder\": \"Введите базовый URL\"\n        },\n        \"apiKey\": {\n            \"label\": \"Ключ API\",\n            \"required\": \"Ключ API обязателен.\",\n            \"placeholder\": \"Введите ключ API\"\n        },\n        \"submit\": \"Сохранить\",\n        \"update\": \"Обновить\",\n        \"deleteConfirm\": \"Вы уверены, что хотите удалить этого провайдера?\",\n        \"model\": {\n            \"title\": \"Список моделей\",\n            \"subheading\": \"Пожалуйста, выберите модели чата, которые вы хотите использовать с этим провайдером.\",\n            \"success\": \"Новые модели успешно добавлены.\"\n        },\n        \"tipLMStudio\": \"Page Assist автоматически загрузит модели, которые вы загрузили в LM Studio. Вам не нужно добавлять их вручную.\"\n    },\n    \"addSuccess\": \"Провайдер успешно добавлен.\",\n    \"deleteSuccess\": \"Провайдер успешно удален.\",\n    \"updateSuccess\": \"Провайдер успешно обновлен.\",\n    \"delete\": \"Удалить\",\n    \"edit\": \"Редактировать\",\n    \"newModel\": \"Добавить модели к провайдеру\",\n    \"noNewModel\": \"Для LMStudio, Ollama, Llamafile, мы загружаем динамически. Ручное добавление не требуется.\",\n    \"searchModel\": \"Поиск модели\",\n    \"selectAll\": \"Выбрать все\",\n    \"save\": \"Сохранить\",\n    \"saving\": \"Сохранение...\",\n    \"manageModels\": {\n        \"columns\": {\n            \"name\": \"Название модели\",\n            \"model_type\": \"Тип модели\",\n            \"model_id\": \"ID модели\",\n            \"provider\": \"Имя провайдера\",\n            \"actions\": \"Действие\",\n            \"nickname\": \"Псевдоним модели\"\n        },\n        \"tooltip\": {\n            \"delete\": \"Удалить\"\n        },\n        \"confirm\": {\n            \"delete\": \"Вы уверены, что хотите удалить эту модель?\"\n        },\n        \"modal\": {\n            \"title\": \"Добавить пользовательскую модель\",\n            \"form\": {\n                \"name\": {\n                    \"label\": \"ID модели\",\n                    \"placeholder\": \"llama3.2\",\n                    \"required\": \"ID модели обязателен.\"\n                },\n                \"provider\": {\n                    \"label\": \"Провайдер\",\n                    \"placeholder\": \"Выберите провайдера\",\n                    \"required\": \"Провайдер обязателен.\"\n                },\n                \"type\": {\n                    \"label\": \"Тип модели\"\n                }\n            }\n        }\n    },\n    \"noModelFound\": \"Модели не найдены. Убедитесь, что вы добавили правильного провайдера с базовым URL и ключом API.\",\n    \"radio\": {\n        \"chat\": \"Модель чата\",\n        \"embedding\": \"Модель встраивания\",\n        \"chatInfo\": \"используется для завершения чата и генерации разговоров\",\n        \"embeddingInfo\": \"используется для RAG и других задач, связанных с семантическим поиском.\"\n    },\n    \"nicknameModal\": {\n        \"title\": \"Добавить / Редактировать псевдоним модели\",\n        \"form\": {\n            \"modelName\": {\n                \"label\": \"Название модели\",\n                \"placeholder\": \"Введите название модели\",\n                \"required\": \"Название модели обязательно.\"\n            },\n            \"modelAvatar\": {\n                \"label\": \"Аватар модели\",\n                \"placeholder\": \"Введите аватар модели\",\n                \"help\": \"Пожалуйста, введите URL аватара модели. Это изображение будет отображаться в окне чата.\"\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/assets/locale/ru/option.json",
    "content": "{\n    \"newChat\": \"Новый чат\",\n    \"selectAPrompt\": \"Выберите подсказку\",\n    \"githubRepository\": \"Репозиторий GitHub\",\n    \"settings\": \"Настройки\",\n    \"sidebarTitle\": \"История чата\",\n    \"error\": \"Ошибка\",\n    \"somethingWentWrong\": \"Что-то пошло не так\",\n    \"validationSelectModel\": \"Пожалуйста, выберите модель, чтобы продолжить\",\n    \"deleteHistoryConfirmation\": \"Вы уверены, что хотите удалить эту историю?\",\n    \"editHistoryTitle\": \"Введите новое название\",\n    \"temporaryChat\": \"Временный чат\", \n    \"more\": {\n        \"copy\": {\n            \"group\": \"Копировать\",\n            \"asText\": \"Копировать как текст\",\n            \"asMarkdown\": \"Копировать как Markdown\",\n            \"success\": \"Скопировано в буфер обмена!\"\n        },\n        \"download\": {\n            \"group\": \"Скачать\",\n            \"text\": \"Текстовый файл (.txt)\",\n            \"markdown\": \"Markdown (.md)\",\n            \"json\": \"JSON файл (.json)\"\n        },\n        \"share\": \"Поделиться\"\n    }}\n"
  },
  {
    "path": "src/assets/locale/ru/playground.json",
    "content": "{\n    \"ollamaState\": {\n        \"searching\": \"Поиск вашего Ollama 🦙\",\n        \"running\": \"Ollama работает 🦙\",\n        \"notRunning\": \"Не удалось подключиться к Ollama 🦙\",\n        \"connectionError\": \"Похоже, у вас возникла ошибка соединения. Пожалуйста, обратитесь к этой <anchor>документации</anchor> для устранения неисправностей.\"\n    },\n    \"formError\": {\n        \"noModel\": \"Пожалуйста, выберите модель\",\n        \"noEmbeddingModel\": \"Пожалуйста, установите модель вложения на странице Настройки > RAG\"\n    },\n    \"form\": {\n        \"textarea\": {\n            \"placeholder\": \"Введите сообщение...\"\n        },\n        \"webSearch\": {\n            \"on\": \"Вкл\",\n            \"off\": \"Выкл\"\n        }\n    },\n    \"tooltip\": {\n        \"searchInternet\": \"Поиск в Интернете\",\n        \"speechToText\": \"Речь в текст\",\n        \"uploadImage\": \"Загрузить изображение\",\n        \"stopStreaming\": \"Остановить поток\",\n        \"knowledge\": \"Знание\",\n        \"clearContext\": \"Очистить контекст\"\n    },\n    \"sendWhenEnter\": \"Отправить при нажатии клавиши Enter\",\n    \"welcome\": \"Здравствуйте! Как я могу помочь вам сегодня?\",\n    \"useOCR\": \"Извлечь текст из изображения (OCR)\"\n}"
  },
  {
    "path": "src/assets/locale/ru/settings.json",
    "content": "{\n  \"generalSettings\": {\n    \"title\": \"Общие настройки\",\n    \"settings\": {\n      \"heading\": \"Настройки веб-интерфейса\",\n      \"speechRecognitionLang\": {\n        \"label\": \"Язык распознавания речи\",\n        \"placeholder\": \"Выберите язык\"\n      },\n      \"language\": {\n        \"label\": \"Язык\",\n        \"placeholder\": \"Выберите язык\"\n      },\n      \"darkMode\": {\n        \"label\": \"Сменить тему\",\n        \"options\": {\n          \"light\": \"Светлая\",\n          \"dark\": \"Темная\"\n        }\n      },\n      \"copilotResumeLastChat\": {\n        \"label\": \"Возобновить последний чат при открытии боковой панели (Copilot)\"\n      },\n      \"turnOnChatWithWebsite\": {\n        \"label\": \"Включить чат с веб-сайтом по умолчанию (Copilot)\"\n      },\n      \"webUIResumeLastChat\": {\n        \"label\": \"Возобновить последний чат при открытии веб-интерфейса\"\n      },\n      \"hideCurrentChatModelSettings\": {\n        \"label\": \"Скрыть текущие настройки модели чата\"\n      },\n      \"restoreLastChatModel\": {\n        \"label\": \"Восстановить последнюю использованную модель для предыдущих чатов\"\n      },\n      \"sendNotificationAfterIndexing\": {\n        \"label\": \"Отправить уведомление после завершения обработки базы знаний\"\n      },\n      \"generateTitle\": {\n        \"label\": \"Сгенерировать заголовок с помощью ИИ\"\n      },\n      \"ollamaStatus\": {\n        \"label\": \"Включить или отключить проверку состояния подключения Ollama\"\n      },\n      \"wideMode\": {\n        \"label\": \"Включить широкоэкранный режим\"\n      },\n      \"openReasoning\": {\n        \"label\": \"Открыть рассуждения свернутыми по умолчанию\"\n      },\n      \"userChatBubble\": {\n        \"label\": \"Использовать пузырь чата для сообщений пользователя\"\n      },\n      \"autoCopyResponseToClipboard\": {\n        \"label\": \"Автоматически копировать ответ в буфер обмена\"\n      },\n      \"useMarkdownForUserMessage\": {\n        \"label\": \"Включить форматирование Markdown для сообщений пользователя\"\n      },\n      \"copyAsFormattedText\": {\n        \"label\": \"Копировать как форматированный текст\"\n      },\n      \"tabMentionsEnabled\": {\n        \"label\": \"Включить упоминания вкладок (@tab)\"\n      },\n      \"pasteLargeTextAsFile\": {\n        \"label\": \"Вставить большой текст как файл\"\n      },\n      \"sidepanelTemporaryChat\": {\n        \"label\": \"Включить временный чат в боковой панели по умолчанию\"\n      }\n    },\n    \"sidepanelRag\": {\n      \"heading\": \"Настройки поиска\",\n      \"ragEnabled\": {\n        \"label\": \"Включить встраивание и поиск\"\n      },\n      \"maxWebsiteContext\": {\n        \"label\": \"Максимальный размер контента для режима полного контекста\",\n        \"placeholder\": \"Размер контента (по умолчанию 4028)\"\n      }\n    },\n    \"webSearch\": {\n      \"heading\": \"Управление веб-поиском\",\n      \"searchMode\": {\n        \"label\": \"Выполнить простой интернет-поиск\"\n      },\n      \"provider\": {\n        \"label\": \"Поисковый движок\",\n        \"placeholder\": \"Выберите поисковый движок\"\n      },\n      \"totalSearchResults\": {\n        \"label\": \"Общее количество результатов поиска\",\n        \"placeholder\": \"Введите общее количество результатов поиска\"\n      },\n      \"visitSpecificWebsite\": {\n        \"label\": \"Посетите веб-сайт, указанный в сообщении.\"\n      },\n      \"searxng\": {\n        \"url\": {\n          \"label\": \"URL-адрес SearXNG\"\n        }\n      },\n      \"braveApi\": {\n        \"label\": \"API-ключ Brave\",\n        \"placeholder\": \"Введите ваш API-ключ Brave\"\n      },\n      \"searchOnByDefault\": {\n        \"label\": \"Поиск в интернете включен по умолчанию\"\n      }\n    },\n    \"system\": {\n      \"heading\": \"Настройки системы\",\n      \"storageSyncEnabled\": {\n        \"label\": \"Включить синхронизацию хранилища браузера (синхронизировать настройки между устройствами)\"\n      },\n      \"deleteChatHistory\": {\n        \"label\": \"Сброс системы\",\n        \"button\": \"Сбросить все\",\n        \"confirm\": \"Вы уверены, что хотите выполнить сброс системы? Это удалит все данные без возможности восстановления.\"\n      },\n      \"export\": {\n        \"label\": \"Экспорт всех данных (история чата, база знаний, подсказки и настройки)\",\n        \"button\": \"Экспорт данных\",\n        \"success\": \"Экспорт выполнен успешно\"\n      },\n      \"import\": {\n        \"label\": \"Импорт всех данных (история чата, база знаний, подсказки и настройки)\",\n        \"button\": \"Импорт данных\",\n        \"success\": \"Импорт выполнен успешно\",\n        \"error\": \"Ошибка импорта\"\n      }\n    },\n    \"tts\": {\n      \"heading\": \"Настройки текст в речь\",\n      \"ttsEnabled\": {\n        \"label\": \"Включить текст в речь\"\n      },\n      \"ttsAutoPlay\": {\n        \"label\": \"Автоматическое воспроизведение голосового ответа после завершения\"\n      },\n      \"ttsProvider\": {\n        \"label\": \"Поставщик текста в речь\",\n        \"placeholder\": \"Выберите поставщика\"\n      },\n      \"ttsVoice\": {\n        \"label\": \"Голос текста в речь\",\n        \"placeholder\": \"Выберите голос\"\n      },\n      \"ssmlEnabled\": {\n        \"label\": \"Включить SSML (язык разметки синтеза речи)\"\n      },\n      \"removeReasoningTagTTS\": {\n        \"label\": \"Удалить тег рассуждения из TTS\"\n      }\n    },\n    \"stt\": {\n      \"heading\": \"Настройки речь в текст\",\n      \"autoStopTimeout\": {\n        \"label\": \"Тайм-аут автоостановки (мс)\",\n        \"placeholder\": \"Введите тайм-аут автоостановки в миллисекундах\"\n      },\n      \"autoSubmitVoiceMessage\": {\n        \"label\": \"Автоматическая отправка голосового сообщения\"\n      }\n    }\n  },\n  \"manageModels\": {\n    \"title\": \"Управление моделями\",\n    \"addBtn\": \"Добавить новую модель\",\n    \"columns\": {\n      \"name\": \"Название\",\n      \"digest\": \"Дайджест\",\n      \"modifiedAt\": \"Изменено\",\n      \"size\": \"Размер\",\n      \"actions\": \"Действия\"\n    },\n    \"expandedColumns\": {\n      \"parentModel\": \"Родительская модель\",\n      \"format\": \"Формат\",\n      \"family\": \"Семейство\",\n      \"parameterSize\": \"Размер параметров\",\n      \"quantizationLevel\": \"Уровень квантования\"\n    },\n    \"tooltip\": {\n      \"delete\": \"Удалить модель\",\n      \"repull\": \"Переполучить модель\"\n    },\n    \"confirm\": {\n      \"delete\": \"Вы уверены, что хотите удалить эту модель?\",\n      \"repull\": \"Вы уверены, что хотите переполучить эту модель?\"\n    },\n    \"modal\": {\n      \"title\": \"Добавить новую модель\",\n      \"placeholder\": \"Введите название модели\",\n      \"pull\": \"Получить модель\"\n    },\n    \"notification\": {\n      \"pullModel\": \"Получение модели\",\n      \"pullModelDescription\": \"Получение модели {{modelName}}. Для получения дополнительной информации проверьте значок расширения.\",\n      \"success\": \"Успех\",\n      \"error\": \"Ошибка\",\n      \"successDescription\": \"Модель успешно получена\",\n      \"successDeleteDescription\": \"Модель успешно удалена\",\n      \"someError\": \"Что-то пошло не так. Пожалуйста, попробуйте позже\"\n    }\n  },\n  \"managePrompts\": {\n    \"title\": \"Управление подсказками\",\n    \"addBtn\": \"Добавить новую подсказку\",\n    \"option1\": \"Обычная\",\n    \"option2\": \"RAG\",\n    \"questionPrompt\": \"Вопросная подсказка\",\n    \"columns\": {\n      \"title\": \"Название\",\n      \"prompt\": \"Подсказка\",\n      \"type\": \"Тип подсказки\",\n      \"actions\": \"Действия\"\n    },\n    \"systemPrompt\": \"Системная подсказка\",\n    \"quickPrompt\": \"Быстрая подсказка\",\n    \"tooltip\": {\n      \"delete\": \"Удалить подсказку\",\n      \"edit\": \"Редактировать подсказку\"\n    },\n    \"confirm\": {\n      \"delete\": \"Вы уверены, что хотите удалить эту подсказку? Это действие нельзя отменить.\"\n    },\n    \"modal\": {\n      \"addTitle\": \"Добавить новую подсказку\",\n      \"editTitle\": \"Редактировать подсказку\"\n    },\n    \"segmented\": {\n      \"custom\": \"Пользовательские подсказки\",\n      \"copilot\": \"Подсказки Copilot\"\n    },\n    \"form\": {\n      \"title\": {\n        \"label\": \"Название\",\n        \"placeholder\": \"Моя замечательная подсказка\",\n        \"required\": \"Пожалуйста, введите название\"\n      },\n      \"prompt\": {\n        \"label\": \"Подсказка\",\n        \"placeholder\": \"Введите подсказку\",\n        \"required\": \"Пожалуйста, введите подсказку\",\n        \"help\": \"Вы можете использовать {key} в качестве переменной в своей подсказке.\",\n        \"missingTextPlaceholder\": \"Переменная {text} отсутствует в подсказке. Пожалуйста, добавьте ее.\"\n      },\n      \"isSystem\": {\n        \"label\": \"Это системная подсказка\"\n      },\n      \"btnSave\": {\n        \"saving\": \"Добавление подсказки...\",\n        \"save\": \"Добавить подсказку\"\n      },\n      \"btnEdit\": {\n        \"saving\": \"Обновление подсказки...\",\n        \"save\": \"Обновить подсказку\"\n      }\n    },\n    \"notification\": {\n      \"addSuccess\": \"Подсказка добавлена\",\n      \"addSuccessDesc\": \"Подсказка успешно добавлена\",\n      \"error\": \"Ошибка\",\n      \"someError\": \"Что-то пошло не так. Пожалуйста, попробуйте позже\",\n      \"updatedSuccess\": \"Подсказка обновлена\",\n      \"updatedSuccessDesc\": \"Подсказка успешно обновлена\",\n      \"deletedSuccess\": \"Подсказка удалена\",\n      \"deletedSuccessDesc\": \"Подсказка успешно удалена\"\n    }\n  },\n  \"manageShare\": {\n    \"title\": \"Управление обменом\",\n    \"heading\": \"Настройка URL обмена страницей\",\n    \"form\": {\n      \"url\": {\n        \"label\": \"URL обмена страницей\",\n        \"placeholder\": \"Введите URL обмена страницей\",\n        \"required\": \"Пожалуйста, введите ваш URL обмена страницей!\",\n        \"help\": \"По соображениям конфиденциальности вы можете самостоятельно разместить страницу обмена и указать здесь URL. <anchor>Узнать больше</anchor>.\"\n      }\n    },\n    \"webshare\": {\n      \"heading\": \"Веб-обмен\",\n      \"columns\": {\n        \"title\": \"Название\",\n        \"url\": \"URL\",\n        \"actions\": \"Действия\"\n      },\n      \"tooltip\": {\n        \"delete\": \"Удалить обмен\"\n      },\n      \"confirm\": {\n        \"delete\": \"Вы уверены, что хотите удалить этот обмен? Это действие нельзя отменить.\"\n      },\n      \"label\": \"Управление общим доступом к странице\",\n      \"description\": \"Включить или отключить функцию обмена страницей\"\n    },\n    \"notification\": {\n      \"pageShareSuccess\": \"URL обмена страницей успешно обновлен\",\n      \"someError\": \"Что-то пошло не так. Пожалуйста, попробуйте позже\",\n      \"webShareDeleteSuccess\": \"Веб-обмен успешно удален\"\n    }\n  },\n  \"ollamaSettings\": {\n    \"title\": \"Настройки Ollama\",\n    \"heading\": \"Настройка Ollama\",\n    \"settings\": {\n      \"ollamaUrl\": {\n        \"label\": \"URL Ollama\",\n        \"placeholder\": \"Введите URL Ollama\"\n      },\n      \"globalEnable\": {\n        \"label\": \"Включить или отключить интеграцию Ollama глобально\",\n        \"warning\": \"При отключении глобальной интеграции Ollama, Page Assist не будет получать модели из Ollama. Вы все еще можете добавить экземпляр Ollama из раздела <anchor>API, совместимого с OpenAI</anchor>, который будет работать нормально.\"\n      },\n      \"advanced\": {\n        \"label\": \"Расширенная конфигурация URL Ollama\",\n        \"urlRewriteEnabled\": {\n          \"label\": \"Включить или отключить пользовательский исходный URL\"\n        },\n        \"rewriteUrl\": {\n          \"label\": \"Пользовательский исходный URL\",\n          \"placeholder\": \"Введите пользовательский исходный URL\"\n        },\n        \"autoCORSFix\": {\n          \"label\": \"Включить или отключить автоматическое исправление CORS для Ollama\"\n        },\n        \"headers\": {\n          \"label\": \"Пользовательские Заголовки\",\n          \"add\": \"Добавить Заголовок\",\n          \"key\": {\n            \"label\": \"Ключ Заголовка\",\n            \"placeholder\": \"Авторизация\"\n          },\n          \"value\": {\n            \"label\": \"Значение Заголовка\",\n            \"placeholder\": \"Токен Bearer\"\n          }\n        },\n        \"help\": \"Если у вас возникают проблемы с подключением к Ollama на странице помощника, вы можете настроить пользовательский исходный URL. Чтобы узнать больше о конфигурации, <anchor>нажмите здесь</anchor>.\"\n      }\n    }\n  },\n  \"manageSearch\": {\n    \"title\": \"Управление веб-поиском\",\n    \"heading\": \"Настройка веб-поиска\"\n  },\n  \"about\": {\n    \"title\": \"О программе\",\n    \"heading\": \"О программе\",\n    \"chromeVersion\": \"Версия Page Assist\",\n    \"ollamaVersion\": \"Версия Ollama\",\n    \"support\": \"Вы можете поддержать проект Page Assist, сделав пожертвование или спонсорирование через следующие платформы:\",\n    \"koFi\": \"Поддержать на Ko-fi\",\n    \"githubSponsor\": \"Стать спонсором на GitHub\",\n    \"githubRepo\": \"Репозиторий GitHub\"\n  },\n  \"manageKnowledge\": {\n    \"title\": \"Управление знаниями\",\n    \"heading\": \"Настройка базы знаний\"\n  },\n  \"rag\": {\n    \"title\": \"Настройки RAG\",\n    \"ragSettings\": {\n      \"label\": \"Настройки RAG\",\n      \"model\": {\n        \"label\": \"Модель вложения\",\n        \"required\": \"Пожалуйста, выберите модель\",\n        \"help\": \"Настоятельно рекомендуется использовать модели вложения, например, `nomic-embed-text`.\",\n        \"placeholder\": \"Выберите модель\"\n      },\n      \"chunkSize\": {\n        \"label\": \"Размер фрагмента\",\n        \"placeholder\": \"Введите размер фрагмента\",\n        \"required\": \"Пожалуйста, введите размер фрагмента\"\n      },\n      \"chunkOverlap\": {\n        \"label\": \"Перекрытие фрагментов\",\n        \"placeholder\": \"Введите перекрытие фрагментов\",\n        \"required\": \"Пожалуйста, введите перекрытие фрагментов\"\n      },\n      \"totalFilePerKB\": {\n        \"label\": \"Лимит загрузки файлов по умолчанию для базы знаний\",\n        \"placeholder\": \"Введите лимит загрузки файлов по умолчанию (например, 10)\",\n        \"required\": \"Пожалуйста, введите лимит загрузки файлов по умолчанию\"\n      },\n      \"noOfRetrievedDocs\": {\n        \"label\": \"Количество извлеченных документов\",\n        \"placeholder\": \"Введите количество извлеченных документов\",\n        \"required\": \"Пожалуйста, введите количество извлеченных документов\"\n      },\n      \"splittingSeparator\": {\n        \"label\": \"Разделитель\",\n        \"placeholder\": \"Введите разделитель (например, \\\\n\\\\n)\",\n        \"required\": \"Пожалуйста, введите разделитель\"\n      },\n      \"splittingStrategy\": {\n        \"label\": \"Разделитель текста\"\n      }\n    },\n    \"prompt\": {\n      \"label\": \"Настройка системной подсказки RAG\",\n      \"option1\": \"Обычная\",\n      \"option2\": \"Веб\",\n      \"alert\": \"Настройка системной подсказки здесь устарела. Используйте раздел Управление подсказками для добавления или редактирования подсказок. Этот раздел будет удален в будущем выпуске\",\n      \"systemPrompt\": \"Системная подсказка\",\n      \"systemPromptPlaceholder\": \"Введите системную подсказку\",\n      \"webSearchPrompt\": \"Подсказка для веб-поиска\",\n      \"webSearchPromptHelp\": \"Не удаляйте `{search_results}` из подсказки.\",\n      \"webSearchPromptError\": \"Пожалуйста, введите подсказку для веб-поиска\",\n      \"webSearchPromptPlaceholder\": \"Введите подсказку для веб-поиска\",\n      \"webSearchFollowUpPrompt\": \"Последующая подсказка для веб-поиска\",\n      \"webSearchFollowUpPromptHelp\": \"Не удаляйте `{chat_history}` и `{question}` из подсказки.\",\n      \"webSearchFollowUpPromptError\": \"Введите подсказку для последующего веб-поиска!\",\n      \"webSearchFollowUpPromptPlaceholder\": \"Ваша подсказка для последующего веб-поиска\"\n    }\n  },\n  \"chromeAiSettings\": {\n    \"title\": \"Настройки ИИ Chrome\"\n  }\n}\n"
  },
  {
    "path": "src/assets/locale/ru/sidepanel.json",
    "content": "{\n    \"tooltip\": {\n        \"embed\": \"Внедрение страницы может занять несколько минут. Пожалуйста, подождите...\",\n        \"clear\": \"Очистить историю чата\",\n        \"history\": \"История чата\",\n        \"openwebui\": \"Открыть веб-интерфейс\"\n    }\n}"
  },
  {
    "path": "src/assets/locale/sv/chrome.json",
    "content": "{\n    \"heading\": \"Konfigurera Chrome AI\",\n    \"status\": {\n        \"label\": \"Aktivera eller inaktivera Chrome AI-stöd på Page Assist\"\n    },\n    \"error\": {\n        \"browser_not_supported\": \"Denna version av Chrome stöds inte av Gemini Nano-modellen. Uppdatera till version 127 eller senare\",\n        \"ai_not_supported\": \"Inställningen chrome://flags/#prompt-api-for-gemini-nano är inte aktiverad. Var vänlig och aktivera den.\",\n        \"ai_not_ready\": \"Gemini Nano är inte redo än; du måste dubbelkolla Chrome-inställningarna.\",\n        \"internal_error\": \"Ett internt fel uppstod. Försök igen senare.\"\n    },\n    \"errorDescription\": \"För att använda Chrome AI behöver du Chrome version 138 eller senare. Följ dessa steg:\\n\\n1. Gå till `chrome://flags/#prompt-api-for-gemini-nano` och aktivera \\\"Prompt API for Gemini Nano\\\".\\n2. Starta om Chrome för att tillämpa flaggan.\\n3. Återvänd till denna sida och klicka på \\\"Ladda ner modell\\\" — detta kommer att ladda ner en modell på 4 GB för första gången.\\n4. När nedladdningen är klar kan Gemini Nano aktiveras via Page Assist.\",\n    \"downloadModel\": \"Ladda ner modell\",\n    \"modelDownloadWarning\": \"Detta kommer att ladda ner en modell med en ungefärlig nedladdningsstorlek på mellan 1,5 GB och 2,4 GB. Se till att du har tillräckligt med diskutrymme.\"\n}"
  },
  {
    "path": "src/assets/locale/sv/common.json",
    "content": "{\n    \"pageAssist\": \"Page Assist\",\n    \"selectAModel\": \"Välj en modell\",\n    \"save\": \"Spara\",\n    \"saved\": \"Sparad\",\n    \"cancel\": \"Avbryt\",\n    \"retry\": \"Försök igen\",\n    \"share\": {\n        \"tooltip\": {\n            \"share\": \"Dela\"\n        },\n        \"modal\": {\n            \"title\": \"Dela länk till chatt\"\n        },\n        \"form\": {\n            \"defaultValue\": {\n                \"name\": \"Anonym\",\n                \"title\": \"Chatt utan titel\"\n            },\n            \"title\": {\n                \"label\": \"Chattitel\",\n                \"placeholder\": \"Ange chattitel\",\n                \"required\": \"Chattitel krävs\"\n            },\n            \"name\": {\n                \"label\": \"Ditt namn\",\n                \"placeholder\": \"Ange ditt namn\",\n                \"required\": \"Ditt namn krävs\"\n            },\n            \"btn\": {\n                \"save\": \"Generera länk\",\n                \"saving\": \"Genererar länk...\"\n            }\n        },\n        \"notification\": {\n            \"successGenerate\": \"Länk kopierad till urklipp\",\n            \"failGenerate\": \"Misslyckades med att generera länk\"\n        }\n    },\n    \"copyToClipboard\": \"Kopiera till urklipp\",\n    \"webSearch\": \"Söker på webben\",\n    \"regenerate\": \"Återskapa\",\n    \"continue\": \"Fortsätt svar\",\n    \"edit\": \"Redigera\",\n    \"delete\": \"Radera\",\n    \"saveAndSubmit\": \"Spara & Skicka\",\n    \"editMessage\": {\n        \"placeholder\": \"Skriv ett meddelande...\"\n    },\n    \"submit\": \"Skicka\",\n    \"noData\": \"Inga data\",\n    \"noHistory\": \"Ingen chattlogg\",\n    \"chatWithCurrentPage\": \"Chatta med nuvarande sidan\",\n    \"beta\": \"Beta\",\n    \"tts\": \"Läs högt\",\n    \"currentChatModelSettings\": \"Nuvarande chattmodellinställningar\",\n    \"modelSettings\": {\n        \"label\": \"Modellinställningar\",\n        \"description\": \"Ställ in modellalternativen globalt för alla chatter\",\n        \"form\": {\n            \"keepAlive\": {\n                \"label\": \"Keep Alive\",\n                \"help\": \"kontrollerar hur länge modellen kommer att vara laddad i minnet efter förfrågan (standard: 5 min)\",\n                \"placeholder\": \"Ange Keep Alive varaktighet (t.ex. 5m, 10m, 1h)\"\n            },\n            \"temperature\": {\n                \"label\": \"Temperatur\",\n                \"placeholder\": \"Ange temperaturvärde (t.ex. 0.7, 1.0)\"\n            },\n            \"numCtx\": {\n                \"label\": \"Kontextfönsterstorlek (num_ctx)\",\n                \"placeholder\": \"Ange kontextfönsterstorlek (standard: 2048)\"\n            },\n            \"numPredict\": {\n                \"label\": \"Max antal tokens (num_predict)\",\n                \"placeholder\": \"Ange Max antal tokens värde (t.ex. 2048, 4096)\"\n            },\n            \"seed\": {\n                \"label\": \"Frö\",\n                \"placeholder\": \"Ange frövärde (t.ex. 1234)\",\n                \"help\": \"Reproducerbarhet av modellens utskrift\"\n            },\n            \"topK\": {\n                \"label\": \"Topp K\",\n                \"placeholder\": \"Ange Topp K värde (t.ex. 40, 100)\"\n            },\n            \"topP\": {\n                \"label\": \"Topp P\",\n                \"placeholder\": \"Ange Topp P värde (t.ex. 0.9, 0.95)\"\n            },\n            \"numGpu\": {\n                \"label\": \"Antal GPU\",\n                \"placeholder\": \"Ange antal lager att skicka till GPU(s)\"\n            },\n            \"systemPrompt\": {\n                \"label\": \"Tillfällig systempromt\",\n                \"placeholder\": \"Ange systemprompt\",\n                \"help\": \"Detta är ett snabbt sätt att ställa in systemprompten i den nuvarande chatten, vilket kommer att åsidosätta den valda systemprompten om den finns.\"\n            }\n        },\n        \"advanced\": \"Fler modellinställningar\"\n    },\n    \"copilot\": {\n        \"summary\": \"Sammanfatta\",\n        \"explain\": \"Förklara\",\n        \"rephrase\": \"Formulera om\",\n        \"translate\": \"Översätt\",\n        \"custom\": \"Custom\"\n    },\n    \"citations\": \"Citat\",\n    \"segmented\": {\n        \"ollama\": \"Ollama-modeller\",\n        \"custom\": \"Custom modeller\"\n    },\n    \"downloadCode\": \"Ladda ner kod\",\n    \"date\": {\n        \"pinned\": \"Fäst\",\n        \"today\": \"Idag\",\n        \"yesterday\": \"Igår\",\n        \"last7Days\": \"Senaste 7 dagarna\",\n        \"older\": \"Äldre\"\n    },\n    \"range\": {\n        \"deleteConfirm\": {\n            \"pinned\": \"Är du säker på att du vill ta bort alla fästa meddelanden?\",\n            \"today\": \"Är du säker på att du vill ta bort alla meddelanden från idag?\",\n            \"yesterday\": \"Är du säker på att du vill ta bort alla meddelanden från igår?\",\n            \"last7Days\": \"Är du säker på att du vill ta bort alla meddelanden från de senaste 7 dagarna?\",\n            \"older\": \"Är du säker på att du vill ta bort alla äldre meddelanden?\"\n        },\n        \"tooltip\": {\n            \"pinned\": \"Ta bort alla fästa meddelanden\",\n            \"today\": \"Ta bort alla meddelanden från idag\",\n            \"yesterday\": \"Ta bort alla meddelanden från igår\",\n            \"last7Days\": \"Ta bort alla meddelanden från de senaste 7 dagarna\",\n            \"older\": \"Ta bort alla äldre meddelanden\"\n        }\n    },\n    \"pin\": \"Fäst\",\n    \"unpin\": \"Ta bort fäst\",\n    \"generationInfo\": \"Generationsinformation\",\n    \"sidebarChat\": \"Sidofältschatt\",\n    \"reasoning\": {\n        \"thinking\": \"Tänker....\",\n        \"thought\": \"Tänkte i {{time}}\"\n    },\n    \"embeddingGen\": \"Skapar embeddings, detta kan ta lite tid\",\n    \"semanticSearch\": \"Utför semantisk sökning\",\n    \"downloading\": \"Laddar ner\",\n    \"cancelPullingModel\": {\n        \"confirm\": \"Är du säker på att du vill avbryta nedladdningen? Detta kommer att stoppa nedladdningsprocessen. Enligt Ollama-dokumentationen kan du återuppta från där du slutade.\"\n    }\n}"
  },
  {
    "path": "src/assets/locale/sv/knowledge.json",
    "content": "{\n    \"addBtn\": \"Lägg till ny kunskap\",\n    \"columns\": {\n        \"title\": \"Titel\",\n        \"status\": \"Status\",\n        \"embeddings\": \"Inbäddningsmodell\",\n        \"createdAt\": \"Skapad den\",\n        \"action\": \"Åtgärder\"\n    },\n    \"expandedColumns\": {\n        \"name\": \"Namn\"\n    },\n    \"confirm\": {\n        \"delete\": \"Är du säker på att du vill ta bort denna kunskap?\"\n    },\n    \"deleteSuccess\": \"Kunskap raderades framgångsrikt\",\n    \"status\": {\n        \"pending\": \"Väntar\",\n        \"finished\": \"Klar\",\n        \"processing\": \"Bearbetar\",\n        \"failed\": \"Misslyckades\"\n    },\n    \"addKnowledge\": \"Lägg till kunskap\",\n    \"updateKnowledge\": \"Lägg till källa\",\n    \"form\": {\n        \"title\": {\n            \"label\": \"Kunskapstitel\",\n            \"placeholder\": \"Ange kunskapstitel\",\n            \"required\": \"Kunskapstitel är obligatorisk\"\n        },\n        \"uploadFile\": {\n            \"label\": \"Ladda upp fil\",\n            \"uploadText\": \"Dra och släpp en fil här eller klicka för att ladda upp\",\n            \"uploadHint\": \"Stödda filtyper: .pdf, .csv, .txt, .md, .docx\",\n            \"required\": \"Fil är obligatorisk\"\n        },\n        \"submit\": \"Skicka in\",\n        \"success\": \"Kunskap tillagd framgångsrikt\"\n    },\n    \"noEmbeddingModel\": \"Vänligen lägg till en inbäddningsmodell från sidan för RAG-inställningar först\"\n}"
  },
  {
    "path": "src/assets/locale/sv/openai.json",
    "content": "{\n    \"settings\": \"OpenAI-kompatibel API\",\n    \"heading\": \"OpenAI-kompatibel API\",\n    \"subheading\": \"Hantera och konfigurera dina OpenAI API-kompatibla leverantörer här.\",\n    \"addBtn\": \"Lägg till leverantör\",\n    \"table\": {\n        \"name\": \"Leverantörsnamn\",\n        \"baseUrl\": \"Bas-URL\",\n        \"actions\": \"Åtgärd\"\n    },\n    \"modal\": {\n        \"titleAdd\": \"Lägg till ny leverantör\",\n        \"name\": {\n            \"label\": \"Leverantörsnamn\",\n            \"required\": \"Leverantörsnamn krävs.\",\n            \"placeholder\": \"Ange leverantörsnamn\"\n        },\n        \"baseUrl\": {\n            \"label\": \"Bas-URL\",\n            \"help\": \"Bas-URL för OpenAI API-leverantören. t.ex. (http://localhost:1234/v1)\",\n            \"required\": \"Bas-URL krävs.\",\n            \"placeholder\": \"Ange bas-URL\"\n        },\n        \"apiKey\": {\n            \"label\": \"API-nyckel\",\n            \"required\": \"API-nyckel krävs.\",\n            \"placeholder\": \"Ange API-nyckel\"\n        },\n        \"submit\": \"Spara\",\n        \"update\": \"Uppdatera\",\n        \"deleteConfirm\": \"Är du säker på att du vill radera denna leverantör?\",\n        \"model\": {\n            \"title\": \"Modellista\",\n            \"subheading\": \"Vänligen välj chattmodellerna du vill använda med denna leverantör.\",\n            \"success\": \"Nya modeller har lagts till framgångsrikt.\"\n        },\n        \"tipLMStudio\": \"Page Assist hämtar automatiskt modellerna du laddat i LM Studio. Ingen manuell tilläggning behövs.\"\n    },\n    \"addSuccess\": \"Leverantör tillagd framgångsrikt.\",\n    \"deleteSuccess\": \"Leverantör raderad framgångsrikt.\",\n    \"updateSuccess\": \"Leverantör uppdaterad framgångsrikt.\",\n    \"delete\": \"Radera\",\n    \"edit\": \"Redigera\",\n    \"newModel\": \"Lägg till modeller till leverantör\",\n    \"noNewModel\": \"För LMStudio hämtar vi dynamiskt. Ingen manuell tilläggning behövs.\",\n    \"searchModel\": \"Sök modell\",\n    \"selectAll\": \"Välj alla\",\n    \"save\": \"Spara\",\n    \"saving\": \"Sparar...\",\n    \"manageModels\": {\n        \"columns\": {\n            \"name\": \"Modellnamn\",\n            \"model_type\": \"Modelltyp\",\n            \"model_id\": \"Modell-ID\",\n            \"provider\": \"Leverantörsnamn\",\n            \"actions\": \"Åtgärd\",\n            \"nickname\": \"Modellnamn\"\n        },\n        \"tooltip\": {\n            \"delete\": \"Radera\"\n        },\n        \"confirm\": {\n            \"delete\": \"Är du säker på att du vill radera denna modell?\"\n        },\n        \"modal\": {\n            \"title\": \"Lägg till Custom-modell\",\n            \"form\": {\n                \"name\": {\n                    \"label\": \"Modell-ID\",\n                    \"placeholder\": \"llama3.2\",\n                    \"required\": \"Modell-ID krävs.\"\n                },\n                \"provider\": {\n                    \"label\": \"Leverantör\",\n                    \"placeholder\": \"Välj leverantör\",\n                    \"required\": \"Leverantör krävs.\"\n                },\n                \"type\": {\n                    \"label\": \"Modelltyp\"\n                }\n            }\n        }\n    },\n    \"noModelFound\": \"Ingen modell hittades. Se till att du har lagt till korrekt leverantör med bas-URL och API-nyckel.\",\n    \"radio\": {\n        \"chat\": \"Chattmodell\",\n        \"embedding\": \"Inbäddningsmodell\",\n        \"chatInfo\": \"används för chattkompletion och konversationsgenerering\",\n        \"embeddingInfo\": \"används för RAG och andra semantiska sökrelaterade uppgifter.\"\n    },\n    \"nicknameModal\": {\n        \"title\": \"Lägg till / Redigera modellnamn\",\n        \"form\": {\n            \"modelName\": {\n                \"label\": \"Modellnamn\",\n                \"placeholder\": \"Ange modellnamn\",\n                \"required\": \"Modellnamn krävs.\"\n            },\n            \"modelAvatar\": {\n                \"label\": \"Modellavatar\",\n                \"placeholder\": \"Ange modellavatar\",\n                \"help\": \"Ange URL:en för modellavataren. Denna bild kommer att visas i chattfönstret.\"\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/assets/locale/sv/option.json",
    "content": "{\n    \"newChat\": \"Ny chatt\",\n    \"selectAPrompt\": \"Välj en fråga\",\n    \"githubRepository\": \"GitHu repository\",\n    \"settings\": \"Inställningar\",\n    \"sidebarTitle\": \"Chatthistorik\",\n    \"error\": \"Fel\",\n    \"somethingWentWrong\": \"Något gick fel\",\n    \"validationSelectModel\": \"Vänligen välj en modell för att fortsätta\",\n    \"deleteHistoryConfirmation\": \"Är du säker på att du vill radera denna historik?\",\n    \"editHistoryTitle\": \"Ange en ny titel\",\n    \"temporaryChat\": \"Tillfällig chatt\", \n    \"more\": {\n        \"copy\": {\n            \"group\": \"Kopiera\",\n            \"asText\": \"Kopiera som text\",\n            \"asMarkdown\": \"Kopiera som Markdown\",\n            \"success\": \"Kopierat till urklipp!\"\n        },\n        \"download\": {\n            \"group\": \"Ladda ner\",\n            \"text\": \"Textfil (.txt)\",\n            \"markdown\": \"Markdown (.md)\",\n            \"json\": \"JSON-fil (.json)\"\n        },\n        \"share\": \"Dela\"\n    }\n}\n"
  },
  {
    "path": "src/assets/locale/sv/playground.json",
    "content": "{\n    \"ollamaState\": {\n        \"searching\": \"Söker efter din Ollama 🦙\",\n        \"running\": \"Ollama körs 🦙\",\n        \"notRunning\": \"Kan inte ansluta till Ollama 🦙\",\n        \"connectionError\": \"Det verkar som att du har ett anslutningsfel. Vänligen se denna <anchor>dokumentation</anchor> för felsökning.\"\n    },\n    \"formError\": {\n        \"noModel\": \"Vänligen välj en modell\",\n        \"noEmbeddingModel\": \"Vänligen ställ in en inbäddningsmodell på sidan Inställningar > RAG\"\n    },\n    \"form\": {\n        \"textarea\": {\n            \"placeholder\": \"Skriv ett meddelande...\"\n        },\n        \"webSearch\": {\n            \"on\": \"På\",\n            \"off\": \"Av\"\n        }\n    },\n    \"tooltip\": {\n        \"searchInternet\": \"Sök på Internet\",\n        \"speechToText\": \"Tal till text\",\n        \"uploadImage\": \"Ladda upp bild\",\n        \"stopStreaming\": \"Stoppa strömning\",\n        \"knowledge\": \"Kunskap\",\n        \"clearContext\": \"Rensa kontext\"\n    },\n    \"sendWhenEnter\": \"Skicka när Enter trycks\",\n    \"welcome\": \"Hej! Hur kan jag hjälpa dig idag?\",\n    \"useOCR\": \"Extrahera text från bild (OCR)\"\n}"
  },
  {
    "path": "src/assets/locale/sv/settings.json",
    "content": "{\n  \"generalSettings\": {\n    \"title\": \"Allmänna Inställningar\",\n    \"settings\": {\n      \"heading\": \"Webbgränssnitt Inställningar\",\n      \"speechRecognitionLang\": {\n        \"label\": \"Taligenkänningsspråk\",\n        \"placeholder\": \"Välj ett språk\"\n      },\n      \"language\": {\n        \"label\": \"Språk\",\n        \"placeholder\": \"Välj ett språk\"\n      },\n      \"darkMode\": {\n        \"label\": \"Byt Tema\",\n        \"options\": {\n          \"light\": \"Ljust\",\n          \"dark\": \"Mörkt\"\n        }\n      },\n      \"copilotResumeLastChat\": {\n        \"label\": \"Återuppta den senaste chatten när du öppnar sidopanelen (Copilot)\"\n      },\n      \"turnOnChatWithWebsite\": {\n        \"label\": \"Aktivera Chatta med Webbplats som standard (Copilot)\"\n      },\n      \"webUIResumeLastChat\": {\n        \"label\": \"Återuppta den senaste chatten när du öppnar webbgränssnittet\"\n      },\n      \"hideCurrentChatModelSettings\": {\n        \"label\": \"Göm de nuvarande chattmodellinställningarna\"\n      },\n      \"restoreLastChatModel\": {\n        \"label\": \"Återställ den senast använda modellen för tidigare chatter\"\n      },\n      \"sendNotificationAfterIndexing\": {\n        \"label\": \"Skicka meddelande efter att ha avslutat bearbetning av kunskapsbasen\"\n      },\n      \"generateTitle\": {\n        \"label\": \"Generera titel med AI\"\n      },\n      \"ollamaStatus\": {\n        \"label\": \"Aktivera eller inaktivera Ollama anslutningsstatuskontroll\"\n      },\n      \"wideMode\": {\n        \"label\": \"Aktivera bredskärmsläge\"\n      },\n      \"openReasoning\": {\n        \"label\": \"Öppna resonemang kollapsat som standard\"\n      },\n      \"userChatBubble\": {\n        \"label\": \"Använd chattbubbla för användarmeddelanden\"\n      },\n      \"autoCopyResponseToClipboard\": {\n        \"label\": \"Kopiera svar automatiskt till urklipp\"\n      },\n      \"useMarkdownForUserMessage\": {\n        \"label\": \"Aktivera Markdown-formatering för användarmeddelanden\"\n      },\n      \"copyAsFormattedText\": {\n        \"label\": \"Kopiera som formaterad text\"\n      },\n      \"tabMentionsEnabled\": {\n        \"label\": \"Aktivera Flikbenämningar (@tab)\"\n      },\n      \"pasteLargeTextAsFile\": {\n        \"label\": \"Klistra in stor text som fil\"\n      },\n      \"sidepanelTemporaryChat\": {\n        \"label\": \"Aktivera Temporär Chatt i Sidopanelen som standard\"\n      }\n    },\n    \"sidepanelRag\": {\n      \"heading\": \"Hämtningsinställningar\",\n      \"ragEnabled\": {\n        \"label\": \"Aktivera Inbäddning och Hämtning\"\n      },\n      \"maxWebsiteContext\": {\n        \"label\": \"Maximal Innehållsstorlek för Fullständigt Kontextläge\",\n        \"placeholder\": \"Innehållsstorlek (standard 4028)\"\n      }\n    },\n    \"webSearch\": {\n      \"heading\": \"Hantera Webb Sök\",\n      \"searchMode\": {\n        \"label\": \"Utför Enkel InternetSökning\"\n      },\n      \"provider\": {\n        \"label\": \"Sökmotor\",\n        \"placeholder\": \"Välj en sökmotor\"\n      },\n      \"totalSearchResults\": {\n        \"label\": \"Totalt Sökresultat\",\n        \"placeholder\": \"Ange Totalt Sökresultat\"\n      },\n      \"visitSpecificWebsite\": {\n        \"label\": \"Besök webbplatsen som nämns i meddelandet\"\n      },\n      \"searxng\": {\n        \"url\": {\n          \"label\": \"SearXNG URL\"\n        }\n      },\n      \"braveApi\": {\n        \"label\": \"Brave API-nyckel\",\n        \"placeholder\": \"Ange din Brave API-nyckel\"\n      },\n      \"searchOnByDefault\": {\n        \"label\": \"Internetsökning PÅ som standard\"\n      }\n    },\n    \"system\": {\n      \"heading\": \"Systeminställningar\",\n      \"storageSyncEnabled\": {\n        \"label\": \"Aktivera synkronisering av webbläsarlagring (synkronisera inställningar mellan enheter)\"\n      },\n      \"deleteChatHistory\": {\n        \"label\": \"Systemåterställning\",\n        \"button\": \"Återställ Allt\",\n        \"confirm\": \"Är du säker på att du vill utföra en systemåterställning? Detta kommer att radera all data och kan inte ångras.\"\n      },\n      \"export\": {\n        \"label\": \"Exportera all data (chatthistorik, kunskapsbas, prompts och inställningar)\",\n        \"button\": \"Exportera data\",\n        \"success\": \"Export lyckades\"\n      },\n      \"import\": {\n        \"label\": \"Importera all data (chatthistorik, kunskapsbas, prompts och inställningar)\",\n        \"button\": \"Importera data\",\n        \"success\": \"Import lyckades\",\n        \"error\": \"Importfel\"\n      }\n    },\n    \"tts\": {\n      \"heading\": \"Text till Tal Inställningar\",\n      \"ttsEnabled\": {\n        \"label\": \"Aktivera Text till Tal\"\n      },\n      \"ttsAutoPlay\": {\n        \"label\": \"Spela upp röstsvaret automatiskt efter slutförande\"\n      },\n      \"ttsProvider\": {\n        \"label\": \"Text till Tal Leverantör\",\n        \"placeholder\": \"Välj en leverantör\"\n      },\n      \"ttsVoice\": {\n        \"label\": \"Text till Tal Röst\",\n        \"placeholder\": \"Välj en röst\"\n      },\n      \"ssmlEnabled\": {\n        \"label\": \"Aktivera SSML (Speech Synthesis Markup Language)\"\n      },\n      \"removeReasoningTagTTS\": {\n        \"label\": \"Ta bort resonemangstagg från Text till Tal\"\n      }\n    },\n    \"stt\": {\n      \"heading\": \"Tal till Text Inställningar\",\n      \"autoStopTimeout\": {\n        \"label\": \"Automatiskt Stopptidsgräns (ms)\",\n        \"placeholder\": \"Ange automatisk stopptidsgräns i millisekunder\"\n      },\n      \"autoSubmitVoiceMessage\": {\n        \"label\": \"Skicka Röstmeddelande Automatiskt\"\n      }\n    }\n  },\n  \"manageModels\": {\n    \"title\": \"Hantera Modeller\",\n    \"addBtn\": \"Lägg till Ny Modell\",\n    \"columns\": {\n      \"name\": \"Namn\",\n      \"digest\": \"Sammanfattning\",\n      \"modifiedAt\": \"Modifierad Den\",\n      \"size\": \"Storlek\",\n      \"actions\": \"Åtgärder\"\n    },\n    \"expandedColumns\": {\n      \"parentModel\": \"Föräldramodell\",\n      \"format\": \"Format\",\n      \"family\": \"Familj\",\n      \"parameterSize\": \"Parameterstorlek\",\n      \"quantizationLevel\": \"Kvantifieringsnivå\"\n    },\n    \"tooltip\": {\n      \"delete\": \"Radera Modell\",\n      \"repull\": \"Hämta Modell Igen\"\n    },\n    \"confirm\": {\n      \"delete\": \"Är du säker på att du vill radera denna modell?\",\n      \"repull\": \"Är du säker på att du vill hämta modellen igen?\"\n    },\n    \"modal\": {\n      \"title\": \"Lägg till Ny Modell\",\n      \"placeholder\": \"Ange Modellnamn\",\n      \"pull\": \"Hämta Modell\"\n    },\n    \"notification\": {\n      \"pullModel\": \"Hämtar Modell\",\n      \"pullModelDescription\": \"Hämtar {{modelName}} modell. För mer information, kontrollera ikonen för tillägget.\",\n      \"success\": \"Lyckades\",\n      \"error\": \"Fel\",\n      \"successDescription\": \"Modellen hämtades framgångsrikt\",\n      \"successDeleteDescription\": \"Modellen raderades framgångsrikt\",\n      \"someError\": \"Något gick fel. Försök igen senare\"\n    }\n  },\n  \"managePrompts\": {\n    \"title\": \"Hantera instruktioner\",\n    \"addBtn\": \"Lägg till ny instruktion\",\n    \"option1\": \"Normal\",\n    \"option2\": \"RAG\",\n    \"questionPrompt\": \"Frågeinstruktion\",\n    \"segmented\": {\n      \"custom\": \"Custom instruktion\",\n      \"copilot\": \"Copilot instruktion\"\n    },\n    \"columns\": {\n      \"title\": \"Titel\",\n      \"prompt\": \"instruktion\",\n      \"type\": \"Instruktionstyp\",\n      \"actions\": \"Åtgärder\"\n    },\n    \"systemPrompt\": \"Systeminstruktion\",\n    \"quickPrompt\": \"Snabb instruktion\",\n    \"tooltip\": {\n      \"delete\": \"Radera instruktion\",\n      \"edit\": \"Redigera instruktion\"\n    },\n    \"confirm\": {\n      \"delete\": \"Är du säker på att du vill radera denna instruktion? Denna åtgärd kan inte ångras.\"\n    },\n    \"modal\": {\n      \"addTitle\": \"Lägg till ny instruktion\",\n      \"editTitle\": \"Redigera instruktion\"\n    },\n    \"form\": {\n      \"title\": {\n        \"label\": \"Titel\",\n        \"placeholder\": \"Min fantastiska instruktion\",\n        \"required\": \"Vänligen ange en titel\"\n      },\n      \"prompt\": {\n        \"label\": \"Instruktion\",\n        \"placeholder\": \"Ange instruktion\",\n        \"required\": \"Vänligen ange en instruktion\",\n        \"help\": \"Du kan använda {key} som variabel i din instruktion.\",\n        \"missingTextPlaceholder\": \"Variabeln {text} saknas i instruktionen. Lägg till den.\"\n      },\n      \"isSystem\": {\n        \"label\": \"Är Systeminstruktion\"\n      },\n      \"btnSave\": {\n        \"saving\": \"Lägger till instruktion...\",\n        \"save\": \"Lägg till instruktion\"\n      },\n      \"btnEdit\": {\n        \"saving\": \"Uppdaterar instruktion...\",\n        \"save\": \"Uppdatera instruktion\"\n      }\n    },\n    \"notification\": {\n      \"addSuccess\": \"Instruktion Tillagd\",\n      \"addSuccessDesc\": \"Instruktionen har lagts till framgångsrikt\",\n      \"error\": \"Fel\",\n      \"someError\": \"Något gick fel. Försök igen senare\",\n      \"updatedSuccess\": \"Instruktion Uppdaterad\",\n      \"updatedSuccessDesc\": \"Instruktionen har uppdaterats framgångsrikt\",\n      \"deletedSuccess\": \"Instruktion Raderad\",\n      \"deletedSuccessDesc\": \"Instruktionen har raderats framgångsrikt\"\n    }\n  },\n  \"manageShare\": {\n    \"title\": \"Hantera Delningar\",\n    \"heading\": \"Konfigurera Sidans Delnings-URL\",\n    \"form\": {\n      \"url\": {\n        \"label\": \"Sidans Delnings-URL\",\n        \"placeholder\": \"Ange Sidans Delnings-URL\",\n        \"required\": \"Vänligen ange din Sidans Delnings-URL!\",\n        \"help\": \"För integritetsskäl, kan du använda egen värd för sidans delning och ange URL här. <anchor>Läs Mer</anchor>.\"\n      }\n    },\n    \"webshare\": {\n      \"heading\": \"Webbdelning\",\n      \"columns\": {\n        \"title\": \"Titel\",\n        \"url\": \"URL\",\n        \"actions\": \"Åtgärder\"\n      },\n      \"tooltip\": {\n        \"delete\": \"Radera Delning\"\n      },\n      \"confirm\": {\n        \"delete\": \"Är du säker på att du vill radera denna delning? Denna åtgärd kan inte ångras.\"\n      },\n      \"label\": \"Hantera Sidans Delning\",\n      \"description\": \"Aktivera eller inaktivera funktionen för sidans delning\"\n    },\n    \"notification\": {\n      \"pageShareSuccess\": \"Sidans Delnings-URL uppdaterad framgångsrikt\",\n      \"someError\": \"Något gick fel. Försök igen senare\",\n      \"webShareDeleteSuccess\": \"Webbdelning raderades framgångsrikt\"\n    }\n  },\n  \"ollamaSettings\": {\n    \"title\": \"Ollamainställningar\",\n    \"heading\": \"Konfigurera Ollama\",\n    \"settings\": {\n      \"ollamaUrl\": {\n        \"label\": \"Ollama URL\",\n        \"placeholder\": \"Ange Ollama URL\"\n      },\n      \"globalEnable\": {\n        \"label\": \"Aktivera eller inaktivera Ollama-integration globalt\",\n        \"warning\": \"Genom att inaktivera Ollama-integration globalt kommer Page Assist inte att hämta modeller från Ollama. Du kan fortfarande lägga till Ollama-instans från <anchor>OpenAI-kompatibel API</anchor>-sektionen som kommer att fungera bra.\"\n      },\n      \"advanced\": {\n        \"label\": \"Avancerad Ollama URL-konfiguration\",\n        \"urlRewriteEnabled\": {\n          \"label\": \"Aktivera eller inaktivera custom ursprungs-URL\"\n        },\n        \"rewriteUrl\": {\n          \"label\": \"Custom ursprungs-URL\",\n          \"placeholder\": \"Ange Custom ursprungs-URL\"\n        },\n        \"autoCORSFix\": {\n          \"label\": \"Aktivera eller inaktivera automatisk Ollama CORS-korrigering\"\n        },\n        \"headers\": {\n          \"label\": \"Custom Headers\",\n          \"add\": \"Lägg till Header\",\n          \"key\": {\n            \"label\": \"Headernyckel\",\n            \"placeholder\": \"Auktorisering\"\n          },\n          \"value\": {\n            \"label\": \"Headervärde\",\n            \"placeholder\": \"Bärare token\"\n          }\n        },\n        \"help\": \"Om du har anslutningsproblem med Ollama på Page Assist, kan du konfigurera en custom ursprungs-URL. För att veta mer om konfigurationen, <anchor>klicka här</anchor>.\"\n      }\n    }\n  },\n  \"manageSearch\": {\n    \"title\": \"Hantera Webbsök\",\n    \"heading\": \"Konfigurera Webbsök\"\n  },\n  \"about\": {\n    \"title\": \"Om\",\n    \"heading\": \"Om\",\n    \"chromeVersion\": \"Page Assist Version\",\n    \"ollamaVersion\": \"Ollamaversion\",\n    \"support\": \"Du kan stödja Page Assist-projektet genom att donera eller sponsra genom följande plattformar:\",\n    \"koFi\": \"Stöd på Ko-fi\",\n    \"githubSponsor\": \"Sponsra på GitHub\",\n    \"githubRepo\": \"GitHub repository\"\n  },\n  \"manageKnowledge\": {\n    \"title\": \"Hantera kunskap\",\n    \"heading\": \"Konfigurera kunskapsbas\"\n  },\n  \"rag\": {\n    \"title\": \"Pipeline-inställningar\",\n    \"ragSettings\": {\n      \"label\": \"RAG-inställningar\",\n      \"model\": {\n        \"label\": \"Inbäddningsmodell\",\n        \"required\": \"Vänligen välj en modell\",\n        \"help\": \"Det rekommenderas starkt att använda inbäddningsmodeller som 'nomic-embed-text'.\",\n        \"placeholder\": \"Välj en modell\"\n      },\n      \"chunkSize\": {\n        \"label\": \"Chunkstorlek\",\n        \"placeholder\": \"Ange chunkstorlek\",\n        \"required\": \"Vänligen ange en chunkstorlek\"\n      },\n      \"chunkOverlap\": {\n        \"label\": \"Chunköverlapp\",\n        \"placeholder\": \"Ange chunköverlapp\",\n        \"required\": \"Vänligen ange en chunköverlapp\"\n      },\n      \"totalFilePerKB\": {\n        \"label\": \"Standardgräns för kunskapsbas filuppladdning\",\n        \"placeholder\": \"Ange standardgräns för filuppladdning (t.ex. 10)\",\n        \"required\": \"Vänligen ange standardgräns för filuppladdning\"\n      },\n      \"noOfRetrievedDocs\": {\n        \"label\": \"Antal hämtade dokument\",\n        \"placeholder\": \"Ange antal hämtade dokument\",\n        \"required\": \"Vänligen ange antal hämtade dokument\"\n      },\n      \"splittingSeparator\": {\n        \"label\": \"Separator\",\n        \"placeholder\": \"Ange separator (t.ex. \\\\n\\\\n)\",\n        \"required\": \"Vänligen ange en separator\"\n      },\n      \"splittingStrategy\": {\n        \"label\": \"Textdelare\"\n      }\n    },\n    \"prompt\": {\n      \"label\": \"Konfigurera RAG-instruktion\",\n      \"option1\": \"Normal\",\n      \"option2\": \"Webb\",\n      \"alert\": \"Att konfigurera systeminstruktionen här är föråldrat. Använd Hantera instruktioner-sektionen för att lägga till eller redigera instruktionar. Denna sektion kommer att tas bort i en framtida version\",\n      \"systemPrompt\": \"Systeminstruktion\",\n      \"systemPromptPlaceholder\": \"Ange systeminstruktion\",\n      \"webSearchPrompt\": \"Webbsökinstruktion\",\n      \"webSearchPromptHelp\": \"Ta inte bort '{search_results}' från instruktionen.\",\n      \"webSearchPromptError\": \"Vänligen ange en instruktion för webbsökning\",\n      \"webSearchPromptPlaceholder\": \"Ange instruktion för webbsök\",\n      \"webSearchFollowUpPrompt\": \"Uppföljningsinstruktion för webbsök\",\n      \"webSearchFollowUpPromptHelp\": \"Ta inte bort '{chat_history}' och '{question}' från instruktionen.\",\n      \"webSearchFollowUpPromptError\": \"Vänligen ange din uppföljningsinstruktion för webbsök!\",\n      \"webSearchFollowUpPromptPlaceholder\": \"Din uppföljningsinstruktion för webbsök\"\n    }\n  },\n  \"chromeAiSettings\": {\n    \"title\": \"Chrome AI-inställningar\"\n  }\n}\n"
  },
  {
    "path": "src/assets/locale/sv/sidepanel.json",
    "content": "{\n    \"tooltip\": {\n        \"embed\": \"Det kan ta några minuter att bädda in sidan. Vänta...\",\n        \"clear\": \"Radera chatthistorik\",\n        \"history\": \"Chatthistorik\",\n        \"openwebui\": \"Öppna WebUI\"\n    }\n}"
  },
  {
    "path": "src/assets/locale/uk/chrome.json",
    "content": "{\n    \"heading\": \"Налаштування Chrome AI\",\n    \"status\": {\n        \"label\": \"Ввімкнути або вимкнути підтримку Chrome AI на Page Assist\"\n    },\n    \"error\": {\n        \"browser_not_supported\": \"Ця версія Chrome не підтримується моделлю Gemini Nano. Будь ласка, оновіть до версії 127 або новішої\",\n        \"ai_not_supported\": \"Налаштування chrome://flags/#prompt-api-for-gemini-nano не увімкнено. Будь ласка, увімкніть його.\",\n        \"ai_not_ready\": \"Модель Gemini Nano ще не готова; вам потрібно перевірити налаштування Chrome знову\",\n        \"internal_error\": \"Стався внутрішній збій. Будь ласка, спробуйте пізніше.\"\n    },\n    \"errorDescription\": \"Щоб використовувати Chrome AI, вам потрібна версія Chrome 138 або новіша. Виконайте ці кроки:\\n\\n1. Перейдіть до `chrome://flags/#prompt-api-for-gemini-nano` та увімкніть \\\"Prompt API for Gemini Nano\\\".\\n2. Перезапустіть Chrome, щоб застосувати прапорець.\\n3. Поверніться на цю сторінку та натисніть \\\"Завантажити модель\\\" — це завантажить модель обсягом 4 ГБ вперше.\\n4. Після завантаження Gemini Nano можна буде увімкнути за допомогою Page Assist.\",\n    \"downloadModel\": \"Завантажити модель\",\n    \"modelDownloadWarning\": \"Це призведе до завантаження моделі з приблизним розміром файлу від 1,5 ГБ до 2,4 ГБ. Переконайтеся, що у вас достатньо місця на диску.\"\n}\n"
  },
  {
    "path": "src/assets/locale/uk/common.json",
    "content": "{\n    \"pageAssist\": \"Допомога на сторінці\",\n    \"selectAModel\": \"Виберіть модель\",\n    \"save\": \"Зберегти\",\n    \"saved\": \"Збережено\",\n    \"cancel\": \"Скасувати\",\n    \"retry\": \"Спробувати знову\",\n    \"share\": {\n        \"tooltip\": {\n            \"share\": \"Поділитися\"\n        },\n        \"modal\": {\n            \"title\": \"Поділитися посиланням на чат\"\n        },\n        \"form\": {\n            \"defaultValue\": {\n                \"name\": \"Анонімний\",\n                \"title\": \"Неназваний чат\"\n            },\n            \"title\": {\n                \"label\": \"Назва чату\",\n                \"placeholder\": \"Введіть назву чату\",\n                \"required\": \"Назва чату обовʼязкова\"\n            },\n            \"name\": {\n                \"label\": \"Ваше імʼя\",\n                \"placeholder\": \"Введіть ваше імʼя\",\n                \"required\": \"Ваше імʼя обов'язкове\"\n            },\n            \"btn\": {\n                \"save\": \"Створити посилання\",\n                \"saving\": \"Створення посилання...\"\n            }\n        },\n        \"notification\": {\n            \"successGenerate\": \"Посилання скопійовано до буфера обміну\",\n            \"failGenerate\": \"Не вдалося створити посилання\"\n        }\n    },\n    \"copyToClipboard\": \"Копіювати в буфер обміну\",\n    \"webSearch\": \"Пошук у мережі\",\n    \"regenerate\": \"Перегенерувати\",\n    \"continue\": \"Продовжити відповідь\",\n    \"edit\": \"Редагувати\",\n    \"delete\": \"Видалити\",\n    \"saveAndSubmit\": \"Зберегти та надіслати\",\n    \"editMessage\": {\n        \"placeholder\": \"Введіть повідомлення...\"\n    },\n    \"submit\": \"Надіслати\",\n    \"noData\": \"Немає даних\",\n    \"noHistory\": \"Немає історії чату\",\n    \"chatWithCurrentPage\": \"Чат з поточної сторінки\",\n    \"beta\": \"Бета\",\n    \"tts\": \"Читання вголос\",\n    \"currentChatModelSettings\": \"Налаштування поточної моделі чату\",\n    \"modelSettings\": {\n        \"label\": \"Налаштування моделі\",\n        \"description\": \"Встановіть глобальні параметри моделі для всіх чатів\",\n        \"form\": {\n            \"keepAlive\": {\n                \"label\": \"Тривалість збереження в памʼяті\",\n                \"help\": \"вказує, як довго модель залишатиметься завантаженою у памʼять після запиту (за замовчуванням: 5 хв)\",\n                \"placeholder\": \"Введіть тривалість збереження у памʼяті (наприклад, 5m, 10m, 1h)\"\n            },\n            \"temperature\": {\n                \"label\": \"Температура\",\n                \"placeholder\": \"Введіть значення температури (наприклад, 0.7, 1.0)\"\n            },\n            \"numCtx\": {\n                \"label\": \"Розмір контекстного вікна (num_ctx)\",\n                \"placeholder\": \"Введіть значення розміру контекстного вікна (за замовчуванням: 2048)\"\n            },\n            \"numPredict\": {\n                \"label\": \"Максимальна кількість токенів\",\n                \"placeholder\": \"Введіть максимальну кількість токенів (наприклад, 2048, 4096)\"\n            },\n            \"seed\": {\n                \"label\": \"Зерно\",\n                \"placeholder\": \"Введіть значення зерна (наприклад, 1234)\",\n                \"help\": \"Повторюваність виводу моделі\"\n            },\n            \"topK\": {\n                \"label\": \"Top K\",\n                \"placeholder\": \"Введіть значення для Верхніх K (наприклад, 40, 100)\"\n            },\n            \"topP\": {\n                \"label\": \"Top P\",\n                \"placeholder\": \"Введіть значення для Верхнього P (наприклад, 0.9, 0.95)\"\n            },\n            \"numGpu\": {\n                \"label\": \"Кількість GPU\",\n                \"placeholder\": \"Введіть кількість шарів для відправки на GPU\"\n            },\n            \"systemPrompt\": {\n                \"label\": \"Тимчасовий системний запит\",\n                \"placeholder\": \"Введіть системний запит\",\n                \"help\": \"Швидкий спосіб встановити системний запит для поточного чату, який замінить вибраний системний запит, якщо він існує.\"\n            }\n        },\n        \"advanced\": \"Додаткові налаштування моделі\"\n    },\n    \"copilot\": {\n        \"summary\": \"Підсумувати\",\n        \"explain\": \"Пояснити\",\n        \"rephrase\": \"Перефразувати\",\n        \"translate\": \"Перекласти\",\n        \"custom\": \"Власне\"\n    },\n    \"citations\": \"Цитати\",\n    \"segmented\": {\n        \"ollama\": \"Моделі Ollama\",\n        \"custom\": \"Власні моделі\"\n    },\n    \"downloadCode\": \"Завантажити код\",\n    \"date\": {\n        \"pinned\": \"Прикріплено\",\n        \"today\": \"Сьогодні\",\n        \"yesterday\": \"Вчора\",\n        \"last7Days\": \"Останні 7 днів\",\n        \"older\": \"Старіше\"\n    },\n    \"range\": {\n        \"deleteConfirm\": {\n            \"pinned\": \"Ви впевнені, що хочете видалити всі прикріплені повідомлення?\",\n            \"today\": \"Ви впевнені, що хочете видалити всі повідомлення за сьогодні?\",\n            \"yesterday\": \"Ви впевнені, що хочете видалити всі повідомлення за вчора?\",\n            \"last7Days\": \"Ви впевнені, що хочете видалити всі повідомлення за останні 7 днів?\",\n            \"older\": \"Ви впевнені, що хочете видалити всі старіші повідомлення?\"\n        },\n        \"tooltip\": {\n            \"pinned\": \"Видалити всі прикріплені повідомлення\",\n            \"today\": \"Видалити всі повідомлення за сьогодні\",\n            \"yesterday\": \"Видалити всі повідомлення за вчора\",\n            \"last7Days\": \"Видалити всі повідомлення за останні 7 днів\",\n            \"older\": \"Видалити всі старіші повідомлення\"\n        }\n    },\n    \"pin\": \"Прикріпити\",\n    \"unpin\": \"Відкріпити\",\n    \"generationInfo\": \"Інформація про генерацію\",\n    \"sidebarChat\": \"Бічний чат\",\n    \"reasoning\": {\n        \"thinking\": \"Думаю....\",\n        \"thought\": \"Думав протягом {{time}}\"\n    },\n    \"embeddingGen\": \"Створюю вставки, це може зайняти деякий час\",\n    \"semanticSearch\": \"Виконую семантичний пошук\",\n    \"downloading\": \"Завантаження\",\n    \"cancelPullingModel\": {\n        \"confirm\": \"Ви впевнені, що хочете скасувати завантаження? Це зупинить процес завантаження. Згідно з документацією Ollama, ви можете відновити завантаження з того місця, де зупинилися.\"\n    }\n}"
  },
  {
    "path": "src/assets/locale/uk/knowledge.json",
    "content": "{\n    \"addBtn\": \"Додати нове знання\",\n    \"columns\": {\n        \"title\": \"Назва\",\n        \"status\": \"Статус\",\n        \"embeddings\": \"Модель вкладень\",\n        \"createdAt\": \"Створено в\",\n        \"action\": \"Дії\"\n    },\n    \"expandedColumns\": {\n        \"name\": \"Імʼя\"\n    },\n    \"confirm\": {\n        \"delete\": \"Ви впевнені, що хочете видалити це знання?\"\n    },\n    \"deleteSuccess\": \"Знання успішно видалено\",\n    \"status\": {\n        \"pending\": \"Очікування\",\n        \"finished\": \"Завершено\",\n        \"processing\": \"Обробка\",\n        \"failed\": \"Не вдалося\"\n    },\n    \"addKnowledge\": \"Додати знання\",\n    \"updateKnowledge\": \"Оновити знання\",\n    \"form\": {\n        \"title\": {\n            \"label\": \"Назва знань\",\n            \"placeholder\": \"Введіть назву знань\",\n            \"required\": \"Назва знань обовʼязкова\"\n        },\n        \"uploadFile\": {\n            \"label\": \"Завантажити файл\",\n            \"uploadText\": \"Перетягніть файл сюди або натисніть для завантаження\",\n            \"uploadHint\": \"Підтримуються типи файлів: .pdf, .csv, .txt, .md, .docx\",\n            \"required\": \"Файл обовʼязковий\"\n        },\n        \"submit\": \"Надіслати\",\n        \"success\": \"Знання успішно додано\"\n    },\n    \"noEmbeddingModel\": \"Будь ласка, додайте модель вкладень з сторінки налаштувань RAG спочатку\"\n}\n"
  },
  {
    "path": "src/assets/locale/uk/openai.json",
    "content": "{\n    \"settings\": \"Сумісний з OpenAI API\",\n    \"heading\": \"Сумісний з OpenAI API\",\n    \"subheading\": \"Тут керування та налаштування ваших постачальників OpenAI API.\",\n    \"addBtn\": \"Додати постачальника\",\n    \"table\": {\n        \"name\": \"Назва постачальника\",\n        \"baseUrl\": \"Базовий URL\",\n        \"actions\": \"Дії\"\n    },\n    \"modal\": {\n        \"titleAdd\": \"Додати нового постачальника\",\n        \"name\": {\n            \"label\": \"Назва постачальника\",\n            \"required\": \"Назва постачальника обовʼязкова.\",\n            \"placeholder\": \"Введіть назву постачальника\"\n        },\n        \"baseUrl\": {\n            \"label\": \"Базовий URL\",\n            \"help\": \"Базовий URL постачальника OpenAI API. Наприклад, (http://localhost:1234/v1)\",\n            \"required\": \"Базовий URL обовʼязковий.\",\n            \"placeholder\": \"Введіть базовий URL\"\n        },\n        \"apiKey\": {\n            \"label\": \"Ключ API\",\n            \"required\": \"Ключ API обовʼязковий.\",\n            \"placeholder\": \"Введіть ключ API\"\n        },\n        \"submit\": \"Зберегти\",\n        \"update\": \"Оновити\",\n        \"deleteConfirm\": \"Ви впевнені, що хочете видалити цього постачальника?\",\n        \"model\": {\n            \"title\": \"Список моделей\",\n            \"subheading\": \"Виберіть чат-моделі, які ви хочете використовувати з цим постачальником.\",\n            \"success\": \"Моделі успішно додано.\"\n        },\n        \"tipLMStudio\": \"Page Assist автоматично отримає моделі, завантажені в LM Studio. Вам не потрібно додавати їх вручну.\"\n    },\n    \"addSuccess\": \"Постачальника додано успішно.\",\n    \"deleteSuccess\": \"Постачальника видалено успішно.\",\n    \"updateSuccess\": \"Постачальника оновлено успішно.\",\n    \"delete\": \"Видалити\",\n    \"edit\": \"Редагувати\",\n    \"newModel\": \"Додати моделі до постачальника\",\n    \"noNewModel\": \"Для LMStudio, Ollama, Llamafile ми отримуємо моделі динамічно. Ручного додавання не потрібно.\",\n    \"searchModel\": \"Пошук моделі\",\n    \"selectAll\": \"Вибрати все\",\n    \"save\": \"Зберегти\",\n    \"saving\": \"Збереження...\",\n    \"manageModels\": {\n        \"columns\": {\n            \"name\": \"Назва моделі\",\n            \"model_type\": \"Тип моделі\",\n            \"model_id\": \"Ідентифікатор моделі\",\n            \"provider\": \"Назва постачальника\",\n            \"actions\": \"Дії\",\n            \"nickname\": \"Псевдонім моделі\"\n        },\n        \"tooltip\": {\n            \"delete\": \"Видалити\"\n        },\n        \"confirm\": {\n            \"delete\": \"Ви впевнені, що хочете видалити цю модель?\"\n        },\n        \"modal\": {\n            \"title\": \"Додати власну модель\",\n            \"form\": {\n                \"name\": {\n                    \"label\": \"Ідентифікатор моделі\",\n                    \"placeholder\": \"llama3.2\",\n                    \"required\": \"Ідентифікатор моделі обовʼязковий.\"\n                },\n                \"provider\": {\n                    \"label\": \"Постачальник\",\n                    \"placeholder\": \"Виберіть постачальника\",\n                    \"required\": \"Постачальник обовʼязковий.\"\n                },\n                \"type\": {\n                    \"label\": \"Тип моделі\"\n                }\n            }\n        }\n    },\n    \"noModelFound\": \"Не знайдено моделей. Переконайтеся, що ви додали правильного постачальника з базовим URL та ключем API.\",\n    \"radio\": {\n        \"chat\": \"Модель чату\",\n        \"embedding\": \"Модель вкладень\",\n        \"chatInfo\": \"використовується для завершення чату та генерації розмови\",\n        \"embeddingInfo\": \"використовується для RAG та інших повʼязаних завдань семантичного пошуку.\"\n    },\n    \"nicknameModal\": {\n        \"title\": \"Додати / Редагувати псевдонім моделі\",\n        \"form\": {\n            \"modelName\": {\n                \"label\": \"Назва моделі\",\n                \"placeholder\": \"Введіть назву моделі\",\n                \"required\": \"Назва моделі обов'язкова.\"\n            },\n            \"modelAvatar\": {\n                \"label\": \"Аватар моделі\",\n                \"placeholder\": \"Введіть аватар моделі\",\n                \"help\": \"Будь ласка, введіть URL аватара моделі. Це зображення буде відображатися у вікні чату.\"\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/assets/locale/uk/option.json",
    "content": "{\n    \"newChat\": \"Новий чат\",\n    \"selectAPrompt\": \"Виберіть запит\",\n    \"githubRepository\": \"Репозиторій GitHub\",\n    \"settings\": \"Налаштування\",\n    \"sidebarTitle\": \"Історія чату\",\n    \"error\": \"Збій\",\n    \"somethingWentWrong\": \"Щось пішло не так\",\n    \"validationSelectModel\": \"Будь ласка, виберіть модель для продовження\",\n    \"deleteHistoryConfirmation\": \"Ви впевнені, що хочете видалити цю історію?\",\n    \"editHistoryTitle\": \"Введіть нову назву\",\n    \"temporaryChat\": \"Тимчасовий чат\",\n    \"more\": {\n        \"copy\": {\n            \"group\": \"Копіювати\",\n            \"asText\": \"Копіювати як текст\",\n            \"asMarkdown\": \"Копіювати як Markdown\",\n            \"success\": \"Скопійовано в буфер обміну!\"\n        },\n        \"download\": {\n            \"group\": \"Завантажити\",\n            \"text\": \"Текстовий файл (.txt)\",\n            \"markdown\": \"Markdown (.md)\",\n            \"json\": \"JSON файл (.json)\"\n        },\n        \"share\": \"Поділитися\"\n    }\n}"
  },
  {
    "path": "src/assets/locale/uk/playground.json",
    "content": "{\n    \"ollamaState\": {\n        \"searching\": \"Пошук вашого Ollama 🦙\",\n        \"running\": \"Ollama працює 🦙\",\n        \"notRunning\": \"Неможливо підключитися до Ollama 🦙\",\n        \"connectionError\": \"Здається, у вас виникла проблема з підключенням. Будь ласка, зверніться до цієї документації<anchor> для усунення несправностей.</anchor>\"\n    },\n    \"formError\": {\n        \"noModel\": \"Виберіть модель\",\n        \"noEmbeddingModel\": \"Налаштуйте модель вкладень на сторінці Налаштування > RAG\"\n    },\n    \"form\": {\n        \"textarea\": {\n            \"placeholder\": \"Введіть повідомлення...\"\n        },\n        \"webSearch\": {\n            \"on\": \"Увімкнено\",\n            \"off\": \"Вимкнено\"\n        }\n    },\n    \"tooltip\": {\n        \"searchInternet\": \"Пошук в Інтернеті\",\n        \"speechToText\": \"Голос у текст\",\n        \"uploadImage\": \"Завантажити зображення\",\n        \"stopStreaming\": \"Зупинити трансляцію\",\n        \"knowledge\": \"Знання\",\n        \"clearContext\": \"Очистити контекст\"\n    },\n    \"sendWhenEnter\": \"Надсилати при натисканні Enter\",\n    \"welcome\": \"Вітаю! Як я можу допомогти вам сьогодні?\",\n    \"useOCR\": \"Витягти текст із зображення (OCR)\"\n}"
  },
  {
    "path": "src/assets/locale/uk/settings.json",
    "content": "{\n  \"generalSettings\": {\n    \"title\": \"Загальні налаштування\",\n    \"settings\": {\n      \"heading\": \"Налаштування веб-інтерфейсу\",\n      \"speechRecognitionLang\": {\n        \"label\": \"Мова для розпізнавання голосу\",\n        \"placeholder\": \"Виберіть мову\"\n      },\n      \"language\": {\n        \"label\": \"Мова інтерфейсу\",\n        \"placeholder\": \"Виберіть мову\"\n      },\n      \"darkMode\": {\n        \"label\": \"Змінити тему\",\n        \"options\": {\n          \"light\": \"Світла\",\n          \"dark\": \"Темна\"\n        }\n      },\n      \"copilotResumeLastChat\": {\n        \"label\": \"Поновити останню розмову при відкритті бічної панелі (Copilot)\"\n      },\n      \"turnOnChatWithWebsite\": {\n        \"label\": \"Увімкнути чат з веб-сайтом за замовчуванням (Copilot)\"\n      },\n      \"webUIResumeLastChat\": {\n        \"label\": \"Поновити останню розмову при відкритті веб-інтерфейсу\"\n      },\n      \"hideCurrentChatModelSettings\": {\n        \"label\": \"Приховати налаштування поточної моделі чату\"\n      },\n      \"restoreLastChatModel\": {\n        \"label\": \"Відновити останню використану модель для попередніх чатів\"\n      },\n      \"sendNotificationAfterIndexing\": {\n        \"label\": \"Надсилати сповіщення після завершення обробки бази знань\"\n      },\n      \"generateTitle\": {\n        \"label\": \"Створювати заголовок за допомогою AI\"\n      },\n      \"ollamaStatus\": {\n        \"label\": \"Увімкнути або вимкнути перевірку стану з'єднання Ollama\"\n      },\n      \"wideMode\": {\n        \"label\": \"Увімкнути широкоекранний режим\"\n      },\n      \"openReasoning\": {\n        \"label\": \"Розгорнути міркування за замовчуванням\"\n      },\n      \"userChatBubble\": {\n        \"label\": \"Використовувати бульбашку чату для повідомлень користувача\"\n      },\n      \"autoCopyResponseToClipboard\": {\n        \"label\": \"Автоматично копіювати відповідь до буфера обміну\"\n      },\n      \"useMarkdownForUserMessage\": {\n        \"label\": \"Увімкнути форматування Markdown для повідомлень користувача\"\n      },\n      \"copyAsFormattedText\": {\n        \"label\": \"Копіювати як форматований текст\"\n      },\n      \"tabMentionsEnabled\": {\n        \"label\": \"Увімкнути згадування вкладок (@tab)\"\n      },\n      \"pasteLargeTextAsFile\": {\n        \"label\": \"Вставити великий текст як файл\"\n      },\n      \"sidepanelTemporaryChat\": {\n        \"label\": \"Увімкнути тимчасовий чат у бічній панелі за замовчуванням\"\n      }\n    },\n    \"sidepanelRag\": {\n      \"heading\": \"Налаштування пошуку\",\n      \"ragEnabled\": {\n        \"label\": \"Увімкнути вбудовування та пошук\"\n      },\n      \"maxWebsiteContext\": {\n        \"label\": \"Максимальний розмір вмісту для режиму повного контексту\",\n        \"placeholder\": \"Розмір вмісту (за замовчуванням 4028)\"\n      }\n    },\n    \"webSearch\": {\n      \"heading\": \"Управління веб-пошуком\",\n      \"searchMode\": {\n        \"label\": \"Виконувати простий пошук в Інтернеті\"\n      },\n      \"provider\": {\n        \"label\": \"Пошукова система\",\n        \"placeholder\": \"Виберіть пошукову систему\"\n      },\n      \"totalSearchResults\": {\n        \"label\": \"Загальна кількість результатів пошуку\",\n        \"placeholder\": \"Введіть загальну кількість результатів пошуку\"\n      },\n      \"visitSpecificWebsite\": {\n        \"label\": \"Відвідати веб-сайт, згаданий у повідомленні\"\n      },\n      \"searxng\": {\n        \"url\": {\n          \"label\": \"SearXNG URL-адреса\"\n        }\n      },\n      \"braveApi\": {\n        \"label\": \"Ключ API Brave\",\n        \"placeholder\": \"Введіть ваш ключ API Brave\"\n      },\n      \"searchOnByDefault\": {\n        \"label\": \"Пошук в Інтернеті увімкнено за замовчуванням\"\n      }\n    },\n    \"system\": {\n      \"heading\": \"Системні налаштування\",\n      \"storageSyncEnabled\": {\n        \"label\": \"Увімкнути синхронізацію сховища браузера (синхронізувати налаштування між пристроями)\"\n      },\n      \"deleteChatHistory\": {\n        \"label\": \"Скидання системи\",\n        \"button\": \"Скинути все\",\n        \"confirm\": \"Ви впевнені, що хочете виконати скидання системи? Це призведе до видалення всіх даних, і цю дію неможливо буде скасувати.\"\n      },\n      \"export\": {\n        \"label\": \"Експортувати всі дані (історію чату, базу знань, підказки та налаштування)\",\n        \"button\": \"Експортувати дані\",\n        \"success\": \"Експорт успішний\"\n      },\n      \"import\": {\n        \"label\": \"Імпортувати всі дані (історію чату, базу знань, підказки та налаштування)\",\n        \"button\": \"Імпортувати дані\",\n        \"success\": \"Імпорт успішний\",\n        \"error\": \"Помилка імпорту\"\n      }\n    },\n    \"tts\": {\n      \"heading\": \"Налаштування Текст-у-Голос\",\n      \"ttsEnabled\": {\n        \"label\": \"Увімкнути Текст-у-Голос\"\n      },\n      \"ttsAutoPlay\": {\n        \"label\": \"Автоматично відтворювати голосову відповідь після завершення\"\n      },\n      \"ttsProvider\": {\n        \"label\": \"Поставник Текст-у-Голос\",\n        \"placeholder\": \"Виберіть постачальника\"\n      },\n      \"ttsVoice\": {\n        \"label\": \"Голос Текст-у-Голос\",\n        \"placeholder\": \"Виберіть голос\"\n      },\n      \"ssmlEnabled\": {\n        \"label\": \"Ввімкнути SSML (Мова Розмітки для Синтезу Голосу)\"\n      },\n      \"removeReasoningTagTTS\": {\n        \"label\": \"Видалити тег міркування з TTS\"\n      }\n    },\n    \"stt\": {\n      \"heading\": \"Налаштування Голос-у-Текст\",\n      \"autoStopTimeout\": {\n        \"label\": \"Час автоматичної зупинки (мс)\",\n        \"placeholder\": \"Введіть час автоматичної зупинки в мілісекундах\"\n      },\n      \"autoSubmitVoiceMessage\": {\n        \"label\": \"Автоматичне надсилання голосового повідомлення\"\n      }\n    }\n  },\n  \"manageModels\": {\n    \"title\": \"Управління моделями\",\n    \"addBtn\": \"Додати нову модель\",\n    \"columns\": {\n      \"name\": \"Назва\",\n      \"digest\": \"Хеш\",\n      \"modifiedAt\": \"Оновлено\",\n      \"size\": \"Розмір\",\n      \"actions\": \"Дії\"\n    },\n    \"expandedColumns\": {\n      \"parentModel\": \"Батьківська модель\",\n      \"format\": \"Формат\",\n      \"family\": \"Сімʼя\",\n      \"parameterSize\": \"Розмір параметрів\",\n      \"quantizationLevel\": \"Рівень квантування\"\n    },\n    \"tooltip\": {\n      \"delete\": \"Видалити модель\",\n      \"repull\": \"Повторно завантажити модель\"\n    },\n    \"confirm\": {\n      \"delete\": \"Ви впевнені, що хочете видалити цю модель?\",\n      \"repull\": \"Ви впевнені, що хочете повторно завантажити цю модель?\"\n    },\n    \"modal\": {\n      \"title\": \"Додати нову модель\",\n      \"placeholder\": \"Введіть назву моделі\",\n      \"pull\": \"Завантажити модель\"\n    },\n    \"notification\": {\n      \"pullModel\": \"Завантаження моделі\",\n      \"pullModelDescription\": \"Завантажується модель {{modelName}}. Для отримання додаткової інформації перевірте іконку розширення.\",\n      \"success\": \"Успіх\",\n      \"error\": \"Помилка\",\n      \"successDescription\": \"Модель успішно завантажена\",\n      \"successDeleteDescription\": \"Модель успішно видалена\",\n      \"someError\": \"Щось пішло не так. Будь ласка, спробуйте пізніше.\"\n    }\n  },\n  \"managePrompts\": {\n    \"title\": \"Управління запитами\",\n    \"addBtn\": \"Додати новий запит\",\n    \"option1\": \"Нормальний\",\n    \"option2\": \"RAG\",\n    \"questionPrompt\": \"Питання-запит\",\n    \"segmented\": {\n      \"custom\": \"Власні запити\",\n      \"copilot\": \"Запити Copilot\"\n    },\n    \"columns\": {\n      \"title\": \"Назва\",\n      \"prompt\": \"Запит\",\n      \"type\": \"Тип запиту\",\n      \"actions\": \"Дії\"\n    },\n    \"systemPrompt\": \"Системний запит\",\n    \"quickPrompt\": \"Швидкий запит\",\n    \"tooltip\": {\n      \"delete\": \"Видалити запит\",\n      \"edit\": \"Редагувати запит\"\n    },\n    \"confirm\": {\n      \"delete\": \"Ви впевнені, що хочете видалити цей запит? Його не вдасться відновити.\"\n    },\n    \"modal\": {\n      \"addTitle\": \"Додати новий запит\",\n      \"editTitle\": \"Редагувати запит\"\n    },\n    \"form\": {\n      \"title\": {\n        \"label\": \"Назва\",\n        \"placeholder\": \"Мій чудовий запит\",\n        \"required\": \"Будь ласка, введіть назву\"\n      },\n      \"prompt\": {\n        \"label\": \"Запит\",\n        \"placeholder\": \"Введіть запит\",\n        \"required\": \"Будь ласка, введіть запит\",\n        \"help\": \"Ви можете використовувати {key} як змінну у вашому запиті.\",\n        \"missingTextPlaceholder\": \"Змінна {text} відсутня в запиті. Додайте її, будь ласка.\"\n      },\n      \"isSystem\": {\n        \"label\": \"Це системний запит\"\n      },\n      \"btnSave\": {\n        \"saving\": \"Додавання запиту...\",\n        \"save\": \"Додати запит\"\n      },\n      \"btnEdit\": {\n        \"saving\": \"Оновлення запиту...\",\n        \"save\": \"Оновити запит\"\n      }\n    },\n    \"notification\": {\n      \"addSuccess\": \"Запит додано\",\n      \"addSuccessDesc\": \"Запит успішно додано\",\n      \"error\": \"Збій\",\n      \"someError\": \"Щось пішло не так. Спробуйте знову пізніше\",\n      \"updatedSuccess\": \"Запит оновлено\",\n      \"updatedSuccessDesc\": \"Запит успішно оновлено\",\n      \"deletedSuccess\": \"Запит видалено\",\n      \"deletedSuccessDesc\": \"Запит успішно видалено\"\n    }\n  },\n  \"manageShare\": {\n    \"title\": \"Управління спільним доступом\",\n    \"heading\": \"Налаштування URL спільного доступу до сторінки\",\n    \"form\": {\n      \"url\": {\n        \"label\": \"URL спільного доступу до сторінки\",\n        \"placeholder\": \"Введіть URL спільного доступу до сторінки\",\n        \"required\": \"Будь ласка, введіть ваш URL спільного доступу!\",\n        \"help\": \"З міркувань конфіденційності, ви можете самостійно обробляти спільний доступ до сторінки через цей URL. <anchor>Дізнатися більше</anchor>.\"\n      }\n    },\n    \"webshare\": {\n      \"heading\": \"Спільний доступ через веб\",\n      \"columns\": {\n        \"title\": \"Назва\",\n        \"url\": \"URL\",\n        \"actions\": \"Дії\"\n      },\n      \"tooltip\": {\n        \"delete\": \"Видалити спільний доступ\"\n      },\n      \"confirm\": {\n        \"delete\": \"Ви впевнені, що хочете видалити цей спільний доступ? Його не вдасться відновити.\"\n      },\n      \"label\": \"Управління спільним доступом до сторінки\",\n      \"description\": \"Включіть або вимкніть функцію спільного доступу до сторінки\"\n    },\n    \"notification\": {\n      \"pageShareSuccess\": \"URL спільного доступу до сторінки оновлено успішно\",\n      \"someError\": \"Щось пішло не так. Будь ласка, спробуйте пізніше\",\n      \"webShareDeleteSuccess\": \"Спільний доступ видалено успішно\"\n    }\n  },\n  \"ollamaSettings\": {\n    \"title\": \"Налаштування Ollama\",\n    \"heading\": \"Налаштуйте Ollama\",\n    \"settings\": {\n      \"ollamaUrl\": {\n        \"label\": \"URL до Ollama\",\n        \"placeholder\": \"Введіть URL до Ollama\"\n      },\n      \"globalEnable\": {\n        \"label\": \"Увімкнути або вимкнути інтеграцію Ollama глобально\",\n        \"warning\": \"При вимкненні глобальної інтеграції Ollama, Page Assist не буде отримувати моделі з Ollama. Ви все ще можете додати екземпляр Ollama з розділу <anchor>API, сумісного з OpenAI</anchor>, який працюватиме нормально.\"\n      },\n      \"advanced\": {\n        \"label\": \"Розширене налаштування URL Ollama\",\n        \"urlRewriteEnabled\": {\n          \"label\": \"Увімкнути або вимкнути налаштування власного URL походження запиту\"\n        },\n        \"rewriteUrl\": {\n          \"label\": \"Власний URL походження запиту\",\n          \"placeholder\": \"Введіть власний URL походження\"\n        },\n        \"autoCORSFix\": {\n          \"label\": \"Увімкнути або вимкнути автоматичне виправлення CORS для Ollama\"\n        },\n        \"headers\": {\n          \"label\": \"Власні заголовки\",\n          \"add\": \"Додати заголовок\",\n          \"key\": {\n            \"label\": \"Ключ заголовка\",\n            \"placeholder\": \"Authorization\"\n          },\n          \"value\": {\n            \"label\": \"Значення заголовка\",\n            \"placeholder\": \"Bearer token\"\n          }\n        },\n        \"help\": \"Якщо у вас виникають проблеми з підключенням до Ollama на Page Assist, ви можете налаштувати власний URL походження запиту. Щоб дізнатися більше про це налаштування, <anchor>натисніть тут</anchor>.\"\n      }\n    }\n  },\n  \"manageSearch\": {\n    \"title\": \"Управління пошуком в Інтернеті\",\n    \"heading\": \"Налаштуйте пошук в Інтернеті\"\n  },\n  \"about\": {\n    \"title\": \"Про додаток\",\n    \"heading\": \"Інформація про цей додаток\",\n    \"chromeVersion\": \"Версія Page Assist\",\n    \"ollamaVersion\": \"Версія Ollama\",\n    \"support\": \"Ви можете підтримати проект Page Assist, зробивши пожертву або ставши спонсором через наступні платформи:\",\n    \"koFi\": \"Підтримка на Ko-fi\",\n    \"githubSponsor\": \"Стати спонсором на GitHub\",\n    \"githubRepo\": \"Репозиторій GitHub\"\n  },\n  \"manageKnowledge\": {\n    \"title\": \"Управління знаннями\",\n    \"heading\": \"Налаштування бази знань\"\n  },\n  \"rag\": {\n    \"title\": \"Налаштування Pipeline\",\n    \"ragSettings\": {\n      \"label\": \"Налаштування RAG\",\n      \"model\": {\n        \"label\": \"Модель вбудованих даних\",\n        \"required\": \"Будь ласка, виберіть модель\",\n        \"help\": \"Рекомендується використовувати моделі вбудованих даних, такі як `nomic-embed-text`.\",\n        \"placeholder\": \"Вибрати модель\"\n      },\n      \"chunkSize\": {\n        \"label\": \"Розмір шматка\",\n        \"placeholder\": \"Ввести розмір шматка\",\n        \"required\": \"Будь ласка, введіть розмір шматка\"\n      },\n      \"chunkOverlap\": {\n        \"label\": \"Перекриття шматків\",\n        \"placeholder\": \"Ввести перекриття шматків\",\n        \"required\": \"Будь ласка, введіть перекриття шматків\"\n      },\n      \"totalFilePerKB\": {\n        \"label\": \"Типовий ліміт кількості завантажень файлів до бази знань\",\n        \"placeholder\": \"Введіть типовий ліміт (напр. 10)\",\n        \"required\": \"Будь ласка, введіть типовий ліміт кількості файлів для завантаження\"\n      },\n      \"noOfRetrievedDocs\": {\n        \"label\": \"Кількість отриманих документів\",\n        \"placeholder\": \"Ввести кількість отриманих документів\",\n        \"required\": \"Будь ласка, введіть кількість документів\"\n      },\n      \"splittingSeparator\": {\n        \"label\": \"Роздільник\",\n        \"placeholder\": \"Введіть роздільник (напр., \\\\n\\\\n)\",\n        \"required\": \"Будь ласка, введіть роздільник\"\n      },\n      \"splittingStrategy\": {\n        \"label\": \"Розділювач тексту\"\n      }\n    },\n    \"prompt\": {\n      \"label\": \"Налаштування запиту з RAG\",\n      \"option1\": \"Нормальний\",\n      \"option2\": \"З Веб-пошуком\",\n      \"alert\": \"Налаштування системного запиту тут застаріло. Використовуйте розділ «Управління запитами», щоб додавати або редагувати запити. Цей розділ буде видалено в майбутньому оновленні\",\n      \"systemPrompt\": \"Системний запит\",\n      \"systemPromptPlaceholder\": \"Ввести системний запит\",\n      \"webSearchPrompt\": \"Запит веб-пошуку\",\n      \"webSearchPromptHelp\": \"Не видаляйте `{search_results}` із запиту.\",\n      \"webSearchPromptError\": \"Будь ласка, введіть запит для веб-пошуку\",\n      \"webSearchPromptPlaceholder\": \"Ввести запит для веб-пошуку\",\n      \"webSearchFollowUpPrompt\": \"Запит для подальшого пошуку в мережі\",\n      \"webSearchFollowUpPromptHelp\": \"Не видаляйте `{chat_history}` і `{question}` із запиту.\",\n      \"webSearchFollowUpPromptError\": \"Будь ласка, введіть запит для подальшого пошуку в мережі!\",\n      \"webSearchFollowUpPromptPlaceholder\": \"Ваш запит для подальшого пошуку в мережі\"\n    }\n  },\n  \"chromeAiSettings\": {\n    \"title\": \"Налаштування Chrome AI\"\n  }\n}\n"
  },
  {
    "path": "src/assets/locale/uk/sidepanel.json",
    "content": "{\n    \"tooltip\": {\n        \"embed\": \"Може знадобитися кілька хвилин для вкладення сторінки у базу. Будь ласка, зачекайте...\",\n        \"clear\": \"Стерти історію чату\",\n        \"history\": \"Історія чату\",\n        \"openwebui\": \"Відкрити веб-інтерфейс\"\n    }\n}"
  },
  {
    "path": "src/assets/locale/zh/chrome.json",
    "content": "{\n  \"heading\": \"配置Chrome人工智能\",\n  \"status\": {\n      \"label\": \"在页面辅助功能中启用或禁用Chrome人工智能支持\"\n  },\n  \"error\": {\n      \"browser_not_supported\": \"此版本的Chrome不受Gemini Nano模型支持。请更新到127版本或更高版本\",\n      \"ai_not_supported\": \"设置chrome://flags/#prompt-api-for-gemini-nano未启用。请启用它。\",\n      \"ai_not_ready\": \"Gemini Nano尚未准备就绪；您需要再次检查Chrome设置。\",\n      \"internal_error\": \"发生内部错误。请稍后重试。\"\n  },\n  \"errorDescription\": \"要使用Chrome人工智能，您需要Chrome138或更高版本。请按照以下步骤操作：\\n\\n1. 前往`chrome://flags/#prompt-api-for-gemini-nano`并启用\\\"Gemini Nano的提示API\\\"。\\n2. 重启Chrome以应用该标志。\\n3. 返回此页面并单击\\\"下载模型\\\" - 这将首次下载4GB的模型。\\n4. 下载完成后，可以通过页面辅助功能启用Gemini Nano。\",\n  \"downloadModel\": \"下载模型\",\n  \"modelDownloadWarning\": \"这将下载一个模型，下载大小约为1.5 GB到2.4 GB。请确保您有足够的磁盘空间。\"\n}"
  },
  {
    "path": "src/assets/locale/zh/common.json",
    "content": "{\n    \"pageAssist\": \"Page Assist\",\n    \"selectAModel\": \"选择一个模型\",\n    \"save\": \"保存\",\n    \"saved\": \"已保存\",\n    \"cancel\": \"取消\",\n    \"retry\": \"重试\",\n    \"share\": {\n        \"tooltip\": {\n            \"share\": \"分享\"\n        },\n        \"modal\": {\n            \"title\": \"为聊天创建分享链接\"\n        },\n        \"form\": {\n            \"defaultValue\": {\n                \"name\": \"匿名\",\n                \"title\": \"未命名聊天\"\n            },\n            \"title\": {\n                \"label\": \"聊天标题\",\n                \"placeholder\": \"输入聊天标题\",\n                \"required\": \"聊天标题是必填的\"\n            },\n            \"name\": {\n                \"label\": \"您的名字\",\n                \"placeholder\": \"输入您的名字\",\n                \"required\": \"您的名字是必填的\"\n            },\n            \"btn\": {\n                \"save\": \"生成链接\",\n                \"saving\": \"正在生成链接...\"\n            }\n        },\n        \"notification\": {\n            \"successGenerate\": \"链接已复制到剪贴板\",\n            \"failGenerate\": \"生成链接失败\"\n        }\n    },\n    \"copyToClipboard\": \"复制到剪贴板\",\n    \"webSearch\": \"搜索网络\",\n    \"regenerate\": \"重新生成\",\n    \"continue\": \"继续回答\",\n    \"edit\": \"编辑\",\n    \"delete\": \"删除\",\n    \"saveAndSubmit\": \"保存 & 提交\",\n    \"editMessage\": {\n        \"placeholder\": \"输入一条消息...\"\n    },\n    \"submit\": \"提交\",\n    \"noData\": \"无数据\",\n    \"noHistory\": \"无聊天记录\",\n    \"chatWithCurrentPage\": \"与当前页面聊天\",\n    \"beta\": \"Beta\",\n    \"tts\": \"朗读\",\n    \"currentChatModelSettings\": \"当前聊天模型设置\",\n    \"modelSettings\": {\n        \"label\": \"模型设置\",\n        \"description\": \"全局设置所有聊天的模型选项\",\n        \"form\": {\n            \"keepAlive\": {\n                \"label\": \"保留时间\",\n                \"help\": \"控制请求后模型在内存中保持的时间（默认：5分钟）\",\n                \"placeholder\": \"输入保留时间（例如：5分、10分、1小时）\"\n            },\n            \"temperature\": {\n                \"label\": \"温度\",\n                \"placeholder\": \"输入温度值（例如：0.7、1.0）\"\n            },\n            \"numCtx\": {\n                \"label\": \"上下文窗口大小 (num_ctx)\",\n                \"placeholder\": \"输入上下文窗口大小（默认：2048）\"\n            },\n            \"numPredict\": {\n                \"label\": \"最大令牌数 (num_predict)\",\n                \"placeholder\": \"输入最大令牌数（例如：2048、4096）\"\n            },\n            \"seed\": {\n                \"label\": \"随机种子\",\n                \"placeholder\": \"输入随机种子值（例如：1234）\",\n                \"help\": \"模型输出的可重复性\"\n            },\n            \"topK\": {\n                \"label\": \"Top K\",\n                \"placeholder\": \"输入Top K值（例如：40、100）\"\n            },\n            \"topP\": {\n                \"label\": \"Top P\",\n                \"placeholder\": \"输入Top P值（例如：0.9、0.95）\"\n            },\n            \"useMMap\": {\n                \"label\": \"使用 mmap\"\n            },\n            \"tfsZ\": {\n                \"label\": \"TFS-Z\",\n                \"placeholder\": \"例如：1.0, 1.1\"\n            },\n            \"numKeep\": {\n                \"label\": \"Num Keep\",\n                \"placeholder\": \"例如：256, 512\"\n            },\n            \"numThread\": {\n                \"label\": \"Num Thread\",\n                \"placeholder\": \"例如：8, 16\"\n            },\n            \"useMlock\": {\n                \"label\": \"使用 mlock\"\n            },\n            \"minP\": {\n                \"label\": \"Min P\",\n                \"placeholder\": \"例如：0.05\"\n            },\n            \"repeatPenalty\": {\n                \"label\": \"Repeat Penalty\",\n                \"placeholder\": \"例如：1.1, 1.2\"\n            },\n            \"repeatLastN\": {\n                \"label\": \"Repeat Last N\",\n                \"placeholder\": \"例如：64, 128\"\n            },\n            \"numGpu\": {\n                \"label\": \"Num GPU\",\n                \"placeholder\": \"输入要发送到 GPU 的层数\"\n            },\n            \"systemPrompt\": {\n                \"label\": \"临时系统提示\",\n                \"placeholder\": \"输入系统提示\",\n                \"help\": \"这是一种在当前聊天中快速设置系统提示的方法，如果存在已选择的系统提示，它将覆盖该提示。\"\n            }\n        },\n        \"advanced\": \"更多模型设置\"\n    },\n    \"copilot\": {\n        \"summary\": \"总结\",\n        \"explain\": \"解释\",\n        \"rephrase\": \"重述\",\n        \"translate\": \"翻译\",\n        \"custom\": \"自定义\"\n    },\n    \"citations\": \"引用\",\n    \"segmented\": {\n        \"ollama\": \"Ollama 模型\",\n        \"custom\": \"自定义模型\"\n    },\n    \"downloadCode\": \"下载代码\",\n    \"date\": {\n        \"pinned\": \"已置顶\",\n        \"today\": \"今天\",\n        \"yesterday\": \"昨天\",\n        \"last7Days\": \"最近7天\",\n        \"older\": \"更早\"\n    },\n    \"range\": {\n        \"deleteConfirm\": {\n            \"pinned\": \"确定要删除所有置顶的消息吗？\",\n            \"today\": \"确定要删除今天的所有消息吗？\",\n            \"yesterday\": \"确定要删除昨天的所有消息吗？\",\n            \"last7Days\": \"确定要删除最近7天的所有消息吗？\",\n            \"older\": \"确定要删除所有更早的消息吗？\"\n        },\n        \"tooltip\": {\n            \"pinned\": \"删除所有置顶消息\",\n            \"today\": \"删除今天的所有消息\",\n            \"yesterday\": \"删除昨天的所有消息\",\n            \"last7Days\": \"删除最近7天的所有消息\",\n            \"older\": \"删除所有更早的消息\"\n        }\n    },\n    \"pin\": \"置顶\",\n    \"unpin\": \"取消置顶\",\n    \"generationInfo\": \"生成信息\",\n    \"sidebarChat\": \"侧边栏聊天\",\n    \"reasoning\": {\n        \"thinking\": \"思考中....\",\n        \"thought\": \"思考了 {{time}}\"\n    },\n    \"mermaid\": \"Mermaid\",\n    \"embeddingGen\": \"正在创建嵌入向量，这可能需要一些时间\",\n    \"semanticSearch\": \"正在执行语义搜索\",\n    \"downloading\": \"正在下载\",\n    \"cancelPullingModel\": {\n        \"confirm\": \"确定要取消下载吗？这将停止下载过程。根据 Ollama 文档，您可以从中断处重新开始下载。\"\n    }\n}"
  },
  {
    "path": "src/assets/locale/zh/knowledge.json",
    "content": "{\n    \"addBtn\": \"添加新知识\",\n    \"columns\": {\n        \"title\": \"标题\",\n        \"status\": \"状态\",\n        \"embeddings\": \"嵌入模型\",\n        \"createdAt\": \"创建于\",\n        \"action\": \"操作\"\n    },\n    \"expandedColumns\": {\n        \"name\": \"名称\"\n    },\n    \"confirm\": {\n        \"delete\": \"您确定要删除此知识吗?\"\n    },\n    \"deleteSuccess\": \"知识删除成功\",\n    \"status\": {\n        \"pending\": \"待定\",\n        \"finished\": \"已完成\",\n        \"processing\": \"处理中\",\n        \"failed\": \"失败\"\n    },\n    \"addKnowledge\": \"添加知识\",\n    \"updateKnowledge\": \"更新知识\",\n    \"form\": {\n        \"title\": {\n            \"label\": \"知识标题\",\n            \"placeholder\": \"输入知识标题\",\n            \"required\": \"知识标题是必需的\"\n        },\n        \"uploadFile\": {\n            \"label\": \"上传文件\",\n            \"uploadText\": \"将文件拖放到此处或点击上传\",\n            \"uploadHint\": \"支持的文件类型: .pdf, .csv, .txt, .md\",\n            \"required\": \"文件是必需的\"\n        },\n        \"submit\": \"提交\",\n        \"success\": \"知识添加成功\"\n    },\n    \"noEmbeddingModel\": \"请先从RAG设置页面添加一个嵌入模型\"\n}"
  },
  {
    "path": "src/assets/locale/zh/openai.json",
    "content": "{\n    \"settings\": \"OpenAI 兼容 API\",\n    \"heading\": \"OpenAI 兼容 API\",\n    \"subheading\": \"在此管理和配置您的 OpenAI API 兼容提供商。\",\n    \"addBtn\": \"添加提供商\",\n    \"table\": {\n        \"name\": \"提供商名称\",\n        \"baseUrl\": \"基础 URL\",\n        \"actions\": \"操作\"\n    },\n    \"modal\": {\n        \"titleAdd\": \"添加新提供商\",\n        \"titleEdit\": \"编辑提供商\",\n        \"name\": {\n            \"label\": \"提供商名称\",\n            \"required\": \"提供商名称为必填项。\",\n            \"placeholder\": \"输入提供商名称\"\n        },\n        \"baseUrl\": {\n            \"label\": \"基础 URL\",\n            \"help\": \"OpenAI API 提供商的基础 URL。例如 (http://localhost:1234/v1)\",\n            \"required\": \"基础 URL 为必填项。\",\n            \"placeholder\": \"输入基础 URL\"\n        },\n        \"apiKey\": {\n            \"label\": \"API 密钥\",\n            \"required\": \"API 密钥为必填项。\",\n            \"placeholder\": \"输入 API 密钥\"\n        },\n        \"submit\": \"保存\",\n        \"update\": \"更新\",\n        \"deleteConfirm\": \"您确定要删除此提供商吗？\",\n        \"model\": {\n            \"title\": \"模型列表\",\n            \"subheading\": \"请选择您想要与此提供商一起使用的聊天模型。\",\n            \"success\": \"成功添加新模型。\"\n        },\n        \"tipLMStudio\": \"Page Assist 将自动获取您在 LM Studio 中加载的模型。您无需手动添加它们。\"\n    },\n    \"addSuccess\": \"提供商添加成功。\",\n    \"deleteSuccess\": \"提供商删除成功。\",\n    \"updateSuccess\": \"提供商更新成功。\",\n    \"delete\": \"删除\",\n    \"edit\": \"编辑\",\n    \"newModel\": \"向提供商添加模型\",\n    \"noNewModel\": \"对于 LMStudio, Ollama, Llamafile，我们动态获取。无需手动添加。\",\n    \"searchModel\": \"搜索模型\",\n    \"selectAll\": \"全选\",\n    \"save\": \"保存\",\n    \"saving\": \"保存中...\",\n    \"manageModels\": {\n        \"columns\": {\n            \"name\": \"模型名称\",\n            \"model_type\": \"模型类型\",\n            \"model_id\": \"模型 ID\",\n            \"provider\": \"提供商名称\",\n            \"actions\": \"操作\",\n            \"nickname\": \"模型昵称\"\n        },\n        \"tooltip\": {\n            \"delete\": \"删除\"\n        },\n        \"confirm\": {\n            \"delete\": \"您确定要删除此模型吗？\"\n        },\n        \"modal\": {\n            \"title\": \"添加自定义模型\",\n            \"titleEdit\": \"编辑自定义模型\",\n            \"form\": {\n                \"name\": {\n                    \"label\": \"模型 ID\",\n                    \"placeholder\": \"llama3.2\",\n                    \"required\": \"模型 ID 为必填项。\"\n                },\n                \"provider\": {\n                    \"label\": \"提供商\",\n                    \"placeholder\": \"选择提供商\",\n                    \"required\": \"提供商为必填项。\"\n                },\n                \"type\": {\n                    \"label\": \"模型类型\"\n                }\n            }\n        }\n    },\n    \"noModelFound\": \"未找到模型。请确保您已添加正确的提供商，包括基础 URL 和 API 密钥。\",\n    \"radio\": {\n        \"chat\": \"聊天模型\",\n        \"embedding\": \"嵌入模型\",\n        \"chatInfo\": \"用于聊天补全和对话生成\",\n        \"embeddingInfo\": \"用于 RAG 和其他语义搜索相关任务。\"\n    },\n    \"nicknameModal\": {\n        \"title\": \"添加/编辑模型昵称\",\n        \"form\": {\n            \"modelName\": {\n                \"label\": \"模型名称\",\n                \"placeholder\": \"请输入模型名称\",\n                \"required\": \"模型名称为必填项。\"\n            },\n            \"modelAvatar\": {\n                \"label\": \"模型头像\",\n                \"placeholder\": \"请输入模型头像\",\n                \"help\": \"请输入模型头像的URL地址。此图像将显示在聊天窗口中。\"\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/assets/locale/zh/option.json",
    "content": "{\n    \"newChat\": \"新聊天\",\n    \"selectAPrompt\": \"选择一个提示词\",\n    \"githubRepository\": \"GitHub 仓库\",\n    \"settings\": \"设置\",\n    \"sidebarTitle\": \"聊天历史\",\n    \"error\": \"错误\",\n    \"somethingWentWrong\": \"出现了错误\",\n    \"validationSelectModel\": \"请选择一个模型以继续\",\n    \"deleteHistoryConfirmation\": \"你确定要删除这个历史记录吗？\",\n    \"editHistoryTitle\": \"输入一个新的标题\",\n    \"temporaryChat\": \"临时聊天\",\n    \"more\": {\n        \"copy\": {\n            \"group\": \"复制\",\n            \"asText\": \"复制为文本\",\n            \"asMarkdown\": \"复制为 Markdown\",\n            \"success\": \"已复制到剪贴板！\"\n        },\n        \"download\": {\n            \"group\": \"下载\",\n            \"text\": \"文本文件 (.txt)\",\n            \"markdown\": \"Markdown 文件 (.md)\",\n            \"json\": \"JSON 文件 (.json)\",\n            \"image\": \"图片 (.png)\"\n        },\n        \"share\": \"分享\"\n    }\n}"
  },
  {
    "path": "src/assets/locale/zh/playground.json",
    "content": "{\n    \"ollamaState\": {\n        \"searching\": \"正在搜索您的Ollama 🦙\",\n        \"running\": \"Ollama正在运行 🦙\",\n        \"notRunning\": \"无法连接到Ollama 🦙\",\n        \"connectionError\": \"看起来您遇到了连接错误。请参阅这<anchor>文档</anchor>进行故障排除。\"\n    },\n    \"formError\": {\n        \"noModel\": \"请选择一个模型\",\n        \"noEmbeddingModel\": \"请在设置>RAG页面设置一个文本嵌入模型\"\n    },\n    \"form\": {\n        \"textarea\": {\n            \"placeholder\": \"输入一条消息...\"\n        },\n        \"webSearch\": {\n            \"on\": \"开\",\n            \"off\": \"关\"\n        }\n    },\n    \"tooltip\": {\n        \"searchInternet\": \"搜索互联网\",\n        \"speechToText\": \"语音到文本\",\n        \"uploadImage\": \"上传图片\",\n        \"stopStreaming\": \"停止流式传输\",\n        \"knowledge\": \"知识\",\n        \"vision\": \"[实验性] 视觉聊天\",\n        \"clearContext\": \"清除上下文\"\n    },\n    \"sendWhenEnter\": \"按Enter发送\",\n    \"welcome\": \"你好！今天我能帮你什么？\",\n    \"useOCR\": \"从图片中提取文字（OCR）\"\n}"
  },
  {
    "path": "src/assets/locale/zh/settings.json",
    "content": "{\n  \"generalSettings\": {\n    \"title\": \"一般设置\",\n    \"settings\": {\n      \"heading\": \"Web UI 设置\",\n      \"speechRecognitionLang\": {\n        \"label\": \"语音识别语言\",\n        \"placeholder\": \"选择一种语言\"\n      },\n      \"language\": {\n        \"label\": \"语言\",\n        \"placeholder\": \"选择一种语言\"\n      },\n      \"darkMode\": {\n        \"label\": \"更改主题\",\n        \"options\": {\n          \"light\": \"亮色\",\n          \"dark\": \"暗色\"\n        }\n      },\n      \"searchMode\": {\n        \"label\": \"使用简化的互联网搜索\"\n      },\n      \"copilotResumeLastChat\": {\n        \"label\": \"打开侧边栏时恢复上次聊天（Copilot）\"\n      },\n      \"turnOnChatWithWebsite\": {\n        \"label\": \"默认启用与网站对话（Copilot）\"\n      },\n      \"webUIResumeLastChat\": {\n        \"label\": \"打开Web UI时恢复上次聊天\"\n      },\n      \"hideCurrentChatModelSettings\": {\n        \"label\": \"隐藏当前聊天模型设置\"\n      },\n      \"restoreLastChatModel\": {\n        \"label\": \"恢复上次用于之前聊天的模型\"\n      },\n      \"sendNotificationAfterIndexing\": {\n        \"label\": \"完成知识库处理后发送通知\"\n      },\n      \"generateTitle\": {\n        \"label\": \"使用人工智能生成标题\"\n      },\n      \"ollamaStatus\": {\n        \"label\": \"启用或禁用Ollama连接状态检查\"\n      },\n      \"wideMode\": {\n        \"label\": \"启用宽屏模式\"\n      },\n      \"openReasoning\": {\n        \"label\": \"默认展开推理过程\"\n      },\n      \"userChatBubble\": {\n        \"label\": \"使用聊天气泡显示用户消息\"\n      },\n      \"autoCopyResponseToClipboard\": {\n        \"label\": \"自动复制回复到剪贴板\"\n      },\n      \"useMarkdownForUserMessage\": {\n        \"label\": \"为用户消息启用Markdown格式\"\n      },\n      \"copyAsFormattedText\": {\n        \"label\": \"复制为格式化文本\"\n      },\n      \"tabMentionsEnabled\": {\n        \"label\": \"启用标签提及功能 (@tab)\"\n      },\n      \"pasteLargeTextAsFile\": {\n        \"label\": \"将大文本粘贴为文件\"\n      },\n      \"sidepanelTemporaryChat\": {\n        \"label\": \"默认启用侧边栏临时聊天\"\n      }\n    },\n    \"sidepanelRag\": {\n      \"heading\": \"检索设置\",\n      \"ragEnabled\": {\n        \"label\": \"启用嵌入和检索\"\n      },\n      \"maxWebsiteContext\": {\n        \"label\": \"完整上下文模式的最大内容大小\",\n        \"placeholder\": \"内容大小（默认4028）\"\n      }\n    },\n    \"webSearch\": {\n      \"heading\": \"管理网络搜索\",\n      \"searchMode\": {\n        \"label\": \"执行简单的网际网路搜索\"\n      },\n      \"provider\": {\n        \"label\": \"搜索引擎\",\n        \"placeholder\": \"选择一个搜索引擎\"\n      },\n      \"totalSearchResults\": {\n        \"label\": \"总搜索结果\",\n        \"placeholder\": \"输入总搜索结果\"\n      },\n      \"visitSpecificWebsite\": {\n        \"label\": \"访问消息中提到的网站。\"\n      },\n      \"searxng\": {\n        \"url\": {\n          \"label\": \"SearXNG 网址\"\n        }\n      },\n      \"braveApi\": {\n        \"label\": \"Brave API 密钥\",\n        \"placeholder\": \"输入您的 Brave API 密钥\"\n      },\n      \"searchOnByDefault\": {\n        \"label\": \"默认开启网络搜索\"\n      }\n    },\n    \"system\": {\n      \"heading\": \"系统设置\",\n      \"storageSyncEnabled\": {\n        \"label\": \"启用浏览器存储同步（跨设备同步设置）\"\n      },\n      \"deleteChatHistory\": {\n        \"label\": \"系统重置\",\n        \"button\": \"全部重置\",\n        \"confirm\": \"您确定要执行系统重置吗？这将清除所有数据且无法撤消。\"\n      },\n      \"export\": {\n        \"label\": \"导出所有数据（聊天记录、知识库、提示和设置）\",\n        \"button\": \"导出数据\",\n        \"success\": \"导出成功\"\n      },\n      \"import\": {\n        \"label\": \"导入所有数据（聊天记录、知识库、提示和设置）\",\n        \"button\": \"导入数据\",\n        \"success\": \"导入成功\",\n        \"error\": \"导入错误\"\n      }\n    },\n    \"tts\": {\n      \"heading\": \"文本转语音设置\",\n      \"ttsEnabled\": {\n        \"label\": \"启用文本转语音\"\n      },\n      \"ttsAutoPlay\": {\n        \"label\": \"完成后自动播放语音回复\"\n      },\n      \"ttsProvider\": {\n        \"label\": \"文本转语音提供商\",\n        \"placeholder\": \"选择一个提供商\"\n      },\n      \"ttsVoice\": {\n        \"label\": \"文本转语音语音\",\n        \"placeholder\": \"选择一种语音\"\n      },\n      \"ssmlEnabled\": {\n        \"label\": \"启用SSML(语音合成标记语言)\"\n      },\n      \"removeReasoningTagTTS\": {\n        \"label\": \"从语音合成中移除推理标签\"\n      }\n    },\n    \"stt\": {\n      \"heading\": \"语音转文本设置\",\n      \"autoStopTimeout\": {\n        \"label\": \"自动停止超时（毫秒）\",\n        \"placeholder\": \"输入自动停止超时时间（毫秒）\"\n      },\n      \"autoSubmitVoiceMessage\": {\n        \"label\": \"自动提交语音消息\"\n      }\n    }\n  },\n  \"manageModels\": {\n    \"title\": \"管理模型\",\n    \"addBtn\": \"添加新模型\",\n    \"columns\": {\n      \"name\": \"名称\",\n      \"digest\": \"摘要\",\n      \"modifiedAt\": \"修改时间\",\n      \"size\": \"大小\",\n      \"actions\": \"操作\"\n    },\n    \"expandedColumns\": {\n      \"parentModel\": \"父模型\",\n      \"format\": \"格式\",\n      \"family\": \"家族\",\n      \"parameterSize\": \"参数大小\",\n      \"quantizationLevel\": \"量化级别\"\n    },\n    \"tooltip\": {\n      \"delete\": \"删除模型\",\n      \"repull\": \"重新拉取模型\"\n    },\n    \"confirm\": {\n      \"delete\": \"您确定要删除此模型吗？\",\n      \"repull\": \"您确定要重新拉取此模型吗？\"\n    },\n    \"modal\": {\n      \"title\": \"添加新模型\",\n      \"placeholder\": \"输入模型名称\",\n      \"pull\": \"拉取模型\"\n    },\n    \"notification\": {\n      \"pullModel\": \"正在拉取模型\",\n      \"pullModelDescription\": \"正在拉取 {{modelName}} ，请查看扩展图标。\",\n      \"success\": \"成功\",\n      \"error\": \"错误\",\n      \"successDescription\": \"成功拉取了模型\",\n      \"successDeleteDescription\": \"成功删除了模型\",\n      \"someError\": \"出现了问题。请稍后再试。\"\n    }\n  },\n  \"managePrompts\": {\n    \"title\": \"管理提示词\",\n    \"addBtn\": \"添加新提示词\",\n    \"option1\": \"普通\",\n    \"option2\": \"RAG\",\n    \"questionPrompt\": \"问题提示词\",\n    \"columns\": {\n      \"title\": \"标题\",\n      \"prompt\": \"提示词\",\n      \"type\": \"提示词类型\",\n      \"actions\": \"操作\"\n    },\n    \"systemPrompt\": \"系统提示词\",\n    \"quickPrompt\": \"快速提示词\",\n    \"tooltip\": {\n      \"delete\": \"删除提示词\",\n      \"edit\": \"编辑提示词\"\n    },\n    \"confirm\": {\n      \"delete\": \"您确定要删除此提示词吗？这个操作不能撤销。\"\n    },\n    \"modal\": {\n      \"addTitle\": \"添加新提示词\",\n      \"editTitle\": \"编辑提示词\"\n    },\n    \"segmented\": {\n      \"custom\": \"自定义提示\",\n      \"copilot\": \"Copilot 提示\"\n    },\n    \"form\": {\n      \"title\": {\n        \"label\": \"标题\",\n        \"placeholder\": \"我厉害的提示词\",\n        \"required\": \"请输入标题\",\n        \"help\": \"您可以在提示词中使用 {key} 作为变量。\"\n      },\n      \"prompt\": {\n        \"label\": \"提示词\",\n        \"placeholder\": \"输入提示词\",\n        \"required\": \"请输入提示词\",\n        \"help\": \"您可以在提示词中使用 {key} 作为变量。\",\n        \"missingTextPlaceholder\": \"提示中缺少{text}变量。请添加它。\"\n      },\n      \"isSystem\": {\n        \"label\": \"是否为系统提示词\"\n      },\n      \"btnSave\": {\n        \"saving\": \"添加提示词中...\",\n        \"save\": \"添加提示词\"\n      },\n      \"btnEdit\": {\n        \"saving\": \"更新提示词中...\",\n        \"save\": \"更新提示词\"\n      }\n    },\n    \"notification\": {\n      \"addSuccess\": \"提示词已添加\",\n      \"addSuccessDesc\": \"提示词已成功添加\",\n      \"error\": \"错误\",\n      \"someError\": \"出现了问题。请稍后再试。\",\n      \"updatedSuccess\": \"提示词已更新\",\n      \"updatedSuccessDesc\": \"提示词已成功更新\",\n      \"deletedSuccess\": \"提示词已删除\",\n      \"deletedSuccessDesc\": \"提示词已成功删除\"\n    }\n  },\n  \"manageShare\": {\n    \"title\": \"管理共享\",\n    \"heading\": \"配置对话共享服务 URL\",\n    \"form\": {\n      \"url\": {\n        \"label\": \"对话共享服务 URL\",\n        \"placeholder\": \"输入对话共享服务 URL\",\n        \"required\": \"请输入您的对话共享服务 URL！\",\n        \"help\": \"出于隐私原因，您可以自行托管对话共享服务并在此处提供 URL，<anchor>了解更多</anchor>。\"\n      }\n    },\n    \"webshare\": {\n      \"heading\": \"对话共享\",\n      \"columns\": {\n        \"title\": \"标题\",\n        \"url\": \"URL\",\n        \"actions\": \"操作\"\n      },\n      \"tooltip\": {\n        \"delete\": \"删除对话共享\"\n      },\n      \"confirm\": {\n        \"delete\": \"您确定要删除此对话共享吗？这个操作不能撤销。\"\n      },\n      \"label\": \"管理页面分享\",\n      \"description\": \"启用或禁用页面分享功能 \"\n    },\n    \"notification\": {\n      \"pageShareSuccess\": \"对话共享服务 URL 已成功更新\",\n      \"someError\": \"出现了问题。请稍后再试。\",\n      \"webShareDeleteSuccess\": \"对话共享已成功删除\"\n    }\n  },\n  \"ollamaSettings\": {\n    \"title\": \"Ollama 设置\",\n    \"heading\": \"配置 Ollama\",\n    \"settings\": {\n      \"ollamaUrl\": {\n        \"label\": \"Ollama URL\",\n        \"placeholder\": \"输入 Ollama URL\"\n      },\n      \"globalEnable\": {\n        \"label\": \"启用或禁用全局 Ollama 集成\",\n        \"warning\": \"通过全局禁用 Ollama 集成，Page Assist 将不会从 Ollama 获取模型。您仍然可以从<anchor>OpenAI 兼容 API</anchor>部分添加 Ollama 实例，这将正常工作。\"\n      },\n      \"advanced\": {\n        \"label\": \"Ollama URL 高级配置\",\n        \"urlRewriteEnabled\": {\n          \"label\": \"启用或禁用自定义来源 URL\"\n        },\n        \"rewriteUrl\": {\n          \"label\": \"自定义来源 URL\",\n          \"placeholder\": \"输入自定义来源 URL\"\n        },\n        \"autoCORSFix\": {\n          \"label\": \"启用或禁用自动 Ollama CORS 修复\"\n        },\n        \"headers\": {\n          \"label\": \"自定义头部\",\n          \"add\": \"添加头部\",\n          \"key\": {\n            \"label\": \"头部键\",\n            \"placeholder\": \"授权\"\n          },\n          \"value\": {\n            \"label\": \"头部值\",\n            \"placeholder\": \"承载令牌\"\n          }\n        },\n        \"help\": \"如果您在 Page Assist 上与 Ollama 有连接问题,您可以配置自定义来源 URL。要了解更多关于配置的信息,<anchor>点击此处</anchor>。\"\n      }\n    }\n  },\n  \"manageSearch\": {\n    \"heading\": \"配置网络搜索\",\n    \"title\": \"管理网络搜索\"\n  },\n  \"about\": {\n    \"title\": \"关于\",\n    \"heading\": \"关于\",\n    \"chromeVersion\": \"Page Assist版本\",\n    \"ollamaVersion\": \"Ollama版本\",\n    \"support\": \"您可以通过以下平台捐赠或赞助Page Assist项目:\",\n    \"koFi\": \"在Ko-fi上支持\",\n    \"githubSponsor\": \"在GitHub上赞助\",\n    \"githubRepo\": \"GitHub仓库\"\n  },\n  \"manageKnowledge\": {\n    \"title\": \"管理知识\",\n    \"heading\": \"配置知识库\"\n  },\n  \"rag\": {\n    \"title\": \"Pipeline 设置\",\n    \"ragSettings\": {\n      \"label\": \"RAG 设置\",\n      \"model\": {\n        \"label\": \"文本嵌入模型\",\n        \"required\": \"请选择一个模型\",\n        \"help\": \"建议使用文本嵌入模型，如 `nomic-embed-text`。\",\n        \"placeholder\": \"选择一个模型\"\n      },\n      \"chunkSize\": {\n        \"label\": \"嵌入大小\",\n        \"placeholder\": \"1024-∞\",\n        \"required\": \"请输入块大小\"\n      },\n      \"chunkOverlap\": {\n        \"label\": \"嵌入重叠\",\n        \"placeholder\": \"256-∞\",\n        \"required\": \"请输入嵌入重叠\"\n      },\n      \"totalFilePerKB\": {\n        \"label\": \"知识库默认文件上传限制\",\n        \"placeholder\": \"输入默认文件上传限制（例如：10）\",\n        \"required\": \"请输入默认文件上传限制\"\n      },\n      \"noOfRetrievedDocs\": {\n        \"label\": \"检索文档数量\",\n        \"placeholder\": \"输入检索文档数量\",\n        \"required\": \"请输入检索文档数量\"\n      },\n      \"splittingSeparator\": {\n        \"label\": \"分隔符\",\n        \"placeholder\": \"输入分隔符（例如：\\\\n\\\\n）\",\n        \"required\": \"请输入分隔符\"\n      },\n      \"splittingStrategy\": {\n        \"label\": \"文本分割器\"\n      }\n    },\n    \"prompt\": {\n      \"label\": \"配置 RAG 提示词\",\n      \"option1\": \"普通\",\n      \"option2\": \"搜索\",\n      \"alert\": \"在此配置系统提示词已过时。请使用管理提示词部分添加或编辑提示词。此部分将在未来版本中删除\",\n      \"systemPrompt\": \"系统提示词\",\n      \"systemPromptPlaceholder\": \"输入系统提示词\",\n      \"webSearchPrompt\": \"网页搜索提示词\",\n      \"webSearchPromptHelp\": \"请勿从提示词中删除 `{search_results}`。\",\n      \"webSearchPromptError\": \"请输入一个网页搜索提示词\",\n      \"webSearchPromptPlaceholder\": \"输入网页搜索提示词\",\n      \"webSearchFollowUpPrompt\": \"网页搜索追问提示词\",\n      \"webSearchFollowUpPromptHelp\": \"请勿从提示词中删除 `{chat_history}` 和 `{question}`。\",\n      \"webSearchFollowUpPromptError\": \"请输入您的网页搜索追问提示词！\",\n      \"webSearchFollowUpPromptPlaceholder\": \"您的网页搜索追问提示词\"\n    }\n  },\n  \"chromeAiSettings\": {\n    \"title\": \"Chrome AI 设置\"\n  }\n}\n"
  },
  {
    "path": "src/assets/locale/zh/sidepanel.json",
    "content": "{\n    \"tooltip\": {\n        \"embed\": \"可能需要几分钟才能嵌入搜索结果。请稍候...\",\n        \"clear\": \"清除对话历史\",\n        \"history\": \"对话历史\",\n        \"openwebui\": \"打开网页界面\"\n    }\n}"
  },
  {
    "path": "src/assets/locale/zh-TW/chrome.json",
    "content": "{\n    \"heading\": \"設定 Chrome AI\",\n    \"status\": {\n        \"label\": \"在頁面輔助功能中啟用或停用 Chrome AI 支援\"\n    },\n    \"error\": {\n        \"browser_not_supported\": \"此版本的 Chrome 不支援 Gemini Nano 模型，請更新至版本138或更高\",\n        \"ai_not_supported\": \"設定 chrome://flags/#prompt-api-for-gemini-nano 未啟用，請先啟用它。\",\n        \"ai_not_ready\": \"Gemini Nano 尚未準備好，您需要再次檢查 Chrome 設定。\",\n        \"internal_error\": \"發生內部錯誤，請稍後重試。\"\n    },\n    \"errorDescription\": \"要使用 Chrome AI，您需要 Chrome 版本 138 或更高版本。請按照以下步驟操作：\\n\\n1. 前往 `chrome://flags/#prompt-api-for-gemini-nano` 並啟用「Prompt API for Gemini Nano」。\\n2. 重新啟動 Chrome 以應用該設定。\\n3. 返回此頁面並點擊「下載模型」，首次將下載一個 4GB 的模型。\\n4. 下載完成後，可通過頁面輔助功能啟用 Gemini Nano。\",\n    \"downloadModel\": \"下載模型\",\n    \"modelDownloadWarning\": \"這將下載一個大約 1.5 GB 到 2.4 GB 的模型，請確保您有足夠的硬碟空間。\",\n    \"downloadModal\": {\n        \"title\": \"下載 Gemini Nano 模型\",\n        \"warning\": \"需要下載大量資料\",\n        \"warningDescription\": \"這將下載約 4GB 的資料，請確保您有足夠的硬碟空間和穩定的網路連線。\",\n        \"confirm\": \"立即下載\",\n        \"cancel\": \"取消\",\n        \"downloading\": \"正在下載模型\",\n        \"downloadingDescription\": \"正在下載 Gemini Nano 模型，根據您的網路連線，這可能需要數分鐘。\",\n        \"pleaseWait\": \"下載過程中請勿關閉此視窗...\"\n    },\n    \"downloadSuccess\": \"模型下載成功！\",\n    \"downloadError\": \"模型下載失敗，請再試一次。\"\n}"
  },
  {
    "path": "src/assets/locale/zh-TW/common.json",
    "content": "{\n  \"pageAssist\": \"Page Assist\",\n  \"selectAModel\": \"選擇一個模型\",\n  \"save\": \"儲存\",\n  \"saved\": \"已儲存\",\n  \"cancel\": \"取消\",\n  \"retry\": \"重試\",\n  \"loadMore\": \"載入更多...\",\n  \"share\": {\n    \"tooltip\": {\n      \"share\": \"分享\"\n    },\n    \"modal\": {\n      \"title\": \"為聊天建立分享連結\"\n    },\n    \"form\": {\n      \"defaultValue\": {\n        \"name\": \"匿名\",\n        \"title\": \"未命名聊天\"\n      },\n      \"title\": {\n        \"label\": \"聊天標題\",\n        \"placeholder\": \"輸入聊天標題\",\n        \"required\": \"聊天標題是必填的\"\n      },\n      \"name\": {\n        \"label\": \"您的名字\",\n        \"placeholder\": \"輸入您的名字\",\n        \"required\": \"您的名字是必填的\"\n      },\n      \"btn\": {\n        \"save\": \"生成連結\",\n        \"saving\": \"正在產生連結...\"\n      }\n    },\n    \"notification\": {\n      \"successGenerate\": \"連結已複製到剪貼簿\",\n      \"failGenerate\": \"產生連結失敗\"\n    }\n  },\n  \"copyToClipboard\": \"複製到剪貼簿\",\n  \"webSearch\": \"搜尋網絡\",\n  \"regenerate\": \"重新生成\",\n  \"continue\": \"繼續回應\",\n  \"edit\": \"編輯\",\n  \"delete\": \"刪除\",\n  \"saveAndSubmit\": \"儲存並送出\",\n  \"editMessage\": {\n    \"placeholder\": \"輸入一則訊息...\"\n  },\n  \"submit\": \"送出\",\n  \"noData\": \"沒有資料\",\n  \"noHistory\": \"無聊天記錄\",\n  \"chatWithCurrentPage\": \"與目前頁面聊天\",\n  \"beta\": \"Beta\",\n  \"tts\": \"朗讀\",\n  \"currentChatModelSettings\": \"目前聊天模型設定\",\n  \"modelSettings\": {\n    \"label\": \"模型設定\",\n    \"description\": \"全域設定所有聊天的模型選項\",\n    \"form\": {\n      \"keepAlive\": {\n        \"label\": \"保留時間\",\n        \"help\": \"控制請求後模型在記憶體中保持的時間（預設：5分鐘）\",\n        \"placeholder\": \"例如：5m、10m、1h\"\n      },\n      \"temperature\": {\n        \"label\": \"溫度\",\n        \"placeholder\": \"例如：0.7、1.0\"\n      },\n      \"numCtx\": {\n        \"label\": \"上下文窗口大小 (num_ctx)\",\n        \"placeholder\": \"預設：2048\"\n      },\n      \"numPredict\": {\n        \"label\": \"最大 Token 數 (num_predict)\",\n        \"placeholder\": \"例如：2048、4096\"\n      },\n      \"thinking\": {\n        \"label\": \"思考模式 (Ollama)\",\n        \"levels\": {\n          \"off\": \"關閉\",\n          \"on\": \"開啟\",\n          \"low\": \"低推理強度\",\n          \"medium\": \"中推理強度\",\n          \"high\": \"高推理強度\"\n        }\n      },\n      \"seed\": {\n        \"label\": \"隨機種子\",\n        \"placeholder\": \"例如：1234\",\n        \"help\": \"模型輸出的可重複性\"\n      },\n      \"topK\": {\n        \"label\": \"Top K\",\n        \"placeholder\": \"例如：40、100\"\n      },\n      \"topP\": {\n        \"label\": \"Top P\",\n        \"placeholder\": \"例如：0.9、0.95\"\n      },\n      \"useMMap\": {\n        \"label\": \"useMmap\"\n      },\n      \"tfsZ\": {\n        \"label\": \"TFS-Z\",\n        \"placeholder\": \"例如：1.0、1.1\"\n      },\n      \"numKeep\": {\n        \"label\": \"Num Keep\",\n        \"placeholder\": \"例如：256、512\"\n      },\n      \"numThread\": {\n        \"label\": \"Num Thread\",\n        \"placeholder\": \"例如：8、16\"\n      },\n      \"useMlock\": {\n        \"label\": \"useMlock\"\n      },\n      \"reasoningEffort\": {\n        \"label\": \"Reasoning Effort\",\n        \"placeholder\": \"low, medium, high\"\n      },\n      \"minP\": {\n        \"label\": \"Min P\",\n        \"placeholder\": \"例如：0.05\"\n      },\n      \"repeatPenalty\": {\n        \"label\": \"Repeat Penalty\",\n        \"placeholder\": \"例如：1.1、1.2\"\n      },\n      \"repeatLastN\": {\n        \"label\": \"Repeat Last N\",\n        \"placeholder\": \"例如：64、128\"\n      },\n      \"numGpu\": {\n        \"label\": \"Num GPU\",\n        \"placeholder\": \"輸入要傳送到 GPU 的層數\"\n      },\n      \"systemPrompt\": {\n        \"label\": \"臨時系統提示詞\",\n        \"placeholder\": \"輸入系統提示詞\",\n        \"help\": \"這是一種在當前聊天中設置系統提示詞的快捷方法，如果已存在選定的系統提示詞將會覆蓋它。\"\n      }\n    },\n    \"advanced\": \"更多模型設定\"\n  },\n  \"copilot\": {\n    \"summary\": \"總結\",\n    \"explain\": \"解釋\",\n    \"rephrase\": \"重述\",\n    \"translate\": \"翻譯\",\n    \"custom\": \"自訂\"\n  },\n  \"citations\": \"引用\",\n  \"segmented\": {\n    \"ollama\": \"Ollama 模型\",\n    \"custom\": \"自訂模型\"\n  },\n  \"downloadCode\": \"下載程式碼\",\n  \"date\": {\n    \"pinned\": \"已置頂\",\n    \"today\": \"今天\",\n    \"yesterday\": \"昨天\",\n    \"last7Days\": \"最近7天\",\n    \"older\": \"更早以前\"\n  },\n  \"range\": {\n    \"deleteConfirm\": {\n      \"pinned\": \"確定要刪除所有置頂的紀錄嗎？\",\n      \"today\": \"確定要刪除今天的所有紀錄嗎？\",\n      \"yesterday\": \"確定要刪除昨天的所有紀錄嗎？\",\n      \"last7Days\": \"確定要刪除最近7天的所有紀錄嗎？\",\n      \"older\": \"確定要刪除所有較舊的紀錄嗎？\"\n    },\n    \"tooltip\": {\n      \"pinned\": \"刪除所有置頂的紀錄\",\n      \"today\": \"刪除今天的所有紀錄\",\n      \"yesterday\": \"刪除昨天的所有紀錄\",\n      \"last7Days\": \"刪除最近7天的所有紀錄\",\n      \"older\": \"刪除所有較舊的紀錄\"\n    }\n  },\n  \"historiesDeleted\": \"{{count}} 條紀錄已刪除\",\n  \"deleteHistoriesError\": \"紀錄刪除錯誤\",\n  \"pin\": \"置頂\",\n  \"unpin\": \"取消置頂\",\n  \"generationInfo\": \"生成資訊\",\n  \"sidebarChat\": \"側邊面板聊天\",\n  \"reasoning\": {\n    \"thinking\": \"思考中....\",\n    \"thought\": \"思考了 {{time}}\",\n    \"title\": \"推理過程\",\n    \"expand\": \"顯示推理\",\n    \"collapse\": \"隱藏推理\"\n  },\n  \"embeddingGen\": \"正在建立嵌入向量，這可能需要一些時間\",\n  \"semanticSearch\": \"正在執行語意搜尋\",\n  \"newBranch\": \"新分支\",\n  \"downloading\": \"正在下載\",\n  \"cancelPullingModel\": {\n    \"confirm\": \"確定要取消下載嗎？這將會停止下載過程。根據 Ollama 說明文件，您可以從中斷處重新開始下載。\"\n  },\n  \"saveChat\": \"儲存聊天\",\n  \"projects\": \"專案\",\n  \"projectName\": \"專案名稱\",\n  \"newProject\": \"新增專案\",\n  \"rename\": \"重新命名\",\n  \"yourChats\": \"您的聊天\",\n  \"search\": \"搜尋\",\n  \"searchResults\": \"搜尋結果\",\n  \"loading\": \"載入中\",\n  \"deleteProjectConfirmation\": \"確定要刪除此專案資料夾嗎？聊天紀錄將移回「您的聊天」。\",\n  \"generateTitle\": \"使用 AI 生成標題\",\n  \"generateTitleError\": \"無法生成標題\"\n}\n"
  },
  {
    "path": "src/assets/locale/zh-TW/knowledge.json",
    "content": "{\n    \"addBtn\": \"新增知識\",\n    \"columns\": {\n        \"title\": \"標題\",\n        \"status\": \"狀態\",\n        \"embeddings\": \"嵌入模型\",\n        \"createdAt\": \"創建於\",\n        \"action\": \"操作\"\n    },\n    \"expandedColumns\": {\n        \"name\": \"名稱\"\n    },\n    \"confirm\": {\n        \"delete\": \"您確定要刪除此知識嗎？\",\n        \"deleteSource\": \"您確定要刪除此來源嗎？\"\n    },\n    \"deleteSuccess\": \"知識刪除成功\",\n    \"status\": {\n        \"pending\": \"待處理\",\n        \"finished\": \"已完成\",\n        \"processing\": \"處理中\",\n        \"failed\": \"失敗\"\n    },\n    \"addKnowledge\": \"新增知識\",\n    \"updateKnowledge\": \"新增來源\",\n    \"form\": {\n        \"tabs\": {\n            \"upload\": \"上傳檔案\",\n            \"text\": \"文字輸入\"\n        },\n        \"title\": {\n            \"label\": \"知識標題（選填）\",\n            \"placeholder\": \"輸入知識標題\",\n            \"placeholderOptional\": \"選填標題（預設使用前 50 個字元）\",\n            \"required\": \"知識標題是必需的\"\n        },\n        \"uploadFile\": {\n            \"label\": \"上傳文件\",\n            \"uploadText\": \"將文件拖放到此處或點擊上傳\",\n            \"uploadHint\": \"支援的檔案類型: .pdf, .csv, .txt, .md, .docx\",\n            \"required\": \"文件是必需的\",\n            \"uploadError\": \"不支援的檔案類型\"\n        },\n        \"textInput\": {\n            \"typeLabel\": \"類型\",\n            \"type\": {\n                \"plain\": \"純文字\",\n                \"markdown\": \"Markdown\",\n                \"code\": \"程式碼\"\n            },\n            \"contentLabel\": \"內容\",\n            \"placeholder\": \"請在此處貼上或輸入您的文字...\",\n            \"required\": \"文字內容是必需的\",\n            \"tooLarge\": \"內容過多，請保持在50萬個字元以內。\",\n            \"defaultTitle\": \"未命名文字\"\n        },\n        \"submit\": \"新增\",\n        \"success\": \"知識新增成功\"\n    },\n    \"noEmbeddingModel\": \"請先從 RAG 設定頁面新增一個嵌入模型\",\n    \"newSource\": \"新增來源\",\n    \"editSettings\": {\n        \"title\": \"編輯知識設定\",\n        \"success\": \"知識設定已成功更新\",\n        \"variableInfo\": {\n            \"title\": \"支援的變數\",\n            \"description\": \"您可以在提示詞中使用以下變數：\",\n            \"context\": \"從您的知識庫中檢索的上下文\",\n            \"query\": \"用戶的問題或查詢\"\n        },\n        \"form\": {\n            \"title\": {\n                \"label\": \"知識標題\",\n                \"placeholder\": \"輸入知識標題\",\n                \"required\": \"知識標題是必需的\"\n            },\n            \"systemPrompt\": {\n                \"label\": \"系統提示詞\",\n                \"help\": \"使用您的知識庫自訂 AI 的回應方式。使用 {context} 來檢索資訊，並在提示詞中包含 {question} 作為用戶問題的上下文。注意：如果提示詞中沒有包含 {question}，角色將無法獲取問題的上下文。\",\n                \"placeholder\": \"輸入您的自訂系統提示詞...\",\n                \"prefillButton\": \"使用預設提示詞\"\n            },\n            \"followupPrompt\": {\n                \"label\": \"後續問題提示詞\",\n                \"help\": \"自訂後續問題的處理方式，此提示詞將後續問題視為獨立的問題。\",\n                \"placeholder\": \"輸入您的自訂後續提示詞...\",\n                \"prefillButton\": \"使用預設提示詞\"\n            }\n        },\n        \"tooltip\": \"編輯知識設定\"\n    }\n}"
  },
  {
    "path": "src/assets/locale/zh-TW/openai.json",
    "content": "{\n    \"settings\": \"OpenAI 相容 API\",\n    \"heading\": \"OpenAI 相容 API\",\n    \"subheading\": \"在此管理和設定您與 OpenAI 相容的 API 提供商。\",\n    \"addBtn\": \"新增提供商\",\n    \"table\": {\n        \"name\": \"提供商名稱\",\n        \"baseUrl\": \"Base URL\",\n        \"actions\": \"操作\"\n    },\n    \"modal\": {\n        \"titleAdd\": \"新增提供商\",\n        \"titleEdit\": \"編輯提供商\",\n        \"name\": {\n            \"label\": \"提供商名稱\",\n            \"required\": \"提供商名稱為必填項。\",\n            \"placeholder\": \"輸入提供商名稱\"\n        },\n        \"baseUrl\": {\n            \"label\": \"Base URL\",\n            \"help\": \"OpenAI API 提供商的 Base URL。例如：http://localhost:1234/v1\",\n            \"required\": \"Base URL 為必填項。\",\n            \"placeholder\": \"輸入 Base URL\"\n        },\n        \"apiKey\": {\n            \"label\": \"API 金鑰\",\n            \"required\": \"API 金鑰為必填項。\",\n            \"placeholder\": \"輸入 API 金鑰\"\n        },\n        \"submit\": \"新增\",\n        \"update\": \"更新\",\n        \"deleteConfirm\": \"您確定要刪除此提供商嗎？\",\n        \"model\": {\n            \"title\": \"模型列表\",\n            \"subheading\": \"請選擇您想要從此提供商使用的聊天模型。\",\n            \"success\": \"成功新增模型。\"\n        },\n        \"tipLMStudio\": \"Page Assist 將自動取得您在 LM Studio 中載入的模型，您無需手動新增它們。\"\n    },\n    \"addSuccess\": \"提供商新增成功。\",\n    \"deleteSuccess\": \"提供商刪除成功。\",\n    \"updateSuccess\": \"提供商更新成功。\",\n    \"delete\": \"刪除\",\n    \"edit\": \"編輯\",\n    \"newModel\": \"從提供商新增模型\",\n    \"noNewModel\": \"對於 LMStudio、Ollama、Llamafile，模型採動態取得，無需手動新增。\",\n    \"searchModel\": \"搜尋模型\",\n    \"selectAll\": \"全選\",\n    \"save\": \"儲存\",\n    \"saving\": \"儲存中...\",\n    \"manageModels\": {\n        \"columns\": {\n            \"name\": \"模型名稱\",\n            \"model_type\": \"模型類型\",\n            \"model_id\": \"模型 ID\",\n            \"provider\": \"提供商名稱\",\n            \"actions\": \"操作\",\n            \"nickname\": \"模型暱稱\"\n        },\n        \"tooltip\": {\n            \"delete\": \"刪除\"\n        },\n        \"confirm\": {\n            \"delete\": \"您確定要刪除此模型嗎？\"\n        },\n        \"modal\": {\n            \"title\": \"新增自訂模型\",\n            \"titleEdit\": \"編輯自訂模型\",\n            \"form\": {\n                \"name\": {\n                    \"label\": \"模型 ID\",\n                    \"placeholder\": \"llama3.2\",\n                    \"required\": \"模型 ID 為必填項。\"\n                },\n                \"provider\": {\n                    \"label\": \"提供商\",\n                    \"placeholder\": \"選擇提供商\",\n                    \"required\": \"提供商為必填項。\"\n                },\n                \"type\": {\n                    \"label\": \"模型類型\"\n                }\n            }\n        }\n    },\n    \"noModelFound\": \"未找到模型，請確保您已新增正確的供應商資料，包括 Base URL 和 API 金鑰。\",\n    \"radio\": {\n        \"chat\": \"聊天模型\",\n        \"embedding\": \"嵌入模型\",\n        \"chatInfo\": \"用於聊天補全和對話生成\",\n        \"embeddingInfo\": \"用於 RAG 和其他語義搜尋相關任務。\"\n    },\n    \"nicknameModal\": {\n        \"title\": \"新增 / 編輯模型暱稱\",\n        \"form\": {\n            \"modelName\": {\n                \"label\": \"模型名稱\",\n                \"placeholder\": \"請輸入模型名稱\",\n                \"required\": \"模型名稱為必填項。\"\n            },\n            \"modelAvatar\": {\n                \"label\": \"模型頭像\",\n                \"placeholder\": \"請輸入模型頭像\",\n                \"help\": \"請輸入模型頭像的網址。此圖像將顯示在聊天視窗中。\"\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/assets/locale/zh-TW/option.json",
    "content": "{\n  \"newChat\": \"新聊天\",\n  \"selectAPrompt\": \"選擇一個提示詞\",\n  \"githubRepository\": \"GitHub Repo\",\n  \"settings\": \"設定\",\n  \"sidebarTitle\": \"聊天紀錄\",\n  \"error\": \"錯誤\",\n  \"somethingWentWrong\": \"出現了錯誤\",\n  \"validationSelectModel\": \"請選擇模型以繼續\",\n  \"deleteHistoryConfirmation\": \"您確定要刪除這條紀錄嗎？\",\n  \"editHistoryTitle\": \"輸入一個新的標題\",\n  \"temporaryChat\": \"臨時聊天\",\n  \"more\": {\n    \"copy\": {\n      \"group\": \"複製\",\n      \"asText\": \"複製為文字\",\n      \"asMarkdown\": \"複製為Markdown\",\n      \"success\": \"已複製到剪貼簿！\"\n    },\n    \"download\": {\n      \"group\": \"下載\",\n      \"text\": \"文字檔 (.txt)\",\n      \"markdown\": \"Markdown 文件 (.md)\",\n      \"json\": \"JSON 文件 (.json)\",\n      \"image\": \"圖片 (.png)\"\n    },\n    \"share\": \"分享\"\n  },\n  \"chatSaved\": \"聊天已儲存\",\n  \"temporaryChatSavedSuccessfully\": \"您的臨時聊天已成功儲存\",\n  \"failedToSaveTemporaryChat\": \"無法儲存臨時聊天，請再試一次。\"\n}"
  },
  {
    "path": "src/assets/locale/zh-TW/playground.json",
    "content": "{\n  \"ollamaState\": {\n    \"searching\": \"正在搜尋您的 Ollama 🦙\",\n    \"running\": \"Ollama 正在運行 🦙\",\n    \"notRunning\": \"無法連接到 Ollama 🦙\",\n    \"connectionError\": \"看起來您遇到了連線錯誤，請參閱此<anchor>文件</anchor>進行故障排除。\"\n  },\n  \"formError\": {\n    \"noModel\": \"請選擇一個模型\",\n    \"noEmbeddingModel\": \"請在設定 > RAG 頁面設定一個嵌入模型\"\n  },\n  \"form\": {\n    \"textarea\": {\n      \"placeholder\": \"輸入一則訊息...\"\n    },\n    \"webSearch\": {\n      \"on\": \"開\",\n      \"off\": \"關\"\n    },\n    \"thinking\": {\n      \"on\": \"開啟\",\n      \"off\": \"關閉\",\n      \"level\": \"等級\",\n      \"levels\": {\n        \"low\": \"低\",\n        \"medium\": \"中\",\n        \"high\": \"高\"\n      }\n    }\n  },\n  \"tooltip\": {\n    \"searchInternet\": \"搜尋網路\",\n    \"thinking\": \"啟用推理模式以查看模型的思考過程\",\n    \"speechToText\": \"語音轉文字\",\n    \"uploadImage\": \"上傳圖片\",\n    \"stopStreaming\": \"停止串流\",\n    \"knowledge\": \"知識\",\n    \"vision\": \"[實驗]視覺聊天\",\n    \"clearContext\": \"清除對話上下文\",\n    \"uploadDocuments\": \"上傳文件 (beta)\"\n  },\n  \"sendWhenEnter\": \"按 Enter 送出\",\n  \"welcome\": \"你好！今天我能幫你什麼？\",\n  \"useOCR\": \"從圖片中擷取文字（OCR）\"\n}"
  },
  {
    "path": "src/assets/locale/zh-TW/settings.json",
    "content": "{\n  \"generalSettings\": {\n    \"title\": \"一般設定\",\n    \"settings\": {\n      \"heading\": \"Web UI 設定\",\n      \"speechRecognitionLang\": {\n        \"label\": \"語音辨識語言\",\n        \"placeholder\": \"選擇語言\"\n      },\n      \"language\": {\n        \"label\": \"語言\",\n        \"placeholder\": \"選擇語言\"\n      },\n      \"darkMode\": {\n        \"label\": \"更改主題\",\n        \"options\": {\n          \"light\": \"亮色\",\n          \"dark\": \"暗色\"\n        }\n      },\n      \"defaultCopilotPrompt\": {\n        \"label\": \"側邊面板的預設提示詞 (Copilot)\",\n        \"placeholder\": \"選擇提示詞\"\n      },\n      \"defaultWebUIPrompt\": {\n        \"label\": \"網頁介面預設提示詞\",\n        \"placeholder\": \"選擇提示詞\"\n      },\n      \"copilotResumeLastChat\": {\n        \"label\": \"打開側邊面板 (Copilot) 時恢復上次聊天\"\n      },\n      \"turnOnChatWithWebsite\": {\n        \"label\": \"預設啟用與網站聊天 (Copilot)\"\n      },\n      \"webUIResumeLastChat\": {\n        \"label\": \"開啟 Web UI 時恢復上次聊天\"\n      },\n      \"hideCurrentChatModelSettings\": {\n        \"label\": \"隱藏目前聊天模型設定\"\n      },\n      \"restoreLastChatModel\": {\n        \"label\": \"恢復之前聊天中最後使用的模型\"\n      },\n      \"sendNotificationAfterIndexing\": {\n        \"label\": \"完成知識庫處理後發送通知\"\n      },\n      \"generateTitle\": {\n        \"label\": \"使用 AI 生成標題\"\n      },\n      \"ollamaStatus\": {\n        \"label\": \"啟用或停用 Ollama 連線狀態檢查\"\n      },\n      \"wideMode\": {\n        \"label\": \"啟用寬螢幕模式\"\n      },\n      \"openReasoning\": {\n        \"label\": \"預設展開推理過程\"\n      },\n      \"defaultThinkingMode\": {\n        \"label\": \"在表單中顯示思考模式狀態\"\n      },\n      \"userChatBubble\": {\n        \"label\": \"使用對話泡泡顯示使用者訊息\"\n      },\n      \"autoCopyResponseToClipboard\": {\n        \"label\": \"自動複製回應至剪貼簿\"\n      },\n      \"useMarkdownForUserMessage\": {\n        \"label\": \"啟用使用者訊息的 Markdown 格式\"\n      },\n      \"copyAsFormattedText\": {\n        \"label\": \"複製為格式化文字\"\n      },\n      \"tabMentionsEnabled\": {\n        \"label\": \"啟用分頁提及 (@tab)\"\n      },\n      \"pasteLargeTextAsFile\": {\n        \"label\": \"將大量文字貼上為檔案\"\n      },\n      \"ocrLanguage\": {\n        \"label\": \"預設 OCR 語言\",\n        \"placeholder\": \"選擇 OCR 語言\"\n      },\n      \"sidepanelTemporaryChat\": {\n        \"label\": \"預設為側邊面板啟用臨時聊天\"\n      },\n      \"webuiTemporaryChat\": {\n        \"label\": \"預設為網頁介面啟用臨時聊天\"\n      },\n      \"removeReasoningTagFromCopy\": {\n        \"label\": \"從複製的文字中刪除推理標籤\"\n      },\n      \"youtubeAutoSummarize\": {\n        \"label\": \"在 YouTube 影片顯示「總結」按鈕。\"\n      },\n      \"hideReasoningWidget\": {\n        \"label\": \"從 AI 訊息中隱藏推理元件\"\n      },\n      \"persistChatInput\": {\n        \"label\": \"保留聊天輸入（儲存未送出的訊息）\"\n      },\n      \"enableMessageQueue\": {\n        \"label\": \"在串流時啟用訊息佇列\"\n      },\n      \"optimizeQueueForSmallScreen\": {\n        \"label\": \"為小螢幕優化聊天介面\"\n      },\n      \"tableTextWrap\": {\n        \"label\": \"在 Markdown 表格中啟用文字換行\"\n      },\n      \"showMoreForLargeMessage\": {\n        \"label\": \"對於較長的使用者訊息顯示「顯示更多」\"\n      },\n      \"sidebarPosition\": {\n        \"label\": \"側邊欄位置\",\n        \"options\": {\n          \"left\": \"左側\",\n          \"right\": \"右側\"\n        }\n      }\n    },\n    \"sidepanelRag\": {\n      \"heading\": \"檢索設定\",\n      \"ragEnabled\": {\n        \"label\": \"啟用嵌入和檢索\"\n      },\n      \"maxWebsiteContext\": {\n        \"label\": \"完整上下文模式的最大內容大小\",\n        \"placeholder\": \"內容大小 (預設 4028)\"\n      }\n    },\n    \"webSearch\": {\n      \"heading\": \"網頁搜尋設定\",\n      \"searchMode\": {\n        \"label\": \"執行簡單的網路搜尋\"\n      },\n      \"provider\": {\n        \"label\": \"搜尋引擎\",\n        \"placeholder\": \"選擇搜尋引擎\"\n      },\n      \"totalSearchResults\": {\n        \"label\": \"總搜尋結果數\",\n        \"placeholder\": \"輸入總搜尋結果數\"\n      },\n      \"visitSpecificWebsite\": {\n        \"label\": \"訪問訊息中提到的網站\"\n      },\n      \"searxng\": {\n        \"url\": {\n          \"label\": \"SearXNG URL\"\n        }\n      },\n      \"braveApi\": {\n        \"label\": \"Brave API 金鑰\",\n        \"placeholder\": \"輸入您的 Brave API 金鑰\"\n      },\n      \"tavilyApi\": {\n        \"label\": \"Tavily API 金鑰\",\n        \"placeholder\": \"輸入您的 Tavily API 金鑰\"\n      },\n      \"exa\": {\n        \"label\": \"Exa API 金鑰\",\n        \"placeholder\": \"輸入您的 Exa API 金鑰\"\n      },\n      \"googleDomain\": {\n        \"label\": \"Google 網域\"\n      },\n      \"searchOnByDefault\": {\n        \"label\": \"預設開啟網路搜尋\"\n      },\n      \"firecrawlAPIKey\": {\n        \"label\": \"Firecrawl API 金鑰\",\n        \"placeholder\": \"輸入您的 Firecrawl API 金鑰\"\n      },\n      \"domainFilter\": {\n        \"label\": \"網域過濾清單\",\n        \"description\": \"僅顯示來自這些網域的結果\",\n        \"placeholder\": \"例如：example.com\"\n      },\n      \"blockedDomains\": {\n        \"label\": \"封鎖網域\",\n        \"description\": \"排除來自這些網域的結果\",\n        \"placeholder\": \"例如：spam.com\"\n      }\n    },\n    \"system\": {\n      \"heading\": \"系統設定\",\n      \"storageSyncEnabled\": {\n        \"label\": \"啟用瀏覽器儲存同步（跨裝置同步設定）\"\n      },\n      \"deleteChatHistory\": {\n        \"label\": \"系統重置\",\n        \"button\": \"全部重置\",\n        \"confirm\": \"您確定要執行系統重置嗎？這將清除所有資料並且無法撤銷。\"\n      },\n      \"export\": {\n        \"label\": \"匯出所有資料（聊天記錄、知識庫、提示與設定）\",\n        \"button\": \"匯出資料\",\n        \"success\": \"匯出成功\"\n      },\n      \"import\": {\n        \"label\": \"匯入所有資料（聊天記錄、知識庫、提示與設定）\",\n        \"button\": \"匯入資料\",\n        \"success\": \"匯入成功\",\n        \"error\": \"匯入錯誤\"\n      },\n      \"actionIcon\": {\n        \"label\": \"設定擴充功能圖示點擊的預設動作\"\n      },\n      \"contextMenu\": {\n        \"label\": \"設定右鍵選單點擊的預設動作\"\n      },\n      \"fontSize\": {\n        \"label\": \"字型大小\"\n      },\n      \"webuiBtnSidePanel\": {\n        \"label\": \"在側邊面板顯示網頁介面按鈕\"\n      },\n      \"chatBackgroundImage\": {\n        \"label\": \"聊天背景圖片\"\n      }\n    },\n    \"tts\": {\n      \"heading\": \"文字轉語音設定\",\n      \"ttsEnabled\": {\n        \"label\": \"啟用文字轉語音\"\n      },\n      \"ttsAutoPlay\": {\n        \"label\": \"完成後自動播放語音回應\"\n      },\n      \"ttsProvider\": {\n        \"label\": \"文字轉語音提供商\",\n        \"placeholder\": \"選擇提供商\"\n      },\n      \"ttsVoice\": {\n        \"label\": \"文字轉語音的聲音\",\n        \"placeholder\": \"選擇聲音\"\n      },\n      \"ssmlEnabled\": {\n        \"label\": \"啟用 SSML（Speech Synthesis Markup Language）\"\n      },\n      \"responseSplitting\": {\n        \"label\": \"回應拆分\"\n      },\n      \"removeReasoningTagTTS\": {\n        \"label\": \"從 TTS 中刪除推理標籤\"\n      }\n    },\n    \"stt\": {\n      \"heading\": \"語音轉文字設定\",\n      \"autoStopTimeout\": {\n        \"label\": \"自動停止逾時（毫秒）\",\n        \"placeholder\": \"輸入自動停止逾時的毫秒數\"\n      },\n      \"autoSubmitVoiceMessage\": {\n        \"label\": \"自動送出語音訊息\"\n      }\n    }\n  },\n  \"manageModels\": {\n    \"title\": \"模型管理\",\n    \"addBtn\": \"新增模型\",\n    \"columns\": {\n      \"name\": \"名稱\",\n      \"digest\": \"雜湊值\",\n      \"modifiedAt\": \"修改時間\",\n      \"size\": \"大小\",\n      \"actions\": \"操作\"\n    },\n    \"expandedColumns\": {\n      \"parentModel\": \"父模型\",\n      \"format\": \"格式\",\n      \"family\": \"家族\",\n      \"parameterSize\": \"參數大小\",\n      \"quantizationLevel\": \"量化級別\"\n    },\n    \"tooltip\": {\n      \"delete\": \"刪除模型\",\n      \"repull\": \"重新下載模型\"\n    },\n    \"confirm\": {\n      \"delete\": \"您確定要刪除該模型嗎？\",\n      \"repull\": \"您確定要重新下載該模型嗎？\"\n    },\n    \"modal\": {\n      \"title\": \"新增模型\",\n      \"placeholder\": \"輸入模型名稱\",\n      \"pull\": \"下載模型\"\n    },\n    \"notification\": {\n      \"pullModel\": \"正在下載模型\",\n      \"pullModelDescription\": \"模型 {{modelName}} 下載中，欲了解更多詳細資訊，請查看擴充圖標。\",\n      \"cancellingDownload\": \"取消下載\",\n      \"cancellingDownloadDescription\": \"正在取消模型下載...\",\n      \"success\": \"成功\",\n      \"error\": \"錯誤\",\n      \"successDescription\": \"成功下載模型\",\n      \"successDeleteDescription\": \"成功刪除模型\",\n      \"someError\": \"出了點問題，請稍後重試\"\n    }\n  },\n  \"managePrompts\": {\n    \"title\": \"提示詞管理\",\n    \"addBtn\": \"新增提示詞\",\n    \"option1\": \"一般\",\n    \"option2\": \"RAG\",\n    \"questionPrompt\": \"問題提示詞\",\n    \"segmented\": {\n      \"custom\": \"自訂提示詞\",\n      \"copilot\": \"Copilot 提示詞\"\n    },\n    \"columns\": {\n      \"title\": \"標題\",\n      \"prompt\": \"提示詞\",\n      \"type\": \"提示詞類型\",\n      \"actions\": \"操作\"\n    },\n    \"systemPrompt\": \"系統提示詞\",\n    \"quickPrompt\": \"快速提示詞\",\n    \"tooltip\": {\n      \"delete\": \"刪除提示詞\",\n      \"edit\": \"編輯提示詞\"\n    },\n    \"confirm\": {\n      \"delete\": \"您確實要刪除這個提示詞嗎？此操作無法撤銷。\"\n    },\n    \"modal\": {\n      \"addTitle\": \"新增提示詞\",\n      \"editTitle\": \"編輯提示詞\"\n    },\n    \"form\": {\n      \"title\": {\n        \"label\": \"標題\",\n        \"placeholder\": \"My Awesome Prompt\",\n        \"required\": \"請輸入標題\"\n      },\n      \"prompt\": {\n        \"label\": \"提示詞\",\n        \"placeholder\": \"輸入提示詞\",\n        \"required\": \"請輸入提示詞\",\n        \"help\": \"您可以在提示詞中使用 {key} 作為變數。\",\n        \"missingTextPlaceholder\": \"提示詞中缺少 {text} 變數，請新增。\"\n      },\n      \"isSystem\": {\n        \"label\": \"是系統提示詞\"\n      },\n      \"btnSave\": {\n        \"saving\": \"正在新增提示詞...\",\n        \"save\": \"新增提示詞\"\n      },\n      \"btnEdit\": {\n        \"saving\": \"正在更新提示詞...\",\n        \"save\": \"更新提示詞\"\n      }\n    },\n    \"notification\": {\n      \"addSuccess\": \"提示詞已新增\",\n      \"addSuccessDesc\": \"提示詞已新增成功\",\n      \"error\": \"錯誤\",\n      \"someError\": \"出了點問題，請稍後重試\",\n      \"updatedSuccess\": \"提示詞已更新\",\n      \"updatedSuccessDesc\": \"提示詞已更新成功\",\n      \"deletedSuccess\": \"提示詞已刪除\",\n      \"deletedSuccessDesc\": \"提示詞已刪除成功\"\n    }\n  },\n  \"manageShare\": {\n    \"title\": \"分享管理\",\n    \"heading\": \"設定頁面分享 URL\",\n    \"form\": {\n      \"url\": {\n        \"label\": \"頁面分享 URL\",\n        \"placeholder\": \"輸入頁面分享 URL\",\n        \"required\": \"請輸入頁面分享 URL！\",\n        \"help\": \"出於隱私的考量，您可以自行託管頁面分享服務並在此處提供 URL。<anchor>了解更多</anchor>。\"\n      }\n    },\n    \"webshare\": {\n      \"heading\": \"網頁分享\",\n      \"columns\": {\n        \"title\": \"標題\",\n        \"url\": \"URL\",\n        \"actions\": \"操作\"\n      },\n      \"tooltip\": {\n        \"delete\": \"刪除分享\"\n      },\n      \"confirm\": {\n        \"delete\": \"您確實要刪除分享嗎？此操作無法撤銷。\"\n      },\n      \"label\": \"頁面分享管理\",\n      \"description\": \"啟用或停用頁面分享功能\"\n    },\n    \"notification\": {\n      \"pageShareSuccess\": \"頁面分享網址更新成功\",\n      \"someError\": \"出現錯誤，請稍後重試\",\n      \"webShareDeleteSuccess\": \"網頁分享已成功刪除\"\n    }\n  },\n  \"ollamaSettings\": {\n    \"title\": \"Ollama 設定\",\n    \"heading\": \"Ollama 設定\",\n    \"settings\": {\n      \"ollamaUrl\": {\n        \"label\": \"Ollama URL\",\n        \"placeholder\": \"輸入 Ollama URL\"\n      },\n      \"globalEnable\": {\n        \"label\": \"全域啟用或停用 Ollama 整合功能\",\n        \"warning\": \"透過全域停用 Ollama 整合功能後，Page Assist 將不會從 Ollama 取得模型，您仍然可以從 <anchor>OpenAI 相容 API</anchor> 的部分新增 Ollama 實例，一樣可以正常運作。\"\n      },\n      \"advanced\": {\n        \"label\": \"進階 Ollama URL 設定\",\n        \"urlRewriteEnabled\": {\n          \"label\": \"啟用或停用自訂來源 URL\"\n        },\n        \"rewriteUrl\": {\n          \"label\": \"自訂來源 URL\",\n          \"placeholder\": \"輸入自訂來源 URL\"\n        },\n        \"autoCORSFix\": {\n          \"label\": \"啟用或停用自動 Ollama CORS 修復\"\n        },\n        \"headers\": {\n          \"label\": \"自訂標頭\",\n          \"add\": \"新增標頭\",\n          \"key\": {\n            \"label\": \"Header Key\",\n            \"placeholder\": \"Authorization\"\n          },\n          \"value\": {\n            \"label\": \"Header Value\",\n            \"placeholder\": \"Bearer token\"\n          }\n        },\n        \"help\": \"如果您在 Page Assist 上遇到與 Ollama 的連線問題，您可以設定自訂來源 URL，要了解有關設定的更多資訊，請<anchor>點擊此處</anchor>。\"\n      }\n    }\n  },\n  \"manageSearch\": {\n    \"title\": \"網路搜尋管理\",\n    \"heading\": \"網路搜尋設定\"\n  },\n  \"about\": {\n    \"title\": \"關於\",\n    \"heading\": \"關於\",\n    \"chromeVersion\": \"Page Assist 版本\",\n    \"ollamaVersion\": \"Ollama 版本\",\n    \"support\": \"您可以透過以下平台捐款或贊助來支持 Page Assist 計畫：\",\n    \"koFi\": \"在 Ko-fi 上支持\",\n    \"githubSponsor\": \"在 GitHub 上支持\",\n    \"githubRepo\": \"GitHub Repository\"\n  },\n  \"manageKnowledge\": {\n    \"title\": \"知識管理\",\n    \"heading\": \"知識庫設定\"\n  },\n  \"rag\": {\n    \"title\": \"Pipeline 設定\",\n    \"ragSettings\": {\n      \"label\": \"RAG 設定\",\n      \"model\": {\n        \"label\": \"嵌入模型\",\n        \"required\": \"請選擇模型\",\n        \"help\": \"強烈建議使用像`nomic-embed-text`這樣的嵌入模型。\",\n        \"placeholder\": \"選擇模型\"\n      },\n      \"chunkSize\": {\n        \"label\": \"區塊大小\",\n        \"placeholder\": \"輸入區塊大小\",\n        \"required\": \"請輸入區塊大小\"\n      },\n      \"chunkOverlap\": {\n        \"label\": \"區塊重疊\",\n        \"placeholder\": \"輸入區塊重疊\",\n        \"required\": \"請輸入區塊重疊\"\n      },\n      \"totalFilePerKB\": {\n        \"label\": \"知識庫預設的檔案上傳限制\",\n        \"placeholder\": \"輸入預設的檔案上傳限制（例如：10）\",\n        \"required\": \"請輸入預設檔案上傳限制\"\n      },\n      \"noOfRetrievedDocs\": {\n        \"label\": \"檢索到的文件數\",\n        \"placeholder\": \"輸入檢索到的文件數\",\n        \"required\": \"請輸入檢索到的文件數\"\n      },\n      \"splittingSeparator\": {\n        \"label\": \"分隔符\",\n        \"placeholder\": \"輸入分隔符號（例如：\\\\n\\\\n）\",\n        \"required\": \"請輸入分隔符\"\n      },\n      \"splittingStrategy\": {\n        \"label\": \"文字分割器\"\n      }\n    },\n    \"prompt\": {\n      \"label\": \"設定 RAG 提示詞\",\n      \"option1\": \"正常\",\n      \"option2\": \"網頁\",\n      \"alert\": \"在此設定系統提示詞已被棄用，請使用「提示詞管理」的部分來新增或編輯提示詞，此部分將在未來的版本中移除。\",\n      \"systemPrompt\": \"系統提示詞\",\n      \"systemPromptPlaceholder\": \"輸入系統提示詞\",\n      \"webSearchPrompt\": \"網路搜尋提示詞\",\n      \"webSearchPromptHelp\": \"請勿從提示詞中刪除 `{search_results}`。\",\n      \"webSearchPromptError\": \"請輸入網路搜尋提示詞\",\n      \"webSearchPromptPlaceholder\": \"請輸入網路搜尋提示詞\",\n      \"webSearchFollowUpPrompt\": \"網路搜尋後續提示詞\",\n      \"webSearchFollowUpPromptHelp\": \"請勿從提示詞中移除`{chat_history}`和{question}`。\",\n      \"webSearchFollowUpPromptError\": \"請輸入網路搜尋後續提示詞\",\n      \"webSearchFollowUpPromptPlaceholder\": \"請輸入網路搜尋後續提示詞\"\n    }\n  },\n  \"chromeAiSettings\": {\n    \"title\": \"Chrome AI 設定\"\n  }\n}\n"
  },
  {
    "path": "src/assets/locale/zh-TW/sidepanel.json",
    "content": "{\n    \"tooltip\": {\n        \"embed\": \"嵌入頁面可能需要幾分鐘，請稍候...\",\n        \"clear\": \"刪除聊天紀錄\",\n        \"history\": \"聊天紀錄\",\n        \"openwebui\": \"開啟網頁介面\"\n    }\n}"
  },
  {
    "path": "src/assets/tailwind.css",
    "content": "@font-face {\n  font-family: \"Arimo\";\n  src: url(\"fonts/Arimo.ttf\");\n  font-display: swap;\n}\n\n.arimo {\n  font-family: \"Arimo\", sans-serif;\n  font-weight: 500;\n  font-style: normal;\n}\n\n@tailwind base;\n@tailwind components;\n@tailwind utilities;\n@layer utilities {\n  .mask-bottom-fade {\n    mask-image: linear-gradient(0deg, transparent 0, #000 160px);\n    -webkit-mask-image: linear-gradient(0deg, transparent 0, #000 160px);\n  }\n}\n\n.ant-select-selection-search-input {\n  border: none !important;\n  box-shadow: none !important;\n}\n\n/* Hide scrollbar for Chrome, Safari and Opera */\n.no-scrollbar::-webkit-scrollbar {\n  display: none;\n}\n\n/* Hide scrollbar for IE, Edge and Firefox */\n.no-scrollbar {\n  -ms-overflow-style: none; /* IE and Edge */\n  scrollbar-width: none; /* Firefox */\n}\n\n@keyframes gradient-border {\n  0% {\n    border-image-source: linear-gradient(\n      45deg,\n      #f79533,\n      #f37055,\n      #ef4e7b,\n      #a166ab\n    );\n  }\n  50% {\n    border-image-source: linear-gradient(45deg, #ef4e7b, #a166ab);\n  }\n  74% {\n    border-image-source: linear-gradient(60deg, #5073b8, #1098ad);\n  }\n  100% {\n    border-image-source: linear-gradient(\n      45deg,\n      #f79533,\n      #f37055,\n      #ef4e7b,\n      #a166ab\n    );\n  }\n}\n\n.animated-gradient-border {\n  border: 3px solid;\n  border-image-slice: 1;\n  animation: gradient-border 3s infinite;\n  border-radius: 10px;\n}\n/* Hide scrollbar by default */\n.custom-scrollbar {\n  scrollbar-width: none;\n  -ms-overflow-style: none;\n}\n\n.custom-scrollbar::-webkit-scrollbar {\n  display: none;\n}\n\n/* Show scrollbar on hover */\n.custom-scrollbar:hover {\n  scrollbar-width: thin;\n  -ms-overflow-style: auto;\n}\n\n.custom-scrollbar:hover::-webkit-scrollbar {\n  display: block;\n  width: 15px;\n}\n\n/* Custom scrollbar styles for light theme */\n.custom-scrollbar:hover::-webkit-scrollbar-track {\n  @apply bg-gray-50;\n  border-radius: 6px;\n}\n\n.custom-scrollbar:hover::-webkit-scrollbar-thumb {\n  @apply bg-gray-300;\n  border-radius: 6px;\n  transition: background 0.2s ease;\n}\n\n.custom-scrollbar:hover::-webkit-scrollbar-thumb:hover {\n  @apply bg-gray-400;\n}\n\n/* Custom scrollbar styles for dark theme */\n.dark .custom-scrollbar:hover::-webkit-scrollbar-track {\n  background-color: #262626;\n}\n\n.dark .custom-scrollbar:hover::-webkit-scrollbar-thumb {\n  background-color: #404040;\n}\n\n.dark .custom-scrollbar:hover::-webkit-scrollbar-thumb:hover {\n  background-color: #525252;\n}\n\n/* For Firefox */\n.custom-scrollbar {\n  scrollbar-color: theme(\"colors.gray.300\") theme(\"colors.gray.50\");\n  scrollbar-width: thin;\n}\n\n.dark .custom-scrollbar {\n  scrollbar-color: #404040 #262626;\n}\n\n@keyframes shimmer-text {\n  0% {\n    background-position: 200% 0;\n  }\n  100% {\n    background-position: -200% 0;\n  }\n}\n\n.shimmer-text {\n  background: linear-gradient(90deg, #b8b9bc 25%, #3a3a3a 50%, #b8b9bc 75%);\n  background-size: 200% 100%;\n  background-clip: text;\n  -webkit-background-clip: text;\n  -webkit-text-fill-color: transparent;\n  animation: shimmer-text 4s linear infinite;\n  color: #9ea0a4;\n}\n\n:global(.dark) .shimmer-text {\n  background: linear-gradient(90deg, #a0a2a6 25%, #f5f5f5 50%, #a0a2a6 75%);\n  background-size: 200% 100%;\n  background-clip: text;\n  -webkit-background-clip: text;\n  -webkit-text-fill-color: transparent;\n  animation: shimmer-text 4s linear infinite;\n  color: #bbbdc1;\n}\n/* =====================================\n   Design tokens — change once, theme everywhere\n   ===================================== */\n.table-wrapper {\n  --tbl-font-size: 0.875rem;\n  --tbl-header-weight: 500;\n  --tbl-border-radius: 0.5rem;\n  --tbl-row-padding-y: 1rem;\n  --tbl-row-padding-x: 1.5rem;\n\n  --tbl-bg-light: #ffffff;\n  --tbl-bg-dark: #0d0d0d;\n\n  --tbl-header-bg-light: #f9fafb;\n  --tbl-header-bg-dark: #1f1f1f;\n\n  --tbl-header-text-light: #374151;\n  --tbl-header-text-dark: #e5e7eb;\n\n  --tbl-cell-text-light: #374151;\n  --tbl-cell-text-dark: #d1d5db;\n\n  --tbl-row-border-light: #f3f4f6;\n  --tbl-row-border-dark: #2a2a2a;\n\n  --tbl-row-hover-light: #f8fafc;\n  --tbl-row-hover-dark: #1a1a1a;\n\n  --tbl-stripe-bg-light: #fbfcfd;\n  --tbl-stripe-bg-dark: #121212;\n}\n\n/* =====================================\n   Wrapper\n   ===================================== */\n.table-wrapper {\n  width: 100%;\n  overflow-x: auto;\n  background-color: var(--tbl-bg-light);\n  border-radius: var(--tbl-border-radius);\n  box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.05);\n  scrollbar-width: thin; /* Firefox */\n}\n\n.dark .table-wrapper {\n  background-color: var(--tbl-bg-dark);\n  box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.2);\n}\n\n/* WebKit scrollbar */\n.table-wrapper::-webkit-scrollbar {\n  height: 8px;\n}\n\n.table-wrapper::-webkit-scrollbar-track {\n  background: transparent;\n}\n\n.table-wrapper::-webkit-scrollbar-thumb {\n  background: rgb(0 0 0 / 0.2);\n  border-radius: 4px;\n}\n\n.dark .table-wrapper::-webkit-scrollbar-thumb {\n  background: rgb(255 255 255 / 0.15);\n}\n\n/* =====================================\n   Table basics\n   ===================================== */\n.table-wrapper table {\n  width: 100%;\n  table-layout: fixed;\n  border-collapse: collapse;\n  font-size: var(--tbl-font-size);\n  min-width: 100%;\n}\n\n/* =====================================\n   Header\n   ===================================== */\n.table-wrapper th {\n  position: sticky;\n  top: 0;\n  z-index: 1;\n  background-color: var(--tbl-header-bg-light);\n  color: var(--tbl-header-text-light);\n  font-weight: var(--tbl-header-weight);\n  text-align: left;\n  padding: var(--tbl-row-padding-y) var(--tbl-row-padding-x);\n  border-bottom: 1px solid var(--tbl-row-border-light);\n  min-width: 150px;\n  backdrop-filter: blur(4px);\n}\n\n.dark .table-wrapper th {\n  background-color: var(--tbl-header-bg-dark);\n  color: var(--tbl-header-text-dark);\n  border-bottom-color: var(--tbl-row-border-dark);\n}\n\n/* =====================================\n   Data cells\n   ===================================== */\n.table-wrapper td {\n  padding: var(--tbl-row-padding-y) var(--tbl-row-padding-x);\n  border-bottom: 1px solid var(--tbl-row-border-light);\n  color: var(--tbl-cell-text-light);\n  vertical-align: top;\n  word-wrap: break-word;\n  min-width: 150px;\n}\n\n.dark .table-wrapper td {\n  border-bottom-color: var(--tbl-row-border-dark);\n  color: var(--tbl-cell-text-dark);\n}\n\n/* =====================================\n   Row interactions\n   ===================================== */\n.table-wrapper tbody tr:nth-child(even) td {\n  background-color: var(--tbl-stripe-bg-light);\n}\n\n.dark .table-wrapper tbody tr:nth-child(even) td {\n  background-color: var(--tbl-stripe-bg-dark);\n}\n\n.table-wrapper tr:hover td {\n  background-color: var(--tbl-row-hover-light);\n}\n\n.dark .table-wrapper tr:hover td {\n  background-color: var(--tbl-row-hover-dark);\n}\n\n/* Remove border from last row */\n.table-wrapper tr:last-child td {\n  border-bottom: none;\n}\n\n/* =====================================\n   Helpers for code / pre blocks inside cells\n   ===================================== */\n.table-wrapper pre,\n.table-wrapper code {\n  font-family: 'SFMono-Regular', Consolas, Monaco, monospace;\n  font-size: 0.8em;\n  line-height: 1.4;\n  background-color: rgb(0 0 0 / 0.05);\n  padding: 0.25rem 0.5rem;\n  border-radius: 0.25rem;\n  white-space: pre-wrap;\n  word-break: break-word;\n}\n\n.dark .table-wrapper pre,\n.dark .table-wrapper code {\n  background-color: rgb(255 255 255 / 0.08);\n}\n\n/* =====================================\n   Responsiveness\n   ===================================== */\n@media (max-width: 768px) {\n  .table-wrapper th,\n  .table-wrapper td {\n    padding: 0.75rem 1rem;\n    font-size: 0.8125rem;\n    min-width: 150px;\n  }\n}\n\n@media (max-width: 640px) {\n  /* Option A — keep horizontal scroll (no markup changes needed) */\n  .table-wrapper th,\n  .table-wrapper td {\n    padding: 0.5rem 0.75rem;\n    font-size: 0.75rem;\n    min-width: 180px;\n  }\n\n  /* -------- Uncomment for Option B -----------\n     Stacked mini-card view.\n     Requires each <td> to carry data-label=\"Header\".\n  --------------------------------------------\n  .table-wrapper table,\n  .table-wrapper thead,\n  .table-wrapper tbody,\n  .table-wrapper th,\n  .table-wrapper td,\n  .table-wrapper tr {\n    display: block;\n  }\n\n  .table-wrapper thead {\n    display: none;\n  }\n\n  .table-wrapper tr {\n    margin-bottom: 0.75rem;\n    border: 1px solid var(--tbl-row-border-light);\n    border-radius: 0.375rem;\n    overflow: hidden;\n  }\n\n  .dark .table-wrapper tr {\n    border-color: var(--tbl-row-border-dark);\n  }\n\n  .table-wrapper td {\n    border: none;\n    position: relative;\n    padding-left: 50%;\n  }\n\n  .table-wrapper td::before {\n    content: attr(data-label);\n    position: absolute;\n    left: 1rem;\n    top: 50%;\n    transform: translateY(-50%);\n    font-weight: 500;\n    color: var(--tbl-header-text-light);\n  }\n\n  .dark .table-wrapper td::before {\n    color: var(--tbl-header-text-dark);\n  }\n  ------------------------------------------ */\n}\n"
  },
  {
    "path": "src/chain/chat-with-website.ts",
    "content": "//@ts-nocheck\nimport { BaseLanguageModel } from \"@langchain/core/language_models/base\";\nimport { Document } from \"@langchain/core/documents\";\nimport {\n  ChatPromptTemplate,\n  MessagesPlaceholder,\n  PromptTemplate,\n} from \"@langchain/core/prompts\";\nimport { AIMessage, BaseMessage, HumanMessage } from \"@langchain/core/messages\";\nimport { StringOutputParser } from \"@langchain/core/output_parsers\";\nimport {\n  Runnable,\n  RunnableBranch,\n  RunnableLambda,\n  RunnableMap,\n  RunnableSequence,\n} from \"@langchain/core/runnables\";\nimport type { ChatHistory } from \"~/store\";\ntype RetrievalChainInput = {\n  chat_history: string;\n  question: string;\n};\n\nexport function groupMessagesByConversation(messages: ChatHistory) {\n  if (messages.length % 2 !== 0) {\n    messages.pop();\n  }\n\n  const groupedMessages = [];\n  for (let i = 0; i < messages.length; i += 2) {\n    groupedMessages.push({\n      human: messages[i]?.content,\n      ai: messages[i + 1]?.content,\n    });\n  }\n\n  return groupedMessages;\n}\n\nconst formatChatHistoryAsString = (history: BaseMessage[]) => {\n  return history\n    .map((message) => `${message._getType()}: ${message?.content}`)\n    .join(\"\\n\");\n};\n\nconst formatDocs = (docs: Document[]) => {\n  return docs\n    .map((doc, i) => `<doc id='${i}'>${doc.pageContent}</doc>`)\n    .join(\"\\n\");\n};\n\nconst serializeHistory = (input: any) => {\n  const chatHistory = input.chat_history || [];\n  const convertedChatHistory = [];\n  for (const message of chatHistory) {\n    if (message.human !== undefined) {\n      convertedChatHistory.push(new HumanMessage({ content: message.human }));\n    }\n    if (message[\"ai\"] !== undefined) {\n      convertedChatHistory.push(new AIMessage({ content: message.ai }));\n    }\n  }\n  return convertedChatHistory;\n};\n\nconst createRetrieverChain = (\n  llm: BaseLanguageModel,\n  retriever: Runnable,\n  question_template: string\n) => {\n  const CONDENSE_QUESTION_PROMPT =\n    PromptTemplate.fromTemplate(question_template);\n  const condenseQuestionChain = RunnableSequence.from([\n    CONDENSE_QUESTION_PROMPT,\n    llm,\n    new StringOutputParser(),\n  ]).withConfig({\n    runName: \"CondenseQuestion\",\n  });\n  const hasHistoryCheckFn = RunnableLambda.from(\n    (input: RetrievalChainInput) => input.chat_history.length > 0\n  ).withConfig({ runName: \"HasChatHistoryCheck\" });\n  const conversationChain = condenseQuestionChain.pipe(retriever).withConfig({\n    runName: \"RetrievalChainWithHistory\",\n  });\n  const basicRetrievalChain = RunnableLambda.from(\n    (input: RetrievalChainInput) => input.question\n  )\n    .withConfig({\n      runName: \"Itemgetter:question\",\n    })\n    .pipe(retriever)\n    .withConfig({ runName: \"RetrievalChainWithNoHistory\" });\n\n  return RunnableBranch.from([\n    [hasHistoryCheckFn, conversationChain],\n    basicRetrievalChain,\n  ]).withConfig({\n    runName: \"FindDocs\",\n  });\n};\n\nexport const createChatWithWebsiteChain = ({\n  llm,\n  question_template,\n  question_llm,\n  retriever,\n  response_template,\n}: {\n  llm: BaseLanguageModel;\n  question_llm: BaseLanguageModel;\n  retriever: Runnable;\n  question_template: string;\n  response_template: string;\n}) => {\n  const retrieverChain = createRetrieverChain(\n    question_llm,\n    retriever,\n    question_template\n  );\n  const context = RunnableMap.from({\n    context: RunnableSequence.from([\n      ({ question, chat_history }) => {\n        return {\n          question: question,\n          chat_history: formatChatHistoryAsString(chat_history),\n        };\n      },\n      retrieverChain,\n      RunnableLambda.from(formatDocs).withConfig({\n        runName: \"FormatDocumentChunks\",\n      }),\n    ]),\n    question: RunnableLambda.from(\n      (input: RetrievalChainInput) => input.question\n    ).withConfig({\n      runName: \"Itemgetter:question\",\n    }),\n    chat_history: RunnableLambda.from(\n      (input: RetrievalChainInput) => input.chat_history\n    ).withConfig({\n      runName: \"Itemgetter:chat_history\",\n    }),\n  }).withConfig({ tags: [\"RetrieveDocs\"] });\n  const prompt = ChatPromptTemplate.fromMessages([\n    [\"system\", response_template],\n    new MessagesPlaceholder(\"chat_history\"),\n    [\"human\", \"{question}\"],\n  ]);\n\n  const responseSynthesizerChain = RunnableSequence.from([\n    prompt,\n    llm,\n    new StringOutputParser(),\n  ]).withConfig({\n    tags: [\"GenerateResponse\"],\n  });\n  return RunnableSequence.from([\n    {\n      question: RunnableLambda.from(\n        (input: RetrievalChainInput) => input.question\n      ).withConfig({\n        runName: \"Itemgetter:question\",\n      }),\n      chat_history: RunnableLambda.from(serializeHistory).withConfig({\n        runName: \"SerializeHistory\",\n      }),\n    },\n    context,\n    responseSynthesizerChain,\n  ]);\n};"
  },
  {
    "path": "src/chain/chat-with-x.ts",
    "content": "//@ts-nocheck\nimport { BaseLanguageModel } from \"@langchain/core/language_models/base\"\nimport { Document } from \"@langchain/core/documents\"\nimport {\n  ChatPromptTemplate,\n  MessagesPlaceholder,\n  PromptTemplate\n} from \"@langchain/core/prompts\"\nimport { AIMessage, BaseMessage, HumanMessage } from \"@langchain/core/messages\"\nimport { StringOutputParser } from \"@langchain/core/output_parsers\"\nimport {\n  Runnable,\n  RunnableBranch,\n  RunnableLambda,\n  RunnableMap,\n  RunnableSequence\n} from \"@langchain/core/runnables\"\ntype RetrievalChainInput = {\n  chat_history: string\n  question: string\n}\n\nconst formatChatHistoryAsString = (history: BaseMessage[]) => {\n  return history\n    .map((message) => `${message._getType()}: ${message.content}`)\n    .join(\"\\n\")\n}\n\nexport const formatDocs = (docs: Document[]) => {\n  return docs\n    .filter(\n      (doc, i, self) =>\n        self.findIndex((d) => d.pageContent === doc.pageContent) === i\n    )\n    .map((doc, i) => `<doc id='${i}'>${doc.pageContent}</doc>`)\n    .join(\"\\n\")\n}\n\nconst serializeHistory = (input: any) => {\n  const chatHistory = input.chat_history || []\n  const convertedChatHistory = []\n  for (const message of chatHistory) {\n    if (message.human !== undefined) {\n      convertedChatHistory.push(new HumanMessage({ content: message.human }))\n    }\n    if (message[\"ai\"] !== undefined) {\n      convertedChatHistory.push(new AIMessage({ content: message.ai }))\n    }\n  }\n  return convertedChatHistory\n}\n\nconst createRetrieverChain = (\n  llm: BaseLanguageModel,\n  retriever: Runnable,\n  question_template: string\n) => {\n  const CONDENSE_QUESTION_PROMPT =\n    PromptTemplate.fromTemplate(question_template)\n  const condenseQuestionChain = RunnableSequence.from([\n    CONDENSE_QUESTION_PROMPT,\n    llm,\n    new StringOutputParser()\n  ]).withConfig({\n    runName: \"CondenseQuestion\"\n  })\n  const hasHistoryCheckFn = RunnableLambda.from(\n    (input: RetrievalChainInput) => input.chat_history.length > 0\n  ).withConfig({ runName: \"HasChatHistoryCheck\" })\n  const conversationChain = condenseQuestionChain.pipe(retriever).withConfig({\n    runName: \"RetrievalChainWithHistory\"\n  })\n  const basicRetrievalChain = RunnableLambda.from(\n    (input: RetrievalChainInput) => input.question\n  )\n    .withConfig({\n      runName: \"Itemgetter:question\"\n    })\n    .pipe(retriever)\n    .withConfig({ runName: \"RetrievalChainWithNoHistory\" })\n\n  return RunnableBranch.from([\n    [hasHistoryCheckFn, conversationChain],\n    basicRetrievalChain\n  ]).withConfig({\n    runName: \"FindDocs\"\n  })\n}\n\nexport const createChatWithXChain = ({\n  llm,\n  question_template,\n  question_llm,\n  retriever,\n  response_template\n}: {\n  llm: BaseLanguageModel\n  question_llm: BaseLanguageModel\n  retriever: Runnable\n  question_template: string\n  response_template: string\n}) => {\n  const retrieverChain = createRetrieverChain(\n    question_llm,\n    retriever,\n    question_template\n  )\n  const context = RunnableMap.from({\n    context: RunnableSequence.from([\n      ({ question, chat_history }) => {\n        return {\n          question: question,\n          chat_history: formatChatHistoryAsString(chat_history)\n        }\n      },\n      retrieverChain,\n      RunnableLambda.from(formatDocs).withConfig({\n        runName: \"FormatDocumentChunks\"\n      })\n    ]),\n    question: RunnableLambda.from(\n      (input: RetrievalChainInput) => input.question\n    ).withConfig({\n      runName: \"Itemgetter:question\"\n    }),\n    chat_history: RunnableLambda.from(\n      (input: RetrievalChainInput) => input.chat_history\n    ).withConfig({\n      runName: \"Itemgetter:chat_history\"\n    })\n  }).withConfig({ tags: [\"RetrieveDocs\"] })\n  const prompt = ChatPromptTemplate.fromMessages([\n    [\"system\", response_template],\n    new MessagesPlaceholder(\"chat_history\"),\n    [\"human\", \"{question}\"]\n  ])\n\n  const responseSynthesizerChain = RunnableSequence.from([\n    prompt,\n    llm,\n    new StringOutputParser()\n  ]).withConfig({\n    tags: [\"GenerateResponse\"]\n  })\n  return RunnableSequence.from([\n    {\n      question: RunnableLambda.from(\n        (input: RetrievalChainInput) => input.question\n      ).withConfig({\n        runName: \"Itemgetter:question\"\n      }),\n      chat_history: RunnableLambda.from(serializeHistory).withConfig({\n        runName: \"SerializeHistory\"\n      })\n    },\n    context,\n    responseSynthesizerChain\n  ])\n}\n"
  },
  {
    "path": "src/components/Common/Beta.tsx",
    "content": "import { Tag } from \"antd\"\nimport { useTranslation } from \"react-i18next\"\n\nexport const BetaTag = ({className} : {className?: string}) => {\n  const { t } = useTranslation(\"common\")\n\n  return <Tag className={className} color=\"yellow\">{t(\"beta\")}</Tag>\n}\n"
  },
  {
    "path": "src/components/Common/CodeBlock.tsx",
    "content": "import { programmingLanguages } from \"@/utils/langauge-extension\"\nimport { Tooltip } from \"antd\"\nimport {\n  CopyCheckIcon,\n  CopyIcon,\n  DownloadIcon,\n  EyeIcon,\n  CodeIcon\n} from \"lucide-react\"\nimport { FC, useState, useRef, useEffect, useCallback } from \"react\"\nimport { useTranslation } from \"react-i18next\"\nimport { Prism as SyntaxHighlighter } from \"react-syntax-highlighter\"\nimport { coldarkDark } from \"react-syntax-highlighter/dist/cjs/styles/prism\"\n// import Mermaid from \"./Mermaid\"\n\ninterface Props {\n  language: string\n  value: string\n}\n\nexport const CodeBlock: FC<Props> = ({ language, value }) => {\n  const [isBtnPressed, setIsBtnPressed] = useState(false)\n  const [previewValue, setPreviewValue] = useState(value)\n  const debounceTimeoutRef = useRef<NodeJS.Timeout | null>(null)\n  \n  const computeKey = () => {\n    const base = `${language}::${value?.slice(0, 200)}`\n    let hash = 0\n    for (let i = 0; i < base.length; i++) {\n      hash = (hash * 31 + base.charCodeAt(i)) >>> 0\n    }\n    return hash.toString(36)\n  }\n  const keyRef = useRef<string>(computeKey())\n  const mapRef = useRef<Map<string, boolean> | null>(null)\n  if (!mapRef.current) {\n    if (typeof window !== \"undefined\") {\n      // @ts-ignore\n      if (!window.__codeBlockPreviewState) {\n        // @ts-ignore\n        window.__codeBlockPreviewState = new Map()\n      }\n      // @ts-ignore\n      mapRef.current = window.__codeBlockPreviewState as Map<string, boolean>\n    } else {\n      mapRef.current = new Map()\n    }\n  }\n  const globalStateMap = mapRef.current!\n  const [showPreview, setShowPreview] = useState<boolean>(\n    () => globalStateMap.get(keyRef.current) || false\n  )\n  const { t } = useTranslation(\"common\")\n\n  const handleCopy = () => {\n    navigator.clipboard.writeText(value)\n    setIsBtnPressed(true)\n    setTimeout(() => {\n      setIsBtnPressed(false)\n    }, 4000)\n  }\n\n  const isPreviewable = [\"html\", \"svg\", \"xml\", \"mathml\"].includes(\n    (language || \"\").toLowerCase()\n  )\n\n  const buildPreviewDoc = useCallback(() => {\n    const code = previewValue || \"\"\n    if ((language || \"\").toLowerCase() === \"svg\") {\n      const hasSvgTag = /<svg[\\s>]/i.test(code)\n      let svgMarkup = hasSvgTag\n        ? code\n        : `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'>${code}</svg>`\n\n      const hasWidthHeight = /\\s(width|height)\\s*=/.test(svgMarkup)\n\n      if (!hasWidthHeight && hasSvgTag) {\n        svgMarkup = svgMarkup.replace(\n          /<svg([^>]*?)>/i,\n          '<svg$1 width=\"100%\" height=\"100%\" style=\"max-width: 100%; max-height: 100%;\">'\n        )\n      }\n\n      return `<!doctype html><html><head><meta charset='utf-8'/><style>html,body{margin:0;padding:0;display:flex;align-items:center;justify-content:center;background:#fff;height:100%;overflow:hidden;}svg{max-width:100%;max-height:100%;}</style></head><body>${svgMarkup}</body></html>`\n    }\n    if ((language || \"\").toLowerCase() === \"mathml\") {\n      const hasMathTag = /<math[\\s>]/i.test(code)\n      let mathMarkup = hasMathTag\n        ? code\n        : `<math xmlns='http://www.w3.org/1998/Math/MathML'>${code}</math>`\n\n      return `<!doctype html><html><head><meta charset='utf-8'/><style>html,body{margin:0;padding:20px;background:#fff;font-family:serif;line-height:1.5;display:flex;align-items:center;justify-content:center;min-height:100vh;}math{font-size:1.5em;}</style></head><body>${mathMarkup}</body></html>`\n    }\n    return `<!doctype html><html><head><meta charset='utf-8'/></head><body>${code}</body></html>`\n  }, [previewValue, language])\n\n  const handleDownload = () => {\n    const blob = new Blob([value], { type: \"text/plain\" })\n    const url = window.URL.createObjectURL(blob)\n    const a = document.createElement(\"a\")\n    a.href = url\n    a.download = `code_${new Date().toISOString().replace(/[:.]/g, \"-\")}.${programmingLanguages[language] || language}`\n    document.body.appendChild(a)\n    a.click()\n    document.body.removeChild(a)\n    window.URL.revokeObjectURL(url)\n  }\n\n  useEffect(() => {\n    globalStateMap.set(keyRef.current, showPreview)\n  }, [showPreview])\n\n  useEffect(() => {\n    if (debounceTimeoutRef.current) {\n      clearTimeout(debounceTimeoutRef.current)\n    }\n    \n    debounceTimeoutRef.current = setTimeout(() => {\n      setPreviewValue(value)\n    }, 300) \n    \n    return () => {\n      if (debounceTimeoutRef.current) {\n        clearTimeout(debounceTimeoutRef.current)\n      }\n    }\n  }, [value])\n\n  useEffect(() => {\n    const newKey = computeKey()\n    if (newKey !== keyRef.current) {\n      keyRef.current = newKey\n      if (globalStateMap.has(newKey)) {\n        const prev = globalStateMap.get(newKey)!\n        if (prev !== showPreview) setShowPreview(prev)\n      }\n    }\n  }, [language, value])\n\n  useEffect(() => {\n    if (!isPreviewable && showPreview) setShowPreview(false)\n  }, [isPreviewable])\n\n  return (\n    <>\n      <div className=\"not-prose\">\n        <div className=\" [&_div+div]:!mt-0 my-4 bg-zinc-950 rounded-xl\">\n          <div className=\"flex flex-row px-4 py-2 rounded-t-xl  gap-3 bg-[#2a2a2a]  \">\n            {isPreviewable && (\n              <div className=\"flex rounded-md overflow-hidden border border-gray-700\">\n                <button\n                  onClick={() => setShowPreview(false)}\n                  className={`px-2 flex items-center gap-1 text-xs transition-colors ${\n                    !showPreview\n                      ? \"bg-gray-700 text-white\"\n                      : \"bg-transparent text-gray-300 hover:bg-gray-700/60\"\n                  }`}\n                  aria-label={t(\"showCode\") || \"Code\"}>\n                  <CodeIcon className=\"size-3\" />\n                </button>\n                <button\n                  onClick={() => setShowPreview(true)}\n                  className={`px-2 flex items-center gap-1 text-xs transition-colors ${\n                    showPreview\n                      ? \"bg-gray-700 text-white\"\n                      : \"bg-transparent text-gray-300 hover:bg-gray-700/60\"\n                  }`}\n                  aria-label={t(\"preview\") || \"Preview\"}>\n                  <EyeIcon className=\"size-3\" />\n                </button>\n              </div>\n            )}\n\n            <span className=\"font-mono text-xs text-white  \">{language || \"text\"}</span>\n          </div>\n          <div className=\"sticky top-9 md:top-[5.75rem]\">\n            <div className=\"absolute bottom-0 right-2 flex h-9 items-center gap-1\">\n              <Tooltip title={t(\"downloadCode\")}>\n                <button\n                  onClick={handleDownload}\n                  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\">\n                  <DownloadIcon className=\"size-4\" />\n                </button>\n              </Tooltip>\n              <Tooltip title={t(\"copyToClipboard\")}>\n                <button\n                  onClick={handleCopy}\n                  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\">\n                  {!isBtnPressed ? (\n                    <CopyIcon className=\"size-4\" />\n                  ) : (\n                    <CopyCheckIcon className=\"size-4 text-green-400\" />\n                  )}\n                </button>\n              </Tooltip>\n            </div>\n          </div>\n\n          {!showPreview && (\n            <SyntaxHighlighter\n              language={language}\n              style={coldarkDark}\n              PreTag=\"div\"\n              customStyle={{\n                margin: 0,\n                width: \"100%\",\n                background: \"transparent\",\n                padding: \"1.5rem 1rem\"\n              }}\n              lineNumberStyle={{\n                userSelect: \"none\"\n              }}\n              codeTagProps={{\n                style: {\n                  fontSize: \"0.9rem\",\n                  fontFamily: \"var(--font-mono)\"\n                }\n              }}>\n              {value}\n            </SyntaxHighlighter>\n          )}\n          {showPreview && isPreviewable && (\n            <div className=\"w-full h-[420px] bg-white rounded-b-xl overflow-hidden border-t border-gray-800\">\n              <iframe\n                title=\"Preview\"\n                srcDoc={buildPreviewDoc()}\n                className=\"w-full h-full border-0\"\n                sandbox=\"allow-scripts allow-same-origin\"\n              />\n            </div>\n          )}\n        </div>\n      </div>\n    </>\n  )\n}\n"
  },
  {
    "path": "src/components/Common/DocumentCard.tsx",
    "content": "import { Spin } from \"antd\"\nimport { FileIcon, Loader2, XIcon } from \"lucide-react\"\n\ntype Props = {\n  name: string\n  onRemove: () => void\n  loading?: boolean\n}\n\nexport const DocumentCard: React.FC<Props> = ({ name, onRemove, loading }) => {\n  return (\n    <button\n      disabled={loading}\n      className=\"relative group p-1.5 w-60 flex items-center gap-1 bg-white dark:bg-[#211e1e] border border-gray-200 dark:border-gray-700 rounded-2xl text-left\"\n      type=\"button\">\n      <div className=\"p-3 bg-black/20 dark:bg-[#2a2a2a] text-white rounded-xl\">\n        {loading ? <Spin size=\"small\" /> : <FileIcon className=\"w-6 h-6\" />}\n      </div>\n      <div className=\"flex flex-col justify-center -space-y-0.5 px-2.5 w-full\">\n        <div className=\"dark:text-gray-200 text-sm font-medium line-clamp-1 mb-1\">\n          {name}\n        </div>\n      </div>\n      <div className=\"absolute -top-1 -right-1\">\n        <button\n          onClick={onRemove}\n          className=\"bg-white dark:bg-gray-800 text-black dark:text-gray-200 border border-gray-50 dark:border-gray-700 rounded-full group-hover:visible invisible transition\"\n          type=\"button\">\n          <XIcon className=\"w-3 h-3\" />\n        </button>\n      </div>\n    </button>\n  )\n}\n"
  },
  {
    "path": "src/components/Common/ImageExport.tsx",
    "content": "import { Message } from \"@/types/message\"\nimport { removeModelSuffix } from \"@/db/dexie/models\"\nimport Markdown from \"./Markdown\"\nimport { Avatar } from \"antd\"\n\nexport const ImageExportWrapper = ({ messages }: { messages: Message[] }) => {\n  return (\n    <div\n      id=\"export-container\"\n      className=\"bg-white dark:bg-[#121212] p-8 max-w-3xl mx-auto\">\n      <div className=\"flex flex-col gap-4\">\n        {messages.map((msg, index) => (\n          <div key={index} className=\"flex flex-row gap-4 md:gap-6 my-4\">\n            {/* Avatar Section */}\n            <div className=\"w-8 flex flex-col relative items-end\">\n              {msg.isBot ? (\n                !msg.modelImage ? (\n                  <div className=\"relative h-7 w-7 p-1 rounded-sm text-white flex items-center justify-center\">\n                    <div className=\"absolute size-6  rounded-full bg-gradient-to-r from-green-300 to-purple-400\"></div>\n                  </div>\n                ) : (\n                  <Avatar\n                    src={msg.modelImage}\n                    alt={msg.name}\n                    className=\"size-6\"\n                  />\n                )\n              ) : (\n                <div className=\"relative size-6 p-1 rounded-sm text-white flex items-center justify-center\">\n                  <div className=\"absolute size-6  rounded-full from-blue-400 to-blue-600 bg-gradient-to-r\"></div>\n                </div>\n              )}\n            </div>\n\n            {/* Message Content */}\n            <div className=\"flex w-[calc(100%-50px)] flex-col gap-2\">\n              <span className=\"text-xs font-bold text-gray-800 dark:text-gray-200\">\n                {msg.isBot\n                  ? removeModelSuffix(\n                      `${msg.modelName || msg.name}`.replaceAll(\n                        /accounts\\/[^\\/]+\\/models\\//g,\n                        \"\"\n                      )\n                    )\n                  : \"You\"}\n              </span>\n\n              <div className=\"prose break-words dark:prose-invert prose-p:leading-relaxed prose-pre:p-0 dark:prose-dark\">\n                <Markdown message={msg.message} />\n                {msg.images &&\n                  msg.images.filter((img) => img.length > 0).length > 0 && (\n                    <div className=\"flex flex-wrap gap-2\">\n                      {msg.images.map((img, index) => (\n                        <img\n                          key={index}\n                          src={img}\n                          alt={`Image ${index + 1}`}\n                          className=\"max-w-full max-h-64 rounded-lg dark:ring-1 dark:ring-gray-700\"\n                        />\n                      ))}\n                    </div>\n                  )}\n              </div>\n            </div>\n          </div>\n        ))}\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Common/Markdown.tsx",
    "content": "import \"katex/dist/katex.min.css\"\n\nimport remarkGfm from \"remark-gfm\"\nimport remarkMath from \"remark-math\"\nimport ReactMarkdown from \"react-markdown\"\nimport rehypeKatex from \"rehype-katex\"\n\nimport \"property-information\"\nimport React from \"react\"\nimport { CodeBlock } from \"./CodeBlock\"\nimport { TableBlock } from \"./TableBlock\"\nimport { preprocessLaTeX } from \"@/utils/latex\"\nimport { useStorage } from \"@plasmohq/storage/hook\"\n\nfunction Markdown({\n  message,\n  className = \"prose break-words dark:prose-invert prose-p:leading-relaxed prose-pre:p-0 dark:prose-dark\"\n}: {\n  message: string\n  className?: string\n}) {\n  const [checkWideMode] = useStorage(\"checkWideMode\", false)\n  if (checkWideMode) {\n    className += \" max-w-none\"\n  }\n  message = preprocessLaTeX(message)\n  return (\n    <React.Fragment>\n      <ReactMarkdown\n        className={className}\n        remarkPlugins={[remarkGfm, remarkMath]}\n        rehypePlugins={[rehypeKatex]}\n        components={{\n          pre({ children }) {\n            return children\n          },\n          code({ node, inline, className, children, ...props }) {\n            const match = /language-(\\w+)/.exec(className || \"\")\n            return !inline ? (\n              <CodeBlock\n                language={match ? match[1] : \"\"}\n                value={String(children).replace(/\\n$/, \"\")}\n              />\n            ) : (\n              <code className={`${className} font-semibold`} {...props}>\n                {children}\n              </code>\n            )\n          },\n          a({ node, ...props }) {\n            return (\n              <a\n                target=\"_blank\"\n                rel=\"noreferrer\"\n                className=\"text-blue-500 text-sm hover:underline\"\n                {...props}>\n                {props.children}\n              </a>\n            )\n          },\n          table({ children,    }) {\n            return <TableBlock>{children}</TableBlock>\n          },\n          p({ children }) {\n            return <p className=\"mb-2 last:mb-0\">{children}</p>\n          }\n        }}>\n        {message}\n      </ReactMarkdown>\n    </React.Fragment>\n  )\n}\n\nexport default Markdown\n"
  },
  {
    "path": "src/components/Common/McpServerToggle.tsx",
    "content": "import { Popover, Switch, Tooltip } from \"antd\"\nimport { useState } from \"react\"\nimport { useQuery, useQueryClient } from \"@tanstack/react-query\"\nimport { useTranslation } from \"react-i18next\"\nimport { useNavigate } from \"react-router-dom\"\nimport { KeyRound, Plus } from \"lucide-react\"\nimport { MCPIcon } from \"@/components/Icons/MCPIcon\"\nimport { getAllMcpServers, updateMcpServer } from \"@/db/dexie/mcp\"\nimport type { McpServer } from \"@/libs/mcp/types\"\nimport { hasValidOAuthTokens } from \"@/libs/mcp/oauth\"\n\nconst getRootDomain = (hostname: string) => {\n  const parts = hostname.split(\".\")\n  if (parts.length <= 2) return hostname\n  return parts.slice(-2).join(\".\")\n}\n\nconst isPrivateHost = (hostname: string) => {\n  if (hostname === \"localhost\" || hostname === \"127.0.0.1\" || hostname === \"0.0.0.0\") return true\n  if (hostname.endsWith(\".local\") || hostname.endsWith(\".internal\")) return true\n  if (/^10\\./.test(hostname) || /^192\\.168\\./.test(hostname)) return true\n  if (/^172\\.(1[6-9]|2\\d|3[01])\\./.test(hostname)) return true\n  if (!hostname.includes(\".\")) return true\n  return false\n}\n\nconst getServerFaviconUrl = (serverUrl: string) => {\n  try {\n    const { hostname } = new URL(serverUrl)\n    if (isPrivateHost(hostname)) return null\n    const domain = getRootDomain(hostname)\n    return `https://www.google.com/s2/favicons?domain=${domain}&sz=32`\n  } catch {\n    return null\n  }\n}\n\nexport { getServerFaviconUrl }\n\nconst ServerFavicon = ({ url }: { url: string }) => {\n  const faviconUrl = getServerFaviconUrl(url)\n\n  if (!faviconUrl) {\n    return <MCPIcon className=\"h-4 w-4 shrink-0 text-gray-400\" />\n  }\n\n  return (\n    <img\n      src={faviconUrl}\n      alt=\"\"\n      className=\"h-4 w-4 shrink-0 rounded-sm\"\n      onError={(e) => {\n        e.currentTarget.style.display = \"none\"\n      }}\n    />\n  )\n}\n\nconst EmptyState = ({\n  t,\n  onNavigate\n}: {\n  t: (key: string) => string\n  onNavigate: () => void\n}) => (\n  <div className=\"flex w-56 flex-col items-center py-4 text-center\">\n    <MCPIcon className=\"h-8 w-8 text-gray-300 dark:text-gray-600\" />\n    <p className=\"mt-2 text-sm font-medium text-gray-700 dark:text-gray-200\">\n      {t(\"tooltip.mcpEmpty\")}\n    </p>\n    <p className=\"mt-1 text-xs text-gray-500 dark:text-gray-400\">\n      {t(\"tooltip.mcpEmptyDesc\")}\n    </p>\n    <button\n      type=\"button\"\n      onClick={onNavigate}\n      className=\"mt-3 inline-flex items-center gap-1.5 rounded-md bg-black px-3 py-1.5 text-xs font-medium text-white hover:bg-gray-800 dark:bg-white dark:text-gray-900 dark:hover:bg-gray-100\">\n      <Plus className=\"h-3 w-3\" />\n      {t(\"tooltip.mcpAddServer\")}\n    </button>\n  </div>\n)\n\nexport const McpServerToggle = () => {\n  const { t } = useTranslation(\"playground\")\n  const queryClient = useQueryClient()\n  const navigate = useNavigate()\n  const [open, setOpen] = useState(false)\n\n  const { data: servers } = useQuery({\n    queryKey: [\"mcpServers\"],\n    queryFn: getAllMcpServers\n  })\n\n  const hasServers = servers && servers.length > 0\n  const enabledCount = hasServers\n    ? servers.filter((s) => s.enabled).length\n    : 0\n\n  const handleToggle = async (server: McpServer, checked: boolean) => {\n    await updateMcpServer({ id: server.id, enabled: checked })\n    queryClient.invalidateQueries({ queryKey: [\"mcpServers\"] })\n  }\n\n  const content = hasServers ? (\n    <div className=\"w-56\">\n      <div className=\"space-y-1\">\n        {servers.map((server) => (\n          <div\n            key={server.id}\n            className=\"flex items-center justify-between rounded-md px-2 py-1.5 hover:bg-gray-50 dark:hover:bg-[#353535]\">\n            <div className=\"flex items-center gap-2 min-w-0 mr-3\">\n              <ServerFavicon url={server.url} />\n              <span\n                className=\"truncate text-sm text-gray-700 dark:text-gray-200\"\n                title={server.name}>\n                {server.name}\n              </span>\n              {server.authType === \"oauth\" && (\n                <Tooltip\n                  title={\n                    hasValidOAuthTokens(server.oauthTokens)\n                      ? \"OAuth connected\"\n                      : \"OAuth not connected\"\n                  }>\n                  <KeyRound\n                    className={`h-3 w-3 shrink-0 ${\n                      hasValidOAuthTokens(server.oauthTokens)\n                        ? \"text-green-500\"\n                        : \"text-orange-400\"\n                    }`}\n                  />\n                </Tooltip>\n              )}\n            </div>\n            <Switch\n              size=\"small\"\n              checked={server.enabled}\n              onChange={(checked) => handleToggle(server, checked)}\n            />\n          </div>\n        ))}\n      </div>\n    </div>\n  ) : (\n    <EmptyState\n      t={t}\n      onNavigate={() => {\n        setOpen(false)\n        navigate(\"/settings/mcp\")\n      }}\n    />\n  )\n\n  return (\n    <Popover\n      content={content}\n      title={hasServers ? t(\"tooltip.mcpServers\") : undefined}\n      trigger=\"click\"\n      open={open}\n      onOpenChange={setOpen}\n      placement=\"topRight\">\n      <button\n        type=\"button\"\n        className=\"relative inline-flex items-center justify-center dark:text-gray-300\">\n        <MCPIcon className=\"h-5 w-5\" />\n        {enabledCount > 0 && (\n          <span className=\"absolute -top-1.5 -right-1.5 flex h-3.5 w-3.5 items-center justify-center rounded-full bg-blue-500 text-[9px] font-medium text-white\">\n            {enabledCount}\n          </span>\n        )}\n      </button>\n    </Popover>\n  )\n}\n"
  },
  {
    "path": "src/components/Common/Mermaid.tsx",
    "content": "import { useEffect, useRef, useState } from \"react\"\r\nimport mermaid from \"mermaid\"\r\n\r\nfunction Mermaid({ code }: { code: string }) {\r\n  const ref = useRef<HTMLDivElement>(null)\r\n  const [hasError, setHasError] = useState(false)\r\n\r\n  useEffect(() => {\r\n    if (code && ref.current) {\r\n      mermaid\r\n        .run({\r\n          nodes: [ref.current],\r\n          suppressErrors: true\r\n        })\r\n        .catch((e) => {\r\n          setHasError(true)\r\n          console.error(\"[Mermaid] \", e.message)\r\n        })\r\n    }\r\n  }, [code])\r\n\r\n  if (hasError) {\r\n    return null\r\n  }\r\n\r\n  return (\r\n    <div\r\n      className=\"mermaid relative w-full h-[80vh] text-center cursor-pointer overflow-auto\"\r\n      ref={ref}>\r\n      {code}\r\n    </div>\r\n  )\r\n}\r\n\r\nexport default Mermaid\r\n"
  },
  {
    "path": "src/components/Common/Message/ReasoningSection.tsx",
    "content": "import React from \"react\"\nimport { Collapse } from \"antd\"\nimport { Brain } from \"lucide-react\"\nimport { useTranslation } from \"react-i18next\"\nimport ReactMarkdown from \"react-markdown\"\nimport remarkGfm from \"remark-gfm\"\nimport remarkMath from \"remark-math\"\nimport rehypeKatex from \"rehype-katex\"\nimport { Prism as SyntaxHighlighter } from \"react-syntax-highlighter\"\nimport { oneDark } from \"react-syntax-highlighter/dist/esm/styles/prism\"\n\nconst { Panel } = Collapse\n\ninterface ReasoningSectionProps {\n  reasoning: string\n  reasoningTime?: number\n}\n\nexport const ReasoningSection: React.FC<ReasoningSectionProps> = ({\n  reasoning,\n  reasoningTime\n}) => {\n  const { t } = useTranslation(\"common\")\n\n  if (!reasoning || reasoning.trim().length === 0) {\n    return null\n  }\n\n  const formatTime = (seconds: number) => {\n    if (seconds < 1) {\n      return `${Math.round(seconds * 1000)}ms`\n    } else if (seconds < 60) {\n      return `${seconds.toFixed(1)}s`\n    } else {\n      const minutes = Math.floor(seconds / 60)\n      const secs = seconds % 60\n      return `${minutes}m ${secs.toFixed(0)}s`\n    }\n  }\n\n  return (\n    <div className=\"mb-4\">\n      <Collapse\n        defaultActiveKey={[]}\n        ghost\n        className=\"bg-gray-50 dark:bg-[#1a1a1a] rounded-lg border border-gray-200 dark:border-[#404040]\">\n        <Panel\n          header={\n            <div className=\"flex items-center gap-2\">\n              <Brain className=\"h-4 w-4 text-blue-600 dark:text-blue-400\" />\n              <span className=\"font-medium text-gray-700 dark:text-gray-300\">\n                {t(\"reasoning.title\")}\n              </span>\n              {reasoningTime && (\n                <span className=\"text-xs text-gray-500 dark:text-gray-400\">\n                  ({formatTime(reasoningTime)})\n                </span>\n              )}\n            </div>\n          }\n          key=\"1\"\n          className=\"reasoning-panel\">\n          <div className=\"text-sm text-gray-600 dark:text-gray-400 italic prose dark:prose-invert max-w-none\">\n            <ReactMarkdown\n              remarkPlugins={[remarkGfm, remarkMath]}\n              rehypePlugins={[rehypeKatex]}\n              components={{\n                code({ node, inline, className, children, ...props }) {\n                  const match = /language-(\\w+)/.exec(className || \"\")\n                  return !inline && match ? (\n                    <SyntaxHighlighter\n                      style={oneDark as any}\n                      language={match[1]}\n                      PreTag=\"div\"\n                      {...props}>\n                      {String(children).replace(/\\n$/, \"\")}\n                    </SyntaxHighlighter>\n                  ) : (\n                    <code className={className} {...props}>\n                      {children}\n                    </code>\n                  )\n                }\n              }}>\n              {reasoning}\n            </ReactMarkdown>\n          </div>\n        </Panel>\n      </Collapse>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Common/ModelSelect.tsx",
    "content": "import { useQuery } from \"@tanstack/react-query\"\nimport { Avatar, Dropdown, Tooltip } from \"antd\"\nimport { LucideBrain } from \"lucide-react\"\nimport React from \"react\"\nimport { useTranslation } from \"react-i18next\"\nimport { fetchChatModels } from \"@/services/ollama\"\nimport { useMessage } from \"@/hooks/useMessage\"\nimport { ProviderIcons } from \"./ProviderIcon\"\n\ntype Props = {\n  iconClassName?: string\n}\n\nexport const ModelSelect: React.FC<Props> = ({iconClassName = \"size-5\"}) => {\n  const { t } = useTranslation(\"common\")\n  const { setSelectedModel, selectedModel } = useMessage()\n  const { data } = useQuery({\n    queryKey: [\"getAllModelsForSelect\"],\n    queryFn: async () => {\n      const models = await fetchChatModels({ returnEmpty: false })\n      return models\n    }\n  })\n\n  return (\n    <>\n      {data && data.length > 0 && (\n        <Dropdown\n          menu={{\n            items:\n              data?.map((d) => ({\n                key: d.name,\n                label: (\n                  <div className=\"w-52 gap-2 text-lg truncate inline-flex line-clamp-3  items-center  dark:border-gray-700\">\n                    <div>\n                      {d.avatar ? (\n                        <Avatar src={d.avatar} alt={d.name} size=\"small\" />\n                      ) : (\n                        <ProviderIcons\n                          provider={d?.provider}\n                          className=\"h-6 w-6 text-gray-400\"\n                        />\n                      )}\n                    </div>\n                    {d?.nickname || d.model}\n                  </div>\n                ),\n                onClick: () => {\n                  if (selectedModel === d.model) {\n                    setSelectedModel(null)\n                  } else {\n                    setSelectedModel(d.model)\n                  }\n                }\n              })) || [],\n            style: {\n              maxHeight: 500,\n              overflowY: \"scroll\"\n            },\n            className: \"no-scrollbar\",\n            activeKey: selectedModel\n          }}\n          placement={\"topLeft\"}\n          trigger={[\"click\"]}>\n          <Tooltip title={t(\"selectAModel\")}>\n            <button type=\"button\" className=\"dark:text-gray-300\">\n              <LucideBrain className={iconClassName} />\n            </button>\n          </Tooltip>\n        </Dropdown>\n      )}\n    </>\n  )\n}\n"
  },
  {
    "path": "src/components/Common/PageAssistLoader.tsx",
    "content": "\nexport const PageAssistLoader = () => {\n  return (\n    <div className=\"fixed bg-[#1a1a1a] top-0 left-0 right-0 bottom-0 w-full h-screen z-50 overflow-hidden opacity-75 flex flex-col items-center justify-center\">\n      <p className=\"text-center text-white text-lg mt-4\">\n        Loading...\n      </p>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Common/PageAssistProvider.tsx",
    "content": "import { PageAssistContext } from \"@/context\"\nimport { Message } from \"@/types/message\"\nimport React from \"react\"\n\nexport const PageAssistProvider = ({\n  children\n}: {\n  children: React.ReactNode\n}) => {\n  const [messages, setMessages] = React.useState<Message[]>([])\n  const [controller, setController] = React.useState<AbortController | null>(\n    null\n  )\n  const [embeddingController, setEmbeddingController] =\n    React.useState<AbortController | null>(null)\n\n  return (\n    <PageAssistContext.Provider\n      value={{\n        messages,\n        setMessages,\n\n        controller,\n        setController,\n\n        embeddingController,\n        setEmbeddingController\n      }}>\n      {children}\n    </PageAssistContext.Provider>\n  )\n}\n"
  },
  {
    "path": "src/components/Common/Playground/ActionInfo.tsx",
    "content": "import { useTranslation } from \"react-i18next\"\nimport { ChatActionInfo } from \"@/libs/mcp/types\"\n\ntype Props = {\n  action: ChatActionInfo\n}\n\n\nexport const ActionInfo = ({action}: Props) => {\n  const {t} = useTranslation('common')\n  if (typeof action === \"string\") {\n    return (\n      <div className=\"shimmer-text text-[16px]\">\n        {t(action)}\n      </div>\n    )\n  }\n\n  if (action.type === \"mcp\") {\n    return (\n      <div className=\"shimmer-text text-[16px]\">\n        {t(`mcp.action.${action.phase}`, {\n          tool: action.toolName || t(\"mcp.tool\"),\n          server: action.serverName || t(\"mcp.server\"),\n          count: action.toolCount || 0\n        })}\n      </div>\n    )\n  }\n\n  return (\n      <div className=\"shimmer-text text-[16px]\">\n        {t(\"pageAssist\")}\n      </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Common/Playground/DocumentChip.tsx",
    "content": "import React from \"react\"\nimport { Globe } from \"lucide-react\"\n\ninterface DocumentChipProps {\n  document: {\n    title: string\n    url: string\n    favIconUrl?: string\n  }\n}\n\nexport const DocumentChip: React.FC<DocumentChipProps> = ({ document }) => {\n  return (\n    <a\n      href={document.url}\n      target=\"_blank\"\n      rel=\"noreferrer\"\n      className=\"inline-flex items-center gap-2 bg-neutral-50 dark:bg-[#262626] border border-neutral-200 dark:border-[#2a2a2a] rounded-2xl px-3 py-1.5 mr-2 mb-2\">\n      <div className=\"flex items-center gap-2 flex-1 min-w-0\">\n        <div className=\"flex-shrink-0\">\n          {document.favIconUrl ? (\n            <img\n              src={document.favIconUrl}\n              alt=\"\"\n              className=\"w-4 h-4 rounded\"\n              onError={(e) => {\n                const target = e.target as HTMLImageElement\n                target.style.display = \"none\"\n                target.nextElementSibling?.classList.remove(\"hidden\")\n              }}\n            />\n          ) : null}\n          <Globe\n            className={`w-4 h-4 text-neutral-600 dark:text-neutral-400 ${document.favIconUrl ? \"hidden\" : \"\"}`}\n          />\n        </div>\n        <div className=\"flex flex-col max-w-60 truncate\">\n          <span className=\"text-sm font-medium text-neutral-800 dark:text-neutral-200 \">\n            {document.title}\n          </span>\n          <span className=\"text-xs text-neutral-600 dark:text-neutral-400 \">\n            {document.url}\n          </span>\n        </div>{\" \"}\n      </div>\n    </a>\n  )\n}\n"
  },
  {
    "path": "src/components/Common/Playground/DocumentFile.tsx",
    "content": "import React from \"react\"\nimport { FileIcon, Globe } from \"lucide-react\"\nimport { formatFileSize } from \"@/utils/format-file-size\"\n\ninterface DocumentFileProps {\n  document: {\n    filename: string\n    fileSize: number\n  }\n}\n\nexport const DocumentFile: React.FC<DocumentFileProps> = ({ document }) => {\n  return (\n    <button\n      className=\"relative group p-1.5 w-80 flex items-center gap-1 bg-white dark:bg-[#262626] border border-gray-200 dark:border-white/5 rounded-2xl text-left\"\n      type=\"button\">\n      <div className=\"p-3 bg-black/20 dark:bg-white/10 text-white rounded-xl\">\n        <FileIcon className=\"size-5\" />\n      </div>\n      <div className=\"flex flex-col justify-center -space-y-0.5 px-2.5 w-full\">\n        <div className=\"dark:text-gray-100 text-sm font-medium line-clamp-1 mb-1\">\n          {document.filename}\n        </div>\n        <div className=\"flex justify-between text-gray-500 text-xs line-clamp-1\">\n          File{\" \"}\n          <span className=\"capitalize\">\n            {formatFileSize(document.fileSize)}\n          </span>\n        </div>\n      </div>\n    </button>\n  )\n}\n"
  },
  {
    "path": "src/components/Common/Playground/EditMessageForm.tsx",
    "content": "import { useForm } from \"@mantine/form\"\nimport React from \"react\"\nimport { useTranslation } from \"react-i18next\"\nimport useDynamicTextareaSize from \"~/hooks/useDynamicTextareaSize\"\n\ntype Props = {\n  value: string\n  onSumbit: (value: string, isSend: boolean) => void\n  onClose: () => void\n  isBot: boolean\n}\n\nexport const EditMessageForm = (props: Props) => {\n  const [isComposing, setIsComposing] = React.useState(false)\n  const textareaRef = React.useRef<HTMLTextAreaElement>(null)\n  const { t } = useTranslation(\"common\")\n\n  const form = useForm({\n    initialValues: {\n      message: props.value\n    }\n  })\n  useDynamicTextareaSize(textareaRef, form.values.message, 300)\n\n  React.useEffect(() => {\n    form.setFieldValue(\"message\", props.value)\n  }, [props.value])\n\n  return (\n    <form\n      onSubmit={form.onSubmit((data) => {\n        if (isComposing) return\n        props.onClose()\n        props.onSumbit(data.message, true)\n      })}\n      className=\"flex flex-col gap-2\">\n      <textarea\n        {...form.getInputProps(\"message\")}\n        onCompositionStart={() => {\n          if (import.meta.env.BROWSER !== \"firefox\") {\n            setIsComposing(true)\n          }\n        }}\n        onCompositionEnd={() => {\n          if (import.meta.env.BROWSER !== \"firefox\") {\n            setIsComposing(false)\n          }\n        }}\n        required\n        rows={1}\n        style={{ minHeight: \"60px\" }}\n        tabIndex={0}\n        placeholder={t(\"editMessage.placeholder\")}\n        ref={textareaRef}\n        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\"\n      />\n      <div className=\"flex flex-wrap gap-2 mt-2\">\n        <div\n          className={`w-full flex ${\n            !props.isBot ? \"justify-between\" : \"justify-end\"\n          }`}>\n          {!props.isBot && (\n            <button\n              type=\"button\"\n              onClick={() => {\n                props.onSumbit(form.values.message, false)\n                props.onClose()\n              }}\n              aria-label={t(\"save\")}\n              className=\"border border-gray-600 px-2 py-1.5 rounded-lg 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 text-sm\">\n              {t(\"save\")}\n            </button>\n          )}\n          <div className=\"flex space-x-2\">\n            <button\n              aria-label={t(\"save\")}\n              className=\"bg-black px-2 py-1.5 rounded-lg text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-gray-500 hover:bg-gray-900 text-sm\">\n              {props.isBot ? t(\"save\") : t(\"saveAndSubmit\")}\n            </button>\n\n            <button\n              onClick={props.onClose}\n              aria-label={t(\"cancel\")}\n              className=\"border dark:border-gray-600 px-2 py-1.5 rounded-lg 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 text-sm\">\n              {t(\"cancel\")}\n            </button>\n          </div>\n        </div>\n      </div>{\" \"}\n    </form>\n  )\n}\n"
  },
  {
    "path": "src/components/Common/Playground/GenerationInfo.tsx",
    "content": "type GenerationMetrics = {\n  total_duration?: number\n  load_duration?: number\n  prompt_eval_count?: number\n  prompt_eval_duration?: number\n  eval_count?: number\n  eval_duration?: number\n  context?: string\n  response?: string\n}\n\ntype Props = {\n  generationInfo: GenerationMetrics\n}\n\nexport const GenerationInfo = ({ generationInfo }: Props) => {\n  if (!generationInfo) return null\n\n  const calculateTokensPerSecond = (\n    evalCount?: number,\n    evalDuration?: number\n  ) => {\n    if (!evalCount || !evalDuration) return 0\n    return (evalCount / evalDuration) * 1e9\n  }\n\n  const formatDuration = (nanoseconds?: number) => {\n    if (!nanoseconds) return \"0ms\"\n    const ms = nanoseconds / 1e6\n    if (ms < 1) return `${ms.toFixed(3)}ms`\n    if (ms < 1000) return `${Math.round(ms)}ms`\n    return `${(ms / 1000).toFixed(2)}s`\n  }\n\n  const metricsToDisplay = {\n    ...generationInfo,\n    ...(generationInfo?.eval_count && generationInfo?.eval_duration\n      ? {\n          tokens_per_second: calculateTokensPerSecond(\n            generationInfo.eval_count,\n            generationInfo.eval_duration\n          ).toFixed(2)\n        }\n      : {})\n  }\n\n  return (\n    <div className=\"p-2 w-full\">\n      <div className=\"flex flex-col gap-2\">\n        {Object.entries(metricsToDisplay)\n          .filter(([key]) => key !== \"model\")\n          .map(([key, value]) => (\n            <div key={key} className=\"flex flex-wrap justify-between\">\n              <div className=\"font-medium text-xs\">{key}</div>\n              <div className=\"font-medium text-xs break-all\">\n                {key.includes(\"duration\")\n                  ? formatDuration(value as number)\n                  : String(value)}\n              </div>\n            </div>\n          ))}\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Common/Playground/HumanMessge.tsx",
    "content": "import { useStorage } from \"@plasmohq/storage/hook\"\nimport { useState } from \"react\"\nimport Markdown from \"../Markdown\"\n\ntype Props = {\n  message: string\n  isNormalMessage?: boolean\n}\n\nconst MAX_MESSAGE_LENGTH = 500 \n\nexport const HumanMessage = ({ message }: Props) => {\n  const [useMarkdownForUserMessage] = useStorage(\"useMarkdownForUserMessage\", false)\n  const [showMoreForLargeMessage] = useStorage(\"showMoreForLargeMessage\", false)\n  const [isExpanded, setIsExpanded] = useState(false)\n\n  const shouldTruncate = showMoreForLargeMessage && message.length > MAX_MESSAGE_LENGTH\n\n  if (useMarkdownForUserMessage) {\n    if (shouldTruncate && !isExpanded) {\n      const truncatedMessage = message.slice(0, MAX_MESSAGE_LENGTH) + \"...\"\n      return (\n        <div>\n          <div className=\"relative\">\n            <Markdown message={truncatedMessage} />\n            <div className=\"absolute bottom-0 left-0 right-0 h-12 bg-gradient-to-t from-gray-50 dark:from-[#242424] to-transparent pointer-events-none\" />\n          </div>\n          <button\n            onClick={() => setIsExpanded(true)}\n            className=\"text-gray-700 dark:text-neutral-50 hover:text-gray-900 dark:hover:text-white text-sm mt-2  font-medium shadow-sm hover:shadow-md transition-all\">\n            Show more\n          </button>\n        </div>\n      )\n    }\n    return (\n      <div>\n        <Markdown message={message} />\n        {shouldTruncate && isExpanded && (\n          <button\n            onClick={() => setIsExpanded(false)}\n            className=\"text-gray-700 dark:text-neutral-50 hover:text-gray-900 dark:hover:text-white text-sm mt-2  font-medium shadow-sm hover:shadow-md transition-all\">\n            Show less\n          </button>\n        )}\n      </div>\n    )\n  }\n\n  if (shouldTruncate && !isExpanded) {\n    const truncatedMessage = message.slice(0, MAX_MESSAGE_LENGTH) + \"...\"\n    return (\n      <div>\n        <div className=\"relative\">\n          <span className=\"whitespace-pre-wrap\">{truncatedMessage}</span>\n          <div className=\"absolute bottom-0 left-0 right-0 h-12 bg-gradient-to-t from-gray-50 dark:from-[#242424] to-transparent pointer-events-none\" />\n        </div>\n        <button\n          onClick={() => setIsExpanded(true)}\n          className=\"text-gray-700 dark:text-neutral-50 hover:text-gray-900 dark:hover:text-white text-sm mt-2 block  font-medium shadow-sm hover:shadow-md transition-all\">\n          Show more\n        </button>\n      </div>\n    )\n  }\n\n  return (\n    <div>\n      <span className=\"whitespace-pre-wrap\">{message}</span>\n      {shouldTruncate && isExpanded && (\n        <button\n          onClick={() => setIsExpanded(false)}\n          className=\"text-gray-700 dark:text-neutral-50 hover:text-gray-900 dark:hover:text-white text-sm mt-2 block  font-medium shadow-sm hover:shadow-md transition-all\">\n          Show less\n        </button>\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Common/Playground/McpInvocationBlock.tsx",
    "content": "import {\n  CheckCircle2,\n  ChevronDown,\n  ChevronRight,\n  Loader2,\n  Wrench,\n  XCircle\n} from \"lucide-react\"\nimport React from \"react\"\nimport { useTranslation } from \"react-i18next\"\nimport Markdown from \"../Markdown\"\nimport { PlaygroundToolInvocation } from \"./message-groups\"\n\nconst OUTPUT_CLAMP_HEIGHT = 200\nconst MAX_PREVIEW_CHARS = 2000\n\ntype Props = {\n  invocation: PlaygroundToolInvocation\n}\n\nconst formatArgs = (args: unknown): string => {\n  if (!args || (typeof args === \"object\" && Object.keys(args).length === 0)) {\n    return \"\"\n  }\n\n  try {\n    return JSON.stringify(args, null, 2)\n  } catch {\n    return String(args)\n  }\n}\n\nconst ToolOutput = ({\n  content,\n  noOutputLabel,\n  outputLabel\n}: {\n  content: string\n  noOutputLabel: string\n  outputLabel: string\n}) => {\n  const contentRef = React.useRef<HTMLDivElement>(null)\n  const [isExpanded, setIsExpanded] = React.useState(false)\n  const rawContent = content.trim().length > 0 ? content : noOutputLabel\n  const isTruncatable = rawContent.length > MAX_PREVIEW_CHARS\n  const displayContent = !isExpanded && isTruncatable\n    ? rawContent.slice(0, MAX_PREVIEW_CHARS)\n    : rawContent\n  const [isHeightClamped, setIsHeightClamped] = React.useState(false)\n\n  React.useEffect(() => {\n    const el = contentRef.current\n    if (el) {\n      setIsHeightClamped(el.scrollHeight > OUTPUT_CLAMP_HEIGHT)\n    }\n  }, [displayContent])\n\n  const showToggle = isTruncatable || isHeightClamped\n\n  return (\n    <div>\n      <p className=\"mb-1 text-xs font-medium text-gray-500 dark:text-gray-400\">\n        {outputLabel}\n      </p>\n      <div\n        ref={contentRef}\n        className=\"relative overflow-hidden rounded-lg bg-gray-50 px-3 py-2 text-sm text-gray-700 dark:bg-white/5 dark:text-gray-300\"\n        style={\n          !isExpanded && isHeightClamped\n            ? { maxHeight: OUTPUT_CLAMP_HEIGHT }\n            : undefined\n        }>\n        <Markdown message={displayContent} />\n        {!isExpanded && isHeightClamped && (\n          <div className=\"pointer-events-none absolute inset-x-0 bottom-0 h-12 bg-gradient-to-t from-gray-50 dark:from-[#1a1a1a]\" />\n        )}\n      </div>\n      {showToggle && (\n        <button\n          type=\"button\"\n          onClick={() => setIsExpanded((v) => !v)}\n          className=\"mt-1 text-xs font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300\">\n          {isExpanded ? \"Show less\" : \"Show more\"}\n        </button>\n      )}\n    </div>\n  )\n}\n\nexport const McpInvocationBlock = ({ invocation }: Props) => {\n  const { t } = useTranslation(\"common\")\n  const [isOpen, setIsOpen] = React.useState(\n    false\n  )\n\n  const hasResult = Boolean(invocation.result)\n  const isError = Boolean(invocation.result?.toolError)\n  const formattedArgs = formatArgs(invocation.args)\n\n  return (\n    <div className=\"rounded-xl border border-gray-200/80 dark:border-white/10\">\n      <button\n        type=\"button\"\n        onClick={() => setIsOpen((v) => !v)}\n        className=\"flex w-full items-center gap-2 px-3 py-2 text-sm text-gray-700 transition-colors hover:bg-gray-50 dark:text-gray-200 dark:hover:bg-white/5 rounded-xl\">\n        <span className=\"flex size-5 shrink-0 items-center justify-center rounded-full border border-gray-200 text-gray-500 dark:border-white/10 dark:text-gray-400\">\n          <Wrench className=\"size-3\" />\n        </span>\n        <span className=\"min-w-0 flex-1 text-left font-medium truncate\">\n          {invocation.displayName}\n        </span>\n        {invocation.serverName && (\n          <span className=\"hidden sm:inline shrink-0 text-xs text-gray-400 dark:text-gray-500\">\n            {invocation.serverName}\n          </span>\n        )}\n        <span className=\"shrink-0\">\n          {hasResult ? (\n            isError ? (\n              <XCircle className=\"size-3.5 text-red-500\" />\n            ) : (\n              <CheckCircle2 className=\"size-3.5 text-green-500\" />\n            )\n          ) : (\n            <Loader2 className=\"size-3.5 animate-spin text-gray-400\" />\n          )}\n        </span>\n        {isOpen ? (\n          <ChevronDown className=\"size-3.5 shrink-0 text-gray-400\" />\n        ) : (\n          <ChevronRight className=\"size-3.5 shrink-0 text-gray-400\" />\n        )}\n      </button>\n\n      {isOpen && (\n        <div className=\"border-t border-gray-200/80 px-3 py-2.5 dark:border-white/10\">\n          {formattedArgs.length > 0 && (\n            <div className=\"mb-2.5\">\n              <p className=\"mb-1 text-xs font-medium text-gray-500 dark:text-gray-400\">\n                {t(\"mcp.arguments\")}\n              </p>\n              <pre className=\"overflow-x-auto rounded-lg bg-gray-50 px-3 py-2 text-xs text-gray-700 dark:bg-white/5 dark:text-gray-300\">\n                {formattedArgs}\n              </pre>\n            </div>\n          )}\n\n          {invocation.result && (\n            <ToolOutput\n              content={invocation.result.content}\n              noOutputLabel={t(\"mcp.noOutput\")}\n              outputLabel={t(\"mcp.output\")}\n            />\n          )}\n        </div>\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Common/Playground/Message.tsx",
    "content": "import Markdown from \"../../Common/Markdown\"\nimport React, { useEffect } from \"react\"\nimport { Tag, Image, Tooltip, Collapse, Popover, Avatar } from \"antd\"\nimport { ActionInfo } from \"./ActionInfo\"\nimport {\n  CheckIcon,\n  CopyIcon,\n  GitBranchIcon,\n  InfoIcon,\n  Pen,\n  PlayCircle,\n  RotateCcw,\n  Square,\n  Volume2Icon\n} from \"lucide-react\"\nimport { EditMessageForm } from \"./EditMessageForm\"\nimport { useTranslation } from \"react-i18next\"\nimport { MessageSource } from \"./MessageSource\"\nimport { useTTS } from \"@/hooks/useTTS\"\nimport { tagColors } from \"@/utils/color\"\nimport { removeModelSuffix } from \"@/db/dexie/models\"\nimport { GenerationInfo } from \"./GenerationInfo\"\nimport { parseReasoning } from \"@/libs/reasoning\"\nimport { humanizeMilliseconds } from \"@/utils/humanize-milliseconds\"\nimport { useStorage } from \"@plasmohq/storage/hook\"\nimport { PlaygroundUserMessageBubble } from \"./PlaygroundUserMessage\"\nimport { copyToClipboard } from \"@/utils/clipboard\"\nimport { ChatDocuments } from \"@/models/ChatTypes\"\nimport { ChatActionInfo, ChatMessageKind, McpToolCall } from \"@/libs/mcp/types\"\nimport { isTraceMessageKind } from \"@/libs/mcp/utils\"\nimport {\n  PlaygroundMessageSegment,\n  PlaygroundToolInvocation\n} from \"./message-groups\"\nimport { McpInvocationBlock } from \"./McpInvocationBlock\"\n\nconst messageRenderStyle: React.CSSProperties = {\n  contentVisibility: \"auto\",\n  containIntrinsicSize: \"220px\"\n}\n\ntype Props = {\n  message: string\n  message_type?: string\n  hideCopy?: boolean\n  botAvatar?: JSX.Element\n  userAvatar?: JSX.Element\n  isBot: boolean\n  name: string\n  images?: string[]\n  isLastMessage: boolean\n  actionIndex: number\n  onRengerate?: () => void\n  onEditFormSubmit: (\n    messageIndex: number,\n    isHuman: boolean,\n    value: string,\n    isSend: boolean\n  ) => void\n  isProcessing: boolean\n  webSearch?: {}\n  isSearchingInternet?: boolean\n  sources?: any[]\n  hideEditAndRegenerate?: boolean\n  hideContinue?: boolean\n  onSourceClick?: (source: any) => void\n  isTTSEnabled?: boolean\n  generationInfo?: any\n  isStreaming: boolean\n  reasoningTimeTaken?: number\n  openReasoning?: boolean\n  modelImage?: string\n  modelName?: string\n  onContinue?: () => void\n  documents?: ChatDocuments\n  actionInfo?: ChatActionInfo | null\n  onNewBranch?: (messageIndex: number) => void\n  temporaryChat?: boolean\n  messageKind?: ChatMessageKind\n  toolCalls?: McpToolCall[]\n  toolCallId?: string\n  toolName?: string\n  toolServerName?: string\n  toolError?: boolean\n  segments?: PlaygroundMessageSegment[]\n}\n\nconst hasStandaloneAssistantText = (segments?: PlaygroundMessageSegment[]) =>\n  (segments || []).some(\n    (segment) =>\n      segment.type === \"text\" && segment.message.message.trim().length > 0\n  )\n\nconst getPrimaryAssistantText = (\n  message: string,\n  segments?: PlaygroundMessageSegment[]\n) => {\n  const textSegment = [...(segments || [])]\n    .reverse()\n    .find(\n      (segment) =>\n        segment.type === \"text\" && segment.message.message.trim().length > 0\n    )\n\n  if (textSegment?.type === \"text\") {\n    return textSegment.message.message\n  }\n\n  return segments ? \"\" : message\n}\n\nconst renderAssistantText = ({\n  keyPrefix,\n  message,\n  isStreaming,\n  openReasoning,\n  hideReasoningWidget,\n  reasoningTimeTaken,\n  t\n}: {\n  keyPrefix: string\n  message: string\n  isStreaming: boolean\n  openReasoning?: boolean\n  hideReasoningWidget: boolean\n  reasoningTimeTaken?: number\n  t: (key: string, options?: any) => string\n}) =>\n  parseReasoning(message).map((entry, index) => {\n    if (entry.type === \"reasoning\" && !hideReasoningWidget) {\n      return (\n        <Collapse\n          key={`${keyPrefix}-reasoning-${index}`}\n          className=\"border-none text-gray-500 dark:text-gray-400 !mb-3 \"\n          defaultActiveKey={openReasoning ? \"reasoning\" : undefined}\n          items={[\n            {\n              key: \"reasoning\",\n              label:\n                isStreaming && entry?.reasoning_running ? (\n                  <div className=\"flex items-center gap-2\">\n                    <span className=\"italic shimmer-text\">\n                      {t(\"reasoning.thinking\")}\n                    </span>\n                  </div>\n                ) : (\n                  t(\"reasoning.thought\", {\n                    time: humanizeMilliseconds(reasoningTimeTaken)\n                  })\n                ),\n              children: <Markdown message={entry.content} />\n            }\n          ]}\n        />\n      )\n    }\n\n    return <Markdown key={`${keyPrefix}-content-${index}`} message={entry.content} />\n  })\n\nconst McpInvocationGroup = ({\n  content,\n  invocations,\n  isStreaming,\n  openReasoning,\n  hideReasoningWidget,\n  t\n}: {\n  content: string\n  invocations: PlaygroundToolInvocation[]\n  isStreaming: boolean\n  openReasoning?: boolean\n  hideReasoningWidget: boolean\n  t: (key: string, options?: any) => string\n}) => (\n  <div className=\"space-y-3  dark:border-white/10\">\n    {content.trim().length > 0 && (\n      <div className=\"space-y-3\">\n        {renderAssistantText({\n          keyPrefix: `tool-content-${content.length}`,\n          message: content,\n          isStreaming,\n          openReasoning,\n          hideReasoningWidget,\n          t\n        })}\n      </div>\n    )}\n\n    <div className=\"space-y-4\">\n      {invocations.map((invocation) => (\n        <McpInvocationBlock key={invocation.id} invocation={invocation} />\n      ))}\n    </div>\n  </div>\n)\n\nconst PlaygroundMessageComponent = (props: Props) => {\n  const [isBtnPressed, setIsBtnPressed] = React.useState(false)\n  const [editMode, setEditMode] = React.useState(false)\n  const [checkWideMode] = useStorage(\"checkWideMode\", false)\n  const [isUserChatBubble] = useStorage(\"userChatBubble\", true)\n  const [hideReasoningWidget] = useStorage(\"hideReasoningWidget\", false)\n  const [autoCopyResponseToClipboard] = useStorage(\n    \"autoCopyResponseToClipboard\",\n    false\n  )\n  const [autoPlayTTS] = useStorage(\"isTTSAutoPlayEnabled\", false)\n  const [copyAsFormattedText] = useStorage(\"copyAsFormattedText\", false)\n  const { t } = useTranslation(\"common\")\n  const { cancel, isSpeaking, speak } = useTTS()\n  const hasSegmentedAssistantText = hasStandaloneAssistantText(props.segments)\n  const isTraceOnly = props.isBot\n    ? props.segments\n      ? !hasSegmentedAssistantText\n      : isTraceMessageKind(props.messageKind)\n    : false\n  const primaryAssistantText = getPrimaryAssistantText(\n    props.message,\n    props.segments\n  )\n  const copyableMessage = props.isBot ? primaryAssistantText : props.message\n\n  const autoCopyToClipboard = async () => {\n    if (\n      autoCopyResponseToClipboard &&\n      props.isBot &&\n      !isTraceOnly &&\n      props.isLastMessage &&\n      !props.isStreaming &&\n      !props.isProcessing &&\n      copyableMessage.trim().length > 0\n    ) {\n      await copyToClipboard({\n        text: copyableMessage,\n        formatted: copyAsFormattedText\n      })\n      setIsBtnPressed(true)\n      setTimeout(() => {\n        setIsBtnPressed(false)\n      }, 2000)\n    }\n  }\n\n  useEffect(() => {\n    autoCopyToClipboard()\n  }, [\n    autoCopyResponseToClipboard,\n    props.isBot,\n    isTraceOnly,\n    props.isLastMessage,\n    props.isStreaming,\n    props.isProcessing,\n    copyableMessage\n  ])\n\n  useEffect(() => {\n    if (\n      autoPlayTTS &&\n      props.isTTSEnabled &&\n      props.isBot &&\n      !isTraceOnly &&\n      props.isLastMessage &&\n      !props.isStreaming &&\n      !props.isProcessing &&\n      copyableMessage.trim().length > 0\n    ) {\n      speak({\n        utterance: copyableMessage\n      })\n    }\n  }, [\n    autoPlayTTS,\n    props.isTTSEnabled,\n    props.isBot,\n    isTraceOnly,\n    props.isLastMessage,\n    props.isStreaming,\n    props.isProcessing,\n    copyableMessage\n  ])\n\n  if (isUserChatBubble && !props.isBot) {\n    return <PlaygroundUserMessageBubble {...props} />\n  }\n\n  return (\n    <div\n      className={`group relative flex w-full max-w-3xl flex-col items-end justify-center pb-2 text-gray-800 dark:text-gray-100 md:px-4 lg:w-4/5 ${checkWideMode ? \"max-w-none\" : \"\"}`}\n      style={messageRenderStyle}>\n      <div className=\"m-auto my-2 flex w-full flex-row gap-4 md:gap-6\">\n        <div className=\"relative flex w-8 flex-col items-end\">\n          {props.isBot ? (\n            !props.modelImage ? (\n              <div className=\"relative flex h-7 w-7 items-center justify-center rounded-sm p-1 text-white text-opacity-100\">\n                <div className=\"absolute h-8 w-8 rounded-full bg-gradient-to-r from-green-300 to-purple-400\"></div>\n              </div>\n            ) : (\n              <Avatar\n                src={props.modelImage}\n                alt={props.name}\n                className=\"size-8\"\n              />\n            )\n          ) : !props.userAvatar ? (\n            <div className=\"relative flex h-7 w-7 items-center justify-center rounded-sm p-1 text-white text-opacity-100\">\n              <div className=\"absolute h-8 w-8 rounded-full bg-gradient-to-r from-blue-400 to-blue-600\"></div>\n            </div>\n          ) : (\n            props.userAvatar\n          )}\n        </div>\n\n        <div className=\"flex w-[calc(100%-50px)] flex-col gap-2 lg:w-[calc(100%-115px)]\">\n          <span className=\"text-xs font-bold text-gray-800 dark:text-white\">\n            {props.isBot\n              ? props.name === \"chrome::gemini-nano::page-assist\"\n                ? \"Gemini Nano\"\n                : removeModelSuffix(\n                    `${props?.modelName || props?.name}`?.replaceAll(\n                      /accounts\\/[^\\/]+\\/models\\//g,\n                      \"\"\n                    )\n                  )\n              : \"You\"}\n          </span>\n\n          {props.isBot && props.isSearchingInternet && props.isLastMessage ? (\n            <ActionInfo action={\"webSearch\"} />\n          ) : null}\n          {props.isBot && props.actionInfo && props.isLastMessage ? (\n            <ActionInfo action={props.actionInfo} />\n          ) : null}\n\n          <div>\n            {props?.message_type && (\n              <Tag color={tagColors[props?.message_type] || \"default\"}>\n                {t(`copilot.${props?.message_type}`)}\n              </Tag>\n            )}\n          </div>\n\n          <div className=\"flex flex-grow flex-col gap-4\">\n            {!editMode ? (\n              props.isBot ? (\n                props.segments && props.segments.length > 0 ? (\n                  props.segments.map((segment) => {\n                    if (segment.type === \"text\") {\n                      return (\n                        <div key={segment.key} className=\"space-y-3\">\n                          {renderAssistantText({\n                            keyPrefix: segment.key,\n                            message: segment.message.message,\n                            isStreaming: props.isStreaming,\n                            openReasoning: props.openReasoning,\n                            hideReasoningWidget,\n                            reasoningTimeTaken:\n                              segment.message.reasoning_time_taken,\n                            t\n                          })}\n                        </div>\n                      )\n                    }\n\n                    return (\n                      <McpInvocationGroup\n                        key={segment.key}\n                        content={segment.content}\n                        invocations={segment.invocations}\n                        isStreaming={props.isStreaming}\n                        openReasoning={props.openReasoning}\n                        hideReasoningWidget={hideReasoningWidget}\n                        t={t}\n                      />\n                    )\n                  })\n                ) : (\n                  renderAssistantText({\n                    keyPrefix: \"assistant\",\n                    message: props.message,\n                    isStreaming: props.isStreaming,\n                    openReasoning: props.openReasoning,\n                    hideReasoningWidget,\n                    reasoningTimeTaken: props.reasoningTimeTaken,\n                    t\n                  })\n                )\n              ) : (\n                <p\n                  className={`prose whitespace-pre-line text-sm prose-p:leading-relaxed prose-pre:p-0 dark:prose-invert dark:prose-dark ${\n                    props.message_type &&\n                    \"italic text-sm text-gray-500 dark:text-gray-400\"\n                  } ${checkWideMode ? \"max-w-none\" : \"\"}`}>\n                  {props.message}\n                </p>\n              )\n            ) : (\n              <EditMessageForm\n                value={copyableMessage}\n                onSumbit={(value, isSend) =>\n                  props.onEditFormSubmit(\n                    props.actionIndex,\n                    !props.isBot,\n                    value,\n                    isSend\n                  )\n                }\n                onClose={() => setEditMode(false)}\n                isBot={props.isBot}\n              />\n            )}\n          </div>\n\n          {props.images &&\n            props.images.filter((img) => img.length > 0).length > 0 && (\n              <div className=\"mt-2 flex flex-wrap gap-2\">\n                {props.images\n                  .filter((image) => image.length > 0)\n                  .map((image, index) => (\n                    <Image\n                      key={index}\n                      src={image}\n                      alt={`Uploaded Image ${index + 1}`}\n                      width={180}\n                      className=\"relative rounded-md\"\n                    />\n                  ))}\n              </div>\n            )}\n\n          {props.isBot &&\n            !isTraceOnly &&\n            props?.sources &&\n            props?.sources.length > 0 && (\n              <Collapse\n                className=\"mt-2\"\n                ghost\n                items={[\n                  {\n                    key: \"1\",\n                    label: (\n                      <div className=\"italic text-gray-500 dark:text-gray-400\">\n                        {t(\"citations\")}\n                      </div>\n                    ),\n                    children: (\n                      <div className=\"mb-3 flex flex-wrap gap-2\">\n                        {props?.sources?.map((source, index) => (\n                          <MessageSource\n                            onSourceClick={props.onSourceClick}\n                            key={index}\n                            source={source}\n                          />\n                        ))}\n                      </div>\n                    )\n                  }\n                ]}\n              />\n            )}\n\n          {!props.isProcessing && !editMode && !isTraceOnly ? (\n            <div\n              className={`flex gap-2 space-x-2 ${\n                !props.isLastMessage\n                  ? \"invisible group-hover:visible\"\n                  : \"\"\n              }`}>\n              {props.isTTSEnabled && (\n                <Tooltip title={t(\"tts\")}>\n                  <button\n                    aria-label={t(\"tts\")}\n                    onClick={() => {\n                      if (isSpeaking) {\n                        cancel()\n                      } else {\n                        speak({\n                          utterance: copyableMessage\n                        })\n                      }\n                    }}\n                    className=\"flex h-6 w-6 items-center justify-center rounded-full border border-gray-300 bg-gray-100 transition-colors duration-200 hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 dark:border-none dark:bg-[#242424] dark:hover:bg-gray-700\">\n                    {!isSpeaking ? (\n                      <Volume2Icon className=\"h-3 w-3 text-gray-400 group-hover:text-gray-500\" />\n                    ) : (\n                      <Square className=\"h-3 w-3 text-red-400 group-hover:text-red-500\" />\n                    )}\n                  </button>\n                </Tooltip>\n              )}\n\n              {!props.hideCopy && (\n                <Tooltip title={t(\"copyToClipboard\")}>\n                  <button\n                    aria-label={t(\"copyToClipboard\")}\n                    onClick={async () => {\n                      await copyToClipboard({\n                        text: copyableMessage,\n                        formatted: copyAsFormattedText\n                      })\n\n                      setIsBtnPressed(true)\n                      setTimeout(() => {\n                        setIsBtnPressed(false)\n                      }, 2000)\n                    }}\n                    className=\"flex h-6 w-6 items-center justify-center rounded-full border border-gray-300 bg-gray-100 transition-colors duration-200 hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 dark:border-none dark:bg-[#242424] dark:hover:bg-gray-700\">\n                    {!isBtnPressed ? (\n                      <CopyIcon className=\"h-3 w-3 text-gray-400 group-hover:text-gray-500\" />\n                    ) : (\n                      <CheckIcon className=\"h-3 w-3 text-green-400 group-hover:text-green-500\" />\n                    )}\n                  </button>\n                </Tooltip>\n              )}\n\n              {props.isBot && (\n                <>\n                  {props.generationInfo && (\n                    <Popover\n                      content={\n                        <GenerationInfo generationInfo={props.generationInfo} />\n                      }\n                      title={t(\"generationInfo\")}>\n                      <button\n                        aria-label={t(\"generationInfo\")}\n                        className=\"flex h-6 w-6 items-center justify-center rounded-full border border-gray-300 bg-gray-100 transition-colors duration-200 hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 dark:border-none dark:bg-[#242424] dark:hover:bg-gray-700\">\n                        <InfoIcon className=\"h-3 w-3 text-gray-400 group-hover:text-gray-500\" />\n                      </button>\n                    </Popover>\n                  )}\n\n                  {!props.hideEditAndRegenerate &&\n                    props.isLastMessage &&\n                    props.onRengerate && (\n                    <Tooltip title={t(\"regenerate\")}>\n                      <button\n                        aria-label={t(\"regenerate\")}\n                        onClick={props.onRengerate}\n                        className=\"flex h-6 w-6 items-center justify-center rounded-full border border-gray-300 bg-gray-100 transition-colors duration-200 hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 dark:border-none dark:bg-[#242424] dark:hover:bg-gray-700\">\n                        <RotateCcw className=\"h-3 w-3 text-gray-400 group-hover:text-gray-500\" />\n                      </button>\n                    </Tooltip>\n                  )}\n\n                  {props?.onNewBranch && !props?.temporaryChat && (\n                    <Tooltip title={t(\"newBranch\")}>\n                      <button\n                        aria-label={t(\"newBranch\")}\n                        onClick={() => props?.onNewBranch?.(props.actionIndex)}\n                        className=\"flex h-6 w-6 items-center justify-center rounded-full border border-gray-300 bg-gray-100 transition-colors duration-200 hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 dark:border-none dark:bg-[#242424] dark:hover:bg-gray-700\">\n                        <GitBranchIcon className=\"h-3 w-3 text-gray-400 group-hover:text-gray-500\" />\n                      </button>\n                    </Tooltip>\n                  )}\n\n                  {!props.hideContinue && props.isLastMessage && (\n                    <Tooltip title={t(\"continue\")}>\n                      <button\n                        aria-label={t(\"continue\")}\n                        onClick={props?.onContinue}\n                        className=\"flex h-6 w-6 items-center justify-center rounded-full border border-gray-300 bg-gray-100 transition-colors duration-200 hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 dark:border-none dark:bg-[#242424] dark:hover:bg-gray-700\">\n                        <PlayCircle className=\"h-3 w-3 text-gray-400 group-hover:text-gray-500\" />\n                      </button>\n                    </Tooltip>\n                  )}\n                </>\n              )}\n\n              {!props.hideEditAndRegenerate && (\n                <Tooltip title={t(\"edit\")}>\n                  <button\n                    onClick={() => setEditMode(true)}\n                    aria-label={t(\"edit\")}\n                    className=\"flex h-6 w-6 items-center justify-center rounded-full border border-gray-300 bg-gray-100 transition-colors duration-200 hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 dark:border-none dark:bg-[#242424] dark:hover:bg-gray-700\">\n                    <Pen className=\"h-3 w-3 text-gray-400 group-hover:text-gray-500\" />\n                  </button>\n                </Tooltip>\n              )}\n            </div>\n          ) : (\n            !isTraceOnly && (\n              <div className=\"invisible\">\n                <div className=\"flex h-6 w-6 items-center justify-center rounded-full border border-gray-300 bg-gray-100 transition-colors duration-200 hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 dark:border-none dark:bg-[#242424] dark:hover:bg-gray-700\"></div>\n              </div>\n            )\n          )}\n        </div>\n      </div>\n    </div>\n  )\n}\n\nconst areSegmentsEqual = (\n  a?: PlaygroundMessageSegment[],\n  b?: PlaygroundMessageSegment[]\n) => {\n  if (a === b) return true\n  if (!a || !b || a.length !== b.length) return false\n  for (let i = 0; i < a.length; i++) {\n    const sa = a[i]\n    const sb = b[i]\n    if (sa.type !== sb.type || sa.key !== sb.key) return false\n    if (sa.type === \"text\" && sb.type === \"text\") {\n      if (sa.message.message !== sb.message.message) return false\n    } else if (sa.type === \"tool_invocations\" && sb.type === \"tool_invocations\") {\n      if (\n        sa.content !== sb.content ||\n        sa.invocations.length !== sb.invocations.length\n      )\n        return false\n      for (let j = 0; j < sa.invocations.length; j++) {\n        const ia = sa.invocations[j]\n        const ib = sb.invocations[j]\n        if (\n          ia.id !== ib.id ||\n          ia.result?.content !== ib.result?.content ||\n          ia.result?.toolError !== ib.result?.toolError\n        )\n          return false\n      }\n    }\n  }\n  return true\n}\n\nconst arePlaygroundMessagePropsEqual = (previous: Props, next: Props) =>\n  previous.message === next.message &&\n  previous.message_type === next.message_type &&\n  previous.hideCopy === next.hideCopy &&\n  previous.botAvatar === next.botAvatar &&\n  previous.userAvatar === next.userAvatar &&\n  previous.isBot === next.isBot &&\n  previous.name === next.name &&\n  previous.images === next.images &&\n  previous.isLastMessage === next.isLastMessage &&\n  previous.actionIndex === next.actionIndex &&\n  previous.onRengerate === next.onRengerate &&\n  previous.onEditFormSubmit === next.onEditFormSubmit &&\n  previous.isProcessing === next.isProcessing &&\n  previous.webSearch === next.webSearch &&\n  previous.isSearchingInternet === next.isSearchingInternet &&\n  previous.sources === next.sources &&\n  previous.hideEditAndRegenerate === next.hideEditAndRegenerate &&\n  previous.hideContinue === next.hideContinue &&\n  previous.onSourceClick === next.onSourceClick &&\n  previous.isTTSEnabled === next.isTTSEnabled &&\n  previous.generationInfo === next.generationInfo &&\n  previous.isStreaming === next.isStreaming &&\n  previous.reasoningTimeTaken === next.reasoningTimeTaken &&\n  previous.openReasoning === next.openReasoning &&\n  previous.modelImage === next.modelImage &&\n  previous.modelName === next.modelName &&\n  previous.onContinue === next.onContinue &&\n  previous.documents === next.documents &&\n  previous.actionInfo === next.actionInfo &&\n  previous.onNewBranch === next.onNewBranch &&\n  previous.temporaryChat === next.temporaryChat &&\n  previous.messageKind === next.messageKind &&\n  previous.toolCalls === next.toolCalls &&\n  previous.toolCallId === next.toolCallId &&\n  previous.toolName === next.toolName &&\n  previous.toolServerName === next.toolServerName &&\n  previous.toolError === next.toolError &&\n  areSegmentsEqual(previous.segments, next.segments)\n\nexport const PlaygroundMessage = React.memo(\n  PlaygroundMessageComponent,\n  arePlaygroundMessagePropsEqual\n)\n"
  },
  {
    "path": "src/components/Common/Playground/MessageSource.tsx",
    "content": "import { KnowledgeIcon } from \"@/components/Option/Knowledge/KnowledgeIcon\"\n\ntype Props = {\n  source: {\n    name?: string\n    url?: string\n    mode?: string\n    type?: string\n    pageContent?: string\n    content?: string\n  }\n  onSourceClick?: (source: any) => void\n}\n\nexport const MessageSource: React.FC<Props> = ({ source, onSourceClick }) => {\n  if (source?.mode === \"rag\" || source?.mode === \"chat\") {\n    return (\n      <button\n        onClick={() => {\n          onSourceClick && onSourceClick(source)\n        }}\n        className=\"inline-flex gap-2   cursor-pointer transition-shadow duration-300 ease-in-out hover:shadow-lg  items-center rounded-md bg-gray-100 p-1 text-xs text-gray-800 border border-gray-300 dark:bg-[#2a2a2a] dark:border-[#404040] dark:text-gray-100 opacity-80 hover:opacity-100\">\n        <KnowledgeIcon type={source.type} className=\"h-3 w-3\" />\n        <span className=\"text-xs\">{source.name}</span>\n      </button>\n    )\n  }\n\n  return (\n    <a\n      href={source?.url}\n      target=\"_blank\"\n      className=\"inline-flex cursor-pointer transition-shadow duration-300 ease-in-out hover:shadow-lg  items-center rounded-md bg-gray-100 p-1 text-xs text-gray-800 border border-gray-300 dark:bg-[#2a2a2a] dark:border-[#404040] dark:text-gray-100 opacity-80 hover:opacity-100\">\n      <span className=\"text-xs\">{source.name}</span>\n    </a>\n  )\n}\n"
  },
  {
    "path": "src/components/Common/Playground/MessageSourcePopup.tsx",
    "content": "import { KnowledgeIcon } from \"@/components/Option/Knowledge/KnowledgeIcon\"\nimport { Modal } from \"antd\"\n\ntype Props = {\n  source: any\n  open: boolean\n  setOpen: (open: boolean) => void\n}\n\nexport const MessageSourcePopup: React.FC<Props> = ({\n  source,\n  open,\n  setOpen\n}) => {\n  return (\n    <Modal\n      open={open}\n      // mask={false}\n      zIndex={10000}\n      onCancel={() => setOpen(false)}\n      footer={null}\n      onOk={() => setOpen(false)}>\n      <div className=\"flex flex-col gap-2 mt-6\">\n        <h4 className=\"bg-gray-100 text-md dark:bg-gray-800 inline-flex gap-2 items-center text-gray-800 dark:text-gray-100 font-semibold p-2\">\n          {source?.type && (\n            <KnowledgeIcon type={source?.type} className=\"h-4 w-5\" />\n          )}\n          {source?.name}\n        </h4>\n        {source?.type === \"pdf\" ? (\n          <>\n            <p className=\"text-gray-500 text-sm\">{source?.pageContent}</p>\n\n            <div className=\"flex flex-wrap gap-3\">\n              <span className=\"border border-gray-300 dark:border-gray-700 rounded-md p-1 text-gray-500 text-xs\">\n                {`Page ${source?.metadata?.page}`}\n              </span>\n\n              <span className=\"border border-gray-300 dark:border-gray-700 rounded-md p-1 text-xs text-gray-500\">\n                {`Line ${source?.metadata?.loc?.lines?.from} - ${source?.metadata?.loc?.lines?.to}`}\n              </span>\n            </div>\n          </>\n        ) : (\n          <>\n            <p className=\"text-gray-500 text-sm\">{source?.pageContent}</p>\n          </>\n        )}\n      </div>\n    </Modal>\n  )\n}\n"
  },
  {
    "path": "src/components/Common/Playground/PlaygroundUserMessage.tsx",
    "content": "import { useTTS } from \"@/hooks/useTTS\"\nimport { useStorage } from \"@plasmohq/storage/hook\"\nimport React from \"react\"\nimport { useTranslation } from \"react-i18next\"\nimport { EditMessageForm } from \"./EditMessageForm\"\nimport { Image, Tag, Tooltip } from \"antd\"\nimport { CheckIcon, CopyIcon, Pen, PlayIcon, Square } from \"lucide-react\"\nimport { HumanMessage } from \"./HumanMessge\"\nimport { ChatDocuments } from \"@/models/ChatTypes\"\nimport { DocumentChip } from \"./DocumentChip\"\nimport { DocumentFile } from \"./DocumentFile\"\nimport { tagColors } from \"@/utils/color\"\n\nconst messageRenderStyle: React.CSSProperties = {\n  contentVisibility: \"auto\",\n  containIntrinsicSize: \"180px\"\n}\n\ntype Props = {\n  message: string\n  message_type?: string\n  hideCopy?: boolean\n  botAvatar?: JSX.Element\n  userAvatar?: JSX.Element\n  isBot: boolean\n  name: string\n  images?: string[]\n  isLastMessage: boolean\n  actionIndex: number\n  onRengerate?: () => void\n  onEditFormSubmit: (\n    messageIndex: number,\n    isHuman: boolean,\n    value: string,\n    isSend: boolean\n  ) => void\n  isProcessing: boolean\n  webSearch?: {}\n  isSearchingInternet?: boolean\n  sources?: any[]\n  hideEditAndRegenerate?: boolean\n  onSourceClick?: (source: any) => void\n  isTTSEnabled?: boolean\n  generationInfo?: any\n  isStreaming: boolean\n  reasoningTimeTaken?: number\n  openReasoning?: boolean\n  modelImage?: string\n  modelName?: string\n  documents?: ChatDocuments\n}\n\nexport const PlaygroundUserMessageBubble: React.FC<Props> = (props) => {\n  const [checkWideMode] = useStorage(\"checkWideMode\", false)\n  const [isBtnPressed, setIsBtnPressed] = React.useState(false)\n  const [editMode, setEditMode] = React.useState(false)\n  const { t } = useTranslation(\"common\")\n  const { cancel, isSpeaking, speak } = useTTS()\n\n  return (\n    <div\n      className={`group gap-2 relative flex w-full max-w-3xl flex-col items-end justify-center pb-2 md:px-4 lg:w-4/5 text-[#242424] dark:text-gray-100 ${checkWideMode ? \"max-w-none\" : \"\"}`}\n      style={messageRenderStyle}>\n      {!editMode && props?.message_type ? (\n        <Tag color={props?.message_type?.startsWith(\"custom_copilot_custom_\") ? \"orange\" : tagColors[props?.message_type] || \"default\"}>\n          {props?.message_type?.startsWith(\"custom_copilot_custom_\")\n            ? t(\"copilot.custom\")\n            : t(`copilot.${props?.message_type}`)}\n        </Tag>\n      ) : null}\n\n      {props?.documents &&\n        props?.documents.length > 0 &&\n        props.documents.filter((d) => d.type === \"file\").length > 0 && (\n          <div className=\"flex flex-wrap gap-2\">\n            {props.documents\n              .filter((d) => d.type === \"file\")\n              .map((doc, index) => (\n                <DocumentFile\n                  key={index}\n                  document={{\n                    filename: doc.filename!,\n                    fileSize: doc.fileSize!\n                  }}\n                />\n              ))}\n          </div>\n        )}\n\n      {props?.documents &&\n        props?.documents.length > 0 &&\n        props.documents.filter((d) => d.type === \"tab\").length > 0 && (\n          <div className=\"flex flex-wrap gap-2\">\n            {props.documents\n              .filter((d) => d.type === \"tab\")\n              .map((doc, index) => (\n                <DocumentChip\n                  key={index}\n                  document={{\n                    title: doc.title,\n                    url: doc.url,\n                    favIconUrl: doc.favIconUrl\n                  }}\n                />\n              ))}\n          </div>\n        )}\n\n      {!editMode && props?.message?.length > 0 && (\n        <div\n          dir=\"auto\"\n          data-is-not-editable={!editMode}\n          className={`message-bubble bg-gray-50 dark:bg-[#242424] rounded-3xl prose dark:prose-invert break-words text-primary min-h-7 prose-p:opacity-95 prose-strong:opacity-100 bg-foreground border border-input-border max-w-[100%] sm:max-w-[90%] px-4 py-2.5 rounded-br-lg dark:border-[#2a2a2a] ${\n            props.message_type && !editMode ? \"italic\" : \"\"\n          }`}>\n          <HumanMessage message={props.message} />\n        </div>\n      )}\n\n      {editMode && (\n        <div\n          dir=\"auto\"\n          className={`message-bubble bg-gray-50 dark:bg-[#2a2a2a] rounded-3xl prose dark:prose-invert break-words text-primary min-h-7 prose-p:opacity-95 prose-strong:opacity-100 bg-foreground border border-input-border max-w-[100%] sm:max-w-[90%] px-4 py-2.5 rounded-br-lg dark:border-[#2a2a2a] ${\n            props.message_type && !editMode ? \"italic\" : \"\"\n          }`}>\n          <div className=\"w-screen max-w-[100%]\">\n            <EditMessageForm\n              value={props.message}\n              onSumbit={(value, isSend) =>\n                props.onEditFormSubmit(\n                  props.actionIndex,\n                  true,\n                  value,\n                  isSend\n                )\n              }\n              onClose={() => setEditMode(false)}\n              isBot={props.isBot}\n            />\n          </div>\n        </div>\n      )}\n\n      {props.images &&\n        props.images.filter((img) => img.length > 0).length > 0 && (\n          <div>\n            {props.images\n              .filter((image) => image.length > 0)\n              .map((image, index) => (\n                <Image\n                  key={index}\n                  src={image}\n                  alt=\"Uploaded Image\"\n                  width={180}\n                  className=\"rounded-lg relative\"\n                />\n              ))}\n          </div>\n        )}\n\n      {!props.isProcessing && !editMode ? (\n        <div\n          className={`space-x-2 gap-2 flex ${\n            !props.isLastMessage\n              ? //  there is few style issue so i am commenting this out for v1.4.5 release\n                // next release we will fix this\n                \"invisible group-hover:visible\"\n              : // ? \"hidden group-hover:flex\"\n                \"\"\n            // : \"flex\"\n          }`}>\n          {props.isTTSEnabled && (\n            <Tooltip title={t(\"tts\")}>\n              <button\n                aria-label={t(\"tts\")}\n                onClick={() => {\n                  if (isSpeaking) {\n                    cancel()\n                  } else {\n                    speak({\n                      utterance: props.message\n                    })\n                  }\n                }}\n                className=\"flex items-center justify-center w-6 h-6 rounded-full bg-gray-100 dark:bg-[#242424] 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\">\n                {!isSpeaking ? (\n                  <PlayIcon className=\"w-3 h-3 text-gray-400 group-hover:text-gray-500\" />\n                ) : (\n                  <Square className=\"w-3 h-3 text-red-400 group-hover:text-red-500\" />\n                )}\n              </button>\n            </Tooltip>\n          )}\n          {!props.hideCopy && (\n            <Tooltip title={t(\"copyToClipboard\")}>\n              <button\n                aria-label={t(\"copyToClipboard\")}\n                onClick={() => {\n                  navigator.clipboard.writeText(props.message)\n                  setIsBtnPressed(true)\n                  setTimeout(() => {\n                    setIsBtnPressed(false)\n                  }, 2000)\n                }}\n                className=\"flex items-center justify-center w-6 h-6 rounded-full bg-gray-50 dark:bg-[#242424] 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\">\n                {!isBtnPressed ? (\n                  <CopyIcon className=\"w-3 h-3 text-gray-400 group-hover:text-gray-500\" />\n                ) : (\n                  <CheckIcon className=\"w-3 h-3 text-green-400 group-hover:text-green-500\" />\n                )}\n              </button>\n            </Tooltip>\n          )}\n\n          {!props.hideEditAndRegenerate && (\n            <Tooltip title={t(\"edit\")}>\n              <button\n                onClick={() => setEditMode(true)}\n                aria-label={t(\"edit\")}\n                className=\"flex items-center justify-center w-6 h-6 rounded-full bg-gray-50 dark:bg-[#242424] 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\">\n                <Pen className=\"w-3 h-3 text-gray-400 group-hover:text-gray-500\" />\n              </button>\n            </Tooltip>\n          )}\n        </div>\n      ) : (\n        // add invisible div to prevent layout shift\n        <div className=\"invisible\">\n          <div className=\"flex items-center justify-center w-6 h-6 rounded-full bg-gray-50 dark:bg-[#242424] 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\"></div>\n        </div>\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Common/Playground/message-groups.ts",
    "content": "import React from \"react\"\nimport { parseMcpToolName } from \"@/libs/mcp/utils\"\nimport type { McpToolCall } from \"@/libs/mcp/types\"\nimport type { Message } from \"@/store/option\"\n\nexport type PlaygroundToolResult = {\n  content: string\n  toolCallId?: string\n  toolName?: string\n  toolServerName?: string\n  toolError?: boolean\n}\n\nexport type PlaygroundToolInvocation = {\n  id: string\n  name: string\n  displayName: string\n  serverName?: string\n  args?: unknown\n  result?: PlaygroundToolResult\n}\n\nexport type PlaygroundMessageSegment =\n  | {\n      type: \"text\"\n      key: string\n      message: Message\n    }\n  | {\n      type: \"tool_invocations\"\n      key: string\n      content: string\n      invocations: PlaygroundToolInvocation[]\n    }\n\nexport type PlaygroundMessageGroup = Message & {\n  renderKey: string\n  startIndex: number\n  endIndex: number\n  actionIndex: number\n  segments: PlaygroundMessageSegment[]\n  sourceMessages: Message[]\n}\n\nconst createToolResult = (message: Message): PlaygroundToolResult => ({\n  content: message.message,\n  toolCallId: message.toolCallId,\n  toolName: message.toolName,\n  toolServerName: message.toolServerName,\n  toolError: message.toolError\n})\n\nconst createToolInvocation = (\n  toolCall: McpToolCall,\n  result?: PlaygroundToolResult\n): PlaygroundToolInvocation => {\n  const parsedTool = parseMcpToolName(toolCall.name)\n\n  return {\n    id: toolCall.id,\n    name: toolCall.name,\n    displayName: toolCall.displayName || parsedTool.displayName,\n    serverName: toolCall.serverName || parsedTool.serverName,\n    args: toolCall.args,\n    result\n  }\n}\n\nconst createFallbackInvocation = (\n  message: Message,\n  index: number\n): PlaygroundToolInvocation => {\n  const fallbackName = message.toolName || \"Tool\"\n  const parsedTool = parseMcpToolName(fallbackName)\n\n  return {\n    id: message.toolCallId || `tool-result-${index}`,\n    name: fallbackName,\n    displayName: message.toolName || parsedTool.displayName,\n    serverName: message.toolServerName || parsedTool.serverName,\n    result: createToolResult(message)\n  }\n}\n\nconst createToolInvocationSegment = (\n  assistantMessage: Message,\n  toolResultMessages: Message[],\n  index: number\n): PlaygroundMessageSegment => {\n  const pendingResults = [...toolResultMessages]\n  const resultsByToolCallId = new Map<string, PlaygroundToolResult>()\n\n  for (const toolResultMessage of toolResultMessages) {\n    if (toolResultMessage.toolCallId) {\n      resultsByToolCallId.set(\n        toolResultMessage.toolCallId,\n        createToolResult(toolResultMessage)\n      )\n    }\n  }\n\n  const invocations = (assistantMessage.toolCalls || []).map((toolCall) => {\n    const matchedResult = toolCall.id\n      ? resultsByToolCallId.get(toolCall.id)\n      : undefined\n\n    if (matchedResult) {\n      resultsByToolCallId.delete(toolCall.id)\n      const matchedIndex = pendingResults.findIndex(\n        (currentResult) => currentResult.toolCallId === toolCall.id\n      )\n\n      if (matchedIndex !== -1) {\n        pendingResults.splice(matchedIndex, 1)\n      }\n    }\n\n    const fallbackResult = matchedResult\n      ? undefined\n      : pendingResults.length > 0\n        ? createToolResult(pendingResults.shift()!)\n        : undefined\n\n    return createToolInvocation(toolCall, matchedResult || fallbackResult)\n  })\n\n  for (const unmatchedResult of pendingResults) {\n    invocations.push(createFallbackInvocation(unmatchedResult, index + invocations.length))\n  }\n\n  return {\n    type: \"tool_invocations\",\n    key: `tool-${index}`,\n    content: assistantMessage.message,\n    invocations\n  }\n}\n\nconst createTextSegment = (\n  message: Message,\n  index: number\n): PlaygroundMessageSegment => ({\n  type: \"text\",\n  key: `text-${index}`,\n  message\n})\n\nconst isMessageEqual = (a: Message, b: Message) => {\n  if (a === b) return true\n  return (\n    a.message === b.message &&\n    a.isBot === b.isBot &&\n    a.id === b.id &&\n    a.messageKind === b.messageKind &&\n    a.toolCallId === b.toolCallId &&\n    a.toolError === b.toolError &&\n    a.toolCalls === b.toolCalls &&\n    a.generationInfo === b.generationInfo &&\n    a.reasoning_time_taken === b.reasoning_time_taken\n  )\n}\n\nconst areSourceMessagesEqual = (\n  sourceMessages: Message[],\n  messages: Message[],\n  startIndex: number,\n  endIndex: number\n) => {\n  if (sourceMessages.length !== endIndex - startIndex + 1) {\n    return false\n  }\n\n  for (let index = startIndex; index <= endIndex; index += 1) {\n    if (!isMessageEqual(sourceMessages[index - startIndex], messages[index])) {\n      return false\n    }\n  }\n\n  return true\n}\n\nconst createRenderKey = (sourceMessages: Message[], startIndex: number) => {\n  const keyParts = sourceMessages\n    .map((message, index) => message.id || `${startIndex + index}-${message.isBot ? \"bot\" : \"user\"}`)\n    .join(\":\")\n\n  return keyParts || `group-${startIndex}`\n}\n\nconst findActionIndex = (\n  messages: Message[],\n  startIndex: number,\n  endIndex: number\n) => {\n  for (let index = endIndex; index >= startIndex; index -= 1) {\n    const message = messages[index]\n\n    if (!message.messageKind || message.messageKind === \"text\") {\n      return index\n    }\n  }\n\n  return endIndex\n}\n\nconst buildAssistantSegments = (\n  messages: Message[],\n  startIndex: number,\n  endIndex: number\n) => {\n  const segments: PlaygroundMessageSegment[] = []\n\n  for (let index = startIndex; index <= endIndex; index += 1) {\n    const message = messages[index]\n\n    if (message.messageKind === \"assistant_tool_calls\") {\n      if (message.message && message.message.trim().length > 0) {\n        segments.push(createTextSegment(message, index))\n      }\n\n      const toolResultMessages: Message[] = []\n      let cursor = index + 1\n\n      while (\n        cursor <= endIndex &&\n        messages[cursor]?.messageKind === \"tool_result\"\n      ) {\n        toolResultMessages.push(messages[cursor])\n        cursor += 1\n      }\n\n      segments.push(\n        createToolInvocationSegment(\n          { ...message, message: \"\" },\n          toolResultMessages,\n          index\n        )\n      )\n      index = cursor - 1\n      continue\n    }\n\n    if (message.messageKind === \"tool_result\") {\n      segments.push(\n        createToolInvocationSegment(\n          {\n            ...message,\n            message: \"\",\n            toolCalls: []\n          },\n          [message],\n          index\n        )\n      )\n      continue\n    }\n\n    segments.push(createTextSegment(message, index))\n  }\n\n  return segments\n}\n\nexport const buildPlaygroundMessageGroups = (\n  messages: Message[],\n  previousGroups: PlaygroundMessageGroup[] = []\n): PlaygroundMessageGroup[] => {\n  const groups: PlaygroundMessageGroup[] = []\n  let groupIndex = 0\n\n  for (let index = 0; index < messages.length; index += 1) {\n    const currentMessage = messages[index]\n\n    if (!currentMessage.isBot) {\n      const previousGroup = previousGroups[groupIndex]\n\n      if (\n        previousGroup &&\n        previousGroup.startIndex === index &&\n        previousGroup.endIndex === index &&\n        previousGroup.actionIndex === index &&\n        areSourceMessagesEqual(previousGroup.sourceMessages, messages, index, index)\n      ) {\n        groups.push(previousGroup)\n      } else {\n        const sourceMessages = [currentMessage]\n        groups.push({\n          ...currentMessage,\n          renderKey: createRenderKey(sourceMessages, index),\n          startIndex: index,\n          endIndex: index,\n          actionIndex: index,\n          segments: [createTextSegment(currentMessage, index)],\n          sourceMessages\n        })\n      }\n      groupIndex += 1\n      continue\n    }\n\n    let endIndex = index\n\n    while (messages[endIndex + 1]?.isBot) {\n      endIndex += 1\n    }\n\n    const actionIndex = findActionIndex(messages, index, endIndex)\n    const previousGroup = previousGroups[groupIndex]\n\n    if (\n      previousGroup &&\n      previousGroup.startIndex === index &&\n      previousGroup.endIndex === endIndex &&\n      previousGroup.actionIndex === actionIndex &&\n      areSourceMessagesEqual(previousGroup.sourceMessages, messages, index, endIndex)\n    ) {\n      groups.push(previousGroup)\n    } else {\n      const actionMessage = messages[actionIndex]\n      const sourceMessages = messages.slice(index, endIndex + 1)\n\n      groups.push({\n        ...actionMessage,\n        renderKey: createRenderKey(sourceMessages, index),\n        startIndex: index,\n        endIndex,\n        actionIndex,\n        segments: buildAssistantSegments(messages, index, endIndex),\n        sourceMessages\n      })\n    }\n    groupIndex += 1\n\n    index = endIndex\n  }\n\n  return groups\n}\n\nexport const usePlaygroundMessageGroups = (messages: Message[]) => {\n  const previousGroupsRef = React.useRef<PlaygroundMessageGroup[]>([])\n\n  return React.useMemo(() => {\n    const groups = buildPlaygroundMessageGroups(messages, previousGroupsRef.current)\n    previousGroupsRef.current = groups\n    return groups\n  }, [messages])\n}\n"
  },
  {
    "path": "src/components/Common/PromptSelect.tsx",
    "content": "import { useQuery } from \"@tanstack/react-query\"\nimport { Dropdown, Empty, Tooltip } from \"antd\"\nimport { BookIcon, ComputerIcon, ZapIcon } from \"lucide-react\"\nimport React from \"react\"\nimport { useTranslation } from \"react-i18next\"\nimport { getAllPrompts } from \"@/db/dexie/helpers\"\n\ntype Props = {\n  setSelectedSystemPrompt: (promptId: string | undefined) => void\n  setSelectedQuickPrompt: (prompt: string | undefined) => void\n  selectedSystemPrompt: string | undefined\n  className?: string\n  iconClassName?: string\n}\n\nexport const PromptSelect: React.FC<Props> = ({\n  setSelectedQuickPrompt,\n  setSelectedSystemPrompt,\n  selectedSystemPrompt,\n  className = \"dark:text-gray-300\",\n  iconClassName = \"size-5\"\n}) => {\n  const { t } = useTranslation(\"option\")\n\n  const { data } = useQuery({\n    queryKey: [\"getAllPromptsForSelect\"],\n    queryFn: getAllPrompts\n  })\n\n  const handlePromptChange = (value?: string) => {\n    if (!value) {\n      setSelectedSystemPrompt(undefined)\n      setSelectedQuickPrompt(undefined)\n      return\n    }\n    const prompt = data?.find((prompt) => prompt.id === value)\n    if (prompt?.is_system) {\n      setSelectedSystemPrompt(prompt.id)\n    } else {\n      setSelectedSystemPrompt(undefined)\n      setSelectedQuickPrompt(prompt!.content)\n    }\n  }\n  return (\n    <>\n      {data && (\n        <Dropdown\n          menu={{\n            items:\n              data.length > 0\n                ? data?.map((prompt) => ({\n                    key: prompt.id,\n                    label: (\n                      <div className=\"w-52 gap-2 text-lg truncate inline-flex line-clamp-3  items-center  dark:border-gray-700\">\n                        <span\n                          key={prompt.title}\n                          className=\"flex flex-row gap-3 items-center\">\n                          {prompt.is_system ? (\n                            <ComputerIcon className=\"w-4 h-4\" />\n                          ) : (\n                            <ZapIcon className=\"w-4 h-4\" />\n                          )}\n                          {prompt.title}\n                        </span>\n                      </div>\n                    ),\n                    onClick: () => {\n                      if (selectedSystemPrompt === prompt.id) {\n                        setSelectedSystemPrompt(undefined)\n                      } else {\n                        handlePromptChange(prompt.id)\n                      }\n                    }\n                  }))\n                : [\n                    {\n                      key: \"empty\",\n                      label: <Empty />\n                    }\n                  ],\n            style: {\n              maxHeight: 500,\n              overflowY: \"scroll\"\n            },\n            className: \"no-scrollbar\",\n            activeKey: selectedSystemPrompt\n          }}\n          placement={\"topLeft\"}\n          trigger={[\"click\"]}>\n          <Tooltip title={t(\"selectAPrompt\")}>\n            <button type=\"button\" className={className}>\n              <BookIcon className={iconClassName} />\n            </button>\n          </Tooltip>\n        </Dropdown>\n      )}\n    </>\n  )\n}\n"
  },
  {
    "path": "src/components/Common/ProviderIcon.tsx",
    "content": "import { ChromeIcon, CpuIcon } from \"lucide-react\"\nimport { OllamaIcon } from \"../Icons/Ollama\"\nimport { FireworksMonoIcon } from \"../Icons/Fireworks\"\nimport { GroqMonoIcon } from \"../Icons/Groq\"\nimport { LMStudioIcon } from \"../Icons/LMStudio\"\nimport { OpenAiIcon } from \"../Icons/OpenAI\"\nimport { TogtherMonoIcon } from \"../Icons/Togther\"\nimport { OpenRouterIcon } from \"../Icons/OpenRouter\"\nimport { LLamaFile } from \"../Icons/Llamafile\"\nimport { GeminiIcon } from \"../Icons/GeminiIcon\"\nimport { MistarlIcon } from \"../Icons/Mistral\"\nimport { DeepSeekIcon } from \"../Icons/DeepSeek\"\nimport { SiliconFlowIcon } from \"../Icons/SiliconFlow\"\nimport { VolcEngineIcon } from \"../Icons/VolcEngine\"\nimport { TencentCloudIcon } from \"../Icons/TencentCloud\"\nimport { AliBaBaCloudIcon } from \"../Icons/AliBaBaCloud\"\nimport { LlamaCppLogo } from \"../Icons/LlamacppLogo\"\nimport { InfinigenceAI } from \"../Icons/InfinigenceAI\"\nimport { NovitaIcon } from \"../Icons/Novita\"\nimport { VllmLogo } from \"../Icons/VllmLogo\"\nimport { MoonshotIcon } from \"../Icons/Moonshot\"\nimport { XAIIcon } from \"../Icons/XAI\"\nimport { HuggingFaceIcon } from \"../Icons/HuggingFaceIcon\"\nimport { VercelIcon } from \"../Icons/VercelIcon\"\nimport { ChutesIcon } from \"../Icons/ChutesIcon\"\nimport { AnthropicIcon } from \"../Icons/AnthropicIcon\"\nimport { BigModelZhipuIcon } from \"../Icons/BigModelZhipuIcon\"\nimport { CanopyWaveIcon } from \"../Icons/CanopyWaveIcon\"\nimport { MiniMaxIcon } from \"../Icons/MiniMaxIcon\"\n\nexport const ProviderIcons = ({\n  provider,\n  className\n}: {\n  provider: string\n  className?: string\n}) => {\n  switch (provider) {\n    case \"chrome\":\n      return <ChromeIcon className={className} />\n    case \"custom\":\n      return <CpuIcon className={className} />\n    case \"fireworks\":\n      return <FireworksMonoIcon className={className} />\n    case \"groq\":\n      return <GroqMonoIcon className={className} />\n    case \"lmstudio\":\n      return <LMStudioIcon className={className} />\n    case \"openai\":\n      return <OpenAiIcon className={className} />\n    case \"together\":\n      return <TogtherMonoIcon className={className} />\n    case \"openrouter\":\n      return <OpenRouterIcon className={className} />\n    case \"llamafile\":\n      return <LLamaFile className={className} />\n    case \"gemini\":\n      return <GeminiIcon className={className} />\n    case \"mistral\":\n      return <MistarlIcon className={className} />\n    case \"deepseek\":\n      return <DeepSeekIcon className={className} />\n    case \"siliconflow\":\n      return <SiliconFlowIcon className={className} />\n    case \"volcengine\":\n      return <VolcEngineIcon className={className} />\n    case \"tencentcloud\":\n      return <TencentCloudIcon className={className} />\n    case \"alibabacloud\":\n      return <AliBaBaCloudIcon className={className} />\n    case \"llamacpp\":\n      return <LlamaCppLogo className={className} />\n    case \"infinitenceai\":\n      return <InfinigenceAI className={className} />\n    case \"novita\":\n      return <NovitaIcon className={className} />\n    case \"vllm\":\n      return <VllmLogo className={className} />\n    case \"moonshot\":\n      return <MoonshotIcon className={className} />\n    case \"xai\":\n      return <XAIIcon className={className} />\n    case \"huggingface\":\n      return <HuggingFaceIcon className={className} />\n    case \"vercel\":\n      return <VercelIcon className={className} />\n    case \"chutes\":\n      return <ChutesIcon className={className} />\n    case \"anthropic\":\n      return <AnthropicIcon className={className} />\n    case \"canopywave\":\n      return <CanopyWaveIcon className={className} />\n    case 'zhipu':\n      return <BigModelZhipuIcon className={className} />\n    case 'minimax':\n      return <MiniMaxIcon className={className} />\n    default:\n      return <OllamaIcon className={className} />\n  }\n}\n"
  },
  {
    "path": "src/components/Common/QueuedMessagesList.tsx",
    "content": "import type { QueuedMessage } from \"@/hooks/useMessageQueue\"\nimport { ImageIcon, PencilIcon, Trash2Icon, ArrowUpIcon } from \"lucide-react\"\nimport { Image, Tooltip } from \"antd\"\n\ntype Props = {\n  queuedMessages: QueuedMessage[]\n  onDelete: (id: string) => void\n  onEdit: (id: string) => void\n  onSend: (id: string) => void\n  title?: string\n}\n\nexport const QueuedMessagesList = ({\n  queuedMessages,\n  onDelete,\n  onEdit,\n  onSend\n}: Props) => {\n  if (queuedMessages.length === 0) {\n    return null\n  }\n\n  return (\n    <div className=\"p-4\">\n      <div className=\"max-h-32 space-y-2 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-300 dark:scrollbar-thumb-[#404040] scrollbar-track-transparent\">\n        {queuedMessages.map((item) => (\n          <div\n            key={item.id}\n            className=\"flex items-center  justify-between gap-2 rounded-xl border border-gray-200 bg-white/80 px-2 py-1.5 dark:border-[#404040] dark:bg-[#2a2a2a]/80\">\n            <div className=\"min-w-0 inline-flex max-w-full items-center gap-2\">\n              {item.images.length > 0 && (\n                <div className=\"inline-flex items-center gap-1 text-[11px] text-gray-500 dark:text-gray-400\">\n                  <Image\n                    className=\"h-3 w-3 rounded-sm\"\n                    src={item.images[0]}\n                    preview={false}\n                    width={12}\n                    height={12}\n                  />\n                </div>\n              )}\n              <span\n                title={item.message}\n                className=\"truncate text-xs text-gray-700 dark:text-gray-200\">\n                {item.message || \"Image\"}\n              </span>\n            </div>\n            <div className=\"inline-flex shrink-0 items-center gap-1 text-[11px]\">\n              <Tooltip title=\"Delete\">\n                <button\n                  type=\"button\"\n                  onClick={() => onDelete(item.id)}\n                  className=\"rounded-lg border border-gray-300 p-1 text-gray-600 hover:text-red-600 dark:border-[#5a5a5a] dark:text-gray-300 dark:hover:text-red-400\">\n                  <Trash2Icon className=\"h-3.5 w-3.5\" />\n                </button>\n              </Tooltip>\n              <Tooltip title=\"Edit\">\n                <button\n                  type=\"button\"\n                  onClick={() => onEdit(item.id)}\n                  className=\"rounded-lg border border-gray-300 p-1 text-gray-600 hover:text-gray-900 dark:border-[#5a5a5a] dark:text-gray-300 dark:hover:text-white\">\n                  <PencilIcon className=\"h-3.5 w-3.5\" />\n                </button>\n              </Tooltip>\n              <Tooltip title=\"Send now\">\n                <button\n                  type=\"button\"\n                  onClick={() => onSend(item.id)}\n                  className=\"rounded-lg border border-gray-300 p-1 text-gray-600 hover:text-gray-900 dark:border-[#5a5a5a] dark:text-gray-300 dark:hover:text-white\">\n                  <ArrowUpIcon className=\"h-3.5 w-3.5\" />\n                </button>\n              </Tooltip>\n            </div>\n          </div>\n        ))}\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Common/SaveButton.tsx",
    "content": "import { useState } from \"react\"\nimport { CheckIcon } from \"lucide-react\"\nimport { useTranslation } from \"react-i18next\"\ntype Props = {\n  onClick?: () => void\n  disabled?: boolean\n  className?: string\n  text?: string\n  textOnSave?: string\n  btnType?: \"button\" | \"submit\" | \"reset\"\n}\n\nexport const SaveButton = ({\n  onClick,\n  disabled,\n  className,\n  text = \"save\",\n  textOnSave = \"saved\",\n  btnType = \"button\"\n}: Props) => {\n  const [clickedSave, setClickedSave] = useState(false)\n  const { t } = useTranslation(\"common\")\n  return (\n    <button\n      type={btnType}\n      onClick={() => {\n        setClickedSave(true)\n        if (onClick) {\n          onClick()\n        }\n        setTimeout(() => {\n          setClickedSave(false)\n        }, 1000)\n      }}\n      disabled={disabled}\n      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}`}>\n      {clickedSave ? <CheckIcon className=\"w-4 h-4 mr-2\" /> : null}\n      {clickedSave ? t(textOnSave) : t(text)}\n    </button>\n  )\n}\n"
  },
  {
    "path": "src/components/Common/Settings/AdvanceOllamaSettings.tsx",
    "content": "import { Divider, Input, Switch } from \"antd\"\nimport { useTranslation } from \"react-i18next\"\nimport { Form } from \"antd\"\nimport React from \"react\"\nimport {\n  customOllamaHeaders,\n  getIsAutoCORSFix,\n  getRewriteUrl,\n  isUrlRewriteEnabled,\n  setAutoCORSFix,\n  setCustomOllamaHeaders,\n  setRewriteUrl,\n  setUrlRewriteEnabled\n} from \"@/services/app\"\nimport { Trash2Icon } from \"lucide-react\"\nimport { SaveButton } from \"../SaveButton\"\n\nexport const AdvanceOllamaSettings = () => {\n  const [form] = Form.useForm()\n  const watchUrlRewriteEnabled = Form.useWatch(\"urlRewriteEnabled\", form)\n  const { t } = useTranslation(\"settings\")\n\n  const fetchAdvancedData = async () => {\n    try {\n      const [urlRewriteEnabled, rewriteUrl, headers, autoCORSFix] =\n        await Promise.all([\n          isUrlRewriteEnabled(),\n          getRewriteUrl(),\n          customOllamaHeaders(),\n          getIsAutoCORSFix()\n        ])\n      form.setFieldsValue({\n        urlRewriteEnabled,\n        rewriteUrl,\n        headers,\n        autoCORSFix\n      })\n    } catch (e) {\n      console.error(e)\n    }\n  }\n\n  React.useEffect(() => {\n    fetchAdvancedData()\n  }, [])\n\n  return (\n    <Form\n      onFinish={(e) => {\n        const headers = e?.headers?.filter(\n          (header: { key: string; value: string }) => header.key && header.value\n        )\n        setUrlRewriteEnabled(e.urlRewriteEnabled)\n        setRewriteUrl(e.rewriteUrl)\n        setCustomOllamaHeaders(headers)\n        setAutoCORSFix(e.autoCORSFix)\n      }}\n      form={form}\n      layout=\"vertical\"\n      className=\"space-y-4\">\n      <Form.Item\n        name=\"urlRewriteEnabled\"\n        label={t(\"ollamaSettings.settings.advanced.urlRewriteEnabled.label\")}>\n        <Switch />\n      </Form.Item>\n      <Form.Item\n        required={watchUrlRewriteEnabled}\n        name=\"rewriteUrl\"\n        label={t(\"ollamaSettings.settings.advanced.rewriteUrl.label\")}>\n        <Input\n          disabled={!watchUrlRewriteEnabled}\n          className=\"w-full\"\n          placeholder={t(\n            \"ollamaSettings.settings.advanced.rewriteUrl.placeholder\"\n          )}\n        />\n      </Form.Item>\n\n      <Form.Item\n        name=\"autoCORSFix\"\n        label={t(\"ollamaSettings.settings.advanced.autoCORSFix.label\")}>\n        <Switch />\n      </Form.Item>\n\n      <Form.List name=\"headers\">\n        {(fields, { add, remove }) => (\n          <div className=\"flex flex-col \">\n            <div className=\"flex justify-between items-center\">\n              <h3 className=\"text-md font-semibold\">\n                {t(\"ollamaSettings.settings.advanced.headers.label\")}\n              </h3>\n              <button\n                type=\"button\"\n                className=\"dark:bg-white dark:text-black text-white bg-black p-1.5 text-xs rounded-md\"\n                onClick={() => {\n                  add()\n                }}>\n                {t(\"ollamaSettings.settings.advanced.headers.add\")}\n              </button>\n            </div>\n            {fields.map((field, index) => (\n              <div key={field.key} className=\"flex items-center   w-full\">\n                <div className=\"flex-grow flex mt-3 space-x-4\">\n                  <Form.Item\n                    label={t(\n                      \"ollamaSettings.settings.advanced.headers.key.label\"\n                    )}\n                    name={[field.name, \"key\"]}\n                    className=\"flex-1 mb-0\">\n                    <Input\n                      className=\"w-full\"\n                      placeholder={t(\n                        \"ollamaSettings.settings.advanced.headers.key.placeholder\"\n                      )}\n                    />\n                  </Form.Item>\n                  <Form.Item\n                    label={t(\n                      \"ollamaSettings.settings.advanced.headers.value.label\"\n                    )}\n                    name={[field.name, \"value\"]}\n                    className=\"flex-1 mb-0\">\n                    <Input\n                      className=\"w-full\"\n                      placeholder={t(\n                        \"ollamaSettings.settings.advanced.headers.value.placeholder\"\n                      )}\n                    />\n                  </Form.Item>\n                </div>\n                <button\n                  type=\"button\"\n                  onClick={() => {\n                    remove(field.name)\n                  }}\n                  className=\"shrink-0 ml-2 text-red-500 dark:text-red-400\">\n                  <Trash2Icon className=\"w-5 h-5\" />\n                </button>\n              </div>\n            ))}\n          </div>\n        )}\n      </Form.List>\n      <Divider />\n\n      <Form.Item className=\"flex justify-end\">\n        <SaveButton btnType=\"submit\" />\n      </Form.Item>\n    </Form>\n  )\n}\n"
  },
  {
    "path": "src/components/Common/Settings/CurrentChatModelSettings.tsx",
    "content": "import { getPromptById } from \"@/db/dexie/helpers\"\nimport { useMessageOption } from \"@/hooks/useMessageOption\"\nimport { FileIcon, X } from \"lucide-react\"\nimport { getAllModelSettings, getModelSettings } from \"@/services/model-settings\"\nimport { useStoreChatModelSettings } from \"@/store/model\"\nimport { useQuery } from \"@tanstack/react-query\"\nimport {\n  Collapse,\n  Divider,\n  Drawer,\n  Form,\n  Input,\n  InputNumber,\n  Modal,\n  Select,\n  Skeleton,\n  Switch\n} from \"antd\"\nimport React, { useCallback } from \"react\"\nimport { useTranslation } from \"react-i18next\"\nimport { SaveButton } from \"../SaveButton\"\nimport { getOCRLanguage } from \"@/services/ocr\"\nimport { ocrLanguages } from \"@/data/ocr-language\"\nimport { useMessage } from \"@/hooks/useMessage\"\n\ntype Props = {\n  open: boolean\n  setOpen: (open: boolean) => void\n  useDrawer?: boolean\n  isOCREnabled?: boolean\n}\n\nexport const CurrentChatModelSettings = ({\n  open,\n  setOpen,\n  useDrawer,\n  isOCREnabled\n}: Props) => {\n  const { t } = useTranslation(\"common\")\n  const [form] = Form.useForm()\n  const cUserSettings = useStoreChatModelSettings()\n  const { selectedModel } = useMessage()\n  const {\n    selectedSystemPrompt,\n    uploadedFiles,\n    removeUploadedFile,\n    fileRetrievalEnabled,\n    setFileRetrievalEnabled\n  } = useMessageOption()\n\n  const savePrompt = useCallback(\n    (value: string) => {\n      cUserSettings.setX(\"systemPrompt\", value)\n    },\n    [cUserSettings]\n  )\n\n  const saveSettings = useCallback(\n    (values: any) => {\n      Object.entries(values).forEach(([key, value]) => {\n        if (key !== \"systemPrompt\" && key !== \"ocrLanguage\") {\n          cUserSettings.setX(key, value)\n        }\n      })\n    },\n    [cUserSettings]\n  )\n\n  const { isPending: isLoading } = useQuery({\n    queryKey: [\"fetchModelConfig2\", open, selectedModel],\n    queryFn: async () => {\n      const data = await getAllModelSettings()\n\n      // Load model-specific settings if a model is selected\n      let modelSpecificSettings = null\n      if (selectedModel) {\n        try {\n          modelSpecificSettings = await getModelSettings(selectedModel)\n        } catch (e) {\n          console.error(\"Failed to load model-specific settings:\", e)\n        }\n      }\n\n      const ocrLang = await getOCRLanguage()\n\n      if (isOCREnabled) {\n        cUserSettings.setOcrLanguage(ocrLang)\n      }\n      let tempSystemPrompt = \"\"\n\n      // i hate this method but i need this feature so badly that i need to do this\n      if (selectedSystemPrompt) {\n        const prompt = await getPromptById(selectedSystemPrompt)\n        tempSystemPrompt = prompt?.content ?? \"\"\n      }\n\n      form.setFieldsValue({\n        temperature: cUserSettings.temperature ?? modelSpecificSettings?.temperature ?? data.temperature,\n        topK: cUserSettings.topK ?? modelSpecificSettings?.topK ?? data.topK,\n        topP: cUserSettings.topP ?? modelSpecificSettings?.topP ?? data.topP,\n        keepAlive: cUserSettings.keepAlive ?? modelSpecificSettings?.keepAlive ?? data.keepAlive,\n        numCtx: cUserSettings.numCtx ?? modelSpecificSettings?.numCtx ?? data.numCtx,\n        seed: cUserSettings.seed ?? modelSpecificSettings?.seed,\n        numGpu: cUserSettings.numGpu ?? modelSpecificSettings?.numGpu ?? data.numGpu,\n        numPredict: cUserSettings.numPredict ?? modelSpecificSettings?.numPredict ?? data.numPredict,\n        systemPrompt: cUserSettings.systemPrompt ?? tempSystemPrompt,\n        useMMap: cUserSettings.useMMap ?? modelSpecificSettings?.useMMap ?? data.useMMap,\n        minP: cUserSettings.minP ?? modelSpecificSettings?.minP ?? data.minP,\n        repeatLastN: cUserSettings.repeatLastN ?? modelSpecificSettings?.repeatLastN ?? data.repeatLastN,\n        repeatPenalty: cUserSettings.repeatPenalty ?? modelSpecificSettings?.repeatPenalty ?? data.repeatPenalty,\n        useMlock: cUserSettings.useMlock ?? modelSpecificSettings?.useMlock ?? data.useMlock,\n        tfsZ: cUserSettings.tfsZ ?? modelSpecificSettings?.tfsZ ?? data.tfsZ,\n        numKeep: cUserSettings.numKeep ?? modelSpecificSettings?.numKeep ?? data.numKeep,\n        numThread: cUserSettings.numThread ?? modelSpecificSettings?.numThread ?? data.numThread,\n        reasoningEffort: cUserSettings?.reasoningEffort ?? modelSpecificSettings?.reasoningEffort,\n        thinking: cUserSettings?.thinking ?? modelSpecificSettings?.thinking\n      })\n      return data\n    },\n    enabled: open,\n    refetchOnMount: false,\n    refetchOnWindowFocus: false\n  })\n\n  const renderBody = () => {\n    return (\n      <>\n        {!isLoading ? (\n          <Form\n            form={form}\n            layout=\"vertical\"\n            onFinish={(values) => {\n              saveSettings(values)\n              setOpen(false)\n            }}>\n            {useDrawer && (\n              <>\n                <Form.Item\n                  name=\"systemPrompt\"\n                  help={t(\"modelSettings.form.systemPrompt.help\")}\n                  label={t(\"modelSettings.form.systemPrompt.label\")}>\n                  <Input.TextArea\n                    rows={4}\n                    placeholder={t(\n                      \"modelSettings.form.systemPrompt.placeholder\"\n                    )}\n                    onChange={(e) => savePrompt(e.target.value)}\n                  />\n                </Form.Item>\n                <Divider />\n              </>\n            )}\n\n            {isOCREnabled && (\n              <div className=\"flex flex-col space-y-2 mb-3\">\n                <span className=\"text-gray-700   dark:text-neutral-50\">\n                  OCR Language\n                </span>\n\n                <Select\n                  showSearch\n                  style={{ width: \"100%\" }}\n                  options={ocrLanguages}\n                  value={cUserSettings.ocrLanguage}\n                  filterOption={(input, option) =>\n                    option!.label.toLowerCase().indexOf(input.toLowerCase()) >=\n                      0 ||\n                    option!.value.toLowerCase().indexOf(input.toLowerCase()) >=\n                      0\n                  }\n                  onChange={(value) => {\n                    cUserSettings.setOcrLanguage(value)\n                  }}\n                />\n                <Divider />\n\n              </div>\n            )}\n\n            <Form.Item\n              name=\"keepAlive\"\n              help={t(\"modelSettings.form.keepAlive.help\")}\n              label={t(\"modelSettings.form.keepAlive.label\")}>\n              <Input\n                placeholder={t(\"modelSettings.form.keepAlive.placeholder\")}\n              />\n            </Form.Item>\n\n            <Form.Item\n              name=\"temperature\"\n              label={t(\"modelSettings.form.temperature.label\")}>\n              <InputNumber\n                style={{ width: \"100%\" }}\n                placeholder={t(\"modelSettings.form.temperature.placeholder\")}\n              />\n            </Form.Item>\n\n            <Form.Item\n              name=\"seed\"\n              help={t(\"modelSettings.form.seed.help\")}\n              label={t(\"modelSettings.form.seed.label\")}>\n              <InputNumber\n                style={{ width: \"100%\" }}\n                placeholder={t(\"modelSettings.form.seed.placeholder\")}\n              />\n            </Form.Item>\n\n            <Form.Item\n              name=\"numCtx\"\n              label={t(\"modelSettings.form.numCtx.label\")}>\n              <InputNumber\n                style={{ width: \"100%\" }}\n                placeholder={t(\"modelSettings.form.numCtx.placeholder\")}\n              />\n            </Form.Item>\n\n            <Form.Item\n              name=\"numPredict\"\n              label={t(\"modelSettings.form.numPredict.label\")}>\n              <InputNumber\n                style={{ width: \"100%\" }}\n                placeholder={t(\"modelSettings.form.numPredict.placeholder\")}\n              />\n            </Form.Item>\n\n            <Form.Item\n              name=\"thinking\"\n              label={t(\"modelSettings.form.thinking.label\")}>\n              <Switch />\n            </Form.Item>\n\n            {uploadedFiles.length > 0 && (\n              <>\n                <Divider />\n                <div className=\"mb-4\">\n                  <div className=\"flex items-center justify-between mb-3\">\n                    <h4 className=\"font-medium text-gray-900 dark:text-gray-100\">\n                      Uploaded Files ({uploadedFiles.length})\n                    </h4>\n                    <div className=\"flex items-center gap-2\">\n                      <span className=\"text-sm text-gray-600 dark:text-gray-300\">\n                        File Retrieval\n                      </span>\n                      <Switch\n                        size=\"small\"\n                        checked={fileRetrievalEnabled}\n                        onChange={setFileRetrievalEnabled}\n                      />\n                    </div>\n                  </div>\n                  <div className=\"space-y-2 max-h-32 overflow-y-auto\">\n                    {uploadedFiles.map((file) => (\n                      <div\n                        key={file.id}\n                        className=\"flex items-center justify-between p-2 bg-gray-50 dark:bg-gray-800 rounded-md\">\n                        <div className=\"flex items-center gap-2 flex-1 min-w-0\">\n                          <FileIcon className=\"h-4 w-4 text-gray-500 flex-shrink-0\" />\n                          <div className=\"min-w-0 flex-1\">\n                            <p className=\"text-sm font-medium text-gray-900 dark:text-gray-100 truncate\">\n                              {file.filename}\n                            </p>\n                            <div className=\"flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400\">\n                              <span>{(file.size / 1024).toFixed(1)} KB</span>\n                              {fileRetrievalEnabled && (\n                                <span className=\"flex items-center gap-1\">\n                                  <span\n                                    className={`inline-block w-2 h-2 rounded-full ${\n                                      file.processed\n                                        ? \"bg-green-500\"\n                                        : \"bg-yellow-500\"\n                                    }`}\n                                  />\n                                  {file.processed\n                                    ? \"Processed\"\n                                    : \"Processing...\"}\n                                </span>\n                              )}\n                            </div>\n                          </div>\n                        </div>\n                        <button\n                          onClick={() => removeUploadedFile(file.id)}\n                          className=\"p-1 text-red-500 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-900/20 rounded\">\n                          <X className=\"h-4 w-4\" />\n                        </button>\n                      </div>\n                    ))}\n                  </div>\n                </div>\n              </>\n            )}\n\n            <Divider />\n\n            <Collapse\n              ghost\n              className=\"border-none bg-transparent\"\n              items={[\n                {\n                  key: \"1\",\n                  label: t(\"modelSettings.advanced\"),\n                  children: (\n                    <React.Fragment>\n                      <Form.Item\n                        name=\"topK\"\n                        label={t(\"modelSettings.form.topK.label\")}>\n                        <InputNumber\n                          style={{ width: \"100%\" }}\n                          placeholder={t(\"modelSettings.form.topK.placeholder\")}\n                        />\n                      </Form.Item>\n\n                      <Form.Item\n                        name=\"topP\"\n                        label={t(\"modelSettings.form.topP.label\")}>\n                        <InputNumber\n                          style={{ width: \"100%\" }}\n                          placeholder={t(\"modelSettings.form.topP.placeholder\")}\n                        />\n                      </Form.Item>\n\n                      <Form.Item\n                        name=\"numGpu\"\n                        label={t(\"modelSettings.form.numGpu.label\")}>\n                        <InputNumber\n                          style={{ width: \"100%\" }}\n                          placeholder={t(\n                            \"modelSettings.form.numGpu.placeholder\"\n                          )}\n                        />\n                      </Form.Item>\n\n                      <Form.Item\n                        name=\"minP\"\n                        label={t(\"modelSettings.form.minP.label\")}>\n                        <InputNumber\n                          style={{ width: \"100%\" }}\n                          placeholder={t(\"modelSettings.form.minP.placeholder\")}\n                        />\n                      </Form.Item>\n                      <Form.Item\n                        name=\"repeatPenalty\"\n                        label={t(\"modelSettings.form.repeatPenalty.label\")}>\n                        <InputNumber\n                          style={{ width: \"100%\" }}\n                          placeholder={t(\n                            \"modelSettings.form.repeatPenalty.placeholder\"\n                          )}\n                        />\n                      </Form.Item>\n                      <Form.Item\n                        name=\"repeatLastN\"\n                        label={t(\"modelSettings.form.repeatLastN.label\")}>\n                        <InputNumber\n                          style={{ width: \"100%\" }}\n                          placeholder={t(\n                            \"modelSettings.form.repeatLastN.placeholder\"\n                          )}\n                        />\n                      </Form.Item>\n                      <Form.Item\n                        name=\"tfsZ\"\n                        label={t(\"modelSettings.form.tfsZ.label\")}>\n                        <InputNumber\n                          style={{ width: \"100%\" }}\n                          placeholder={t(\"modelSettings.form.tfsZ.placeholder\")}\n                        />\n                      </Form.Item>\n                      <Form.Item\n                        name=\"numKeep\"\n                        label={t(\"modelSettings.form.numKeep.label\")}>\n                        <InputNumber\n                          style={{ width: \"100%\" }}\n                          placeholder={t(\n                            \"modelSettings.form.numKeep.placeholder\"\n                          )}\n                        />\n                      </Form.Item>\n                      <Form.Item\n                        name=\"numThread\"\n                        label={t(\"modelSettings.form.numThread.label\")}>\n                        <InputNumber\n                          style={{ width: \"100%\" }}\n                          placeholder={t(\n                            \"modelSettings.form.numThread.placeholder\"\n                          )}\n                        />\n                      </Form.Item>\n                      <Form.Item\n                        name=\"useMMap\"\n                        label={t(\"modelSettings.form.useMMap.label\")}>\n                        <Switch />\n                      </Form.Item>\n                      <Form.Item\n                        name=\"useMlock\"\n                        label={t(\"modelSettings.form.useMlock.label\")}>\n                        <Switch />\n                      </Form.Item>\n                      <Form.Item\n                        name=\"reasoningEffort\"\n                        label={t(\"modelSettings.form.reasoningEffort.label\")}>\n                        <Input\n                          style={{ width: \"100%\" }}\n                          placeholder={t(\n                            \"modelSettings.form.reasoningEffort.placeholder\"\n                          )}\n                        />\n                      </Form.Item>\n                    </React.Fragment>\n                  )\n                }\n              ]}\n            />\n            <SaveButton\n              className=\"w-full text-center inline-flex items-center justify-center\"\n              btnType=\"submit\"\n            />\n          </Form>\n        ) : (\n          <Skeleton active />\n        )}\n      </>\n    )\n  }\n\n  if (useDrawer) {\n    return (\n      <Drawer\n        placement=\"right\"\n        open={open}\n        onClose={() => setOpen(false)}\n        width={500}\n        title={t(\"currentChatModelSettings\")}>\n        {renderBody()}\n      </Drawer>\n    )\n  }\n\n  return (\n    <Modal\n      title={t(\"currentChatModelSettings\")}\n      open={open}\n      onOk={() => setOpen(false)}\n      onCancel={() => setOpen(false)}\n      footer={null}>\n      {renderBody()}\n    </Modal>\n  )\n}\n"
  },
  {
    "path": "src/components/Common/ShareModal.tsx",
    "content": "import { Form, Image, Input, Modal, Tooltip, message } from \"antd\"\nimport { Share } from \"lucide-react\"\nimport { useState } from \"react\"\nimport type { Message } from \"~/store/option\"\nimport Markdown from \"./Markdown\"\nimport React from \"react\"\nimport { useMutation } from \"@tanstack/react-query\"\nimport { getPageShareUrl } from \"~/services/ollama\"\nimport { cleanUrl } from \"~/libs/clean-url\"\nimport { getTitleById, getUserId, saveWebshare } from \"@/db/dexie/helpers\"\nimport { useTranslation } from \"react-i18next\"\nimport fetcher from \"@/libs/fetcher\"\nimport { removeModelSuffix } from \"@/db/dexie/models\"\n\ntype Props = {\n  messages: Message[]\n  historyId: string\n  open: boolean\n  setOpen: (state: boolean) => void\n}\n\nconst reformatMessages = (messages: Message[], username: string) => {\n  return messages.map((message, idx) => {\n    return {\n      id: idx,\n      name: message.isBot\n        ? removeModelSuffix(\n            `${message?.modelName || message?.name}`?.replaceAll(\n              /accounts\\/[^\\/]+\\/models\\//g,\n              \"\"\n            )\n          )\n        : username,\n      isBot: message.isBot,\n      message: message.message,\n      reasoning_time_taken: message.reasoning_time_taken,\n      search: message.search,\n      images: message.images,\n      modelName: message.modelName,\n      modelImage: message.modelImage,\n      sources: message.sources\n    }\n  })\n}\n\nexport const PlaygroundMessage = (\n  props: Message & {\n    username: string\n  }\n) => {\n  return (\n    <div className=\"group w-full text-gray-800 dark:text-gray-100\">\n      <div className=\"text-base gap-4 md:gap-6 md:max-w-2xl lg:max-w-xl xl:max-w-3xl flex lg:px-0 m-auto w-full\">\n        <div className=\"flex flex-row gap-4 md:gap-6 md:max-w-2xl lg:max-w-xl xl:max-w-3xl m-auto w-full\">\n          <div className=\"w-8 flex flex-col relative items-end\">\n            <div className=\"relative h-7 w-7 p-1 rounded-sm text-white flex items-center justify-center  text-opacity-100r\">\n              {props.isBot ? (\n                <div className=\"absolute h-8 w-8 rounded-full bg-gradient-to-r from-green-300 to-purple-400\"></div>\n              ) : (\n                <div className=\"absolute h-8 w-8 rounded-full from-blue-400 to-blue-600 bg-gradient-to-r\"></div>\n              )}\n            </div>\n          </div>\n          <div className=\"flex w-[calc(100%-50px)] flex-col gap-3 lg:w-[calc(100%-115px)]\">\n            <span className=\"text-xs font-bold text-gray-800 dark:text-white\">\n              {props.isBot\n                ? removeModelSuffix(\n                    `${props?.modelName || props?.name}`?.replaceAll(\n                      /accounts\\/[^\\/]+\\/models\\//g,\n                      \"\"\n                    )\n                  )\n                : props.username}\n            </span>\n\n            <div className=\"flex flex-grow flex-col\">\n              <Markdown message={props.message} />\n            </div>\n            {/* source if aviable */}\n            {props.images && props.images.length > 0 && (\n              <div className=\"flex md:max-w-2xl lg:max-w-xl xl:max-w-3xl mt-4 m-auto w-full\">\n                {props.images\n                  .filter((image) => image.length > 0)\n                  .map((image, index) => (\n                    <Image\n                      key={index}\n                      src={image}\n                      alt=\"Uploaded Image\"\n                      width={180}\n                      className=\"rounded-md relative\"\n                    />\n                  ))}\n              </div>\n            )}\n          </div>\n        </div>\n      </div>\n    </div>\n  )\n}\n\nexport const ShareModal: React.FC<Props> = ({\n  messages,\n  historyId,\n  open,\n  setOpen\n}) => {\n  const { t } = useTranslation(\"common\")\n  const [form] = Form.useForm()\n  const name = Form.useWatch(\"name\", form)\n\n  React.useEffect(() => {\n    if (messages.length > 0) {\n      getTitleById(historyId).then((title) => {\n        form.setFieldsValue({\n          title\n        })\n      })\n    }\n  }, [messages, historyId])\n\n  const onSubmit = async (values: { title: string; name: string }) => {\n    const owner_id = await getUserId()\n    const chat = reformatMessages(messages, values.name)\n    const title = values.title\n    const url = await getPageShareUrl()\n    const res = await fetcher(`${cleanUrl(url)}/api/v1/share/create`, {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\"\n      },\n      body: JSON.stringify({\n        owner_id,\n        messages: chat,\n        title\n      })\n    })\n\n    if (!res.ok) throw new Error(t(\"share.notification.failGenerate\"))\n\n    const data = await res.json()\n\n    return {\n      ...data,\n      url: `${cleanUrl(url)}/share/${data.chat_id}`,\n      api_url: cleanUrl(url),\n      share_id: data.chat_id\n    }\n  }\n\n  const { mutate: createShareLink, isPending } = useMutation({\n    mutationFn: onSubmit,\n    onSuccess: async (data) => {\n      const url = data.url\n      navigator.clipboard.writeText(url)\n      message.success(t(\"share.notification.successGenerate\"))\n      await saveWebshare({\n        title: data.title,\n        url,\n        api_url: data.api_url,\n        share_id: data.share_id\n      })\n      setOpen(false)\n    },\n    onError: (error) => {\n      message.error(error?.message || t(\"share.notification.failGenerate\"))\n    }\n  })\n\n  return (\n    <Modal\n      title={t(\"share.modal.title\")}\n      open={open}\n      footer={null}\n      width={600}\n      onCancel={() => setOpen(false)}>\n      <Form\n        form={form}\n        layout=\"vertical\"\n        onFinish={createShareLink}\n        initialValues={{\n          title: t(\"share.form.defaultValue.title\"),\n          name: t(\"share.form.defaultValue.name\")\n        }}>\n        <Form.Item\n          name=\"title\"\n          label={t(\"share.form.title.label\")}\n          rules={[{ required: true, message: t(\"share.form.title.required\") }]}>\n          <Input size=\"large\" placeholder={t(\"share.form.title.placeholder\")} />\n        </Form.Item>\n        <Form.Item\n          name=\"name\"\n          label={t(\"share.form.name.label\")}\n          rules={[{ required: true, message: t(\"share.form.name.required\") }]}>\n          <Input size=\"large\" placeholder={t(\"share.form.name.placeholder\")} />\n        </Form.Item>\n\n        <Form.Item>\n          <div className=\"max-h-[180px] overflow-x-auto border dark:border-gray-700 rounded-md p-2\">\n            <div className=\"flex flex-col p-3\">\n              {messages.map((message, index) => (\n                <PlaygroundMessage\n                  key={index}\n                  {...message}\n                  name={message?.modelName}\n                  username={name}\n                />\n              ))}\n            </div>\n          </div>\n        </Form.Item>\n\n        <Form.Item>\n          <div className=\"flex justify-end\">\n            <button\n              type=\"submit\"\n              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 \">\n              {isPending\n                ? t(\"share.form.btn.saving\")\n                : t(\"share.form.btn.save\")}\n            </button>\n          </div>\n        </Form.Item>\n      </Form>\n    </Modal>\n  )\n}\n"
  },
  {
    "path": "src/components/Common/TableBlock.tsx",
    "content": "import { Dropdown, Tooltip, ConfigProvider, Modal } from \"antd\"\nimport {\n  CopyCheckIcon,\n  CopyIcon,\n  DownloadIcon,\n  TableIcon,\n  ExpandIcon,\n  XIcon\n} from \"lucide-react\"\nimport { FC, useState, useMemo, useRef } from \"react\"\nimport { useTranslation } from \"react-i18next\"\nimport { useStorage } from \"@plasmohq/storage/hook\"\n\ninterface TableProps {\n  children: React.ReactNode\n}\n\ninterface TableData {\n  headers: string[]\n  rows: string[][]\n}\n\nexport const TableBlock: FC<TableProps> = ({ children }) => {\n  const [copyStatus, setCopyStatus] = useState<string>(\"\")\n  const [isModalOpen, setIsModalOpen] = useState(false)\n  const { t } = useTranslation(\"common\")\n  const ref = useRef<HTMLDivElement>(null)\n  const [tableTextWrap] = useStorage(\"tableTextWrap\", false)\n\n  const parseData = () => {\n    // get table from ref\n    const table = ref.current\n    if (!table) return\n\n    const headers: string[] = []\n    const rows: string[][] = []\n\n    const headerCells = table.querySelectorAll(\"thead th\")\n    headerCells.forEach((cell) => {\n      headers.push(cell.textContent || \"\")\n    })\n\n    const bodyRows = table.querySelectorAll(\"tbody tr\")\n    bodyRows.forEach((row) => {\n      const rowData: string[] = []\n      const cells = row.querySelectorAll(\"td\")\n      cells.forEach((cell) => {\n        rowData.push(cell.textContent || \"\")\n      })\n      rows.push(rowData)\n    })\n\n    return { headers, rows }\n  }\n\n  const convertToCSV = () => {\n    const tableData = parseData()\n    if (!tableData) return\n\n    const escapeCSV = (value: string): string => {\n      if (value.includes(\",\") || value.includes('\"') || value.includes(\"\\n\")) {\n        return `\"${value.replace(/\"/g, '\"\"')}\"`\n      }\n      return value\n    }\n\n    const csvRows = []\n\n    // Add headers\n    if (tableData.headers.length > 0) {\n      csvRows.push(tableData.headers.map(escapeCSV).join(\",\"))\n    }\n\n    // Add data rows\n    tableData.rows.forEach((row) => {\n      csvRows.push(row.map(escapeCSV).join(\",\"))\n    })\n\n    return csvRows.join(\"\\n\")\n  }\n\n  const handleCopyCSV = () => {\n    const csvContent = convertToCSV()\n    navigator.clipboard.writeText(csvContent)\n    setCopyStatus(\"csv\")\n    setTimeout(() => setCopyStatus(\"\"), 3000)\n  }\n\n  const handleDownloadCSV = () => {\n    const csvContent = convertToCSV()\n    downloadFile(csvContent, `table-${Date.now()}.csv`, \"text/csv\")\n  }\n\n  const handleExpandTable = () => {\n    setIsModalOpen(true)\n  }\n\n  const handleCloseModal = () => {\n    setIsModalOpen(false)\n  }\n\n  const downloadFile = (\n    content: string,\n    filename: string,\n    mimeType: string\n  ) => {\n    const blob = new Blob([content], { type: mimeType })\n    const url = window.URL.createObjectURL(blob)\n    const a = document.createElement(\"a\")\n    a.href = url\n    a.download = filename\n    document.body.appendChild(a)\n    a.click()\n    document.body.removeChild(a)\n    window.URL.revokeObjectURL(url)\n  }\n\n  return (\n    <div>\n      <div className=\"my-4 bg-white dark:bg-[#1a1a1a] rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden\">\n        <div className=\"flex flex-row px-4 py-2 rounded-t-xl bg-gray-50 dark:bg-[#1a1a1a] border-b border-gray-200 dark:border-gray-700\">\n          <div className=\"flex items-center gap-2 flex-1\">\n            <TableIcon className=\"size-4 text-gray-600 dark:text-gray-300\" />\n            <span className=\"font-mono text-xs text-gray-700 dark:text-gray-300\">\n              Table\n            </span>\n          </div>\n\n          <div className=\"flex items-center gap-1\">\n            <Tooltip title=\"Copy as CSV\">\n              <button\n                onClick={handleCopyCSV}\n                className=\"flex gap-1.5 items-center rounded bg-none p-1 text-xs text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 hover:text-gray-800 dark:hover:text-gray-100 focus:outline-none transition-colors\">\n                {copyStatus === \"csv\" ? (\n                  <CopyCheckIcon className=\"size-4 text-green-500\" />\n                ) : (\n                  <CopyIcon className=\"size-4\" />\n                )}\n              </button>\n            </Tooltip>\n\n            <ConfigProvider\n              theme={{\n                components: {\n                  Dropdown: {\n                    colorBgElevated: \"#1a1a1a\",\n                    colorText: \"#ffffff\",\n                    colorBgTextHover: \"#2a2a2a\",\n                    borderRadiusOuter: 8,\n                    boxShadowSecondary:\n                      \"0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 9px 28px 8px rgba(0, 0, 0, 0.05)\"\n                  }\n                }\n              }}>\n              <Tooltip title=\"Download CSV\">\n                <button\n                  onClick={handleDownloadCSV}\n                  className=\"flex gap-1.5 items-center rounded bg-none p-1 text-xs text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 hover:text-gray-800 dark:hover:text-gray-100 focus:outline-none transition-colors\">\n                  <DownloadIcon className=\"size-4\" />\n                </button>\n              </Tooltip>\n            </ConfigProvider>\n          </div>\n        </div>\n\n        <div className=\"overflow-x-auto\">\n          <div\n            ref={ref}\n            className={`prose prose-gray dark:prose-invert max-w-none [&_table]:table-fixed [&_table]:w-full [&_table]:border-collapse [&_thead]:bg-neutral-50 [&_thead]:dark:bg-[#2a2a2a] [&_th]:px-6 [&_th]:py-4 [&_th]:text-left [&_th]:font-semibold [&_th]:text-gray-900 [&_th]:dark:text-gray-100 [&_th]:uppercase [&_th]:tracking-wider ${tableTextWrap ? '[&_th]:break-words [&_th]:overflow-wrap-anywhere' : '[&_th]:whitespace-nowrap'} [&_th:nth-child(1)]:w-1/2 [&_th:nth-child(2)]:w-1/2 [&_th:nth-child(3)]:w-1/3 [&_th]:border-b [&_th]:border-gray-200 [&_th]:dark:border-gray-700 [&_td]:px-6 [&_td]:py-4 [&_td]:text-gray-700 [&_td]:dark:text-gray-300 [&_td]:text-left ${tableTextWrap ? '[&_td]:break-words [&_td]:overflow-wrap-anywhere' : '[&_td]:whitespace-nowrap'} [&_td]:border-b [&_td]:border-gray-200 [&_td]:dark:border-gray-700 [&_tr:last-child_td]:border-b-0`}\n            style={{\n              fontSize: `calc(0.875rem * var(--font-scale, 1))`\n            }}>\n            {children}\n          </div>\n        </div>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Icons/AliBaBaCloud.tsx",
    "content": "import React from \"react\"\n\nexport const AliBaBaCloudIcon = React.forwardRef<\n  SVGSVGElement,\n  React.SVGProps<SVGSVGElement>\n>((props, ref) => {\n  return (\n    <svg xmlns=\"http://www.w3.org/2000/svg\" \n      className=\"icon\" \n      viewBox=\"0 0 1024 1024\" \n      fill=\"currentColor\"\n      fillRule=\"evenodd\"\n      ref={ref}\n      style={{ flex: \"none\", lineHeight: 1 }}\n      {...props}>\n      <path d=\"M207.872 234.496h200.192l-39.936 63.488-143.36 43.52c-23.04 7.168-44.032 19.456-43.52 43.52l1.024 250.88c0 24.064 20.48 37.888 43.52 43.52l136.704 35.328 47.104 74.752H207.872c-79.36 0-143.872-62.976-143.872-139.776V374.272c0-76.8 65.024-139.776 143.872-139.776zM816.128 234.496h-200.192l39.936 63.488 143.36 43.52c23.04 7.168 44.032 19.456 43.52 43.52l-1.024 250.88c0 24.064-20.48 37.888-43.52 43.52l-136.704 35.328-47.104 74.752h201.728c79.36 0 143.872-62.976 143.872-139.776V374.272c0-76.8-65.024-139.776-143.872-139.776z\" p-id=\"7083\"></path><path d=\"M365.056 477.696h300.032v66.048H365.056z\" p-id=\"7084\"></path>\n    </svg>\n  )\n})\n"
  },
  {
    "path": "src/components/Icons/AnthropicIcon.tsx",
    "content": "import React from \"react\"\n\nexport const AnthropicIcon = React.forwardRef<\n  SVGSVGElement,\n  React.SVGProps<SVGSVGElement>\n>((props, ref) => {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      style={{ flex: \"none\", lineHeight: 1 }}\n      viewBox=\"0 0 24 24\"\n      ref={ref}\n      fill=\"currentColor\"\n      fillRule=\"evenodd\"\n      {...props}>\n      <path d=\"M13.827 3.52h3.603L24 20h-3.603l-6.57-16.48zm-7.258 0h3.767L16.906 20h-3.674l-1.343-3.461H5.017l-1.344 3.46H0L6.57 3.522zm4.132 9.959L8.453 7.687 6.205 13.48H10.7z\" />\n    </svg>\n  )\n})\n"
  },
  {
    "path": "src/components/Icons/BigModelZhipuIcon.tsx",
    "content": "import React from \"react\"\n\nexport const BigModelZhipuIcon = React.forwardRef<\n  SVGSVGElement,\n  React.SVGProps<SVGSVGElement>\n>((props, ref) => {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      fill=\"none\"\n      viewBox=\"0 0 25 25\"\n        ref={ref}\n        {...props}\n      >\n      <g clipPath=\"url(#clip0_15376_1182)\">\n        <path\n          fill=\"url(#paint0_linear_15376_1182)\"\n          d=\"M21.052 11.79V5.916a.05.05 0 0 0-.025-.043L10.55.006A.1.1 0 0 0 10.525 0v2.458c0 4.77 3.986 9.326 8.903 9.326q-.15 0-.296.005z\"></path>\n        <path\n          fill=\"url(#paint1_linear_15376_1182)\"\n          d=\"M0 11.79v6.083q0 .029.024.044l10.477 6.076a.1.1 0 0 0 .024.007v-2.546c0-4.94-3.985-9.659-8.902-9.659q.15 0 .296-.005z\"></path>\n        <path\n          fill=\"url(#paint2_linear_15376_1182)\"\n          d=\"M0 11.79V5.916q0-.028.024-.043L10.501.006A.1.1 0 0 1 10.525 0v2.458c0 4.77-3.985 9.326-8.902 9.326q.15 0 .296.005z\"></path>\n        <path\n          fill=\"url(#paint3_linear_15376_1182)\"\n          d=\"M21.052 11.79v6.083a.05.05 0 0 1-.025.044L10.55 23.993a.1.1 0 0 1-.025.007v-2.546c0-4.94 3.986-9.659 8.903-9.659q-.15 0-.296-.005z\"></path>\n        <path\n          fill=\"#F7F8FA\"\n          d=\"M10.527 2.458c0 4.77 3.986 9.326 8.903 9.326l-.296.005q.147.006.296.006c-4.917 0-8.903 4.72-8.903 9.66 0-4.897-3.915-9.575-8.772-9.657l-.001-.006.167-.003-.167-.003v-.006c4.857-.079 8.773-4.595 8.773-9.322\"></path>\n      </g>\n      <defs>\n        <linearGradient\n          id=\"paint0_linear_15376_1182\"\n          x1=\"9.938\"\n          x2=\"20.463\"\n          y1=\"0.565\"\n          y2=\"7.686\"\n          gradientUnits=\"userSpaceOnUse\">\n          <stop stopColor=\"#988EFF\"></stop>\n          <stop offset=\"1\" stopColor=\"#2153DD\"></stop>\n        </linearGradient>\n        <linearGradient\n          id=\"paint1_linear_15376_1182\"\n          x1=\"9.348\"\n          x2=\"-1.571\"\n          y1=\"21.242\"\n          y2=\"11.426\"\n          gradientUnits=\"userSpaceOnUse\">\n          <stop stopColor=\"#8EAEFF\"></stop>\n          <stop offset=\"1\" stopColor=\"#2199DD\"></stop>\n        </linearGradient>\n        <linearGradient\n          id=\"paint2_linear_15376_1182\"\n          x1=\"1.283\"\n          x2=\"9.869\"\n          y1=\"11.79\"\n          y2=\"-0.121\"\n          gradientUnits=\"userSpaceOnUse\">\n          <stop stopColor=\"#988EFF\"></stop>\n          <stop offset=\"1\" stopColor=\"#2153DD\"></stop>\n        </linearGradient>\n        <linearGradient\n          id=\"paint3_linear_15376_1182\"\n          x1=\"21.052\"\n          x2=\"10.766\"\n          y1=\"11.298\"\n          y2=\"23.796\"\n          gradientUnits=\"userSpaceOnUse\">\n          <stop stopColor=\"#8EA7FF\"></stop>\n          <stop offset=\"1\" stopColor=\"#2161DD\"></stop>\n        </linearGradient>\n        <clipPath id=\"clip0_15376_1182\">\n          <path fill=\"#fff\" d=\"M0 0h123v25H0z\"></path>\n        </clipPath>\n      </defs>\n    </svg>\n  )\n})\n"
  },
  {
    "path": "src/components/Icons/CSVIcon.tsx",
    "content": "import React from \"react\"\n\nexport const CSVIcon = React.forwardRef<\n  SVGSVGElement,\n  React.SVGProps<SVGSVGElement>\n>((props, ref) => {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      version=\"1.1\"\n      viewBox=\"0 0 303.188 303.188\"\n      xmlSpace=\"preserve\"\n      ref={ref}\n      {...props}>\n      <path\n        fill=\"#E4E4E4\"\n        d=\"M219.821 0L32.842 0 32.842 303.188 270.346 303.188 270.346 50.525z\"></path>\n      <path\n        fill=\"#007934\"\n        d=\"M227.64 25.263L32.842 25.263 32.842 0 219.821 0z\"></path>\n      <g fill=\"#A4A9AD\">\n        <path d=\"M114.872 227.984c-2.982 0-5.311 1.223-6.982 3.666-1.671 2.444-2.507 5.814-2.507 10.109 0 8.929 3.396 13.393 10.188 13.393 2.052 0 4.041-.285 5.967-.856a59.8 59.8 0 005.808-2.063v10.601c-3.872 1.713-8.252 2.57-13.14 2.57-7.004 0-12.373-2.031-16.107-6.094-3.734-4.062-5.602-9.934-5.602-17.615 0-4.803.904-9.023 2.714-12.663 1.809-3.64 4.411-6.438 7.808-8.395 3.396-1.957 7.39-2.937 11.98-2.937 5.016 0 9.808 1.09 14.378 3.27l-3.841 9.871a42.982 42.982 0 00-5.141-2.031c-1.714-.55-3.554-.826-5.523-.826zM166.732 250.678c0 2.878-.729 5.433-2.191 7.665-1.459 2.232-3.565 3.967-6.315 5.205-2.751 1.237-5.977 1.856-9.681 1.856-3.089 0-5.681-.217-7.775-.65-2.095-.434-4.274-1.191-6.538-2.27v-11.172a37.254 37.254 0 007.458 2.872c2.582.689 4.951 1.032 7.109 1.032 1.862 0 3.227-.322 4.095-.969.867-.645 1.302-1.476 1.302-2.491 0-.635-.175-1.19-.524-1.666-.349-.477-.91-.958-1.682-1.444-.772-.486-2.83-1.48-6.173-2.983-3.026-1.375-5.296-2.708-6.809-3.999s-2.634-2.771-3.364-4.443-1.095-3.65-1.095-5.936c0-4.273 1.555-7.605 4.666-9.997 3.109-2.391 7.384-3.587 12.822-3.587 4.803 0 9.7 1.111 14.694 3.333l-3.841 9.681c-4.337-1.989-8.082-2.984-11.234-2.984-1.63 0-2.814.286-3.555.857s-1.111 1.28-1.111 2.127c0 .91.471 1.725 1.412 2.443.941.72 3.496 2.031 7.665 3.936 3.999 1.799 6.776 3.729 8.331 5.792 1.557 2.063 2.334 4.661 2.334 7.792zM199.964 218.368h14.027l-15.202 46.401H184.03l-15.139-46.401h14.092l6.316 23.519c1.312 5.227 2.031 8.865 2.158 10.918.148-1.481.443-3.333.889-5.555.443-2.222.835-3.967 1.174-5.236l6.444-23.646z\"></path>\n      </g>\n      <path fill=\"#D1D3D3\" d=\"M219.821 50.525L270.346 50.525 219.821 0z\"></path>\n      <path fill=\"#007934\" d=\"M134.957 80.344H168.231V95.762H134.957z\"></path>\n      <path fill=\"#007934\" d=\"M175.602 80.344H208.875V95.762H175.602z\"></path>\n      <path fill=\"#007934\" d=\"M134.957 102.661H168.231V118.08H134.957z\"></path>\n      <path fill=\"#007934\" d=\"M175.602 102.661H208.875V118.08H175.602z\"></path>\n      <path fill=\"#007934\" d=\"M134.957 124.979H168.231V140.397H134.957z\"></path>\n      <path fill=\"#007934\" d=\"M175.602 124.979H208.875V140.397H175.602z\"></path>\n      <path\n        fill=\"#007934\"\n        d=\"M94.312 124.979H127.58500000000001V140.397H94.312z\"></path>\n      <path fill=\"#007934\" d=\"M134.957 147.298H168.231V162.716H134.957z\"></path>\n      <path fill=\"#007934\" d=\"M175.602 147.298H208.875V162.716H175.602z\"></path>\n      <path\n        fill=\"#007934\"\n        d=\"M94.312 147.298H127.58500000000001V162.716H94.312z\"></path>\n      <path\n        fill=\"#007934\"\n        d=\"M127.088 116.162h-10.04l-6.262-10.041-6.196 10.041h-9.821l10.656-16.435L95.406 84.04h9.624l5.8 9.932 5.581-9.932h9.909l-10.173 16.369 10.941 15.753z\"></path>\n    </svg>\n  )\n})\n"
  },
  {
    "path": "src/components/Icons/CanopyWaveIcon.tsx",
    "content": "import React from \"react\"\n\nexport const CanopyWaveIcon = React.forwardRef<\n  SVGSVGElement,\n  React.SVGProps<SVGSVGElement>\n>((props, ref) => {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 230 219\"\n        fill=\"none\"\n        ref={ref}\n        {...props}\n      >\n      <path\n        fill=\"#20702A\"\n        d=\"M185.92 152.59c.23.23-5.84.41-13.48.41h-13.88l-8.41-8.54c-4.62-4.7-8.23-8.71-8.02-8.92.21-.22 4.28-.57 9.04-.79 7.37-.34 8.99-.12 10.99 1.49 1.29 1.04 7.07 5.06 12.84 8.92 5.77 3.87 10.69 7.21 10.92 7.43m-107.99-7.58L69.86 153l-28.36-.05 5.5-3.4c3.03-1.87 8.61-5.58 12.41-8.23 3.81-2.65 8.11-5.27 9.58-5.82 3.19-1.2 17.01-.72 17.01.59 0 .51-3.63 4.52-8.07 8.92m89.64-46.31 3.93 3.19-9.05.06-9.04.05-3.54-3.75c-5.09-5.39-11.22-13.23-10.74-13.72.23-.22 2.7-.51 5.5-.64 4.88-.22 5.35 0 12.04 5.69 3.83 3.26 8.74 7.36 10.9 9.12m-86.91-2.45L75.7 102H56.93l10.01-8.05c5.5-4.42 10.26-8.47 10.59-9 .7-1.13 11.47-1.3 11.47-.17 0 1.18-2.85 5.1-8.34 11.47\"></path>\n      <path\n        fill=\"#81B127\"\n        d=\"M77.93 145.01c4.44-4.4 8.07-8.41 8.07-8.92 0-1.31-13.82-1.79-17.01-.59-1.47.55-5.77 3.17-9.58 5.82-3.8 2.65-9.38 6.36-12.41 8.23l-5.18 3.2-1.83-4.26c-1.06-2.48-1.98-4.85-2.05-5.25-.07-.41-.76-2.26-1.53-4.11-.78-1.86-1.41-3.52-1.41-3.7s3.26-2.44 7.25-5.02C55.17 122.06 75 105.39 75 102.88c0-.48-4.29-.88-9.54-.88H75.7l4.96-5.75C86.15 89.88 89 85.96 89 84.78c0-1.13-10.77-.96-11.47.17-.33.53-5.09 4.58-10.59 9L56.93 102h-1.01l-3.03-6.75c-4.8-10.68-4.76-11.07 1.67-16.16 9.02-7.16 28.91-26.64 36.86-36.13 4.06-4.84 9.75-12.43 12.65-16.88 6.67-10.25 8.93-10.8 8.93-2.19 0 14.87-10 34.58-26.23 51.71-3.71 3.9-6.45 7.39-6.09 7.75S87.8 84 95.7 84c16.5 0 17.06.32 12.98 7.29-6.23 10.63-18.64 25.04-30.75 35.71l-7.94 7 10.75.21c5.92.11 13.57.24 17.01.28 4.37.06 6.75.58 7.89 1.72 1.52 1.52 1.45 1.96-.91 5.96-3.13 5.33-14.69 18.09-23.59 26.05-9.87 8.82-25.92 19.2-40.61 26.27-7.16 3.44-13.81 6.53-14.76 6.85-2.12.71-3.07-.96-4.77-8.34-.7-3.02-1.68-6.31-2.2-7.31-.78-1.52-.26-2.06 3.39-3.47 14.75-5.73 33.67-16.81 42.83-25.09l4.48-4.05-27.57-.08-.02-.04 27.95.04Zm89.64-46.31c-2.16-1.76-7.07-5.86-10.9-9.12-6.69-5.69-7.16-5.91-12.04-5.69-2.8.13-5.27.42-5.5.64-.48.49 5.65 8.33 10.74 13.72l3.54 3.75 9.04-.05 5.13-.03c-1.28.05-2.75.08-4.42.08h-9.6l6.47 6.58c7.08 7.2 23.8 20.27 29.95 23.41 2.14 1.09 4.09 2.33 4.33 2.75.25.42-1.03 4.58-2.85 9.26l-3.29 8.5-2.27.08c-.38-.32-5.22-3.62-10.9-7.42-5.77-3.86-11.55-7.88-12.84-8.92-1.24-.99-2.33-1.46-4.73-1.59.28-.04.49-.1.62-.18.56-.35-4.21-5.63-10.6-11.73-15.29-14.61-29.53-32.97-28.68-36.99.34-1.57 1.87-1.75 14.74-1.75 7.91 0 14.58-.34 14.84-.76s-2.93-4.58-7.08-9.25c-12.97-14.57-20.89-28.4-23.82-41.57-2.1-9.44-1.7-13.81 1.3-14.24 1.24-.18 2.25.13 2.25.69 0 1.27 7.95 12.93 13.73 20.13 6.41 7.98 25.24 26.96 36.55 36.84 5.38 4.7 9.58 9.02 9.33 9.6s-1.66 3.99-3.13 7.56-3.14 7.06-3.7 7.75c-.31.38-1.17.67-2.62.87Zm41.26 93.84c-1.12 4.38-2.33 8.26-2.68 8.63-1.54 1.6-25.93-10.38-40.09-19.68-12.87-8.46-25.43-19.59-34.57-30.64-7.72-9.32-9.78-13.43-7.46-14.87 1.82-1.12 25.16-2.35 29.45-1.55.44.08.88.15 1.3.19-1.02.02-2.22.07-3.61.13-4.76.22-8.83.57-9.04.79-.21.21 3.4 4.22 8.02 8.92l8.41 8.54h15.46-.07l-14.22.5 5.88 4.58c12.49 9.72 24.16 16.69 36.64 21.88 4.26 1.77 7.94 3.54 8.18 3.93.24.38-.48 4.28-1.6 8.65M81 200.61c-7.33 1.81-12.91 1.78-13.62-.06-.96-2.52.37-3.24 7.14-3.87 4.41-.41 9.71-1.95 16.03-4.65 16.93-7.24 30.73-7.22 48.76.05 6.24 2.51 12.38 4.25 16.37 4.62 6.89.64 8.23 1.5 6.19 3.96-1.75 2.1-10.24 1.34-18.37-1.65-13-4.77-17.24-6.4-21.59-6.85-2.25-.23-4.54-.14-8.07-.01-.42.02-.86.04-1.32.05-9.83.37-11.83.77-18.52 3.73-4.12 1.83-9.97 3.94-13 4.68m3-10.66c-7.01 2.26-15.74 3.28-16.57 1.93-.95-1.53-.11-1.95 5.73-2.86 7.4-1.16 14.58-3.35 19.52-5.95 6.2-3.26 19.56-5.17 27.98-3.99 8.57 1.21 8.8 1.27 20.53 6.01 5.32 2.15 11.4 3.95 13.5 4.01 2.09.05 4.88.4 6.19.77 2 .56 2.15.86.96 1.85-2.83 2.35-13.45-.08-27.84-6.37-6-2.61-7.46-2.83-19-2.83-11.52 0-13.01.22-19 2.83-3.57 1.55-8.97 3.62-12 4.6m-7.03-6.01c-8.08 1.31-8.7 1.31-9.51-.01l-.01-.02c-.25-.39-.43-.68-.38-.92.14-.62 1.82-.9 7.89-1.96 4.11-.71 10.95-2.78 15.2-4.6 16.74-7.16 33.2-7.19 48.98-.09 7.23 3.25 11.29 4.42 18.11 5.22 5.48.64 6.97 1.57 4.73 2.95-2.01 1.25-15.45-1.64-22.9-4.91-17.16-7.54-30.99-7.55-48.11-.02-4.15 1.82-10.45 3.78-14 4.36\"></path>\n    </svg>\n  )\n})\n"
  },
  {
    "path": "src/components/Icons/ChatSettings.tsx",
    "content": "import React from \"react\"\n\nexport const ChatSettings = React.forwardRef<\n  SVGSVGElement,\n  React.SVGProps<SVGSVGElement>\n>((props, ref) => {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      className=\"lucide lucide-message-circle-x\"\n      viewBox=\"0 0 24 24\"\n      ref={ref}\n      strokeWidth={2}\n      {...props}>\n      <path d=\"M7.9 20A9 9 0 104 16.1L2 22z\"\n      ></path>\n      <path\n      strokeWidth={1}\n\n        d=\"M12 2h-.44a2 2 0 00-2 2v.18a2 2 0 01-1 1.73l-.43.25a2 2 0 01-2 0l-.15-.08a2 2 0 00-2.73.73l-.22.38a2 2 0 00.73 2.73l.15.1a2 2 0 011 1.72v.51a2 2 0 01-1 1.74l-.15.09a2 2 0 00-.73 2.73l.22.38a2 2 0 002.73.73l.15-.08a2 2 0 012 0l.43.25a2 2 0 011 1.73V20a2 2 0 002 2H12a2 2 0 002-2v-.18a2 2 0 011-1.73l.43-.25a2 2 0 012 0l.15.08a2 2 0 002.73-.73l.22-.39a2 2 0 00-.73-2.73l-.15-.08a2 2 0 01-1-1.74v-.5a2 2 0 011-1.74l.15-.09a2 2 0 00.73-2.73l-.22-.38a2 2 0 00-2.73-.73l-.15.08a2 2 0 01-2 0L15 5.91a2 2 0 01-1-1.73V4a2 2 0 00-2-2z\"\n        transform=\"matrix(.5 0 0 .5 6 6)\"></path>\n      <circle cx=\"12\" cy=\"12\" r=\"0.5\"></circle>\n    </svg>\n  )\n})\n"
  },
  {
    "path": "src/components/Icons/ChutesIcon.tsx",
    "content": "import React from \"react\"\n\nexport const ChutesIcon = React.forwardRef<\n  SVGSVGElement,\n  React.SVGProps<SVGSVGElement>\n>((props, ref) => {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      fill=\"none\"\n      className=\"size-6\"\n      viewBox=\"0 0 62 41\"\n      ref={ref}\n      {...props}>\n      <path\n        fill=\"currentColor\"\n        d=\"M38.01 39.694c-.884 1.442-2.758 1.712-3.966.57l-5.37-5.074c-1.208-1.141-1.19-3.163.04-4.278l5.412-4.914c6.017-5.463 13.943-7.592 21.487-5.773l4.072.983c.146.035.28.109.392.214.59.557.26 1.597-.525 1.656l-.087.006c-7.45.557-14.284 4.907-18.49 11.77z\"></path>\n      <path\n        fill=\"url(#paint0_linear_10244_130)\"\n        d=\"M15.296 36.591c-1.123 1.246-3.02 1.131-4.005-.242L.547 21.371c-.98-1.366-.602-3.344.8-4.189L22.772 4.275C29.603.158 37.73-.277 44.809 3.093l15.54 7.403c.24.114.45.291.61.515.856 1.192-.06 2.895-1.453 2.704l-9.258-1.268c-7.393-1.013-14.834 1.838-20.131 7.712z\"></path>\n      <defs>\n        <linearGradient\n          id=\"paint0_linear_10244_130\"\n          x1=\"33.853\"\n          x2=\"25.55\"\n          y1=\"0.174\"\n          y2=\"41.449\"\n          gradientUnits=\"userSpaceOnUse\">\n          <stop stopColor=\"currentColor\"></stop>\n          <stop offset=\"1\" stopColor=\"currentColor\"></stop>\n        </linearGradient>\n      </defs>\n    </svg>\n  )\n})\n"
  },
  {
    "path": "src/components/Icons/DeepSeek.tsx",
    "content": "import React from \"react\"\n\nexport const DeepSeekIcon = React.forwardRef<\n  SVGSVGElement,\n  React.SVGProps<SVGSVGElement>\n>((props, ref) => {\n  return (\n    <svg\n      fill=\"currentColor\"\n      fillRule=\"evenodd\"\n      ref={ref}\n      style={{ flex: \"none\", lineHeight: 1 }}\n      viewBox=\"0 0 24 24\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      {...props}>\n      <path d=\"M23.748 4.482c-.254-.124-.364.113-.512.234-.051.039-.094.09-.137.136-.372.397-.806.657-1.373.626-.829-.046-1.537.214-2.163.848-.133-.782-.575-1.248-1.247-1.548-.352-.156-.708-.311-.955-.65-.172-.241-.219-.51-.305-.774-.055-.16-.11-.323-.293-.35-.2-.031-.278.136-.356.276-.313.572-.434 1.202-.422 1.84.027 1.436.633 2.58 1.838 3.393.137.093.172.187.129.323-.082.28-.18.552-.266.833-.055.179-.137.217-.329.14a5.526 5.526 0 01-1.736-1.18c-.857-.828-1.631-1.742-2.597-2.458a11.365 11.365 0 00-.689-.471c-.985-.957.13-1.743.388-1.836.27-.098.093-.432-.779-.428-.872.004-1.67.295-2.687.684a3.055 3.055 0 01-.465.137 9.597 9.597 0 00-2.883-.102c-1.885.21-3.39 1.102-4.497 2.623C.082 8.606-.231 10.684.152 12.85c.403 2.284 1.569 4.175 3.36 5.653 1.858 1.533 3.997 2.284 6.438 2.14 1.482-.085 3.133-.284 4.994-1.86.47.234.962.327 1.78.397.63.059 1.236-.03 1.705-.128.735-.156.684-.837.419-.961-2.155-1.004-1.682-.595-2.113-.926 1.096-1.296 2.746-2.642 3.392-7.003.05-.347.007-.565 0-.845-.004-.17.035-.237.23-.256a4.173 4.173 0 001.545-.475c1.396-.763 1.96-2.015 2.093-3.517.02-.23-.004-.467-.247-.588zM11.581 18c-2.089-1.642-3.102-2.183-3.52-2.16-.392.024-.321.471-.235.763.09.288.207.486.371.739.114.167.192.416-.113.603-.673.416-1.842-.14-1.897-.167-1.361-.802-2.5-1.86-3.301-3.307-.774-1.393-1.224-2.887-1.298-4.482-.02-.386.093-.522.477-.592a4.696 4.696 0 011.529-.039c2.132.312 3.946 1.265 5.468 2.774.868.86 1.525 1.887 2.202 2.891.72 1.066 1.494 2.082 2.48 2.914.348.292.625.514.891.677-.802.09-2.14.11-3.054-.614zm1-6.44a.306.306 0 01.415-.287.302.302 0 01.2.288.306.306 0 01-.31.307.303.303 0 01-.304-.308zm3.11 1.596c-.2.081-.399.151-.59.16a1.245 1.245 0 01-.798-.254c-.274-.23-.47-.358-.552-.758a1.73 1.73 0 01.016-.588c.07-.327-.008-.537-.239-.727-.187-.156-.426-.199-.688-.199a.559.559 0 01-.254-.078c-.11-.054-.2-.19-.114-.358.028-.054.16-.186.192-.21.356-.202.767-.136 1.146.016.352.144.618.408 1.001.782.391.451.462.576.685.914.176.265.336.537.445.848.067.195-.019.354-.25.452z\" />\n    </svg>\n  )\n})\n"
  },
  {
    "path": "src/components/Icons/Fireworks.tsx",
    "content": "import React from \"react\"\n\nexport const FireworksMonoIcon = React.forwardRef<\n  SVGSVGElement,\n  React.SVGProps<SVGSVGElement>\n>((props, ref) => {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      fill=\"none\"\n      viewBox=\"0 0 638 315\"\n      ref={ref}\n      {...props}>\n      <path\n        fill=\"#6720FF\"\n        d=\"M318.563 221.755c-17.7 0-33.584-10.508-40.357-26.777L196.549 0h47.793l74.5 178.361L393.273 0h47.793L358.92 195.048c-6.808 16.199-22.657 26.707-40.357 26.707zM425.111 314.933c-17.63 0-33.444-10.439-40.287-26.567-6.877-16.269-3.317-34.842 9.112-47.445l148.721-150.64 18.572 43.813-136.153 137.654 194.071-1.082 18.573 43.813-212.574.524-.07-.07h.035zM0 314.408l18.573-43.813 194.07 1.082L76.525 133.988l18.573-43.813 148.721 150.641c12.428 12.568 16.024 31.21 9.111 47.444-6.842 16.164-22.727 26.567-40.287 26.567L.07 314.339l-.07.069z\"></path>\n    </svg>\n  )\n})\n"
  },
  {
    "path": "src/components/Icons/GeminiIcon.tsx",
    "content": "import React from \"react\"\n\nexport const GeminiIcon = React.forwardRef<\n  SVGSVGElement,\n  React.SVGProps<SVGSVGElement>\n>((props, ref) => {\n  return (\n    <svg\n      fill=\"currentColor\"\n      fillRule=\"evenodd\"\n      ref={ref}\n      viewBox=\"0 0 24 24\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      {...props}>\n      <path d=\"M12 24A14.304 14.304 0 000 12 14.304 14.304 0 0012 0a14.305 14.305 0 0012 12 14.305 14.305 0 00-12 12\"></path>\n    </svg>\n  )\n})\n"
  },
  {
    "path": "src/components/Icons/Groq.tsx",
    "content": "import React from \"react\"\n\nexport const GroqMonoIcon = React.forwardRef<\n  SVGSVGElement,\n  React.SVGProps<SVGSVGElement>\n>((props, ref) => {\n  return (\n    <svg\n      fill=\"currentColor\"\n      fillRule=\"evenodd\"\n      ref={ref}\n      viewBox=\"0 0 24 24\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      {...props}>\n      <path d=\"M12.036 2c-3.853-.035-7 3-7.036 6.781-.035 3.782 3.055 6.872 6.908 6.907h2.42v-2.566h-2.292c-2.407.028-4.38-1.866-4.408-4.23-.029-2.362 1.901-4.298 4.308-4.326h.1c2.407 0 4.358 1.915 4.365 4.278v6.305c0 2.342-1.944 4.25-4.323 4.279a4.375 4.375 0 01-3.033-1.252l-1.851 1.818A7 7 0 0012.029 22h.092c3.803-.056 6.858-3.083 6.879-6.816v-6.5C18.907 4.963 15.817 2 12.036 2z\"></path>\n    </svg>\n  )\n})\n"
  },
  {
    "path": "src/components/Icons/HuggingFaceIcon.tsx",
    "content": "import React from \"react\"\n\nexport const HuggingFaceIcon = React.forwardRef<\n  SVGSVGElement,\n  React.SVGProps<SVGSVGElement>\n>((props, ref) => {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 24 24\"\n      {...props}\n      ref={ref}\n      >\n      <path\n        fill=\"#FF9D0B\"\n        d=\"M2.25 11.535c0-3.407 1.847-6.554 4.844-8.258a9.82 9.82 0 0 1 9.687 0c2.997 1.704 4.844 4.851 4.844 8.258 0 5.266-4.337 9.535-9.687 9.535S2.25 16.8 2.25 11.535\"></path>\n      <path\n        fill=\"#FFD21E\"\n        d=\"M11.938 20.086c4.797 0 8.687-3.829 8.687-8.551s-3.89-8.55-8.687-8.55c-4.798 0-8.688 3.828-8.688 8.55s3.89 8.55 8.688 8.55z\"></path>\n      <path\n        fill=\"#FF323D\"\n        d=\"M11.875 15.113c2.457 0 3.25-2.156 3.25-3.263 0-.576-.393-.394-1.023-.089-.582.283-1.365.675-2.224.675-1.798 0-3.25-1.693-3.25-.586s.79 3.263 3.25 3.263z\"></path>\n      <path\n        fill=\"#3A3B45\"\n        d=\"M14.76 9.21c.32.108.445.753.767.585.447-.233.707-.708.659-1.204a1.235 1.235 0 0 0-.879-1.059 1.26 1.26 0 0 0-1.33.394c-.322.384-.377.92-.14 1.36.153.283.638-.177.925-.079zm-5.887 0c-.32.108-.448.753-.768.585a1.23 1.23 0 0 1-.658-1.204c.048-.495.395-.913.878-1.059a1.26 1.26 0 0 1 1.33.394c.322.384.377.92.14 1.36-.152.283-.64-.177-.925-.079zm1.12 5.34a2.17 2.17 0 0 1 1.325-1.106c.07-.02.144.06.219.171l.192.306c.069.1.139.175.209.175.074 0 .15-.074.223-.172l.205-.302c.08-.11.157-.188.234-.165.537.168.986.536 1.25 1.026.932-.724 1.275-1.905 1.275-2.633 0-.508-.306-.426-.81-.19l-.616.296c-.52.24-1.148.48-1.824.48s-1.302-.24-1.823-.48l-.589-.283c-.52-.248-.838-.342-.838.177 0 .703.32 1.831 1.187 2.56z\"></path>\n      <path\n        fill=\"#FF9D0B\"\n        d=\"M17.812 10.366a.806.806 0 0 0 .813-.8c0-.441-.364-.8-.813-.8a.806.806 0 0 0-.812.8c0 .442.364.8.812.8m-11.624 0a.806.806 0 0 0 .812-.8c0-.441-.364-.8-.812-.8a.806.806 0 0 0-.813.8c0 .442.364.8.813.8m-1.673 2.707c-.405 0-.765.162-1.017.46a1.46 1.46 0 0 0-.333.925 1.8 1.8 0 0 0-.485-.074c-.387 0-.737.146-.985.409a1.41 1.41 0 0 0-.2 1.722 1.3 1.3 0 0 0-.447.694c-.06.222-.12.69.2 1.166a1.27 1.27 0 0 0-.093 1.236c.238.533.81.958 1.89 1.405l.24.096c.768.3 1.473.492 1.478.494.89.243 1.808.375 2.732.394 1.465 0 2.513-.443 3.115-1.314.93-1.342.842-2.575-.274-3.763l-.151-.154c-.692-.684-1.155-1.69-1.25-1.912-.195-.655-.71-1.383-1.562-1.383-.46.007-.889.233-1.15.605-.25-.31-.495-.553-.715-.694a1.87 1.87 0 0 0-.993-.312m14.97 0a1.3 1.3 0 0 1 1.017.46c.216.262.333.588.333.925q.238-.07.487-.074c.388 0 .738.146.985.409a1.41 1.41 0 0 1 .2 1.722c.22.178.377.422.445.694.06.222.12.69-.2 1.166.244.37.279.836.093 1.236-.238.533-.81.958-1.889 1.405l-.239.096c-.77.3-1.475.492-1.48.494-.89.243-1.808.375-2.732.394-1.465 0-2.513-.443-3.115-1.314-.93-1.342-.842-2.575.274-3.763l.151-.154c.695-.684 1.157-1.69 1.252-1.912.195-.655.708-1.383 1.56-1.383.46.007.889.233 1.15.605.25-.31.495-.553.718-.694.244-.162.523-.265.814-.3z\"></path>\n      <path\n        fill=\"#FFD21E\"\n        d=\"M9.785 20.132c.688-.994.638-1.74-.305-2.667-.945-.928-1.495-2.288-1.495-2.288s-.205-.788-.672-.714-.81 1.25.17 1.971c.977.721-.195 1.21-.573.534-.375-.677-1.405-2.416-1.94-2.751-.532-.332-.907-.148-.782.541.125.687 2.357 2.35 2.14 2.707-.218.362-.983-.42-.983-.42S2.953 14.9 2.43 15.46c-.52.558.398 1.026 1.7 1.803 1.308.778 1.41.985 1.225 1.28-.187.295-3.07-2.1-3.34-1.083-.27 1.011 2.943 1.304 2.745 2.006-.2.7-2.265-1.324-2.685-.537-.425.79 2.913 1.718 2.94 1.725 1.075.276 3.813.859 4.77-.522m4.432 0c-.687-.994-.64-1.74.305-2.667.943-.928 1.493-2.288 1.493-2.288s.205-.788.675-.714c.465.074.807 1.25-.17 1.971-.98.721.195 1.21.57.534.377-.677 1.407-2.416 1.94-2.751.532-.332.91-.148.782.541-.125.687-2.355 2.35-2.137 2.707.215.362.98-.42.98-.42S21.05 14.9 21.57 15.46c.52.558-.395 1.026-1.7 1.803-1.308.778-1.408.985-1.225 1.28.187.295 3.07-2.1 3.34-1.083.27 1.011-2.94 1.304-2.743 2.006.2.7 2.263-1.324 2.685-.537.423.79-2.912 1.718-2.94 1.725-1.077.276-3.815.859-4.77-.522\"></path>\n    </svg>\n  )\n})\n"
  },
  {
    "path": "src/components/Icons/InfinigenceAI.tsx",
    "content": "import React from \"react\"\n\nexport const InfinigenceAI = React.forwardRef<\n  SVGSVGElement,\n  React.SVGProps<SVGSVGElement>\n>((props, ref) => {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 24 25\"\n      fill=\"none\"\n      ref={ref}\n      {...props}>\n      <rect y=\"0.987091\" width=\"24\" height=\"24\" rx=\"4\" fill=\"white\"/>\n      <path d=\"M13.754 18.5639V7.48907H7.25702V10.4151H10.246V18.5639H7.25702V21.4742H16.743V18.5639H13.754Z\" fill=\"#7F1084\"/>\n      <path d=\"M16.743 4.5H13.754V7.48895H16.743V4.5Z\" fill=\"#2EA7E0\"/>\n    </svg>\n  )\n})"
  },
  {
    "path": "src/components/Icons/LMStudio.tsx",
    "content": "import React from \"react\"\n\nexport const LMStudioIcon = React.forwardRef<\n  SVGSVGElement,\n  React.SVGProps<SVGSVGElement>\n>((props, ref) => {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      version=\"1.1\"\n      viewBox=\"0 0 400 400\"\n      ref={ref}\n      {...props}>\n      <g fillRule=\"evenodd\" stroke=\"none\">\n        <path\n          fill=\"#e8e7fb\"\n          d=\"M67.4 72.259c-5.769 1.202-9.512 4.038-12.288 9.309-4.893 9.293.894 20.401 12.014 23.06 3.175.759 143.468.805 146.874.048 10.342-2.298 16.347-13.881 11.829-22.816-2.637-5.217-7.014-8.83-11.479-9.475-2.46-.356-145.259-.478-146.95-.126m68.344 45.342c-8.892 1.302-15.53 12.726-12.41 21.358.802 2.219 4.759 7.547 6.119 8.239.631.321 1.867 1.038 2.747 1.593l1.6 1.009 32 .255c49.309.393 112.007.372 115.91-.039 6.911-.727 11.083-4.282 14.24-12.135 3.048-7.582-4.232-19.056-12.885-20.308-2.974-.431-144.374-.404-147.321.028M96.2 162.752c-4.884.616-12.6 6.458-12.6 9.54 0 .411-.179.927-.399 1.147-.644.644-.939 7.331-.412 9.361.256.99.573 2.25.703 2.8.596 2.516 4.449 6.408 8.059 8.143L95 195.4h148.417l2.632-1.296c13.165-6.481 12.698-24.333-.802-30.641l-1.847-.863-72.6-.05c-39.93-.027-73.5.064-74.6.202m-53 48.574c-4.023 1.665-4.585 2.031-6.894 4.486-8.512 9.048-3.066 24.451 9.494 26.852 4.488.858 143.367.784 147.474-.078 4.985-1.047 8.598-4.273 11.409-10.186 3.248-6.832-.763-16.99-8.083-20.469l-2.8-1.331-74.2-.092-74.2-.092-2.2.91m78.6 44.266l-27.6.208-2.812 1.4c-3.134 1.561-7.186 5.547-7.607 7.484a11.163 11.163 0 01-.712 2.079c-.623 1.279-.569 9.897.065 10.531.257.257.466.762.466 1.125 0 1.889 4.333 6.484 7.516 7.968.596.279 1.444.71 1.884.96 1.519.86 141.142 1.459 147.81.633 7.495-.928 11.802-4.425 14.294-11.608 2.292-6.603-1.908-16.104-8.504-19.241l-2.8-1.331-23.6-.249c-26.499-.28-57.576-.267-98.4.041m84 45.243c-1.32.193-2.76.51-3.2.704-.44.195-1.34.597-2 .893-1.379.62-6.191 5.402-6.86 6.818-3.445 7.292-1.386 16.437 4.74 21.055.375.282.915.741 1.2 1.021 1.74 1.7.964 1.674 49.12 1.674 51.793 0 47.612.277 51.219-3.387 9.072-9.214 6.427-23.756-5.115-28.125l-2.304-.873-42.2-.065c-23.21-.036-43.28.092-44.6.285\"></path>\n        <path\n          fill=\"#5b3dd2\"\n          d=\"M347.9 1.127c-.669.658-1.1 1.511-1.1 2.177 0 .829-.195 1.096-.8 1.096-.882 0-1.09.63-.3.912.291.104.352.327.145.533-.206.207-.429.146-.533-.145-.545-1.525-3.712.883-3.712 2.822 0 .593-.18 1.078-.4 1.078-.22 0-.4.36-.4.8 0 .44-.18.8-.4.8-.22 0-.4-.18-.4-.4 0-.22-.36-.4-.8-.4-.907 0-1.025.415-.32 1.12.334.334.344.48.033.48-.245 0-.525-.225-.621-.5-.126-.359-.42-.282-1.043.271-.592.525-.738.9-.458 1.18.638.638.484 1.049-.391 1.049-.882 0-1.09.63-.3.912.291.104.352.327.145.533-.206.207-.429.146-.533-.145-.531-1.488-1.712-.47-1.712 1.476 0 1.087-.18 2.088-.4 2.224-.241.149-.4-.388-.4-1.353 0-1.724-.644-2.555-1.062-1.372-.142.399-.59 1.096-.997 1.55-.721.804-.719.847.06 1.683.847.91 1.136 1.789.319.972-.538-.538-1.92-.654-1.92-.16 0 .176.216.536.48.8.373.373.373.48 0 .48-.264 0-.48-.18-.48-.4 0-.22-.36-.4-.8-.4-.533 0-.8.267-.8.8 0 .444-.267.8-.6.8-.33 0-.6-.191-.6-.424 0-.233-.18-.312-.4-.176-.565.349-.497 1.688.1 1.967.4.186.4.28 0 .466-.275.129-.5.579-.5 1 0 .489-.278.767-.767.767-.421 0-.858.225-.969.5-.153.377-.304.355-.615-.093-.349-.504-.579-.437-1.53.45-.615.574-1.389 1.144-1.719 1.266-.451.168-.376.291.3.497 1.115.339 1.181.98.1.98-.882 0-1.09.63-.3.912.291.104.352.327.145.533-.206.207-.429.146-.533-.145-.252-.705-1.513-.622-2.147.142-.292.353-.955.802-1.473.999-.973.37-.883 1.159.132 1.159.327 0 .576.38.576.88 0 .684-.107.773-.48.4-.58-.58-2.72-.643-2.72-.08 0 .22.36.4.8.4.44 0 .8.18.8.4 0 .22-.36.4-.8.4-.44 0-.8.18-.8.4 0 .22-.332.4-.738.4-1.121 0-2.399 1.544-1.658 2.003.315.194.475.451.355.571-.119.12-.602-.141-1.071-.578-.907-.845-1.783-1.131-.968-.316.564.564.644 1.92.113 1.92-.201 0-.447-.225-.545-.5-.104-.291-.327-.352-.533-.145-.207.206-.146.429.145.533.591.211.683 2.512.1 2.512-.22 0-.4-.242-.4-.538 0-.296-.391-.881-.868-1.3-.854-.75-.863-.75-.587.005.203.553-.059 1.119-.934 2.022l-1.216 1.255.843.904c.463.498.71 1.028.547 1.178-.162.151-.375.049-.473-.226-.194-.543-1.712-.703-1.712-.18 0 .176.216.536.48.8.373.373.373.48 0 .48-.264 0-.48-.18-.48-.4 0-.579-.893-.492-2.339.229-1.442.72-1.624 1.733-.461 2.571.44.317.8.739.8.937 0 .199-.273.135-.606-.142-.502-.417-.752-.339-1.454.451-.568.639-.907.789-1.027.454-.195-.543-1.713-.703-1.713-.18 0 .176.216.536.48.8.373.373.29.48-.376.48-.471 0-.973.187-1.114.416-.142.229-.646.293-1.121.143-.726-.231-.785-.181-.375.313.383.461.383.691 0 1.074-.383.383-.368.561.068.83.441.273.324.575-.574 1.472-.714.715-1.189.95-1.292.641-.089-.269-.403-.489-.696-.489-.293 0-.598-.194-.677-.431-.079-.238-.479-.06-.895.4-.417.461-.546.831-.29.831.254 0 .462-.18.462-.4 0-.22.18-.4.4-.4.22 0 .4.45.4 1 0 .554-.232 1-.52 1-1.37 0-2.674 1.023-2.677 2.1-.002.821-.2 1.1-.779 1.1-.427 0-.896.192-1.041.427-.145.235-.539.322-.874.193-.336-.128-.858-.029-1.16.222-.684.568-.717 1.835-.053 2.057.355.118.292.37-.219.881-.393.393-.836.594-.984.446-.149-.148-.01-.43.307-.626s.456-.478.307-.626c-.317-.317-1.52.811-2.129 1.995-.258.501-.759.796-1.263.743-.889-.094-2.019.917-1.55 1.386.153.153.432.03.62-.274.286-.463.446-.45.979.083.807.806.835 1.356.036.693-.479-.397-.721-.397-1.2 0-.415.344-.6.359-.6.049 0-.247-.18-.449-.4-.449-.557 0-.501 1.339.08 1.92.334.334.344.48.033.48-.245 0-.522-.225-.615-.5-.223-.664-1.313.215-2.001 1.614-.701 1.428-1.813 2.058-2.287 1.298-.201-.324-.494-.461-.65-.306-.155.156.375.904 1.179 1.662.803.758 1.461 1.482 1.461 1.609s.552.808 1.228 1.515c.675.706 1.327 1.785 1.45 2.396.122.612.38 1.112.572 1.112.193 0 .35.324.35.72 0 .396.202.922.449 1.169 1.04 1.04.613 8.502-.666 11.662-1.298 3.204-4.378 6.849-5.788 6.849-.217 0-.395.142-.395.316 0 .173-1.035.824-2.3 1.445a365.85 365.85 0 00-2.995 1.485c-.382.195-6.457.354-13.5.354-10.852 0-12.805.087-12.805.573 0 .329-.47.628-1.1.7-1.095.126-1.94 1.927-.904 1.927.248 0 .334.189.192.42-.16.258-.546.09-1.003-.437-.973-1.122-1.585-.689-1.585 1.121 0 1.132-.107 1.349-.48.976-.538-.538-1.92-.654-1.92-.16 0 .176.188.508.417.737.449.449-.193 1.158-1.752 1.935-.467.233-1.072.87-1.344 1.416l-.495.992 24.887.28c13.688.154 34.967.387 47.287.517 29.008.308 32.901.513 35.8 1.884 12.567 5.943 12.674 24.025.179 30.319L337 150.2l-29.2.25c-16.06.137-47.875.272-70.7.3-36.6.044-41.5.121-41.5.65 0 .726-.18.744-1.187.115-1.018-.635-1.402-.207-.751.836.409.654.415.932.028 1.319-.387.387-.49.255-.49-.623 0-1.26-.907-1.271-1.146-.014-.081.422-.351.916-.6 1.097-.333.243-.32.385.046.534.275.111.5.404.5.649 0 .311-.146.301-.48-.033-.538-.538-1.92-.654-1.92-.16 0 .176.188.508.417.737.289.289.223.578-.217.943-.476.395-.748.412-1.091.069-.542-.542-1.909.485-1.909 1.433 0 .317-.544 1.085-1.209 1.706l-1.209 1.13 12.509.031c12.353.031 16.393.094 54.909.846 10.78.211 25.106.384 31.836.384l12.236.001 2.364 1.187c1.3.653 2.851 1.643 3.447 2.2.595.557 1.258 1.013 1.473 1.013.598 0 3.15 3.278 3.843 4.937 3.729 8.925.173 18.505-8.43 22.709L295.8 195.8l-27.4.311c-15.07.17-48.43.44-74.132.6-43.852.271-46.764.332-47.244.989-.282.385-.807.7-1.168.7-.361 0-.656.18-.656.4 0 .22-.36.4-.8.4-.907 0-1.025.415-.32 1.12.705.705 1.12.587 1.12-.32 0-.44.18-.8.4-.8.62 0 .472 1.185-.236 1.893-.541.541-.691.552-1 .072-.312-.484-.418-.48-.742.023-.208.324-.518.449-.689.279-.17-.171-.05-.471.267-.667.317-.196.448-.485.291-.642-.378-.378-1.891 1.008-1.891 1.731 0 .4-.263.485-.9.293-.734-.222-.808-.175-.4.253.595.625.672 2.765.1 2.765-.22 0-.4-.18-.4-.4 0-.95-1.567-.264-3.2 1.4-.972.99-1.999 1.8-2.283 1.8-.707 0-.653.662.076.942.327.125 13.399.238 29.05.25 15.651.012 28.587.103 28.745.201.159.098 8.462.22 18.45.272 9.989.052 22.032.201 26.762.331l8.6.236 2.742 1.382c12.14 6.118 13.016 20.707 1.759 29.283-3.543 2.699-10.526 2.895-110.501 3.095-22.66.046-40.975.171-40.7.279.275.107.5.361.5.562 0 .531-1.356.451-1.92-.113-.815-.815-.529.061.316.968.437.469.683.957.545 1.083s-.331.004-.429-.271c-.387-1.084-3.712.256-3.712 1.496 0 .406-.225.818-.5.916-.275.098-.355.333-.178.522.177.188.505.078.73-.246.309-.445.458-.466.612-.088.111.275.548.5.969.5.422 0 .767.18.767.4 0 .559-.935.499-1.57-.1-.424-.4-.567-.4-.713 0-.22.6-1.567.666-1.921.093-.401-.649-.996-.034-.996 1.031 0 .539-.235.976-.525.976-.639 0-2.445 1.897-2.856 3-.288.77-.312.765-.656-.126-.435-1.131-1.163-.787-1.163.55 0 .537-.18.976-.4.976-.22 0-.4-.208-.4-.462 0-.262-.345-.15-.799.261-.624.565-.697.844-.334 1.282.37.446.288.606-.401.786-.881.23-1.214.933-.442.933.233 0 .3.199.15.443-.195.315-.362.3-.579-.052-.433-.7-1.194-.108-1.198.933-.003.931-1.653 1.983-2.562 1.634-.776-.298-1.555.91-1.284 1.991.153.609.024 1.14-.355 1.454-.361.3-.596.331-.596.079 0-.228.225-.496.5-.594.275-.098.366-.322.202-.497-.482-.517-1.902 1.049-1.902 2.098 0 .783-.151.917-.8.711-.911-.289-1.205 1.189-.3 1.512.291.104.352.327.145.533-.206.207-.429.146-.533-.145-.098-.275-.524-.5-.945-.5-.82 0-1.031-.589-.36-1.004.223-.138.307-.412.185-.609-.357-.578-1.792 1.129-1.792 2.133 0 .506-.216 1.136-.48 1.4-.373.373-.373.48 0 .48.264 0 .48.203.48.45 0 .278-.422.366-1.104.229-1.491-.298-2.983 2.538-1.596 3.033.275.098.374.315.219.481-.368.396-1.519-.765-1.519-1.531 0-.327.212-.666.471-.752.259-.087.342-.365.185-.619-.379-.614-1.856.842-1.856 1.829 0 1.299-.96 2.878-1.623 2.67-.356-.111-1.116.321-1.775 1.008-.633.661-1.514 1.202-1.957 1.202-.626 0-.698.107-.325.48.581.581.637 1.92.08 1.92-.22 0-.4-.208-.4-.462 0-.262-.346-.149-.8.262-.44.398-.8 1.135-.8 1.638 0 1.455-.735 1.513-.874.07-.158-1.64-.88-1.713-1.061-.108-.167 1.479-.729 2.2-1.714 2.2-.468 0-.751.283-.751.751 0 .413-.289.991-.642 1.284-.353.292-.802.955-.999 1.473-.37.973-1.159.883-1.159-.132 0-.317-.18-.576-.4-.576-.517 0-.517.923 0 1.44.267.267.234.759-.1 1.477-.591 1.273-1.1 1.404-1.1.283 0-.44-.18-.8-.4-.8-.22 0-.4.36-.4.8 0 .612-.267.8-1.133.8-1.403 0-1.8.357-1.2 1.08.346.417.345.668-.004 1.017-.255.255-.463.877-.463 1.383s-.18.92-.4.92c-.22 0-.4-.36-.4-.8 0-.44-.18-.8-.4-.8-.22 0-.4.692-.4 1.538 0 1.98-1.236 3.282-2.417 2.544-1.055-.659-2.322-.661-1.663-.002.662.662.601 1.12-.149 1.12-.808 0-1.771.963-1.771 1.771 0 .706-.62.844-1.004.222-.49-.792-.996.077-.996 1.711 0 1.288-.107 1.549-.48 1.176-.564-.564-1.92-.644-1.92-.113 0 .201.225.442.5.535.642.216-.33 1.498-1.135 1.498-.941 0-2.965 2.312-2.965 3.387 0 .557-.18 1.013-.4 1.013-.22 0-.4-.18-.4-.4 0-.22-.36-.4-.8-.4-.858 0-1.073.584-.374 1.016.594.367-.746 1.784-1.688 1.784-.471 0-.738.289-.738.8 0 .882-.63 1.09-.912.3-.098-.275-.333-.355-.522-.178-.188.177-.124.457.144.623.382.236.38.532-.011 1.375-.61 1.312-.915 1.354-1.303.18l-.298-.9-.177.9c-.195.992-.536 1.125-1.196.465-.291-.291-.518-.3-.686-.027a.48.48 0 00.154.658c.875.54.439 1.804-.622 1.804-1.157 0-2.171.827-2.171 1.771 0 .346-.332.629-.738.629-1.404 0-2.799 2.488-1.616 2.882.245.082.354.502.242.933-.191.736-.221.729-.492-.115-.342-1.065-.461-1.092-1.264-.289-.488.488-.513.73-.123 1.2.299.361.328.589.073.589-.228 0-.496-.225-.595-.5-.117-.325-.519-.115-1.146.6-.532.605-1.204 1.1-1.493 1.1-.29 0-1.031.54-1.648 1.2-.617.66-1.409 1.2-1.76 1.2-.71 0-.83.439-.28 1.02.218.231-.245.966-1.181 1.873-.848.821-1.65 1.316-1.784 1.1-.407-.659-.995-.438-.995.374 0 .421-.225.842-.5.935-.39.131-.379.314.049.833.357.432.392.665.1.665a.44.44 0 01-.449-.428c0-1.174-1.805-.116-1.925 1.128-.094.976-.319 1.3-.9 1.3-.76 0-.775.598-.775 30.4V400h400V0h-47.706l-.75.864c-.412.475-.878.736-1.035.578-.157-.157-.026-.446.291-.642s.437-.496.267-.667c-.171-.17-.494-.045-.719.279-.309.445-.458.466-.612.088-.286-.705-.584-.603-1.836.627m-23.44 23.524c-.517.551-.843 1.099-.724 1.218.216.216 2.264-1.639 2.264-2.051 0-.471-.644-.123-1.54.833m-24.8 24.791c-.729.824-.26 1.215.521.434.34-.34.619-.722.619-.847 0-.42-.594-.205-1.14.413m-6.46 4.925c0 .201.225.458.5.569.378.154.357.303-.088.612-.324.225-.468.53-.319.678.148.148.56-.022.917-.378.575-.576.58-.715.047-1.248-.684-.684-1.057-.766-1.057-.233m-51.32 200.421c1.083.218 13.167.436 27.72.5l25.8.112 2.725 1.291c8.65 4.097 11.7 11.48 8.968 21.709-.567 2.121-5.397 7.101-8.089 8.34-3.84 1.768 6.594 1.603-146.747 2.318-57.562.268-60.657.189-60.657-1.538 0-.176-.272-.32-.605-.32-.846 0-2.087-.772-3.763-2.339-.788-.736-1.837-1.612-2.332-1.946-.495-.334-.9-.856-.9-1.161 0-.305-.18-.554-.4-.554-.22 0-.4-.54-.4-1.2 0-.66-.138-1.2-.306-1.2-1.415 0-2.366-10.159-1.107-11.824.323-.427.593-1.091.6-1.476.007-.385.213-.7.456-.7.275 0 .347-.385.191-1.009-.178-.707-.071-1.079.356-1.243.336-.128.61-.399.61-.601 0-.504 3.48-3.947 3.989-3.947.226 0 .411-.27.411-.6 0-.33.349-.6.776-.6.427 0 .888-.18 1.024-.4.136-.22.597-.4 1.024-.4.427 0 .776-.18.776-.4 0-.22.529-.4 1.176-.4.647 0 1.288-.18 1.424-.4.334-.54 144.606-.552 147.28-.012M60.8 282.4c0 .44-.18.8-.4.8-.22 0-.4-.36-.4-.8 0-.44.18-.8.4-.8.22 0 .4.36.4.8m-10.924 11.419c.341.341.498.74.35.888-.148.149-.426.017-.617-.293-.288-.466-.457-.472-.979-.039-.75.623-.84.229-.13-.577.633-.719.636-.719 1.376.021m-6.276 3.557c0 .563.18 1.024.4 1.024.22 0 .4-.349.4-.776 0-.427-.18-.888-.4-1.024-.227-.14-.4.196-.4.776m249 2.919c.44.135 12.59.352 27 .482 20.984.189 26.439.345 27.4.786.66.302 1.785.803 2.5 1.112 5.953 2.576 9.633 11.582 7.896 19.325-.64 2.852-6.726 10-8.513 10-.398 0-.904.181-1.124.401-.947.947-143.384 2.134-144.316 1.202-.221-.221-.756-.403-1.188-.403-1.915 0-7.766-5.027-8.963-7.7-1.574-3.516-2.249-6.74-1.952-9.318.443-3.836.748-5.389 1.102-5.608.197-.122.358-.526.358-.898.001-1.076 5.176-6.755 6.87-7.539.511-.236 1.41-.656 1.997-.933 3.209-1.515 4.025-1.537 47.733-1.343 23.32.103 42.76.299 43.2.434M21.076 320.981c-.912.912-1.322.754-.474-.183.4-.442.809-.72.91-.619.101.1-.095.461-.436.802M12 329.112c0 .171-.36.571-.8.888-.44.317-.8.897-.8 1.288 0 .392-.18.712-.4.712-.703 0-.43-1.197.5-2.19.921-.983 1.5-1.253 1.5-.698\"></path>\n        <path\n          fill=\"#6e6be5\"\n          d=\"M32.4.6c.653.787-.133 1.814-1.292 1.688-1.103-.119-2.558 2.329-1.874 3.153.336.405.34.559.015.559-.247 0-.449-.18-.449-.4 0-.68-1.439-.441-2.162.359-.498.55-.551.803-.192.923.73.243.036 1.118-.886 1.118-.437 0-1.18.36-1.652.8-.472.44-1.082.8-1.354.8-.748 0-1.756 2.238-1.24 2.754.293.293.216.498-.254.678-.452.174-.949-.091-1.512-.808-.462-.586-.939-.966-1.062-.843-.122.122.182.652.676 1.178.575.612.743 1.052.468 1.222-.456.282-.606 1.819-.177 1.819.139 0 .59-.389 1.003-.864.412-.475.857-.756.989-.624s-.261.74-.873 1.352c-.706.707-1.172.935-1.276.625-.211-.633-1.396-.629-2.033.008-.395.395-.564.387-.83-.043-.232-.376-.416-.406-.607-.097-.15.244-.083.443.15.443.784 0 .433.704-.469.94-.708.185-.791.336-.4.727.64.64.628 1.133-.027 1.133-1.188 0-2.663 1.007-2.454 1.675.169.539-.094.7-1.307.8-1.424.117-2.688 1.925-1.347 1.925.235 0 .428-.18.428-.4 0-.22.216-.4.48-.4.373 0 .373.107 0 .48-.264.264-.48.804-.48 1.2 0 .396-.18.72-.4.72-.22 0-.4-.36-.4-.8 0-.533-.267-.8-.8-.8-.44 0-.8-.18-.8-.4 0-.583-.768-.489-1.432.174-.359.36-.423.668-.171.824.222.137.403.059.403-.174 0-.991 1.117-.302 1.906 1.176.954 1.788.863 1.975-.394.804-.469-.437-.952-.698-1.071-.578-.12.12.002.354.272.52.659.407-1.57 2.559-2.348 2.266-.795-.299-1.712 1.729-1.11 2.455.324.39.323.533-.006.533-.247 0-.449-.18-.449-.4 0-.22-.435-.4-.967-.4-.531 0-1.041-.223-1.132-.496-.119-.357-.361-.301-.862.2-.642.641-.645.747-.049 1.344.357.356.769.526.917.378.149-.148.01-.43-.307-.626s-.456-.478-.307-.626c.148-.148.552.014.899.361.698.698-.189 2.265-1.281 2.265C.098 33.2 0 46.882 0 104.4c0 39.16.098 71.2.218 71.2.476 0 2.182-2 2.182-2.558 0-.753.664-1.186.996-.649.421.683 1.004.454 1.004-.393 0-.44.18-.8.4-.8.22 0 .4.18.4.4 0 .22.202.4.449.4.308 0 .291-.19-.054-.606-.415-.5-.354-.736.351-1.347.47-.407.854-1.007.854-1.332 0-.866 1.32-1.915 2.41-1.915.52 0 1.44-.473 2.043-1.05.603-.578 1.509-1.257 2.014-1.508.504-.252 1.128-.881 1.386-1.397.257-.517 1.386-1.869 2.508-3.005 1.121-1.136 2.039-2.41 2.039-2.833 0-.596.107-.66.48-.287.744.744 1.12.573 1.12-.508 0-1.062 3.677-5.012 4.665-5.012.294 0 .535-.283.535-.629 0-.779.953-1.771 1.702-1.771.307 0 .981-.45 1.498-1s1.101-1 1.298-1c.638 0 1.502-1.121 1.502-1.948 0-1.011.78-1.532 1.222-.817.249.403.428.287.713-.465.21-.552.67-1.243 1.023-1.535.353-.293.642-.871.642-1.284 0-.862.426-.965 1.12-.271.373.373.48.334.48-.178 0-.362.49-.893 1.088-1.18.599-.287 1.29-.926 1.536-1.419a8.704 8.704 0 011.112-1.632c.365-.404.664-1.035.664-1.402 0-.467.341-.669 1.13-.669.689 0 1.498-.391 2.07-1 .517-.55 1.178-1 1.47-1 .291 0 .53-.42.53-.933 0-1.201.388-1.575 1.057-1.02.407.338.614.222.896-.504.202-.518.943-1.483 1.647-2.143.704-.66 1.445-1.625 1.647-2.143.301-.775.471-.857.955-.455.475.394.639.356.852-.197.153-.399.034-.827-.284-1.024-.301-.185-.392-.482-.204-.659.189-.177.424-.097.522.178.286.801.912.577.912-.327 0-.624.182-.769.744-.591.923.293 2.074-.876 1.321-1.342-.616-.38.334-1.231 2.459-2.203 1.239-.567 1.436-.803 1.055-1.262-.371-.447-.318-.624.252-.842.424-.163.786-.082.878.195.215.643.891.599.891-.058 0-.292.45-.953 1-1.47s1-1.166 1-1.443c0-.277.493-.561 1.097-.63 1.202-.139 1.753-1.402.854-1.958-.574-.354-.168-.593 1.449-.849.535-.085.842-.491.927-1.226.099-.858.51-1.273 1.876-1.895 2.712-1.236 2.585-1.599-.561-1.599-3.023 0-3.543-.151-7.933-2.303-4.055-1.988-8.509-7.059-8.509-9.688 0-.419-.18-.873-.4-1.009-.491-.303-.535-7.54-.052-8.505l1.408-2.808c1.703-3.392 6.663-8.485 8.264-8.487.341 0 .791-.162 1-.359.897-.848 6.704-.968 58.18-1.201 52.935-.24 53.425-.249 21-.39-18.26-.08-33.47-.129-33.8-.109-.403.024-.436-.037-.1-.186.275-.122.5-.567.5-.988 0-.876.423-.984 1.12-.287.334.334.48.344.48.033 0-.245-.225-.525-.5-.621-.892-.312.913-1.892 2.162-1.892.871 0 1.138-.187 1.138-.8 0-.533.267-.8.8-.8.622 0 .8-.267.8-1.2 0-1.163.511-1.591.985-.824.306.495 3.415-2.564 3.415-3.36 0-.349.346-.616.8-.616.613 0 .8-.267.8-1.138 0-1.384 1.226-2.676 1.9-2.002.465.465 1.7-.384 1.7-1.17 0-1.541 1.32-2.89 2.829-2.89 1.304 0 1.571-.136 1.571-.8 0-.882.63-1.09.912-.3.098.275.333.355.522.178.188-.177.084-.482-.233-.677-.479-.296-.435-.478.264-1.084.462-.401.908-1.086.992-1.523.087-.456.532-.848 1.047-.922.493-.07.896-.295.896-.5 0-.205.36-.372.8-.372.533 0 .8-.267.8-.8 0-.907.415-1.025 1.12-.32.334.334.48.344.48.033 0-.245-.225-.526-.5-.624-.328-.116-.121-.534.6-1.214.641-.604 1.1-1.462 1.1-2.056 0-.56.18-1.019.4-1.019.22 0 .4.18.4.4 0 1.521 2.278-.177 2.468-1.839.096-.849.374-1.195 1.032-1.289.495-.07.9-.295.9-.5 0-.205.283-.372.629-.372.876 0 2.128-1.281 1.467-1.501-.71-.237-.614-.899.131-.899.345 0 1.27-.675 2.057-1.5.892-.935 1.497-1.312 1.607-1 .096.275.377.5.622.5.311 0 .301-.146-.033-.48-.662-.662-.601-1.12.149-1.12.928 0 1.688-.924 1.839-2.236.088-.769.394-1.207.914-1.306.429-.081 1.286-.689 1.903-1.349.926-.991 1.226-1.114 1.719-.706.361.3.596.331.596.079 0-.228-.225-.49-.499-.582-.558-.186 2.579-3.5 3.312-3.5.581 0 1.587-1.016 1.587-1.602 0-.835 2.253-2.73 3.405-2.865 1.354-.159 2.904-2.478 2.213-3.311-.491-.592-.148-1.022.815-1.022.385 0 .805-.225.934-.5.186-.4.28-.4.466 0 .129.275.435.5.68.5.311 0 .301-.146-.033-.48-.264-.264-.48-.624-.48-.8 0-.494 1.382-.378 1.92.16.373.373.48.141.48-1.047 0-1.467 2.429-4.633 3.555-4.633.811 0 2.57-1.197 2.991-2.036.25-.496.982-1.194 1.626-1.551C183.163-.04 184.961 0 106.784 0 40.268 0 31.958.067 32.4.6m-7.2 9c0 .44-.18.8-.4.8-.22 0-.4-.36-.4-.8 0-.44.18-.8.4-.8.22 0 .4.36.4.8m-13.304 8.397c-.383.447-.502.876-.28 1.013.211.13.384.047.384-.186 0-.595 1.095-.531 1.312.076.098.275.311.377.473.226.163-.15-.047-.64-.465-1.089-.751-.807-.768-.807-1.424-.04M148 33.6c0 .22.259.4.576.4.317 0 .688-.18.824-.4.136-.22-.123-.4-.576-.4-.453 0-.824.18-.824.4m64 38.065c.944.299 9.994.491 27.8.59 26.017.146 27.734.229 30.628 1.495 1.61.705 6.37 5.12 6.952 6.45.289.66.834 1.895 1.212 2.743.378.849.828 2.379 1 3.4l.314 1.857.047-1.709c.065-2.374-2.541-7.925-4.39-9.351A56.667 56.667 0 01273 75.021c-3.36-2.99-3.371-2.991-33.2-3.176-14.52-.089-27.03-.266-27.8-.393l-1.4-.23 1.4.443m67.67 18.137c-.065 1.49-1.446 5.885-2.423 7.71-.53.991-3.779 3.733-5.647 4.766-4.286 2.37-3.753 2.305-20.8 2.533l-15.8.211 15.82.089c8.701.049 15.991-.054 16.2-.229.209-.175 1.1-.561 1.98-.858 3.405-1.147 7.812-4.615 8.954-7.045.549-1.169.732-1.622 1.631-4.024.257-.685.396-2.035.311-3l-.156-1.755-.07 1.602M136 116.8c-.795.514.204.514 1.4 0l.8-.344-.8-.022c-.44-.012-1.07.153-1.4.366m114.728-.1c5.79.062 15.15.062 20.8 0 5.65-.063.912-.114-10.528-.114-11.44 0-16.062.051-10.272.114m47.201.8c6.67.061 17.47.061 24 0 6.529-.062 1.071-.112-12.129-.112s-18.542.05-11.871.112m37.271.5c.33.213.925.391 1.322.394 1.189.01 4.064 1.485 5.488 2.816.734.685 1.624 1.458 1.978 1.718.96.703 2.322 2.774 2.629 3.996.148.592.43 1.076.626 1.076.197 0 .357.529.357 1.176 0 .647.18 1.288.4 1.424.22.136.4 1.677.4 3.424 0 1.851-.167 3.176-.4 3.176-.22 0-.385.495-.368 1.1l.033 1.1.549-1.2c2.996-6.541-2.585-17.499-10.242-20.113-1.783-.608-3.67-.668-2.772-.087m-206.562 1.8c-.639.55-.999.994-.8.986.512-.021 2.848-1.982 2.362-1.984-.22 0-.923.449-1.562.998m-6.795 11.233c-.34.884-.293 5.07.063 5.628.162.253.297-1.052.3-2.9.006-3.626 0-3.675-.363-2.728m224.708 9.267c-.558 1.747-1.314 2.811-3.534 4.976-1.31 1.278-2.615 2.324-2.9 2.324-.284 0-.517.18-.517.4 0 .22-.324.4-.72.4-.396 0-.891.158-1.1.351-.426.394-16.64.829-33.58.9-6.16.026-13 .178-15.2.337-2.32.167 5.988.315 19.776.35 27.868.073 29.149-.064 34.072-3.622 1.603-1.158 4.752-5.462 4.752-6.494 0-.847-.772-.789-1.049.078m-223.326.7c0 .22.265.749.588 1.176.323.428.587.597.587.377 0-.22-.264-.749-.587-1.177-.323-.427-.588-.596-.588-.376m2.375 3.4c.47.66.944 1.2 1.054 1.2.11 0-.184-.54-.654-1.2-.47-.66-.944-1.2-1.054-1.2-.11 0 .184.54.654 1.2m-96.8 4.8c0 .22-.36.4-.8.4-.44 0-.8.18-.8.4 0 .675 1.036.445 1.8-.4.411-.454.524-.8.262-.8-.254 0-.462.18-.462.4m103.8.61c1.07.66 2.13.969 3.2.935l1.6-.052-1.835-.258c-1.009-.141-2.307-.567-2.886-.946-.578-.379-1.192-.683-1.365-.677-.173.007.406.456 1.286.998m139.122.889c2.377.071 6.157.07 8.4-.001 2.243-.071.298-.129-4.322-.129-4.62.001-6.455.059-4.078.13M18.3 158.233c-.381.382-1.145-.236-.861-.695.168-.274.392-.267.679.02.236.236.318.54.182.675m77.838 3.673c-.413.263 84.511.256 127.662-.011 9.24-.057-15.6-.144-55.2-.193-39.6-.05-72.208.042-72.462.204m160.591.794c6.67.061 17.47.061 24 0 6.529-.062 1.071-.112-12.129-.112s-18.542.05-11.871.112m38.471.591c.55.24 1.523.678 2.161.973.639.295 1.324.536 1.522.536.198 0 .936.539 1.639 1.198.703.659 1.427 1.199 1.609 1.2 1.1.006 3.945 4.281 4.456 6.697.151.713.441 1.399.644 1.524.811.501.419 5.076-.521 6.076-.761.811-.804 1.007-.3 1.364.493.348.478.381-.089.199-.76-.243-2.775.643-3.528 1.55-.27.326-.711.592-.98.592-.805 0-1.967 1.48-1.337 1.704.389.138.307.432-.316 1.141-.463.525-.96 1.29-1.105 1.7-.201.568-.391.64-.797.304-.67-.557-1.058-.184-1.058 1.018 0 .797-.201.933-1.376.933-1.427 0-2.271.677-1.466 1.174.321.198.228.496-.307.98-.596.54-.816.578-.984.17-.166-.4-.351-.359-.786.176-.53.651-2.193.703-23.925.74-12.846.022-23.773.157-24.282.3-.509.143 10.741.213 25 .156 17.458-.071 26.318-.247 27.126-.541 3.65-1.326 6.675-3.519 8.83-6.401 5.115-6.842 3.935-17.575-2.411-21.941-.968-.666-1.998-1.443-2.289-1.728-.841-.822-4.233-2.288-5.226-2.258-.886.027-.884.036.096.464m-210.145 4.564c-.58.58-1.055 1.24-1.055 1.466 0 .227.045.336.1.244.055-.093.629-.753 1.275-1.467 1.408-1.556 1.171-1.735-.32-.243M81.8 175.2c-.15.473-.183.95-.073 1.06.11.11.323-.187.473-.66.15-.473.183-.95.073-1.06-.11-.11-.323.187-.473.66M98 196.338c.55.147 12.61.169 26.8.047l25.8-.221-26.8-.048c-14.74-.026-26.35.074-25.8.222m92.93-.038c9.201.06 24.141.06 33.2 0 9.058-.06 1.53-.109-16.73-.109s-25.672.049-16.47.109M46.138 209.506c-.414.264 70.635.258 123.062-.01 11.11-.057-11.93-.144-51.2-.194-39.27-.049-71.608.042-71.862.204m-4.238 1.321c-.825.436-1.5.881-1.5.991 0 .109.675-.14 1.5-.554.825-.414 1.77-.854 2.1-.977.533-.199.533-.225 0-.238-.33-.007-1.275.343-2.1.778m152.7-.4c.66.153 12.304.3 25.876.326 15.968.03 24.764.188 24.924.447.136.22.687.4 1.224.4.537 0 .976.18.976.4 0 .22.36.4.8.4.44 0 .8.18.8.4 0 .22.285.4.633.4 1.896 0 6.696 6.252 7.14 9.3.265 1.825 1.027 2.687 1.027 1.162 0-5.261-7.501-12.6-13.4-13.109-3.2-.276-51.166-.397-50-.126M35.375 215.7L34.2 217l1.3-1.175c1.212-1.095 1.484-1.425 1.175-1.425-.069 0-.654.585-1.3 1.3m222.104 12.6c-.093.165-.286.792-.429 1.393-.189.789-.746 1.313-2.006 1.887-.998.455-1.835 1.142-1.957 1.607a1.065 1.065 0 01-1.05.813c-.525 0-.837.256-.837.688 0 .898-4.371 5.312-5.26 5.312-.401 0-.737.366-.812.884-.202 1.395-.804 1.432-24.928 1.511-12.54.041-23.7.215-24.8.387-1.1.171 9.16.329 22.8.351 30.205.047 30.075.065 34.991-4.699 2.725-2.641 4.461-5.757 4.722-8.473.172-1.786.002-2.438-.434-1.661m-10.44 9.238c-.139.223-.057.527.181.674.615.381 1.012-.14.498-.654-.287-.287-.511-.294-.679-.02m-211.401.279c.036.372 3.562 3.656 3.562 3.318 0-.093-.81-.963-1.8-1.935-.99-.972-1.783-1.594-1.762-1.383m14.262 6.076c.935.084 2.465.084 3.4 0s.17-.153-1.7-.153c-1.87 0-2.635.069-1.7.153m65 10.807c9.845.059 25.955.059 35.8 0 9.845-.06 1.79-.108-17.9-.108-19.69 0-27.745.048-17.9.108m130.375 1.142c2.38.495 4.325 1.315 4.325 1.822 0 .185.27.336.6.336.33 0 .6.252.6.56 0 .308.612 1.172 1.361 1.921.748.748 1.753 2.251 2.232 3.34.48 1.088 1.038 2.292 1.24 2.674.202.382.367 1.226.367 1.876 0 .65.18 1.293.4 1.429.22.136.4 1.036.4 2s-.18 1.864-.4 2c-.22.136-.4 1.047-.4 2.024 0 .977-.18 1.776-.4 1.776-.22 0-.4.264-.4.586 0 1.395-3.051 5.814-4.015 5.814-.16 0-.374.315-.475.7-.101.385-.707.97-1.347 1.299-.64.33-1.253.694-1.363.809-.538.561-2.593 1.192-3.885 1.192-.797 0-1.373.05-1.282.111.639.423 53.988-.371 54.427-.811.275-.275.869-.508 1.32-.518.451-.01 1.63-.819 2.62-1.797.99-.978 1.98-1.789 2.2-1.801.674-.038 3.4-3.728 3.4-4.602 0-.452.207-1.029.461-1.283.644-.644.658-9.54.016-10.182-.262-.262-.477-.78-.477-1.151 0-2.187-4.391-6.885-8.205-8.778L295.8 255.8l-13.1-.1-26.325-.2c-10.473-.079-12.783-.008-11.1.342M81.849 268.215c-.137.358-.231 1.041-.208 1.518.026.548.162.353.369-.53.35-1.487.255-2.07-.161-.988m-.243 5.818c.003.532.183 1.237.4 1.567.276.422.317.185.136-.8-.317-1.725-.544-2.051-.536-.767m215.007 27.319c1.617.382 6.187 4.554 6.187 5.648 0 .33.142.6.316.6.374 0 2.182 3.597 2.829 5.628.561 1.763.601 7.035.055 7.372-.22.136-.4.592-.4 1.014 0 .773-2.528 5.757-3.354 6.612-.245.255-.916.983-1.492 1.618-1.325 1.466-2.314 2.252-3.464 2.757-.614.269 7.313.363 24.392.287 15.671-.069 25.449-.259 25.69-.5.213-.213.634-.388.935-.388 2.466 0 8.893-7.785 8.893-10.773 0-.649.18-1.291.4-1.427.22-.136.4-1.306.4-2.6s-.18-2.464-.4-2.6c-.22-.136-.402-.822-.404-1.524-.007-2.555-6.031-10.276-8.018-10.276-.428 0-.778-.18-.778-.4 0-.22-.414-.4-.92-.4s-1.092-.172-1.303-.383c-.233-.233-10.282-.429-25.719-.5-15.177-.07-24.738.024-23.845.235M218.1 333.9c7.865.06 20.735.06 28.6 0 7.865-.061 1.43-.11-14.3-.11s-22.165.049-14.3.11m64.8-.002a111.9 111.9 0 007 0c1.925-.073.35-.133-3.5-.133s-5.425.06-3.5.133\"></path>\n        <path\n          fill=\"#645be4\"\n          d=\"M181.468.566c-.119.311-.718.806-1.331 1.1-.612.294-1.323.954-1.579 1.467-.43.864-2.179 2.067-3.003 2.067-1.126 0-3.555 3.166-3.555 4.633 0 1.188-.107 1.42-.48 1.047-.538-.538-1.92-.654-1.92-.16 0 .176.216.536.48.8.334.334.344.48.033.48-.245 0-.551-.225-.68-.5-.186-.4-.28-.4-.466 0-.129.275-.549.5-.934.5-.963 0-1.306.43-.815 1.022.691.833-.859 3.152-2.213 3.311-1.152.135-3.405 2.03-3.405 2.865 0 .586-1.006 1.602-1.587 1.602-.733 0-3.87 3.314-3.312 3.5.274.092.499.354.499.582 0 .252-.235.221-.596-.079-.493-.408-.793-.285-1.719.706-.617.66-1.474 1.268-1.903 1.349-.52.099-.826.537-.914 1.306-.151 1.312-.911 2.236-1.839 2.236-.75 0-.811.458-.149 1.12.334.334.344.48.033.48-.245 0-.526-.225-.622-.5-.11-.312-.715.065-1.607 1-.787.825-1.712 1.5-2.057 1.5-.745 0-.841.662-.131.899.661.22-.591 1.501-1.467 1.501-.346 0-.629.167-.629.372s-.405.43-.9.5c-.658.094-.936.44-1.032 1.289-.19 1.662-2.468 3.36-2.468 1.839 0-.22-.18-.4-.4-.4-.22 0-.4.459-.4 1.019 0 .594-.459 1.452-1.1 2.056-.721.68-.928 1.098-.6 1.214.275.098.5.379.5.624 0 .311-.146.301-.48-.033-.705-.705-1.12-.587-1.12.32 0 .533-.267.8-.8.8-.44 0-.8.167-.8.372s-.403.43-.896.5c-.509.073-.96.465-1.045.91-.082.429-.669 1.267-1.304 1.86-.635.593-1.155 1.367-1.155 1.718 0 .478-.398.64-1.571.64-1.509 0-2.829 1.349-2.829 2.89 0 .786-1.235 1.635-1.7 1.17-.674-.674-1.9.618-1.9 2.002 0 .871-.187 1.138-.8 1.138-.533 0-.8.267-.8.8 0 .882-.63 1.09-.912.3-.098-.275-.333-.355-.522-.178-.188.177-.098.473.202.658.424.262.268.608-.702 1.556-.686.67-1.352 1.049-1.48.841-.475-.768-.986-.341-.986.823 0 .933-.178 1.2-.8 1.2-.533 0-.8.267-.8.8 0 .613-.267.8-1.138.8-1.249 0-3.054 1.58-2.162 1.892.275.096.5.376.5.621 0 .311-.146.301-.48-.033-.697-.697-1.12-.589-1.12.287 0 .421-.225.855-.5.963-.275.108 12.91.227 29.3.264 16.39.037 40.87.255 54.4.485 13.53.231 35.94.535 49.8.678 28.114.288 27.175.189 30.6 3.242.55.491 1.613 1.364 2.363 1.942 3.391 2.613 5.675 11.742 3.987 15.94-.247.616-.607 1.524-.8 2.019-.851 2.186-1.436 3.144-2.701 4.416-1.709 1.72-4.975 3.809-7.049 4.51-.88.297-1.815.697-2.077.888-.264.191-43.675.445-97.2.567-74.531.171-96.861.332-97.323.703-.33.265-1.263.785-2.074 1.155-1.097.502-1.506.953-1.6 1.764-.084.726-.393 1.133-.926 1.217-1.617.256-2.023.495-1.449.849.899.556.348 1.819-.854 1.958-.604.069-1.097.353-1.097.63s-.45.926-1 1.443-1 1.178-1 1.47c0 .657-.676.701-.891.058-.092-.277-.454-.358-.878-.195-.57.218-.623.395-.252.842.381.459.184.695-1.055 1.262-2.125.972-3.075 1.823-2.459 2.203.753.466-.398 1.635-1.321 1.342-.562-.178-.744-.033-.744.591 0 .904-.626 1.128-.912.327-.098-.275-.333-.355-.522-.178-.188.177-.097.474.204.659.318.197.437.625.284 1.024-.213.553-.377.591-.852.197-.484-.402-.654-.32-.955.455-.202.518-.943 1.483-1.647 2.143-.704.66-1.445 1.625-1.647 2.143-.282.726-.489.842-.896.504-.669-.555-1.057-.181-1.057 1.02 0 .513-.239.933-.53.933-.292 0-.953.45-1.47 1-.572.609-1.381 1-2.07 1-.789 0-1.13.202-1.13.669 0 .367-.299.998-.664 1.402a8.704 8.704 0 00-1.112 1.632c-.246.493-.937 1.132-1.536 1.419-.598.287-1.088.818-1.088 1.18 0 .512-.107.551-.48.178-.694-.694-1.12-.591-1.12.271 0 .413-.289.991-.642 1.284-.353.292-.813.983-1.023 1.535-.285.752-.464.868-.713.465-.442-.715-1.222-.194-1.222.817 0 .827-.864 1.948-1.502 1.948-.197 0-.781.45-1.298 1s-1.191 1-1.498 1c-.749 0-1.702.992-1.702 1.771 0 .346-.241.629-.535.629-.988 0-4.665 3.95-4.665 5.012 0 1.081-.376 1.252-1.12.508-.373-.373-.48-.309-.48.287 0 .423-.918 1.697-2.039 2.833-1.122 1.136-2.251 2.488-2.508 3.005-.258.516-.882 1.145-1.386 1.397-.505.251-1.411.93-2.014 1.508-.603.577-1.554 1.05-2.113 1.05-1.06 0-2.34 1.086-2.34 1.985 0 .286-.384.855-.854 1.262-.705.611-.766.847-.351 1.347.345.416.362.606.054.606-.247 0-.449-.18-.449-.4 0-.22-.18-.4-.4-.4-.22 0-.4.36-.4.8 0 .847-.583 1.076-1.004.393-.336-.544-.996-.1-.996.67 0 .339-.54 1.12-1.2 1.737L0 175.921v81.639c0 80.483.011 81.64.775 81.64.581 0 .806-.324.9-1.3.12-1.244 1.925-2.302 1.925-1.128a.44.44 0 00.449.428c.292 0 .257-.233-.1-.665-.428-.519-.439-.702-.049-.833.275-.093.5-.514.5-.935 0-.812.588-1.033.995-.374.134.216.936-.279 1.784-1.1.936-.907 1.399-1.642 1.181-1.873-.55-.581-.43-1.02.28-1.02.351 0 1.143-.54 1.76-1.2.617-.66 1.358-1.2 1.648-1.2.289 0 .961-.495 1.493-1.1.627-.715 1.029-.925 1.146-.6.099.275.367.5.595.5.255 0 .226-.228-.073-.589-.39-.47-.365-.712.123-1.2.803-.803.922-.776 1.264.289.271.844.301.851.492.115.112-.431.003-.851-.242-.933-1.223-.408.224-2.882 1.685-2.882.368 0 .669-.283.669-.629 0-.944 1.014-1.771 2.171-1.771 1.098 0 1.428-1.041.594-1.875-.291-.291-.3-.518-.027-.686a.48.48 0 01.658.154c.422.683.939.456 1.125-.493l.177-.9.298.9c.388 1.174.693 1.132 1.303-.18.391-.843.393-1.139.011-1.375-.268-.166-.332-.446-.144-.623.189-.177.424-.097.522.178.282.79.912.582.912-.3 0-.511.267-.8.738-.8.942 0 2.282-1.417 1.688-1.784-.699-.432-.484-1.016.374-1.016.44 0 .8.18.8.4 0 .22.18.4.4.4.22 0 .4-.456.4-1.013 0-1.075 2.024-3.387 2.965-3.387.805 0 1.777-1.282 1.135-1.498-.275-.093-.5-.334-.5-.535 0-.531 1.356-.451 1.92.113.373.373.48.112.48-1.176 0-1.634.506-2.503.996-1.711.384.622 1.004.484 1.004-.222 0-.808.963-1.771 1.771-1.771.75 0 .811-.458.149-1.12-.659-.659.608-.657 1.663.002 1.181.738 2.417-.564 2.417-2.544 0-.846.18-1.538.4-1.538.22 0 .4.36.4.8 0 .44.18.8.4.8.22 0 .4-.414.4-.92s.208-1.128.463-1.383c.349-.349.35-.6.004-1.017-.6-.723-.203-1.08 1.2-1.08.866 0 1.133-.188 1.133-.8 0-.44.18-.8.4-.8.22 0 .4.36.4.8 0 1.121.509.99 1.1-.283.334-.718.367-1.21.1-1.477-.517-.517-.517-1.44 0-1.44.22 0 .4.259.4.576 0 1.015.789 1.105 1.159.132.197-.518.646-1.181.999-1.473.353-.293.642-.871.642-1.284 0-.468.283-.751.751-.751.965 0 1.543-.713 1.714-2.112.19-1.564.91-1.55 1.061.02.139 1.443.874 1.385.874-.07 0-.503.36-1.24.8-1.638.454-.411.8-.524.8-.262 0 .254.18.462.4.462.557 0 .501-1.339-.08-1.92-.373-.373-.301-.48.325-.48.443 0 1.324-.541 1.957-1.202.659-.687 1.419-1.119 1.775-1.008.663.208 1.623-1.371 1.623-2.67 0-.987 1.477-2.443 1.856-1.829.157.254.074.532-.185.619-.259.086-.471.425-.471.752 0 .766 1.151 1.927 1.519 1.531.155-.166.056-.383-.219-.481-1.387-.495.105-3.331 1.596-3.033.682.137 1.104.049 1.104-.229 0-.247-.216-.45-.48-.45-.373 0-.373-.107 0-.48.264-.264.48-.894.48-1.4 0-1.004 1.435-2.711 1.792-2.133.122.197.038.471-.185.609-.671.415-.46 1.004.36 1.004.421 0 .847.225.945.5.104.291.327.352.533.145.207-.206.146-.429-.145-.533-.905-.323-.611-1.801.3-1.512.649.206.8.072.8-.711 0-1.049 1.42-2.615 1.902-2.098.164.175.073.399-.202.497-.275.098-.5.366-.5.594 0 .252.235.221.596-.079.379-.314.508-.845.355-1.454-.271-1.081.508-2.289 1.284-1.991.909.349 2.559-.703 2.562-1.634.004-1.041.765-1.633 1.198-.933.217.352.384.367.579.052.15-.244.083-.443-.15-.443-.772 0-.439-.703.442-.933.689-.18.771-.34.401-.786-.363-.438-.29-.717.334-1.282.454-.411.799-.523.799-.261 0 .254.18.462.4.462.22 0 .4-.439.4-.976 0-1.337.728-1.681 1.163-.55.344.891.368.896.656.126.411-1.103 2.217-3 2.856-3 .29 0 .525-.437.525-.976 0-1.065.595-1.68.996-1.031.354.573 1.701.507 1.921-.093.146-.4.289-.4.713 0 .635.599 1.57.659 1.57.1 0-.22-.345-.4-.767-.4-.421 0-.858-.225-.969-.5-.154-.378-.303-.357-.612.088-.225.324-.553.434-.73.246-.177-.189-.097-.424.178-.522.275-.098.5-.51.5-.916 0-1.24 3.325-2.58 3.712-1.496.098.275.291.397.429.271s-.108-.614-.545-1.083c-.845-.907-1.131-1.783-.316-.968.564.564 1.92.644 1.92.113 0-.201-.225-.455-.5-.563-.275-.108 20.007-.215 45.072-.238 25.064-.023 45.666-.137 45.782-.253.116-.116-32.389-.188-72.234-.161-51.435.036-72.68-.075-73.256-.384-.446-.239-1.363-.434-2.038-.434-.674 0-1.226-.18-1.226-.4 0-.22-.341-.4-.757-.4-1.938 0-8.043-6.657-8.043-8.771 0-.43-.18-.893-.4-1.029-.531-.328-.531-10.472 0-10.8.22-.136.4-.885.4-1.664 0-1.41 5.495-7.736 6.72-7.736.239 0 1.09-.387 1.892-.86 2.834-1.672 3.347-1.692 49.188-1.923 24.42-.123 44.175-.267 43.9-.32-.661-.128-.646-.897.017-.897.284 0 1.311-.81 2.283-1.8 1.633-1.664 3.2-2.35 3.2-1.4 0 .22.18.4.4.4.572 0 .495-2.14-.1-2.765-.408-.428-.334-.475.4-.253.637.192.9.107.9-.293 0-.723 1.513-2.109 1.891-1.731.157.157.026.446-.291.642s-.437.496-.267.667c.171.17.481.045.689-.279.324-.503.43-.507.742-.023.309.48.459.469 1-.072.708-.708.856-1.893.236-1.893-.22 0-.4.36-.4.8 0 .907-.415 1.025-1.12.32-.705-.705-.587-1.12.32-1.12.44 0 .8-.18.8-.4 0-.22.295-.4.656-.4.361 0 .886-.315 1.168-.7.48-.658 3.354-.712 47.71-.9 26.734-.113 47.344-.354 47.533-.556.193-.206-.34-.253-1.267-.112-2.09.318-141.725.363-144.6.047-2.443-.269-7.476-2.565-9.158-4.179-2.102-2.017-4.039-4.622-4.041-5.433 0-.459-.124-.957-.275-1.108-1.72-1.72-1.767-9.808-.077-13.154.193-.382.351-1.057.351-1.5 0-.443.136-.805.302-.805.167 0 .524-.5.794-1.11.27-.611.989-1.407 1.598-1.769.608-.363 1.5-1.03 1.982-1.484 1.126-1.059 3.578-2.437 4.336-2.437.323 0 .588-.161.588-.358 0-.847 6.469-.969 63.8-1.202 33.22-.135 53.38-.318 44.8-.406-8.58-.088-15.936-.131-16.346-.094-.59.053-.485-.177.5-1.097.685-.64 1.246-1.424 1.246-1.741 0-.948 1.367-1.975 1.909-1.433.343.343.615.326 1.091-.069.44-.365.506-.654.217-.943-.229-.229-.417-.561-.417-.737 0-.494 1.382-.378 1.92.16.334.334.48.344.48.033 0-.245-.225-.538-.5-.649-.366-.149-.379-.291-.046-.534.249-.181.519-.675.6-1.097.239-1.257 1.146-1.246 1.146.014 0 .878.103 1.01.49.623s.381-.665-.028-1.319c-.651-1.043-.267-1.471.751-.836 1.007.629 1.187.611 1.187-.115 0-.528-3.678-.6-30.7-.601l-30.7-.001-2.32-1.399c-1.276-.769-2.446-1.399-2.6-1.399-.486-.001-2.276-1.739-3.5-3.4-.649-.879-1.495-1.948-1.88-2.375-.385-.427-.7-1.192-.7-1.7 0-.509-.16-.925-.356-.925-.52 0-.933-2.59-.927-5.8.007-3.355.432-6.2.925-6.2.197 0 .358-.36.358-.8 0-.44.136-.8.302-.8.167 0 .529-.51.804-1.134 1.354-3.056 6.685-7.072 10.494-7.904.77-.168 33.89-.407 73.6-.53 50.37-.157 64.796-.286 47.713-.428l-24.487-.204.495-.992c.272-.546.877-1.183 1.344-1.416 1.559-.777 2.201-1.486 1.752-1.935-.229-.229-.417-.561-.417-.737 0-.494 1.382-.378 1.92.16.373.373.48.156.48-.976 0-1.81.612-2.243 1.585-1.121.457.527.843.695 1.003.437.142-.231.056-.42-.192-.42-1.036 0-.191-1.801.904-1.927.63-.072 1.1-.371 1.1-.7 0-.486 1.953-.573 12.805-.573 7.043 0 13.118-.159 13.5-.354.382-.195 1.73-.863 2.995-1.485 1.265-.621 2.3-1.272 2.3-1.445 0-.174.178-.316.395-.316 1.41 0 4.49-3.645 5.788-6.849 1.279-3.16 1.706-10.622.666-11.662-.247-.247-.449-.773-.449-1.169s-.157-.72-.35-.72c-.192 0-.45-.5-.572-1.112-.123-.611-.775-1.69-1.45-2.396-.676-.707-1.228-1.388-1.228-1.515s-.658-.851-1.461-1.609c-.804-.758-1.334-1.506-1.179-1.662.156-.155.449-.018.65.306.474.76 1.586.13 2.287-1.298.688-1.399 1.778-2.278 2.001-1.614.093.275.37.5.615.5.311 0 .301-.146-.033-.48-.581-.581-.637-1.92-.08-1.92.22 0 .4.202.4.449 0 .31.185.295.6-.049.479-.397.721-.397 1.2 0 .799.663.771.113-.036-.693-.533-.533-.693-.546-.979-.083-.188.304-.467.427-.62.274-.469-.469.661-1.48 1.55-1.386.504.053 1.005-.242 1.263-.743.609-1.184 1.812-2.312 2.129-1.995.149.148.01.43-.307.626s-.456.478-.307.626c.148.148.591-.053.984-.446.511-.511.574-.763.219-.881-.664-.222-.631-1.489.053-2.057.302-.251.824-.35 1.16-.222.335.129.729.042.874-.193.145-.235.614-.427 1.041-.427.579 0 .777-.279.779-1.1.003-1.077 1.307-2.1 2.677-2.1.288 0 .52-.446.52-1 0-.55-.18-1-.4-1-.22 0-.4.18-.4.4 0 .22-.208.4-.462.4-.256 0-.127-.37.29-.831.416-.46.816-.638.895-.4.079.237.384.431.677.431.293 0 .607.22.696.489.103.309.578.074 1.292-.641.87-.869 1.007-1.204.596-1.458-.409-.253-.425-.459-.068-.889.354-.427.348-.683-.026-1.075-.395-.416-.322-.465.376-.255.477.144.982.074 1.124-.155.141-.229.643-.416 1.114-.416.666 0 .749-.107.376-.48-.264-.264-.48-.624-.48-.8 0-.523 1.518-.363 1.713.18.12.335.459.185 1.027-.454.702-.79.952-.868 1.454-.451.333.277.606.341.606.142 0-.198-.36-.62-.8-.937-1.163-.838-.981-1.851.461-2.571 1.446-.721 2.339-.808 2.339-.229 0 .22.216.4.48.4.373 0 .373-.107 0-.48-.264-.264-.48-.624-.48-.8 0-.523 1.518-.363 1.712.18.098.275.311.377.473.226.163-.15-.084-.68-.547-1.178l-.843-.904 1.216-1.255c.875-.903 1.137-1.469.934-2.022-.276-.755-.267-.755.587-.005.477.419.868 1.004.868 1.3 0 .296.18.538.4.538.583 0 .491-2.301-.1-2.512-.291-.104-.352-.327-.145-.533.206-.207.429-.146.533.145.098.275.344.5.545.5.531 0 .451-1.356-.113-1.92-.815-.815.061-.529.968.316.469.437.952.698 1.071.578.12-.12-.04-.377-.355-.571-.741-.459.537-2.003 1.658-2.003.406 0 .738-.18.738-.4 0-.22.36-.4.8-.4.44 0 .8-.18.8-.4 0-.22-.36-.4-.8-.4-.44 0-.8-.18-.8-.4 0-.563 2.14-.5 2.72.08.373.373.48.284.48-.4 0-.5-.249-.88-.576-.88-1.015 0-1.105-.789-.132-1.159.518-.197 1.181-.646 1.473-.999.634-.764 1.895-.847 2.147-.142.104.291.327.352.533.145.207-.206.146-.429-.145-.533-.79-.282-.582-.912.3-.912 1.081 0 1.015-.641-.1-.98-.676-.206-.751-.329-.3-.497.33-.122 1.104-.692 1.719-1.266.951-.887 1.181-.954 1.53-.45.311.448.462.47.615.093.111-.275.548-.5.969-.5.489 0 .767-.278.767-.767 0-.421.225-.871.5-1 .4-.186.4-.28 0-.466-.597-.279-.665-1.618-.1-1.967.22-.136.4-.057.4.176 0 .233.27.424.6.424.333 0 .6-.356.6-.8 0-.533.267-.8.8-.8.44 0 .8.18.8.4 0 .22.216.4.48.4.373 0 .373-.107 0-.48-.264-.264-.48-.624-.48-.8 0-.494 1.382-.378 1.92.16.817.817.528-.062-.319-.972-.779-.836-.781-.879-.06-1.683.407-.454.855-1.151.997-1.55.418-1.183 1.062-.352 1.062 1.372 0 .965.159 1.502.4 1.353.22-.136.4-1.137.4-2.224 0-1.946 1.181-2.964 1.712-1.476.104.291.327.352.533.145.207-.206.146-.429-.145-.533-.79-.282-.582-.912.3-.912.875 0 1.029-.411.391-1.049-.28-.28-.134-.655.458-1.18.623-.553.917-.63 1.043-.271.096.275.376.5.621.5.311 0 .301-.146-.033-.48-.705-.705-.587-1.12.32-1.12.44 0 .8.18.8.4 0 .22.18.4.4.4.22 0 .4-.36.4-.8 0-.44.18-.8.4-.8.22 0 .4-.485.4-1.078 0-1.939 3.167-4.347 3.712-2.822.104.291.327.352.533.145.207-.206.146-.429-.145-.533-.79-.282-.582-.912.3-.912.613 0 .8-.267.8-1.138 0-1.455 1.457-2.928 2.024-2.046.328.51.427.508.776-.016.341-.512.453-.517.764-.035.306.476.47.458 1.047-.119.376-.376.563-.805.415-.953-.148-.149-.44-.005-.648.319-.332.516-.427.515-.778-.012-.356-.533-.448-.533-.83 0-.325.452-.481.476-.634.1-.291-.717-164.939-.737-165.656-.02-.373.373-.48.373-.48 0 0-.671-1.068-.602-1.332.086M326 23.818c0 .412-2.048 2.267-2.264 2.051-.119-.119.207-.667.724-1.218.896-.956 1.54-1.304 1.54-.833M149.4 33.6c-.136.22-.507.4-.824.4-.317 0-.576-.18-.576-.4 0-.22.371-.4.824-.4.453 0 .712.18.576.4m151.4 15.429c0 .125-.279.507-.619.847-.781.781-1.25.39-.521-.434.546-.618 1.14-.833 1.14-.413m-6.543 5.571c.533.533.528.672-.047 1.248-.357.356-.769.526-.917.378-.149-.148-.005-.453.319-.678.445-.309.466-.458.088-.612-.496-.201-.709-.936-.271-.936.125 0 .498.27.828.6m-11.943 62.272c.377.149 11.126.252 23.886.228 12.76-.024 22.39-.099 21.4-.165-3.866-.261-45.933-.319-45.286-.063m51.986.61a5.661 5.661 0 001.8 0c.495-.095.09-.173-.9-.173s-1.395.078-.9.173m6.091 1.609c.544.39 1.624 1.241 2.4 1.891l1.409 1.182-1.2-1.318c-.66-.724-1.74-1.575-2.4-1.891l-1.2-.573.991.709m7.853 10.309c.329 1.471.558 1.79.55.767-.003-.422-.189-1.037-.412-1.367-.297-.438-.334-.276-.138.6m.676 4.6c0 1.21.075 1.705.167 1.1a8.899 8.899 0 000-2.2c-.092-.605-.167-.11-.167 1.1m-.843 4.538c-.262.726-.468 1.576-.458 1.89.011.315.293-.21.628-1.167.335-.956.541-1.806.458-1.89-.083-.083-.366.442-.628 1.167m-1.892 4.362c-.617.935-.865 1.489-.553 1.231.601-.497 2.132-2.931 1.844-2.931-.093 0-.674.765-1.291 1.7M29 149.6c-.764.845-1.8 1.075-1.8.4 0-.22.36-.4.8-.4.44 0 .8-.18.8-.4 0-.22.208-.4.462-.4.262 0 .149.346-.262.8m305.4.4l-1 .32 1-.007c.55-.004 1.54-.145 2.2-.313l1.2-.305-1.2-.008c-.66-.004-1.65.137-2.2.313m-316.961 7.538c-.139.223-.057.527.181.674.615.381 1.012-.14.498-.654-.287-.287-.511-.294-.679-.02m223.833 4.522c.51.143 5.788.247 11.728.23 5.94-.016 10.08-.084 9.2-.15-3.395-.256-21.807-.326-20.928-.08m52.033.627c.609.092 1.509.09 2-.005.492-.095-.005-.171-1.105-.168-1.1.003-1.503.081-.895.173m7.604 2.913c.56.44 1.148.8 1.308.8.16 0-.167-.36-.726-.8-.56-.44-1.148-.8-1.308-.8-.16 0 .167.36.726.8m1.924 1.3c.092.055.707.55 1.367 1.1l1.2 1-.97-1.1c-.533-.605-1.148-1.1-1.366-1.1-.219 0-.322.045-.231.1m-1.74 25.714c-.821.558-1.493 1.101-1.493 1.206 0 .105.72-.285 1.6-.867 1.397-.925 1.87-1.353 1.493-1.353-.059 0-.779.456-1.6 1.014m-43.964 3.286c6.891.061 18.051.061 24.8 0 6.749-.062 1.111-.112-12.529-.112s-19.162.05-12.271.112M191 209.612c1.1.203 13.61.439 27.8.524 14.19.085 22.29.041 18-.097-4.29-.138-15.63-.297-25.2-.352-9.57-.056-18.57-.179-20-.273-2.571-.17-2.577-.168-.6.198m55 1.073c.33.122 1.306.557 2.169.966.864.409 1.636.678 1.716.598.212-.213-3.385-1.855-3.981-1.817-.313.019-.277.115.096.253m8 5.115c1.195 1.21 2.263 2.2 2.373 2.2.11 0-.778-.99-1.973-2.2-1.195-1.21-2.263-2.2-2.373-2.2-.11 0 .778.99 1.973 2.2m3.258 4.874c.408 1.238.771 2.897.806 3.688.063 1.421.066 1.423.222.177.163-1.294-.888-5.569-1.455-5.92-.173-.107.018.818.427 2.055m.851 8.526c0 .99.078 1.395.173.9a5.661 5.661 0 000-1.8c-.095-.495-.173-.09-.173.9m-1.909 5.6c-.341.66-.531 1.2-.421 1.2.11 0 .48-.54.821-1.2.341-.66.531-1.2.421-1.2-.11 0-.48.54-.821 1.2m-7.4 6.854c-1.039.57-1.142.702-.4.514.55-.14 1.264-.505 1.586-.811.739-.703.554-.657-1.186.297m-43.3 1.846c6.875.061 18.125.061 25 0 6.875-.062 1.25-.112-12.5-.112s-19.375.05-12.5.112M94.6 254.8c-.284.46.364.46 1.8 0 .955-.306.943-.322-.276-.36-.702-.022-1.388.14-1.524.36m91.67-.1c9.938.059 26.318.059 36.4 0 10.081-.06 1.95-.108-18.07-.108s-28.269.048-18.33.108m-94.27.9c0 .22-.349.4-.776.4-.427 0-.888.18-1.024.4-.136.22-.597.4-1.024.4-.464 0-.776.274-.776.68 0 .529.107.573.48.2.264-.264.696-.48.959-.48.264 0 1.263-.448 2.22-.995 1.376-.787 1.552-.996.841-1-.495-.003-.9.175-.9.395m164.328-.1c6.231.062 16.311.062 22.4 0 6.09-.062.992-.113-11.328-.113s-17.302.051-11.072.113M85.7 259.82c-.935.969-1.7 1.926-1.7 2.128 0 .201-.27.47-.6.597-.33.126-.6.431-.6.676 0 .782.632.245 2.969-2.521 2.564-3.035 2.522-3.566-.069-.88m218.283 1.48c1.658 1.766 2.017 2.084 2.017 1.783 0-.064-.855-.919-1.9-1.9l-1.9-1.783 1.783 1.9m-222.351 5.839c-.672 1.257-.543 7.729.187 9.315.61 1.327.615 1.329.35.146-.549-2.452-.635-7.476-.147-8.653.563-1.359.251-2.005-.39-.808m226.044 1.661c-.009.55.127 1.81.304 2.8.24 1.35.327 1.5.347.6.015-.66-.122-1.92-.304-2.8-.207-1.002-.337-1.226-.347-.6M60 282.4c0 .44.18.8.4.8.22 0 .4-.36.4-.8 0-.44-.18-.8-.4-.8-.22 0-.4.36-.4.8m24.007-.552c.008.383 3.177 3.128 3.342 2.895.056-.079-.675-.841-1.624-1.695-.949-.854-1.722-1.394-1.718-1.2m217.571 2.852l-1.378 1.5 1.5-1.378c.825-.759 1.5-1.434 1.5-1.5 0-.306-.338-.018-1.622 1.378M94.8 288.59c1.532.509 5.52.585 26.8.51l25-.088-25.4-.239c-13.97-.132-26.03-.361-26.8-.51-1.091-.211-1.002-.139.4.327m163.929-.29c6.45.062 16.89.062 23.2 0 6.309-.062 1.031-.113-11.729-.113s-17.922.051-11.471.113m-106.01.798c1.825.074 4.705.074 6.4-.001 1.695-.075.201-.136-3.319-.135-3.52 0-4.906.062-3.081.136m-104.219 4.7c-.71.806-.62 1.2.13.577.522-.433.691-.427.979.039.191.31.469.442.617.293.148-.148-.009-.547-.35-.888-.74-.74-.743-.74-1.376-.021m-4.1 3.826c0 .427-.18.776-.4.776-.22 0-.4-.461-.4-1.024 0-.58.173-.916.4-.776.22.136.4.597.4 1.024m169.074 2.275c3.781.066 10.081.066 14 0 3.919-.065.826-.119-6.874-.119-7.7 0-10.907.054-7.126.119m95.255 1.201c6.45.062 16.89.062 23.2 0 6.309-.062 1.031-.113-11.729-.113s-17.922.051-11.471.113m45.311 5.877c.571.672 1.336 1.668 1.7 2.212.363.544.66.786.66.539 0-.248-.765-1.244-1.7-2.213-.935-.969-1.232-1.211-.66-.538M20.602 320.798c-.399.441-.595.802-.435.802.421 0 1.561-1.205 1.345-1.421-.101-.101-.51.177-.91.619M10.5 329.81c-.93.993-1.203 2.19-.5 2.19.22 0 .4-.32.4-.712 0-.391.36-.971.8-1.288.796-.574 1.072-1.222.5-1.176-.165.014-.705.457-1.2.986m339.7 1.225l-1.2 1.031 1.3-.86c.715-.473 1.3-.938 1.3-1.033 0-.3-.169-.195-1.4.862m-44.073 2.065c4.8.064 12.54.063 17.2-.001 4.66-.063.733-.116-8.727-.116s-13.273.053-8.473.117\"></path>\n        <path\n          fill=\"#807feb\"\n          d=\"M0 15.167c0 11.742.113 15.219.5 15.4.4.186.4.28 0 .466-.658.307-.648 2.167.011 2.167 1.301 0 1.996-1.613 1.049-2.435-.489-.424-.745-.904-.569-1.068.175-.163.399-.072.497.203.098.275.614.5 1.145.5.532 0 .967.18.967.4 0 .22.202.4.449.4.329 0 .33-.143.006-.533-.602-.726.315-2.754 1.11-2.455.778.293 3.007-1.859 2.348-2.266-.27-.166-.392-.4-.272-.52.119-.12.602.141 1.071.578 1.257 1.171 1.348.984.394-.804-.789-1.478-1.906-2.167-1.906-1.176 0 .233-.181.311-.403.174-.252-.156-.188-.464.171-.824C7.232 22.711 8 22.617 8 23.2c0 .22.36.4.8.4.533 0 .8.267.8.8 0 .44.18.8.4.8.22 0 .4-.324.4-.72 0-.396.216-.936.48-1.2.373-.373.373-.48 0-.48-.264 0-.48.18-.48.4 0 .22-.193.4-.428.4-1.341 0-.077-1.808 1.347-1.925 1.213-.1 1.476-.261 1.307-.8-.209-.668 1.266-1.675 2.454-1.675.655 0 .667-.493.027-1.133-.391-.391-.308-.542.4-.727.902-.236 1.253-.94.469-.94-.233 0-.3-.199-.15-.443.191-.309.375-.279.607.097.266.43.435.438.83.043.637-.637 1.822-.641 2.033-.008.104.31.57.082 1.276-.625.612-.612 1.005-1.22.873-1.352-.132-.132-.577.149-.989.624-.413.475-.864.864-1.003.864-.429 0-.279-1.537.177-1.819.275-.17.107-.61-.468-1.222-.494-.526-.798-1.056-.676-1.178.123-.123.6.257 1.062.843.563.717 1.06.982 1.512.808.47-.18.547-.385.254-.678-.516-.516.492-2.754 1.24-2.754.272 0 .882-.36 1.354-.8.472-.44 1.215-.8 1.652-.8.922 0 1.616-.875.886-1.118-.359-.12-.306-.373.192-.923.723-.8 2.162-1.039 2.162-.359 0 .22.202.4.449.4.325 0 .321-.154-.015-.559-.684-.824.771-3.272 1.874-3.153 1.159.126 1.945-.901 1.292-1.688-.427-.515-2.758-.6-16.449-.6H0v15.167M24.4 9.6c0 .44.18.8.4.8.22 0 .4-.36.4-.8 0-.44-.18-.8-.4-.8-.22 0-.4.36-.4.8m-10.615 9.526c-.162.151-.375.049-.473-.226-.217-.607-1.312-.671-1.312-.076 0 .233-.173.316-.384.186-.222-.137-.103-.566.28-1.013.656-.767.673-.767 1.424.04.418.449.628.939.465 1.089M67.6 71.6c-.33.213-1.264.391-2.076.394-.812.003-1.588.186-1.724.406-.136.22-.552.4-.924.4-1.653.002-6.585 5.032-8.32 8.487a3959.54 3959.54 0 00-1.408 2.808c-.483.965-.439 8.202.052 8.505.22.136.4.59.4 1.009 0 3.641 5.991 9.122 12.671 11.594 2.351.87 197.079.053 200.296-.84 4.141-1.15 9.587-4.73 10.783-7.087 2.635-5.194 3.01-10.282 1.083-14.676l-.965-2.2c-.652-1.489-5.32-5.897-7.04-6.65-2.896-1.266-4.604-1.349-30.628-1.489-17.929-.096-26.913-.286-28-.592-2.085-.586-143.295-.654-144.2-.069m146.75.785c4.465.645 8.842 4.258 11.479 9.475 4.518 8.935-1.487 20.518-11.829 22.816-3.406.757-143.699.711-146.874-.048-11.12-2.659-16.907-13.767-12.014-23.06 2.776-5.271 6.519-8.107 12.288-9.309 1.691-.352 144.49-.23 146.95.126M137.4 116.8c-.44.189-1.47.356-2.288.372-3.549.067-8.868 4.433-11.479 9.422-1.565 2.992-2.06 9.957-.884 12.445 2.514 5.319 4.518 7.62 8.505 9.763l3.346 1.798 66.6.038 66.6.038-54.8-.309c-30.14-.171-60.29-.37-67-.444l-12.2-.133-1.6-1.004c-.88-.552-2.116-1.267-2.747-1.588-1.36-.692-5.317-6.02-6.119-8.239-2.654-7.344 1.423-16.525 8.994-20.252l2.655-1.307 72.598-.108c85.498-.128 78.164-.557 84.078 4.918 3.583 3.317 5.882 11.714 4.291 15.671-3.031 7.539-7.386 11.406-13.674 12.141-2.46.288-2.239.309 1.524.146 7.885-.341 17.285-.545 35.594-.772 10.723-.133 18.149-.379 18.38-.61.212-.212.71-.386 1.106-.386.396 0 .72-.18.72-.4 0-.22.233-.4.517-.4 1.197 0 6.283-5.673 6.283-7.007 0-.34.27-.722.6-.848.342-.132.6-.728.6-1.388 0-.636.18-1.157.4-1.157.233 0 .4-1.325.4-3.176 0-1.747-.18-3.288-.4-3.424-.22-.136-.4-.777-.4-1.424 0-.647-.16-1.176-.357-1.176-.196 0-.478-.484-.626-1.076-.318-1.267-1.681-3.303-2.716-4.057-.402-.293-1.163-.965-1.69-1.492-3.447-3.448-3.927-3.505-31.811-3.818-13.53-.152-36.39-.457-50.8-.678-32.198-.493-121.102-.551-122.2-.079m-42.124 45.489c-4.239.392-12.076 6.674-12.076 9.679 0 .432-.16.884-.356 1.005-.377.233-.744 1.954-1.109 5.209-.219 1.94.464 6.044 1.129 6.798.185.209.336.73.336 1.159 0 .779 2 3.513 4.048 5.533.598.59 2.617 1.824 4.487 2.742l3.4 1.669 73.732-.029c49.436-.019 74.193-.164 75.13-.441.89-.263 9.805-.413 24.556-.413 21.573 0 23.198-.048 23.728-.7.435-.535.62-.576.786-.176.168.408.388.37.984-.17.535-.484.628-.782.307-.98-.805-.497.039-1.174 1.466-1.174 1.175 0 1.376-.136 1.376-.933 0-1.202.388-1.575 1.058-1.018.406.336.596.264.797-.304.145-.41.642-1.175 1.105-1.7.623-.709.705-1.003.316-1.141-.63-.224.532-1.704 1.337-1.704.269 0 .71-.266.98-.592.753-.907 2.768-1.793 3.528-1.55.567.182.582.149.089-.199-.504-.357-.461-.553.3-1.364.94-1 1.332-5.575.521-6.076-.203-.125-.493-.811-.644-1.524-.511-2.416-3.356-6.691-4.456-6.697-.182-.001-.906-.541-1.609-1.2-.703-.659-1.522-1.198-1.82-1.198-.298 0-.713-.155-.922-.345-1.495-1.359-3.014-1.453-28.38-1.753-27.664-.328-171.36-.669-174.124-.413m149.971 1.174c13.5 6.308 13.967 24.16.802 30.641l-2.632 1.296H95l-3.449-1.657c-3.61-1.735-7.463-5.627-8.059-8.143-.13-.55-.447-1.81-.703-2.8-.527-2.03-.232-8.717.412-9.361.22-.22.399-.736.399-1.147 0-3.082 7.716-8.924 12.6-9.54 1.1-.138 34.67-.229 74.6-.202l72.6.05 1.847.863M44.8 210.161c-5.903 1.661-11.095 6.127-12.126 10.429-2.571 10.725 2.525 20.574 11.605 22.428 2.685.549 146.826.657 148.481.112 1.015-.334 8.812-.541 26.44-.702 25.344-.231 26-.273 26-1.652 0-.478.274-.776.713-.776.909 0 5.287-4.398 5.287-5.312 0-.432.312-.688.837-.688.506 0 .922-.322 1.05-.813.123-.467.963-1.154 1.976-1.616 1.368-.623 1.817-1.071 2.002-1.995.131-.656.395-1.29.587-1.408.471-.291.446-3.768-.027-3.768-.207 0-.5-.855-.652-1.9-.444-3.048-5.244-9.3-7.14-9.3-.348 0-.633-.18-.633-.4 0-.22-.36-.4-.8-.4-.44 0-.8-.18-.8-.4 0-.22-.439-.4-.976-.4-.537 0-1.088-.18-1.224-.4-.16-.26-8.771-.4-24.524-.4-19.917 0-24.839-.108-27.411-.6-4.308-.824-145.743-.862-148.665-.039m151.8 1.77c7.32 3.479 11.331 13.637 8.083 20.469-2.811 5.913-6.424 9.139-11.409 10.186-4.107.862-142.986.936-147.474.078-12.56-2.401-18.006-17.804-9.494-26.852 2.309-2.455 2.871-2.821 6.894-4.486l2.2-.91 74.2.092 74.2.092 2.8 1.331m51.3 26.302c-.381.382-1.145-.236-.861-.695.168-.274.392-.267.679.02.236.236.318.54.182.675M95 255.361c-3.28.935-6.856 2.808-8.045 4.214A145.47 145.47 0 0184.84 262c-.687.77-1.351 1.85-1.475 2.4-.125.55-.384 1.36-.575 1.8-1.272 2.922-1.227 10.126.071 11.58.186.209.339.71.339 1.114 0 1.992 6.499 8.306 8.548 8.306.333 0 .704.161.826.358.121.197.987.492 1.923.657 2.971.523 144.51.696 147.703.181 3.214-.519 5.8-1.215 5.8-1.562 0-.131.564-.488 1.252-.793.689-.305 1.34-.889 1.447-1.298.107-.408.326-.743.486-.743.964 0 4.015-4.419 4.015-5.814 0-.322.18-.586.4-.586.22 0 .4-.799.4-1.776s.18-1.888.4-2.024c.22-.136.4-1.036.4-2s-.18-1.864-.4-2c-.22-.136-.4-.779-.4-1.429s-.165-1.494-.367-1.876c-.202-.382-.76-1.586-1.24-2.674-.479-1.089-1.484-2.592-2.232-3.34-.749-.749-1.361-1.613-1.361-1.921 0-.308-.27-.56-.6-.56-.33 0-.6-.153-.6-.339 0-.561-2.802-1.583-6.143-2.242-4.043-.797-145.671-.853-148.457-.058m125.2.19l23.6.249 2.8 1.331c6.596 3.137 10.796 12.638 8.504 19.241-2.492 7.183-6.799 10.68-14.294 11.608-6.668.826-146.291.227-147.81-.633a33.04 33.04 0 00-1.884-.96c-3.183-1.484-7.516-6.079-7.516-7.968 0-.363-.209-.868-.466-1.125-.634-.634-.688-9.252-.065-10.531.258-.53.578-1.465.712-2.079.421-1.937 4.473-5.923 7.607-7.484l2.812-1.4 27.6-.208c40.824-.308 71.901-.321 98.4-.041m-16 45.11c-1.1.33-2.45.702-3 .828-1.605.368-7.819 6.819-8.391 8.711-1.525 5.048-1.571 8.649-.171 13.4.956 3.243 6.07 8.303 9.396 9.296 2.438.728 66.153 1.257 84.566.703 10.618-.32 11.183-.469 14.154-3.755.576-.635 1.247-1.363 1.492-1.618.826-.855 3.354-5.839 3.354-6.612 0-.422.18-.878.4-1.014.546-.337.506-5.609-.055-7.372-.647-2.031-2.455-5.628-2.829-5.628-.174 0-.316-.27-.316-.6 0-1.544-4.624-5.291-7.458-6.043-3.854-1.023-87.803-1.295-91.142-.296m90.704.827c11.542 4.369 14.187 18.911 5.115 28.125-3.607 3.664.574 3.387-51.219 3.387-48.156 0-47.38.026-49.12-1.674-.285-.28-.825-.739-1.2-1.021-6.126-4.618-8.185-13.763-4.74-21.055.669-1.416 5.481-6.198 6.86-6.818a627.9 627.9 0 012-.893c2.076-.918 8.456-1.05 47.8-.989l42.2.065 2.304.873\"></path>\n      </g>\n    </svg>\n  )\n})\n"
  },
  {
    "path": "src/components/Icons/LlamacppLogo.tsx",
    "content": "import React from \"react\"\n\nexport const LlamaCppLogo = React.forwardRef<\n  SVGSVGElement,\n  React.SVGProps<SVGSVGElement>\n>((props, ref) => {\n  return (\n    <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    viewBox=\"0 0 22 28\"\n    {...props}\n    ref={ref}\n    >\n    <path\n      fill=\"#FF8236\"\n      fillRule=\"evenodd\"\n      d=\"M8.076.883c.5.246.349.605.207.92l-.156.355c-.283.64-.566 1.282-.912 1.887-.753 1.317-.608 2.57.134 3.82.048.08.09.163.143.267l.103.198c-2.171.578-3.933 2.039-4.937 2.871a25 25 0 0 1-.401.328c.067-.64.12-1.292.171-1.947.078-.974.156-1.953.284-2.9.189-1.385.628-2.704 1.432-3.87.93-1.347 2.204-2.046 3.932-1.93ZM3.69 12.375c2.092-1.99 4.547-2.983 7.4-2.878 1.907.07 3.682.624 5.3 1.814l-.658 1.128-.001.002-1.023 1.755c-.237-.111-.468-.23-.697-.347-.488-.25-.965-.495-1.47-.654-4.92-1.552-8.474 2.513-7.991 6.727.266 2.332 1.935 3.78 4.309 3.856 1.238.04 2.39-.288 3.503-.784.225-.1.448-.206.69-.321l.376-.178c.09.351.193.73.296 1.114.185.686.375 1.388.51 1.978-2.958 1.498-6.097 2.197-9.24.746C1.777 24.85.428 21.687.813 18.356c.265-2.301 1.233-4.281 2.877-5.981m15.601 8.976v-2.117h1.958v-1.945h-1.973v-2.082h-2.014v2.108H15.26v1.923h2.043v2.113h1.99m-7.015-4.06h2.035v1.924h-2.026v2.141h-1.99V19.24H8.31v-1.93h1.977v-2.113h1.99zM10.845 2.842C9.11 4.19 8.675 6.16 8.257 8.194c1.46-.37 2.936-.29 4.41-.135l-.232-.461-.03-.059c-.544-1.087-.613-2.124.243-3.118.326-.38.595-.812.862-1.24l.057-.091c.29-.463.191-.67-.35-.769-.86-.156-1.683-.014-2.372.521\"\n      clipRule=\"evenodd\"\n    ></path>\n  </svg>\n  )\n})\n"
  },
  {
    "path": "src/components/Icons/Llamafile.tsx",
    "content": "// copied logo from Hugging Face webiste \nimport React from \"react\"\n\nexport const LLamaFile = React.forwardRef<\n  SVGSVGElement,\n  React.SVGProps<SVGSVGElement>\n>((props, ref) => {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"1em\"\n      height=\"1em\"\n      className=\"text-black inline-block text-sm\"\n      viewBox=\"0 0 16 16\"\n      ref={ref}\n      {...props}>\n      <path\n        fill=\"currentColor\"\n        fillRule=\"evenodd\"\n        d=\"M7.66 5.82H4.72a.31.31 0 0 1-.32-.32c0-.64-.52-1.16-1.16-1.16H1.6a.7.7 0 0 0-.7.7v8.3c0 .52.43.95.96.95h9.4c.4 0 .75-.3.82-.7l.65-3.84.74-.07a2.08 2.08 0 0 0 .28-4.1l-.94-.22-.11-.2a2.3 2.3 0 0 0-.54-.64v-.05a5.6 5.6 0 0 1 .1-1.02l.07-.45c.01-.1.02-.21.01-.32a.6.6 0 0 0-.1-.27.5.5 0 0 0-.54-.21c-.12.03-.2.1-.24.14a1 1 0 0 0-.12.13A4.8 4.8 0 0 0 10.76 4h-.33l.05-.49.04-.28.06-.53c.01-.1.02-.26 0-.42a1 1 0 0 0-.15-.43.87.87 0 0 0-.93-.35.96.96 0 0 0-.4.22c-.08.06-.14.13-.18.19a5.5 5.5 0 0 0-.55 1.25c-.15.52-.28 1.2-.24 1.85-.2.24-.36.51-.47.8Zm.66.8c.06-.52.3-.98.67-1.3l.02-.01c-.15-.72.05-1.65.28-2.28.15-.41.31-.7.41-.72.05-.01.06.07.06.22l-.07.56v.06a11 11 0 0 0-.1 1.28c.01.21.05.39.14.49a2 2 0 0 1 .57-.08h.42c.52 0 1 .28 1.25.73l.2.36c.06.1.16.18.28.21l1.11.26a1.24 1.24 0 0 1-.17 2.45l-1.38.12-.76 4.48h-4.2L8.33 6.6Z\"\n        clipRule=\"evenodd\"></path>\n    </svg>\n  )\n})\n"
  },
  {
    "path": "src/components/Icons/MCPIcon.tsx",
    "content": "import React from \"react\"\n\nexport const MCPIcon = React.forwardRef<\n  SVGSVGElement,\n  React.SVGProps<SVGSVGElement>\n>((props, ref) => {\n  return (\n   <svg\n    xmlns=\"http://www.w3.org/2000/svg\"\n    fill=\"currentColor\"\n    fillRule=\"evenodd\"\n    style={{ flex: \"none\", lineHeight: \"1\" }}\n    viewBox=\"0 0 24 24\"\n    ref={ref}\n    {...props}\n  >\n    <path d=\"M15.688 2.343a2.59 2.59 0 0 0-3.61 0l-9.626 9.44a.863.863 0 0 1-1.203 0 .823.823 0 0 1 0-1.18l9.626-9.44a4.313 4.313 0 0 1 6.016 0 4.12 4.12 0 0 1 1.204 3.54 4.3 4.3 0 0 1 3.609 1.18l.05.05a4.115 4.115 0 0 1 0 5.9l-8.706 8.537a.274.274 0 0 0 0 .393l1.788 1.754a.823.823 0 0 1 0 1.18.863.863 0 0 1-1.203 0l-1.788-1.753a1.92 1.92 0 0 1 0-2.754l8.706-8.538a2.47 2.47 0 0 0 0-3.54l-.05-.049a2.59 2.59 0 0 0-3.607-.003l-7.172 7.034-.002.002-.098.097a.863.863 0 0 1-1.204 0 .823.823 0 0 1 0-1.18l7.273-7.133a2.47 2.47 0 0 0-.003-3.537\"></path>\n    <path d=\"M14.485 4.703a.823.823 0 0 0 0-1.18.863.863 0 0 0-1.204 0l-7.119 6.982a4.115 4.115 0 0 0 0 5.9 4.314 4.314 0 0 0 6.016 0l7.12-6.982a.823.823 0 0 0 0-1.18.863.863 0 0 0-1.204 0l-7.119 6.982a2.59 2.59 0 0 1-3.61 0 2.47 2.47 0 0 1 0-3.54z\"></path>\n  </svg>\n  )\n})"
  },
  {
    "path": "src/components/Icons/MiniMaxIcon.tsx",
    "content": "import React from \"react\"\n\nexport const MiniMaxIcon = React.forwardRef<\n  SVGSVGElement,\n  React.SVGProps<SVGSVGElement>\n>((props, ref) => {\n  return (\n    <svg\n      fill=\"currentColor\"\n      fillRule=\"evenodd\"\n      viewBox=\"0 0 24 24\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      ref={ref}\n      {...props}>\n      <path d=\"M16.278 2c1.156 0 2.093.927 2.093 2.07v12.501a.74.74 0 00.744.709.74.74 0 00.743-.709V9.099a2.06 2.06 0 012.071-2.049A2.06 2.06 0 0124 9.1v6.561a.649.649 0 01-.652.645.649.649 0 01-.653-.645V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v7.472a2.037 2.037 0 01-2.048 2.026 2.037 2.037 0 01-2.048-2.026v-12.5a.785.785 0 00-.788-.753.785.785 0 00-.789.752l-.001 15.904A2.037 2.037 0 0113.441 22a2.037 2.037 0 01-2.048-2.026V18.04c0-.356.292-.645.652-.645.36 0 .652.289.652.645v1.934c0 .263.142.506.372.638.23.131.514.131.744 0a.734.734 0 00.372-.638V4.07c0-1.143.937-2.07 2.093-2.07zm-5.674 0c1.156 0 2.093.927 2.093 2.07v11.523a.648.648 0 01-.652.645.648.648 0 01-.652-.645V4.07a.785.785 0 00-.789-.78.785.785 0 00-.789.78v14.013a2.06 2.06 0 01-2.07 2.048 2.06 2.06 0 01-2.071-2.048V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v3.8a2.06 2.06 0 01-2.071 2.049A2.06 2.06 0 010 12.9v-1.378c0-.357.292-.646.652-.646.36 0 .653.29.653.646V12.9c0 .418.343.757.766.757s.766-.339.766-.757V9.099a2.06 2.06 0 012.07-2.048 2.06 2.06 0 012.071 2.048v8.984c0 .419.343.758.767.758.423 0 .766-.339.766-.758V4.07c0-1.143.937-2.07 2.093-2.07z\" />\n    </svg>\n  )\n})\n"
  },
  {
    "path": "src/components/Icons/Mistral.tsx",
    "content": "import React from \"react\"\n\nexport const MistarlIcon = React.forwardRef<\n  SVGSVGElement,\n  React.SVGProps<SVGSVGElement>\n>((props, ref) => {\n  return (\n    <svg\n      {...props}\n      fill=\"currentColor\"\n      fillRule=\"evenodd\"\n      ref={ref}\n      style={{ flex: \"none\", lineHeight: 1, ...props.style }}\n      viewBox=\"0 0 24 24\"\n      xmlns=\"http://www.w3.org/2000/svg\">\n      <g fill=\"none\" fillRule=\"nonzero\">\n        <path\n          d=\"M15 6v4h-2V6h2zm4-4v4h-2V2h2zM3 2H1h2zM1 2h2v20H1V2zm8 12h2v4H9v-4zm8 0h2v8h-2v-8z\"\n          fill=\"#000\"\n        />\n        <path d=\"M19 2h4v4h-4V2zM3 2h4v4H3V2z\" fill=\"#F7D046\" />\n        <path d=\"M15 10V6h8v4h-8zM3 10V6h8v4H3z\" fill=\"#F2A73B\" />\n        <path d=\"M3 14v-4h20v4z\" fill=\"#EE792F\" />\n        <path\n          d=\"M11 14h4v4h-4v-4zm8 0h4v4h-4v-4zM3 14h4v4H3v-4z\"\n          fill=\"#EB5829\"\n        />\n        <path d=\"M19 18h4v4h-4v-4zM3 18h4v4H3v-4z\" fill=\"#EA3326\" />\n      </g>\n    </svg>\n  )\n})\n"
  },
  {
    "path": "src/components/Icons/Moonshot.tsx",
    "content": "import React from \"react\"\n\nexport const MoonshotIcon = React.forwardRef<\n  SVGSVGElement,\n  React.SVGProps<SVGSVGElement>\n>((props, ref) => {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      fill=\"currentColor\"\n      fillRule=\"evenodd\"\n      viewBox=\"0 0 24 24\"\n      ref={ref}\n      {...props}>\n      <path d=\"m1.052 16.916 9.539 2.552a21 21 0 0 0 .06 2.033l5.956 1.593a12 12 0 0 1-5.586.865l-.18-.016-.044-.004-.084-.009-.094-.01-.157-.02-.107-.014-.11-.016a12 12 0 0 1-.32-.051l-.042-.008-.075-.013-.107-.02-.07-.015-.093-.019-.075-.016-.095-.02-.097-.023-.094-.022-.068-.017-.088-.022-.09-.024-.095-.025-.082-.023-.109-.03-.062-.02-.084-.025-.093-.028-.105-.034-.058-.019-.08-.026-.09-.031-.066-.024-.044-.015-.068-.025-.101-.037-.057-.022-.08-.03-.087-.035-.088-.035-.079-.032-.095-.04-.063-.028-.063-.027-.041-.018-.066-.03-.103-.047-.052-.024-.096-.046-.062-.03-.084-.04-.086-.044-.093-.047-.052-.027-.103-.055-.057-.03-.058-.032-.046-.026-.094-.053-.06-.034-.051-.03-.072-.041-.082-.05-.093-.056-.052-.032-.084-.053-.061-.039-.079-.05-.07-.047-.053-.035-.054-.036-.044-.03-.044-.03-.04-.028-.057-.04-.076-.054-.069-.05-.074-.054-.056-.042-.076-.057-.076-.059-.086-.067-.045-.035-.064-.052-.074-.06-.089-.073-.046-.039-.046-.039-.043-.037-.045-.04-.061-.053-.07-.062-.068-.06-.062-.058-.067-.062-.053-.05-.088-.084-.099-.097-.029-.028-.041-.042-.069-.07-.05-.051-.05-.053a7 7 0 0 1-.168-.179l-.08-.088-.062-.07-.071-.08-.042-.049-.053-.062-.058-.068-.046-.056-.027-.033-.045-.055-.066-.082-.041-.052-.05-.064-.02-.025a12 12 0 0 1-1.44-2.402m-1.02-5.794 11.353 3.037a21 21 0 0 0-.469 2.011l10.817 2.894a12 12 0 0 1-1.845 2.005L.657 15.923l-.016-.046-.035-.104-.05-.153-.007-.023a12 12 0 0 1-.207-.741l-.03-.126-.018-.08-.021-.097-.018-.081-.018-.09-.017-.084-.018-.094q-.04-.212-.071-.426l-.017-.118-.011-.083-.013-.102-.019-.161-.005-.047a12 12 0 0 1-.034-2.145m1.593-5.15 11.948 3.196a21 21 0 0 0-1.01 1.875l11.295 3.022c-.142.82-.368 1.612-.668 2.365l-11.55-3.09L.124 10.26l.015-.1.008-.049.01-.067.015-.087.018-.098q.04-.222.088-.442l.028-.124.02-.085.024-.097q.033-.135.07-.268l.028-.102.023-.083.03-.1.025-.082.03-.096.026-.082.031-.095a12 12 0 0 1 1.01-2.232zm4.442-4.4L17.352 4.59a21 21 0 0 0-1.688 1.721l7.823 2.093c.267.852.442 1.744.513 2.665L2.106 5.213l.045-.065.027-.04.04-.055.046-.065.055-.076.054-.072.064-.086.05-.065.057-.073.055-.07.06-.074.055-.069.065-.077.054-.066.066-.077.053-.06.072-.082.053-.06.067-.074.054-.058.073-.078.058-.06.063-.067.168-.17.1-.098.059-.056.076-.071a12 12 0 0 1 2.272-1.677M12.017 0h.097l.082.001.069.001.054.002.068.002.046.001.076.003.047.002.06.003.054.002.087.005.105.007.144.011.088.007.044.004.077.008.082.008.047.005.102.012.05.006.108.014.081.01.042.006.065.01.207.032.07.012.065.011.14.026.092.018.11.022.046.01.075.016.041.01L14.7.3l.042.01.065.015.049.012.071.017.096.024.112.03.113.03.113.032.05.015.07.02.078.024.073.023.05.016.05.016.076.025.099.033.102.036.048.017.064.023.093.034.11.041.116.045.1.04.047.02.06.024.041.018.063.026.04.018.057.025.11.048.1.046.074.035.075.036.06.028.092.046.091.045.102.052.053.028.049.026.046.024.06.033.041.022.052.029.088.05.106.06.087.051.057.034.053.032.096.059.088.055.098.062.036.024.064.041.084.056.04.027.062.042.062.043.023.017q.081.055.161.114l.083.06.065.048.056.043.086.065.082.064.04.03.05.041.086.069.079.065.085.071c.712.6 1.353 1.283 1.909 2.031L7.222.994l.062-.027.065-.028.081-.034.086-.035q.17-.068.341-.131l.096-.035.093-.033.084-.03.096-.031A8 8 0 0 1 8.49.525l.091-.027.086-.025.102-.03.085-.023.1-.026L9.04.37l.09-.023.091-.022.095-.022.09-.02.098-.021.091-.02.095-.018.092-.018.1-.018.091-.016.098-.017.092-.014.097-.015.092-.013.102-.013.091-.012.105-.012.09-.01.105-.01q.14-.015.28-.024l.106-.008.09-.005.11-.006.093-.004.1-.004.097-.002.099-.002.197-.002z\"></path>\n    </svg>\n  )\n})\n"
  },
  {
    "path": "src/components/Icons/Novita.tsx",
    "content": "import React from \"react\"\n\nexport const NovitaIcon = React.forwardRef<\n  SVGSVGElement,\n  React.SVGProps<SVGSVGElement>\n>((props, ref) => {\n  return (\n    <svg\n      fill=\"currentColor\"\n      fillRule=\"evenodd\"\n      ref={ref}\n      style={{ flex: \"none\", lineHeight: 1, ...props.style }}\n      viewBox=\"0 0 24 24\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      {...props}>\n      <path\n        clipRule=\"evenodd\"\n        d=\"M9.167 4.17v5.665L0 19.003h9.167v-5.666l5.666 5.666H24L9.167 4.17z\"\n        fill=\"#23D57C\"\n        fillRule=\"evenodd\"\n      />\n    </svg>\n  )\n})\n"
  },
  {
    "path": "src/components/Icons/Ollama.tsx",
    "content": "import React from \"react\"\n\nexport const OllamaIcon = React.forwardRef<\n  SVGSVGElement,\n  React.SVGProps<SVGSVGElement>\n>((props, ref) => {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      fill=\"currentColor\"\n      fillRule=\"evenodd\"\n      viewBox=\"0 0 646 854\"\n      ref={ref}\n      {...props}>\n      <path d=\"M140.629.24c-7.969 1.287-17.532 5.456-24.275 10.605-20.413 15.51-36.229 48.428-42.91 89.438-2.514 15.509-4.23 37.026-4.23 53.455 0 19.371 2.268 44.136 5.517 61.239.736 3.801 1.103 7.173.797 7.418-.245.245-3.25 2.697-6.62 5.394-11.525 9.195-24.705 23.356-33.778 36.291-17.41 24.704-28.688 52.78-33.409 83.185-1.839 12.015-2.33 36.29-.858 48.305 3.25 27.708 11.586 51.125 25.87 72.581l4.658 6.927-1.349 2.268c-9.563 16.061-17.716 39.294-21.516 61.607-3.004 17.655-3.372 22.375-3.372 46.037 0 23.847.307 28.567 3.127 45.057 3.371 19.739 10.237 40.642 17.9 54.558 2.513 4.536 8.643 13.976 9.378 14.467.246.122-.49 2.39-1.655 5.026-8.827 19.31-16.367 44.995-19.493 66.635-2.207 14.834-2.514 19.616-2.514 35.248 0 19.922 1.104 29.608 5.272 45.485l.613 2.329h52.535l-1.716-3.249c-10.605-19.616-11.586-56.029-2.452-92.38 4.168-16.797 8.888-29.118 17.716-46.099l5.272-10.298v-6.314c0-5.885-.123-6.559-2.023-10.421-1.472-2.943-3.433-5.456-6.927-8.889-5.947-5.762-10.238-11.831-13.67-19.31-15.08-32.735-18.023-81.346-7.418-122.786 4.414-17.287 11.709-32.673 19.371-41.071 5.21-5.763 7.908-12.199 7.908-18.881 0-6.927-2.452-12.628-7.97-18.574-15.815-16.919-25.562-37.517-29.056-61.485-4.965-34.145 4.046-71.355 24.52-100.84 20.046-28.935 48.183-47.509 79.631-52.474 7.049-1.165 20.229-.981 27.585.368 8.031 1.41 13.057.98 18.207-1.472 6.375-3.003 9.563-6.743 13.302-15.325 3.31-7.662 5.885-11.831 12.812-20.474 8.337-10.36 16.367-17.41 29.24-25.931 14.713-9.624 31.448-16.612 48.122-19.984 6.068-1.226 8.888-1.41 20.229-1.41s14.161.184 20.229 1.41c24.459 4.966 48.735 17.594 68.106 35.493 4.168 3.862 14.16 16.245 17.348 21.395 1.226 2.022 3.372 6.314 4.72 9.501 3.739 8.582 6.927 12.322 13.302 15.325 4.966 2.391 10.176 2.882 17.9 1.594 12.199-2.084 21.578-1.9 33.532.552 40.704 8.214 76.136 41.746 91.829 86.68 13.67 39.416 9.808 80.672-10.544 112.18-3.433 5.334-6.866 9.625-11.831 14.897-10.728 11.463-10.728 25.685-.061 37.455 17.532 19.187 28.505 66.389 25.194 108.012-2.206 27.463-9.256 52.045-18.942 65.96-1.716 2.452-5.271 6.62-7.969 9.195-3.494 3.433-5.455 5.946-6.927 8.889-1.9 3.862-2.023 4.536-2.023 10.421v6.314l5.272 10.298c8.828 16.981 13.548 29.302 17.716 46.099 9.012 35.861 8.215 71.538-2.084 91.829-.858 1.716-1.594 3.31-1.594 3.494 0 .184 11.709.306 26.053.306h25.992l.674-2.636c.368-1.409.981-3.555 1.287-4.781.675-2.697 2.023-10.666 3.127-18.329 1.042-7.724 1.042-36.168 0-44.75-3.923-31.141-10.483-55.845-21.21-79.201-1.165-2.636-1.901-4.904-1.656-5.026.307-.184 2.023-2.636 3.862-5.395 13.364-20.229 21.578-45.669 25.747-79.262 1.103-9.257 1.103-49.041 0-57.93-2.943-22.926-6.498-38.497-12.383-54.251-2.452-6.559-8.95-20.413-11.708-24.888l-1.349-2.268 4.659-6.927c14.283-21.456 22.62-44.873 25.869-72.581 1.471-12.015.981-36.29-.858-48.305-4.782-30.467-16-58.42-33.409-83.185-9.073-12.935-22.253-27.096-33.777-36.291-3.372-2.697-6.376-5.149-6.621-5.394-.306-.245.062-3.617.797-7.418 7.418-38.681 7.172-86.924-.613-124.625-6.743-32.857-19.003-58.971-34.819-74.051C523.209 4.286 510.336-.864 494.888.117c-35.432 2.085-63.998 42.85-75.278 107.093-1.839 10.36-3.432 22.498-3.432 25.808 0 1.287-.246 2.329-.552 2.329-.307 0-2.697-1.226-5.272-2.758-27.34-16.184-57.746-24.827-87.354-24.827-29.608 0-60.014 8.643-87.354 24.827-2.575 1.532-4.965 2.758-5.272 2.758-.306 0-.552-1.042-.552-2.329 0-3.433-1.655-15.938-3.432-25.808-10.238-57.684-33.716-95.875-64.918-105.499C157.181.424 144.982-.434 140.629.24zm10.422 49.899c8.827 6.988 18.635 26.972 24.275 49.347 1.042 4.046 2.145 8.705 2.452 10.421.245 1.656.919 5.395 1.471 8.276 2.391 12.996 3.494 27.034 3.617 44.137l.061 16.858-4.23 6.252-4.229 6.314h-9.87c-11.524 0-22.988 1.472-33.961 4.414-3.923.981-7.724 1.962-8.459 2.146-1.165.245-1.349-.123-2.023-5.15-3.617-27.279-3.433-57.5.552-82.634 4.413-28.014 14.712-53.393 24.765-60.871 2.391-1.778 2.82-1.717 5.579.49zm349.538-.43c6.069 4.476 12.751 16.368 17.716 31.57 9.992 30.406 12.812 72.152 7.54 111.875-.674 5.027-.858 5.395-2.023 5.15-.735-.184-4.536-1.165-8.459-2.146-10.973-2.942-22.437-4.414-33.961-4.414h-9.87l-4.229-6.314-4.23-6.252.061-16.858c.123-23.785 2.33-42.359 7.601-63.018 5.579-22.19 15.448-42.175 24.214-49.163 2.759-2.207 3.188-2.268 5.64-.43z\"></path>\n      <path d=\"M313.498 358.237c-13.303 1.288-16.919 1.778-23.295 3.066-10.36 2.145-24.214 6.927-33.838 11.647-33.47 16.367-56.519 43.646-63.569 75.216-1.41 6.253-1.594 8.337-1.594 18.881 0 10.421.184 12.689 1.533 18.635 9.379 41.256 47.385 71.723 96.549 77.301 10.666 1.165 56.765 1.165 67.431 0 39.478-4.475 73.439-25.869 88.703-55.907 4.045-8.03 6.007-13.241 7.846-21.394 1.349-5.946 1.533-8.214 1.533-18.635 0-10.544-.184-12.628-1.594-18.881-10.238-45.853-54.742-81.959-109.3-88.825-7.111-.858-25.746-1.594-30.405-1.104zm22.926 33.348c18.207 1.962 36.536 8.46 51.248 18.268 7.908 5.272 19.065 16.306 23.846 23.54 5.885 8.949 9.256 18.083 10.789 29.179.674 5.088.307 8.95-1.533 17.164-2.881 12.26-11.831 25.072-23.907 34.022-5.64 4.107-17.348 10.054-24.52 12.383-13.609 4.352-22.498 5.149-54.252 4.904-20.719-.184-24.398-.368-30.344-1.471-20.29-3.801-36.351-11.893-47.998-24.214-9.441-9.931-13.732-19.003-16.061-33.654-1.042-6.805.919-18.084 4.904-27.586 4.843-11.586 17.348-25.991 29.731-34.267 14.344-9.563 33.225-16.367 50.573-18.206 6.682-.736 20.842-.736 27.524-.062z\"></path>\n      <path d=\"M299.584 436.336c-4.659 2.513-7.908 8.888-6.927 13.608 1.103 5.088 5.578 10.238 12.566 14.468 3.74 2.268 3.985 2.574 4.169 4.842.122 1.349-.368 5.211-1.042 8.644-.736 3.371-1.288 6.927-1.288 7.908.062 2.636 2.514 6.927 5.088 9.011 2.269 1.839 2.698 1.9 9.073 2.084 5.824.184 7.05.061 9.379-1.042 6.008-2.943 7.54-8.337 5.333-18.697-1.839-8.643-1.471-9.992 3.127-12.628 4.842-2.82 9.992-7.785 11.524-11.157 2.943-6.436.245-13.731-6.253-17.103-1.593-.797-3.555-1.164-6.436-1.164-4.475 0-7.356 1.042-12.628 4.413l-3.004 1.901-1.9-1.165c-7.785-4.598-9.195-5.149-13.916-5.088-3.371 0-5.21.306-6.865 1.165zM150.744 365.165c-10.85 3.433-18.942 11.402-23.11 22.743-2.023 5.395-3.004 13.916-2.146 18.513 2.023 10.973 11.034 20.965 21.272 23.724 12.873 3.371 22.497 1.164 31.018-7.295 4.965-4.843 7.663-9.073 10.36-15.939 1.961-4.842 2.084-5.7 2.084-12.566l.061-7.356-2.574-5.272c-4.108-8.337-11.525-14.529-20.107-16.797-4.843-1.226-12.628-1.164-16.858.245zM478.153 364.982c-8.398 2.268-15.877 8.52-19.862 16.735l-2.574 5.272.061 7.356c0 6.866.123 7.724 2.084 12.566 2.698 6.866 5.395 11.096 10.36 15.939 8.521 8.459 18.145 10.666 31.019 7.295 7.417-1.962 14.834-8.215 18.39-15.51 3.065-6.191 3.8-10.666 2.82-17.716-2.268-16.122-11.709-27.83-25.747-31.937-4.107-1.226-12.076-1.226-16.551 0z\"></path>\n    </svg>\n  )\n})\n"
  },
  {
    "path": "src/components/Icons/OpenAI.tsx",
    "content": "import React from \"react\"\n\nexport const OpenAiIcon = React.forwardRef<\n  SVGSVGElement,\n  React.SVGProps<SVGSVGElement>\n>((props, ref) => {\n  return (\n    <svg\n      fill=\"currentColor\"\n      fillRule=\"evenodd\"\n      ref={ref}\n      viewBox=\"0 0 24 24\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      {...props}>\n      <path d=\"M21.55 10.004a5.416 5.416 0 00-.478-4.501c-1.217-2.09-3.662-3.166-6.05-2.66A5.59 5.59 0 0010.831 1C8.39.995 6.224 2.546 5.473 4.838A5.553 5.553 0 001.76 7.496a5.487 5.487 0 00.691 6.5 5.416 5.416 0 00.477 4.502c1.217 2.09 3.662 3.165 6.05 2.66A5.586 5.586 0 0013.168 23c2.443.006 4.61-1.546 5.361-3.84a5.553 5.553 0 003.715-2.66 5.488 5.488 0 00-.693-6.497v.001zm-8.381 11.558a4.199 4.199 0 01-2.675-.954c.034-.018.093-.05.132-.074l4.44-2.53a.71.71 0 00.364-.623v-6.176l1.877 1.069c.02.01.033.029.036.05v5.115c-.003 2.274-1.87 4.118-4.174 4.123zM4.192 17.78a4.059 4.059 0 01-.498-2.763c.032.02.09.055.131.078l4.44 2.53c.225.13.504.13.73 0l5.42-3.088v2.138a.068.068 0 01-.027.057L9.9 19.288c-1.999 1.136-4.552.46-5.707-1.51h-.001zM3.023 8.216A4.15 4.15 0 015.198 6.41l-.002.151v5.06a.711.711 0 00.364.624l5.42 3.087-1.876 1.07a.067.067 0 01-.063.005l-4.489-2.559c-1.995-1.14-2.679-3.658-1.53-5.63h.001zm15.417 3.54l-5.42-3.088L14.896 7.6a.067.067 0 01.063-.006l4.489 2.557c1.998 1.14 2.683 3.662 1.529 5.633a4.163 4.163 0 01-2.174 1.807V12.38a.71.71 0 00-.363-.623zm1.867-2.773a6.04 6.04 0 00-.132-.078l-4.44-2.53a.731.731 0 00-.729 0l-5.42 3.088V7.325a.068.068 0 01.027-.057L14.1 4.713c2-1.137 4.555-.46 5.707 1.513.487.833.664 1.809.499 2.757h.001zm-11.741 3.81l-1.877-1.068a.065.065 0 01-.036-.051V6.559c.001-2.277 1.873-4.122 4.181-4.12.976 0 1.92.338 2.671.954-.034.018-.092.05-.131.073l-4.44 2.53a.71.71 0 00-.365.623l-.003 6.173v.002zm1.02-2.168L12 9.25l2.414 1.375v2.75L12 14.75l-2.415-1.375v-2.75z\"></path>\n    </svg>\n  )\n})\n"
  },
  {
    "path": "src/components/Icons/OpenRouter.tsx",
    "content": "import React from \"react\"\n\nexport const OpenRouterIcon = React.forwardRef<\n  SVGSVGElement,\n  React.SVGProps<SVGSVGElement>\n>((props, ref) => {\n  return (\n    <svg\n      fill=\"currentColor\"\n      fillRule=\"evenodd\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 24 24\"\n      ref={ref}\n      {...props}>\n      <path d=\"M16.804 1.957l7.22 4.105v.087L16.73 10.21l.017-2.117-.821-.03c-1.059-.028-1.611.002-2.268.11-1.064.175-2.038.577-3.147 1.352L8.345 11.03c-.284.195-.495.336-.68.455l-.515.322-.397.234.385.23.53.338c.476.314 1.17.796 2.701 1.866 1.11.775 2.083 1.177 3.147 1.352l.3.045c.694.091 1.375.094 2.825.033l.022-2.159 7.22 4.105v.087L16.589 22l.014-1.862-.635.022c-1.386.042-2.137.002-3.138-.162-1.694-.28-3.26-.926-4.881-2.059l-2.158-1.5a21.997 21.997 0 00-.755-.498l-.467-.28a55.927 55.927 0 00-.76-.43C2.908 14.73.563 14.116 0 14.116V9.888l.14.004c.564-.007 2.91-.622 3.809-1.124l1.016-.58.438-.274c.428-.28 1.072-.726 2.686-1.853 1.621-1.133 3.186-1.78 4.881-2.059 1.152-.19 1.974-.213 3.814-.138l.02-1.907z\"></path>\n    </svg>\n  )\n})\n"
  },
  {
    "path": "src/components/Icons/PDFIcon.tsx",
    "content": "import React from \"react\"\n\nexport const PDFIcon = React.forwardRef<\n  SVGSVGElement,\n  React.SVGProps<SVGSVGElement>\n>((props, ref) => {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      version=\"1.1\"\n      viewBox=\"0 0 303.188 303.188\"\n      xmlSpace=\"preserve\"\n      ref={ref}\n      {...props}>\n      <path\n        fill=\"#E8E8E8\"\n        d=\"M219.821 0L32.842 0 32.842 303.188 270.346 303.188 270.346 50.525z\"></path>\n      <path\n        fill=\"#FB3449\"\n        d=\"M230.013 149.935c-3.643-6.493-16.231-8.533-22.006-9.451-4.552-.724-9.199-.94-13.803-.936-3.615-.024-7.177.154-10.693.354-1.296.087-2.579.199-3.861.31a93.594 93.594 0 01-3.813-4.202c-7.82-9.257-14.134-19.755-19.279-30.664 1.366-5.271 2.459-10.772 3.119-16.485 1.205-10.427 1.619-22.31-2.288-32.251-1.349-3.431-4.946-7.608-9.096-5.528-4.771 2.392-6.113 9.169-6.502 13.973-.313 3.883-.094 7.776.558 11.594.664 3.844 1.733 7.494 2.897 11.139a165.324 165.324 0 003.588 9.943 171.593 171.593 0 01-2.63 7.603c-2.152 5.643-4.479 11.004-6.717 16.161l-3.465 7.507c-3.576 7.855-7.458 15.566-11.815 23.02-10.163 3.585-19.283 7.741-26.857 12.625-4.063 2.625-7.652 5.476-10.641 8.603-2.822 2.952-5.69 6.783-5.941 11.024-.141 2.394.807 4.717 2.768 6.137 2.697 2.015 6.271 1.881 9.4 1.225 10.25-2.15 18.121-10.961 24.824-18.387 4.617-5.115 9.872-11.61 15.369-19.465l.037-.054c9.428-2.923 19.689-5.391 30.579-7.205 4.975-.825 10.082-1.5 15.291-1.974 3.663 3.431 7.621 6.555 11.939 9.164 3.363 2.069 6.94 3.816 10.684 5.119 3.786 1.237 7.595 2.247 11.528 2.886 1.986.284 4.017.413 6.092.335 4.631-.175 11.278-1.951 11.714-7.57.134-1.72-.237-3.228-.98-4.55zm-110.869 10.31a170.827 170.827 0 01-6.232 9.041c-4.827 6.568-10.34 14.369-18.322 17.286-1.516.554-3.512 1.126-5.616 1.002-1.874-.11-3.722-.937-3.637-3.065.042-1.114.587-2.535 1.423-3.931.915-1.531 2.048-2.935 3.275-4.226 2.629-2.762 5.953-5.439 9.777-7.918 5.865-3.805 12.867-7.23 20.672-10.286-.449.71-.897 1.416-1.34 2.097zm27.222-84.26a38.169 38.169 0 01-.323-10.503 24.858 24.858 0 011.038-4.952c.428-1.33 1.352-4.576 2.826-4.993 2.43-.688 3.177 4.529 3.452 6.005 1.566 8.396.186 17.733-1.693 25.969-.299 1.31-.632 2.599-.973 3.883a121.219 121.219 0 01-1.648-4.821c-1.1-3.525-2.106-7.091-2.679-10.588zm16.683 66.28a236.508 236.508 0 00-25.979 5.708c.983-.275 5.475-8.788 6.477-10.555 4.721-8.315 8.583-17.042 11.358-26.197 4.9 9.691 10.847 18.962 18.153 27.214.673.749 1.357 1.489 2.053 2.22-4.094.441-8.123.978-12.062 1.61zm61.744 11.694c-.334 1.805-4.189 2.837-5.988 3.121-5.316.836-10.94.167-16.028-1.542-3.491-1.172-6.858-2.768-10.057-4.688-3.18-1.921-6.155-4.181-8.936-6.673 3.429-.206 6.9-.341 10.388-.275 3.488.035 7.003.211 10.475.664 6.511.726 13.807 2.961 18.932 7.186 1.009.833 1.331 1.569 1.214 2.207z\"></path>\n      <path\n        fill=\"#FB3449\"\n        d=\"M227.64 25.263L32.842 25.263 32.842 0 219.821 0z\"></path>\n      <g fill=\"#A4A9AD\">\n        <path d=\"M126.841 241.152c0 5.361-1.58 9.501-4.742 12.421-3.162 2.921-7.652 4.381-13.472 4.381h-3.643v15.917H92.022v-47.979h16.606c6.06 0 10.611 1.324 13.652 3.971 3.041 2.647 4.561 6.41 4.561 11.289zm-21.856 6.235h2.363c1.947 0 3.495-.546 4.644-1.641 1.149-1.094 1.723-2.604 1.723-4.529 0-3.238-1.794-4.857-5.382-4.857h-3.348v11.027zM175.215 248.864c0 8.007-2.205 14.177-6.613 18.509s-10.606 6.498-18.591 6.498h-15.523v-47.979h16.606c7.701 0 13.646 1.969 17.836 5.907 4.189 3.938 6.285 9.627 6.285 17.065zm-13.455.46c0-4.398-.87-7.657-2.609-9.78-1.739-2.122-4.381-3.183-7.926-3.183h-3.773v26.877h2.888c3.939 0 6.826-1.143 8.664-3.43 1.837-2.285 2.756-5.78 2.756-10.484zM196.579 273.871h-12.766v-47.979h28.355v10.403h-15.589v9.156h14.374v10.403h-14.374v18.017z\"></path>\n      </g>\n      <path fill=\"#D1D3D3\" d=\"M219.821 50.525L270.346 50.525 219.821 0z\"></path>\n    </svg>\n  )\n})\n"
  },
  {
    "path": "src/components/Icons/SiliconFlow.tsx",
    "content": "import React from \"react\"\n\nexport const SiliconFlowIcon = React.forwardRef<\n  SVGSVGElement,\n  React.SVGProps<SVGSVGElement>\n>((props, ref) => {\n  return (\n    <svg\n      fill=\"currentColor\"\n      fillRule=\"evenodd\"\n      ref={ref}\n      style={{ flex: \"none\", lineHeight: 1 }}\n      viewBox=\"0 0 120 120\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      {...props}>\n      <path fillRule=\"evenodd\" d=\"M100.74 12h-7.506c-24.021 0-37.867 15.347-37.867 38.867V54.9a30.862 30.862 0 0 0-8.507-1.196C29.816 53.703 16 67.52 16 84.563c0 17.044 13.816 30.86 30.86 30.86 17.044 0 30.86-13.816 30.86-30.86 0-2.073-.209-4.14-.623-6.172h23.643c6.225-.023 11.26-5.076 11.26-11.301 0-6.226-5.035-11.279-11.26-11.302H77.22v-5.922c0-9.008 6.505-15.513 16.014-15.513h7.506c6.107-.093 11.01-5.069 11.01-11.177 0-6.107-4.903-11.084-11.01-11.176zM56.035 84.563a9.175 9.175 0 1 0-18.35 0 9.175 9.175 0 0 0 18.35 0z\" />\n    </svg>\n  )\n})\n"
  },
  {
    "path": "src/components/Icons/TXTIcon.tsx",
    "content": "import React from \"react\"\n\nexport const TXTIcon = React.forwardRef<\n  SVGSVGElement,\n  React.SVGProps<SVGSVGElement>\n>((props, ref) => {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"-4 0 64 64\"\n      ref={ref}\n      {...props}>\n      <path\n        fill=\"#F9CA06\"\n        fillRule=\"evenodd\"\n        d=\"M5.151-.036A5.074 5.074 0 00.077 5.038v53.841a5.073 5.073 0 005.074 5.074h45.774a5.074 5.074 0 005.074-5.074V20.274L37.097-.036H5.151z\"\n        clipRule=\"evenodd\"></path>\n      <g fillRule=\"evenodd\" clipRule=\"evenodd\">\n        <path\n          fill=\"#F7BC04\"\n          d=\"M56.008 20.316v1H43.209s-6.312-1.26-6.129-6.708c0 0 .208 5.708 6.004 5.708h12.924z\"></path>\n        <path\n          fill=\"#fff\"\n          d=\"M37.106-.036v14.561c0 1.656 1.104 5.792 6.104 5.792h12.799L37.106-.036z\"\n          opacity=\"0.5\"></path>\n      </g>\n      <path\n        fill=\"#fff\"\n        d=\"M18.763 43.045h-3.277v10.047a.734.734 0 01-.756.738.73.73 0 01-.738-.738V43.045h-3.259c-.36 0-.648-.288-.648-.684 0-.36.288-.648.648-.648h8.03c.36 0 .648.288.648.685a.645.645 0 01-.648.647zm11.7 10.803a.64.64 0 01-.541-.27l-3.727-4.97-3.745 4.97a.639.639 0 01-.54.27.71.71 0 01-.72-.72c0-.144.036-.306.144-.432l3.889-5.131-3.619-4.826a.721.721 0 01-.144-.414c0-.343.288-.721.72-.721.216 0 .432.108.576.288l3.439 4.627 3.439-4.646a.642.642 0 01.541-.27c.378 0 .738.306.738.721a.7.7 0 01-.126.414l-3.619 4.808 3.89 5.149c.09.126.126.27.126.415a.739.739 0 01-.721.738zm11.195-10.803h-3.277v10.047a.734.734 0 01-.756.738.73.73 0 01-.738-.738V43.045h-3.259c-.36 0-.648-.288-.648-.684 0-.36.288-.648.648-.648h8.03c.36 0 .648.288.648.685a.644.644 0 01-.648.647z\"></path>\n    </svg>\n  )\n})\n"
  },
  {
    "path": "src/components/Icons/TencentCloud.tsx",
    "content": "import React from \"react\"\n\nexport const TencentCloudIcon = React.forwardRef<\n  SVGSVGElement,\n  React.SVGProps<SVGSVGElement>\n>((props, ref) => {\n  return (\n    <svg \n      className=\"icon\" \n      viewBox=\"0 0 1024 1024\" \n      fill=\"currentColor\"\n      fillRule=\"evenodd\"\n      ref={ref}\n      style={{ flex: \"none\", lineHeight: 1 }}\n      {...props}>\n      <path d=\"M512 170.666667c130.474667 0 240.938667 83.797333 277.930667 199.296a198.826667 198.826667 0 0 0-41.557334-0.597334 222.293333 222.293333 0 0 0-49.706666 10.624C668.202667 309.333333 596.096 259.754667 512 259.754667c-100.266667 0-183.466667 70.528-199.381333 163.029333a279.04 279.04 0 0 0-89.429334-3.84C241.28 278.954667 363.690667 170.666667 512 170.666667z\" fill=\"#2c2c2c\" p-id=\"8175\"></path><path d=\"M258.474667 417.322667c54.442667 0 104.192 20.181333 142.165333 53.418666 16.085333 14.08 45.226667 39.68 87.381333 76.8l-7.381333-6.528-61.568 60.885334-54.4-54.4c-34.218667-34.261333-66.090667-47.957333-106.197333-47.957334a133.589333 133.589333 0 0 0 0 267.221334c10.666667 0 29.312 0.768 56.064 2.346666l-90.453334 77.141334A215.893333 215.893333 0 0 1 258.432 417.28z\" fill=\"#2c2c2c\" p-id=\"8176\"></path><path d=\"M674.346667 434.474667a215.808 215.808 0 0 1 168.618666 397.354666c-15.36 6.485333-38.186667 15.957333-63.146666 16.213334-72.106667 0.597333-244.181333 0.896-516.352 0.938666h-42.666667a206248.106667 206248.106667 0 0 0 397.013333-380.714666c18.261333-17.578667 41.130667-27.264 56.533334-33.792z m41.856 80.554666c-9.258667 3.925333-23.04 9.770667-34.048 20.352-30.165333 29.098667-109.952 105.642667-239.445334 229.632h53.418667c148.181333 0 242.773333-0.213333 283.733333-0.554666 15.061333-0.128 28.842667-5.845333 38.101334-9.813334a130.133333 130.133333 0 0 0-101.76-239.616z\" fill=\"#2c2c2c\" p-id=\"8177\"></path>\n    </svg>    \n  )\n})\n"
  },
  {
    "path": "src/components/Icons/Togther.tsx",
    "content": "import React from \"react\"\n\nexport const TogtherMonoIcon = React.forwardRef<\n  SVGSVGElement,\n  React.SVGProps<SVGSVGElement>\n>((props, ref) => {\n  return (\n    <svg\n      fill=\"currentColor\"\n      fillRule=\"evenodd\"\n      ref={ref}\n      viewBox=\"0 0 24 24\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      {...props}>\n      <g>\n        <path\n          d=\"M17.385 11.23a4.615 4.615 0 100-9.23 4.615 4.615 0 000 9.23zm0 10.77a4.615 4.615 0 100-9.23 4.615 4.615 0 000 9.23zm-10.77 0a4.615 4.615 0 100-9.23 4.615 4.615 0 000 9.23z\"\n          opacity=\".2\"></path>\n        <circle cx=\"6.615\" cy=\"6.615\" r=\"4.615\"></circle>\n      </g>\n    </svg>\n  )\n})\n"
  },
  {
    "path": "src/components/Icons/VercelIcon.tsx",
    "content": "import React from \"react\"\n\nexport const VercelIcon = React.forwardRef<\n  SVGSVGElement,\n  React.SVGProps<SVGSVGElement>\n>((props, ref) => {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      fill=\"none\"\n      viewBox=\"0 0 76 65\"\n      {...props}\n      ref={ref}>\n      <path fill=\"currentColor\" d=\"m37.527 0 37.528 65H0z\"></path>\n    </svg>\n  )\n})\n"
  },
  {
    "path": "src/components/Icons/VllmLogo.tsx",
    "content": "import React from \"react\"\n\nexport const VllmLogo = React.forwardRef<\n  SVGSVGElement,\n  React.SVGProps<SVGSVGElement>\n>((props, ref) => {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 460 460\"\n      {...props}\n      ref={ref}>\n      <path\n        fill=\"#63A1FC\"\n        d=\"M196.952 394.188c-.976-.241-1.952-.482-3.039-1.382-.02-1.292.071-1.925.435-2.828 2.018-6.88 3.763-13.487 5.508-20.096 5.243-19.703 10.593-39.379 15.702-59.117 6.44-24.884 12.58-49.846 19.042-74.725 6.918-26.634 14.083-53.204 21.092-79.815 2.446-9.288 5.003-18.56 7.012-27.945 1.034-4.832 3.499-7.432 7.857-9.664 20.658-10.58 41.092-21.597 61.63-32.41 17.864-9.405 35.762-18.745 54.706-28.67-1.102 3.546-1.9 6.116-2.783 9.439-1.73 6.836-3.437 12.903-5.01 19.005-4.519 17.514-8.84 35.081-13.508 52.556-5.004 18.734-10.468 37.345-15.447 56.086-4.72 17.765-8.925 35.667-13.62 53.44-8.386 31.752-16.987 63.448-25.466 95.177-4.489 16.797-8.91 33.612-13.452 50.764-2.26 0-4.23-.004-6.2 0z\"></path>\n      <path\n        fill=\"#EBB432\"\n        d=\"M194.075 390.248a139 139 0 0 1-.331 2.214c-5.776-10.717-11.576-21.707-17.187-32.793-6.359-12.562-12.54-25.213-18.813-37.818-7.388-14.847-14.768-29.696-22.19-44.525-8.9-17.78-17.885-35.518-26.756-53.313-14.122-28.33-28.171-56.694-42.28-85.03-1.384-2.78-2.964-5.462-4.752-8.736 2.163-.132 3.585-.295 5.007-.295 40.662-.014 81.325.008 121.987-.05 3.117-.005 5.813-.059 5.094 5.157.006 84.33-.002 167.77.002 251.208 0 1.327.143 2.654.219 3.981\"></path>\n      <path\n        fill=\"#D7D8D9\"\n        d=\"M197.123 394.504c31.315-.378 62.802-.44 94.288-.5 1.97-.005 3.94-.001 6.2-.001 4.543-17.152 8.963-33.967 13.452-50.764 8.479-31.729 17.08-63.425 25.467-95.178 4.694-17.772 8.899-35.674 13.62-53.44 4.978-18.74 10.442-37.351 15.446-56.085 4.668-17.475 8.99-35.042 13.507-52.556 1.574-6.102 3.28-12.17 5.01-18.646 2.473-1.275 4.861-2.156 8.033-3.328-2.087 8.294-3.922 15.954-5.95 23.563-3.664 13.747-7.485 27.451-11.163 41.194-6.513 24.331-13.055 48.655-19.422 73.024-6.365 24.36-12.432 48.796-18.823 73.147-3.322 12.66-7.213 25.171-10.574 37.822-4.892 18.406-9.477 36.893-14.313 55.314-2.477 9.434-5.3 18.778-7.79 28.209-.796 3.017-2.461 3.779-5.397 3.77-31.831-.083-63.664-.152-95.494.07-4.283.03-4.791-2.612-6.097-5.615\"></path>\n      <path\n        fill=\"#D8D8D8\"\n        d=\"M194.348 389.978c-.35-1.057-.492-2.384-.492-3.71-.004-83.44.004-166.88.071-250.782 1.527-.384 2.994-.306 5.123-.192v6.588c0 73.977-.002 147.954.012 221.932 0 1.82.177 3.64.532 5.765-1.483 6.912-3.228 13.52-5.246 20.399\"></path>\n    </svg>\n  )\n})\n"
  },
  {
    "path": "src/components/Icons/VolcEngine.tsx",
    "content": "import React from \"react\"\n\nexport const VolcEngineIcon = React.forwardRef<\n  SVGSVGElement,\n  React.SVGProps<SVGSVGElement>\n>((props, ref) => {\n  return (\n    <svg xmlns=\"http://www.w3.org/2000/svg\" \n      fill=\"currentColor\"\n      fillRule=\"evenodd\"\n      ref={ref}\n      style={{ flex: \"none\", lineHeight: 1 }}\n      viewBox=\"0 0 57 56\"\n      {...props}>\n      <path fillRule=\"evenodd\" d=\"M28.0024 0C27.1265 0 26.3871 0.611817 26.299 1.4217C26.2814 1.56696 26.2682 1.71661 26.2506 1.86186C25.6608 7.69833 25.3351 13.548 25.63 19.4021C25.7796 22.3247 26.7876 51.0493 26.924 55.0723C26.9416 55.6533 27.4214 56.1111 28.0024 56.1067C28.579 56.1067 29.05 55.6489 29.0676 55.0723C29.2217 50.5387 30.2252 22.3247 30.3749 19.4021C30.6698 13.5436 30.3441 7.69833 29.7542 1.86186C29.7366 1.71661 29.7234 1.56696 29.7058 1.4217C29.6178 0.611817 28.8739 0 28.0024 0ZM15.4181 29.9572C15.2729 28.8965 15.176 27.8709 14.912 26.8762C14.7535 26.2775 14.1813 25.8506 13.5167 25.8506C12.852 25.8506 12.2798 26.2775 12.1214 26.8762C11.8573 27.8753 11.7604 28.9009 11.6152 29.9572C11.3247 31.8675 11.6548 33.7602 11.8265 35.6485C11.9497 37.0702 12.2534 40.3581 12.4339 41.771C12.4647 42.0219 12.4999 42.2728 12.5351 42.5281C12.5747 43.0387 12.9973 43.4436 13.5167 43.4436C14.036 43.4436 14.463 43.0387 14.4982 42.5281C14.529 42.2772 14.5642 42.0263 14.5994 41.771C14.7799 40.3581 15.0836 37.0702 15.2069 35.6485C15.3785 33.7602 15.7086 31.8675 15.4181 29.9572ZM37.32 12.5137C37.2936 12.2673 37.2452 12.0296 37.2056 11.7831C37.0999 11.1228 36.4925 10.6387 35.7751 10.6387C35.7575 10.6387 35.7443 10.6387 35.7267 10.6387C35.709 10.6387 35.6958 10.6387 35.6782 10.6387C34.9608 10.6387 34.3578 11.1272 34.2477 11.7831C34.2081 12.0252 34.1597 12.2673 34.1333 12.5137C33.5215 18.4558 33.407 20.036 33.5699 25.692C33.6095 27.0389 34.1993 42.7437 34.6439 49.456C34.6791 49.993 35.128 50.4112 35.6694 50.4112C35.6914 50.4112 35.709 50.4068 35.7311 50.4068C35.7531 50.4068 35.7707 50.4112 35.7927 50.4112C36.3341 50.4112 36.783 49.993 36.8182 49.456C37.2628 42.7437 37.8526 27.0389 37.8922 25.692C38.0551 20.036 37.9362 18.4558 37.3288 12.5137ZM44.3849 29.9572C44.2397 28.8965 44.1428 27.8709 43.8788 26.8762C43.7203 26.2775 43.1481 25.8506 42.4835 25.8506C41.8188 25.8506 41.2466 26.2775 41.0882 26.8762C40.8241 27.8753 40.7272 28.9009 40.582 29.9572C40.2915 31.8675 40.6216 33.7602 40.7933 35.6485C40.9165 37.0702 41.2202 40.3581 41.4007 41.771C41.4315 42.0219 41.4667 42.2728 41.5019 42.5281C41.5415 43.0387 41.9641 43.4436 42.4835 43.4436C43.0028 43.4436 43.4298 43.0387 43.465 42.5281C43.4958 42.2772 43.531 42.0263 43.5662 41.771C43.7467 40.3581 44.0504 37.0702 44.1737 35.6485C44.3453 33.7602 44.6754 31.8675 44.3849 29.9572ZM21.8708 12.5137C21.8444 12.2673 21.796 12.0296 21.7564 11.7831C21.6507 11.1228 21.0433 10.6387 20.3259 10.6387C20.3082 10.6387 20.295 10.6387 20.2774 10.6387C20.2598 10.6387 20.2466 10.6387 20.229 10.6387C19.5116 10.6387 18.9085 11.1272 18.7985 11.7831C18.7589 12.0252 18.7105 12.2673 18.6841 12.5137C18.0723 18.4558 17.9578 20.036 18.1207 25.692C18.1603 27.0389 18.7501 42.7437 19.1947 49.456C19.2299 49.993 19.6788 50.4112 20.2202 50.4112C20.2422 50.4112 20.2598 50.4068 20.2818 50.4068C20.3038 50.4068 20.3215 50.4112 20.3435 50.4112C20.8849 50.4112 21.3338 49.993 21.369 49.456C21.8136 42.7437 22.4034 27.0389 22.443 25.692C22.6059 20.036 22.487 18.4558 21.8796 12.5137Z\"></path>\n    </svg>\n  )\n})\n"
  },
  {
    "path": "src/components/Icons/XAI.tsx",
    "content": "import React from \"react\"\n\nexport const XAIIcon = React.forwardRef<\n  SVGSVGElement,\n  React.SVGProps<SVGSVGElement>\n>((props, ref) => {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      fill=\"currentColor\"\n      fillRule=\"evenodd\"\n      viewBox=\"0 0 24 24\"\n      ref={ref}\n      {...props}>\n      <path d=\"M6.469 8.776 16.512 23h-4.464L2.005 8.776zm-.004 7.9 2.233 3.164L6.467 23H2zM22 2.582V23h-3.659V7.764zM22 1l-9.952 14.095-2.233-3.163L17.533 1z\"></path>\n    </svg>\n  )\n})\n"
  },
  {
    "path": "src/components/Layouts/Header.tsx",
    "content": "import { useStorage } from \"@plasmohq/storage/hook\"\nimport {\n  BrainCog,\n  ChevronLeft,\n  ChevronRight,\n  CogIcon,\n  ComputerIcon,\n  GithubIcon,\n  PanelLeftIcon,\n  ZapIcon,\n  SaveIcon\n} from \"lucide-react\"\nimport { useTranslation } from \"react-i18next\"\nimport { useLocation, NavLink } from \"react-router-dom\"\nimport { SelectedKnowledge } from \"../Option/Knowledge/SelectedKnowledge\"\nimport { ModelSelect } from \"../Common/ModelSelect\"\nimport { PromptSelect } from \"../Common/PromptSelect\"\nimport { useQuery } from \"@tanstack/react-query\"\nimport { fetchChatModels } from \"~/services/ollama\"\nimport { useMessageOption } from \"~/hooks/useMessageOption\"\nimport { Avatar, Select, Tooltip } from \"antd\"\nimport { getAllPrompts } from \"@/db/dexie/helpers\"\nimport { ProviderIcons } from \"../Common/ProviderIcon\"\nimport { NewChat } from \"./NewChat\"\nimport { MoreOptions } from \"./MoreOptions\"\ntype Props = {\n  setSidebarOpen: (open: boolean) => void\n  setOpenModelSettings: (open: boolean) => void\n  saveTemporaryChat?: () => Promise<string>\n}\n\nexport const Header: React.FC<Props> = ({\n  setOpenModelSettings,\n  setSidebarOpen,\n  saveTemporaryChat\n}) => {\n  const { t, i18n } = useTranslation([\"option\", \"common\"])\n  const isRTL = i18n?.dir() === \"rtl\"\n\n  const [shareModeEnabled] = useStorage(\"shareMode\", false)\n  const [hideCurrentChatModelSettings] = useStorage(\n    \"hideCurrentChatModelSettings\",\n    false\n  )\n  const {\n    selectedModel,\n    setSelectedModel,\n    clearChat,\n    selectedSystemPrompt,\n    setSelectedQuickPrompt,\n    setSelectedSystemPrompt,\n    messages,\n    streaming,\n    historyId,\n    temporaryChat\n  } = useMessageOption()\n  const {\n    data: models,\n    isLoading: isModelsLoading,\n    refetch\n  } = useQuery({\n    queryKey: [\"fetchModel\"],\n    queryFn: () => fetchChatModels({ returnEmpty: true }),\n    refetchIntervalInBackground: false,\n    staleTime: 1000 * 60 * 1\n  })\n\n  const { data: prompts, isLoading: isPromptLoading } = useQuery({\n    queryKey: [\"fetchAllPromptsLayout\"],\n    queryFn: getAllPrompts\n  })\n\n  const { pathname } = useLocation()\n\n  const getPromptInfoById = (id: string) => {\n    return prompts?.find((prompt) => prompt.id === id)\n  }\n\n  const handlePromptChange = (value?: string) => {\n    if (!value) {\n      setSelectedSystemPrompt(undefined)\n      setSelectedQuickPrompt(undefined)\n      return\n    }\n    const prompt = getPromptInfoById(value)\n    if (prompt?.is_system) {\n      setSelectedSystemPrompt(prompt.id)\n    } else {\n      setSelectedSystemPrompt(undefined)\n      setSelectedQuickPrompt(prompt!.content)\n    }\n  }\n\n  return (\n    <div\n      data-istemporary-chat={temporaryChat}\n      className={`absolute top-0 z-10 flex h-14 w-full flex-row items-center justify-center p-3 overflow-x-auto lg:overflow-x-visible bg-gray-50/80 backdrop-blur-3xl border-b  dark:bg-[#1a1a1a]/80 dark:border-gray-600 data-[istemporary-chat='true']:bg-gray-200/80 data-[istemporary-chat='true']:dark:bg-black/80`}>\n      <div className=\"flex gap-2 items-center\">\n        {pathname !== \"/\" && (\n          <div>\n            <NavLink\n              to=\"/\"\n              className=\"text-gray-500 items-center dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors\">\n              {isRTL ? (\n                <ChevronRight className={`w-8 h-8`} />\n              ) : (\n                <ChevronLeft className={`w-8 h-8`} />\n              )}\n            </NavLink>\n          </div>\n        )}\n        <div>\n          <button\n            className=\"text-gray-500 dark:text-gray-400\"\n            onClick={() => setSidebarOpen(true)}>\n            <PanelLeftIcon className=\"w-6 h-6\" />\n          </button>\n        </div>\n        <NewChat clearChat={clearChat} />\n        <span className=\"text-lg font-thin text-zinc-300 dark:text-zinc-600\">\n          {\"/\"}\n        </span>\n        <div className=\"hidden lg:block\">\n          <Select\n            className=\"min-w-80  max-w-[460px]\"\n            placeholder={t(\"common:selectAModel\")}\n            // loadingText={t(\"common:selectAModel\")}\n            value={selectedModel}\n            onChange={(e) => {\n              setSelectedModel(e)\n              localStorage.setItem(\"selectedModel\", e)\n            }}\n            filterOption={(input, option) => {\n              //@ts-ignore\n              return (\n                option?.label?.props[\"data-title\"]\n                  ?.toLowerCase()\n                  ?.indexOf(input.toLowerCase()) >= 0\n              )\n            }}\n            showSearch\n            loading={isModelsLoading}\n            options={models?.map((model) => ({\n              label: (\n                <span\n                  key={model.model}\n                  data-title={model.name}\n                  className=\"flex flex-row gap-3 items-center \">\n                  {model?.avatar ? (\n                    <Avatar src={model.avatar} alt={model.name} size=\"small\" />\n                  ) : (\n                    <ProviderIcons\n                      provider={model?.provider}\n                      className=\"w-5 h-5\"\n                    />\n                  )}\n                  <span className=\"line-clamp-2\">\n                    {model?.nickname || model.model}\n                  </span>\n                </span>\n              ),\n              value: model.model\n            }))}\n            size=\"large\"\n            // onRefresh={() => {\n            //   refetch()\n            // }}\n          />\n        </div>\n        <div className=\"lg:hidden\">\n          <ModelSelect />\n        </div>\n        <span className=\"text-lg font-thin text-zinc-300 dark:text-zinc-600\">\n          {\"/\"}\n        </span>\n        <div className=\"hidden lg:block\">\n          <Select\n            size=\"large\"\n            loading={isPromptLoading}\n            showSearch\n            placeholder={t(\"selectAPrompt\")}\n            className=\"w-60\"\n            allowClear\n            onChange={handlePromptChange}\n            value={selectedSystemPrompt}\n            filterOption={(input, option) =>\n              //@ts-ignore\n              option.label.key.toLowerCase().indexOf(input.toLowerCase()) >= 0\n            }\n            options={prompts?.map((prompt) => ({\n              label: (\n                <span\n                  key={prompt.title}\n                  className=\"flex flex-row gap-3 items-center\">\n                  {prompt.is_system ? (\n                    <ComputerIcon className=\"w-4 h-4\" />\n                  ) : (\n                    <ZapIcon className=\"w-4 h-4\" />\n                  )}\n                  {prompt.title}\n                </span>\n              ),\n              value: prompt.id\n            }))}\n          />\n        </div>\n        <div className=\"lg:hidden\">\n          <PromptSelect\n            selectedSystemPrompt={selectedSystemPrompt}\n            setSelectedSystemPrompt={setSelectedSystemPrompt}\n            setSelectedQuickPrompt={setSelectedQuickPrompt}\n          />\n        </div>\n        <SelectedKnowledge />\n      </div>\n      <div className=\"flex flex-1 justify-end px-4\">\n        <div className=\"ml-4 flex items-center md:ml-6\">\n          <div className=\"flex gap-4 items-center\">\n            {temporaryChat && messages.length > 0 && !streaming && (\n              <Tooltip title={t(\"common:saveChat\")}>\n                <button\n                  onClick={saveTemporaryChat}\n                  className=\"!text-gray-500 dark:text-gray-300 hover:text-gray-600 dark:hover:text-gray-300 transition-colors\">\n                  <SaveIcon className=\"w-6 h-6\" />\n                </button>\n              </Tooltip>\n            )}\n            {messages.length > 0 && !streaming && (\n              <MoreOptions\n                shareModeEnabled={shareModeEnabled}\n                historyId={historyId}\n                messages={messages}\n              />\n            )}\n            {!hideCurrentChatModelSettings && (\n              <Tooltip title={t(\"common:currentChatModelSettings\")}>\n                <button\n                  onClick={() => setOpenModelSettings(true)}\n                  className=\"!text-gray-500 dark:text-gray-300 hover:text-gray-600 dark:hover:text-gray-300 transition-colors\">\n                  <BrainCog className=\"w-6 h-6\" />\n                </button>\n              </Tooltip>\n            )}\n            <Tooltip title={t(\"githubRepository\")}>\n              <a\n                href=\"https://github.com/n4ze3m/page-assist\"\n                target=\"_blank\"\n                className=\"!text-gray-500 hidden lg:block dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors\">\n                <GithubIcon className=\"w-6 h-6\" />\n              </a>\n            </Tooltip>\n            <Tooltip title={t(\"settings\")}>\n              <NavLink\n                to=\"/settings\"\n                className=\"!text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors\">\n                <CogIcon className=\"w-6 h-6\" />\n              </NavLink>\n            </Tooltip>\n          </div>\n        </div>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Layouts/Layout.tsx",
    "content": "import React, { useState } from \"react\"\n\nimport { Sidebar } from \"../Option/Sidebar\"\nimport { Drawer, Tooltip } from \"antd\"\n\nimport { useTranslation } from \"react-i18next\"\n\nimport { CurrentChatModelSettings } from \"../Common/Settings/CurrentChatModelSettings\"\nimport { Header } from \"./Header\"\nimport { EraserIcon, XIcon } from \"lucide-react\"\n// import { PageAssitDatabase } from \"@/db/\"\nimport { useMessageOption } from \"@/hooks/useMessageOption\"\nimport {\n  useChatShortcuts,\n  useSidebarShortcuts\n} from \"@/hooks/keyboard/useKeyboardShortcuts\"\nimport { useQueryClient } from \"@tanstack/react-query\"\nimport { useStoreChatModelSettings } from \"@/store/model\"\nimport { PageAssistDatabase } from \"@/db/dexie/chat\"\nimport { useMigration } from \"../../hooks/useMigration\"\nimport { useStorage } from \"@plasmohq/storage/hook\"\n\nexport default function OptionLayout({\n  children\n}: {\n  children: React.ReactNode\n}) {\n  const [sidebarOpen, setSidebarOpen] = useState(false)\n  const { t } = useTranslation([\"option\", \"common\", \"settings\"])\n  const [openModelSettings, setOpenModelSettings] = useState(false)\n  useMigration()\n  const [sidebarPosition] = useStorage(\"sidebarPosition\", \"left\")\n  const {\n    setMessages,\n    setHistory,\n    setHistoryId,\n    historyId,\n    clearChat,\n    setSelectedModel,\n    temporaryChat,\n    setSelectedSystemPrompt,\n    setContextFiles,\n    useOCR,\n    selectedModel,\n    saveTemporaryChat\n  } = useMessageOption()\n  const queryClient = useQueryClient()\n  const { setSystemPrompt } = useStoreChatModelSettings()\n\n  // Create toggle function for sidebar\n  const toggleSidebar = () => {\n    setSidebarOpen((prev) => !prev)\n  }\n\n  // Initialize shortcuts\n  useChatShortcuts(clearChat, true)\n  useSidebarShortcuts(toggleSidebar, true)\n\n  return (\n    <div className=\"flex h-full w-full\">\n      <main className=\"relative h-dvh w-full\">\n        <div className=\"relative z-20 w-full\">\n          <Header\n            setSidebarOpen={setSidebarOpen}\n            setOpenModelSettings={setOpenModelSettings}\n            saveTemporaryChat={saveTemporaryChat}\n          />\n        </div>\n        {/* <div className=\"relative flex h-full flex-col items-center\"> */}\n        {children}\n        {/* </div> */}\n        <Drawer\n          title={\n            <div className=\"flex items-center justify-between\">\n              {t(\"sidebarTitle\")}\n\n              <div className=\"flex items-center space-x-3\">\n                <Tooltip\n                  title={t(\n                    \"settings:generalSettings.system.deleteChatHistory.label\"\n                  )}\n                  placement=\"right\">\n                  <button\n                    onClick={async () => {\n                      const confirm = window.confirm(\n                        t(\n                          \"settings:generalSettings.system.deleteChatHistory.confirm\"\n                        )\n                      )\n\n                      if (confirm) {\n                        const db = new PageAssistDatabase()\n                        await db.deleteAllChatHistory()\n                        await queryClient.invalidateQueries({\n                          queryKey: [\"fetchChatHistory\"]\n                        })\n                        clearChat()\n                      }\n                    }}\n                    className=\"text-gray-600 hover:text-gray-800 dark:text-gray-300 dark:hover:text-gray-100\">\n                    <EraserIcon className=\"size-5\" />\n                  </button>\n                </Tooltip>\n                <button\n                  onClick={() => setSidebarOpen(false)}\n                  className=\"md:hidden\">\n                  <XIcon className=\"h-5 w-5 text-gray-500 dark:text-gray-400\" />\n                </button>\n              </div>\n            </div>\n          }\n          placement={sidebarPosition === \"right\" ? \"right\" : \"left\"}\n          closeIcon={null}\n          onClose={() => setSidebarOpen(false)}\n          open={sidebarOpen}>\n          <Sidebar\n            isOpen={sidebarOpen}\n            onClose={() => setSidebarOpen(false)}\n            setMessages={setMessages}\n            setHistory={setHistory}\n            setHistoryId={setHistoryId}\n            setSelectedModel={setSelectedModel}\n            setSelectedSystemPrompt={setSelectedSystemPrompt}\n            clearChat={clearChat}\n            historyId={historyId}\n            setSystemPrompt={setSystemPrompt}\n            temporaryChat={temporaryChat}\n            history={history}\n            setContext={setContextFiles}\n            selectedModel={selectedModel}\n          />\n        </Drawer>\n\n        <CurrentChatModelSettings\n          open={openModelSettings}\n          setOpen={setOpenModelSettings}\n          useDrawer\n          isOCREnabled={useOCR}\n        />\n      </main>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Layouts/LinkComponent.tsx",
    "content": "import { Link } from \"react-router-dom\"\nimport { BetaTag } from \"../Common/Beta\"\n\nfunction classNames(...classes: string[]) {\n  return classes.filter(Boolean).join(\" \")\n}\n\nexport const LinkComponent = (item: {\n  href: string\n  name: string | JSX.Element\n  icon: any\n  current: string\n  beta?: boolean\n}) => {\n  return (\n    <li className=\"inline-flex items-center\">\n      <Link\n        to={item.href}\n        className={classNames(\n          item.current === item.href\n            ? \"bg-gray-100 text-gray-600 dark:bg-[#262626] dark:text-white\"\n            : \"text-gray-700 hover:text-gray-600 hover:bg-gray-100 dark:text-gray-200 dark:hover:text-white dark:hover:bg-[#262626]\",\n          \"group flex gap-x-3 rounded-md py-2 pl-2 pr-3 text-sm font-semibold\"\n        )}>\n        <item.icon\n          className={classNames(\n            item.current === item.href\n              ? \"text-gray-600 dark:text-white\"\n              : \"text-gray-500 group-hover:text-gray-600 dark:text-gray-200 dark:group-hover:text-white\",\n            \"h-6 w-6 shrink-0\"\n          )}\n          aria-hidden=\"true\"\n        />\n        {item.name}\n      </Link>\n      {item.beta && <BetaTag />}\n    </li>\n  )\n}\n"
  },
  {
    "path": "src/components/Layouts/MoreOptions.tsx",
    "content": "import {\n  MoreHorizontal,\n  FileText,\n  Share2,\n  FileJson,\n  FileCode,\n  ImageIcon\n} from \"lucide-react\"\nimport { Dropdown, MenuProps, message } from \"antd\"\nimport { Message } from \"@/types/message\"\nimport { useState } from \"react\"\nimport { ShareModal } from \"../Common/ShareModal\"\nimport { useTranslation } from \"react-i18next\"\nimport { removeModelSuffix } from \"@/db/dexie/models\"\nimport { copyToClipboard } from \"@/utils/clipboard\"\nimport ReactDOM from \"react-dom\"\nimport html2canvas from \"html2canvas\"\nimport { ImageExportWrapper } from \"../Common/ImageExport\"\nimport { convertMathDelimiters } from \"@/utils/math-delimiter\"\ninterface MoreOptionsProps {\n  messages: Message[]\n  historyId: string\n  shareModeEnabled: boolean\n}\nconst formatAsText = (messages: Message[]) => {\n  return messages\n    .map((msg) => {\n      const text = `${msg.isBot ? removeModelSuffix(`${msg.modelName || msg.name}`?.replaceAll(/accounts\\/[^\\/]+\\/models\\//g, \"\")) : \"You\"}: ${msg.message}`\n      return text\n    })\n    .join(\"\\n\\n\")\n}\n\n\nconst formatAsMarkdown = (messages: Message[]) => {\n  return messages\n    .map((msg) => {\n      let content = `### **${msg.isBot ? removeModelSuffix(`${msg.modelName || msg.name}`?.replaceAll(/accounts\\/[^\\/]+\\/models\\//g, \"\")) : \"You\"}**:\\n\\n${convertMathDelimiters(msg.message)}`\n\n      if (msg.images && msg.images.length > 0) {\n        const imageMarkdown = msg.images\n          .filter((img) => img.length > 0)\n          .map((img) => `\\n\\n![Image](${img})`)\n          .join(\"\\n\")\n        content += imageMarkdown\n      }\n\n      return content\n    })\n    .join(\"\\n\\n\")\n}\n\nconst downloadFile = (content: string, filename: string) => {\n  const blob = new Blob([content], { type: \"text/plain;charset=utf-8\" })\n  const url = URL.createObjectURL(blob)\n  const link = document.createElement(\"a\")\n  link.href = url\n  link.download = filename\n  document.body.appendChild(link)\n  link.click()\n  document.body.removeChild(link)\n  URL.revokeObjectURL(url)\n}\n\nconst generateChatImage = async (messages: Message[]) => {\n  const root = document.createElement(\"div\")\n  document.body.appendChild(root)\n  const element = <ImageExportWrapper messages={messages} />\n  ReactDOM.render(element, root)\n  await new Promise((resolve) => setTimeout(resolve, 100))\n  const container = document.getElementById(\"export-container\")\n  if (!container) {\n    throw new Error(\"Export container not found\")\n  }\n  const canvas = await html2canvas(container, {\n    useCORS: true,\n    backgroundColor: \"#ffffff\",\n    scale: 2\n  })\n  ReactDOM.unmountComponentAtNode(root)\n  document.body.removeChild(root)\n\n  return canvas.toDataURL(\"image/png\")\n}\n\nexport const MoreOptions = ({\n  shareModeEnabled = false,\n  historyId,\n  messages\n}: MoreOptionsProps) => {\n  const { t } = useTranslation([\"option\", \"settings\"])\n  const [onShareOpen, setOnShareOpen] = useState(false)\n  const baseItems: MenuProps[\"items\"] = [\n    {\n      type: \"group\",\n      label: t(\"more.copy.group\"),\n      children: [\n        {\n          key: \"copy-text\",\n          label: t(\"more.copy.asText\"),\n          icon: <FileText className=\"w-4 h-4\" />,\n          onClick: async () => {\n            await copyToClipboard({\n              text: formatAsText(messages),\n              formatted: false\n            })\n            message.success(t(\"more.copy.success\"))\n          }\n        },\n        {\n          key: \"copy-as-formatted-text\",\n          label: t(\n            \"settings:generalSettings.settings.copyAsFormattedText.label\"\n          ),\n          icon: <FileText className=\"w-4 h-4\" />,\n          onClick: async () => {\n            const mkd = formatAsMarkdown(messages)\n            await copyToClipboard({\n              text: mkd,\n              formatted: true\n            })\n            message.success(t(\"more.copy.success\"))\n          }\n        },\n        {\n          key: \"copy-markdown\",\n          label: t(\"more.copy.asMarkdown\"),\n          icon: <FileCode className=\"w-4 h-4\" />,\n          onClick: async () => {\n            await copyToClipboard({\n              text: formatAsMarkdown(messages),\n              formatted: false\n            })\n            message.success(t(\"more.copy.success\"))\n          }\n        }\n      ]\n    },\n    {\n      type: \"divider\"\n    },\n    {\n      type: \"group\",\n      label: t(\"more.download.group\"),\n      children: [\n        {\n          key: \"download-txt\",\n          label: t(\"more.download.text\"),\n          icon: <FileText className=\"w-4 h-4\" />,\n          onClick: () => {\n            downloadFile(formatAsText(messages), \"chat.txt\")\n          }\n        },\n        {\n          key: \"download-md\",\n          label: t(\"more.download.markdown\"),\n          icon: <FileCode className=\"w-4 h-4\" />,\n          onClick: () => {\n            downloadFile(formatAsMarkdown(messages), \"chat.md\")\n          }\n        },\n        {\n          key: \"download-json\",\n          label: t(\"more.download.json\"),\n          icon: <FileJson className=\"w-4 h-4\" />,\n          onClick: () => {\n            const jsonContent = JSON.stringify(messages, null, 2)\n            downloadFile(jsonContent, \"chat.json\")\n          }\n        },\n        {\n          key: \"download-image\",\n          label: t(\"more.download.image\"),\n          icon: <ImageIcon className=\"w-4 h-4\" />,\n          onClick: async () => {\n            try {\n              const dataUrl = await generateChatImage(messages)\n              const link = document.createElement(\"a\")\n              link.download = `chat_${new Date().toISOString()}.png`\n              link.href = dataUrl\n              link.click()\n            } catch (e) {\n              message.error(\"Failed to generate image\")\n            }\n          }\n        }\n      ]\n    }\n  ]\n\n  const shareItem = {\n    type: \"divider\"\n  } as const\n\n  const shareOption = {\n    key: \"share\",\n    label: t(\"more.share\"),\n    icon: <Share2 className=\"w-4 h-4\" />,\n    onClick: () => {\n      setOnShareOpen(true)\n    }\n  }\n\n  const items = shareModeEnabled\n    ? [...baseItems, shareItem, shareOption]\n    : baseItems\n\n  return (\n    <>\n      <Dropdown\n        menu={{\n          items\n        }}\n        trigger={[\"click\"]}\n        placement=\"bottomRight\">\n        <button className=\"!text-gray-500 dark:text-gray-300 hover:text-gray-600 dark:hover:text-gray-300 transition-colors\">\n          <MoreHorizontal className=\"w-6 h-6\" />\n        </button>\n      </Dropdown>\n      <ShareModal\n        open={onShareOpen}\n        historyId={historyId}\n        messages={messages}\n        setOpen={setOnShareOpen}\n      />\n    </>\n  )\n}\n"
  },
  {
    "path": "src/components/Layouts/NewChat.tsx",
    "content": "import { SquarePen } from \"lucide-react\"\nimport { useTranslation } from \"react-i18next\"\nimport { notification, Tooltip } from \"antd\"\nimport { useMessageOption } from \"@/hooks/useMessageOption\"\nimport { BsIncognito } from \"react-icons/bs\"\nimport { isFireFoxPrivateMode } from \"@/utils/is-private-mode\"\n\ntype Props = {\n  clearChat: () => void\n}\n\nexport const NewChat: React.FC<Props> = ({ clearChat }) => {\n  const { t } = useTranslation([\"option\", \"common\"])\n\n  const { temporaryChat, setTemporaryChat, messages } = useMessageOption()\n\n  return (\n    <div className=\"flex items-center justify-between\">\n      <button\n        onClick={clearChat}\n        className=\"inline-flex dark:bg-transparent bg-white items-center rounded-s-lg rounded-e-none border dark:border-gray-700 bg-transparent px-3 py-2.5 pe-6 text-xs lg:text-sm font-medium leading-4 text-gray-800 dark:text-white disabled:opacity-50 ease-in-out transition-colors duration-200 hover:bg-gray-100 dark:hover:bg-gray-800 dark:hover:text-white\">\n        <SquarePen className=\"size-4 sm:size-5\" />\n        <span className=\"truncate ms-3 hidden sm:inline\">{t(\"newChat\")}</span>\n      </button>\n      <Tooltip title={t(\"temporaryChat\")}>\n        <button\n          data-istemporary-chat={temporaryChat}\n          onClick={() => {\n            if (isFireFoxPrivateMode) {\n              notification.error({\n                message: \"Error\",\n                description:\n                  \"Page Assist can't save chat in Firefox Private Mode. Temporary chat is enabled by default. More fixes coming soon.\"\n              })\n              return\n            }\n\n            setTemporaryChat(!temporaryChat)\n            if (messages.length > 0) {\n              clearChat()\n            }\n          }}\n          className=\"inline-flex dark:bg-transparent bg-white items-center rounded-lg border-s-0 rounded-s-none border dark:border-gray-700 bg-transparent px-3 py-2.5 text-xs lg:text-sm font-medium leading-4 text-gray-800 dark:text-white disabled:opacity-50 ease-in-out transition-colors duration-200 hover:bg-gray-100 dark:hover:bg-gray-800 dark:hover:text-white data-[istemporary-chat='true']:bg-gray-100 data-[istemporary-chat='true']:dark:bg-gray-800\">\n          <BsIncognito className=\"size-4 sm:size-5 text-gray-500 dark:text-gray-400\" />\n        </button>\n      </Tooltip>\n      {/* </Dropdown> */}\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Layouts/SettingsOptionLayout.tsx",
    "content": "import {\n  BookIcon,\n  BrainCircuitIcon,\n  OrbitIcon,\n  ShareIcon,\n  BlocksIcon,\n  InfoIcon,\n  CombineIcon,\n  ChromeIcon,\n  CpuIcon,\n} from \"lucide-react\"\nimport { useTranslation } from \"react-i18next\"\nimport { Link, useLocation } from \"react-router-dom\"\nimport { OllamaIcon } from \"../Icons/Ollama\"\nimport { LinkComponent } from \"./LinkComponent\"\nimport { MCPIcon } from \"../Icons/MCPIcon\"\n\n\nexport const SettingsLayout = ({ children }: { children: React.ReactNode }) => {\n  const location = useLocation()\n  const { t } = useTranslation([\"settings\", \"common\", \"openai\"])\n  return (\n    <div className=\"flex min-h-screen  w-full flex-col\">\n      <main className=\"relative w-full flex-1\">\n        <div className=\"mx-auto w-full h-full custom-scrollbar overflow-y-auto\">\n          <div className=\"flex flex-col lg:flex-row lg:gap-x-16 lg:px-24\">\n            <aside className=\"sticky lg:mt-0 mt-14 top-0  bg-white dark:bg-[#1a1a1a] border-b dark:border-gray-600 lg:border-0 lg:bg-transparent lg:dark:bg-transparent\">\n              <nav className=\"w-full overflow-x-auto px-4 py-4 sm:px-6 lg:px-0 lg:py-0 lg:mt-20\">\n                <ul\n                  role=\"list\"\n                  className=\"flex flex-row lg:flex-col gap-x-3 gap-y-1 min-w-max lg:min-w-0\">\n                  <LinkComponent\n                    href=\"/settings\"\n                    name={t(\"generalSettings.title\")}\n                    icon={OrbitIcon}\n                    current={location.pathname}\n                  />\n                  <LinkComponent\n                    href=\"/settings/rag\"\n                    name={t(\"rag.title\")}\n                    icon={CombineIcon}\n                    current={location.pathname}\n                  />\n                  <LinkComponent\n                    href=\"/settings/ollama\"\n                    name={t(\"ollamaSettings.title\")}\n                    icon={OllamaIcon}\n                    current={location.pathname}\n                  />\n                  {import.meta.env.BROWSER !== \"firefox\" && (\n                    <LinkComponent\n                      href=\"/settings/chrome\"\n                      name={t(\"chromeAiSettings.title\")}\n                      icon={ChromeIcon}\n                      current={location.pathname}\n                      beta\n                    />\n                  )}\n                  <LinkComponent\n                    href=\"/settings/openai\"\n                    name={t(\"openai:settings\")}\n                    icon={CpuIcon}\n                    current={location.pathname}\n                  />\n                  <LinkComponent\n                    href=\"/settings/mcp\"\n                    name={t(\"mcpSettings.title\")}\n                    icon={MCPIcon}\n                    current={location.pathname}\n                    beta\n                  />\n                  <LinkComponent\n                    href=\"/settings/model\"\n                    name={t(\"manageModels.title\")}\n                    current={location.pathname}\n                    icon={BrainCircuitIcon}\n                  />\n                  <LinkComponent\n                    href=\"/settings/knowledge\"\n                    name={\n                      <div className=\"inline-flex items-center gap-2\">\n                        {t(\"manageKnowledge.title\")}\n                      </div>\n                    }\n                    icon={BlocksIcon}\n                    current={location.pathname}\n                  />\n                  <LinkComponent\n                    href=\"/settings/prompt\"\n                    name={t(\"managePrompts.title\")}\n                    icon={BookIcon}\n                    current={location.pathname}\n                  />\n                  <LinkComponent\n                    href=\"/settings/share\"\n                    name={t(\"manageShare.title\")}\n                    icon={ShareIcon}\n                    current={location.pathname}\n                  />\n                  <LinkComponent\n                    href=\"/settings/about\"\n                    name={t(\"about.title\")}\n                    icon={InfoIcon}\n                    current={location.pathname}\n                  />\n                </ul>\n              </nav>\n            </aside>\n            <main className=\"flex-1 px-4 py-8 sm:px-6 lg:px-0 lg:py-20\">\n              <div className=\"mx-auto max-w-4xl space-y-8 sm:space-y-10\">\n                {children}\n              </div>\n            </main>\n          </div>\n        </div>\n      </main>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Layouts/SidePanelSettingsLayout.tsx",
    "content": "import {\n  BrainCircuitIcon,\n  OrbitIcon,\n  CpuIcon,\n  ChevronLeft,\n  ChevronRight\n} from \"lucide-react\"\nimport { useTranslation } from \"react-i18next\"\nimport { useLocation, Link } from \"react-router-dom\"\nimport { LinkComponent } from \"./LinkComponent\"\nimport logoImage from \"~/assets/icon.png\"\n\nexport const SidePanelSettingsLayout = ({\n  children\n}: {\n  children: React.ReactNode\n}) => {\n  const location = useLocation()\n  const { t, i18n } = useTranslation([\"settings\", \"common\", \"openai\"])\n  const isRTL = i18n?.dir() === \"rtl\"\n\n  return (\n    <div className=\"flex w-full flex-col min-h-screen bg-neutral-50 dark:bg-[#1a1a1a]\">\n      {/* Mobile-optimized Header */}\n      <header className=\"sticky top-0 z-20 bg-white dark:bg-[#1a1a1a] border-b border-gray-200 dark:border-gray-700 shadow-sm\">\n        <div className=\"flex items-center justify-between px-4 py-3 sm:px-6\">\n          <div className=\"flex items-center gap-3\">\n            <Link\n              to=\"/\"\n              className=\"flex items-center justify-center w-8 h-8 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-pink-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900\"\n              aria-label={t(\"common:goBack\")}>\n              {isRTL ? (\n                <ChevronRight className=\"h-5 w-5 text-gray-600 dark:text-gray-400\" />\n              ) : (\n                <ChevronLeft className=\"h-5 w-5 text-gray-600 dark:text-gray-400\" />\n              )}\n            </Link>\n            <div className=\"flex items-center gap-2\">\n              <img\n                className=\"h-7 w-7 rounded-lg\"\n                src={logoImage}\n                alt={t(\"common:pageAssist\")}\n              />\n              <div className=\"flex flex-col\">\n                <span className=\"text-sm font-semibold text-gray-900 dark:text-white\">\n                  {t(\"common:pageAssist\")}\n                </span>\n              </div>\n            </div>\n          </div>\n        </div>\n      </header>\n\n      <main className=\"relative w-full flex-1\">\n        <div className=\"mx-auto w-full h-full custom-scrollbar overflow-y-auto\">\n          <div className=\"flex flex-col lg:flex-row lg:gap-x-16 lg:px-24\">\n            <aside className=\"sticky lg:mt-0 top-0 bg-white dark:bg-[#1a1a1a] border-b dark:border-gray-600 lg:border-0 lg:bg-transparent lg:dark:bg-transparent\">\n              <nav className=\"w-full overflow-x-auto px-4 py-4 sm:px-6 lg:px-0 lg:py-0 lg:mt-8\">\n                <ul\n                  role=\"list\"\n                  className=\"flex flex-row lg:flex-col gap-x-3 gap-y-1 min-w-max lg:min-w-0\">\n                  <LinkComponent\n                    href=\"/settings\"\n                    name={t(\"generalSettings.title\")}\n                    icon={OrbitIcon}\n                    current={location.pathname}\n                  />\n\n                  <LinkComponent\n                    href=\"/settings/openai\"\n                    name={t(\"openai:settings\")}\n                    icon={CpuIcon}\n                    current={location.pathname}\n                  />\n                  <LinkComponent\n                    href=\"/settings/model\"\n                    name={t(\"manageModels.title\")}\n                    current={location.pathname}\n                    icon={BrainCircuitIcon}\n                  />\n                </ul>\n              </nav>\n            </aside>\n            <main className=\"flex-1 px-4 py-6 lg:px-0 lg:py-12\">\n              <div className=\"mx-auto max-w-4xl space-y-6 sm:space-y-8\">\n                {children}\n              </div>\n            </main>\n          </div>\n        </div>\n      </main>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Option/Knowledge/AddKnowledge.tsx",
    "content": "import { createKnowledge } from \"@/db/dexie/knowledge\"\nimport { Source } from \"@/db/knowledge\"\nimport { defaultEmbeddingModelForRag } from \"@/services/ollama\"\nimport { convertTextToSource, convertToSource } from \"@/utils/to-source\"\nimport { useMutation } from \"@tanstack/react-query\"\nimport { Modal, Form, Input, Upload, message, Tabs, Select } from \"antd\"\nimport { InboxIcon } from \"lucide-react\"\nimport { useTranslation } from \"react-i18next\"\nimport PubSub from \"pubsub-js\"\nimport { KNOWLEDGE_QUEUE } from \"@/queue\"\nimport { useStorage } from \"@plasmohq/storage/hook\"\nimport { unsupportedTypes } from \"./utils/unsupported-types\"\nimport React from \"react\"\n\ntype Props = {\n  open: boolean\n  setOpen: React.Dispatch<React.SetStateAction<boolean>>\n}\n\nexport const AddKnowledge = ({ open, setOpen }: Props) => {\n  const { t } = useTranslation([\"knowledge\", \"common\"])\n  const [form] = Form.useForm()\n  const [totalFilePerKB] = useStorage(\"totalFilePerKB\", 5)\n  const [mode, setMode] = React.useState<\"upload\" | \"text\">(\"upload\")\n\n  const onUploadHandler = async (data: any) => {\n    const defaultEM = await defaultEmbeddingModelForRag()\n\n    if (!defaultEM) {\n      throw new Error(t(\"noEmbeddingModel\"))\n    }\n\n  const source: Source[] = []\n\n    const allowedTypes = [\n      \"application/pdf\",\n      \"text/csv\",\n      \"text/plain\",\n      \"text/markdown\",\n      \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\"\n    ]\n\n    if (mode === \"upload\") {\n      for (const file of data.file || []) {\n        let mime = file.type\n        if (!allowedTypes.includes(mime)) {\n          mime = \"text/plain\"\n        }\n        const _src = await convertToSource({ file, mime, sourceType: \"file_upload\" })\n        source.push(_src)\n      }\n    } else {\n      // Text mode validation\n      const rawText: string = (data?.textContent || \"\").trim()\n      const textType: string = data?.textType || \"plain\"\n      if (!rawText) {\n        throw new Error(t(\"form.textInput.required\"))\n      }\n      // Prevent oversized content (e.g., > 500k chars)\n      if (rawText.length > 500000) {\n        throw new Error(t(\"form.textInput.tooLarge\"))\n      }\n\n      const asMarkdown = textType === \"markdown\"\n      const filename = data?.title\n        ? `${data?.title}.txt`\n        : `pasted_${new Date().getTime()}.txt`\n      const _src = await convertTextToSource({\n        text: rawText,\n        filename,\n        mime: asMarkdown ? \"text/markdown\" : \"text/plain\",\n        asMarkdown,\n        sourceType: \"text_input\"\n      })\n      source.push(_src)\n    }\n\n    let title = data?.title?.trim()\n    if (!title || title.length === 0) {\n      if (mode === \"text\") {\n        const text = (data?.textContent || \"\").trim()\n        title = text.substring(0, 50) || t(\"form.textInput.defaultTitle\")\n      } else if ((data?.file || []).length > 0) {\n        title = (data.file[0]?.name as string) || t(\"form.textInput.defaultTitle\")\n      } else {\n        title = t(\"form.textInput.defaultTitle\")\n      }\n    }\n\n    const knowledge = await createKnowledge({\n      embedding_model: defaultEM,\n      source,\n      title\n    })\n\n    return knowledge.id\n  }\n\n  const { mutate: saveKnowledge, isPending: isSaving } = useMutation({\n    mutationFn: onUploadHandler,\n    onError: (error) => {\n      message.error(error.message)\n    },\n    onSuccess: async (id) => {\n      message.success(t(\"form.success\"))\n      PubSub.publish(KNOWLEDGE_QUEUE, id)\n      form.resetFields()\n      setOpen(false)\n    }\n  })\n\n  return (\n    <Modal\n      title={t(\"addKnowledge\")}\n      open={open}\n      footer={null}\n      onCancel={() => setOpen(false)}>\n      <Tabs\n        activeKey={mode}\n        onChange={(key) => setMode(key as any)}\n        items={[\n          { key: \"upload\", label: t(\"form.tabs.upload\") },\n          { key: \"text\", label: t(\"form.tabs.text\") }\n        ]}\n      />\n      <Form onFinish={saveKnowledge} form={form} layout=\"vertical\">\n        {/* Title is optional now */}\n        <Form.Item name=\"title\" label={t(\"form.title.label\")}>\n          <Input size=\"large\" placeholder={t(\"form.title.placeholderOptional\")} />\n        </Form.Item>\n\n        {mode === \"upload\" ? (\n          <Form.Item\n            name=\"file\"\n            label={t(\"form.uploadFile.label\")}\n            rules={[\n              {\n                required: true,\n                message: t(\"form.uploadFile.required\")\n              }\n            ]}\n            getValueFromEvent={(e) => {\n              if (Array.isArray(e)) {\n                return e\n              }\n              return e?.fileList\n            }}>\n            <Upload.Dragger\n              multiple={true}\n              maxCount={totalFilePerKB}\n              beforeUpload={(file) => {\n                const allowedTypes = [\n                  \"application/pdf\",\n                  \"text/csv\",\n                  \"text/plain\",\n                  \"text/markdown\",\n                  \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\"\n                ]\n                  .map((type) => type.toLowerCase())\n                  .join(\", \")\n\n                if (unsupportedTypes.includes(file.type.toLowerCase())) {\n                  message.error(\n                    t(\"form.uploadFile.uploadError\", { allowedTypes })\n                  )\n                  return Upload.LIST_IGNORE\n                }\n\n                return false\n              }}>\n              <div className=\"p-3\">\n                <p className=\"flex justify-center ant-upload-drag-icon\">\n                  <InboxIcon className=\"w-10 h-10 text-gray-400\" />\n                </p>\n                <p className=\"ant-upload-text\">\n                  {t(\"form.uploadFile.uploadText\")}\n                </p>\n              </div>\n            </Upload.Dragger>\n          </Form.Item>\n        ) : (\n          <>\n            <Form.Item\n              name=\"textType\"\n              label={t(\"form.textInput.typeLabel\")}\n              initialValue=\"plain\">\n              <Select\n                options={[\n                  { value: \"plain\", label: t(\"form.textInput.type.plain\") },\n                  { value: \"markdown\", label: t(\"form.textInput.type.markdown\") },\n                  { value: \"code\", label: t(\"form.textInput.type.code\") }\n                ]}\n              />\n            </Form.Item>\n            <Form.Item\n              name=\"textContent\"\n              label={t(\"form.textInput.contentLabel\")}\n              rules={[{ required: true, message: t(\"form.textInput.required\") }]}>\n              <Input.TextArea\n                autoSize={{ minRows: 8, maxRows: 16 }}\n                placeholder={t(\"form.textInput.placeholder\")}\n              />\n            </Form.Item>\n          </>\n        )}\n\n        <Form.Item>\n          <button\n            type=\"submit\"\n            disabled={isSaving}\n            className=\"inline-flex items-center justify-center w-full px-2 py-2 font-medium leading-4 text-center text-white bg-black border border-transparent rounded-md shadow-sm text-md 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\">\n            {t(\"form.submit\")}\n          </button>\n        </Form.Item>\n      </Form>\n    </Modal>\n  )\n}\n"
  },
  {
    "path": "src/components/Option/Knowledge/EditKnowledgeSettings.tsx",
    "content": "import { getKnowledgeById, updateKnowledgebase } from \"@/db/dexie/knowledge\"\nimport { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\"\nimport { Alert, Button, Form, Input, Modal, Skeleton, message } from \"antd\"\nimport { Loader2 } from \"lucide-react\"\nimport React from \"react\"\nimport { useTranslation } from \"react-i18next\"\n\nconst DEFAULT_RAG_QUESTION_PROMPT =\n  \"Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.   Chat History: {chat_history} Follow Up Input: {question} Standalone question:\"\n\nconst DEFAUTL_RAG_SYSTEM_PROMPT = `You are a helpful AI assistant. Use the following pieces of context to answer the question at the end. If you don't know the answer, just say you don't know. DO NOT try to make up an answer. If the question is not related to the context, politely respond that you are tuned to only answer questions that are related to the context.  {context}  Question: {question} Helpful answer:`\n\ntype Props = {\n  id: string\n  open: boolean\n  setOpen: (open: boolean) => void\n}\n\nexport const EditKnowledgeSettings: React.FC<Props> = ({\n  id,\n  open,\n  setOpen\n}) => {\n  const [form] = Form.useForm()\n  const { t } = useTranslation([\"knowledge\", \"common\"])\n  const queryClient = useQueryClient()\n\n  const { status } = useQuery({\n    queryKey: [\"fetchKnowledgeById\", id],\n    queryFn: async () => {\n      const data = await getKnowledgeById(id)\n      if (data) {\n        form.setFieldsValue({\n          title: data.title,\n          systemPrompt: data.systemPrompt || \"\",\n          followupPrompt: data.followupPrompt || \"\"\n        })\n      }\n      return data\n    },\n    enabled: open && !!id,\n    staleTime: 0\n  })\n\n  const { mutate, isPending } = useMutation({\n    mutationFn: updateKnowledgebase,\n    onSuccess: async () => {\n      await queryClient.invalidateQueries({\n        queryKey: [\"fetchAllKnowledge\"]\n      })\n      await queryClient.invalidateQueries({\n        queryKey: [\"fetchKnowledgeById\", id]\n      })\n      message.success(t(\"editSettings.success\"))\n      setOpen(false)\n    },\n    onError: (error) => {\n      message.error(error.message)\n    }\n  })\n\n  const handleSubmit = (values: any) => {\n    mutate({\n      id,\n      title: values.title,\n      systemPrompt: values.systemPrompt,\n      followupPrompt: values.followupPrompt\n    })\n  }\n\n  return (\n    <Modal\n      title={t(\"editSettings.title\")}\n      open={open}\n      onCancel={() => {\n        setOpen(false)\n      }}\n      footer={null}\n      width={700}>\n      {status === \"pending\" && <Skeleton active />}\n      {status === \"success\" && (\n        <Form onFinish={handleSubmit} form={form} layout=\"vertical\">\n          <Form.Item\n            name=\"title\"\n            label={t(\"editSettings.form.title.label\")}\n            rules={[\n              {\n                required: true,\n                message: t(\"editSettings.form.title.required\")\n              }\n            ]}>\n            <Input\n              size=\"large\"\n              placeholder={t(\"editSettings.form.title.placeholder\")}\n            />\n          </Form.Item>\n\n          <Form.Item\n            name=\"systemPrompt\"\n            label={\n              <div className=\"flex items-center justify-between w-full\">\n                <span>{t(\"editSettings.form.systemPrompt.label\")}</span>\n                <Button\n                  size=\"small\"\n                  type=\"link\"\n                  onClick={() => {\n                    form.setFieldValue(\n                      \"systemPrompt\",\n                      DEFAUTL_RAG_SYSTEM_PROMPT\n                    )\n                  }}>\n                  {t(\"editSettings.form.systemPrompt.prefillButton\")}\n                </Button>\n              </div>\n            }\n            help={t(\"editSettings.form.systemPrompt.help\")}>\n            <Input.TextArea\n              autoSize={{ minRows: 4, maxRows: 10 }}\n              placeholder={t(\"editSettings.form.systemPrompt.placeholder\")}\n            />\n          </Form.Item>\n\n          <Form.Item\n            name=\"followupPrompt\"\n            label={\n              <div className=\"flex items-center justify-between w-full\">\n                <span>{t(\"editSettings.form.followupPrompt.label\")}</span>\n                <Button\n                  size=\"small\"\n                  type=\"link\"\n                  onClick={() => {\n                    form.setFieldValue(\n                      \"followupPrompt\",\n                      DEFAULT_RAG_QUESTION_PROMPT\n                    )\n                  }}>\n                  {t(\"editSettings.form.followupPrompt.prefillButton\")}\n                </Button>\n              </div>\n            }\n            help={t(\"editSettings.form.followupPrompt.help\")}>\n            <Input.TextArea\n              autoSize={{ minRows: 4, maxRows: 10 }}\n              placeholder={t(\"editSettings.form.followupPrompt.placeholder\")}\n            />\n          </Form.Item>\n\n          <button\n            type=\"submit\"\n            disabled={isPending}\n            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\">\n            {isPending ? (\n              <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n            ) : (\n              t(\"common:save\")\n            )}\n          </button>\n        </Form>\n      )}\n    </Modal>\n  )\n}\n"
  },
  {
    "path": "src/components/Option/Knowledge/KnowledgeIcon.tsx",
    "content": "import { CSVIcon } from \"@/components/Icons/CSVIcon\"\nimport { PDFIcon } from \"@/components/Icons/PDFIcon\"\nimport { TXTIcon } from \"@/components/Icons/TXTIcon\"\n\ntype Props = {\n  type: string\n  className?: string\n}\n\nexport const KnowledgeIcon = ({ type, className = \"w-6 h-6\" }: Props) => {\n  if (type === \"pdf\" || type === \"application/pdf\") {\n    return <PDFIcon className={className} />\n  } else if (type === \"csv\" || type === \"text/csv\") {\n    return <CSVIcon className={className} />\n  } else if (type === \"txt\" || type === \"text/plain\") {\n    return <TXTIcon className={className} />\n  }\n}\n"
  },
  {
    "path": "src/components/Option/Knowledge/KnowledgeSelect.tsx",
    "content": "import { getAllKnowledge } from \"@/db/dexie/knowledge\"\nimport { useMessageOption } from \"@/hooks/useMessageOption\"\nimport { useQuery } from \"@tanstack/react-query\"\nimport { Dropdown, Tooltip } from \"antd\"\nimport { Blocks } from \"lucide-react\"\nimport React from \"react\"\nimport { useTranslation } from \"react-i18next\"\n\nexport const KnowledgeSelect: React.FC = () => {\n  const { t } = useTranslation(\"playground\")\n  const { setSelectedKnowledge, selectedKnowledge } = useMessageOption()\n  const { data } = useQuery({\n    queryKey: [\"getAllKnowledge\"],\n    queryFn: async () => {\n      const data = await getAllKnowledge(\"finished\")\n      return data\n    },\n    refetchInterval: 1000\n  })\n\n  return (\n    <>\n      {data && data.length > 0 && (\n        <Dropdown\n          menu={{\n            items:\n              data?.map((d) => ({\n                key: d.id,\n                label: (\n                  <div className=\"w-52 gap-2 text-lg truncate inline-flex line-clamp-3  items-center  dark:border-gray-700\">\n                    <div>\n                      <Blocks className=\"h-6 w-6 text-gray-400\" />\n                    </div>\n                    {d.title}\n                  </div>\n                ),\n                onClick: () => {\n                  const knowledge = data?.find((k) => k.id === d.id)\n                  if (selectedKnowledge?.id === d.id) {\n                    setSelectedKnowledge(null)\n                  } else {\n                    setSelectedKnowledge(knowledge)\n                  }\n                }\n              })) || [],\n            style: {\n              maxHeight: 500,\n              overflowY: \"scroll\"\n            },\n            className: \"no-scrollbar\",\n            activeKey: selectedKnowledge?.id\n          }}\n          placement={\"topLeft\"}\n          trigger={[\"click\"]}>\n          <Tooltip title={t(\"tooltip.knowledge\")}>\n            <button type=\"button\" className=\"dark:text-gray-300\">\n              <Blocks className=\"h-6 w-6\" />\n            </button>\n          </Tooltip>\n        </Dropdown>\n      )}\n    </>\n  )\n}\n"
  },
  {
    "path": "src/components/Option/Knowledge/SelectedKnowledge.tsx",
    "content": "import { Blocks, XIcon } from \"lucide-react\"\nimport { useMessageOption } from \"@/hooks/useMessageOption\"\nimport { Tooltip } from \"antd\"\n\nexport const SelectedKnowledge = () => {\n  const { selectedKnowledge: knowledge, setSelectedKnowledge } =\n    useMessageOption()\n\n  if (!knowledge) return <></>\n\n  return (\n    <div className=\"flex  flex-row items-center gap-3\">\n      <span className=\"text-lg font-thin text-zinc-300 dark:text-zinc-600\">\n        {\"/\"}\n      </span>\n      <div className=\"border flex  justify-between items-center rounded-full px-2 py-1 gap-2 bg-gray-100 dark:bg-[#2a2a2a] dark:border-[#404040]\">\n        <Tooltip title={knowledge.title}>\n          <div className=\"inline-flex items-center gap-2 max-w-[150px]\">\n            <Blocks className=\"h-5  w-5 text-gray-400 flex-shrink-0\" />\n            <span className=\"text-xs hidden lg:inline-block font-semibold dark:text-gray-100 truncate\">\n              {knowledge.title}\n            </span>\n          </div>\n        </Tooltip>\n        <div>\n          <button\n            onClick={() => setSelectedKnowledge(null)}\n            className=\"flex items-center justify-center   bg-white  dark:bg-[#1a1a1a] p-1 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 text-black dark:text-gray-100\">\n            <XIcon className=\"h-3 w-3\" />\n          </button>\n        </div>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Option/Knowledge/UpdateKnowledge.tsx",
    "content": "import { Source } from \"@/db/knowledge\"\nimport { addNewSources } from \"@/db/dexie/knowledge\"\nimport { defaultEmbeddingModelForRag } from \"@/services/ollama\"\nimport { convertTextToSource, convertToSource } from \"@/utils/to-source\"\nimport { useMutation } from \"@tanstack/react-query\"\nimport {\n  Modal,\n  Form,\n  Input,\n  Upload,\n  message,\n  UploadFile,\n  Tabs,\n  Select\n} from \"antd\"\nimport { InboxIcon } from \"lucide-react\"\nimport { useTranslation } from \"react-i18next\"\nimport PubSub from \"pubsub-js\"\nimport { KNOWLEDGE_QUEUE } from \"@/queue\"\nimport { useStorage } from \"@plasmohq/storage/hook\"\nimport { unsupportedTypes } from \"./utils/unsupported-types\"\nimport React from \"react\"\n\ntype Props = {\n  id: string\n  open: boolean\n  setOpen: React.Dispatch<React.SetStateAction<boolean>>\n}\n\nexport const UpdateKnowledge = ({ id, open, setOpen }: Props) => {\n  const { t } = useTranslation([\"knowledge\", \"common\"])\n  const [form] = Form.useForm()\n  const [totalFilePerKB] = useStorage(\"totalFilePerKB\", 5)\n  const [mode, setMode] = React.useState<\"upload\" | \"text\">(\"upload\")\n\n  const onUploadHandler = async (data: any) => {\n    const defaultEM = await defaultEmbeddingModelForRag()\n\n    if (!defaultEM) {\n      throw new Error(t(\"noEmbeddingModel\"))\n    }\n\n    const source: Source[] = []\n\n    const allowedTypes = [\n      \"application/pdf\",\n      \"text/csv\",\n      \"text/plain\",\n      \"text/markdown\",\n      \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\"\n    ]\n\n    if (mode === \"upload\") {\n      for (const file of data.file || []) {\n        let mime = file.type\n        if (!allowedTypes.includes(mime)) {\n          mime = \"text/plain\"\n        }\n        const _src = await convertToSource({\n          file,\n          mime,\n          sourceType: \"file_upload\"\n        })\n        source.push(_src)\n      }\n    } else {\n      const rawText: string = (data?.textContent || \"\").trim()\n      const textType: string = data?.textType || \"plain\"\n      if (!rawText) {\n        throw new Error(t(\"form.textInput.required\"))\n      }\n      if (rawText.length > 500000) {\n        throw new Error(t(\"form.textInput.tooLarge\"))\n      }\n\n      const asMarkdown = textType === \"markdown\"\n      const filename = `pasted_${new Date().getTime()}.txt`\n      const _src = await convertTextToSource({\n        text: rawText,\n        filename,\n        mime: asMarkdown ? \"text/markdown\" : \"text/plain\",\n        asMarkdown,\n        sourceType: \"text_input\"\n      })\n      source.push(_src)\n    }\n\n    await addNewSources(id, source)\n    return id\n  }\n\n  const { mutate: saveKnowledge, isPending: isSaving } = useMutation({\n    mutationFn: onUploadHandler,\n    onError: (error) => {\n      message.error(error.message)\n    },\n    onSuccess: async (id) => {\n      message.success(t(\"form.success\"))\n      PubSub.publish(KNOWLEDGE_QUEUE, id)\n      form.resetFields()\n      setOpen(false)\n    }\n  })\n\n  return (\n    <Modal\n      title={t(\"updateKnowledge\")}\n      open={open}\n      footer={null}\n      onCancel={() => setOpen(false)}>\n      <Tabs\n        activeKey={mode}\n        onChange={(key) => setMode(key as any)}\n        items={[\n          { key: \"upload\", label: t(\"form.tabs.upload\") },\n          { key: \"text\", label: t(\"form.tabs.text\") }\n        ]}\n      />\n      <Form onFinish={saveKnowledge} form={form} layout=\"vertical\">\n        {mode === \"upload\" ? (\n          <Form.Item\n            name=\"file\"\n            label={t(\"form.uploadFile.label\")}\n            rules={[\n              {\n                required: true,\n                message: t(\"form.uploadFile.required\")\n              }\n            ]}\n            getValueFromEvent={(e) => {\n              if (Array.isArray(e)) {\n                return e\n              }\n              return e?.fileList\n            }}>\n            <Upload.Dragger\n              multiple={true}\n              maxCount={totalFilePerKB}\n              beforeUpload={(file) => {\n                const allowedTypes = [\n                  \"application/pdf\",\n                  \"text/csv\",\n                  \"text/plain\",\n                  \"text/markdown\",\n                  \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\"\n                ]\n                  .map((type) => type.toLowerCase())\n                  .join(\", \")\n\n                if (unsupportedTypes.includes(file.type.toLowerCase())) {\n                  message.error(\n                    t(\"form.uploadFile.uploadError\", { allowedTypes })\n                  )\n                  return Upload.LIST_IGNORE\n                }\n\n                return false\n              }}>\n              <div className=\"p-3\">\n                <p className=\"flex justify-center ant-upload-drag-icon\">\n                  <InboxIcon className=\"w-10 h-10 text-gray-400\" />\n                </p>\n                <p className=\"ant-upload-text\">\n                  {t(\"form.uploadFile.uploadText\")}\n                </p>\n              </div>\n            </Upload.Dragger>\n          </Form.Item>\n        ) : (\n          <>\n            <Form.Item\n              name=\"textType\"\n              label={t(\"form.textInput.typeLabel\")}\n              initialValue=\"plain\">\n              <Select\n                options={[\n                  { value: \"plain\", label: t(\"form.textInput.type.plain\") },\n                  {\n                    value: \"markdown\",\n                    label: t(\"form.textInput.type.markdown\")\n                  },\n                  { value: \"code\", label: t(\"form.textInput.type.code\") }\n                ]}\n              />\n            </Form.Item>\n            <Form.Item\n              name=\"textContent\"\n              label={t(\"form.textInput.contentLabel\")}\n              rules={[\n                { required: true, message: t(\"form.textInput.required\") }\n              ]}>\n              <Input.TextArea\n                autoSize={{ minRows: 8, maxRows: 16 }}\n                placeholder={t(\"form.textInput.placeholder\")}\n              />\n            </Form.Item>\n          </>\n        )}\n\n        <Form.Item>\n          <button\n            type=\"submit\"\n            disabled={isSaving}\n            className=\"inline-flex items-center justify-center w-full px-2 py-2 font-medium leading-4 text-center text-white bg-black border border-transparent rounded-md shadow-sm text-md 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\">\n            {t(\"form.submit\")}\n          </button>\n        </Form.Item>\n      </Form>\n    </Modal>\n  )\n}\n"
  },
  {
    "path": "src/components/Option/Knowledge/index.tsx",
    "content": "import { useState } from \"react\"\nimport { useTranslation } from \"react-i18next\"\nimport { AddKnowledge } from \"./AddKnowledge\"\nimport { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\"\nimport {\n  deleteKnowledge,\n  deleteSource,\n  getAllKnowledge\n} from \"@/db/dexie/knowledge\"\nimport { Skeleton, Table, Tag, Tooltip, message, notification } from \"antd\"\nimport { FileUpIcon, Trash2, Settings } from \"lucide-react\"\nimport { useMessageOption } from \"@/hooks/useMessageOption\"\nimport { removeModelSuffix } from \"@/db/dexie/models\"\nimport { UpdateKnowledge } from \"./UpdateKnowledge\"\nimport { EditKnowledgeSettings } from \"./EditKnowledgeSettings\"\nimport { isFireFoxPrivateMode } from \"@/utils/is-private-mode\"\n\nexport const KnowledgeSettings = () => {\n  const { t } = useTranslation([\"knowledge\", \"common\"])\n  const [open, setOpen] = useState(false)\n  const queryClient = useQueryClient()\n  const { selectedKnowledge, setSelectedKnowledge } = useMessageOption()\n  const [openUpdate, setOpenUpdate] = useState(false)\n  const [updateKnowledgeId, setUpdateKnowledgeId] = useState(\"\")\n  const [openEditSettings, setOpenEditSettings] = useState(false)\n  const [editSettingsKnowledgeId, setEditSettingsKnowledgeId] = useState(\"\")\n\n  const { data, status } = useQuery({\n    queryKey: [\"fetchAllKnowledge\"],\n    queryFn: () => getAllKnowledge(),\n    refetchInterval: 1000\n  })\n\n  const { mutate: deleteKnowledgeMutation, isPending: isDeleting } =\n    useMutation({\n      mutationFn: deleteKnowledge,\n      onSuccess: () => {\n        queryClient.invalidateQueries({\n          queryKey: [\"fetchAllKnowledge\"]\n        })\n\n        message.success(t(\"deleteSuccess\"))\n      },\n      onError: (error) => {\n        message.error(error.message)\n      }\n    })\n\n  const statusColor = {\n    finished: \"green\",\n    processing: \"yellow\",\n    pending: \"gray\",\n    failed: \"red\"\n  }\n\n  return (\n    <div>\n      <div>\n        {/* Add new model button */}\n        <div className=\"mb-6\">\n          <div className=\"-ml-4 -mt-2 flex flex-wrap items-center justify-end sm:flex-nowrap\">\n            <div className=\"ml-4 mt-2 flex-shrink-0\">\n              <button\n                onClick={() => {\n                  if (isFireFoxPrivateMode) {\n                    notification.error({\n                      message: \"Page Assist can't save data\",\n                      description:\n                        \"Firefox Private Mode does not support saving data to IndexedDB. Please add knowledge base from a normal window.\"\n                    })\n                    return\n                  }\n                  setOpen(true)\n                }}\n                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\">\n                {t(\"addBtn\")}\n              </button>\n            </div>\n          </div>\n        </div>\n        {status === \"pending\" && <Skeleton paragraph={{ rows: 8 }} />}\n\n        {status === \"success\" && (\n          <Table\n            columns={[\n              {\n                title: t(\"columns.title\"),\n                dataIndex: \"title\",\n                key: \"title\"\n              },\n              {\n                title: t(\"columns.status\"),\n                dataIndex: \"status\",\n                key: \"status\",\n                render: (text: string) => (\n                  <Tag color={statusColor[text]}>{t(`status.${text}`)}</Tag>\n                )\n              },\n              {\n                title: t(\"columns.embeddings\"),\n                dataIndex: \"embedding_model\",\n                key: \"embedding_model\",\n                render: (text) => removeModelSuffix(text)\n              },\n              {\n                title: t(\"columns.createdAt\"),\n                dataIndex: \"createdAt\",\n                key: \"createdAt\",\n                render: (text: number) => new Date(text).toLocaleString()\n              },\n              {\n                title: t(\"columns.action\"),\n                key: \"action\",\n                render: (text: string, record: any) => (\n                  <div className=\"flex gap-4\">\n                    <Tooltip title={t(\"editSettings.tooltip\", )}>\n                      <button\n                        disabled={isDeleting}\n                        onClick={() => {\n                          setEditSettingsKnowledgeId(record.id)\n                          setOpenEditSettings(true)\n                        }}\n                        className=\"text-gray-800 dark:text-gray-500 disabled:opacity-50\">\n                        <Settings className=\"w-5 h-5\" />\n                      </button>\n                    </Tooltip>\n                    <Tooltip title={t(\"updateKnowledge\")}>\n                      <button\n                        disabled={isDeleting || record.status === \"processing\"}\n                        onClick={() => {\n                          setUpdateKnowledgeId(record.id)\n                          setOpenUpdate(true)\n                        }}\n                        className=\"text-gray-700 dark:text-gray-400 disabled:opacity-50\">\n                        <FileUpIcon className=\"w-5 h-5\" />\n                      </button>\n                    </Tooltip>\n                    <Tooltip title={t(\"common:delete\")}>\n                      <button\n                        disabled={isDeleting}\n                        onClick={() => {\n                          if (window.confirm(t(\"confirm.delete\"))) {\n                            deleteKnowledgeMutation(record.id)\n                            if (selectedKnowledge?.id === record?.id) {\n                              setSelectedKnowledge(null)\n                            }\n                          }\n                        }}\n                        className=\"text-red-500 dark:text-red-400\">\n                        <Trash2 className=\"w-5 h-5\" />\n                      </button>\n                    </Tooltip>\n                  </div>\n                )\n              }\n            ]}\n            expandable={{\n              expandedRowRender: (record) => (\n                <Table\n                  pagination={false}\n                  columns={[\n                    {\n                      title: t(\"expandedColumns.name\"),\n                      key: \"filename\",\n                      dataIndex: \"filename\"\n                    },\n                    {\n                      title: t(\"columns.action\"),\n                      key: \"action\",\n                      render: (text: string, r: any) => (\n                        <div className=\"flex gap-4\">\n                          <Tooltip title={t(\"common:delete\")}>\n                            <button\n                              disabled={\n                                isDeleting || record.status === \"processing\"\n                              }\n                              onClick={async () => {\n                                if (window.confirm(t(\"confirm.deleteSource\"))) {\n                                  await deleteSource(record.id, r.source_id)\n                                }\n                              }}\n                              className=\"text-red-500 dark:text-red-400 disabled:opacity-50\">\n                              <Trash2 className=\"w-5 h-5\" />\n                            </button>\n                          </Tooltip>\n                        </div>\n                      )\n                    }\n                  ]}\n                  dataSource={record.source}\n                  locale={{\n                    emptyText: t(\"common:noData\")\n                  }}\n                />\n              ),\n              defaultExpandAllRows: false\n            }}\n            bordered\n            dataSource={data}\n            rowKey={(record) => `${record.name}-${record.id}`}\n          />\n        )}\n      </div>\n\n      <AddKnowledge open={open} setOpen={setOpen} />\n      <UpdateKnowledge\n        id={updateKnowledgeId}\n        open={openUpdate}\n        setOpen={setOpenUpdate}\n      />\n      <EditKnowledgeSettings\n        id={editSettingsKnowledgeId}\n        open={openEditSettings}\n        setOpen={setOpenEditSettings}\n      />\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Option/Knowledge/utils/unsupported-types.ts",
    "content": "export const imageTypes = [\n\"image/png\",\n  \"image/jpeg\",\n  \"image/jpg\",\n  \"image/gif\",\n  \"image/webp\",\n  \"image/svg+xml\",\n  \"image/bmp\",\n  \"image/tiff\",\n  \"image/ico\",\n  \"image/heic\",\n  \"image/heif\",\n  \"image/avif\",\n]\n\n\nexport const otherUnsupportedTypes = [\n\n  // Binary files\n  \"application/x-msdownload\", // .exe\n  \"application/x-msi\", // .msi\n  \"application/x-dmp\", // .dmp\n  \"application/zip\", // .zip\n  \"application/x-zip-compressed\",\n  \"application/x-rar-compressed\", // .rar\n  \"application/x-7z-compressed\", // .7z\n  \"application/x-tar\", // .tar\n  \"application/x-gzip\", // .gz\n  \"application/java-archive\", // .jar\n  \"application/octet-stream\", // generic binary\n  \"application/x-apple-diskimage\", // .dmg\n  \"application/x-debian-package\", // .deb\n  \"application/x-rpm\", // .rpm\n  \"application/x-sh\", // .sh\n  \"application/x-ms-installer\", // Windows installer\n  \"application/vnd.microsoft.portable-executable\", // .exe\n  \"application/x-unix-archive\", // Unix archive\n  \"application/x-bzip2\", // .bz2\n  \"application/x-xz\", // .xz\n\n  // Audio files\n  \"audio/mpeg\", // .mp3\n  \"audio/wav\", // .wav\n  \"audio/ogg\", // .ogg\n  \"audio/flac\", // .flac\n  \"audio/aac\", // .aac\n\n  // Video files\n  \"video/mp4\",\n  \"video/mpeg\",\n  \"video/quicktime\",\n  \"video/x-msvideo\",\n  \"video/webm\",\n  \"video/x-matroska\",\n  \"video/x-ms-wmv\",\n  \"video/x-flv\",\n\n  // Font files\n  \"font/ttf\",\n  \"font/otf\",\n  \"font/woff\",\n  \"font/woff2\",\n  \"application/x-font-ttf\",\n  \"application/x-font-otf\",\n  \"application/font-woff\",\n  \"application/font-woff2\"\n]\n\n\n\n\nexport const unsupportedTypes = [\n  ...imageTypes,\n  ...otherUnsupportedTypes,\n]\n\n"
  },
  {
    "path": "src/components/Option/Models/AddCustomModelModal.tsx",
    "content": "import { createModel } from \"@/db/dexie/models\"\nimport { getAllOpenAIConfig, getOpenAIConfigById } from \"@/db/dexie/openai\"\nimport { getAllOpenAIModels } from \"@/libs/openai\"\nimport { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\"\nimport { Input, Modal, Form, Select, Radio, AutoComplete, Spin } from \"antd\"\nimport { Loader2 } from \"lucide-react\"\nimport { useTranslation } from \"react-i18next\"\nimport { useMemo } from \"react\"\nimport { ProviderIcons } from \"@/components/Common/ProviderIcon\"\n\ntype Props = {\n  open: boolean\n  setOpen: (open: boolean) => void\n}\n\nexport const AddCustomModelModal: React.FC<Props> = ({ open, setOpen }) => {\n  const { t } = useTranslation([\"openai\"])\n  const [form] = Form.useForm()\n  const queryClient = useQueryClient()\n  const selectedProviderId = Form.useWatch(\"provider_id\", form)\n  const searchValue = Form.useWatch(\"model_id\", form)\n\n  const { data, isPending } = useQuery({\n    queryKey: [\"fetchProviders\"],\n    queryFn: async () => {\n      const providers = await getAllOpenAIConfig()\n      return providers.filter((provider) => provider.provider !== \"lmstudio\")\n    }\n  })\n\n  const {\n    data: providerModels,\n    isFetching: isFetchingModels,\n    status: modelsStatus\n  } = useQuery({\n    queryKey: [\"providerModels\", selectedProviderId],\n    queryFn: async () => {\n      const config = await getOpenAIConfigById(selectedProviderId as string)\n      const models = await getAllOpenAIModels({\n        baseUrl: config.baseUrl,\n        apiKey: config.apiKey,\n        customHeaders: config.headers\n      })\n      return models\n    },\n    enabled: !!selectedProviderId\n  })\n\n  const autoCompleteOptions = useMemo(() => {\n    const list = providerModels ?? []\n    const cleaned = list.map((m) => ({\n      value: m.id,\n      label: `${m.name ?? m.id}`.replaceAll(/accounts\\/[^\\/]+\\/models\\//g, \"\")\n    }))\n    if (searchValue && !cleaned.some((o) => o.value === searchValue)) {\n      return [{ value: searchValue, label: searchValue }, ...cleaned]\n    }\n    return cleaned\n  }, [providerModels, searchValue])\n\n  const onFinish = async (values: {\n    model_id: string\n    model_type: \"chat\" | \"embedding\"\n    provider_id: string\n  }) => {\n    await createModel(\n      values.model_id,\n      values.model_id,\n      values.provider_id,\n      values.model_type\n    )\n\n    return true\n  }\n\n  const { mutate: createModelMutation, isPending: isSaving } = useMutation({\n    mutationFn: onFinish,\n    onSuccess: () => {\n      queryClient.invalidateQueries({\n        queryKey: [\"fetchCustomModels\"]\n      })\n      queryClient.invalidateQueries({\n        queryKey: [\"fetchModel\"]\n      })\n      form.resetFields()\n      setOpen(false)\n    }\n  })\n\n  return (\n    <Modal\n      footer={null}\n      open={open}\n      title={t(\"manageModels.modal.title\")}\n      onCancel={() => {\n        form.resetFields()\n        setOpen(false)\n      }}>\n      <Form form={form} onFinish={createModelMutation} layout=\"vertical\">\n        <Form.Item\n          name=\"model_id\"\n          label={t(\"manageModels.modal.form.name.label\")}\n          rules={[\n            {\n              required: true,\n              message: t(\"manageModels.modal.form.name.required\")\n            }\n          ]}>\n          <AutoComplete\n            options={autoCompleteOptions}\n            placeholder={t(\"manageModels.modal.form.name.placeholder\")}\n            size=\"large\"\n            disabled={!selectedProviderId}\n            filterOption={(inputValue, option) =>\n              (option?.label as string)\n                ?.toLowerCase()\n                .includes(inputValue.toLowerCase()) ||\n              (option?.value as string)\n                ?.toLowerCase()\n                .includes(inputValue.toLowerCase())\n            }\n            notFoundContent={\n              selectedProviderId ? (\n                modelsStatus === \"pending\" || isFetchingModels ? (\n                  <div className=\"flex items-center justify-center py-2\">\n                    <Spin size=\"small\" />\n                  </div>\n                ) : (\n                  t(\"noModelFound\")\n                )\n              ) : (\n                t(\"manageModels.modal.form.provider.placeholder\")\n              )\n            }\n          />\n        </Form.Item>\n\n        <Form.Item\n          name=\"provider_id\"\n          label={t(\"manageModels.modal.form.provider.label\")}\n          rules={[\n            {\n              required: true,\n              message: t(\"manageModels.modal.form.provider.required\")\n            }\n          ]}>\n          <Select\n            placeholder={t(\"manageModels.modal.form.provider.placeholder\")}\n            size=\"large\"\n            loading={isPending}\n            showSearch\n            filterOption={(input, option) => {\n              //@ts-ignore\n              return (\n                option?.label?.props[\"data-title\"]\n                  ?.toLowerCase()\n                  ?.indexOf(input.toLowerCase()) >= 0\n              )\n            }}\n            options={data?.map((e: any) => ({\n              value: e.id,\n              label: (\n                <span\n                  key={e.id}\n                  data-title={e.name}\n                  className=\"flex flex-row gap-3 items-center \">\n                  <ProviderIcons provider={e?.provider} className=\"size-4\" />\n                  <span className=\"line-clamp-2\">{e.name}</span>\n                </span>\n              )\n            }))}\n          />\n          {/* {data?.map((provider: any) => (\n              <Select.Option key={provider.id} value={provider.id}>\n                {provider.name}\n              </Select.Option>\n            ))} */}\n          {/* </Select> */}\n        </Form.Item>\n\n        <Form.Item\n          name=\"model_type\"\n          label={t(\"manageModels.modal.form.type.label\")}\n          initialValue=\"chat\"\n          rules={[\n            {\n              required: true,\n              message: t(\"manageModels.modal.form.type.required\")\n            }\n          ]}>\n          <Radio.Group>\n            <Radio value=\"chat\">{t(\"radio.chat\")}</Radio>\n            <Radio value=\"embedding\">{t(\"radio.embedding\")}</Radio>\n          </Radio.Group>\n        </Form.Item>\n\n        <Form.Item>\n          <button\n            type=\"submit\"\n            disabled={isSaving}\n            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 \">\n            {!isSaving ? (\n              t(\"common:save\")\n            ) : (\n              <Loader2 className=\"w-5 h-5  animate-spin\" />\n            )}\n          </button>\n        </Form.Item>\n      </Form>\n    </Modal>\n  )\n}\n"
  },
  {
    "path": "src/components/Option/Models/AddOllamaModelModal.tsx",
    "content": "import { useForm } from \"@mantine/form\"\nimport { useMutation, useQueryClient } from \"@tanstack/react-query\"\nimport { Input, Modal, notification, Button } from \"antd\"\nimport { Download, X } from \"lucide-react\"\nimport { useTranslation } from \"react-i18next\"\nimport { useState, useEffect } from \"react\"\nimport { getDownloadState } from \"~/utils/pull-ollama\"\nimport { browser } from \"wxt/browser\"\nimport { CancelPullingModel } from \"./CancelPullingModel\"\n\ntype Props = {\n  open: boolean\n  setOpen: (open: boolean) => void\n}\n\nexport const AddOllamaModelModal: React.FC<Props> = ({ open, setOpen }) => {\n  const { t } = useTranslation([\"settings\", \"common\", \"openai\"])\n  const queryClient = useQueryClient()\n  const [downloadState, setDownloadState] = useState({\n    modelName: null,\n    isDownloading: false\n  })\n\n  const form = useForm({\n    initialValues: {\n      model: \"\"\n    }\n  })\n\n  useEffect(() => {\n    const checkDownloadState = async () => {\n      const state = await getDownloadState()\n      if (\n        state &&\n        typeof state === \"object\" &&\n        \"modelName\" in state &&\n        \"isDownloading\" in state\n      ) {\n        setDownloadState(state)\n      } else {\n        setDownloadState({ modelName: null, isDownloading: false })\n      }\n    }\n\n    if (open) {\n      checkDownloadState()\n      const interval = setInterval(checkDownloadState, 1000)\n\n      return () => clearInterval(interval)\n    }\n  }, [open])\n\n  const pullModel = async (modelName: string) => {\n    modelName.replaceAll(\"ollama pull\", \"\").replaceAll(\"ollama run\", \"\").trim()\n    notification.info({\n      message: t(\"manageModels.notification.pullModel\"),\n      description: t(\"manageModels.notification.pullModelDescription\", {\n        modelName\n      })\n    })\n\n    setOpen(false)\n\n    form.reset()\n\n    browser.runtime.sendMessage({\n      type: \"pull_model\",\n      modelName\n    })\n\n    return true\n  }\n\n  const { mutate: pullOllamaModel } = useMutation({\n    mutationFn: pullModel\n  })\n\n  const cancelDownloadModel = () => {\n    browser.runtime.sendMessage({\n      type: \"cancel_download\"\n    })\n    notification.info({\n      message: t(\"manageModels.notification.cancellingDownload\"),\n      description: t(\"manageModels.notification.cancellingDownloadDescription\")\n    })\n  }\n\n  return (\n    <Modal\n      footer={null}\n      open={open}\n      title={t(\"manageModels.modal.title\")}\n      onCancel={() => setOpen(false)}>\n      {downloadState.isDownloading && (\n        <CancelPullingModel\n          cancelDownloadModel={cancelDownloadModel}\n          modelName={downloadState.modelName}\n        />\n      )}\n\n      <form onSubmit={form.onSubmit((values) => pullOllamaModel(values.model))}>\n        <Input\n          {...form.getInputProps(\"model\")}\n          required\n          placeholder={\"qwen2.5:3b\"}\n          size=\"large\"\n          disabled={downloadState.isDownloading}\n        />\n\n        <button\n          type=\"submit\"\n          disabled={downloadState.isDownloading}\n          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 \">\n          <Download className=\"w-5 h-5 mr-3\" />\n          {downloadState.isDownloading\n            ? t(\"common:downloading\")\n            : t(\"manageModels.modal.pull\")}\n        </button>\n      </form>\n    </Modal>\n  )\n}\n"
  },
  {
    "path": "src/components/Option/Models/AddUpdateModelSettings.tsx",
    "content": "import { getModelSettings, setModelSettings } from \"@/services/model-settings\"\nimport { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\"\nimport {\n  Collapse,\n  Form,\n  Input,\n  InputNumber,\n  Modal,\n  Skeleton,\n  Switch\n} from \"antd\"\nimport { Loader2 } from \"lucide-react\"\nimport React from \"react\"\nimport { useTranslation } from \"react-i18next\"\n\ntype Props = {\n  model_id: string\n  open: boolean\n  setOpen: (open: boolean) => void\n}\n\nexport const AddUpdateModelSettings: React.FC<Props> = ({\n  model_id,\n  open,\n  setOpen\n}) => {\n  const [form] = Form.useForm()\n  const { t } = useTranslation(\"common\")\n  const queryClient = useQueryClient()\n\n  const { status, isError } = useQuery({\n    queryKey: [\"fetchModelSettings\", model_id],\n    queryFn: async () => {\n      const data = await getModelSettings(model_id)\n      form.setFieldsValue({\n        ...data,\n        thinking: data?.thinking || false\n      })\n      return data\n    },\n    staleTime: 0\n  })\n\n  const { mutate, isPending } = useMutation({\n    mutationFn: setModelSettings,\n    onSuccess: async () => {\n      await queryClient.invalidateQueries({\n        queryKey: [\"fetchModel\"]\n      })\n      await queryClient.invalidateQueries({\n        queryKey: [\"fetchAllModels\"]\n      })\n      await queryClient.invalidateQueries({\n        queryKey: [\"fetchCustomModels\"]\n      })\n      form.resetFields()\n      setOpen(false)\n    }\n  })\n\n  return (\n    <Modal\n      title={t(\"modelSettings.label\")}\n      open={open}\n      onCancel={() => {\n        form.resetFields()\n        setOpen(false)\n      }}\n      footer={null}>\n      {status === \"pending\" && <Skeleton active />}\n      {status === \"success\" && (\n        <Form\n          onFinish={(values: any) => {\n            mutate({\n              model_id,\n              settings: values\n            })\n          }}\n          form={form}\n          layout=\"vertical\">\n          <Form.Item\n            name=\"keepAlive\"\n            help={t(\"modelSettings.form.keepAlive.help\")}\n            label={t(\"modelSettings.form.keepAlive.label\")}>\n            <Input\n              size=\"large\"\n              placeholder={t(\"modelSettings.form.keepAlive.placeholder\")}\n            />\n          </Form.Item>\n          <Form.Item\n            name=\"temperature\"\n            label={t(\"modelSettings.form.temperature.label\")}>\n            <InputNumber\n              size=\"large\"\n              style={{ width: \"100%\" }}\n              placeholder={t(\"modelSettings.form.temperature.placeholder\")}\n            />\n          </Form.Item>\n\n          <Form.Item name=\"numCtx\" label={t(\"modelSettings.form.numCtx.label\")}>\n            <InputNumber\n              style={{ width: \"100%\" }}\n              placeholder={t(\"modelSettings.form.numCtx.placeholder\")}\n              size=\"large\"\n            />\n          </Form.Item>\n          <Form.Item\n            name=\"numPredict\"\n            label={t(\"modelSettings.form.numPredict.label\")}>\n            <InputNumber\n              style={{ width: \"100%\" }}\n              placeholder={t(\"modelSettings.form.numPredict.placeholder\")}\n            />\n          </Form.Item>\n\n          <Form.Item\n            name=\"thinking\"\n            label={t(\"modelSettings.form.thinking.label\")}>\n            <Switch />\n          </Form.Item>\n\n          <Collapse\n            ghost\n            className=\"border-none bg-transparent\"\n            items={[\n              {\n                key: \"1\",\n                label: t(\"modelSettings.advanced\"),\n                children: (\n                  <React.Fragment>\n                    <Form.Item\n                      name=\"topK\"\n                      label={t(\"modelSettings.form.topK.label\")}>\n                      <InputNumber\n                        style={{ width: \"100%\" }}\n                        placeholder={t(\"modelSettings.form.topK.placeholder\")}\n                        size=\"large\"\n                      />\n                    </Form.Item>\n\n                    <Form.Item\n                      name=\"topP\"\n                      label={t(\"modelSettings.form.topP.label\")}>\n                      <InputNumber\n                        style={{ width: \"100%\" }}\n                        size=\"large\"\n                        placeholder={t(\"modelSettings.form.topP.placeholder\")}\n                      />\n                    </Form.Item>\n                    <Form.Item\n                      name=\"numGpu\"\n                      label={t(\"modelSettings.form.numGpu.label\")}>\n                      <InputNumber\n                        style={{ width: \"100%\" }}\n                        size=\"large\"\n                        placeholder={t(\"modelSettings.form.numGpu.placeholder\")}\n                      />\n                    </Form.Item>\n                    <Form.Item\n                      name=\"minP\"\n                      label={t(\"modelSettings.form.minP.label\")}>\n                      <InputNumber\n                        style={{ width: \"100%\" }}\n                        placeholder={t(\"modelSettings.form.minP.placeholder\")}\n                      />\n                    </Form.Item>\n                    <Form.Item\n                      name=\"repeatPenalty\"\n                      label={t(\"modelSettings.form.repeatPenalty.label\")}>\n                      <InputNumber\n                        style={{ width: \"100%\" }}\n                        placeholder={t(\n                          \"modelSettings.form.repeatPenalty.placeholder\"\n                        )}\n                      />\n                    </Form.Item>\n                    <Form.Item\n                      name=\"repeatLastN\"\n                      label={t(\"modelSettings.form.repeatLastN.label\")}>\n                      <InputNumber\n                        style={{ width: \"100%\" }}\n                        placeholder={t(\n                          \"modelSettings.form.repeatLastN.placeholder\"\n                        )}\n                      />\n                    </Form.Item>\n                    <Form.Item\n                      name=\"tfsZ\"\n                      label={t(\"modelSettings.form.tfsZ.label\")}>\n                      <InputNumber\n                        style={{ width: \"100%\" }}\n                        placeholder={t(\"modelSettings.form.tfsZ.placeholder\")}\n                      />\n                    </Form.Item>\n                    <Form.Item\n                      name=\"numKeep\"\n                      label={t(\"modelSettings.form.numKeep.label\")}>\n                      <InputNumber\n                        style={{ width: \"100%\" }}\n                        placeholder={t(\n                          \"modelSettings.form.numKeep.placeholder\"\n                        )}\n                      />\n                    </Form.Item>\n                    <Form.Item\n                      name=\"numThread\"\n                      label={t(\"modelSettings.form.numThread.label\")}>\n                      <InputNumber\n                        style={{ width: \"100%\" }}\n                        placeholder={t(\n                          \"modelSettings.form.numThread.placeholder\"\n                        )}\n                      />\n                    </Form.Item>\n                    <Form.Item\n                      name=\"useMMap\"\n                      label={t(\"modelSettings.form.useMMap.label\")}>\n                      <Switch />\n                    </Form.Item>\n                    <Form.Item\n                      name=\"useMlock\"\n                      label={t(\"modelSettings.form.useMlock.label\")}>\n                      <Switch />\n                    </Form.Item>\n                  </React.Fragment>\n                )\n              }\n            ]}\n          />\n\n          <button\n            type=\"submit\"\n            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 \">\n            {isPending ? (\n              <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n            ) : (\n              t(\"save\")\n            )}\n          </button>\n        </Form>\n      )}\n    </Modal>\n  )\n}\n"
  },
  {
    "path": "src/components/Option/Models/AddUpdateOAIModelSettings.tsx",
    "content": "import { SaveButton } from \"@/components/Common/SaveButton\"\nimport { getModelSettings, setModelSettings } from \"@/services/model-settings\"\nimport { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\"\nimport {\n  Collapse,\n  Form,\n  Input,\n  InputNumber,\n  Modal,\n  Skeleton,\n  Switch\n} from \"antd\"\nimport { Loader2 } from \"lucide-react\"\nimport React from \"react\"\nimport { useTranslation } from \"react-i18next\"\n\ntype Props = {\n  model_id: string\n  open: boolean\n  setOpen: (open: boolean) => void\n}\n\nexport const AddUpdateOAIModelSettings: React.FC<Props> = ({\n  model_id,\n  open,\n  setOpen\n}) => {\n  const [form] = Form.useForm()\n  const { t } = useTranslation(\"common\")\n  const queryClient = useQueryClient()\n\n  const { status, isError } = useQuery({\n    queryKey: [\"fetchModelSettings\", model_id],\n    queryFn: async () => {\n      const data = await getModelSettings(model_id)\n      form.setFieldsValue(data)\n      return data\n    },\n    staleTime: 0\n  })\n\n  const { mutate, isPending } = useMutation({\n    mutationFn: setModelSettings,\n    onSuccess: async () => {\n      await queryClient.invalidateQueries({\n        queryKey: [\"fetchModel\"]\n      })\n      await queryClient.invalidateQueries({\n        queryKey: [\"fetchAllModels\"]\n      })\n      await queryClient.invalidateQueries({\n        queryKey: [\"fetchCustomModels\"]\n      })\n      form.resetFields()\n      setOpen(false)\n    }\n  })\n\n  return (\n    <Modal\n      title={t(\"modelSettings.label\")}\n      open={open}\n      onCancel={() => {\n        form.resetFields()\n        setOpen(false)\n      }}\n      footer={null}>\n      {status === \"pending\" && <Skeleton active />}\n      {status === \"success\" && (\n        <Form\n          onFinish={(values: any) => {\n            mutate({\n              model_id,\n              settings: values\n            })\n          }}\n          form={form}\n          layout=\"vertical\">\n          <Form.Item\n            name=\"temperature\"\n            label={t(\"modelSettings.form.temperature.label\")}>\n            <InputNumber\n              size=\"large\"\n              style={{ width: \"100%\" }}\n              placeholder={t(\"modelSettings.form.temperature.placeholder\")}\n            />\n          </Form.Item>\n          <Form.Item\n            name=\"numPredict\"\n            label={t(\"modelSettings.form.numPredict.label\")}>\n            <InputNumber\n              style={{ width: \"100%\" }}\n              placeholder={t(\"modelSettings.form.numPredict.placeholder\")}\n            />\n          </Form.Item>\n          <Form.Item name=\"topP\" label={t(\"modelSettings.form.topP.label\")}>\n            <InputNumber\n              style={{ width: \"100%\" }}\n              size=\"large\"\n              placeholder={t(\"modelSettings.form.topP.placeholder\")}\n            />\n          </Form.Item>\n          <Form.Item\n            name=\"reasoningEffort\"\n            label={t(\"modelSettings.form.reasoningEffort.label\")}>\n            <Input\n              style={{ width: \"100%\" }}\n              placeholder={t(\"modelSettings.form.reasoningEffort.placeholder\")}\n            />\n          </Form.Item>\n          <Form.Item name=\"topK\" label={t(\"modelSettings.form.topK.label\")}>\n            <InputNumber\n              style={{ width: \"100%\" }}\n              placeholder={t(\"modelSettings.form.topK.placeholder\")}\n            />\n          </Form.Item>\n          <Form.Item name=\"minP\" label={t(\"modelSettings.form.minP.label\")}>\n            <InputNumber\n              style={{ width: \"100%\" }}\n              placeholder={t(\"modelSettings.form.minP.placeholder\")}\n            />\n          </Form.Item>\n          <Form.Item\n            name=\"numCtx\"\n            label={`${t(\"modelSettings.form.numCtx.label\")} (Ollama)`}>\n            <InputNumber\n              style={{ width: \"100%\" }}\n              placeholder={t(\"modelSettings.form.numCtx.placeholder\")}\n            />\n          </Form.Item>\n\n          <button\n            type=\"submit\"\n            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 \">\n            {isPending ? (\n              <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n            ) : (\n              t(\"save\")\n            )}\n          </button>\n        </Form>\n      )}\n    </Modal>\n  )\n}\n"
  },
  {
    "path": "src/components/Option/Models/CancelPullingModel.tsx",
    "content": "import { Loader2 } from \"lucide-react\"\nimport { useTranslation } from \"react-i18next\"\n\ntype Props = {\n  modelName: string\n  cancelDownloadModel: () => void\n}\n\nexport const CancelPullingModel = ({\n  modelName,\n  cancelDownloadModel\n}: Props) => {\n  const { t } = useTranslation(\"common\")\n  return (\n    <div className=\"mb-4 p-3  bg-neutral-50  dark:bg-[#2a2a2a] border border-neutral-200 dark:border-neutral-700 rounded-lg\">\n      <div className=\"flex items-center justify-between\">\n        <div className=\"flex items-center gap-3\">\n          <Loader2 className=\"w-5 h-5 animate-spin text-gray-700 dark:text-gray-300\" />\n          <span className=\"text-sm font-medium text-gray-700 dark:text-gray-300\">\n            {`${t(\"downloading\")} ${modelName}...`}\n          </span>\n        </div>\n        <button\n          className=\"bg-red-600 text-white rounded-md px-3 py-1.5 text-sm font-medium hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2\"\n          onClick={() => {\n            if (confirm(t(\"cancelPullingModel.confirm\"))) {\n              cancelDownloadModel()\n            }\n          }}>\n          {t(\"common:cancel\")}\n        </button>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Option/Models/CustomModelsTable.tsx",
    "content": "import { getAllCustomModels, deleteModel } from \"@/db/dexie/models\"\nimport { useStorage } from \"@plasmohq/storage/hook\"\nimport { useQuery, useQueryClient, useMutation } from \"@tanstack/react-query\"\nimport { Avatar, Skeleton, Table, Tag, Tooltip } from \"antd\"\nimport { Pencil, Settings, Trash2 } from \"lucide-react\"\nimport { useState } from \"react\"\nimport { useTranslation } from \"react-i18next\"\nimport { ModelNickModelNicknameModal } from \"./ModelNicknameModal\"\nimport { AddUpdateOAIModelSettings } from \"./AddUpdateOAIModelSettings\"\nimport { isFireFoxPrivateMode } from \"@/utils/is-private-mode\"\n\nexport const CustomModelsTable = () => {\n  const [selectedModel, setSelectedModel] = useStorage(\"selectedModel\")\n  const [openNicknameModal, setOpenNicknameModal] = useState(false)\n  const [model, setModel] = useState<{\n    model_id: string\n    model_name?: string\n    model_avatar?: string\n  }>({\n    model_id: \"\",\n    model_name: \"\",\n    model_avatar: \"\"\n  })\n  const { t } = useTranslation([\"openai\", \"common\"])\n  const [openSettingsModal, setOpenSettingsModal] = useState(false)\n  const queryClient = useQueryClient()\n\n  const { data, status } = useQuery({\n    queryKey: [\"fetchCustomModels\"],\n    queryFn: () => getAllCustomModels()\n  })\n\n  const { mutate: deleteCustomModel } = useMutation({\n    mutationFn: deleteModel,\n    onSuccess: () => {\n      queryClient.invalidateQueries({\n        queryKey: [\"fetchCustomModels\"]\n      })\n    }\n  })\n\n  return (\n    <div>\n      <div>\n        {status === \"pending\" && <Skeleton paragraph={{ rows: 8 }} />}\n\n        {status === \"success\" && (\n          <div className=\"overflow-x-auto\">\n            <Table\n              columns={[\n                {\n                  title: t(\"manageModels.columns.nickname\"),\n                  dataIndex: \"nickname\",\n                  key: \"nickname\",\n                  render: (text: string, record: any) => (\n                    <div className=\"flex items-center gap-2\">\n                      {record.avatar && (\n                        <Avatar\n                          size=\"small\"\n                          src={record.avatar}\n                          alt={record.nickname}\n                        />\n                      )}\n                      <span>{text}</span>\n                      <button\n                        onClick={() => {\n                          setModel({\n                            model_id: record.id,\n                            model_name: record.nickname,\n                            model_avatar: record.avatar\n                          })\n                          setOpenNicknameModal(true)\n                        }}>\n                        <Pencil className=\"size-3\" />\n                      </button>\n                    </div>\n                  )\n                },\n                {\n                  title: t(\"manageModels.columns.model_id\"),\n                  dataIndex: \"model_id\",\n                  key: \"model_id\"\n                },\n                {\n                  title: t(\"manageModels.columns.model_type\"),\n                  dataIndex: \"model_type\",\n                  render: (txt) => (\n                    <Tag color={txt === \"chat\" ? \"green\" : \"blue\"}>\n                      {t(`radio.${txt}`)}\n                    </Tag>\n                  )\n                },\n                {\n                  title: t(\"manageModels.columns.provider\"),\n                  dataIndex: \"provider\",\n                  render: (_, record) => record.provider.name\n                },\n                {\n                  title: t(\"manageModels.columns.actions\"),\n                  render: (_, record) => (\n                    <div className=\"flex gap-2\">\n                      <Tooltip title={t(\"common:modelSettings.label\")}>\n                        <button\n                          onClick={() => {\n                            setModel({\n                              model_id: record.id\n                            })\n                            setOpenSettingsModal(true)\n                          }}\n                          disabled={isFireFoxPrivateMode}\n                          className=\"text-gray-700 dark:text-gray-400 disabled:opacity-50\">\n                          <Settings className=\"size-4\" />\n                        </button>\n                      </Tooltip>\n                      <Tooltip title={t(\"manageModels.tooltip.delete\")}>\n                        <button\n                          onClick={() => {\n                            if (\n                              window.confirm(t(\"manageModels.confirm.delete\"))\n                            ) {\n                              deleteCustomModel(record.id)\n                              if (\n                                selectedModel &&\n                                selectedModel === record.id\n                              ) {\n                                setSelectedModel(null)\n                              }\n                            }\n                          }}\n                          disabled={isFireFoxPrivateMode}\n                          className=\"text-red-500 dark:text-red-400 disabled:opacity-50\">\n                          <Trash2 className=\"w-5 h-5\" />\n                        </button>\n                      </Tooltip>\n                    </div>\n                  )\n                }\n              ]}\n              bordered\n              dataSource={data}\n            />\n          </div>\n        )}\n      </div>\n      <ModelNickModelNicknameModal\n        model_id={model.model_id}\n        open={openNicknameModal}\n        setOpen={setOpenNicknameModal}\n        model_name={model.model_name}\n        model_avatar={model.model_avatar}\n      />\n\n      <AddUpdateOAIModelSettings\n        model_id={model.model_id}\n        open={openSettingsModal}\n        setOpen={setOpenSettingsModal}\n      />\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Option/Models/ModelNicknameModal.tsx",
    "content": "import { SaveButton } from \"@/components/Common/SaveButton\"\nimport { saveModelNickname } from \"@/db/dexie/nickname\"\nimport { useMutation, useQueryClient } from \"@tanstack/react-query\"\nimport { Form, Input, Modal } from \"antd\"\nimport React from \"react\"\nimport { useTranslation } from \"react-i18next\"\n\ntype Props = {\n  model_id: string\n  open: boolean\n  setOpen: (open: boolean) => void\n  model_name?: string\n  model_avatar?: string\n}\n\nexport const ModelNickModelNicknameModal: React.FC<Props> = ({\n  model_id,\n  open,\n  setOpen,\n  model_avatar,\n  model_name\n}) => {\n  const [form] = Form.useForm()\n  const { t } = useTranslation(\"openai\")\n  const queryClient = useQueryClient()\n\n  React.useEffect(() => {\n    form.setFieldsValue({\n      model_name,\n      model_avatar\n    })\n  }, [model_id, model_avatar, model_name])\n\n  const { mutate } = useMutation({\n    mutationFn: saveModelNickname,\n    onSuccess: async () => {\n      await queryClient.invalidateQueries({\n        queryKey: [\"fetchModel\"]\n      })\n      await queryClient.invalidateQueries({\n        queryKey: [\"fetchAllModels\"]\n      })\n      await queryClient.invalidateQueries({\n        queryKey: [\"fetchCustomModels\"]\n      })\n      form.resetFields()\n\n      setOpen(false)\n    }\n  })\n\n  return (\n    <Modal\n      title={t(\"nicknameModal.title\")}\n      open={open}\n      onCancel={() => {\n        form.resetFields()\n        setOpen(false)\n      }}\n      footer={null}>\n      <Form\n        form={form}\n        layout=\"vertical\"\n        initialValues={{\n          model_name,\n          model_avatar\n        }}\n        onFinish={async (e) => {\n          await mutate({\n            model_id,\n            ...e\n          })\n        }}>\n        <Form.Item\n          name=\"model_name\"\n          label={t(\"nicknameModal.form.modelName.label\")}\n          rules={[\n            {\n              required: true,\n              message: t(\"nicknameModal.form.modelName.required\")\n            }\n          ]}>\n          <Input placeholder=\"DeepSeek R1\" />\n        </Form.Item>\n        <Form.Item\n          name=\"model_avatar\"\n          label={t(\"nicknameModal.form.modelAvatar.label\")}\n          help={t(\"nicknameModal.form.modelAvatar.help\")}>\n          <Input placeholder={\"https://example.com/model.png\"} />\n        </Form.Item>\n        <SaveButton btnType=\"submit\" className=\"w-full flex justify-center\" />\n      </Form>\n    </Modal>\n  )\n}\n"
  },
  {
    "path": "src/components/Option/Models/OllamaModelsTable.tsx",
    "content": "import { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\"\nimport { Skeleton, Table, Tag, Tooltip, notification, Avatar, Switch } from \"antd\"\nimport { bytePerSecondFormatter } from \"~/libs/byte-formater\"\nimport { deleteModel, getAllModels } from \"~/services/ollama\"\nimport dayjs from \"dayjs\"\nimport relativeTime from \"dayjs/plugin/relativeTime\"\nimport { useForm } from \"@mantine/form\"\nimport {\n  ExternalLink,\n  Pencil,\n  RotateCcw,\n  Settings,\n  Trash2,\n  X\n} from \"lucide-react\"\nimport { useTranslation } from \"react-i18next\"\nimport { useStorage } from \"@plasmohq/storage/hook\"\nimport { ModelNickModelNicknameModal } from \"./ModelNicknameModal\"\nimport { useState, useEffect } from \"react\"\nimport { AddUpdateModelSettings } from \"./AddUpdateModelSettings\"\nimport { getDownloadState } from \"~/utils/pull-ollama\"\nimport { browser } from \"wxt/browser\"\nimport { CancelPullingModel } from \"./CancelPullingModel\"\nimport { setModelState } from \"@/db/dexie/modelState\"\n\ndayjs.extend(relativeTime)\n\nexport const OllamaModelsTable = () => {\n  const queryClient = useQueryClient()\n  const { t } = useTranslation([\"settings\", \"common\", \"openai\"])\n  const [selectedModel, setSelectedModel] = useStorage(\"selectedModel\")\n  const [openNicknameModal, setOpenNicknameModal] = useState(false)\n  const [openSettingsModal, setOpenSettingsModal] = useState(false)\n  const [downloadState, setDownloadState] = useState({\n    modelName: null,\n    isDownloading: false\n  })\n  const [model, setModel] = useState<{\n    model_id: string\n    model_name?: string\n    model_avatar?: string\n  }>({\n    model_id: \"\",\n    model_name: \"\",\n    model_avatar: \"\"\n  })\n\n  const form = useForm({\n    initialValues: {\n      model: \"\"\n    }\n  })\n\n  useEffect(() => {\n    const checkDownloadState = async () => {\n      const state = await getDownloadState()\n      if (\n        state &&\n        typeof state === \"object\" &&\n        \"modelName\" in state &&\n        \"isDownloading\" in state\n      ) {\n        setDownloadState(state)\n      } else {\n        setDownloadState({ modelName: null, isDownloading: false })\n      }\n    }\n\n    checkDownloadState()\n    const interval = setInterval(checkDownloadState, 1000)\n\n    return () => clearInterval(interval)\n  }, [])\n\n  const { data, status } = useQuery({\n    queryKey: [\"fetchAllModels\"],\n    queryFn: async () => await getAllModels({ returnEmpty: true, includeDisabled: true })\n  })\n\n  const { mutate: deleteOllamaModel } = useMutation({\n    mutationFn: deleteModel,\n    onSuccess: () => {\n      queryClient.invalidateQueries({\n        queryKey: [\"fetchAllModels\"]\n      })\n      notification.success({\n        message: t(\"manageModels.notification.success\"),\n        description: t(\"manageModels.notification.successDeleteDescription\")\n      })\n    },\n    onError: (error) => {\n      notification.error({\n        message: \"Error\",\n        description: error?.message || t(\"manageModels.notification.someError\")\n      })\n    }\n  })\n\n  const pullModel = async (modelName: string) => {\n    notification.info({\n      message: t(\"manageModels.notification.pullModel\"),\n      description: t(\"manageModels.notification.pullModelDescription\", {\n        modelName\n      })\n    })\n\n    form.reset()\n\n    browser.runtime.sendMessage({\n      type: \"pull_model\",\n      modelName\n    })\n\n    return true\n  }\n\n  const { mutate: pullOllamaModel } = useMutation({\n    mutationFn: pullModel\n  })\n\n  const { mutate: toggleModelEnabled } = useMutation({\n    mutationFn: async ({ modelId, isEnabled }: { modelId: string; isEnabled: boolean }) => {\n      await setModelState(modelId, isEnabled)\n    },\n    onSuccess: () => {\n      queryClient.invalidateQueries({\n        queryKey: [\"fetchAllModels\"]\n      })\n      queryClient.invalidateQueries({\n        queryKey: [\"fetchModel\"]\n      })\n    },\n    onError: (error) => {\n      notification.error({\n        message: \"Error\",\n        description: error?.message || \"Failed to update model state\"\n      })\n    }\n  })\n\n  const cancelDownloadModel = () => {\n    browser.runtime.sendMessage({\n      type: \"cancel_download\"\n    })\n    notification.info({\n      message: t(\"manageModels.notification.cancellingDownload\"),\n      description: t(\"manageModels.notification.cancellingDownloadDescription\")\n    })\n  }\n\n  return (\n    <div>\n      {downloadState.isDownloading && (\n        <CancelPullingModel\n          cancelDownloadModel={cancelDownloadModel}\n          modelName={downloadState.modelName}\n        />\n      )}\n\n      <div>\n        {status === \"pending\" && <Skeleton paragraph={{ rows: 8 }} />}\n\n        {status === \"success\" && (\n          <div className=\"overflow-x-auto\">\n            <Table\n              columns={[\n                {\n                  title: t(\"openai:manageModels.columns.nickname\"),\n                  dataIndex: \"nickname\",\n                  key: \"nickname\",\n                  render: (text: string, record: any) => (\n                    <div className=\"flex items-center gap-2\">\n                      {record.avatar && (\n                        <Avatar\n                          size=\"small\"\n                          src={record.avatar}\n                          alt={record.nickname}\n                        />\n                      )}\n                      <span>{text}</span>\n                      <button\n                        onClick={() => {\n                          setModel({\n                            model_id: record.model,\n                            model_name: record.nickname,\n                            model_avatar: record.avatar\n                          })\n                          setOpenNicknameModal(true)\n                        }}>\n                        <Pencil className=\"size-3\" />\n                      </button>\n                    </div>\n                  )\n                },\n                {\n                  title: \"Model ID\",\n                  dataIndex: \"name\",\n                  key: \"name\"\n                },\n                {\n                  title: t(\"manageModels.columns.digest\"),\n                  dataIndex: \"digest\",\n                  key: \"digest\",\n                  render: (text: string) => (\n                    <Tooltip title={text}>\n                      <Tag\n                        className=\"cursor-pointer\"\n                        color=\"blue\">{`${text?.slice(0, 5)}...${text?.slice(-4)}`}</Tag>\n                    </Tooltip>\n                  )\n                },\n                {\n                  title: t(\"manageModels.columns.modifiedAt\"),\n                  dataIndex: \"modified_at\",\n                  key: \"modified_at\",\n                  render: (text: string) => dayjs(text).fromNow(true)\n                },\n                {\n                  title: t(\"manageModels.columns.size\"),\n                  dataIndex: \"size\",\n                  key: \"size\",\n                  render: (text: number) => bytePerSecondFormatter(text)\n                },\n                {\n                  title: t(\"manageModels.columns.actions\"),\n                  render: (_, record) => (\n                    <div className=\"flex gap-2 items-center\">\n                      <Tooltip title={record.is_enabled ? \"Disable model\" : \"Enable model\"}>\n                        <Switch\n                          checked={record.is_enabled}\n                          size=\"small\"\n                          onChange={(checked) => {\n                            toggleModelEnabled({\n                              modelId: record.name,\n                              isEnabled: checked\n                            })\n                          }}\n                        />\n                      </Tooltip>\n                      <Tooltip title={t(\"common:modelSettings.label\")}>\n                        <button\n                          onClick={() => {\n                            setModel({\n                              model_id: record.model\n                            })\n                            setOpenSettingsModal(true)\n                          }}\n                          className=\"text-gray-700 dark:text-gray-400\">\n                          <Settings className=\"size-4\" />\n                        </button>\n                      </Tooltip>\n                      <Tooltip title={t(\"manageModels.tooltip.delete\")}>\n                        <button\n                          onClick={() => {\n                            if (\n                              window.confirm(t(\"manageModels.confirm.delete\"))\n                            ) {\n                              deleteOllamaModel(record.model)\n                              if (\n                                selectedModel &&\n                                selectedModel === record.model\n                              ) {\n                                setSelectedModel(null)\n                              }\n                            }\n                          }}\n                          className=\"text-red-500 dark:text-red-400\">\n                          <Trash2 className=\"size-4\" />\n                        </button>\n                      </Tooltip>\n\n                      <Tooltip title={t(\"manageModels.tooltip.repull\")}>\n                        <button\n                          onClick={() => {\n                            if (\n                              window.confirm(t(\"manageModels.confirm.repull\"))\n                            ) {\n                              pullOllamaModel(record.model)\n                            }\n                          }}\n                          className=\"text-gray-700 dark:text-gray-400\">\n                          <RotateCcw className=\"size-4\" />\n                        </button>\n                      </Tooltip>\n                    </div>\n                  )\n                }\n              ]}\n              expandable={{\n                expandedRowRender: (record) => (\n                  <Table\n                    pagination={false}\n                    columns={[\n                      {\n                        title: t(\"manageModels.expandedColumns.parentModel\"),\n                        key: \"parent_model\",\n                        dataIndex: \"parent_model\"\n                      },\n                      {\n                        title: t(\"manageModels.expandedColumns.format\"),\n                        key: \"format\",\n                        dataIndex: \"format\"\n                      },\n                      {\n                        title: t(\"manageModels.expandedColumns.family\"),\n                        key: \"family\",\n                        dataIndex: \"family\"\n                      },\n                      {\n                        title: t(\"manageModels.expandedColumns.parameterSize\"),\n                        key: \"parameter_size\",\n                        dataIndex: \"parameter_size\"\n                      },\n                      {\n                        title: t(\n                          \"manageModels.expandedColumns.quantizationLevel\"\n                        ),\n                        key: \"quantization_level\",\n                        dataIndex: \"quantization_level\"\n                      }\n                    ]}\n                    dataSource={[record.details]}\n                    locale={{\n                      emptyText: t(\"common:noData\")\n                    }}\n                  />\n                ),\n                defaultExpandAllRows: false\n              }}\n              bordered\n              dataSource={data}\n              rowKey={(record) => `${record.model}-${record.digest}`}\n              footer={() => (\n                <div>\n                  <a\n                    href=\"https://ollama.com/search\"\n                    target=\"_blank\"\n                    rel=\"noopener noreferrer\"\n                    className=\"inline-flex items-center gap-2 text-[12px] text-neutral-50 dark:text-neutral-400 hover:text-neutral-300 dark:hover:text-neutral-200 \"\n                    style={{ textDecoration: \"none\" }}>\n                    {t(\n                      \"manageModels.getMoreModels\",\n                      \"Get more models from Ollama\"\n                    )}\n\n                    <ExternalLink className=\"size-[12px] \" />\n                  </a>\n                </div>\n              )}\n            />\n          </div>\n        )}\n      </div>\n      <ModelNickModelNicknameModal\n        model_id={model.model_id}\n        open={openNicknameModal}\n        setOpen={setOpenNicknameModal}\n        model_name={model.model_name}\n        model_avatar={model.model_avatar}\n      />\n\n      <AddUpdateModelSettings\n        model_id={model.model_id}\n        open={openSettingsModal}\n        setOpen={setOpenSettingsModal}\n      />\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Option/Models/index.tsx",
    "content": "import { notification, Segmented } from \"antd\"\nimport dayjs from \"dayjs\"\nimport relativeTime from \"dayjs/plugin/relativeTime\"\nimport { useState } from \"react\"\nimport { useTranslation } from \"react-i18next\"\nimport { OllamaModelsTable } from \"./OllamaModelsTable\"\nimport { CustomModelsTable } from \"./CustomModelsTable\"\nimport { AddOllamaModelModal } from \"./AddOllamaModelModal\"\nimport { AddCustomModelModal } from \"./AddCustomModelModal\"\nimport { isFireFoxPrivateMode } from \"@/utils/is-private-mode\"\n\ndayjs.extend(relativeTime)\n\nexport const ModelsBody = () => {\n  const [open, setOpen] = useState(false)\n  const [openAddModelModal, setOpenAddModelModal] = useState(false)\n  const [segmented, setSegmented] = useState<string>(\"ollama\")\n\n  const { t } = useTranslation([\"settings\", \"common\", \"openai\"])\n\n  return (\n    <div>\n      <div>\n        {/* Add new model button */}\n        <div className=\"mb-6\">\n          <div className=\"-ml-4 -mt-2 flex flex-wrap items-center justify-end sm:flex-nowrap\">\n            <div className=\"ml-4 mt-2 flex-shrink-0\">\n              <button\n                onClick={() => {\n                  if (segmented === \"ollama\") {\n                    setOpen(true)\n                  } else {\n                    if (isFireFoxPrivateMode) {\n                      notification.error({\n                        message: \"Page Assist can't save data\",\n                        description:\n                          \"Firefox Private Mode does not support saving data to IndexedDB. Please add custom model from a normal window.\"\n                      })\n                      return\n                    }\n                    setOpenAddModelModal(true)\n                  }\n                }}\n                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\">\n                {t(\"manageModels.addBtn\")}\n              </button>\n            </div>\n          </div>\n          <div className=\"flex items-center justify-end mt-3\">\n            <Segmented\n              options={[\n                {\n                  label: t(\"common:segmented.ollama\"),\n                  value: \"ollama\"\n                },\n                {\n                  label: t(\"common:segmented.custom\"),\n                  value: \"custom\"\n                }\n              ]}\n              onChange={(value) => {\n                setSegmented(value)\n              }}\n            />\n          </div>\n        </div>\n\n        {segmented === \"ollama\" ? <OllamaModelsTable /> : <CustomModelsTable />}\n      </div>\n\n      <AddOllamaModelModal open={open} setOpen={setOpen} />\n\n      <AddCustomModelModal\n        open={openAddModelModal}\n        setOpen={setOpenAddModelModal}\n      />\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Option/Playground/DocumentChip.tsx",
    "content": "import React from \"react\"\nimport { Globe, X } from \"lucide-react\"\nimport { TabInfo } from \"~/hooks/useTabMentions\"\n\ninterface DocumentChipProps {\n  document: TabInfo\n  onRemove: (id: number) => void\n}\n\nexport const DocumentChip: React.FC<DocumentChipProps> = ({\n  document,\n  onRemove,\n}) => {\n  return (\n    <div className=\"inline-flex items-center gap-2 bg-neutral-50 dark:bg-[#404040] border border-neutral-200 dark:border-[#525252] rounded-lg px-3 py-1.5 mr-2 mb-2\">\n      <div className=\"flex items-center gap-2 flex-1 min-w-0\">\n        <div className=\"flex-shrink-0\">\n          {document.favIconUrl ? (\n            <img\n              src={document.favIconUrl}\n              alt=\"\"\n              className=\"w-4 h-4 rounded\"\n              onError={(e) => {\n                const target = e.target as HTMLImageElement\n                target.style.display = \"none\"\n                target.nextElementSibling?.classList.remove(\"hidden\")\n              }}\n            />\n          ) : null}\n          <Globe\n            className={`w-4 h-4 text-neutral-600 dark:text-neutral-400 ${document.favIconUrl ? \"hidden\" : \"\"}`}\n          />\n        </div>\n        <div className=\"flex flex-col max-w-60 truncate\">\n          <span className=\"text-sm font-medium text-neutral-800 dark:text-neutral-200 \">\n            {document.title}\n          </span>\n        </div>{\" \"}\n      </div>\n\n      <button\n        onClick={() => onRemove(document.id)}\n        className=\"flex-shrink-0 text-neutral-600 dark:text-neutral-400 hover:text-neutral-800 dark:hover:text-neutral-200 transition-colors\"\n        type=\"button\">\n        <X className=\"w-3 h-3\" />\n      </button>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Option/Playground/MentionHighlighter.tsx",
    "content": "import React from \"react\"\n\ninterface MentionHighlighterProps {\n  value: string\n  className?: string\n  style?: React.CSSProperties\n}\n\nexport const MentionHighlighter: React.FC<MentionHighlighterProps> = ({\n  value,\n  className = \"\",\n  style = {}\n}) => {\n  const highlightMentions = (text: string) => {\n    // Regular expression to match @mentions (starting with @ followed by non-space characters)\n    const mentionRegex = /@([^\\s@]+)/g\n    const parts = []\n    let lastIndex = 0\n    let match\n\n    while ((match = mentionRegex.exec(text)) !== null) {\n      // Add text before the mention\n      if (match.index > lastIndex) {\n        parts.push(\n          <span key={`text-${lastIndex}`}>\n            {text.substring(lastIndex, match.index)}\n          </span>\n        )\n      }\n\n      // Add the highlighted mention\n      parts.push(\n        <span\n          key={`mention-${match.index}`}\n          className=\"bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300 px-1 py-0.5 rounded font-medium\"\n        >\n          {match[0]}\n        </span>\n      )\n\n      lastIndex = match.index + match[0].length\n    }\n\n    // Add remaining text after the last mention\n    if (lastIndex < text.length) {\n      parts.push(\n        <span key={`text-${lastIndex}`}>\n          {text.substring(lastIndex)}\n        </span>\n      )\n    }\n\n    return parts.length > 0 ? parts : [<span key=\"empty\">{text}</span>]\n  }\n\n  const renderTextWithLineBreaks = (text: string) => {\n    const lines = text.split('\\n')\n    \n    return lines.map((line, lineIndex) => (\n      <React.Fragment key={lineIndex}>\n        {highlightMentions(line)}\n        {lineIndex < lines.length - 1 && <br />}\n      </React.Fragment>\n    ))\n  }\n\n  return (\n    <div\n      className={`pointer-events-none whitespace-pre-wrap break-words ${className}`}\n      style={{\n        ...style,\n        wordWrap: 'break-word',\n        overflowWrap: 'break-word'\n      }}\n      aria-hidden=\"true\"\n    >\n      {renderTextWithLineBreaks(value)}\n      {/* Add a space at the end to maintain cursor positioning */}\n      <span>&nbsp;</span>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Option/Playground/MentionsDropdown.tsx",
    "content": "import React from \"react\"\nimport { TabInfo, MentionPosition } from \"~/hooks/useTabMentions\"\nimport { Globe, X, RefreshCw } from \"lucide-react\"\n\ninterface MentionsDropdownProps {\n  show: boolean\n  tabs: TabInfo[]\n  mentionPosition: MentionPosition | null\n  onSelectTab: (tab: TabInfo) => void\n  onClose: () => void\n  textareaRef: React.RefObject<HTMLTextAreaElement>\n  refetchTabs: () => Promise<void>\n  onMentionsOpen: () => Promise<void>\n}\n\nexport const MentionsDropdown: React.FC<MentionsDropdownProps> = ({\n  show,\n  tabs,\n  mentionPosition,\n  onSelectTab,\n  onClose,\n  textareaRef,\n  refetchTabs,\n  onMentionsOpen\n}) => {\n  const [selectedIndex, setSelectedIndex] = React.useState(0)\n  const [isRefreshing, setIsRefreshing] = React.useState(false)\n  const dropdownRef = React.useRef<HTMLDivElement>(null)\n  const [position, setPosition] = React.useState({ top: 0, left: 0 })\n\n  React.useEffect(() => {\n    setSelectedIndex(0)\n  }, [tabs])\n\n  React.useEffect(() => {\n    if (show && textareaRef.current && dropdownRef.current) {\n      const textareaRect = textareaRef.current.getBoundingClientRect()\n      const dropdownHeight = dropdownRef.current.offsetHeight || 320 \n      \n      setPosition({\n        top: -dropdownHeight - 8, \n        left: 0\n      })\n    }\n  }, [show, tabs])\n\n  React.useEffect(() => {\n    if (show) {\n      onMentionsOpen()\n    }\n  }, [show, onMentionsOpen])\n\n  React.useEffect(() => {\n    const handleKeyDown = (e: KeyboardEvent) => {\n      if (!show) return\n\n      switch (e.key) {\n        case \"ArrowDown\":\n          e.preventDefault()\n          setSelectedIndex((prev) => (prev + 1) % tabs.length)\n          break\n        case \"ArrowUp\":\n          e.preventDefault()\n          setSelectedIndex((prev) => (prev - 1 + tabs.length) % tabs.length)\n          break\n        case \"Enter\":\n          e.preventDefault()\n          if (tabs[selectedIndex]) {\n            onSelectTab(tabs[selectedIndex])\n          }\n          break\n        case \"Escape\":\n          e.preventDefault()\n          onClose()\n          break\n      }\n    }\n\n    if (show) {\n      document.addEventListener(\"keydown\", handleKeyDown)\n      return () => document.removeEventListener(\"keydown\", handleKeyDown)\n    }\n  }, [show, tabs, selectedIndex, onSelectTab, onClose])\n\n  const handleRefreshTabs = async () => {\n    if (isRefreshing) return\n    \n    setIsRefreshing(true)\n    try {\n      await refetchTabs()\n    } catch (error) {\n      console.error(\"Failed to refresh tabs:\", error)\n    } finally {\n      setIsRefreshing(false)\n    }\n  }\n\n  if (!show || tabs.length === 0) return null\n\n  return (\n    <div\n      ref={dropdownRef}\n      className=\"absolute z-50 bg-neutral-50 dark:bg-[#2a2a2a] border border-gray-300 dark:border-gray-600 rounded-lg shadow-lg max-h-80 overflow-y-auto w-80\"\n      style={{\n        top: position.top,\n        left: position.left,\n      }}\n    >\n      <div className=\"p-2 border-b border-gray-200 dark:border-gray-600 flex items-center justify-between\">\n        <span className=\"text-sm font-medium text-gray-700 dark:text-gray-100\">\n          Select Tab\n        </span>\n        <div className=\"flex items-center gap-2\">\n          <button\n            onClick={handleRefreshTabs}\n            disabled={isRefreshing}\n            type=\"button\"\n            className=\"text-gray-400 hover:text-gray-600 dark:hover:text-gray-100 disabled:opacity-50 disabled:cursor-not-allowed\"\n            title=\"Refresh tabs (Ctrl+R)\">\n            <RefreshCw className={`h-4 w-4 ${isRefreshing ? 'animate-spin' : ''}`} />\n          </button>\n          <button\n            onClick={onClose}\n            className=\"text-gray-400 hover:text-gray-600 dark:hover:text-gray-100\">\n            <X className=\"h-4 w-4\" />\n          </button>\n        </div>\n      </div>\n\n      <div className=\"max-h-56 overflow-y-auto\">\n        {tabs.map((tab, index) => (\n          <button\n            key={tab.id}\n            onClick={() => onSelectTab(tab)}\n            className={`w-full text-left p-3 hover:bg-gray-100 dark:hover:bg-gray-600 flex items-center gap-3 transition-colors ${\n              index === selectedIndex\n                ? \"bg-gray-100 dark:bg-gray-600 border-r-2 border-blue-500\"\n                : \"\"\n            }`}>\n            <div className=\"flex-shrink-0\">\n              {tab.favIconUrl ? (\n                <img\n                  src={tab.favIconUrl}\n                  alt=\"\"\n                  className=\"w-4 h-4 rounded\"\n                  onError={(e) => {\n                    const target = e.target as HTMLImageElement\n                    target.style.display = \"none\"\n                    target.nextElementSibling?.classList.remove(\"hidden\")\n                  }}\n                />\n              ) : null}\n              <Globe\n                className={`w-4 h-4 text-gray-400 ${tab.favIconUrl ? \"hidden\" : \"\"}`}\n              />\n            </div>\n\n            <div className=\"flex-1 min-w-0\">\n              <div className=\"text-sm font-medium text-gray-900 dark:text-gray-100 truncate\">\n                {tab.title}\n              </div>\n            </div>\n          </button>\n        ))}\n      </div>\n\n      {tabs.length === 0 && mentionPosition?.query && (\n        <div className=\"p-4 text-center text-gray-500 dark:text-gray-400 text-sm\">\n          <p>No tabs found matching \"{mentionPosition.query}\"</p>\n          <button\n            onClick={handleRefreshTabs}\n            disabled={isRefreshing}\n            className=\"mt-2 text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300 disabled:opacity-50\">\n            {isRefreshing ? \"Refreshing...\" : \"Refresh tabs\"}\n          </button>\n        </div>\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Option/Playground/Playground.tsx",
    "content": "import React from \"react\"\nimport { PlaygroundForm } from \"./PlaygroundForm\"\nimport { PlaygroundChat } from \"./PlaygroundChat\"\nimport { useMessageOption } from \"@/hooks/useMessageOption\"\nimport { webUIResumeLastChat } from \"@/services/app\"\nimport {\n  formatToChatHistory,\n  formatToMessage,\n  getPromptById,\n  getRecentChatFromWebUI\n} from \"@/db/dexie/helpers\"\nimport { useStoreChatModelSettings } from \"@/store/model\"\nimport { useSmartScroll } from \"@/hooks/useSmartScroll\"\nimport { ChevronDown } from \"lucide-react\"\nimport { useStorage } from \"@plasmohq/storage/hook\"\nimport { Storage } from \"@plasmohq/storage\"\nimport { otherUnsupportedTypes } from \"../Knowledge/utils/unsupported-types\"\nconst PlaygroundComponent = () => {\n  const drop = React.useRef<HTMLDivElement>(null)\n  const [dropedFile, setDropedFile] = React.useState<File | undefined>()\n  const [defaultWebUIPrompt] = useStorage(\"defaultWebUIPrompt\", undefined)\n\n  const [chatBackgroundImage] = useStorage({\n    key: \"chatBackgroundImage\",\n    instance: new Storage({\n      area: \"local\"\n    })\n  })\n\n  const {\n    selectedKnowledge,\n    messages,\n    historyId,\n    setHistoryId,\n    setHistory,\n    setMessages,\n    setSelectedSystemPrompt,\n    streaming,\n    webuiTemporaryChat,\n    setTemporaryChat,\n    setSelectedQuickPrompt\n  } = useMessageOption()\n  const { setSystemPrompt } = useStoreChatModelSettings()\n  const { containerRef, isAutoScrollToBottom, autoScrollToBottom } =\n    useSmartScroll(messages, streaming, 120, historyId)\n\n  const [dropState, setDropState] = React.useState<\n    \"idle\" | \"dragging\" | \"error\"\n  >(\"idle\")\n\n  React.useEffect(() => {\n    if (selectedKnowledge) {\n      return\n    }\n\n    if (!drop.current) {\n      return\n    }\n    const handleDragOver = (e: DragEvent) => {\n      e.preventDefault()\n      e.stopPropagation()\n    }\n\n    const handleDrop = (e: DragEvent) => {\n      e.preventDefault()\n      e.stopPropagation()\n\n      setDropState(\"idle\")\n\n      const files = Array.from(e.dataTransfer?.files || [])\n\n      const hasUnsupportedFiles = files.some((file) =>\n        otherUnsupportedTypes.includes(file.type)\n      )\n\n      if (hasUnsupportedFiles) {\n        setDropState(\"error\")\n        return\n      }\n\n      const newFiles = Array.from(e.dataTransfer?.files || []).slice(0, 5) // Allow multiple files\n      if (newFiles.length > 0) {\n        newFiles.forEach((file) => {\n          setDropedFile(file)\n        })\n      }\n    }\n    const handleDragEnter = (e: DragEvent) => {\n      e.preventDefault()\n      e.stopPropagation()\n      setDropState(\"dragging\")\n    }\n\n    const handleDragLeave = (e: DragEvent) => {\n      e.preventDefault()\n      e.stopPropagation()\n      setDropState(\"idle\")\n    }\n\n    drop.current.addEventListener(\"dragover\", handleDragOver)\n    drop.current.addEventListener(\"drop\", handleDrop)\n    drop.current.addEventListener(\"dragenter\", handleDragEnter)\n    drop.current.addEventListener(\"dragleave\", handleDragLeave)\n\n    return () => {\n      if (drop.current) {\n        drop.current.removeEventListener(\"dragover\", handleDragOver)\n        drop.current.removeEventListener(\"drop\", handleDrop)\n        drop.current.removeEventListener(\"dragenter\", handleDragEnter)\n        drop.current.removeEventListener(\"dragleave\", handleDragLeave)\n      }\n    }\n  }, [selectedKnowledge])\n\n  const setRecentMessagesOnLoad = async () => {\n    const isEnabled = await webUIResumeLastChat()\n    if (!isEnabled) {\n      return\n    }\n    if (messages.length === 0) {\n      const recentChat = await getRecentChatFromWebUI()\n      if (recentChat) {\n        setHistoryId(recentChat.history.id)\n        setHistory(formatToChatHistory(recentChat.messages))\n        setMessages(formatToMessage(recentChat.messages))\n\n        const lastUsedPrompt = recentChat?.history?.last_used_prompt\n        if (lastUsedPrompt) {\n          if (lastUsedPrompt.prompt_id) {\n            const prompt = await getPromptById(lastUsedPrompt.prompt_id)\n            if (prompt) {\n              setSelectedSystemPrompt(lastUsedPrompt.prompt_id)\n              setSystemPrompt(prompt.content)\n            }\n          }\n          setSystemPrompt(lastUsedPrompt.prompt_content)\n        }\n      }\n    }\n  }\n\n  React.useEffect(() => {\n    const loadDefaultPrompt = async () => {\n      if (defaultWebUIPrompt && messages.length === 0) {\n        try {\n          const prompt = await getPromptById(defaultWebUIPrompt)\n          if (prompt) {\n            if (prompt.is_system) {\n              setSelectedSystemPrompt(prompt.id)\n              setSystemPrompt(prompt.content)\n            } else {\n              setSelectedSystemPrompt(undefined)\n              setSelectedQuickPrompt(prompt!.content)\n            }\n          }\n        } catch (error) {\n          console.error(\"Failed to load default prompt:\", error)\n        }\n      }\n    }\n\n    loadDefaultPrompt()\n  }, [defaultWebUIPrompt])\n\n  React.useEffect(() => {\n    setRecentMessagesOnLoad()\n  }, [])\n\n  React.useEffect(() => {\n    if (webuiTemporaryChat) {\n      setTemporaryChat(true)\n    }\n  }, [webuiTemporaryChat])\n\n  return (\n    <div\n      ref={drop}\n      data-is-dragging={dropState === \"dragging\"}\n      className=\"relative flex h-full flex-col items-center bg-white dark:bg-[#1a1a1a] data-[is-dragging=true]:bg-gray-100 data-[is-dragging=true]:dark:bg-gray-800\"\n      style={\n        chatBackgroundImage\n          ? {\n              backgroundImage: `url(${chatBackgroundImage})`,\n              backgroundSize: \"cover\",\n              backgroundPosition: \"center\",\n              backgroundRepeat: \"no-repeat\"\n            }\n          : {}\n      }>\n      {/* Background overlay for opacity effect */}\n      {chatBackgroundImage && (\n        <div\n          className=\"absolute inset-0 bg-white dark:bg-[#1a1a1a]\"\n          style={{ opacity: 0.9, pointerEvents: \"none\" }}\n        />\n      )}\n\n      <div\n        ref={containerRef}\n        className=\"custom-scrollbar flex h-full w-full flex-col items-center overflow-x-hidden overflow-y-auto px-5 relative z-10\">\n        <PlaygroundChat />\n      </div>\n      <div className=\"absolute bottom-0 w-full z-10\">\n        {!isAutoScrollToBottom && (\n          <div className=\"absolute bottom-full mb-2 z-10 left-0 right-0 flex justify-center pointer-events-none\">\n            <button\n              onClick={() => autoScrollToBottom()}\n              className=\"bg-gray-50 shadow border border-gray-200 dark:border-none dark:bg-white/20 p-1.5 rounded-full pointer-events-auto hover:bg-gray-100 dark:hover:bg-white/30 transition-colors\">\n              <ChevronDown className=\"size-4 text-gray-600 dark:text-gray-300\" />\n            </button>\n          </div>\n        )}\n        <PlaygroundForm dropedFile={dropedFile} />\n      </div>\n    </div>\n  )\n}\n\nexport const Playground = React.memo(PlaygroundComponent)\n"
  },
  {
    "path": "src/components/Option/Playground/PlaygroundChat.tsx",
    "content": "import React from \"react\"\nimport { useMessageOption } from \"~/hooks/useMessageOption\"\nimport { PlaygroundEmpty } from \"./PlaygroundEmpty\"\nimport { PlaygroundMessage } from \"~/components/Common/Playground/Message\"\nimport { MessageSourcePopup } from \"@/components/Common/Playground/MessageSourcePopup\"\nimport { useStorage } from \"@plasmohq/storage/hook\"\nimport { usePlaygroundMessageGroups } from \"@/components/Common/Playground/message-groups\"\n\nconst PlaygroundChatComponent = () => {\n  const {\n    messages,\n    streaming,\n    regenerateLastMessage,\n    isSearchingInternet,\n    editMessage,\n    ttsEnabled,\n    onSubmit,\n    actionInfo,\n    createChatBranch,\n    temporaryChat\n  } = useMessageOption()\n  const [isSourceOpen, setIsSourceOpen] = React.useState(false)\n  const [source, setSource] = React.useState<any>(null)\n  const [openReasoning] = useStorage(\"openReasoning\", false)\n  const messageGroups = usePlaygroundMessageGroups(messages)\n  const lastGroupIndex = messageGroups.length - 1\n\n  const handleEditMessage = React.useCallback(\n    (\n      actionIndex: number,\n      isHuman: boolean,\n      value: string,\n      isSend: boolean\n    ) => {\n      editMessage(actionIndex, value, isHuman, isSend)\n    },\n    [editMessage]\n  )\n\n  const handleSourceClick = React.useCallback((data: any) => {\n    setSource(data)\n    setIsSourceOpen(true)\n  }, [])\n\n  const handleContinue = React.useCallback(() => {\n    onSubmit({\n      image: \"\",\n      message: \"\",\n      isContinue: true\n    })\n  }, [onSubmit])\n\n  return (\n    <>\n      <div className=\"relative flex w-full flex-col items-center pt-16 pb-4\">\n        {messages.length === 0 && (\n          <div className=\"mt-32 w-full\">\n            <PlaygroundEmpty />\n          </div>\n        )}\n        {messageGroups.map((message, index) => (\n          <PlaygroundMessage\n            key={message.renderKey}\n            isBot={message.isBot}\n            message={message.message}\n            name={message.name}\n            images={message.images || []}\n            isLastMessage={index === lastGroupIndex}\n            actionIndex={message.actionIndex}\n            onRengerate={\n              index === lastGroupIndex ? regenerateLastMessage : undefined\n            }\n            isProcessing={streaming && index === lastGroupIndex}\n            isSearchingInternet={\n              index === lastGroupIndex ? isSearchingInternet : false\n            }\n            sources={message.sources}\n            onEditFormSubmit={handleEditMessage}\n            onSourceClick={handleSourceClick}\n            onNewBranch={createChatBranch}\n            isTTSEnabled={ttsEnabled}\n            generationInfo={message?.generationInfo}\n            isStreaming={streaming && index === lastGroupIndex}\n            reasoningTimeTaken={message?.reasoning_time_taken}\n            openReasoning={openReasoning}\n            modelImage={message?.modelImage}\n            modelName={message?.modelName}\n            temporaryChat={temporaryChat}  \n            messageKind={message?.messageKind}\n            toolCalls={message?.toolCalls}\n            toolCallId={message?.toolCallId}\n            toolName={message?.toolName}\n            toolServerName={message?.toolServerName}\n            toolError={message?.toolError}\n            segments={message.segments}\n            onContinue={index === lastGroupIndex ? handleContinue : undefined}\n            documents={message?.documents}\n            actionInfo={index === lastGroupIndex ? actionInfo : null}\n          />\n        ))}\n      </div>\n      <div className=\"w-full pb-[157px]\"></div>\n\n      <MessageSourcePopup\n        open={isSourceOpen}\n        setOpen={setIsSourceOpen}\n        source={source}\n      />\n    </>\n  )\n}\n\nexport const PlaygroundChat = React.memo(PlaygroundChatComponent)\n"
  },
  {
    "path": "src/components/Option/Playground/PlaygroundEmpty.tsx",
    "content": "import { cleanUrl } from \"@/libs/clean-url\"\nimport { useStorage } from \"@plasmohq/storage/hook\"\nimport { useQuery } from \"@tanstack/react-query\"\nimport { RotateCcw } from \"lucide-react\"\nimport { useEffect, useState } from \"react\"\nimport { Trans, useTranslation } from \"react-i18next\"\nimport {\n  getOllamaURL,\n  isOllamaRunning,\n  setOllamaURL as saveOllamaURL\n} from \"~/services/ollama\"\n\nexport const PlaygroundEmpty = () => {\n  const [ollamaURL, setOllamaURL] = useState<string>(\"\")\n  const { t } = useTranslation([\"playground\", \"common\"])\n\n  const [checkOllamaStatus] = useStorage(\"checkOllamaStatus\", true)\n\n  const {\n    data: ollamaInfo,\n    status: ollamaStatus,\n    refetch,\n    isRefetching\n  } = useQuery({\n    queryKey: [\"ollamaStatus\"],\n    queryFn: async () => {\n      const ollamaURL = await getOllamaURL()\n      const isOk = await isOllamaRunning()\n\n      if (ollamaURL) {\n        saveOllamaURL(ollamaURL)\n      }\n\n      return {\n        isOk,\n        ollamaURL\n      }\n    },\n    enabled: checkOllamaStatus\n  })\n\n  useEffect(() => {\n    if (ollamaInfo?.ollamaURL) {\n      setOllamaURL(ollamaInfo.ollamaURL)\n    }\n  }, [ollamaInfo])\n\n\n  if (!checkOllamaStatus) {\n    return (\n      <div className=\"mx-auto sm:max-w-xl px-4 mt-10\">\n        <div className=\"rounded-lg justify-center items-center flex flex-col border p-8 bg-gray-50 dark:bg-[#262626] dark:border-gray-600\">\n          <h1 className=\"text-sm  font-medium text-center text-gray-500 dark:text-gray-400 flex gap-3 items-center justify-center\">\n            <span >👋</span>\n            <span className=\"text-gray-700 dark:text-gray-300\">\n              {t(\"welcome\")}\n            </span>\n          </h1>\n        </div>\n      </div>\n    )\n  }\n  return (\n    <div className=\"mx-auto sm:max-w-xl px-4 mt-10\">\n      <div className=\"rounded-lg justify-center items-center flex flex-col border p-8 bg-gray-50 dark:bg-[#262626]  dark:border-gray-600\">\n        {(ollamaStatus === \"pending\" || isRefetching) && (\n          <div className=\"inline-flex items-center space-x-2\">\n            <div className=\"w-3 h-3 bg-blue-500 rounded-full animate-pulse\"></div>\n            <p className=\"dark:text-gray-400 text-gray-900\">\n              {t(\"ollamaState.searching\")}\n            </p>\n          </div>\n        )}\n        {!isRefetching && ollamaStatus === \"success\" ? (\n          ollamaInfo.isOk ? (\n            <div className=\"inline-flex  items-center space-x-2\">\n              <div className=\"w-3 h-3 bg-green-500 rounded-full animate-pulse\"></div>\n              <p className=\"dark:text-gray-400 text-gray-900\">\n                {t(\"ollamaState.running\")}\n              </p>\n            </div>\n          ) : (\n            <div className=\"flex flex-col space-y-2 justify-center items-center\">\n              <div className=\"inline-flex  space-x-2\">\n                <div className=\"w-3 h-3 bg-red-500 rounded-full animate-pulse\"></div>\n                <p className=\"dark:text-gray-400 text-gray-900\">\n                  {t(\"ollamaState.notRunning\")}\n                </p>\n              </div>\n\n              <input\n                className=\"bg-gray-100 dark:bg-[#262626] dark:text-gray-100 rounded-md px-4 py-2 mt-2 w-full\"\n                type=\"url\"\n                value={ollamaURL}\n                onChange={(e) => setOllamaURL(e.target.value)}\n              />\n\n              <button\n                onClick={() => {\n                  saveOllamaURL(ollamaURL)\n                  refetch()\n                }}\n                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 \">\n                <RotateCcw className=\"h-4 w-4 mr-3\" />\n                {t(\"common:retry\")}\n              </button>\n\n              {ollamaURL &&\n                cleanUrl(ollamaURL) !== \"http://127.0.0.1:11434\" && (\n                  <p className=\"text-xs text-gray-500 dark:text-gray-400 mb-4 text-center\">\n                    <Trans\n                      i18nKey=\"playground:ollamaState.connectionError\"\n                      components={{\n                        anchor: (\n                          <a\n                            href=\"https://github.com/n4ze3m/page-assist/blob/main/docs/connection-issue.md\"\n                            target=\"__blank\"\n                            className=\"text-blue-600 dark:text-blue-400\"></a>\n                        )\n                      }}\n                    />\n                  </p>\n                )}\n            </div>\n          )\n        ) : null}\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Option/Playground/PlaygroundFile.tsx",
    "content": "import { UploadedFile } from \"@/db\"\nimport { FileIcon, XIcon } from \"lucide-react\"\nimport { formatFileSize } from \"@/utils/format-file-size\"\n\ntype Props = {\n  file: UploadedFile\n  removeUploadedFile: (id: string) => void\n}\n\nexport const PlaygroundFile: React.FC<Props> = ({\n  file,\n  removeUploadedFile\n}) => {\n  return (\n    <button\n      className=\"relative group p-1.5 w-60 flex items-center gap-1 bg-white dark:bg-[#1a1a1a] border border-gray-200 dark:border-white/5 rounded-2xl text-left\"\n      type=\"button\">\n      <div className=\"p-3 bg-black/20 dark:bg-white/10 text-white rounded-xl\">\n        <FileIcon className=\"size-5\" />\n      </div>\n      <div className=\"flex flex-col justify-center -space-y-0.5 px-2.5 w-full\">\n        <div className=\"dark:text-gray-100 text-sm font-medium line-clamp-1 mb-1\">\n          {file.filename}\n        </div>\n        <div className=\"flex justify-between text-gray-500 text-xs line-clamp-1\">\n          File{\" \"}\n          <span className=\"capitalize\">\n            {formatFileSize(file.size)}\n          </span>\n        </div>\n      </div>\n      <div className=\"absolute -top-1 -right-1\">\n        <button\n          onClick={() => removeUploadedFile(file.id)}\n          className=\"bg-white dark:bg-gray-700 text-black dark:text-gray-100 border border-gray-50 dark:border-[#404040] rounded-full group-hover:visible invisible transition\"\n          type=\"button\">\n          <XIcon className=\"w-4 h-4\" />\n        </button>\n      </div>\n    </button>\n  )\n}\n"
  },
  {
    "path": "src/components/Option/Playground/PlaygroundForm.tsx",
    "content": "import { useForm } from \"@mantine/form\"\nimport { useMutation, useQueryClient } from \"@tanstack/react-query\"\nimport React from \"react\"\nimport useDynamicTextareaSize from \"~/hooks/useDynamicTextareaSize\"\nimport { toBase64 } from \"~/libs/to-base64\"\nimport { useMessageOption } from \"~/hooks/useMessageOption\"\nimport { Checkbox, Dropdown, Switch, Tooltip, Select, Popover } from \"antd\"\nimport { Image } from \"antd\"\nimport { useWebUI } from \"~/store/webui\"\nimport { defaultEmbeddingModelForRag } from \"~/services/ollama\"\nimport {\n  EraserIcon,\n  ImageIcon,\n  MicIcon,\n  StopCircleIcon,\n  X,\n  FileIcon,\n  FileText,\n  PaperclipIcon,\n  Brain,\n  PlusIcon,\n  MinusIcon,\n  ArrowUp\n} from \"lucide-react\"\nimport { getVariable } from \"@/utils/select-variable\"\nimport { useTranslation } from \"react-i18next\"\nimport { KnowledgeSelect } from \"../Knowledge/KnowledgeSelect\"\nimport { useSpeechRecognition } from \"@/hooks/useSpeechRecognition\"\nimport { PiGlobe } from \"react-icons/pi\"\nimport { handleChatInputKeyDown } from \"@/utils/key-down\"\nimport { getIsSimpleInternetSearch } from \"@/services/search\"\nimport { useStorage } from \"@plasmohq/storage/hook\"\nimport { useTabMentions } from \"~/hooks/useTabMentions\"\nimport { useFocusShortcuts } from \"~/hooks/keyboard\"\nimport { MentionsDropdown } from \"./MentionsDropdown\"\nimport { DocumentChip } from \"./DocumentChip\"\nimport { otherUnsupportedTypes } from \"../Knowledge/utils/unsupported-types\"\nimport { PASTED_TEXT_CHAR_LIMIT } from \"@/utils/constant\"\nimport { PlaygroundFile } from \"./PlaygroundFile\"\nimport { isThinkingCapableModel, isGptOssModel } from \"~/libs/model-utils\"\nimport { useStoreChatModelSettings } from \"~/store/model\"\nimport { useMessageQueue } from \"@/hooks/useMessageQueue\"\nimport { QueuedMessagesList } from \"@/components/Common/QueuedMessagesList\"\nimport { McpServerToggle } from \"@/components/Common/McpServerToggle\"\ntype Props = {\n  dropedFile: File | undefined\n}\n\nexport const PlaygroundForm = ({ dropedFile }: Props) => {\n  const { t } = useTranslation([\"playground\", \"common\"])\n  const inputRef = React.useRef<HTMLInputElement>(null)\n  const fileInputRef = React.useRef<HTMLInputElement>(null)\n  const combinedUploadInputRef = React.useRef<HTMLInputElement>(null)\n\n  const [typing, setTyping] = React.useState<boolean>(false)\n  const [checkWideMode] = useStorage(\"checkWideMode\", false)\n  const {\n    onSubmit,\n    selectedModel,\n    chatMode,\n    speechToTextLanguage,\n    stopStreamingRequest,\n    streaming: isSending,\n    webSearch,\n    setWebSearch,\n    selectedQuickPrompt,\n    textareaRef,\n    setSelectedQuickPrompt,\n    selectedKnowledge,\n    temporaryChat,\n    useOCR,\n    setUseOCR,\n    defaultInternetSearchOn,\n    setHistory,\n    history,\n    uploadedFiles,\n    fileRetrievalEnabled,\n    setFileRetrievalEnabled,\n    handleFileUpload,\n    removeUploadedFile,\n    clearUploadedFiles\n  } = useMessageOption()\n\n  const [autoSubmitVoiceMessage] = useStorage(\"autoSubmitVoiceMessage\", false)\n\n  const [autoStopTimeout] = useStorage(\"autoStopTimeout\", 2000)\n\n  // Thinking mode state\n  const [defaultThinkingMode] = useStorage(\"defaultThinkingMode\", false)\n  const thinking = useStoreChatModelSettings((state) => state.thinking)\n  const setThinking = useStoreChatModelSettings((state) => state.setThinking)\n\n  const {\n    tabMentionsEnabled,\n    showMentions,\n    mentionPosition,\n    filteredTabs,\n    selectedDocuments,\n    handleTextChange,\n    insertMention,\n    closeMentions,\n    removeDocument,\n    clearSelectedDocuments,\n    reloadTabs,\n    handleMentionsOpen\n  } = useTabMentions(textareaRef)\n\n  // Enable focus shortcuts (Shift+Esc to focus textarea)\n  useFocusShortcuts(textareaRef, true)\n\n  const [pasteLargeTextAsFile] = useStorage(\"pasteLargeTextAsFile\", false)\n  const [persistChatInput] = useStorage(\"persistChatInput\", false)\n  const [persistedMessage, setPersistedMessage] = useStorage(\n    \"playgroundPersistedMessage\",\n    \"\"\n  )\n  const [enableMessageQueue] = useStorage(\"enableMessageQueue\", false)\n  const [showMcpServersInChat] = useStorage(\"showMcpServersInChat\", true)\n  const [optimizeQueueForSmallScreen] = useStorage(\n    \"optimizeQueueForSmallScreen\",\n    false\n  )\n\n  const isMobile = () => {\n    return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(\n      navigator.userAgent\n    )\n  }\n\n  const textAreaFocus = () => {\n    if (textareaRef.current) {\n      if (\n        textareaRef.current.selectionStart === textareaRef.current.selectionEnd\n      ) {\n        if (!isMobile()) {\n          textareaRef.current.focus()\n        } else {\n          textareaRef.current.blur()\n        }\n      }\n    }\n  }\n\n  const form = useForm({\n    initialValues: {\n      message: \"\",\n      image: \"\",\n      images: [] as string[]\n    }\n  })\n\n  React.useEffect(() => {\n    textAreaFocus()\n    if (defaultInternetSearchOn) {\n      setWebSearch(true)\n    }\n  }, [])\n\n  React.useEffect(() => {\n    if (defaultInternetSearchOn) {\n      setWebSearch(true)\n    }\n  }, [defaultInternetSearchOn])\n\n  // Separate effect for restoring persisted message to handle async useStorage\n  React.useEffect(() => {\n    if (persistChatInput && persistedMessage && !form.values.message) {\n      form.setFieldValue(\"message\", persistedMessage)\n    }\n  }, [persistChatInput, persistedMessage])\n\n  const onInputChange = async (\n    e: React.ChangeEvent<HTMLInputElement> | File\n  ) => {\n    if (e instanceof File) {\n      const isUnsupported = otherUnsupportedTypes.includes(e.type)\n\n      if (isUnsupported) {\n        console.error(\"File type not supported:\", e.type)\n        return\n      }\n\n      const isImage = e.type.startsWith(\"image/\")\n      if (isImage) {\n        const base64 = await toBase64(e)\n        const currentImages = form.values.images || []\n        form.setFieldValue(\"images\", [...currentImages, base64])\n      } else {\n        await handleFileUpload(e)\n      }\n    } else {\n      if (e.target.files) {\n        onFileInputChange(e)\n      }\n    }\n  }\n\n  const onFileInputChange = async (e: React.ChangeEvent<HTMLInputElement>) => {\n    if (e.target.files && e.target.files.length > 0) {\n      const files = Array.from(e.target.files)\n\n      for (const file of files) {\n        const isUnsupported = otherUnsupportedTypes.includes(file.type)\n\n        if (isUnsupported) {\n          console.error(\"File type not supported:\", file.type)\n          continue\n        }\n\n        const isImage = file.type.startsWith(\"image/\")\n        if (isImage) {\n          const base64 = await toBase64(file)\n          const currentImages = form.values.images || []\n          form.setFieldValue(\"images\", [...currentImages, base64])\n        } else {\n          await handleFileUpload(file)\n        }\n      }\n    }\n  }\n\n  const onCombinedUploadInputChange = async (\n    e: React.ChangeEvent<HTMLInputElement>\n  ) => {\n    if (!e.target.files || e.target.files.length === 0) {\n      return\n    }\n    const files = Array.from(e.target.files)\n    for (const file of files) {\n      await onInputChange(file)\n    }\n    e.target.value = \"\"\n  }\n  const removeImage = (index: number) => {\n    const currentImages = form.values.images || []\n    const newImages = currentImages.filter((_, i) => i !== index)\n    form.setFieldValue(\"images\", newImages)\n  }\n\n  const handlePaste = async (e: React.ClipboardEvent) => {\n    if (e.clipboardData.files.length > 0) {\n      onInputChange(e.clipboardData.files[0])\n      return\n    }\n\n    const pastedText = e.clipboardData.getData(\"text/plain\")\n\n    if (\n      pasteLargeTextAsFile &&\n      pastedText &&\n      pastedText.length > PASTED_TEXT_CHAR_LIMIT\n    ) {\n      e.preventDefault()\n      const blob = new Blob([pastedText], { type: \"text/plain\" })\n      const file = new File([blob], `pasted-text-${Date.now()}.txt`, {\n        type: \"text/plain\"\n      })\n\n      await handleFileUpload(file)\n      return\n    }\n  }\n  React.useEffect(() => {\n    if (dropedFile) {\n      onInputChange(dropedFile)\n    }\n  }, [dropedFile])\n\n  useDynamicTextareaSize(textareaRef, form.values.message, 300)\n\n  const {\n    transcript,\n    isListening,\n    resetTranscript,\n    start: startListening,\n    stop: stopSpeechRecognition,\n    supported: browserSupportsSpeechRecognition\n  } = useSpeechRecognition({\n    autoStop: autoSubmitVoiceMessage,\n    autoStopTimeout,\n    onEnd: async () => {\n      if (autoSubmitVoiceMessage) {\n        submitForm()\n      }\n    }\n  })\n  const { sendWhenEnter, setSendWhenEnter } = useWebUI()\n\n  React.useEffect(() => {\n    if (isListening) {\n      form.setFieldValue(\"message\", transcript)\n    }\n  }, [transcript])\n\n  React.useEffect(() => {\n    if (selectedQuickPrompt) {\n      const word = getVariable(selectedQuickPrompt)\n      form.setFieldValue(\"message\", selectedQuickPrompt)\n      if (word) {\n        textareaRef.current?.focus()\n        const interval = setTimeout(() => {\n          textareaRef.current?.setSelectionRange(word.start, word.end)\n          setSelectedQuickPrompt(null)\n        }, 100)\n        return () => {\n          clearInterval(interval)\n        }\n      }\n    }\n  }, [selectedQuickPrompt])\n\n  const queryClient = useQueryClient()\n\n  const { mutateAsync: sendMessage } = useMutation({\n    mutationFn: onSubmit,\n    onSuccess: () => {\n      textAreaFocus()\n      queryClient.invalidateQueries({\n        queryKey: [\"fetchChatHistory\"]\n      })\n    },\n    onError: (error) => {\n      textAreaFocus()\n    }\n  })\n\n  const validateBeforeMessageSend = async () => {\n    if (!selectedModel || selectedModel.length === 0) {\n      form.setFieldError(\"message\", t(\"formError.noModel\"))\n      return false\n    }\n\n    if (webSearch) {\n      const defaultEM = await defaultEmbeddingModelForRag()\n      const simpleSearch = await getIsSimpleInternetSearch()\n      if (!defaultEM && !simpleSearch) {\n        form.setFieldError(\"message\", t(\"formError.noEmbeddingModel\"))\n        return false\n      }\n    }\n\n    return true\n  }\n\n  const sendQueuedTextMessage = async (payload: {\n    message: string\n    images: string[]\n  }) => {\n    const trimmedMessage = payload.message.trim()\n    const hasImages = payload.images.length > 0\n    if (!trimmedMessage && !hasImages) {\n      throw new Error(\"Queue item is empty\")\n    }\n\n    const isValid = await validateBeforeMessageSend()\n    if (!isValid) {\n      throw new Error(\"Validation failed\")\n    }\n\n    await sendMessage({\n      image: payload.images.length > 0 ? payload.images[0] : \"\",\n      images: payload.images,\n      message: trimmedMessage,\n      docs: []\n    })\n  }\n\n  const {\n    queuedMessages,\n    enqueueMessage,\n    deleteQueuedMessage,\n    takeQueuedMessage,\n    sendQueuedMessageNow\n  } = useMessageQueue({\n    enabled: enableMessageQueue,\n    streaming: isSending,\n    onSendMessage: sendQueuedTextMessage,\n    onStopStreaming: stopStreamingRequest\n  })\n  const [isQueuePanelExpanded, setIsQueuePanelExpanded] = React.useState(false)\n  const hasQueuedMessages = queuedMessages.length > 0\n  const useCompactActions = optimizeQueueForSmallScreen\n  const [isCompactActionsPopoverOpen, setIsCompactActionsPopoverOpen] =\n    React.useState(false)\n\n  React.useEffect(() => {\n    if (\n      !enableMessageQueue ||\n      !optimizeQueueForSmallScreen ||\n      !hasQueuedMessages\n    ) {\n      setIsQueuePanelExpanded(false)\n    }\n  }, [enableMessageQueue, hasQueuedMessages, optimizeQueueForSmallScreen])\n\n  React.useEffect(() => {\n    if (!useCompactActions) {\n      setIsCompactActionsPopoverOpen(false)\n    }\n  }, [useCompactActions])\n\n  const sendFormValue = async (value: {\n    message: string\n    image: string\n    images: string[]\n  }) => {\n    if (\n      value.message.trim().length === 0 &&\n      (!value.images || value.images.length === 0) &&\n      selectedDocuments.length === 0 &&\n      uploadedFiles.length === 0\n    ) {\n      return\n    }\n\n    const isValid = await validateBeforeMessageSend()\n    if (!isValid) {\n      return\n    }\n\n    form.reset()\n    clearSelectedDocuments()\n    clearUploadedFiles()\n    if (persistChatInput) {\n      setPersistedMessage(\"\")\n    }\n    textAreaFocus()\n\n    await sendMessage({\n      image: value.images && value.images.length > 0 ? value.images[0] : \"\",\n      images: value.images,\n      message: value.message.trim(),\n      docs: selectedDocuments.map((doc) => ({\n        type: \"tab\",\n        tabId: doc.id,\n        title: doc.title,\n        url: doc.url,\n        favIconUrl: doc.favIconUrl\n      }))\n    })\n  }\n\n  const handleFormSubmit = async (value: {\n    message: string\n    image: string\n    images: string[]\n  }) => {\n    await stopListening()\n\n    if (enableMessageQueue && isSending) {\n      const enqueued = enqueueMessage({\n        message: value.message,\n        images: value.images || []\n      })\n      if (enqueued) {\n        form.setFieldValue(\"message\", \"\")\n        form.setFieldValue(\"images\", [])\n        if (persistChatInput) {\n          setPersistedMessage(\"\")\n        }\n        closeMentions()\n      }\n      return\n    }\n\n    await sendFormValue(value)\n  }\n\n  const submitForm = () => {\n    form.onSubmit(handleFormSubmit)()\n  }\n\n  const handleKeyDown = (e: React.KeyboardEvent) => {\n    if (import.meta.env.BROWSER !== \"firefox\") {\n      if (e.key === \"Process\" || e.key === \"229\") return\n    }\n\n    if (\n      showMentions &&\n      (e.key === \"ArrowDown\" ||\n        e.key === \"ArrowUp\" ||\n        e.key === \"Enter\" ||\n        e.key === \"Escape\")\n    ) {\n      return\n    }\n\n    if (\n      handleChatInputKeyDown({\n        e,\n        sendWhenEnter,\n        typing,\n        isSending: isSending && !enableMessageQueue\n      })\n    ) {\n      e.preventDefault()\n      stopListening()\n      submitForm()\n    }\n  }\n\n  const stopListening = async () => {\n    if (isListening) {\n      stopSpeechRecognition()\n    }\n  }\n\n  const compactActionsPopoverContent = (\n    <div className=\"w-60 space-y-2\">\n      {!selectedKnowledge && (\n        <div className=\"flex items-center justify-between rounded-lg border border-gray-200 px-2 py-1.5 dark:border-[#404040]\">\n          <span className=\"text-xs text-gray-600 dark:text-gray-300\">\n            {t(\"tooltip.searchInternet\")}\n          </span>\n          <Switch\n            size=\"small\"\n            value={webSearch}\n            onChange={(enabled) => setWebSearch(enabled)}\n            checkedChildren={t(\"form.webSearch.on\")}\n            unCheckedChildren={t(\"form.webSearch.off\")}\n          />\n        </div>\n      )}\n      {defaultThinkingMode && isThinkingCapableModel(selectedModel) && (\n        isGptOssModel(selectedModel) ? (\n          <div className=\"flex items-center justify-between rounded-lg border border-gray-200 px-2 py-1.5 dark:border-[#404040]\">\n            <span className=\"text-xs text-gray-600 dark:text-gray-300\">\n              {t(\"tooltip.thinking\")}\n            </span>\n            <Select\n              size=\"small\"\n              value={typeof thinking === \"string\" ? thinking : \"medium\"}\n              onChange={(value) =>\n                setThinking?.(value as \"low\" | \"medium\" | \"high\")\n              }\n              options={[\n                {\n                  value: \"low\",\n                  label: t(\"form.thinking.levels.low\")\n                },\n                {\n                  value: \"medium\",\n                  label: t(\"form.thinking.levels.medium\")\n                },\n                {\n                  value: \"high\",\n                  label: t(\"form.thinking.levels.high\")\n                }\n              ]}\n              className=\"w-24\"\n            />\n          </div>\n        ) : (\n          <div className=\"flex items-center justify-between rounded-lg border border-gray-200 px-2 py-1.5 dark:border-[#404040]\">\n            <span className=\"text-xs text-gray-600 dark:text-gray-300\">\n              {t(\"tooltip.thinking\")}\n            </span>\n            <Switch\n              size=\"small\"\n              checked={!!thinking}\n              onChange={(enabled) => setThinking?.(enabled)}\n              checkedChildren={t(\"form.thinking.on\")}\n              unCheckedChildren={t(\"form.thinking.off\")}\n            />\n          </div>\n        )\n      )}\n      <div className=\"flex items-center justify-between rounded-lg border border-gray-200 px-2 py-1.5 dark:border-[#404040]\">\n        <span className=\"text-xs text-gray-600 dark:text-gray-300\">\n          {t(\"sendWhenEnter\")}\n        </span>\n        <Switch\n          size=\"small\"\n          checked={sendWhenEnter}\n          onChange={(enabled) => setSendWhenEnter(enabled)}\n        />\n      </div>\n      <div className=\"flex items-center justify-between rounded-lg border border-gray-200 px-2 py-1.5 dark:border-[#404040]\">\n        <span className=\"text-xs text-gray-600 dark:text-gray-300\">\n          {t(\"useOCR\")}\n        </span>\n        <Switch\n          size=\"small\"\n          checked={useOCR}\n          onChange={(enabled) => setUseOCR(enabled)}\n        />\n      </div>\n      {history.length > 0 && (\n        <button\n          type=\"button\"\n          onClick={() => {\n            setHistory([])\n            setIsCompactActionsPopoverOpen(false)\n          }}\n          className={`flex w-full items-center justify-between rounded-lg border border-gray-200 px-2 py-1.5 text-left dark:border-[#404040] ${\n            chatMode === \"rag\" ? \"hidden\" : \"flex\"\n          }`}>\n          <span className=\"text-xs text-gray-600 dark:text-gray-300\">\n            {t(\"tooltip.clearContext\")}\n          </span>\n          <EraserIcon className=\"h-4 w-4 text-gray-500 dark:text-gray-300\" />\n        </button>\n      )}\n    </div>\n  )\n\n  return (\n    <div className=\"flex w-full flex-col items-center px-2\">\n      <div className=\"relative z-10 flex w-full flex-col items-center justify-center gap-2 text-base\">\n        <div className=\"relative flex w-full flex-row justify-center gap-2 lg:w-3/5\">\n          <div\n            data-istemporary-chat={temporaryChat}\n            data-checkwidemode={checkWideMode}\n            className={` bg-neutral-50/70  dark:bg-[#2a2a2a]/70 relative w-full max-w-[48rem] p-1 backdrop-blur-3xl duration-100 border border-gray-300 rounded-t-xl  dark:border-[#404040] data-[istemporary-chat='true']:bg-gray-200/70 data-[istemporary-chat='true']:dark:bg-black/70 data-[checkwidemode='true']:max-w-none`}>\n            {enableMessageQueue &&\n              optimizeQueueForSmallScreen &&\n              hasQueuedMessages && (\n                <div className=\"px-2 pt-2 md:hidden\">\n                  <button\n                    type=\"button\"\n                    onClick={() =>\n                      setIsQueuePanelExpanded((previous) => !previous)\n                    }\n                    className=\"flex w-full items-center justify-between rounded-lg border border-dashed border-gray-300 bg-white/70 px-3 py-2 text-xs text-gray-700 dark:border-[#4a4a4a] dark:bg-[#303030]/70 dark:text-gray-200\"\n                    aria-expanded={isQueuePanelExpanded}\n                    aria-controls=\"playground-queued-messages\">\n                    <span className=\"inline-flex items-center gap-2 font-medium\">\n                      {t(\"form.queue.title\", \"Queued messages\")}\n                      <span className=\"inline-flex min-w-5 items-center justify-center rounded-full bg-gray-200 px-1.5 py-0.5 text-[11px] text-gray-700 dark:bg-[#454545] dark:text-gray-200\">\n                        {queuedMessages.length}\n                      </span>\n                    </span>\n                    {isQueuePanelExpanded ? (\n                      <MinusIcon className=\"h-3.5 w-3.5\" />\n                    ) : (\n                      <PlusIcon className=\"h-3.5 w-3.5\" />\n                    )}\n                  </button>\n                </div>\n              )}\n            {enableMessageQueue && (\n              <div\n                id=\"playground-queued-messages\"\n                className={\n                  optimizeQueueForSmallScreen && !isQueuePanelExpanded\n                    ? \"hidden md:block\"\n                    : \"block\"\n                }>\n                <QueuedMessagesList\n                  queuedMessages={queuedMessages}\n                  onDelete={deleteQueuedMessage}\n                  onEdit={(id) => {\n                    const queuedItem = takeQueuedMessage(id)\n                    if (!queuedItem) {\n                      return\n                    }\n                    form.setFieldValue(\"message\", queuedItem.message)\n                    form.setFieldValue(\"images\", queuedItem.images || [])\n                    if (persistChatInput) {\n                      setPersistedMessage(queuedItem.message)\n                    }\n                    textAreaFocus()\n                  }}\n                  onSend={sendQueuedMessageNow}\n                  title={t(\"form.queue.title\", \"Queued messages\")}\n                />\n              </div>\n            )}\n            {form.values.images && form.values.images.length > 0 && (\n              <div className=\"p-3 border-b border-gray-200 dark:border-[#404040]\">\n                <div className=\"flex flex-wrap gap-2\">\n                  {form.values.images.map((img, index) => (\n                    <div key={index} className=\"relative\">\n                      <button\n                        type=\"button\"\n                        onClick={() => removeImage(index)}\n                        className=\"absolute -top-2 -right-2 flex items-center justify-center z-10 bg-white dark:bg-[#2a2a2a] p-0.5 rounded-full hover:bg-gray-100 dark:hover:bg-[#404040] text-black dark:text-gray-100 shadow-md\">\n                        <X className=\"h-3 w-3\" />\n                      </button>\n                      <Image\n                        src={img}\n                        alt={`Uploaded Image ${index + 1}`}\n                        preview={true}\n                        className=\"rounded-md max-h-32 object-cover\"\n                      />\n                    </div>\n                  ))}\n                </div>\n              </div>\n            )}\n            {selectedDocuments.length > 0 && (\n              <div className=\"p-3\">\n                <div className=\"max-h-24 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-300 dark:scrollbar-thumb-[#404040] scrollbar-track-transparent\">\n                  <div className=\"flex flex-wrap gap-1.5\">\n                    {selectedDocuments.map((document) => (\n                      <DocumentChip\n                        key={document.id}\n                        document={document}\n                        onRemove={removeDocument}\n                      />\n                    ))}\n                  </div>\n                </div>\n              </div>\n            )}\n            {uploadedFiles.length > 0 && (\n              <div className=\"p-3 border-b border-gray-200 dark:border-[#404040]\">\n                <div className=\"flex items-center justify-end mb-2\">\n                  <div className=\"flex items-center gap-2\">\n                    <Tooltip title={t(\"fileRetrievalEnabled\")}>\n                      <div className=\"inline-flex items-center gap-2\">\n                        <FileText className=\"h-4 w-4 dark:text-gray-300\" />\n                        <Switch\n                          size=\"small\"\n                          checked={fileRetrievalEnabled}\n                          onChange={setFileRetrievalEnabled}\n                        />\n                      </div>\n                    </Tooltip>\n                  </div>\n                </div>\n                <div className=\"max-h-24 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-300 dark:scrollbar-thumb-[#404040] scrollbar-track-transparent\">\n                  <div className=\"flex flex-wrap gap-1.5\">\n                    {uploadedFiles.map((file) => (\n                      <PlaygroundFile\n                        key={file.id}\n                        file={file}\n                        removeUploadedFile={removeUploadedFile}\n                      />\n                    ))}\n                  </div>\n                </div>\n              </div>\n            )}\n            <div>\n              <div className={`flex  bg-transparent `}>\n                <form\n                  onSubmit={form.onSubmit(handleFormSubmit)}\n                  className=\"shrink-0 flex-grow  flex flex-col items-center \">\n                  <input\n                    id=\"file-upload\"\n                    name=\"file-upload\"\n                    type=\"file\"\n                    className=\"sr-only\"\n                    ref={inputRef}\n                    accept=\"image/*\"\n                    multiple={true}\n                    onChange={onInputChange}\n                  />\n                  <input\n                    id=\"document-upload\"\n                    name=\"document-upload\"\n                    type=\"file\"\n                    className=\"sr-only\"\n                    ref={fileInputRef}\n                    accept=\".pdf,.doc,.docx,.txt,.csv\"\n                    multiple={false}\n                    onChange={onFileInputChange}\n                  />\n                  <input\n                    id=\"combined-upload\"\n                    name=\"combined-upload\"\n                    type=\"file\"\n                    className=\"sr-only\"\n                    ref={combinedUploadInputRef}\n                    accept=\"image/*,.pdf,.doc,.docx,.txt,.csv\"\n                    multiple={true}\n                    onChange={onCombinedUploadInputChange}\n                  />\n\n                  <div className=\"w-full  flex flex-col dark:border-[#404040]  px-2 \">\n                    <div className=\"relative\">\n                      <textarea\n                        id=\"textarea-message\"\n                        onCompositionStart={() => {\n                          if (import.meta.env.BROWSER !== \"firefox\") {\n                            setTyping(true)\n                          }\n                        }}\n                        onCompositionEnd={() => {\n                          if (import.meta.env.BROWSER !== \"firefox\") {\n                            setTyping(false)\n                          }\n                        }}\n                        onKeyDown={(e) => handleKeyDown(e)}\n                        ref={textareaRef}\n                        className=\"px-2 py-2 w-full resize-none bg-transparent focus-within:outline-none focus:ring-0 focus-visible:ring-0 ring-0 dark:ring-0 border-0 dark:text-gray-100\"\n                        onPaste={handlePaste}\n                        rows={1}\n                        style={{ minHeight: \"35px\" }}\n                        tabIndex={0}\n                        placeholder={t(\"form.textarea.placeholder\")}\n                        {...form.getInputProps(\"message\")}\n                        onChange={(e) => {\n                          form.getInputProps(\"message\").onChange(e)\n                          // Persist message as user types\n                          if (persistChatInput) {\n                            setPersistedMessage(e.target.value)\n                          }\n                          if (tabMentionsEnabled && textareaRef.current) {\n                            handleTextChange(\n                              e.target.value,\n                              textareaRef.current.selectionStart || 0\n                            )\n                          }\n                        }}\n                        onSelect={(e) => {\n                          if (tabMentionsEnabled && textareaRef.current) {\n                            handleTextChange(\n                              textareaRef.current.value,\n                              textareaRef.current.selectionStart || 0\n                            )\n                          }\n                        }}\n                      />\n\n                      <MentionsDropdown\n                        show={showMentions}\n                        tabs={filteredTabs}\n                        mentionPosition={mentionPosition}\n                        onSelectTab={(tab) =>\n                          insertMention(tab, form.values.message, (value) =>\n                            form.setFieldValue(\"message\", value)\n                          )\n                        }\n                        onClose={closeMentions}\n                        textareaRef={textareaRef}\n                        refetchTabs={async () => {\n                          await reloadTabs()\n                        }}\n                        onMentionsOpen={handleMentionsOpen}\n                      />\n                    </div>\n                    <div className=\"mt-2 flex justify-between items-center\">\n                      <div\n                        className={`items-center gap-3 ${\n                          useCompactActions ? \"hidden md:flex\" : \"flex\"\n                        }`}>\n                        {!selectedKnowledge && (\n                          <Tooltip title={t(\"tooltip.searchInternet\")}>\n                            <div className=\"inline-flex items-center gap-2\">\n                              <PiGlobe\n                                className={`h-5 w-5 dark:text-gray-300 `}\n                              />\n                              <Switch\n                                value={webSearch}\n                                onChange={(e) => setWebSearch(e)}\n                                checkedChildren={t(\"form.webSearch.on\")}\n                                unCheckedChildren={t(\"form.webSearch.off\")}\n                              />\n                            </div>\n                          </Tooltip>\n                        )}\n                        {defaultThinkingMode && isThinkingCapableModel(selectedModel) &&\n                          (isGptOssModel(selectedModel) ? (\n                            // For gpt-oss: Only show level selector (no on/off toggle)\n                            <div className=\"inline-flex items-center gap-2\">\n                              <Tooltip title=\"Adjust reasoning intensity (always enabled)\">\n                                <div className=\"inline-flex items-center gap-2\">\n                                  <Brain className=\"h-5 w-5 text-gray-600 dark:text-gray-400\" />\n                                </div>\n                              </Tooltip>\n                              <Select\n                                size=\"small\"\n                                value={\n                                  typeof thinking === \"string\"\n                                    ? thinking\n                                    : \"medium\"\n                                }\n                                onChange={(value) =>\n                                  setThinking?.(\n                                    value as \"low\" | \"medium\" | \"high\"\n                                  )\n                                }\n                                options={[\n                                  {\n                                    value: \"low\",\n                                    label: t(\"form.thinking.levels.low\")\n                                  },\n                                  {\n                                    value: \"medium\",\n                                    label: t(\"form.thinking.levels.medium\")\n                                  },\n                                  {\n                                    value: \"high\",\n                                    label: t(\"form.thinking.levels.high\")\n                                  }\n                                ]}\n                                className=\"w-24\"\n                              />\n                            </div>\n                          ) : (\n                            // For other models: Show toggle (can enable/disable)\n                            <div className=\"inline-flex items-center gap-2\">\n                              <Tooltip title={t(\"tooltip.thinking\")}>\n                                <div className=\"inline-flex items-center gap-2\">\n                                  <Brain className=\"h-5 w-5 dark:text-gray-300\" />\n                                  <Switch\n                                    checked={!!thinking}\n                                    onChange={(e) => setThinking?.(e)}\n                                    checkedChildren={t(\"form.thinking.on\")}\n                                    unCheckedChildren={t(\"form.thinking.off\")}\n                                  />\n                                </div>\n                              </Tooltip>\n                            </div>\n                          ))}\n                      </div>\n                      <div\n                        className={`flex items-center gap-3 ${\n                          useCompactActions\n                            ? \"w-full justify-between md:w-auto md:justify-end\"\n                            : \"!justify-end\"\n                        }`}>\n                        {useCompactActions && (\n                          <Popover\n                            trigger=\"click\"\n                            placement=\"topRight\"\n                            open={isCompactActionsPopoverOpen}\n                            onOpenChange={setIsCompactActionsPopoverOpen}\n                            content={compactActionsPopoverContent}>\n                            <Tooltip title={t(\"common:more\", \"More\")}>\n                              <button\n                                type=\"button\"\n                                className=\"inline-flex items-center justify-center rounded-md border border-gray-300 p-1.5 dark:border-[#404040] dark:text-gray-300 md:hidden\">\n                                <PlusIcon className=\"h-4 w-4\" />\n                              </button>\n                            </Tooltip>\n                          </Popover>\n                        )}\n                        <div\n                          className={`flex items-center gap-3 ${\n                            useCompactActions ? \"ml-auto\" : \"\"\n                          }`}>\n                        <div\n                          className={`items-center gap-3 ${\n                            useCompactActions ? \"hidden md:flex\" : \"flex\"\n                          }`}>\n                          {history.length > 0 && (\n                            <Tooltip title={t(\"tooltip.clearContext\")}>\n                              <button\n                                type=\"button\"\n                                onClick={() => {\n                                  setHistory([])\n                                }}\n                                className={`flex items-center justify-center dark:text-gray-300 ${\n                                  chatMode === \"rag\" ? \"hidden\" : \"block\"\n                                }`}>\n                                <EraserIcon className=\"h-5 w-5\" />\n                              </button>\n                            </Tooltip>\n                          )}\n                          {!selectedKnowledge && (\n                            <Tooltip title={t(\"tooltip.uploadImage\")}>\n                              <button\n                                type=\"button\"\n                                onClick={() => {\n                                  inputRef.current?.click()\n                                }}\n                                className={`flex items-center justify-center dark:text-gray-300 ${\n                                  chatMode === \"rag\" ? \"hidden\" : \"block\"\n                                }`}>\n                                <ImageIcon className=\"h-5 w-5\" />\n                              </button>\n                            </Tooltip>\n                          )}\n                          {browserSupportsSpeechRecognition && (\n                            <Tooltip title={t(\"tooltip.speechToText\")}>\n                              <button\n                                type=\"button\"\n                                onClick={async () => {\n                                  if (isListening) {\n                                    stopSpeechRecognition()\n                                  } else {\n                                    resetTranscript()\n                                    startListening({\n                                      continuous: true,\n                                      lang: speechToTextLanguage\n                                    })\n                                  }\n                                }}\n                                className={`flex items-center justify-center dark:text-gray-300`}>\n                                {!isListening ? (\n                                  <MicIcon className=\"h-5 w-5\" />\n                                ) : (\n                                  <div className=\"relative\">\n                                    <span className=\"animate-ping absolute inline-flex h-3 w-3 rounded-full bg-red-400 opacity-75\"></span>\n                                    <MicIcon className=\"h-5 w-5\" />\n                                  </div>\n                                )}\n                              </button>\n                            </Tooltip>\n                          )}\n                        </div>\n                        <KnowledgeSelect />\n                        {showMcpServersInChat && <McpServerToggle />}\n                        <Tooltip title={t(\"tooltip.uploadDocuments\")}>\n                          <button\n                            type=\"button\"\n                            onClick={() => {\n                              if (useCompactActions) {\n                                combinedUploadInputRef.current?.click()\n                                return\n                              }\n                              fileInputRef.current?.click()\n                            }}\n                            className={`flex items-center justify-center dark:text-gray-300 ${\n                              useCompactActions ? \"p-1.5\" : \"\"\n                            }`}>\n                            <PaperclipIcon className=\"h-5 w-5\" />\n                          </button>\n                        </Tooltip>\n                        {useCompactActions && browserSupportsSpeechRecognition && (\n                          <Tooltip title={t(\"tooltip.speechToText\")}>\n                            <button\n                              type=\"button\"\n                              onClick={async () => {\n                                if (isListening) {\n                                  stopSpeechRecognition()\n                                } else {\n                                  resetTranscript()\n                                  startListening({\n                                    continuous: true,\n                                    lang: speechToTextLanguage\n                                  })\n                                }\n                              }}\n                              className=\"inline-flex items-center justify-center p-1.5 dark:text-gray-300 md:hidden\">\n                              {!isListening ? (\n                                <MicIcon className=\"h-5 w-5\" />\n                              ) : (\n                                <div className=\"relative\">\n                                  <span className=\"animate-ping absolute inline-flex h-3 w-3 rounded-full bg-red-400 opacity-75\"></span>\n                                  <MicIcon className=\"h-5 w-5\" />\n                                </div>\n                              )}\n                            </button>\n                          </Tooltip>\n                        )}\n\n                        {isSending && !enableMessageQueue ? (\n                          <Tooltip title={t(\"tooltip.stopStreaming\")}>\n                            <button\n                              type=\"button\"\n                              onClick={stopStreamingRequest}\n                              className=\"text-gray-800 dark:text-gray-300 border border-gray-300 dark:border-[#404040] rounded-md p-1\">\n                              <StopCircleIcon className=\"size-5\" />\n                            </button>{\" \"}\n                          </Tooltip>\n                        ) : (\n                          <div className=\"inline-flex items-center gap-2\">\n                            {isSending && (\n                              <Tooltip title={t(\"tooltip.stopStreaming\")}>\n                                <button\n                                  type=\"button\"\n                                  onClick={stopStreamingRequest}\n                                  className=\"text-gray-800 dark:text-gray-300 border border-gray-300 dark:border-[#404040] rounded-md p-1\">\n                                  <StopCircleIcon className=\"size-5\" />\n                                </button>\n                              </Tooltip>\n                            )}\n                            {useCompactActions ? (\n                              <Tooltip\n                                title={\n                                  isSending && enableMessageQueue\n                                    ? t(\"form.queue.add\", \"Queue\")\n                                    : t(\"common:submit\")\n                                }>\n                                <button\n                                  type=\"submit\"\n                                  disabled={isSending && !enableMessageQueue}\n                                  className=\"inline-flex h-9 w-9 items-center justify-center rounded-full bg-gray-900 text-white transition hover:bg-black disabled:opacity-60 dark:bg-white dark:text-black dark:hover:bg-gray-100\">\n                                  <ArrowUp className=\"h-4 w-4\" />\n                                </button>\n                              </Tooltip>\n                            ) : (\n                              <Dropdown.Button\n                                htmlType=\"submit\"\n                                disabled={isSending && !enableMessageQueue}\n                                className=\"!justify-end !w-auto\"\n                                icon={\n                                  <svg\n                                    xmlns=\"http://www.w3.org/2000/svg\"\n                                    fill=\"none\"\n                                    stroke=\"currentColor\"\n                                    strokeLinecap=\"round\"\n                                    strokeLinejoin=\"round\"\n                                    strokeWidth=\"2\"\n                                    className=\"w-5 h-5\"\n                                    viewBox=\"0 0 24 24\">\n                                    <path\n                                      strokeLinecap=\"round\"\n                                      strokeLinejoin=\"round\"\n                                      d=\"m19.5 8.25-7.5 7.5-7.5-7.5\"\n                                    />\n                                  </svg>\n                                }\n                                menu={{\n                                  items: [\n                                    {\n                                      key: 1,\n                                      label: (\n                                        <Checkbox\n                                          checked={sendWhenEnter}\n                                          onChange={(e) =>\n                                            setSendWhenEnter(e.target.checked)\n                                          }>\n                                          {t(\"sendWhenEnter\")}\n                                        </Checkbox>\n                                      )\n                                    },\n                                    {\n                                      key: 2,\n                                      label: (\n                                        <Checkbox\n                                          checked={useOCR}\n                                          onChange={(e) =>\n                                            setUseOCR(e.target.checked)\n                                          }>\n                                          {t(\"useOCR\")}\n                                        </Checkbox>\n                                      )\n                                    }\n                                  ]\n                                }}>\n                                <div className=\"inline-flex gap-2\">\n                                  {sendWhenEnter ? (\n                                    <svg\n                                      xmlns=\"http://www.w3.org/2000/svg\"\n                                      fill=\"none\"\n                                      stroke=\"currentColor\"\n                                      strokeLinecap=\"round\"\n                                      strokeLinejoin=\"round\"\n                                      strokeWidth=\"2\"\n                                      className=\"h-5 w-5\"\n                                      viewBox=\"0 0 24 24\">\n                                      <path d=\"M9 10L4 15 9 20\"></path>\n                                      <path d=\"M20 4v7a4 4 0 01-4 4H4\"></path>\n                                    </svg>\n                                  ) : null}\n                                  {isSending && enableMessageQueue\n                                    ? t(\"form.queue.add\", \"Queue\")\n                                    : t(\"common:submit\")}\n                                </div>\n                              </Dropdown.Button>\n                            )}\n                          </div>\n                        )}\n                        </div>\n                      </div>\n                    </div>\n                  </div>\n                </form>\n              </div>\n              {form.errors.message && (\n                <div className=\"text-red-500 text-center text-sm mt-1\">\n                  {form.errors.message}\n                </div>\n              )}\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Option/Playground/PlaygroundNewChat.tsx",
    "content": "import { PencilIcon } from \"lucide-react\"\nimport { useMessage } from \"../../../hooks/useMessage\"\nimport { useTranslation } from 'react-i18next';\n\nexport const PlaygroundNewChat = () => {\n  const { setHistory, setMessages, setHistoryId } = useMessage()\n  const { t } = useTranslation('optionChat')\n\n  const handleClick = () => {\n    setHistoryId(null)\n    setMessages([])\n    setHistory([])\n  }\n\n  return (\n    <button\n      onClick={handleClick}\n      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\">\n      <PencilIcon className=\"mx-3 h-5 w-5\" aria-hidden=\"true\" />\n      <span className=\"inline-flex font-semibol text-white text-sm\">\n        {t('newChat')}\n      </span>\n    </button>\n  )\n}\n"
  },
  {
    "path": "src/components/Option/Playground/PlaygroundSettings.tsx",
    "content": "import { Modal } from \"antd\"\nimport { useState } from \"react\"\n\nexport const PlaygroundSettings = () => {\n  const [open, setOpen] = useState(false)\n  return (\n    <div className=\"flex-shrink-0 flex flex-col items-center justify-center py-1 \">\n      <div className=\"flex items-center justify-center space-x-2\">\n        <button\n          onClick={() => setOpen(true)}\n          className=\"flex items-center justify-center w-8 h-8 rounded-full transition-colors duration-200 focus:outline-none\">\n          <svg\n            xmlns=\"http://www.w3.org/2000/svg\"\n            fill=\"none\"\n            viewBox=\"0 0 24 24\"\n            strokeWidth={1.5}\n            stroke=\"currentColor\"\n            className=\"w-5 h-5 text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200\">\n            <path\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              d=\"M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z\"\n            />\n            <path\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              d=\"M15 12a3 3 0 11-6 0 3 3 0 016 0z\"\n            />\n          </svg>\n        </button>\n      </div>\n\n      <Modal\n        footer={null}\n        title=\"Playground Settings\"\n        open={open}\n        onCancel={() => setOpen(false)}>\n            Nothing to see here\n      </Modal>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Option/Prompt/index.tsx",
    "content": "import { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\"\nimport {\n  Skeleton,\n  Table,\n  Tooltip,\n  notification,\n  Modal,\n  Input,\n  Form,\n  Switch,\n  Segmented,\n  Tag\n} from \"antd\"\nimport { Trash2, Pen, Computer, Zap } from \"lucide-react\"\nimport { useState } from \"react\"\nimport { useTranslation } from \"react-i18next\"\nimport {\n  deletePromptById,\n  getAllPrompts,\n  savePrompt,\n  updatePrompt\n} from \"@/db/dexie/helpers\"\nimport {\n  getAllCopilotPrompts,\n  setAllCopilotPrompts,\n  getCustomCopilotPrompts,\n  saveCustomCopilotPrompt,\n  updateCustomCopilotPrompt,\n  deleteCustomCopilotPrompt,\n  toggleCustomCopilotPrompt,\n  toggleCopilotPromptEnabled,\n  type CustomCopilotPrompt\n} from \"@/services/application\"\nimport { tagColors } from \"@/utils/color\"\nimport { isFireFoxPrivateMode } from \"@/utils/is-private-mode\"\n\nexport const PromptBody = () => {\n  const queryClient = useQueryClient()\n  const [open, setOpen] = useState(false)\n  const [openEdit, setOpenEdit] = useState(false)\n  const [editId, setEditId] = useState(\"\")\n  const [createForm] = Form.useForm()\n  const [editForm] = Form.useForm()\n  const { t } = useTranslation([\"settings\", \"common\"])\n  const [selectedSegment, setSelectedSegment] = useState<\"custom\" | \"copilot\" | \"custom-copilot\">(\n    \"custom\"\n  )\n\n  const [openCopilotEdit, setOpenCopilotEdit] = useState(false)\n  const [editCopilotId, setEditCopilotId] = useState(\"\")\n  const [editCopilotForm] = Form.useForm()\n\n  // Custom Copilot Prompts state\n  const [openCustomCopilot, setOpenCustomCopilot] = useState(false)\n  const [openEditCustomCopilot, setOpenEditCustomCopilot] = useState(false)\n  const [editCustomCopilotId, setEditCustomCopilotId] = useState(\"\")\n  const [createCustomCopilotForm] = Form.useForm()\n  const [editCustomCopilotForm] = Form.useForm()\n\n  const { data, status } = useQuery({\n    queryKey: [\"fetchAllPrompts\"],\n    queryFn: getAllPrompts\n  })\n\n  const { data: copilotData, status: copilotStatus } = useQuery({\n    queryKey: [\"fetchCopilotPrompts\"],\n    queryFn: getAllCopilotPrompts\n  })\n\n  const { data: customCopilotData, status: customCopilotStatus } = useQuery({\n    queryKey: [\"fetchCustomCopilotPrompts\"],\n    queryFn: getCustomCopilotPrompts\n  })\n\n  const { mutate: deletePrompt } = useMutation({\n    mutationFn: deletePromptById,\n    onSuccess: () => {\n      queryClient.invalidateQueries({\n        queryKey: [\"fetchAllPrompts\"]\n      })\n      notification.success({\n        message: t(\"managePrompts.notification.deletedSuccess\"),\n        description: t(\"managePrompts.notification.deletedSuccessDesc\")\n      })\n    },\n    onError: (error) => {\n      notification.error({\n        message: t(\"managePrompts.notification.error\"),\n        description: error?.message || t(\"managePrompts.notification.someError\")\n      })\n    }\n  })\n\n  const { mutate: savePromptMutation, isPending: savePromptLoading } =\n    useMutation({\n      mutationFn: savePrompt,\n      onSuccess: () => {\n        queryClient.invalidateQueries({\n          queryKey: [\"fetchAllPrompts\"]\n        })\n        setOpen(false)\n        createForm.resetFields()\n        notification.success({\n          message: t(\"managePrompts.notification.addSuccess\"),\n          description: t(\"managePrompts.notification.addSuccessDesc\")\n        })\n      },\n      onError: (error) => {\n        notification.error({\n          message: t(\"managePrompts.notification.error\"),\n          description:\n            error?.message || t(\"managePrompts.notification.someError\")\n        })\n      }\n    })\n\n  const { mutate: updatePromptMutation, isPending: isUpdatingPrompt } =\n    useMutation({\n      mutationFn: async (data: any) => {\n        return await updatePrompt({\n          ...data,\n          id: editId\n        })\n      },\n      onSuccess: () => {\n        queryClient.invalidateQueries({\n          queryKey: [\"fetchAllPrompts\"]\n        })\n        setOpenEdit(false)\n        editForm.resetFields()\n        notification.success({\n          message: t(\"managePrompts.notification.updatedSuccess\"),\n          description: t(\"managePrompts.notification.updatedSuccessDesc\")\n        })\n      },\n      onError: (error) => {\n        notification.error({\n          message: t(\"managePrompts.notification.error\"),\n          description:\n            error?.message || t(\"managePrompts.notification.someError\")\n        })\n      }\n    })\n\n  const { mutate: updateCopilotPrompt, isPending: isUpdatingCopilotPrompt } =\n    useMutation({\n      mutationFn: async (data: any) => {\n        return await setAllCopilotPrompts([\n          {\n            key: data.key,\n            prompt: data.prompt\n          }\n        ])\n      },\n      onSuccess: () => {\n        queryClient.invalidateQueries({\n          queryKey: [\"fetchCopilotPrompts\"]\n        })\n        setOpenCopilotEdit(false)\n        editCopilotForm.resetFields()\n        notification.success({\n          message: t(\"managePrompts.notification.updatedSuccess\"),\n          description: t(\"managePrompts.notification.updatedSuccessDesc\")\n        })\n      },\n      onError: (error) => {\n        notification.error({\n          message: t(\"managePrompts.notification.error\"),\n          description:\n            error?.message || t(\"managePrompts.notification.someError\")\n        })\n      }\n    })\n\n  // Custom Copilot Prompts mutations\n  const { mutate: saveCustomCopilotMutation, isPending: isSavingCustomCopilot } =\n    useMutation({\n      mutationFn: saveCustomCopilotPrompt,\n      onSuccess: () => {\n        queryClient.invalidateQueries({\n          queryKey: [\"fetchCustomCopilotPrompts\"]\n        })\n        setOpenCustomCopilot(false)\n        createCustomCopilotForm.resetFields()\n        notification.success({\n          message: t(\"managePrompts.notification.addSuccess\"),\n          description: t(\"managePrompts.notification.addSuccessDesc\")\n        })\n      },\n      onError: (error) => {\n        notification.error({\n          message: t(\"managePrompts.notification.error\"),\n          description:\n            error?.message || t(\"managePrompts.notification.someError\")\n        })\n      }\n    })\n\n  const { mutate: updateCustomCopilotMutation, isPending: isUpdatingCustomCopilot } =\n    useMutation({\n      mutationFn: async (data: { id: string; title: string; prompt: string }) => {\n        return await updateCustomCopilotPrompt(data.id, {\n          title: data.title,\n          prompt: data.prompt\n        })\n      },\n      onSuccess: () => {\n        queryClient.invalidateQueries({\n          queryKey: [\"fetchCustomCopilotPrompts\"]\n        })\n        setOpenEditCustomCopilot(false)\n        editCustomCopilotForm.resetFields()\n        notification.success({\n          message: t(\"managePrompts.notification.updatedSuccess\"),\n          description: t(\"managePrompts.notification.updatedSuccessDesc\")\n        })\n      },\n      onError: (error) => {\n        notification.error({\n          message: t(\"managePrompts.notification.error\"),\n          description:\n            error?.message || t(\"managePrompts.notification.someError\")\n        })\n      }\n    })\n\n  const { mutate: deleteCustomCopilotMutation } = useMutation({\n    mutationFn: deleteCustomCopilotPrompt,\n    onSuccess: () => {\n      queryClient.invalidateQueries({\n        queryKey: [\"fetchCustomCopilotPrompts\"]\n      })\n      notification.success({\n        message: t(\"managePrompts.notification.deletedSuccess\"),\n        description: t(\"managePrompts.notification.deletedSuccessDesc\")\n      })\n    },\n    onError: (error) => {\n      notification.error({\n        message: t(\"managePrompts.notification.error\"),\n        description: error?.message || t(\"managePrompts.notification.someError\")\n      })\n    }\n  })\n\n  const { mutate: toggleCustomCopilotMutation } = useMutation({\n    mutationFn: ({ id, enabled }: { id: string; enabled: boolean }) =>\n      toggleCustomCopilotPrompt(id, enabled),\n    onSuccess: () => {\n      queryClient.invalidateQueries({\n        queryKey: [\"fetchCustomCopilotPrompts\"]\n      })\n    },\n    onError: (error) => {\n      notification.error({\n        message: t(\"managePrompts.notification.error\"),\n        description: error?.message || t(\"managePrompts.notification.someError\")\n      })\n    }\n  })\n\n  const { mutate: toggleBuiltinCopilotMutation } = useMutation({\n    mutationFn: ({ key, enabled }: { key: string; enabled: boolean }) =>\n      toggleCopilotPromptEnabled(key, enabled),\n    onSuccess: () => {\n      queryClient.invalidateQueries({\n        queryKey: [\"fetchCopilotPrompts\"]\n      })\n    },\n    onError: (error) => {\n      notification.error({\n        message: t(\"managePrompts.notification.error\"),\n        description: error?.message || t(\"managePrompts.notification.someError\")\n      })\n    }\n  })\n\n  function customPrompts() {\n    return (\n      <div>\n        <div className=\"mb-6\">\n          <div className=\"-ml-4 -mt-2 flex flex-wrap items-center justify-end sm:flex-nowrap\">\n            <div className=\"ml-4 mt-2 flex-shrink-0\">\n              <button\n                onClick={() => {\n                  if (isFireFoxPrivateMode) {\n                    notification.error({\n                      message: \"Page Assist can't save data\",\n                      description:\n                        \"Firefox Private Mode does not support saving data to IndexedDB. Please add prompts from a normal window.\"\n                    })\n                    return\n                  }\n                  setOpen(true)\n                }}\n                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\">\n                {t(\"managePrompts.addBtn\")}\n              </button>\n            </div>\n          </div>\n        </div>\n\n        {status === \"pending\" && <Skeleton paragraph={{ rows: 8 }} />}\n\n        {status === \"success\" && (\n          <Table\n            columns={[\n              {\n                title: t(\"managePrompts.columns.title\"),\n                dataIndex: \"title\",\n                key: \"title\",\n                render: (content) => (\n                  <span className=\"line-clamp-1\">{content}</span>\n                )\n              },\n              {\n                title: t(\"managePrompts.columns.prompt\"),\n                dataIndex: \"content\",\n                key: \"content\",\n                render: (content) => (\n                  <span className=\"line-clamp-1\">{content}</span>\n                )\n              },\n              {\n                title: t(\"managePrompts.columns.type\"),\n                dataIndex: \"is_system\",\n                key: \"is_system\",\n                render: (is_system) => (\n                  <span className=\"flex items-center gap-2 text-xs w-32\">\n                    {is_system ? (\n                      <>\n                        <Computer className=\"size-4\" />{\" \"}\n                        {t(\"managePrompts.systemPrompt\")}\n                      </>\n                    ) : (\n                      <>\n                        <Zap className=\"size-4\" />{\" \"}\n                        {t(\"managePrompts.quickPrompt\")}\n                      </>\n                    )}\n                  </span>\n                )\n              },\n              {\n                title: t(\"managePrompts.columns.actions\"),\n                render: (_, record) => (\n                  <div className=\"flex gap-4\">\n                    <Tooltip title={t(\"managePrompts.tooltip.delete\")}>\n                      <button\n                        onClick={() => {\n                          if (\n                            window.confirm(t(\"managePrompts.confirm.delete\"))\n                          ) {\n                            deletePrompt(record.id)\n                          }\n                        }}\n                        disabled={isFireFoxPrivateMode}\n                        className=\"text-red-500 dark:text-red-400 disabled:opacity-50\">\n                        <Trash2 className=\"size-4\" />\n                      </button>\n                    </Tooltip>\n                    <Tooltip title={t(\"managePrompts.tooltip.edit\")}>\n                      <button\n                        onClick={() => {\n                          setEditId(record.id)\n                          editForm.setFieldsValue(record)\n                          setOpenEdit(true)\n                        }}\n                        disabled={isFireFoxPrivateMode}\n                        className=\"text-gray-500 dark:text-gray-400 disabled:opacity-50\">\n                        <Pen className=\"size-4\" />\n                      </button>\n                    </Tooltip>\n                  </div>\n                )\n              }\n            ]}\n            bordered\n            dataSource={data}\n            rowKey={(record) => record.id}\n          />\n        )}\n      </div>\n    )\n  }\n\n  function copilotPrompts() {\n    return (\n      <div>\n        <div className=\"mb-4 p-4 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-md\">\n          <p className=\"text-yellow-800 dark:text-yellow-200 text-sm\">\n            ⚠️ Copilot prompts are deprecated. Please use Custom Copilot instead. Copilot prompts will be removed in a future version.\n          </p>\n        </div>\n\n        {copilotStatus === \"pending\" && <Skeleton paragraph={{ rows: 8 }} />}\n\n        {copilotStatus === \"success\" && (\n          <Table\n            columns={[\n              {\n                title: t(\"managePrompts.columns.title\"),\n                dataIndex: \"key\",\n                key: \"key\",\n                render: (content) => (\n                  <span className=\"line-clamp-1\">\n                    <Tag color={tagColors[content || \"default\"]}>\n                      {t(`common:copilot.${content}`)}\n                    </Tag>\n                  </span>\n                )\n              },\n              {\n                title: t(\"managePrompts.columns.prompt\"),\n                dataIndex: \"prompt\",\n                key: \"prompt\",\n                render: (content) => (\n                  <span className=\"line-clamp-1\">{content}</span>\n                )\n              },\n              {\n                title: \"Enabled\",\n                dataIndex: \"enabled\",\n                key: \"enabled\",\n                render: (enabled, record) => (\n                  <Switch\n                    checked={enabled}\n                    onChange={(checked) =>\n                      toggleBuiltinCopilotMutation({ key: record.key, enabled: checked })\n                    }\n                  />\n                )\n              },\n              {\n                render: (_, record) => (\n                  <div className=\"flex gap-4\">\n                    <Tooltip title={t(\"managePrompts.tooltip.edit\")}>\n                      <button\n                        onClick={() => {\n                          setEditCopilotId(record.key)\n                          editCopilotForm.setFieldsValue(record)\n                          setOpenCopilotEdit(true)\n                        }}\n                        className=\"text-gray-500 dark:text-gray-400\">\n                        <Pen className=\"size-4\" />\n                      </button>\n                    </Tooltip>\n                  </div>\n                )\n              }\n            ]}\n            bordered\n            dataSource={copilotData}\n            rowKey={(record) => record.key}\n          />\n        )}\n      </div>\n    )\n  }\n\n  function customCopilotPrompts() {\n    return (\n      <div>\n        <div className=\"mb-6\">\n          <div className=\"-ml-4 -mt-2 flex flex-wrap items-center justify-end sm:flex-nowrap\">\n            <div className=\"ml-4 mt-2 flex-shrink-0\">\n              <button\n                onClick={() => setOpenCustomCopilot(true)}\n                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\">\n                {t(\"managePrompts.addBtn\")}\n              </button>\n            </div>\n          </div>\n        </div>\n\n        {customCopilotStatus === \"pending\" && <Skeleton paragraph={{ rows: 8 }} />}\n\n        {customCopilotStatus === \"success\" && (\n          <Table\n            columns={[\n              {\n                title: t(\"managePrompts.columns.title\"),\n                dataIndex: \"title\",\n                key: \"title\",\n                render: (content) => (\n                  <span className=\"line-clamp-1\">{content}</span>\n                )\n              },\n              {\n                title: t(\"managePrompts.columns.prompt\"),\n                dataIndex: \"prompt\",\n                key: \"prompt\",\n                render: (content) => (\n                  <span className=\"line-clamp-1\">{content}</span>\n                )\n              },\n              {\n                title: \"Enabled\",\n                dataIndex: \"enabled\",\n                key: \"enabled\",\n                render: (enabled, record) => (\n                  <Switch\n                    checked={enabled}\n                    onChange={(checked) =>\n                      toggleCustomCopilotMutation({ id: record.id, enabled: checked })\n                    }\n                  />\n                )\n              },\n              {\n                title: t(\"managePrompts.columns.actions\"),\n                render: (_, record) => (\n                  <div className=\"flex gap-4\">\n                    <Tooltip title={t(\"managePrompts.tooltip.delete\")}>\n                      <button\n                        onClick={() => {\n                          if (\n                            window.confirm(t(\"managePrompts.confirm.delete\"))\n                          ) {\n                            deleteCustomCopilotMutation(record.id)\n                          }\n                        }}\n                        className=\"text-red-500 dark:text-red-400\">\n                        <Trash2 className=\"size-4\" />\n                      </button>\n                    </Tooltip>\n                    <Tooltip title={t(\"managePrompts.tooltip.edit\")}>\n                      <button\n                        onClick={() => {\n                          setEditCustomCopilotId(record.id)\n                          editCustomCopilotForm.setFieldsValue(record)\n                          setOpenEditCustomCopilot(true)\n                        }}\n                        className=\"text-gray-500 dark:text-gray-400\">\n                        <Pen className=\"size-4\" />\n                      </button>\n                    </Tooltip>\n                  </div>\n                )\n              }\n            ]}\n            bordered\n            dataSource={customCopilotData}\n            rowKey={(record) => record.id}\n          />\n        )}\n      </div>\n    )\n  }\n\n  return (\n    <div>\n      <div className=\"flex items-center justify-end mb-6\">\n        <Segmented\n          size=\"large\"\n          options={[\n            {\n              label: t(\"managePrompts.segmented.custom\"),\n              value: \"custom\"\n            },\n            {\n              label: \"Custom Copilot\",\n              value: \"custom-copilot\"\n            },\n            {\n              label: t(\"managePrompts.segmented.copilot\"),\n              value: \"copilot\"\n            },\n          ]}\n          onChange={(value) => {\n            setSelectedSegment(value as \"custom\" | \"copilot\" | \"custom-copilot\")\n          }}\n        />\n      </div>\n      {selectedSegment === \"custom\" && customPrompts()}\n      {selectedSegment === \"copilot\" && copilotPrompts()}\n      {selectedSegment === \"custom-copilot\" && customCopilotPrompts()}\n\n      <Modal\n        title={t(\"managePrompts.modal.addTitle\")}\n        open={open}\n        onCancel={() => setOpen(false)}\n        footer={null}>\n        <Form\n          onFinish={(values) => savePromptMutation(values)}\n          layout=\"vertical\"\n          form={createForm}>\n          <Form.Item\n            name=\"title\"\n            label={t(\"managePrompts.form.title.label\")}\n            rules={[\n              {\n                required: true,\n                message: t(\"managePrompts.form.title.required\")\n              }\n            ]}>\n            <Input placeholder={t(\"managePrompts.form.title.placeholder\")} />\n          </Form.Item>\n\n          <Form.Item\n            name=\"content\"\n            label={t(\"managePrompts.form.prompt.label\")}\n            rules={[\n              {\n                required: true,\n                message: t(\"managePrompts.form.prompt.required\")\n              }\n            ]}\n            help={t(\"managePrompts.form.prompt.help\")}>\n            <Input.TextArea\n              placeholder={t(\"managePrompts.form.prompt.placeholder\")}\n              autoSize={{ minRows: 3, maxRows: 10 }}\n            />\n          </Form.Item>\n\n          <Form.Item\n            name=\"is_system\"\n            label={t(\"managePrompts.form.isSystem.label\")}\n            valuePropName=\"checked\">\n            <Switch />\n          </Form.Item>\n\n          <Form.Item>\n            <button\n              disabled={savePromptLoading}\n              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 \">\n              {savePromptLoading\n                ? t(\"managePrompts.form.btnSave.saving\")\n                : t(\"managePrompts.form.btnSave.save\")}\n            </button>\n          </Form.Item>\n        </Form>\n      </Modal>\n\n      <Modal\n        title={t(\"managePrompts.modal.editTitle\")}\n        open={openEdit}\n        onCancel={() => setOpenEdit(false)}\n        footer={null}>\n        <Form\n          onFinish={(values) => updatePromptMutation(values)}\n          layout=\"vertical\"\n          form={editForm}>\n          <Form.Item\n            name=\"title\"\n            label={t(\"managePrompts.form.title.label\")}\n            rules={[\n              {\n                required: true,\n                message: t(\"managePrompts.form.title.required\")\n              }\n            ]}>\n            <Input placeholder={t(\"managePrompts.form.title.placeholder\")} />\n          </Form.Item>\n\n          <Form.Item\n            name=\"content\"\n            label={t(\"managePrompts.form.prompt.label\")}\n            rules={[\n              {\n                required: true,\n                message: t(\"managePrompts.form.prompt.required\")\n              }\n            ]}\n            help={t(\"managePrompts.form.prompt.help\")}>\n            <Input.TextArea\n              placeholder={t(\"managePrompts.form.prompt.placeholder\")}\n              autoSize={{ minRows: 3, maxRows: 10 }}\n            />\n          </Form.Item>\n\n          <Form.Item\n            name=\"is_system\"\n            label={t(\"managePrompts.form.isSystem.label\")}\n            valuePropName=\"checked\">\n            <Switch />\n          </Form.Item>\n\n          <Form.Item>\n            <button\n              disabled={isUpdatingPrompt}\n              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 \">\n              {isUpdatingPrompt\n                ? t(\"managePrompts.form.btnEdit.saving\")\n                : t(\"managePrompts.form.btnEdit.save\")}\n            </button>\n          </Form.Item>\n        </Form>\n      </Modal>\n\n      <Modal\n        title={t(\"managePrompts.modal.editTitle\")}\n        open={openCopilotEdit}\n        onCancel={() => setOpenCopilotEdit(false)}\n        footer={null}>\n        <Form\n          onFinish={(values) =>\n            updateCopilotPrompt({\n              key: editCopilotId,\n              prompt: values.prompt\n            })\n          }\n          layout=\"vertical\"\n          form={editCopilotForm}>\n          <Form.Item\n            name=\"prompt\"\n            label={t(\"managePrompts.form.prompt.label\")}\n            rules={[\n              {\n                required: true,\n                message: t(\"managePrompts.form.prompt.required\")\n              },\n              {\n                validator: (_, value) => {\n                  if (value && value.includes(\"{text}\")) {\n                    return Promise.resolve()\n                  }\n                  return Promise.reject(\n                    new Error(\n                      t(\"managePrompts.form.prompt.missingTextPlaceholder\")\n                    )\n                  )\n                }\n              }\n            ]}>\n            <Input.TextArea\n              placeholder={t(\"managePrompts.form.prompt.placeholder\")}\n              autoSize={{ minRows: 3, maxRows: 10 }}\n            />\n          </Form.Item>\n\n          <Form.Item>\n            <button\n              disabled={isUpdatingCopilotPrompt}\n              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 \">\n              {isUpdatingCopilotPrompt\n                ? t(\"managePrompts.form.btnEdit.saving\")\n                : t(\"managePrompts.form.btnEdit.save\")}\n            </button>\n          </Form.Item>\n        </Form>\n      </Modal>\n\n      {/* Custom Copilot Prompts Modals */}\n      <Modal\n        title=\"Add Custom Copilot Prompt\"\n        open={openCustomCopilot}\n        onCancel={() => setOpenCustomCopilot(false)}\n        footer={null}>\n        <Form\n          onFinish={(values) => saveCustomCopilotMutation(values)}\n          layout=\"vertical\"\n          form={createCustomCopilotForm}>\n          <Form.Item\n            name=\"title\"\n            label=\"Title\"\n            rules={[\n              {\n                required: true,\n                message: \"Please enter a title\"\n              }\n            ]}\n            help=\"This will appear in the context menu\">\n            <Input placeholder=\"e.g. Simplify Text\" />\n          </Form.Item>\n\n          <Form.Item\n            name=\"prompt\"\n            label=\"Prompt Template\"\n            rules={[\n              {\n                required: true,\n                message: \"Please enter a prompt\"\n              },\n              {\n                validator: (_, value) => {\n                  if (value && value.includes(\"{text}\")) {\n                    return Promise.resolve()\n                  }\n                  return Promise.reject(\n                    new Error(\"Prompt must include {text} placeholder\")\n                  )\n                }\n              }\n            ]}\n            help=\"Use {text} as placeholder for selected text\">\n            <Input.TextArea\n              placeholder=\"e.g. Simplify the following text:\\n\\n{text}\"\n              autoSize={{ minRows: 3, maxRows: 10 }}\n            />\n          </Form.Item>\n\n          <Form.Item>\n            <button\n              disabled={isSavingCustomCopilot}\n              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 \">\n              {isSavingCustomCopilot ? \"Saving...\" : \"Save\"}\n            </button>\n          </Form.Item>\n        </Form>\n      </Modal>\n\n      <Modal\n        title=\"Edit Custom Copilot Prompt\"\n        open={openEditCustomCopilot}\n        onCancel={() => setOpenEditCustomCopilot(false)}\n        footer={null}>\n        <Form\n          onFinish={(values) =>\n            updateCustomCopilotMutation({\n              id: editCustomCopilotId,\n              title: values.title,\n              prompt: values.prompt\n            })\n          }\n          layout=\"vertical\"\n          form={editCustomCopilotForm}>\n          <Form.Item\n            name=\"title\"\n            label=\"Title\"\n            rules={[\n              {\n                required: true,\n                message: \"Please enter a title\"\n              }\n            ]}\n            help=\"This will appear in the context menu\">\n            <Input placeholder=\"e.g. Simplify Text\" />\n          </Form.Item>\n\n          <Form.Item\n            name=\"prompt\"\n            label=\"Prompt Template\"\n            rules={[\n              {\n                required: true,\n                message: \"Please enter a prompt\"\n              },\n              {\n                validator: (_, value) => {\n                  if (value && value.includes(\"{text}\")) {\n                    return Promise.resolve()\n                  }\n                  return Promise.reject(\n                    new Error(\"Prompt must include {text} placeholder\")\n                  )\n                }\n              }\n            ]}\n            help=\"Use {text} as placeholder for selected text\">\n            <Input.TextArea\n              placeholder=\"e.g. Simplify the following text:\\n\\n{text}\"\n              autoSize={{ minRows: 3, maxRows: 10 }}\n            />\n          </Form.Item>\n\n          <Form.Item>\n            <button\n              disabled={isUpdatingCustomCopilot}\n              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 \">\n              {isUpdatingCustomCopilot ? \"Saving...\" : \"Save\"}\n            </button>\n          </Form.Item>\n        </Form>\n      </Modal>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Option/Settings/about.tsx",
    "content": "import { getOllamaURL } from \"~/services/ollama\"\nimport { useTranslation } from \"react-i18next\"\nimport { useQuery } from \"@tanstack/react-query\"\nimport { Skeleton } from \"antd\"\nimport { cleanUrl } from \"@/libs/clean-url\"\nimport { Descriptions } from \"antd\"\nimport fetcher from \"@/libs/fetcher\"\n\nexport const AboutApp = () => {\n  const { t } = useTranslation(\"settings\")\n\n  const { data, status } = useQuery({\n    queryKey: [\"fetchOllamURL\"],\n    queryFn: async () => {\n      const chromeVersion = browser.runtime.getManifest().version\n      try {\n        const url = await getOllamaURL()\n        const req = await fetcher(`${cleanUrl(url)}/api/version`)\n\n        if (!req.ok) {\n          return {\n            ollama: \"N/A\",\n            chromeVersion\n          }\n        }\n\n        const res = (await req.json()) as { version: string }\n        return {\n          ollama: res.version,\n          chromeVersion\n        }\n      } catch {\n        return {\n          ollama: \"N/A\",\n          chromeVersion\n        }\n      }\n    }\n  })\n\n  return (\n    <div className=\"flex flex-col space-y-3\">\n      {status === \"pending\" && <Skeleton paragraph={{ rows: 4 }} active />}\n      {status === \"success\" && (\n        <div className=\"flex flex-col space-y-4\">\n          <Descriptions\n            title={t(\"about.heading\")}\n            column={1}\n            size=\"middle\"\n            items={[\n              {\n                key: 1,\n                label: t(\"about.chromeVersion\"),\n                children: data.chromeVersion\n              },\n              {\n                key: 1,\n                label: t(\"about.ollamaVersion\"),\n                children: data.ollama\n              },\n              {\n                key: 2,\n                label: \"Community\",\n                children: (\n                  <a\n                    href=\"https://discord.com/invite/bu54382uBd\"\n                    target=\"_blank\"\n                    rel=\"noreferrer\"\n                    className=\"text-blue-500 dark:text-blue-400\">\n                    Discord Server\n                  </a>\n                )\n              },\n              {\n                key: 3,\n                label: \"X (formerly Twitter)\",\n                children: (\n                  <a\n                    href=\"https://twitter.com/page_assist\"\n                    target=\"_blank\"\n                    rel=\"noreferrer\"\n                    className=\"text-blue-500 dark:text-blue-400\">\n                    @page_assist\n                  </a>\n                )\n              }\n            ]}\n          />\n          <div>\n            <p className=\"text-sm text-gray-700 dark:text-gray-400 mb-4\">\n              {t(\"about.support\")}\n            </p>\n\n            <div className=\"flex gap-2\">\n              <a\n                href=\"https://ko-fi.com/n4ze3m\"\n                target=\"_blank\"\n                rel=\"noreferrer\"\n                className=\"text-blue-500 dark:text-blue-400 border dark:border-gray-600 px-2.5 py-2 rounded-md\">\n                {t(\"about.koFi\")}\n              </a>\n\n              <a\n                href=\"https://github.com/sponsors/n4ze3m\"\n                target=\"_blank\"\n                rel=\"noreferrer\"\n                className=\"text-blue-500 dark:text-blue-400 border dark:border-gray-600 px-2.5 py-2 rounded-md\">\n                {t(\"about.githubSponsor\")}\n              </a>\n            </div>\n          </div>\n        </div>\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Option/Settings/chrome.tsx",
    "content": "import { useStorage } from \"@plasmohq/storage/hook\"\nimport { useQuery } from \"@tanstack/react-query\"\nimport { Alert, Skeleton, Switch, Modal, Progress, message } from \"antd\"\nimport { useTranslation } from \"react-i18next\"\nimport { getChromeAISupported } from \"@/utils/chrome\"\nimport Markdown from \"@/components/Common/Markdown\"\nimport { downloadChromeAIModel } from \"@/utils/chrome-download\"\nimport { useState } from \"react\"\n\nexport const ChromeApp = () => {\n  const { t } = useTranslation(\"chrome\")\n  const [chromeAIStatus, setChromeAIStatus] = useStorage(\n    \"chromeAIStatus\",\n    false\n  )\n  const [selectedModel, setSelectedModel] = useStorage(\"selectedModel\")\n  const [showWarningModal, setShowWarningModal] = useState(false)\n  const [isDownloading, setIsDownloading] = useState(false)\n  const [downloadProgress, setDownloadProgress] = useState(0)\n\n  const { status, data, refetch } = useQuery({\n    queryKey: [\"fetchChromeAIInfo\"],\n    queryFn: async () => {\n      const data = await getChromeAISupported()\n      return data\n    }\n  })\n\n  const handleDownloadModel = async () => {\n    try {\n      setIsDownloading(true)\n      setDownloadProgress(0)\n      setShowWarningModal(false)\n\n      await downloadChromeAIModel((progress) => {\n        console.log(\"Download progress:\", progress) \n        const percentage = Math.round(progress.loaded * 100)\n        setDownloadProgress(percentage)\n      })\n\n      message.success(t(\"downloadSuccess\"))\n      setIsDownloading(false)\n      setDownloadProgress(0)\n\n      // Refetch to update the UI\n      refetch()\n    } catch (error) {\n      console.error(\"Download failed:\", error)\n      message.error(t(\"downloadError\"))\n      setIsDownloading(false)\n      setDownloadProgress(0)\n    }\n  }\n  return (\n    <div className=\"flex flex-col space-y-3\">\n      {status === \"pending\" && <Skeleton paragraph={{ rows: 4 }} active />}\n      {status === \"success\" && (\n        <div className=\"flex flex-col space-y-6\">\n          <div>\n            <div>\n              <h2 className=\"text-base font-semibold leading-7 text-gray-900 dark:text-white\">\n                {t(\"heading\")}\n              </h2>\n              <div className=\"border border-b border-gray-200 dark:border-gray-600 mt-3 mb-6\"></div>\n            </div>\n\n            {[\"downloadable\", \"downloading\"].includes(data) ? (\n              <div className=\"flex mb-3 flex-row justify-between\">\n                <div className=\"inline-flex items-center gap-2\">\n                  <span className=\"text-gray-700 text-sm dark:text-neutral-50\">\n                    {t(\"downloadModel.label\", {\n                      defaultValue: \"Download Gemini Nano Model (approx. 4GB)\"\n                    })}\n                  </span>\n                </div>\n                <button\n                  onClick={() => setShowWarningModal(true)}\n                  disabled={isDownloading}\n                  className=\"px-4 py-2 rounded-md font-medium transition-colors duration-200 \n                    dark:bg-white dark:text-black bg-black text-white hover:opacity-90 disabled:opacity-50\">\n                  {isDownloading ? `${downloadProgress}%` : t(\"downloadModel\")}\n                </button>\n              </div>\n            ) : null}\n\n            <div className=\"flex mb-3 flex-row justify-between\">\n              <div className=\"inline-flex items-center gap-2\">\n                <span className=\"text-gray-700 text-sm dark:text-neutral-50\">\n                  {t(\"status.label\")}\n                </span>\n              </div>\n\n              <Switch\n                disabled={data !== \"success\"}\n                checked={chromeAIStatus}\n                onChange={(value) => {\n                  setChromeAIStatus(value)\n                  if (\n                    !value &&\n                    selectedModel === \"chrome::gemini-nano::page-assist\"\n                  ) {\n                    setSelectedModel(null)\n                  }\n                }}\n              />\n            </div>\n            {data !== \"success\" && (\n              <div className=\"space-y-3\">\n                {![\"downloadable\", \"downloading\"].includes(data) ? (\n                  <>\n                    <Alert message={t(`error.${data}`)} type=\"error\" showIcon />\n                    <div className=\"w-full\">\n                      <Markdown\n                        className=\"text-sm text-gray-700 dark:text-neutral-50 leading-7 text-justify\"\n                        message={t(\"errorDescription\")}\n                      />\n                    </div>\n                  </>\n                ) : null}\n              </div>\n            )}\n          </div>\n        </div>\n      )}\n\n      {/* Warning Modal */}\n      <Modal\n        title={t(\"downloadModal.title\")}\n        open={showWarningModal}\n        onOk={handleDownloadModel}\n        onCancel={() => setShowWarningModal(false)}\n        okText={t(\"downloadModal.confirm\")}\n        cancelText={t(\"downloadModal.cancel\")}\n        okButtonProps={{ loading: isDownloading }}\n        cancelButtonProps={{ disabled: isDownloading }}>\n        <div className=\"space-y-4\">\n          <p className=\"text-sm text-gray-600 dark:text-gray-300\">\n            {t(\"modelDownloadWarning\")}\n          </p>\n        </div>\n      </Modal>\n\n      {/* Download Progress Modal */}\n      <Modal\n        title={t(\"downloadModal.downloading\")}\n        open={isDownloading}\n        footer={null}\n        closable={false}\n        maskClosable={false}>\n        <div className=\"space-y-4\">\n          <p className=\"text-sm text-gray-600 dark:text-gray-300\">\n            {t(\"downloadModal.downloadingDescription\")}\n          </p>\n          <Progress\n            percent={downloadProgress}\n            status={downloadProgress === 100 ? \"success\" : \"active\"}\n            strokeColor={{\n              \"0%\": \"#108ee9\",\n              \"100%\": \"#87d068\"\n            }}\n          />\n          <p className=\"text-xs text-gray-500 dark:text-gray-400 text-center\">\n            {t(\"downloadModal.pleaseWait\")}\n          </p>\n        </div>\n      </Modal>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Option/Settings/general-settings.tsx",
    "content": "import { useDarkMode } from \"~/hooks/useDarkmode\"\nimport { Select, Switch } from \"antd\"\nimport { MoonIcon, SunIcon } from \"lucide-react\"\nimport { SearchModeSettings } from \"./search-mode\"\nimport { useTranslation } from \"react-i18next\"\nimport { useI18n } from \"@/hooks/useI18n\"\nimport { TTSModeSettings } from \"./tts-mode\"\nimport { useStorage } from \"@plasmohq/storage/hook\"\nimport { SystemSettings } from \"./system-settings\"\nimport { SSTSettings } from \"./sst-settings\"\nimport { BetaTag } from \"@/components/Common/Beta\"\nimport { getDefaultOcrLanguage, ocrLanguages } from \"@/data/ocr-language\"\nimport { Storage } from \"@plasmohq/storage\"\nimport { useQuery } from \"@tanstack/react-query\"\nimport { getAllPrompts, getAllPromptsSystem } from \"@/db/dexie/helpers\"\n\nexport const GeneralSettings = () => {\n  const [userChatBubble, setUserChatBubble] = useStorage(\"userChatBubble\", true)\n\n  const [defaultCopilotPrompt, setDefaultCopilotPrompt] = useStorage(\n    \"defaultCopilotPrompt\",\n    undefined\n  )\n\n  const [defaultWebUIPrompt, setDefaultWebUIPrompt] = useStorage(\n    \"defaultWebUIPrompt\",\n    undefined\n  )\n\n  const [copilotResumeLastChat, setCopilotResumeLastChat] = useStorage(\n    \"copilotResumeLastChat\",\n    false\n  )\n\n  const [webUIResumeLastChat, setWebUIResumeLastChat] = useStorage(\n    \"webUIResumeLastChat\",\n    false\n  )\n  const [defaultChatWithWebsite, setDefaultChatWithWebsite] = useStorage(\n    \"defaultChatWithWebsite\",\n    false\n  )\n\n  const [restoreLastChatModel, setRestoreLastChatModel] = useStorage(\n    \"restoreLastChatModel\",\n    false\n  )\n\n  const [copyAsFormattedText, setCopyAsFormattedText] = useStorage(\n    \"copyAsFormattedText\",\n    false\n  )\n\n  const [autoCopyResponseToClipboard, setAutoCopyResponseToClipboard] =\n    useStorage(\"autoCopyResponseToClipboard\", false)\n\n  const [generateTitle, setGenerateTitle] = useStorage(\"titleGenEnabled\", false)\n\n  const [hideCurrentChatModelSettings, setHideCurrentChatModelSettings] =\n    useStorage(\"hideCurrentChatModelSettings\", false)\n\n  const [sendNotificationAfterIndexing, setSendNotificationAfterIndexing] =\n    useStorage(\"sendNotificationAfterIndexing\", false)\n\n  const [checkOllamaStatus, setCheckOllamaStatus] = useStorage(\n    \"checkOllamaStatus\",\n    true\n  )\n\n  const [checkWideMode, setCheckWideMode] = useStorage(\"checkWideMode\", false)\n\n  const [openReasoning, setOpenReasoning] = useStorage(\"openReasoning\", false)\n\n  const [defaultThinkingMode, setDefaultThinkingMode] = useStorage(\n    \"defaultThinkingMode\",\n    false\n  )\n\n  const [useMarkdownForUserMessage, setUseMarkdownForUserMessage] = useStorage(\n    \"useMarkdownForUserMessage\",\n    false\n  )\n\n  const [tabMentionsEnabled, setTabMentionsEnabled] = useStorage(\n    \"tabMentionsEnabled\",\n    false\n  )\n  const [pasteLargeTextAsFile, setPasteLargeTextAsFile] = useStorage(\n    \"pasteLargeTextAsFile\",\n    false\n  )\n\n  const [defaultOCRLanguage, setDefaultOCRLanguage] = useStorage(\n    \"defaultOCRLanguage\",\n    getDefaultOcrLanguage()\n  )\n\n  const [sidepanelTemporaryChat, setSidepanelTemporaryChat] = useStorage(\n    \"sidepanelTemporaryChat\",\n    false\n  )\n\n  const [webuiTemporaryChat, setWebuiTemporaryChat] = useStorage(\n    \"webuiTemporaryChat\",\n    false\n  )\n\n  const [removeReasoningTagFromCopy, setRemoveReasoningTagFromCopy] =\n    useStorage(\"removeReasoningTagFromCopy\", true)\n\n  const [youtubeAutoSummarize, setYoutubeAutoSummarize] = useStorage(\n    {\n      key: \"youtubeAutoSummarize\",\n      instance: new Storage({\n        area: \"local\"\n      })\n    },\n    false\n  )\n  const [hideReasoningWidget, setHideReasoningWidget] = useStorage(\n    \"hideReasoningWidget\",\n    false\n  )\n\n  const [persistChatInput, setPersistChatInput] = useStorage(\n    \"persistChatInput\",\n    false\n  )\n\n  const [enableMessageQueue, setEnableMessageQueue] = useStorage(\n    \"enableMessageQueue\",\n    false\n  )\n\n  const [showMcpServersInChat, setShowMcpServersInChat] = useStorage(\n    \"showMcpServersInChat\",\n    true\n  )\n  const [optimizeQueueForSmallScreen, setOptimizeQueueForSmallScreen] =\n    useStorage(\"optimizeQueueForSmallScreen\", false)\n\n  const [tableTextWrap, setTableTextWrap] = useStorage(\"tableTextWrap\", false)\n\n  const [enableMemory, setEnableMemory] = useStorage(\"enableMemory\", false)\n\n  const [showMoreForLargeMessage, setShowMoreForLargeMessage] = useStorage(\n    \"showMoreForLargeMessage\",\n    false\n  )\n\n  const [sidebarPosition, setSidebarPosition] = useStorage(\n    \"sidebarPosition\",\n    \"left\"\n  )\n\n  const { mode, toggleDarkMode } = useDarkMode()\n  const { t } = useTranslation(\"settings\")\n  const { changeLocale, locale, supportLanguage } = useI18n()\n\n  const { data: prompts } = useQuery({\n    queryKey: [\"getAllPromptsForSettings\"],\n    queryFn: getAllPromptsSystem\n  })\n\n  return (\n    <dl className=\"flex flex-col space-y-6 text-sm\">\n      <div>\n        <h2 className=\"text-base font-semibold leading-7 text-gray-900 dark:text-white\">\n          {t(\"generalSettings.title\")}\n        </h2>\n        <div className=\"border border-b border-gray-200 dark:border-gray-600 mt-3\"></div>\n      </div>\n\n      <div className=\"flex flex-row justify-between\">\n        <span className=\"text-gray-700   dark:text-neutral-50\">\n          {t(\"generalSettings.settings.language.label\")}\n        </span>\n\n        <Select\n          placeholder={t(\"generalSettings.settings.language.placeholder\")}\n          allowClear\n          showSearch\n          style={{ width: \"200px\" }}\n          options={supportLanguage}\n          value={locale}\n          filterOption={(input, option) =>\n            option!.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 ||\n            option!.value.toLowerCase().indexOf(input.toLowerCase()) >= 0\n          }\n          onChange={(value) => {\n            changeLocale(value)\n          }}\n        />\n      </div>\n\n      <div className=\"flex flex-row justify-between\">\n        <div className=\"inline-flex items-center gap-2\">\n          <span className=\"text-gray-700   dark:text-neutral-50\">\n            {t(\"generalSettings.settings.copilotResumeLastChat.label\")}\n          </span>\n        </div>\n        <Switch\n          checked={copilotResumeLastChat}\n          onChange={(checked) => setCopilotResumeLastChat(checked)}\n        />\n      </div>\n      <div className=\"flex flex-row justify-between\">\n        <div className=\"inline-flex items-center gap-2\">\n          <span className=\"text-gray-700   dark:text-neutral-50\">\n            {t(\"generalSettings.settings.turnOnChatWithWebsite.label\")}\n          </span>\n        </div>\n        <Switch\n          checked={defaultChatWithWebsite}\n          onChange={(checked) => setDefaultChatWithWebsite(checked)}\n        />\n      </div>\n      <div className=\"flex flex-row justify-between\">\n        <div className=\"inline-flex items-center gap-2\">\n          <span className=\"text-gray-700   dark:text-neutral-50\">\n            {t(\"generalSettings.settings.webUIResumeLastChat.label\")}\n          </span>\n        </div>\n        <Switch\n          checked={webUIResumeLastChat}\n          onChange={(checked) => setWebUIResumeLastChat(checked)}\n        />\n      </div>\n      <div className=\"flex flex-row justify-between\">\n        <div className=\"inline-flex items-center gap-2\">\n          <span className=\"text-gray-700   dark:text-neutral-50\">\n            {t(\"generalSettings.settings.hideCurrentChatModelSettings.label\")}\n          </span>\n        </div>\n\n        <Switch\n          checked={hideCurrentChatModelSettings}\n          onChange={(checked) => setHideCurrentChatModelSettings(checked)}\n        />\n      </div>\n      <div className=\"flex flex-row justify-between\">\n        <div className=\"inline-flex items-center gap-2\">\n          <span className=\"text-gray-700   dark:text-neutral-50\">\n            {t(\"generalSettings.settings.restoreLastChatModel.label\")}\n          </span>\n        </div>\n\n        <Switch\n          checked={restoreLastChatModel}\n          onChange={(checked) => setRestoreLastChatModel(checked)}\n        />\n      </div>\n\n      <div className=\"flex flex-row justify-between\">\n        <div className=\"inline-flex items-center gap-2\">\n          <span className=\"text-gray-700   dark:text-neutral-50\">\n            {t(\"generalSettings.settings.sendNotificationAfterIndexing.label\")}\n          </span>\n        </div>\n\n        <Switch\n          checked={sendNotificationAfterIndexing}\n          onChange={setSendNotificationAfterIndexing}\n        />\n      </div>\n\n      <div className=\"flex flex-row justify-between\">\n        <div className=\"inline-flex items-center gap-2\">\n          <span className=\"text-gray-700   dark:text-neutral-50\">\n            {t(\"generalSettings.settings.generateTitle.label\")}\n          </span>\n        </div>\n\n        <Switch\n          checked={generateTitle}\n          onChange={(checked) => setGenerateTitle(checked)}\n        />\n      </div>\n\n      <div className=\"flex flex-row justify-between\">\n        <div className=\"inline-flex items-center gap-2\">\n          <span className=\"text-gray-700   dark:text-neutral-50\">\n            {t(\"generalSettings.settings.ollamaStatus.label\")}\n          </span>\n        </div>\n\n        <Switch\n          checked={checkOllamaStatus}\n          onChange={(checked) => setCheckOllamaStatus(checked)}\n        />\n      </div>\n\n      <div className=\"flex flex-row justify-between\">\n        <div className=\"inline-flex items-center gap-2\">\n          <span className=\"text-gray-700   dark:text-neutral-50\">\n            {t(\"generalSettings.settings.wideMode.label\")}\n          </span>\n        </div>\n\n        <Switch\n          checked={checkWideMode}\n          onChange={(checked) => setCheckWideMode(checked)}\n        />\n      </div>\n\n      <div className=\"flex flex-row justify-between\">\n        <div className=\"inline-flex items-center gap-2\">\n          <span className=\"text-gray-700   dark:text-neutral-50\">\n            {t(\"generalSettings.settings.openReasoning.label\")}\n          </span>\n        </div>\n\n        <Switch\n          checked={openReasoning}\n          onChange={(checked) => setOpenReasoning(checked)}\n        />\n      </div>\n\n      <div className=\"flex flex-row justify-between\">\n        <div className=\"inline-flex items-center gap-2\">\n          <span className=\"text-gray-700   dark:text-neutral-50\">\n            {t(\"generalSettings.settings.userChatBubble.label\")}\n          </span>\n        </div>\n\n        <Switch\n          checked={userChatBubble}\n          onChange={(checked) => setUserChatBubble(checked)}\n        />\n      </div>\n\n      <div className=\"flex flex-row justify-between\">\n        <div className=\"inline-flex items-center gap-2\">\n          <span className=\"text-gray-700   dark:text-neutral-50\">\n            {t(\"generalSettings.settings.autoCopyResponseToClipboard.label\")}\n          </span>\n        </div>\n\n        <Switch\n          checked={autoCopyResponseToClipboard}\n          onChange={(checked) => setAutoCopyResponseToClipboard(checked)}\n        />\n      </div>\n\n      <div className=\"flex flex-row justify-between\">\n        <div className=\"inline-flex items-center gap-2\">\n          <span className=\"text-gray-700   dark:text-neutral-50\">\n            {t(\"generalSettings.settings.useMarkdownForUserMessage.label\")}\n          </span>\n        </div>\n\n        <Switch\n          checked={useMarkdownForUserMessage}\n          onChange={(checked) => setUseMarkdownForUserMessage(checked)}\n        />\n      </div>\n\n      <div className=\"flex flex-row justify-between\">\n        <div className=\"inline-flex items-center gap-2\">\n          <span className=\"text-gray-700   dark:text-neutral-50\">\n            {t(\"generalSettings.settings.copyAsFormattedText.label\")}\n          </span>\n        </div>\n\n        <Switch\n          checked={copyAsFormattedText}\n          onChange={(checked) => setCopyAsFormattedText(checked)}\n        />\n      </div>\n\n      <div className=\"flex flex-row justify-between\">\n        <div className=\"inline-flex items-center gap-2\">\n          <BetaTag />\n          <span className=\"text-gray-700   dark:text-neutral-50\">\n            {t(\"generalSettings.settings.tabMentionsEnabled.label\")}\n          </span>\n        </div>\n\n        <Switch\n          checked={tabMentionsEnabled}\n          onChange={(checked) => setTabMentionsEnabled(checked)}\n        />\n      </div>\n      <div className=\"flex flex-row justify-between\">\n        <div className=\"inline-flex items-center gap-2\">\n          <span className=\"text-gray-700   dark:text-neutral-50\">\n            {t(\"generalSettings.settings.pasteLargeTextAsFile.label\")}\n          </span>\n        </div>\n\n        <Switch\n          checked={pasteLargeTextAsFile}\n          onChange={(checked) => setPasteLargeTextAsFile(checked)}\n        />\n      </div>\n\n      <div className=\"flex flex-row justify-between\">\n        <span className=\"text-gray-700   dark:text-neutral-50\">\n          {t(\"generalSettings.settings.ocrLanguage.label\")}\n        </span>\n\n        <Select\n          placeholder={t(\"generalSettings.settings.ocrLanguage.placeholder\")}\n          showSearch\n          style={{ width: \"200px\" }}\n          options={ocrLanguages}\n          value={defaultOCRLanguage}\n          filterOption={(input, option) =>\n            option!.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 ||\n            option!.value.toLowerCase().indexOf(input.toLowerCase()) >= 0\n          }\n          onChange={(value) => {\n            setDefaultOCRLanguage(value)\n          }}\n        />\n      </div>\n\n      <div className=\"flex flex-row justify-between\">\n        <span className=\"text-gray-700 dark:text-neutral-50 \">\n          {t(\"generalSettings.settings.sidepanelTemporaryChat.label\")}\n        </span>\n\n        <Switch\n          checked={sidepanelTemporaryChat}\n          onChange={(checked) => setSidepanelTemporaryChat(checked)}\n        />\n      </div>\n\n      <div className=\"flex flex-row justify-between\">\n        <span className=\"text-gray-700 dark:text-neutral-50 \">\n          {t(\"generalSettings.settings.removeReasoningTagFromCopy.label\")}\n        </span>\n\n        <Switch\n          checked={removeReasoningTagFromCopy}\n          onChange={(checked) => setRemoveReasoningTagFromCopy(checked)}\n        />\n      </div>\n\n      {!isFireFox && (\n        <div className=\"flex flex-row justify-between\">\n          <div className=\"inline-flex items-center gap-2\">\n            <BetaTag />\n            <span className=\"text-gray-700 dark:text-neutral-50 \">\n              {t(\"generalSettings.settings.youtubeAutoSummarize.label\")}\n            </span>\n          </div>\n\n          <Switch\n            checked={youtubeAutoSummarize}\n            onChange={(checked) => setYoutubeAutoSummarize(checked)}\n          />\n        </div>\n      )}\n\n      <div className=\"flex flex-row justify-between\">\n        <span className=\"text-gray-700 dark:text-neutral-50 \">\n          {t(\"generalSettings.settings.webuiTemporaryChat.label\")}\n        </span>\n\n        <Switch\n          checked={webuiTemporaryChat}\n          onChange={(checked) => setWebuiTemporaryChat(checked)}\n        />\n      </div>\n\n      <div className=\"flex flex-row justify-between\">\n        <span className=\"text-gray-700   dark:text-neutral-50\">\n          {t(\"generalSettings.settings.defaultCopilotPrompt.label\")}\n        </span>\n\n        <Select\n          placeholder={t(\n            \"generalSettings.settings.defaultCopilotPrompt.placeholder\"\n          )}\n          allowClear\n          showSearch\n          style={{ width: \"200px\" }}\n          options={\n            prompts\n              ? prompts.map((prompt) => ({\n                  key: prompt.id,\n                  value: prompt.id,\n                  label: prompt.title\n                }))\n              : []\n          }\n          value={defaultCopilotPrompt || undefined}\n          filterOption={(input, option) =>\n            option!.label.toLowerCase().indexOf(input.toLowerCase()) >= 0\n          }\n          onChange={(value) => {\n            setDefaultCopilotPrompt(value || null)\n          }}\n        />\n      </div>\n\n      <div className=\"flex flex-row justify-between\">\n        <span className=\"text-gray-700   dark:text-neutral-50\">\n          {t(\"generalSettings.settings.defaultWebUIPrompt.label\")}\n        </span>\n\n        <Select\n          placeholder={t(\n            \"generalSettings.settings.defaultWebUIPrompt.placeholder\"\n          )}\n          allowClear\n          showSearch\n          style={{ width: \"200px\" }}\n          options={\n            prompts\n              ? prompts.map((prompt) => ({\n                  key: prompt.id,\n                  value: prompt.id,\n                  label: prompt.title\n                }))\n              : []\n          }\n          value={defaultWebUIPrompt || undefined}\n          filterOption={(input, option) =>\n            option!.label.toLowerCase().indexOf(input.toLowerCase()) >= 0\n          }\n          onChange={(value) => {\n            setDefaultWebUIPrompt(value || null)\n          }}\n        />\n      </div>\n\n      <div className=\"flex flex-row justify-between\">\n        <div className=\"inline-flex items-center gap-2\">\n          <span className=\"text-gray-700   dark:text-neutral-50\">\n            {t(\"generalSettings.settings.defaultThinkingMode.label\")}\n          </span>\n        </div>\n\n        <Switch\n          checked={defaultThinkingMode}\n          onChange={(checked) => setDefaultThinkingMode(checked)}\n        />\n      </div>\n\n      <div className=\"flex flex-row justify-between\">\n        <div className=\"inline-flex items-center gap-2\">\n          <span className=\"text-gray-700   dark:text-neutral-50\">\n            {t(\"generalSettings.settings.hideReasoningWidget.label\")}\n          </span>\n        </div>\n\n        <Switch\n          checked={hideReasoningWidget}\n          onChange={(checked) => setHideReasoningWidget(checked)}\n        />\n      </div>\n\n      <div className=\"flex flex-row justify-between\">\n        <div className=\"inline-flex items-center gap-2\">\n          <span className=\"text-gray-700   dark:text-neutral-50\">\n            {t(\"generalSettings.settings.persistChatInput.label\")}\n          </span>\n        </div>\n\n        <Switch\n          checked={persistChatInput}\n          onChange={(checked) => setPersistChatInput(checked)}\n        />\n      </div>\n\n      <div className=\"flex flex-row justify-between\">\n        <div className=\"inline-flex items-center gap-2\">\n          <span className=\"text-gray-700   dark:text-neutral-50\">\n            {t(\"generalSettings.settings.tableTextWrap.label\")}\n          </span>\n        </div>\n\n        <Switch\n          checked={tableTextWrap}\n          onChange={(checked) => setTableTextWrap(checked)}\n        />\n      </div>\n\n      <div className=\"flex flex-row justify-between\">\n        <div className=\"inline-flex items-center gap-2\">\n          <BetaTag />\n          <span className=\"text-gray-700   dark:text-neutral-50\">\n            {t(\n              \"generalSettings.settings.enableMemory.label\",\n              \"Enable Memory (Experimental)\"\n            )}\n          </span>\n        </div>\n\n        <Switch\n          checked={enableMemory}\n          onChange={(checked) => setEnableMemory(checked)}\n        />\n      </div>\n\n      <div className=\"flex flex-row justify-between\">\n        <div className=\"inline-flex items-center gap-2\">\n          <span className=\"text-gray-700   dark:text-neutral-50\">\n            {t(\"generalSettings.settings.showMoreForLargeMessage.label\")}\n          </span>\n        </div>\n\n        <Switch\n          checked={showMoreForLargeMessage}\n          onChange={(checked) => setShowMoreForLargeMessage(checked)}\n        />\n      </div>\n\n      <div className=\"flex flex-row justify-between\">\n        <span className=\"text-gray-700 dark:text-neutral-50 \">\n          {t(\"generalSettings.settings.sidebarPosition.label\")}\n        </span>\n\n        <Select\n          allowClear={false}\n          style={{ width: \"200px\" }}\n          options={[\n            {\n              value: \"left\",\n              label: t(\"generalSettings.settings.sidebarPosition.options.left\")\n            },\n            {\n              value: \"right\",\n              label: t(\"generalSettings.settings.sidebarPosition.options.right\")\n            }\n          ]}\n          value={sidebarPosition}\n          onChange={(value) => {\n            setSidebarPosition(value)\n          }}\n        />\n      </div>\n\n      <div className=\"flex flex-row justify-between\">\n        <div className=\"inline-flex items-center gap-2\">\n          <span className=\"text-gray-700   dark:text-neutral-50\">\n            {t(\n              \"generalSettings.settings.enableMessageQueue.label\",\n              \"Enable Message Queue While Streaming\"\n            )}\n          </span>\n        </div>\n\n        <Switch\n          checked={enableMessageQueue}\n          onChange={(checked) => setEnableMessageQueue(checked)}\n        />\n      </div>\n      <div className=\"flex flex-row justify-between\">\n        <div className=\"inline-flex items-center gap-2\">\n          <span\n            className={`text-gray-700 dark:text-neutral-50`}>\n            {t(\n              \"generalSettings.settings.optimizeQueueForSmallScreen.label\",\n              \"Optimize Chat UI for Small Screens\"\n            )}\n          </span>\n        </div>\n        <Switch\n          checked={optimizeQueueForSmallScreen}\n          onChange={(checked) => setOptimizeQueueForSmallScreen(checked)}\n        />\n      </div>\n\n      <div className=\"flex flex-row justify-between\">\n        <div className=\"inline-flex items-center gap-2\">\n          <span className=\"text-gray-700   dark:text-neutral-50\">\n            {t(\n              \"generalSettings.settings.showMcpServersInChat.label\",\n              \"Show MCP Servers Toggle in Chat\"\n            )}\n          </span>\n        </div>\n\n        <Switch\n          checked={showMcpServersInChat}\n          onChange={(checked) => setShowMcpServersInChat(checked)}\n        />\n      </div>\n\n      <div className=\"flex flex-row justify-between\">\n        <span className=\"text-gray-700 dark:text-neutral-50 \">\n          {t(\"generalSettings.settings.darkMode.label\")}\n        </span>\n\n        <button\n          onClick={toggleDarkMode}\n          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 `}>\n          {mode === \"dark\" ? (\n            <SunIcon className=\"w-4 h-4 mr-2\" />\n          ) : (\n            <MoonIcon className=\"w-4 h-4 mr-2\" />\n          )}\n          {mode === \"dark\"\n            ? t(\"generalSettings.settings.darkMode.options.light\")\n            : t(\"generalSettings.settings.darkMode.options.dark\")}\n        </button>\n      </div>\n      <SearchModeSettings />\n      <SSTSettings />\n      <TTSModeSettings />\n      <SystemSettings />\n    </dl>\n  )\n}\n"
  },
  {
    "path": "src/components/Option/Settings/mcp.tsx",
    "content": "import {\n  Alert,\n  Form,\n  Input,\n  Modal,\n  Select,\n  Switch,\n  Table,\n  Tag,\n  Tooltip,\n  message,\n  notification\n} from \"antd\"\nimport { useState } from \"react\"\nimport { useTranslation } from \"react-i18next\"\nimport { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\"\nimport {\n  KeyRound,\n  LogOut,\n  Pencil,\n  RefreshCw,\n  Trash2,\n  Trash2Icon,\n  ExternalLink\n} from \"lucide-react\"\nimport { browser } from \"wxt/browser\"\nimport { hasValidOAuthTokens } from \"@/libs/mcp/oauth\"\nimport { isFireFoxPrivateMode } from \"@/utils/is-private-mode\"\nimport {\n  addMcpServer,\n  deleteMcpServer,\n  getAllMcpServers,\n  updateMcpServer\n} from \"@/db/dexie/mcp\"\nimport { getMcpErrorMessage } from \"@/libs/mcp/errors\"\nimport { inspectMcpServerTools } from \"@/libs/mcp/remote-tools\"\nimport type {\n  McpAvailableTool,\n  McpServer,\n  McpServerInput\n} from \"@/libs/mcp/types\"\nimport {\n  getMcpServerConfigFingerprint,\n  normalizeMcpServerInput\n} from \"@/libs/mcp/utils\"\nimport { getServerFaviconUrl } from \"@/components/Common/McpServerToggle\"\n\ntype ValidationSnapshot = {\n  fingerprint: string\n  cachedTools: McpAvailableTool[]\n  toolsLastSyncedAt?: number\n  toolsSyncError?: string\n}\n\nconst isFormValidationError = (\n  error: unknown\n): error is { errorFields: unknown[] } =>\n  typeof error === \"object\" &&\n  error !== null &&\n  \"errorFields\" in error &&\n  Array.isArray((error as { errorFields: unknown[] }).errorFields)\n\nconst toServerDraft = (values: Partial<McpServerInput>): McpServerInput =>\n  normalizeMcpServerInput(values)\n\nconst toValidationSnapshot = (\n  server: Partial<\n    Pick<McpServer, \"cachedTools\" | \"toolsLastSyncedAt\" | \"toolsSyncError\"> &\n      McpServerInput\n  >\n): ValidationSnapshot => ({\n  fingerprint: getMcpServerConfigFingerprint(server),\n  cachedTools: server.cachedTools || [],\n  toolsLastSyncedAt: server.toolsLastSyncedAt,\n  toolsSyncError: server.toolsSyncError\n})\n\nconst formatTimestamp = (value?: number) =>\n  value ? new Date(value).toLocaleString() : \"\"\n\nconst ToolTags = ({\n  tools,\n  maxVisible = 3\n}: {\n  tools: McpAvailableTool[]\n  maxVisible?: number\n}) => {\n  if (tools.length === 0) {\n    return null\n  }\n\n  const visibleTools = tools.slice(0, maxVisible)\n  const remainingTools = tools.length - visibleTools.length\n\n  return (\n    <div className=\"flex flex-wrap gap-1.5\">\n      {visibleTools.map((tool) => (\n        <Tooltip key={tool.name} title={tool.description || tool.name}>\n          <Tag className=\"mx-0 max-w-full truncate\">{tool.name}</Tag>\n        </Tooltip>\n      ))}\n      {remainingTools > 0 ? (\n        <Tag className=\"mx-0\">+{remainingTools}</Tag>\n      ) : null}\n    </div>\n  )\n}\n\nexport const MCPSettingsApp = () => {\n  const { t } = useTranslation([\"settings\", \"common\"])\n  const [form] = Form.useForm()\n  const queryClient = useQueryClient()\n  const [open, setOpen] = useState(false)\n  const [editingServer, setEditingServer] = useState<McpServer | null>(null)\n  const [validationSnapshot, setValidationSnapshot] =\n    useState<ValidationSnapshot | null>(null)\n  const authType = Form.useWatch(\"authType\", form)\n  const watchedValues = Form.useWatch([], form)\n  const currentFingerprint = getMcpServerConfigFingerprint(watchedValues || {})\n\n  const { data: servers, isLoading } = useQuery({\n    queryKey: [\"mcpServers\"],\n    queryFn: getAllMcpServers\n  })\n\n  const addMutation = useMutation({\n    mutationFn: addMcpServer,\n    onSuccess: async (serverId, variables) => {\n      queryClient.invalidateQueries({ queryKey: [\"mcpServers\"] })\n      setOpen(false)\n      setEditingServer(null)\n      setValidationSnapshot(null)\n      form.resetFields()\n      message.success(t(\"mcpSettings.notification.added\"))\n\n      // Auto-start OAuth flow after adding an OAuth server\n      if (variables.authType === \"oauth\") {\n        const result = await browser.runtime.sendMessage({\n          type: \"mcp_oauth_start\",\n          serverId\n        })\n        if (result?.error) {\n          notification.error({\n            message: \"OAuth Error\",\n            description: result.error\n          })\n        }\n      }\n    }\n  })\n\n  const updateMutation = useMutation({\n    mutationFn: updateMcpServer,\n    onSuccess: () => {\n      queryClient.invalidateQueries({ queryKey: [\"mcpServers\"] })\n      setOpen(false)\n      setEditingServer(null)\n      setValidationSnapshot(null)\n      form.resetFields()\n      message.success(t(\"mcpSettings.notification.updated\"))\n    }\n  })\n\n  const deleteMutation = useMutation({\n    mutationFn: deleteMcpServer,\n    onSuccess: () => {\n      queryClient.invalidateQueries({ queryKey: [\"mcpServers\"] })\n      message.success(t(\"mcpSettings.notification.deleted\"))\n    }\n  })\n\n  const validateMutation = useMutation({\n    mutationFn: inspectMcpServerTools\n  })\n\n  const refreshToolsMutation = useMutation({\n    mutationFn: async (server: McpServer) => {\n      try {\n        const validation = await inspectMcpServerTools(server)\n        return await updateMcpServer({\n          id: server.id,\n          cachedTools: validation.cachedTools,\n          toolsLastSyncedAt: validation.toolsLastSyncedAt,\n          toolsSyncError: undefined\n        })\n      } catch (error) {\n        await updateMcpServer({\n          id: server.id,\n          cachedTools: [],\n          toolsLastSyncedAt: Date.now(),\n          toolsSyncError: getMcpErrorMessage(error)\n        })\n        throw error\n      }\n    },\n    onSuccess: (server) => {\n      queryClient.invalidateQueries({ queryKey: [\"mcpServers\"] })\n      message.success(\n        t(\"mcpSettings.notification.toolsRefreshed\", {\n          name: server.name\n        })\n      )\n    },\n    onError: (error) => {\n      queryClient.invalidateQueries({ queryKey: [\"mcpServers\"] })\n      notification.error({\n        message: t(\"mcpSettings.notification.validationFailedTitle\"),\n        description: getMcpErrorMessage(error)\n      })\n    }\n  })\n\n  const isValidationCurrent =\n    validationSnapshot?.fingerprint === currentFingerprint\n  const hasValidatedTools = (validationSnapshot?.cachedTools.length ?? 0) > 0\n  const isValidationFresh =\n    !!validationSnapshot &&\n    isValidationCurrent &&\n    !validationSnapshot.toolsSyncError &&\n    hasValidatedTools\n  const isValidationStale =\n    !!validationSnapshot &&\n    validationSnapshot.fingerprint.length > 0 &&\n    validationSnapshot.fingerprint !== currentFingerprint\n\n  const closeModal = () => {\n    setOpen(false)\n    setEditingServer(null)\n    setValidationSnapshot(null)\n    form.resetFields()\n  }\n\n  const ensureValidatedTools = async (\n    draft: McpServerInput,\n    force = false\n  ): Promise<ValidationSnapshot> => {\n    const fingerprint = getMcpServerConfigFingerprint(draft)\n\n    if (\n      !force &&\n      validationSnapshot &&\n      validationSnapshot.fingerprint === fingerprint &&\n      !validationSnapshot.toolsSyncError &&\n      validationSnapshot.cachedTools.length > 0\n    ) {\n      return validationSnapshot\n    }\n\n    try {\n      const validation = await validateMutation.mutateAsync(draft)\n      const snapshot: ValidationSnapshot = {\n        fingerprint,\n        cachedTools: validation.cachedTools,\n        toolsLastSyncedAt: validation.toolsLastSyncedAt,\n        toolsSyncError: undefined\n      }\n\n      setValidationSnapshot(snapshot)\n      return snapshot\n    } catch (error) {\n      const snapshot: ValidationSnapshot = {\n        fingerprint,\n        cachedTools: [],\n        toolsLastSyncedAt: Date.now(),\n        toolsSyncError: getMcpErrorMessage(error)\n      }\n\n      setValidationSnapshot(snapshot)\n      throw error\n    }\n  }\n\n  const handleValidateTools = async () => {\n    try {\n      const values = await form.validateFields()\n      const draft = toServerDraft(values)\n      const snapshot = await ensureValidatedTools(draft, true)\n\n      message.success(\n        t(\"mcpSettings.notification.validated\", {\n          count: snapshot.cachedTools.length\n        })\n      )\n    } catch (error) {\n      if (isFormValidationError(error)) {\n        return\n      }\n\n      notification.error({\n        message: t(\"mcpSettings.notification.validationFailedTitle\"),\n        description: getMcpErrorMessage(error)\n      })\n    }\n  }\n\n  const handleSubmit = async (values: McpServerInput) => {\n    const draft = toServerDraft(values)\n\n    // Skip tool validation for OAuth servers that aren't connected yet\n    if (draft.authType === \"oauth\" && !editingServer?.oauthTokens?.accessToken) {\n      const payload = {\n        ...draft,\n        cachedTools: editingServer?.cachedTools || [],\n        toolsLastSyncedAt: editingServer?.toolsLastSyncedAt,\n        toolsSyncError: undefined\n      }\n\n      if (editingServer?.id) {\n        updateMutation.mutate({\n          id: editingServer.id,\n          ...payload,\n          oauthMetadata: editingServer.oauthMetadata,\n          oauthClientRegistration: editingServer.oauthClientRegistration,\n          oauthTokens: editingServer.oauthTokens\n        })\n      } else {\n        addMutation.mutate(payload)\n      }\n      return\n    }\n\n    try {\n      const validation = await ensureValidatedTools(draft)\n      const payload = {\n        ...draft,\n        cachedTools: validation.cachedTools,\n        toolsLastSyncedAt: validation.toolsLastSyncedAt,\n        toolsSyncError: undefined\n      }\n\n      if (editingServer?.id) {\n        updateMutation.mutate({\n          id: editingServer.id,\n          ...payload\n        })\n        return\n      }\n\n      addMutation.mutate(payload)\n    } catch (error) {\n      notification.error({\n        message: t(\"mcpSettings.notification.validationFailedTitle\"),\n        description: getMcpErrorMessage(error)\n      })\n    }\n  }\n\n  const handleEdit = (server: McpServer) => {\n    setEditingServer(server)\n    form.setFieldsValue({\n      ...server,\n      authType: server.authType || \"none\",\n      headers: server.headers || [],\n      enabled: server.enabled ?? true\n    })\n    setValidationSnapshot(toValidationSnapshot(server))\n    setOpen(true)\n  }\n\n  const handleAdd = () => {\n    if (isFireFoxPrivateMode) {\n      notification.error({\n        message: t(\"mcpSettings.notification.storageBlockedTitle\"),\n        description: t(\"mcpSettings.notification.storageBlockedDescription\")\n      })\n      return\n    }\n\n    setEditingServer(null)\n    setValidationSnapshot(null)\n    form.resetFields()\n    form.setFieldsValue({\n      transport: \"http\",\n      enabled: true,\n      authType: \"none\",\n      headers: []\n    })\n    setOpen(true)\n  }\n\n  const renderValidationAlert = () => {\n    if (!validationSnapshot) {\n      return (\n        <Alert\n          type=\"info\"\n          showIcon\n          message={t(\"mcpSettings.modal.validation.idle\")}\n        />\n      )\n    }\n\n    if (isValidationStale) {\n      return (\n        <Alert\n          type=\"warning\"\n          showIcon\n          message={t(\"mcpSettings.modal.validation.stale\")}\n        />\n      )\n    }\n\n    if (validationSnapshot.toolsSyncError) {\n      return (\n        <Alert\n          type=\"error\"\n          showIcon\n          message={t(\"mcpSettings.modal.validation.failed\")}\n          description={validationSnapshot.toolsSyncError}\n        />\n      )\n    }\n\n    return (\n      <Alert\n        type=\"success\"\n        showIcon\n        message={t(\"mcpSettings.modal.validation.success\", {\n          count: validationSnapshot.cachedTools.length\n        })}\n        description={\n          validationSnapshot.toolsLastSyncedAt\n            ? t(\"mcpSettings.modal.validation.syncedAt\", {\n                value: formatTimestamp(validationSnapshot.toolsLastSyncedAt)\n              })\n            : undefined\n        }\n      />\n    )\n  }\n\n  return (\n    <div className=\"w-full max-w-full overflow-hidden\">\n      <div className=\"px-2 sm:px-0\">\n        <div className=\"mb-6\">\n          <div className=\"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n            <div className=\"flex-1\">\n              <h2 className=\"text-base font-semibold leading-7 text-gray-900 dark:text-white\">\n                {t(\"mcpSettings.heading\")}\n              </h2>\n              <p className=\"mt-1 text-sm leading-6 text-gray-600 dark:text-gray-400\">\n                {t(\"mcpSettings.subheading\")}\n              </p>\n            </div>\n            <button\n              onClick={handleAdd}\n              className=\"inline-flex items-center justify-center rounded-md border border-transparent bg-black px-3 py-2 text-sm font-medium leading-4 text-white shadow-sm hover:bg-gray-800 focus:outline-none dark:bg-white dark:text-gray-900 dark:hover:bg-gray-100 disabled:opacity-50\">\n              {t(\"mcpSettings.addBtn\")}\n            </button>\n          </div>\n          <p className=\"mt-3 text-xs text-amber-600 dark:text-amber-400\">\n            MCP tools run without approval. Review connected servers carefully before use.\n          </p>\n          <div className=\"mt-4 border border-b border-gray-200 dark:border-gray-600\"></div>\n        </div>\n\n        <Table\n          rowKey=\"id\"\n          loading={isLoading}\n          dataSource={servers || []}\n          bordered\n          scroll={{ x: 980 }}\n          footer={() => (\n            <a\n              href=\"https://docs.pageassist.xyz/features/mcp.html\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              className=\"text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300\">\n              Learn how to connect MCP servers\n              <ExternalLink className=\"ml-1 inline size-3.5\" />\n            </a>\n          )}\n          columns={[\n            {\n              title: t(\"mcpSettings.table.name\"),\n              dataIndex: \"name\",\n              key: \"name\",\n              render: (value: string, record: McpServer) => {\n                const faviconUrl = getServerFaviconUrl(record.url)\n                return (\n                  <div className=\"flex items-center gap-2\">\n                    {faviconUrl && (\n                      <img\n                        src={faviconUrl}\n                        alt=\"\"\n                        className=\"h-4 w-4 shrink-0 rounded-sm\"\n                        onError={(e) => {\n                          e.currentTarget.style.display = \"none\"\n                        }}\n                      />\n                    )}\n                    <span>{value}</span>\n                  </div>\n                )\n              }\n            },\n            {\n              title: t(\"mcpSettings.table.url\"),\n              dataIndex: \"url\",\n              key: \"url\",\n              width: 200,\n              render: (value: string) => (\n                <span className=\"block max-w-[180px] truncate\" title={value}>\n                  {value}\n                </span>\n              )\n            },\n\n            {\n              title: t(\"mcpSettings.table.auth\"),\n              dataIndex: \"authType\",\n              key: \"authType\",\n              render: (value: string, record: McpServer) => {\n                if (value === \"bearer\") {\n                  return (\n                    <Tooltip title={record.bearerToken ? \"••••••••\" : \"\"}>\n                      <span>{t(\"mcpSettings.auth.bearer\")}</span>\n                    </Tooltip>\n                  )\n                }\n                if (value === \"oauth\") {\n                  const connected = hasValidOAuthTokens(record.oauthTokens)\n                  return (\n                    <Tag color={connected ? \"green\" : \"orange\"}>\n                      {connected ? \"OAuth Connected\" : \"OAuth Disconnected\"}\n                    </Tag>\n                  )\n                }\n                return t(\"mcpSettings.auth.none\")\n              }\n            },\n            {\n              title: t(\"mcpSettings.table.actions\"),\n              key: \"actions\",\n              render: (_, record: McpServer) => (\n                <div className=\"flex items-center gap-3\">\n                  <Tooltip\n                    title={\n                      record.enabled\n                        ? t(\"mcpSettings.actions.disable\")\n                        : t(\"mcpSettings.actions.enable\")\n                    }>\n                    <Switch\n                      size=\"small\"\n                      checked={record.enabled}\n                      onChange={(checked) =>\n                        updateMcpServer({\n                          id: record.id,\n                          enabled: checked\n                        }).then(() =>\n                          queryClient.invalidateQueries({\n                            queryKey: [\"mcpServers\"]\n                          })\n                        )\n                      }\n                    />\n                  </Tooltip>\n                  {record.authType === \"oauth\" && (\n                    <Tooltip\n                      title={\n                        hasValidOAuthTokens(record.oauthTokens)\n                          ? \"Disconnect OAuth\"\n                          : \"Connect with OAuth\"\n                      }>\n                      <button\n                        className={`p-1 ${\n                          hasValidOAuthTokens(record.oauthTokens)\n                            ? \"text-green-600 dark:text-green-400\"\n                            : \"text-orange-500 dark:text-orange-400\"\n                        }`}\n                        onClick={async () => {\n                          if (hasValidOAuthTokens(record.oauthTokens)) {\n                            await browser.runtime.sendMessage({\n                              type: \"mcp_oauth_disconnect\",\n                              serverId: record.id\n                            })\n                            queryClient.invalidateQueries({\n                              queryKey: [\"mcpServers\"]\n                            })\n                            message.success(\"OAuth disconnected\")\n                          } else {\n                            const result = await browser.runtime.sendMessage({\n                              type: \"mcp_oauth_start\",\n                              serverId: record.id\n                            })\n                            if (result?.error) {\n                              notification.error({\n                                message: \"OAuth Error\",\n                                description: result.error\n                              })\n                            } else {\n                              message.info(\n                                \"OAuth flow started. Complete authorization in the opened tab.\"\n                              )\n                              // Poll for completion\n                              const pollInterval = setInterval(async () => {\n                                queryClient.invalidateQueries({\n                                  queryKey: [\"mcpServers\"]\n                                })\n                              }, 2000)\n                              setTimeout(\n                                () => clearInterval(pollInterval),\n                                120_000\n                              )\n                            }\n                          }\n                        }}>\n                        {hasValidOAuthTokens(record.oauthTokens) ? (\n                          <LogOut className=\"size-4\" />\n                        ) : (\n                          <KeyRound className=\"size-4\" />\n                        )}\n                      </button>\n                    </Tooltip>\n                  )}\n                  <Tooltip title={t(\"mcpSettings.actions.refreshTools\")}>\n                    <button\n                      className=\"p-1 text-gray-700 dark:text-gray-400\"\n                      disabled={isFireFoxPrivateMode}\n                      onClick={() => refreshToolsMutation.mutate(record)}>\n                      <RefreshCw\n                        className={`size-4 ${\n                          refreshToolsMutation.isPending &&\n                          refreshToolsMutation.variables?.id === record.id\n                            ? \"animate-spin\"\n                            : \"\"\n                        }`}\n                      />\n                    </button>\n                  </Tooltip>\n                  <Tooltip title={t(\"common:edit\")}>\n                    <button\n                      className=\"p-1 text-gray-700 dark:text-gray-400\"\n                      disabled={isFireFoxPrivateMode}\n                      onClick={() => handleEdit(record)}>\n                      <Pencil className=\"size-4\" />\n                    </button>\n                  </Tooltip>\n                  <Tooltip title={t(\"common:delete\")}>\n                    <button\n                      className=\"p-1 text-red-500 dark:text-red-400\"\n                      disabled={isFireFoxPrivateMode}\n                      onClick={() => {\n                        if (\n                          confirm(\n                            t(\"mcpSettings.modal.deleteConfirm\", {\n                              name: record.name\n                            })\n                          )\n                        ) {\n                          deleteMutation.mutate(record.id)\n                        }\n                      }}>\n                      <Trash2 className=\"size-4\" />\n                    </button>\n                  </Tooltip>\n                </div>\n              )\n            }\n          ]}\n        />\n\n        <Modal\n          open={open}\n          title={\n            editingServer\n              ? t(\"mcpSettings.modal.titleEdit\")\n              : t(\"mcpSettings.modal.titleAdd\")\n          }\n          onCancel={closeModal}\n          footer={null}>\n          <Form\n            form={form}\n            layout=\"vertical\"\n            onFinish={handleSubmit}\n            initialValues={{\n              enabled: true,\n              authType: \"none\",\n              headers: []\n            }}>\n            <Form.Item\n              name=\"name\"\n              label={t(\"mcpSettings.modal.name.label\")}\n              rules={[\n                {\n                  required: true,\n                  message: t(\"mcpSettings.modal.name.required\")\n                }\n              ]}>\n              <Input\n                size=\"large\"\n                placeholder={t(\"mcpSettings.modal.name.placeholder\")}\n              />\n            </Form.Item>\n\n            <Form.Item\n              name=\"url\"\n              label={t(\"mcpSettings.modal.url.label\")}\n              extra={t(\"mcpSettings.modal.transportNotice.description\")}\n              rules={[\n                {\n                  required: true,\n                  message: t(\"mcpSettings.modal.url.required\")\n                },\n                {\n                  validator: async (_, value) => {\n                    try {\n                      const parsed = new URL(value)\n                      if (![\"http:\", \"https:\"].includes(parsed.protocol)) {\n                        throw new Error()\n                      }\n                    } catch (error) {\n                      throw new Error(t(\"mcpSettings.modal.url.invalid\"))\n                    }\n                  }\n                }\n              ]}>\n              <Input\n                size=\"large\"\n                placeholder={t(\"mcpSettings.modal.url.placeholder\")}\n              />\n            </Form.Item>\n\n            <Form.Item\n              name=\"authType\"\n              label={t(\"mcpSettings.modal.auth.label\")}>\n              <Select\n                size=\"large\"\n                options={[\n                  {\n                    label: t(\"mcpSettings.auth.none\"),\n                    value: \"none\"\n                  },\n                  {\n                    label: t(\"mcpSettings.auth.bearer\"),\n                    value: \"bearer\"\n                  },\n                  {\n                    label: \"OAuth 2.1\",\n                    value: \"oauth\"\n                  }\n                ]}\n              />\n            </Form.Item>\n\n            {authType === \"bearer\" ? (\n              <Form.Item\n                name=\"bearerToken\"\n                label={t(\"mcpSettings.modal.bearerToken.label\")}\n                rules={[\n                  {\n                    required: true,\n                    message: t(\"mcpSettings.modal.bearerToken.required\")\n                  }\n                ]}>\n                <Input.Password\n                  size=\"large\"\n                  placeholder={t(\"mcpSettings.modal.bearerToken.placeholder\")}\n                />\n              </Form.Item>\n            ) : null}\n\n            {authType === \"oauth\" ? (\n              <p className=\"mb-4 text-xs text-gray-400 dark:text-gray-500\">\n                Uses your Page Share URL as the OAuth redirect. You can change it in Manage Share settings.\n              </p>\n            ) : null}\n\n            <Form.Item\n              name=\"enabled\"\n              label={t(\"mcpSettings.modal.enabled.label\")}\n              valuePropName=\"checked\">\n              <Switch />\n            </Form.Item>\n\n            <Form.List name=\"headers\">\n              {(fields, { add, remove }) => (\n                <div className=\"flex flex-col\">\n                  <div className=\"mb-3 flex items-center justify-between\">\n                    <h3 className=\"text-sm font-semibold\">\n                      {t(\"mcpSettings.modal.headers.label\")}\n                    </h3>\n                    <button\n                      type=\"button\"\n                      className=\"rounded-md bg-black px-2 py-1 text-xs text-white dark:bg-white dark:text-black\"\n                      onClick={() => add()}>\n                      {t(\"mcpSettings.modal.headers.add\")}\n                    </button>\n                  </div>\n                  {fields.map((field) => (\n                    <div\n                      key={field.key}\n                      className=\"mb-3 flex flex-col items-start gap-2 sm:flex-row sm:items-end\">\n                      <div className=\"w-full space-y-2 sm:flex sm:space-x-2 sm:space-y-0\">\n                        <Form.Item\n                          label={t(\"mcpSettings.modal.headers.key.label\")}\n                          name={[field.name, \"key\"]}\n                          className=\"mb-0 flex-1\">\n                          <Input\n                            placeholder={t(\n                              \"mcpSettings.modal.headers.key.placeholder\"\n                            )}\n                          />\n                        </Form.Item>\n                        <Form.Item\n                          label={t(\"mcpSettings.modal.headers.value.label\")}\n                          name={[field.name, \"value\"]}\n                          className=\"mb-0 flex-1\">\n                          <Input\n                            placeholder={t(\n                              \"mcpSettings.modal.headers.value.placeholder\"\n                            )}\n                          />\n                        </Form.Item>\n                      </div>\n                      <button\n                        type=\"button\"\n                        className=\"shrink-0 p-1 text-red-500 dark:text-red-400\"\n                        onClick={() => remove(field.name)}>\n                        <Trash2Icon className=\"size-4\" />\n                      </button>\n                    </div>\n                  ))}\n                </div>\n              )}\n            </Form.List>\n\n            <button\n              type=\"submit\"\n              disabled={\n                addMutation.isPending ||\n                updateMutation.isPending ||\n                validateMutation.isPending\n              }\n              className=\"mt-4 inline-flex w-full items-center justify-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 disabled:cursor-not-allowed disabled:opacity-60 dark:bg-white dark:text-gray-900 dark:hover:bg-gray-100\">\n              {editingServer\n                ? t(\"mcpSettings.modal.update\")\n                : t(\"mcpSettings.modal.submit\")}\n            </button>\n\n            {!isValidationFresh ? (\n              <p className=\"mt-3 text-xs text-gray-500 dark:text-gray-400\">\n                {t(\"mcpSettings.modal.validation.saveHint\")}\n              </p>\n            ) : null}\n          </Form>\n        </Modal>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Option/Settings/memory.tsx",
    "content": "import { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\"\nimport { Empty, List, Modal, Input, Form, Popconfirm, Skeleton } from \"antd\"\nimport { DeleteOutlined, EditOutlined, PlusOutlined } from \"@ant-design/icons\"\nimport { useState } from \"react\"\nimport { useTranslation } from \"react-i18next\"\nimport {\n  getAllMemories,\n  addMemory,\n  updateMemory,\n  deleteMemory,\n  deleteAllMemories\n} from \"@/db/dexie/memory\"\nimport { Memory } from \"@/db/dexie/types\"\n\nexport const MemorySettings = () => {\n  const { t } = useTranslation(\"settings\")\n  const [isAddModalOpen, setIsAddModalOpen] = useState(false)\n  const [isEditModalOpen, setIsEditModalOpen] = useState(false)\n  const [selectedMemory, setSelectedMemory] = useState<Memory | null>(null)\n  const [addForm] = Form.useForm()\n  const [editForm] = Form.useForm()\n  const queryClient = useQueryClient()\n\n  const { data: memories, isLoading } = useQuery({\n    queryKey: [\"memories\"],\n    queryFn: getAllMemories\n  })\n\n  const { mutate: addMemoryMutation, isPending: isAddingMemory } = useMutation({\n    mutationFn: async (content: string) => {\n      return await addMemory(content)\n    },\n    onSuccess: () => {\n      queryClient.invalidateQueries({ queryKey: [\"memories\"] })\n      setIsAddModalOpen(false)\n      addForm.resetFields()\n    }\n  })\n\n  const { mutate: updateMemoryMutation, isPending: isUpdatingMemory } =\n    useMutation({\n      mutationFn: async ({ id, content }: { id: string; content: string }) => {\n        return await updateMemory(id, content)\n      },\n      onSuccess: () => {\n        queryClient.invalidateQueries({ queryKey: [\"memories\"] })\n        setIsEditModalOpen(false)\n        setSelectedMemory(null)\n        editForm.resetFields()\n      }\n    })\n\n  const { mutate: deleteMemoryMutation } = useMutation({\n    mutationFn: async (id: string) => {\n      return await deleteMemory(id)\n    },\n    onSuccess: () => {\n      queryClient.invalidateQueries({ queryKey: [\"memories\"] })\n    }\n  })\n\n  const { mutate: clearAllMemories } = useMutation({\n    mutationFn: async () => {\n      return await deleteAllMemories()\n    },\n    onSuccess: () => {\n      queryClient.invalidateQueries({ queryKey: [\"memories\"] })\n    }\n  })\n\n  const handleAddMemory = () => {\n    addForm.validateFields().then((values) => {\n      addMemoryMutation(values.content)\n    })\n  }\n\n  const handleEditMemory = () => {\n    editForm.validateFields().then((values) => {\n      if (selectedMemory) {\n        updateMemoryMutation({ id: selectedMemory.id, content: values.content })\n      }\n    })\n  }\n\n  const handleOpenEditModal = (memory: Memory) => {\n    setSelectedMemory(memory)\n    editForm.setFieldsValue({ content: memory.content })\n    setIsEditModalOpen(true)\n  }\n\n  return (\n    <div>\n      <div className=\"flex flex-row justify-between items-center mb-4\">\n        <h2 className=\"text-base font-semibold leading-7 text-gray-900 dark:text-white\">\n          {t(\"memory.title\", \"Memory (Experimental)\")}\n        </h2>\n        <div className=\"flex gap-2\">\n          {memories && memories.length > 0 && (\n            <Popconfirm\n              title={t(\"memory.clearAll.confirm\", \"Clear all memories?\")}\n              onConfirm={() => clearAllMemories()}\n              okText={t(\"memory.clearAll.ok\", \"Yes\")}\n              cancelText={t(\"memory.clearAll.cancel\", \"No\")}>\n              <button className=\"inline-flex items-center rounded-md border border-red-600 bg-transparent px-3 py-1.5 text-sm font-medium text-red-600 hover:bg-red-50 dark:border-red-500 dark:text-red-500 dark:hover:bg-red-950\">\n                {t(\"memory.clearAll.button\", \"Clear All\")}\n              </button>\n            </Popconfirm>\n          )}\n          <button\n            onClick={() => setIsAddModalOpen(true)}\n            className=\"inline-flex items-center gap-2 rounded-md border border-transparent bg-black px-3 py-1.5 text-sm font-medium text-white shadow-sm dark:bg-white dark:text-gray-800\">\n            <PlusOutlined />\n            {t(\"memory.add.button\", \"Add Memory\")}\n          </button>\n        </div>\n      </div>\n\n      <p className=\"text-sm text-gray-600 dark:text-gray-400 mb-4\">\n        {t(\n          \"memory.description\",\n          \"You can personalize your interactions with LLMs by adding memories, making them more helpful and tailored to you.\"\n        )}\n      </p>\n\n      <div className=\"border border-b border-gray-200 dark:border-gray-600 mb-6\"></div>\n\n      {isLoading && <Skeleton active paragraph={{ rows: 3 }} />}\n\n      {!isLoading && (!memories || memories.length === 0) && (\n        <Empty\n          description={t(\n            \"memory.empty\",\n            \"No memories yet. Add your first memory to get started.\"\n          )}\n        />\n      )}\n\n      {!isLoading && memories && memories.length > 0 && (\n        <List\n          dataSource={memories}\n          renderItem={(memory) => (\n            <List.Item\n              key={memory.id}\n              actions={[\n                <button\n                  key=\"edit\"\n                  onClick={() => handleOpenEditModal(memory)}\n                  className=\"inline-flex items-center p-2 text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white\">\n                  <EditOutlined />\n                </button>,\n                <Popconfirm\n                  key=\"delete\"\n                  title={t(\"memory.delete.confirm\", \"Delete this memory?\")}\n                  onConfirm={() => deleteMemoryMutation(memory.id)}\n                  okText={t(\"memory.delete.ok\", \"Yes\")}\n                  cancelText={t(\"memory.delete.cancel\", \"No\")}>\n                  <button className=\"inline-flex items-center p-2 text-red-600 hover:text-red-700 dark:text-red-500 dark:hover:text-red-400\">\n                    <DeleteOutlined />\n                  </button>\n                </Popconfirm>\n              ]}>\n              <List.Item.Meta\n                title={\n                  <div className=\"text-sm text-gray-900 dark:text-white\">\n                    {memory.content}\n                  </div>\n                }\n                description={\n                  <div className=\"text-xs text-gray-500 dark:text-gray-400\">\n                    {t(\"memory.created\", \"Created\")}{\": \"}\n                    {new Date(memory.createdAt).toLocaleString()}\n                    {memory.updatedAt !== memory.createdAt &&\n                      ` • ${t(\"memory.updated\", \"Updated\")}: ${new Date(memory.updatedAt).toLocaleString()}`}\n                  </div>\n                }\n              />\n            </List.Item>\n          )}\n        />\n      )}\n\n      <Modal\n        title={t(\"memory.add.title\", \"Add New Memory\")}\n        open={isAddModalOpen}\n        onOk={handleAddMemory}\n        onCancel={() => {\n          setIsAddModalOpen(false)\n          addForm.resetFields()\n        }}\n        confirmLoading={isAddingMemory}\n        okText={t(\"memory.add.ok\", \"Add\")}\n        cancelText={t(\"memory.add.cancel\", \"Cancel\")}\n        okButtonProps={{\n          className: \"bg-black text-white hover:bg-gray-800 dark:bg-white dark:text-gray-800 dark:hover:bg-gray-200\"\n        }}\n        cancelButtonProps={{\n          className: \"border-gray-300 text-gray-700 hover:border-gray-400 dark:border-gray-600 dark:text-gray-300 dark:hover:border-gray-500\"\n        }}>\n        <Form form={addForm} layout=\"vertical\" className=\"mt-4\">\n          <Form.Item\n            name=\"content\"\n            label={t(\"memory.add.label\", \"Memory Content\")}\n            rules={[\n              {\n                required: true,\n                message: t(\n                  \"memory.add.required\",\n                  \"Please enter memory content\"\n                )\n              },\n              {\n                min: 3,\n                message: t(\n                  \"memory.add.minLength\",\n                  \"Memory must be at least 3 characters\"\n                )\n              }\n            ]}>\n            <Input.TextArea\n              rows={4}\n              placeholder={t(\n                \"memory.add.placeholder\",\n                \"e.g., I prefer code examples in TypeScript\"\n              )}\n            />\n          </Form.Item>\n        </Form>\n      </Modal>\n\n      <Modal\n        title={t(\"memory.edit.title\", \"Edit Memory\")}\n        open={isEditModalOpen}\n        onOk={handleEditMemory}\n        onCancel={() => {\n          setIsEditModalOpen(false)\n          setSelectedMemory(null)\n          editForm.resetFields()\n        }}\n        confirmLoading={isUpdatingMemory}\n        okText={t(\"memory.edit.ok\", \"Save\")}\n        cancelText={t(\"memory.edit.cancel\", \"Cancel\")}\n        okButtonProps={{\n          className: \"bg-black text-white hover:bg-gray-800 dark:bg-white dark:text-gray-800 dark:hover:bg-gray-200\"\n        }}\n        cancelButtonProps={{\n          className: \"border-gray-300 text-gray-700 hover:border-gray-400 dark:border-gray-600 dark:text-gray-300 dark:hover:border-gray-500\"\n        }}>\n        <Form form={editForm} layout=\"vertical\" className=\"mt-4\">\n          <Form.Item\n            name=\"content\"\n            label={t(\"memory.edit.label\", \"Memory Content\")}\n            rules={[\n              {\n                required: true,\n                message: t(\n                  \"memory.edit.required\",\n                  \"Please enter memory content\"\n                )\n              },\n              {\n                min: 3,\n                message: t(\n                  \"memory.edit.minLength\",\n                  \"Memory must be at least 3 characters\"\n                )\n              }\n            ]}>\n            <Input.TextArea rows={4} />\n          </Form.Item>\n        </Form>\n      </Modal>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Option/Settings/model-settings.tsx",
    "content": "import { BetaTag } from \"@/components/Common/Beta\"\nimport { SaveButton } from \"@/components/Common/SaveButton\"\nimport { getAllModelSettings, setModelSetting } from \"@/services/model-settings\"\nimport { useQuery, useQueryClient } from \"@tanstack/react-query\"\nimport { Form, Skeleton, Input, InputNumber, Collapse, Switch } from \"antd\"\nimport React from \"react\"\nimport { useTranslation } from \"react-i18next\"\n\nexport const ModelSettings = () => {\n  const { t } = useTranslation(\"common\")\n  const [form] = Form.useForm()\n  const client = useQueryClient()\n  const { isPending: isLoading } = useQuery({\n    queryKey: [\"fetchModelConfig\"],\n    queryFn: async () => {\n      const data = await getAllModelSettings()\n      form.setFieldsValue(data)\n      return data\n    }\n  })\n\n  return (\n    <div>\n      <div>\n        <div className=\"inline-flex items-center gap-2\">\n          <BetaTag />\n          <h2 className=\"text-base font-semibold leading-7 text-gray-900 dark:text-white\">\n            {t(\"modelSettings.label\")}\n          </h2>\n        </div>\n        <p className=\"text-sm text-gray-700 dark:text-neutral-400 mt-1\">\n          {t(\"modelSettings.description\")}\n        </p>\n        <div className=\"border border-b border-gray-200 dark:border-gray-600 mt-3 mb-6\"></div>\n      </div>\n      {!isLoading ? (\n        <Form\n          onFinish={(values: {\n            keepAlive: string\n            temperature: number\n            topK: number\n            topP: number\n            numGpu: number\n          }) => {\n            Object.entries(values).forEach(([key, value]) => {\n              setModelSetting(key, value)\n            })\n            client.invalidateQueries({\n              queryKey: [\"fetchModelConfig\"]\n            })\n          }}\n          form={form}\n          layout=\"vertical\">\n          <Form.Item\n            name=\"keepAlive\"\n            help={t(\"modelSettings.form.keepAlive.help\")}\n            label={t(\"modelSettings.form.keepAlive.label\")}>\n            <Input\n              size=\"large\"\n              placeholder={t(\"modelSettings.form.keepAlive.placeholder\")}\n            />\n          </Form.Item>\n          <Form.Item\n            name=\"temperature\"\n            label={t(\"modelSettings.form.temperature.label\")}>\n            <InputNumber\n              size=\"large\"\n              style={{ width: \"100%\" }}\n              placeholder={t(\"modelSettings.form.temperature.placeholder\")}\n            />\n          </Form.Item>\n\n          <Form.Item name=\"numCtx\" label={t(\"modelSettings.form.numCtx.label\")}>\n            <InputNumber\n              style={{ width: \"100%\" }}\n              placeholder={t(\"modelSettings.form.numCtx.placeholder\")}\n              size=\"large\"\n            />\n          </Form.Item>\n          <Form.Item\n            name=\"numPredict\"\n            label={t(\"modelSettings.form.numPredict.label\")}>\n            <InputNumber\n              style={{ width: \"100%\" }}\n              placeholder={t(\"modelSettings.form.numPredict.placeholder\")}\n            />\n          </Form.Item>\n          <Collapse\n            ghost\n            className=\"border-none bg-transparent\"\n            items={[\n              {\n                key: \"1\",\n                label: t(\"modelSettings.advanced\"),\n                children: (\n                  <React.Fragment>\n                    <Form.Item\n                      name=\"topK\"\n                      label={t(\"modelSettings.form.topK.label\")}>\n                      <InputNumber\n                        style={{ width: \"100%\" }}\n                        placeholder={t(\"modelSettings.form.topK.placeholder\")}\n                        size=\"large\"\n                      />\n                    </Form.Item>\n\n                    <Form.Item\n                      name=\"topP\"\n                      label={t(\"modelSettings.form.topP.label\")}>\n                      <InputNumber\n                        style={{ width: \"100%\" }}\n                        size=\"large\"\n                        placeholder={t(\"modelSettings.form.topP.placeholder\")}\n                      />\n                    </Form.Item>\n                    <Form.Item\n                      name=\"numGpu\"\n                      label={t(\"modelSettings.form.numGpu.label\")}>\n                      <InputNumber\n                        style={{ width: \"100%\" }}\n                        size=\"large\"\n                        placeholder={t(\"modelSettings.form.numGpu.placeholder\")}\n                      />\n                    </Form.Item>\n                    <Form.Item\n                      name=\"minP\"\n                      label={t(\"modelSettings.form.minP.label\")}>\n                      <InputNumber\n                        style={{ width: \"100%\" }}\n                        placeholder={t(\"modelSettings.form.minP.placeholder\")}\n                      />\n                    </Form.Item>\n                    <Form.Item\n                      name=\"repeatPenalty\"\n                      label={t(\"modelSettings.form.repeatPenalty.label\")}>\n                      <InputNumber\n                        style={{ width: \"100%\" }}\n                        placeholder={t(\n                          \"modelSettings.form.repeatPenalty.placeholder\"\n                        )}\n                      />\n                    </Form.Item>\n                    <Form.Item\n                      name=\"repeatLastN\"\n                      label={t(\"modelSettings.form.repeatLastN.label\")}>\n                      <InputNumber\n                        style={{ width: \"100%\" }}\n                        placeholder={t(\n                          \"modelSettings.form.repeatLastN.placeholder\"\n                        )}\n                      />\n                    </Form.Item>\n                    <Form.Item\n                      name=\"tfsZ\"\n                      label={t(\"modelSettings.form.tfsZ.label\")}>\n                      <InputNumber\n                        style={{ width: \"100%\" }}\n                        placeholder={t(\"modelSettings.form.tfsZ.placeholder\")}\n                      />\n                    </Form.Item>\n                    <Form.Item\n                      name=\"numKeep\"\n                      label={t(\"modelSettings.form.numKeep.label\")}>\n                      <InputNumber\n                        style={{ width: \"100%\" }}\n                        placeholder={t(\n                          \"modelSettings.form.numKeep.placeholder\"\n                        )}\n                      />\n                    </Form.Item>\n                    <Form.Item\n                      name=\"numThread\"\n                      label={t(\"modelSettings.form.numThread.label\")}>\n                      <InputNumber\n                        style={{ width: \"100%\" }}\n                        placeholder={t(\n                          \"modelSettings.form.numThread.placeholder\"\n                        )}\n                      />\n                    </Form.Item>\n                    <Form.Item\n                      name=\"useMMap\"\n                      label={t(\"modelSettings.form.useMMap.label\")}>\n                      <Switch />\n                    </Form.Item>\n                    <Form.Item\n                      name=\"useMlock\"\n                      label={t(\"modelSettings.form.useMlock.label\")}>\n                      <Switch />\n                    </Form.Item>\n                  </React.Fragment>\n                )\n              }\n            ]}\n          />\n\n          <div className=\"flex justify-end\">\n            <SaveButton btnType=\"submit\" />\n          </div>\n        </Form>\n      ) : (\n        <Skeleton active />\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Option/Settings/ollama.tsx",
    "content": "import { useQuery } from \"@tanstack/react-query\"\nimport { Collapse, Skeleton, Switch } from \"antd\"\nimport { useState } from \"react\"\nimport { SaveButton } from \"~/components/Common/SaveButton\"\nimport { getOllamaURL, setOllamaURL as saveOllamaURL } from \"~/services/ollama\"\nimport { Trans, useTranslation } from \"react-i18next\"\nimport { AdvanceOllamaSettings } from \"@/components/Common/Settings/AdvanceOllamaSettings\"\nimport { ModelSettings } from \"./model-settings\"\nimport { useStorage } from \"@plasmohq/storage/hook\"\nimport { AlertCircleIcon } from \"lucide-react\"\nimport { Link } from \"react-router-dom\"\nexport const SettingsOllama = () => {\n  const [ollamaURL, setOllamaURL] = useState<string>(\"\")\n  const [ollamaEnabled, setOllamaEnabled] = useStorage(\n    \"ollamaEnabledStatus\",\n    true\n  )\n  const [_, setCheckOllamaStatus] = useStorage(\"checkOllamaStatus\", true)\n  const { t } = useTranslation(\"settings\")\n\n  const { status } = useQuery({\n    queryKey: [\"fetchOllamURL\"],\n    queryFn: async () => {\n      try {\n        const [ollamaURL] = await Promise.all([getOllamaURL()])\n        setOllamaURL(ollamaURL)\n        return {}\n      } catch (e) {\n        console.error(e)\n        return {}\n      }\n    }\n  })\n\n  return (\n    <div className=\"flex flex-col space-y-3\">\n      {status === \"pending\" && <Skeleton paragraph={{ rows: 4 }} active />}\n      {status === \"success\" && (\n        <div className=\"flex flex-col space-y-6\">\n          <div>\n            <div>\n              <h2 className=\"text-base font-semibold leading-7 text-gray-900 dark:text-white\">\n                {t(\"ollamaSettings.heading\")}\n              </h2>\n              <div className=\"border border-b border-gray-200 dark:border-gray-600 mt-3 mb-6\"></div>\n            </div>\n            <div className=\"mb-3\">\n              <label\n                htmlFor=\"ollamaURL\"\n                className=\"text-sm font-medium dark:text-gray-200\">\n                {t(\"ollamaSettings.settings.ollamaUrl.label\")}\n              </label>\n              <input\n                type=\"url\"\n                id=\"ollamaURL\"\n                value={ollamaURL}\n                onChange={(e) => {\n                  setOllamaURL(e.target.value)\n                }}\n                placeholder={t(\"ollamaSettings.settings.ollamaUrl.placeholder\")}\n                className=\"w-full p-2 border border-gray-300 rounded-md dark:bg-[#262626] dark:text-gray-100\"\n              />\n            </div>\n            <div className=\"flex justify-end mb-3\">\n              <SaveButton\n                onClick={() => {\n                  saveOllamaURL(ollamaURL)\n                }}\n                className=\"mt-2\"\n              />\n            </div>\n            <Collapse\n              size=\"small\"\n              items={[\n                {\n                  key: \"1\",\n                  label: (\n                    <div>\n                      <h2 className=\"text-base font-semibold leading-7 text-gray-900 dark:text-white\">\n                        {t(\"ollamaSettings.settings.advanced.label\")}\n                      </h2>\n                      <p className=\"text-xs text-gray-700 dark:text-gray-400 mb-4\">\n                        <Trans\n                          i18nKey=\"settings:ollamaSettings.settings.advanced.help\"\n                          components={{\n                            anchor: (\n                              <a\n                                href=\"https://github.com/n4ze3m/page-assist/blob/main/docs/connection-issue.md#solutions\"\n                                target=\"__blank\"\n                                className=\"text-blue-600 dark:text-blue-400\"></a>\n                            )\n                          }}\n                        />\n                      </p>\n                    </div>\n                  ),\n                  children: <AdvanceOllamaSettings />\n                }\n              ]}\n            />\n            <div className=\"mt-8 mb-4\">\n              <div className=\"flex items-center justify-between\">\n                <label className=\"text-sm font-medium dark:text-gray-200\">\n                  {t(\"ollamaSettings.settings.globalEnable.label\")}\n                </label>\n                <Switch\n                  checked={ollamaEnabled}\n                  onChange={(checked) => {\n                    setOllamaEnabled(checked)\n                    setCheckOllamaStatus(checked)\n                  }}\n                />\n              </div>\n              {!ollamaEnabled && (\n                <div className=\"mt-2 p-3 bg-yellow-50 dark:bg-yellow-900/30 rounded-md\">\n                  <div className=\"flex\">\n                    <div className=\"flex-shrink-0\">\n                      <AlertCircleIcon className=\"h-5 w-5 text-yellow-400 dark:text-yellow-500\" />\n                    </div>\n                    <div className=\"ml-3\">\n                      <p className=\"text-sm text-yellow-800 dark:text-yellow-200\">\n                        <Trans\n                          i18nKey=\"settings:ollamaSettings.settings.globalEnable.warning\"\n                          components={{\n                            anchor: (\n                              <Link\n                                to=\"/settings/openai\"\n                                className=\"text-blue-600 dark:text-blue-400\"></Link>\n                            )\n                          }}\n                        />\n\n                        {/* {t(\"ollamaSettings.settings.globalEnable.warning\")} */}\n                      </p>\n                    </div>\n                  </div>\n                </div>\n              )}{\" \"}\n            </div>\n          </div>\n\n          <ModelSettings />\n        </div>\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Option/Settings/openai-fetch-model.tsx",
    "content": "import { getOpenAIConfigById } from \"@/db/dexie/openai\"\nimport { getAllOpenAIModels } from \"@/libs/openai\"\nimport { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\"\nimport { useTranslation } from \"react-i18next\"\nimport { Checkbox, Input, Spin, message, Radio } from \"antd\"\nimport { useState, useMemo } from \"react\"\nimport { createManyModels } from \"@/db/dexie/models\"\nimport { Popover } from \"antd\"\nimport { InfoIcon } from \"lucide-react\"\n\ntype Props = {\n  openaiId: string\n  setOpenModelModal: (openModelModal: boolean) => void\n}\n\nexport const OpenAIFetchModel = ({ openaiId, setOpenModelModal }: Props) => {\n  const { t } = useTranslation([\"openai\"])\n  const [selectedModels, setSelectedModels] = useState<string[]>([])\n  const [searchTerm, setSearchTerm] = useState(\"\")\n  const [modelType, setModelType] = useState(\"chat\")\n  const queryClient = useQueryClient()\n\n  const { data, status } = useQuery({\n    queryKey: [\"openAIConfigs\", openaiId],\n    queryFn: async () => {\n      const config = await getOpenAIConfigById(openaiId)\n      const models = await getAllOpenAIModels({\n        baseUrl: config.baseUrl,\n        apiKey: config.apiKey,\n        customHeaders: config.headers\n      })\n      return models\n    },\n    enabled: !!openaiId\n  })\n  const filteredModels = useMemo(() => {\n    return (\n      data?.filter((model) =>\n        (model.name ?? model.id)\n          .toLowerCase()\n          .includes(searchTerm.toLowerCase())\n      ) || []\n    )\n  }, [data, searchTerm])\n\n  const handleSelectAll = (checked: boolean) => {\n    if (checked) {\n      setSelectedModels(filteredModels.map((model) => model.id))\n    } else {\n      setSelectedModels([])\n    }\n  }\n\n  const handleModelSelect = (modelId: string, checked: boolean) => {\n    if (checked) {\n      setSelectedModels((prev) => [...prev, modelId])\n    } else {\n      setSelectedModels((prev) => prev.filter((id) => id !== modelId))\n    }\n  }\n\n  const onSave = async (models: string[]) => {\n    const payload = models.map((id) => ({\n      model_id: id,\n      name: filteredModels.find((model) => model.id === id)?.name ?? id,\n      provider_id: openaiId,\n      model_type: modelType\n    }))\n\n    await createManyModels(payload)\n\n    return true\n  }\n\n  const { mutate: saveModels, isPending: isSaving } = useMutation({\n    mutationFn: onSave,\n    onSuccess: () => {\n      setOpenModelModal(false)\n      queryClient.invalidateQueries({\n        queryKey: [\"fetchModel\"]\n      })\n      message.success(t(\"modal.model.success\"))\n    }\n  })\n\n  const handleSave = () => {\n    saveModels(selectedModels)\n  }\n\n  if (status === \"pending\") {\n    return (\n      <div className=\"flex items-center justify-center h-40\">\n        <Spin size=\"large\" />\n      </div>\n    )\n  }\n  if (status === \"error\" || !data || data.length === 0) {\n    return (\n      <div className=\"flex items-center justify-center h-40\">\n        <p className=\"text-md text-center text-gray-600 dark:text-gray-300\">\n          {t(\"noModelFound\")}\n        </p>\n      </div>\n    )\n  }\n  return (\n    <div className=\"space-y-4\">\n      <p className=\"text-sm text-gray-500 dark:text-gray-400\">\n        {t(\"modal.model.subheading\")}\n      </p>\n\n      <Input\n        placeholder={t(\"searchModel\")}\n        value={searchTerm}\n        onChange={(e) => setSearchTerm(e.target.value)}\n        className=\"w-full\"\n      />\n      <div className=\"flex  justify-between\">\n        <Checkbox\n          checked={selectedModels.length === filteredModels.length}\n          indeterminate={\n            selectedModels.length > 0 &&\n            selectedModels.length < filteredModels.length\n          }\n          onChange={(e) => handleSelectAll(e.target.checked)}>\n          {t(\"selectAll\")}\n        </Checkbox>\n        <div className=\"inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200\">\n          {`${selectedModels?.length} / ${data?.length}`}\n        </div>\n      </div>\n      <div className=\"space-y-2 custom-scrollbar max-h-[300px] border overflow-y-auto dark:border-gray-600 rounded-md p-3\">\n        <div className=\"divide-y divide-gray-200 dark:divide-gray-700\">\n          {filteredModels.map((model, idx) => (\n            <div\n              key={idx}\n              onClick={() => {\n                handleModelSelect(model.id, !selectedModels.includes(model.id))\n              }}\n              className=\"flex cursor-pointer items-center justify-between py-3 hover:bg-gray-50 dark:hover:bg-gray-800 px-2 \">\n              <div className=\"flex items-center space-x-3\">\n                <Checkbox\n                  checked={selectedModels.includes(model.id)}\n                  onChange={(e) =>\n                    handleModelSelect(model.id, e.target.checked)\n                  }\n                />\n                <div className=\"flex flex-col\">\n                  <span className=\"text-sm font-medium text-gray-900 dark:text-gray-100\">\n                    {`${model?.name || model.id}`.replaceAll(\n                      /accounts\\/[^\\/]+\\/models\\//g,\n                      \"\"\n                    )}\n                  </span>\n                  <span className=\"text-xs text-gray-500 dark:text-gray-400\">\n                    {model.id}\n                  </span>\n                </div>\n              </div>\n            </div>\n          ))}\n        </div>\n      </div>\n\n      <div className=\"flex items-center\">\n        <Radio.Group\n          onChange={(e) => setModelType(e.target.value)}\n          value={modelType}>\n          <Radio value=\"chat\">{t(\"radio.chat\")}</Radio>\n          <Radio value=\"embedding\">{t(\"radio.embedding\")}</Radio>\n        </Radio.Group>\n        <Popover\n          content={\n            <div>\n              <p>\n                <b className=\"text-gray-800 dark:text-gray-100\">\n                  {t(\"radio.chat\")}\n                </b>{\" \"}\n                {t(\"radio.chatInfo\")}\n              </p>\n              <p>\n                <b className=\"text-gray-800 dark:text-gray-100\">\n                  {t(\"radio.embedding\")}\n                </b>{\" \"}\n                {t(\"radio.embeddingInfo\")}\n              </p>\n            </div>\n          }>\n          <InfoIcon className=\"ml-2 h-4 w-4 text-gray-500 cursor-pointer\" />\n        </Popover>\n      </div>\n\n      <button\n        onClick={handleSave}\n        disabled={isSaving}\n        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\">\n        {isSaving ? t(\"saving\") : t(\"save\")}\n      </button>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Option/Settings/openai.tsx",
    "content": "/**\n * The `OpenAIApp` component is the main entry point for the OpenAI configuration management functionality in the application.\n * It provides a user interface for adding, editing, deleting, and refetching OpenAI configurations.\n * The component uses React Query to manage the state and perform CRUD operations on the OpenAI configurations.\n * It also includes a modal for fetching the available models from the selected OpenAI configuration.\n */\nimport {\n  Form,\n  Input,\n  Modal,\n  Table,\n  message,\n  Tooltip,\n  Select,\n  Switch,\n  notification\n} from \"antd\"\nimport { useState } from \"react\"\nimport { useWatch } from \"antd/es/form/Form\"\nimport { useTranslation } from \"react-i18next\"\nimport {\n  addOpenAICofig,\n  getAllOpenAIConfig,\n  deleteOpenAIConfig,\n  updateOpenAIConfig\n} from \"@/db/dexie/openai\"\nimport { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\"\nimport { Pencil, Trash2, DownloadIcon, Trash2Icon } from \"lucide-react\"\nimport { OpenAIFetchModel } from \"./openai-fetch-model\"\nimport { OAI_API_PROVIDERS } from \"@/utils/oai-api-providers\"\nimport { ProviderIcons } from \"@/components/Common/ProviderIcon\"\nconst noPopupProvider = [\"lmstudio\", \"llamafile\", \"ollama2\", \"llamacpp\", \"vllm\"]\nimport { isFireFoxPrivateMode } from \"@/utils/is-private-mode\"\nimport { setProviderState, getAllProviderStates } from \"@/db/dexie/providerState\"\nimport { useState as useReactState, useEffect } from \"react\"\n\nexport const OpenAIApp = () => {\n  const { t } = useTranslation([\"openai\", \"settings\"])\n  const [open, setOpen] = useState(false)\n  const [editingConfig, setEditingConfig] = useState(null)\n  const queryClient = useQueryClient()\n  const [form] = Form.useForm()\n  const [openaiId, setOpenaiId] = useState<string | null>(null)\n  const [openModelModal, setOpenModelModal] = useState(false)\n  const [provider, setProvider] = useState(\"custom\")\n  const [providerStates, setProviderStates] = useReactState<Record<string, boolean>>({})\n\n  const { data: configs, isLoading } = useQuery({\n    queryKey: [\"openAIConfigs\"],\n    queryFn: getAllOpenAIConfig\n  })\n\n  useEffect(() => {\n    const loadProviderStates = async () => {\n      const states = await getAllProviderStates()\n      setProviderStates(states)\n    }\n    loadProviderStates()\n  }, [])\n\n  const handleToggleProvider = async (providerId: string, isEnabled: boolean) => {\n    await setProviderState(providerId, isEnabled)\n    const states = await getAllProviderStates()\n    setProviderStates(states)\n    // Invalidate model queries to refresh the list\n    queryClient.invalidateQueries({\n      queryKey: [\"fetchModel\"]\n    })\n    queryClient.invalidateQueries({\n      queryKey: [\"fetchAllModels\"]\n    })\n  }\n\n  const addMutation = useMutation({\n    mutationFn: addOpenAICofig,\n    onSuccess: (data) => {\n      queryClient.invalidateQueries({\n        queryKey: [\"openAIConfigs\"]\n      })\n      setOpen(false)\n      message.success(t(\"addSuccess\"))\n      if (!noPopupProvider.includes(provider)) {\n        setOpenaiId(data)\n        setOpenModelModal(true)\n      }\n      form.resetFields()\n      setProvider(\"custom\")\n    }\n  })\n\n  const updateMutation = useMutation({\n    mutationFn: updateOpenAIConfig,\n    onSuccess: () => {\n      queryClient.invalidateQueries({\n        queryKey: [\"openAIConfigs\"]\n      })\n      setOpen(false)\n      form.resetFields()\n      setEditingConfig(null)\n      message.success(t(\"updateSuccess\"))\n    }\n  })\n\n  const deleteMutation = useMutation({\n    mutationFn: deleteOpenAIConfig,\n    onSuccess: () => {\n      queryClient.invalidateQueries({\n        queryKey: [\"openAIConfigs\"]\n      })\n      message.success(t(\"deleteSuccess\"))\n    }\n  })\n\n  const handleSubmit = (values: {\n    id?: string\n    name: string\n    baseUrl: string\n    apiKey: string\n    fix_cors?: boolean\n    headers?: { key: string; value: string }[]\n  }) => {\n    if (editingConfig) {\n      updateMutation.mutate({\n        id: editingConfig.id,\n        ...values\n      })\n    } else {\n      addMutation.mutate({\n        ...values,\n        provider\n      })\n    }\n  }\n\n  const handleEdit = (record: any) => {\n    setEditingConfig({\n      ...record,\n      headers: record?.headers || []\n    })\n    form.setFieldsValue({\n      ...record,\n      headers: record?.headers || [],\n      fix_cors: record?.fix_cors || false\n    })\n    setOpen(true)\n  }\n\n  const handleDelete = (id: string) => {\n    deleteMutation.mutate(id)\n  }\n\n  const baseUrl = useWatch(\"baseUrl\", form)\n  if (!editingConfig && baseUrl && provider === \"custom\") {\n    const matchedProvider = OAI_API_PROVIDERS.find(\n      (p) => p.baseUrl.toLowerCase() === baseUrl.toLowerCase()\n    )\n    if (matchedProvider) {\n      setProvider(matchedProvider.value)\n    }\n  }\n\n  return (\n    <div className=\"w-full max-w-full overflow-hidden\">\n      <div className=\"px-2 sm:px-0\">\n        <div className=\"mb-6\">\n          <div className=\"flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3\">\n            <div className=\"flex-1\">\n              <h2 className=\"text-base font-semibold leading-7 text-gray-900 dark:text-white\">\n                {t(\"heading\")}\n              </h2>\n              <p className=\"mt-1 text-sm leading-6 text-gray-600 dark:text-gray-400\">\n                {t(\"subheading\")}\n              </p>\n            </div>\n            <div className=\"flex-shrink-0\">\n              <button\n                onClick={() => {\n                  if (isFireFoxPrivateMode) {\n                    notification.error({\n                      message: \"Page Assist can't save data\",\n                      description:\n                        \"Firefox Private Mode does not support saving data to IndexedDB. Please add OpenAI configurations from a normal window.\"\n                    })\n                    return\n                  }\n                  form.resetFields()\n                  setEditingConfig(null)\n                  setOpen(true)\n                }}\n                className=\"inline-flex items-center justify-center rounded-md border border-transparent bg-black px-3 py-2 text-sm 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 w-full sm:w-auto\">\n                {t(\"addBtn\")}\n              </button>\n            </div>\n          </div>\n          <div className=\"border border-b border-gray-200 dark:border-gray-600 mt-4 mb-0\"></div>\n        </div>\n\n        <Table\n          columns={[\n            {\n              title: t(\"table.name\"),\n              dataIndex: \"name\",\n              key: \"name\",\n              ellipsis: true\n            },\n            {\n              title: t(\"table.baseUrl\"),\n              dataIndex: \"baseUrl\",\n              key: \"baseUrl\",\n              render: (text) => (\n                <span className=\"truncate block\" title={text}>\n                  {text}\n                </span>\n              )\n            },\n            {\n              title: t(\"table.actions\"),\n              key: \"actions\",\n              render: (_, record) => {\n                const isEnabled = providerStates[record.id] ?? true\n                return (\n                  <div className=\"flex gap-2 sm:gap-4 justify-start items-center\">\n                    <Tooltip title={isEnabled ? \"Disable provider\" : \"Enable provider\"}>\n                      <Switch\n                        checked={isEnabled}\n                        size=\"small\"\n                        disabled={isFireFoxPrivateMode}\n                        onChange={(checked) => {\n                          handleToggleProvider(record.id, checked)\n                        }}\n                      />\n                    </Tooltip>\n                    <Tooltip title={t(\"edit\")}>\n                      <button\n                        className=\"text-gray-700 dark:text-gray-400 disabled:opacity-50 p-1\"\n                        disabled={isFireFoxPrivateMode}\n                        onClick={() => handleEdit(record)}>\n                        <Pencil className=\"size-4\" />\n                      </button>\n                    </Tooltip>\n\n                    <Tooltip\n                      title={\n                        !noPopupProvider.includes(record.provider)\n                          ? t(\"newModel\")\n                          : t(\"noNewModel\")\n                      }>\n                      <button\n                        className=\"text-gray-700 dark:text-gray-400 disabled:opacity-50 p-1\"\n                        onClick={() => {\n                          setOpenModelModal(true)\n                          setOpenaiId(record.id)\n                        }}\n                        disabled={\n                          !record.id ||\n                          noPopupProvider.includes(record.provider) ||\n                          isFireFoxPrivateMode\n                        }>\n                        <DownloadIcon className=\"size-4\" />\n                      </button>\n                    </Tooltip>\n\n                    <Tooltip title={t(\"delete\")}>\n                      <button\n                        className=\"text-red-500 dark:text-red-400 disabled:opacity-50 p-1\"\n                        disabled={isFireFoxPrivateMode}\n                        onClick={() => {\n                          // add confirmation here\n                          if (\n                            confirm(\n                              t(\"modal.deleteConfirm\", {\n                                name: record.name\n                              })\n                            )\n                          ) {\n                            handleDelete(record.id)\n                          }\n                        }}>\n                        <Trash2 className=\"size-4\" />\n                      </button>\n                    </Tooltip>\n                  </div>\n                )\n              }\n            }\n          ]}\n          dataSource={configs}\n          loading={isLoading}\n          rowKey=\"id\"\n          bordered\n          scroll={{ x: 600 }}\n          className=\"[&_.ant-table]:text-sm\"\n        />\n\n        <Modal\n          open={open}\n          title={editingConfig ? t(\"modal.titleEdit\") : t(\"modal.titleAdd\")}\n          onCancel={() => {\n            form.resetFields()\n            setOpen(false)\n            setEditingConfig(null)\n            setProvider(\"custom\")\n          }}\n          footer={null}>\n          {!editingConfig && (\n            <Select\n              value={provider}\n              onSelect={(e) => {\n                const value = OAI_API_PROVIDERS.find((item) => item.value === e)\n                form.setFieldsValue({\n                  baseUrl: value?.baseUrl,\n                  name: value?.label\n                })\n                setProvider(e)\n              }}\n              filterOption={(input, option) => {\n                //@ts-ignore\n                return (\n                  option?.label?.props[\"data-title\"]\n                    ?.toLowerCase()\n                    ?.indexOf(input.toLowerCase()) >= 0\n                )\n              }}\n              showSearch\n              className=\"w-full !mb-4\"\n              size=\"large\"\n              options={OAI_API_PROVIDERS.map((e) => ({\n                value: e.value,\n                label: (\n                  <span\n                    key={e.value}\n                    data-title={e.label}\n                    className=\"flex flex-row gap-3 items-center\">\n                    <ProviderIcons\n                      provider={e.value}\n                      className=\"size-5 flex-shrink-0\"\n                    />\n                    <span className=\"line-clamp-2 text-sm\">{e.label}</span>\n                  </span>\n                )\n              }))}\n            />\n          )}\n          <Form\n            form={form}\n            layout=\"vertical\"\n            onFinish={handleSubmit}\n            initialValues={{ ...editingConfig }}>\n            <Form.Item\n              name=\"name\"\n              label={t(\"modal.name.label\")}\n              rules={[\n                {\n                  required: true,\n                  message: t(\"modal.name.required\")\n                }\n              ]}>\n              <Input size=\"large\" placeholder={t(\"modal.name.placeholder\")} />\n            </Form.Item>\n\n            <Form.Item\n              name=\"baseUrl\"\n              label={t(\"modal.baseUrl.label\")}\n              help={t(\"modal.baseUrl.help\")}\n              rules={[\n                {\n                  required: true,\n                  message: t(\"modal.baseUrl.required\")\n                }\n              ]}>\n              <Input\n                size=\"large\"\n                placeholder={t(\"modal.baseUrl.placeholder\")}\n              />\n            </Form.Item>\n\n            <Form.Item name=\"apiKey\" label={t(\"modal.apiKey.label\")}>\n              <Input.Password\n                size=\"large\"\n                placeholder={t(\"modal.apiKey.placeholder\")}\n              />\n            </Form.Item>\n\n            <Form.Item\n              name=\"fix_cors\"\n              label={t(\"modal.fixCors.label\", {\n                defaultValue: \"Fix CORS issues\"\n              })}\n              valuePropName=\"checked\">\n              <Switch />\n            </Form.Item>\n\n            <Form.List name=\"headers\">\n              {(fields, { add, remove }) => (\n                <div className=\"flex flex-col\">\n                  <div className=\"flex justify-between items-center mb-3\">\n                    <h3 className=\"text-sm font-semibold\">\n                      {t(\n                        \"settings:ollamaSettings.settings.advanced.headers.label\"\n                      )}\n                    </h3>\n                    <button\n                      type=\"button\"\n                      className=\"dark:bg-white dark:text-black text-white bg-black px-2 py-1 text-xs rounded-md\"\n                      onClick={() => {\n                        add()\n                      }}>\n                      {t(\n                        \"settings:ollamaSettings.settings.advanced.headers.add\"\n                      )}\n                    </button>\n                  </div>\n                  {fields.map((field, index) => (\n                    <div\n                      key={field.key}\n                      className=\"flex flex-col sm:flex-row items-start sm:items-end gap-2 mb-3\">\n                      <div className=\"flex-grow w-full space-y-2 sm:space-y-0 sm:space-x-2 sm:flex\">\n                        <Form.Item\n                          label={t(\n                            \"settings:ollamaSettings.settings.advanced.headers.key.label\"\n                          )}\n                          name={[field.name, \"key\"]}\n                          className=\"flex-1 mb-0 w-full\">\n                          <Input\n                            className=\"w-full\"\n                            placeholder={t(\n                              \"settings:ollamaSettings.settings.advanced.headers.key.placeholder\"\n                            )}\n                          />\n                        </Form.Item>\n                        <Form.Item\n                          label={t(\n                            \"settings:ollamaSettings.settings.advanced.headers.value.label\"\n                          )}\n                          name={[field.name, \"value\"]}\n                          className=\"flex-1 mb-0 w-full\">\n                          <Input\n                            className=\"w-full\"\n                            placeholder={t(\n                              \"settings:ollamaSettings.settings.advanced.headers.value.placeholder\"\n                            )}\n                          />\n                        </Form.Item>\n                      </div>\n                      <button\n                        type=\"button\"\n                        onClick={() => {\n                          remove(field.name)\n                        }}\n                        className=\"shrink-0 p-1 text-red-500 dark:text-red-400 sm:ml-2 self-start sm:self-auto\">\n                        <Trash2Icon className=\"w-4 h-4\" />\n                      </button>\n                    </div>\n                  ))}\n                </div>\n              )}\n            </Form.List>\n            {provider === \"lmstudio\" && (\n              <div className=\"text-xs text-gray-600 dark:text-gray-400 mb-4 p-2 bg-gray-50 dark:bg-gray-800 rounded-md\">\n                {t(\"modal.tipLMStudio\")}\n              </div>\n            )}\n            <button\n              type=\"submit\"\n              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\">\n              {editingConfig ? t(\"modal.update\") : t(\"modal.submit\")}\n            </button>\n          </Form>\n        </Modal>\n\n        <Modal\n          open={openModelModal}\n          title={t(\"modal.model.title\")}\n          footer={null}\n          onCancel={() => setOpenModelModal(false)}>\n          {openaiId ? (\n            <OpenAIFetchModel\n              openaiId={openaiId}\n              setOpenModelModal={setOpenModelModal}\n            />\n          ) : null}\n        </Modal>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Option/Settings/prompt.tsx",
    "content": "import { useQuery, useQueryClient } from \"@tanstack/react-query\"\nimport { Skeleton, Radio, Form, Input } from \"antd\"\nimport React from \"react\"\nimport { useTranslation } from \"react-i18next\"\nimport { SaveButton } from \"~/components/Common/SaveButton\"\nimport {\n  getWebSearchPrompt,\n  geWebSearchFollowUpPrompt,\n  setWebPrompts,\n  promptForRag,\n  setPromptForRag\n} from \"~/services/ollama\"\n\nexport const SettingPrompt = () => {\n  const { t } = useTranslation(\"settings\")\n\n  const [selectedValue, setSelectedValue] = React.useState<\"web\" | \"rag\">(\"rag\")\n\n  const queryClient = useQueryClient()\n\n  const { status, data } = useQuery({\n    queryKey: [\"fetchRagPrompt\"],\n    queryFn: async () => {\n      const [prompt, webSearchPrompt, webSearchFollowUpPrompt] =\n        await Promise.all([\n          promptForRag(),\n          getWebSearchPrompt(),\n          geWebSearchFollowUpPrompt()\n        ])\n\n      return {\n        prompt,\n        webSearchPrompt,\n        webSearchFollowUpPrompt\n      }\n    }\n  })\n\n  return (\n    <div className=\"flex flex-col gap-3\">\n      {status === \"pending\" && <Skeleton paragraph={{ rows: 4 }} active />}\n\n      {status === \"success\" && (\n        <div>\n          <div className=\"my-3 flex justify-end\">\n            <Radio.Group\n              defaultValue={selectedValue}\n              onChange={(e) => setSelectedValue(e.target.value)}>\n              <Radio.Button value=\"rag\">RAG</Radio.Button>\n              <Radio.Button value=\"web\">{t(\"rag.prompt.option2\")}</Radio.Button>\n            </Radio.Group>\n          </div>\n\n          {selectedValue === \"rag\" && (\n            <Form\n              layout=\"vertical\"\n              onFinish={(values) => {\n                // setSystemPromptForNonRagOption(values?.prompt || \"\")\n                setPromptForRag(\n                  values?.systemPrompt || \"\",\n                  values?.questionPrompt || \"\"\n                )\n                queryClient.invalidateQueries({\n                  queryKey: [\"fetchRagPrompt\"]\n                })\n              }}\n              initialValues={{\n                systemPrompt: data.prompt.ragPrompt,\n                questionPrompt: data.prompt.ragQuestionPrompt\n              }}>\n              <Form.Item\n                label={t(\"managePrompts.systemPrompt\")}\n                name=\"systemPrompt\"\n                rules={[\n                  {\n                    required: true,\n                    message: \"Enter a prompt.\"\n                  }\n                ]}>\n                <Input.TextArea\n                  value={data.webSearchPrompt}\n                  rows={5}\n                  placeholder=\"Enter a prompt.\"\n                />\n              </Form.Item>\n              <Form.Item\n                label={t(\"managePrompts.questionPrompt\")}\n                name=\"questionPrompt\"\n                rules={[\n                  {\n                    required: true,\n                    message: \"Enter a follow up prompt.\"\n                  }\n                ]}>\n                <Input.TextArea\n                  value={data.webSearchFollowUpPrompt}\n                  rows={5}\n                  placeholder={t(\n                    \"rag.prompt.webSearchFollowUpPromptPlaceholder\"\n                  )}\n                />\n              </Form.Item>\n              <Form.Item>\n                <div className=\"flex justify-end\">\n                  <SaveButton btnType=\"submit\" />\n                </div>{\" \"}\n              </Form.Item>\n            </Form>\n          )}\n\n          {selectedValue === \"web\" && (\n            <Form\n              layout=\"vertical\"\n              onFinish={(values) => {\n                setWebPrompts(\n                  values?.webSearchPrompt || \"\",\n                  values?.webSearchFollowUpPrompt || \"\"\n                )\n                queryClient.invalidateQueries({\n                  queryKey: [\"fetchRagPrompt\"]\n                })\n              }}\n              initialValues={{\n                webSearchPrompt: data.webSearchPrompt,\n                webSearchFollowUpPrompt: data.webSearchFollowUpPrompt\n              }}>\n              <Form.Item\n                label={t(\"rag.prompt.webSearchPrompt\")}\n                name=\"webSearchPrompt\"\n                help={t(\"rag.prompt.webSearchPromptHelp\")}\n                rules={[\n                  {\n                    required: true,\n                    message: t(\"rag.prompt.webSearchPromptError\")\n                  }\n                ]}>\n                <Input.TextArea\n                  value={data.webSearchPrompt}\n                  rows={5}\n                  placeholder={t(\"rag.prompt.webSearchPromptPlaceholder\")}\n                />\n              </Form.Item>\n              <Form.Item\n                label={t(\"rag.prompt.webSearchFollowUpPrompt\")}\n                name=\"webSearchFollowUpPrompt\"\n                help={t(\"rag.prompt.webSearchFollowUpPromptHelp\")}\n                rules={[\n                  {\n                    required: true,\n                    message: t(\"rag.prompt.webSearchFollowUpPromptError\")\n                  }\n                ]}>\n                <Input.TextArea\n                  value={data.webSearchFollowUpPrompt}\n                  rows={5}\n                  placeholder={t(\n                    \"rag.prompt.webSearchFollowUpPromptPlaceholder\"\n                  )}\n                />\n              </Form.Item>\n              <Form.Item>\n                <div className=\"flex justify-end\">\n                  <SaveButton btnType=\"submit\" />\n                </div>{\" \"}\n              </Form.Item>\n            </Form>\n          )}\n        </div>\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Option/Settings/rag.tsx",
    "content": "import { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\"\nimport { Avatar, Form, Input, InputNumber, Select, Skeleton } from \"antd\"\nimport { SaveButton } from \"~/components/Common/SaveButton\"\nimport {\n  defaultEmbeddingChunkOverlap,\n  defaultEmbeddingChunkSize,\n  defaultEmbeddingModelForRag,\n  defaultSplittingStrategy,\n  defaultSsplttingSeparator,\n  getEmbeddingModels,\n  saveForRag\n} from \"~/services/ollama\"\nimport { SettingPrompt } from \"./prompt\"\nimport { useTranslation } from \"react-i18next\"\nimport { getNoOfRetrievedDocs, getTotalFilePerKB } from \"@/services/app\"\nimport { SidepanelRag } from \"./sidepanel-rag\"\nimport { ProviderIcons } from \"@/components/Common/ProviderIcon\"\nimport { SettingTitle } from \"./title\"\nimport { MemorySettings } from \"./memory\"\nimport { useStorage } from \"@plasmohq/storage/hook\"\n\nexport const RagSettings = () => {\n  const { t } = useTranslation(\"settings\")\n  const [form] = Form.useForm()\n  const splittingStrategy = Form.useWatch(\"splittingStrategy\", form)\n  const queryClient = useQueryClient()\n  const [enableMemory] = useStorage(\"enableMemory\", false)\n\n  const { data: ollamaInfo, status } = useQuery({\n    queryKey: [\"fetchRAGSettings\"],\n    queryFn: async () => {\n      const [\n        allModels,\n        chunkOverlap,\n        chunkSize,\n        defaultEM,\n        totalFilePerKB,\n        noOfRetrievedDocs,\n        splittingStrategy,\n        splittingSeparator\n      ] = await Promise.all([\n        getEmbeddingModels({ returnEmpty: true }),\n        defaultEmbeddingChunkOverlap(),\n        defaultEmbeddingChunkSize(),\n        defaultEmbeddingModelForRag(),\n        getTotalFilePerKB(),\n        getNoOfRetrievedDocs(),\n        defaultSplittingStrategy(),\n        defaultSsplttingSeparator()\n      ])\n      return {\n        models: allModels,\n        chunkOverlap,\n        chunkSize,\n        defaultEM,\n        totalFilePerKB,\n        noOfRetrievedDocs,\n        splittingStrategy,\n        splittingSeparator\n      }\n    }\n  })\n\n  const { mutate: saveRAG, isPending: isSaveRAGPending } = useMutation({\n    mutationFn: async (data: {\n      model: string\n      chunkSize: number\n      overlap: number\n      totalFilePerKB: number\n      noOfRetrievedDocs: number\n      strategy: string\n      separator: string\n    }) => {\n      await saveForRag(\n        data.model,\n        data.chunkSize,\n        data.overlap,\n        data.totalFilePerKB,\n        data.noOfRetrievedDocs,\n        data.strategy,\n        data.separator\n      )\n      return true\n    },\n    onSuccess: () => {\n      queryClient.invalidateQueries({\n        queryKey: [\"fetchRAGSettings\"]\n      })\n    }\n  })\n\n  return (\n    <div className=\"flex flex-col space-y-3\">\n      {status === \"pending\" && <Skeleton paragraph={{ rows: 4 }} active />}\n      {status === \"success\" && (\n        <div className=\"flex flex-col space-y-6\">\n          <div>\n            <div>\n              <h2 className=\"text-base font-semibold leading-7 text-gray-900 dark:text-white\">\n                {t(\"rag.ragSettings.label\")}\n              </h2>\n              <div className=\"border border-b border-gray-200 dark:border-gray-600 mt-3 mb-6\"></div>\n            </div>\n            <Form\n              form={form}\n              layout=\"vertical\"\n              onFinish={(data) => {\n                saveRAG({\n                  model: data.defaultEM,\n                  chunkSize: data.chunkSize,\n                  overlap: data.chunkOverlap,\n                  totalFilePerKB: data.totalFilePerKB,\n                  noOfRetrievedDocs: data.noOfRetrievedDocs,\n                  separator: data.splittingSeparator,\n                  strategy: data.splittingStrategy\n                })\n              }}\n              initialValues={{\n                chunkSize: ollamaInfo?.chunkSize,\n                chunkOverlap: ollamaInfo?.chunkOverlap,\n                defaultEM: ollamaInfo?.defaultEM,\n                totalFilePerKB: ollamaInfo?.totalFilePerKB,\n                noOfRetrievedDocs: ollamaInfo?.noOfRetrievedDocs,\n                splittingStrategy: ollamaInfo?.splittingStrategy,\n                splittingSeparator: ollamaInfo?.splittingSeparator\n              }}>\n              <Form.Item\n                name=\"defaultEM\"\n                label={t(\"rag.ragSettings.model.label\")}\n                help={t(\"rag.ragSettings.model.help\")}\n                rules={[\n                  {\n                    required: true,\n                    message: t(\"rag.ragSettings.model.required\")\n                  }\n                ]}>\n                <Select\n                  size=\"large\"\n                  showSearch\n                  placeholder={t(\"rag.ragSettings.model.placeholder\")}\n                  style={{ width: \"100%\" }}\n                  className=\"mt-4\"\n                  filterOption={(input, option) =>\n                    option.label.key\n                      .toLowerCase()\n                      .indexOf(input.toLowerCase()) >= 0\n                  }\n                  options={ollamaInfo.models?.map((model) => ({\n                    label: (\n                      <span\n                        key={model.model}\n                        className=\"flex flex-row gap-3 items-center truncate\">\n                        {model?.avatar ? (\n                          <Avatar\n                            src={model.avatar}\n                            alt={model.name}\n                            size=\"small\"\n                          />\n                        ) : (\n                          <ProviderIcons\n                            provider={model?.provider}\n                            className=\"w-5 h-5\"\n                          />\n                        )}\n                        <span className=\"truncate\">\n                          {model?.nickname || model?.name}\n                        </span>\n                      </span>\n                    ),\n                    value: model.model\n                  }))}\n                />\n              </Form.Item>\n\n              <Form.Item\n                name=\"splittingStrategy\"\n                label={t(\"rag.ragSettings.splittingStrategy.label\")}\n                rules={[\n                  {\n                    required: true,\n                    message: t(\"rag.ragSettings.model.required\")\n                  }\n                ]}>\n                <Select\n                  size=\"large\"\n                  showSearch\n                  style={{ width: \"100%\" }}\n                  className=\"mt-4\"\n                  options={[\n                    \"RecursiveCharacterTextSplitter\",\n                    \"CharacterTextSplitter\"\n                  ].map((e) => ({\n                    label: e,\n                    value: e\n                  }))}\n                />\n              </Form.Item>\n\n              {splittingStrategy !== \"RecursiveCharacterTextSplitter\" && (\n                <Form.Item\n                  name=\"splittingSeparator\"\n                  label={t(\"rag.ragSettings.splittingSeparator.label\")}\n                  rules={[\n                    {\n                      required: true,\n                      message: t(\"rag.ragSettings.splittingSeparator.required\")\n                    }\n                  ]}>\n                  <Input\n                    size=\"large\"\n                    style={{ width: \"100%\" }}\n                    placeholder={t(\n                      \"rag.ragSettings.splittingSeparator.placeholder\"\n                    )}\n                  />\n                </Form.Item>\n              )}\n\n              <Form.Item\n                name=\"chunkSize\"\n                label={t(\"rag.ragSettings.chunkSize.label\")}\n                rules={[\n                  {\n                    required: true,\n                    message: t(\"rag.ragSettings.chunkSize.required\")\n                  }\n                ]}>\n                <InputNumber\n                  style={{ width: \"100%\" }}\n                  placeholder={t(\"rag.ragSettings.chunkSize.placeholder\")}\n                />\n              </Form.Item>\n              <Form.Item\n                name=\"chunkOverlap\"\n                label={t(\"rag.ragSettings.chunkOverlap.label\")}\n                rules={[\n                  {\n                    required: true,\n                    message: t(\"rag.ragSettings.chunkOverlap.required\")\n                  }\n                ]}>\n                <InputNumber\n                  style={{ width: \"100%\" }}\n                  placeholder={t(\"rag.ragSettings.chunkOverlap.placeholder\")}\n                />\n              </Form.Item>\n\n              <Form.Item\n                name=\"noOfRetrievedDocs\"\n                label={t(\"rag.ragSettings.noOfRetrievedDocs.label\")}\n                rules={[\n                  {\n                    required: true,\n                    message: t(\"rag.ragSettings.noOfRetrievedDocs.required\")\n                  }\n                ]}>\n                <InputNumber\n                  style={{ width: \"100%\" }}\n                  placeholder={t(\n                    \"rag.ragSettings.noOfRetrievedDocs.placeholder\"\n                  )}\n                />\n              </Form.Item>\n\n              <Form.Item\n                name=\"totalFilePerKB\"\n                label={t(\"rag.ragSettings.totalFilePerKB.label\")}\n                rules={[\n                  {\n                    required: true,\n                    message: t(\"rag.ragSettings.totalFilePerKB.required\")\n                  }\n                ]}>\n                <InputNumber\n                  style={{ width: \"100%\" }}\n                  min={1}\n                  placeholder={t(\"rag.ragSettings.totalFilePerKB.placeholder\")}\n                />\n              </Form.Item>\n\n              <div className=\"flex justify-end\">\n                <SaveButton disabled={isSaveRAGPending} btnType=\"submit\" />\n              </div>\n            </Form>\n          </div>\n\n          <SidepanelRag />\n\n          {enableMemory && (\n            <div>\n              <MemorySettings />\n            </div>\n          )}\n\n          <div>\n            <div>\n              <h2 className=\"text-base font-semibold leading-7 text-gray-900 dark:text-white\">\n                {t(\"rag.prompt.label\")}\n              </h2>\n              <div className=\"border border-b border-gray-200 dark:border-gray-600 mt-3 mb-6\"></div>\n            </div>\n            <SettingPrompt />\n          </div>\n\n          <SettingTitle />\n        </div>\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Option/Settings/search-mode.tsx",
    "content": "import { SaveButton } from \"@/components/Common/SaveButton\"\nimport { getSearchSettings, setSearchSettings } from \"@/services/search\"\nimport { ALL_GOOGLE_DOMAINS } from \"@/utils/google-domains\"\nimport { SUPPORTED_SEARCH_PROVIDERS } from \"@/utils/search-provider\"\nimport { useForm } from \"@mantine/form\"\nimport { useQuery } from \"@tanstack/react-query\"\nimport { Select, Skeleton, Switch, InputNumber, Input } from \"antd\"\nimport { useTranslation } from \"react-i18next\"\n\nexport const SearchModeSettings = () => {\n  const { t } = useTranslation(\"settings\")\n\n  const form = useForm({\n    initialValues: {\n      isSimpleInternetSearch: false,\n      searchProvider: \"\",\n      totalSearchResults: 0,\n      visitSpecificWebsite: false,\n      searxngURL: \"\",\n      searxngJSONMode: false,\n      braveApiKey: \"\",\n      tavilyApiKey: \"\",\n      googleDomain: \"\",\n      defaultInternetSearchOn: false,\n      exaAPIKey: \"\",\n      firecrawlAPIKey: \"\",\n      ollamaSearchApiKey: \"\",\n      kagiApiKey: \"\",\n      perplexityApiKey: \"\",\n      domainFilterList: [] as string[],\n      blockedDomainList: [] as string[]\n    }\n  })\n\n  const { status } = useQuery({\n    queryKey: [\"fetchSearchSettings\"],\n    queryFn: async () => {\n      const data = await getSearchSettings()\n      form.setValues(data)\n      return data\n    }\n  })\n\n  if (status === \"pending\" || status === \"error\") {\n    return <Skeleton active />\n  }\n\n  return (\n    <div>\n      <div className=\"mb-5\">\n        <h2 className=\"text-base font-semibold leading-7 text-gray-900 dark:text-white\">\n          {t(\"generalSettings.webSearch.heading\")}\n        </h2>\n        <div className=\"border border-b border-gray-200 dark:border-gray-600 mt-3\"></div>\n      </div>\n      <form\n        onSubmit={form.onSubmit(async (values) => {\n          await setSearchSettings(values)\n        })}\n        className=\"space-y-4\">\n        <div className=\"flex sm:flex-row flex-col space-y-4 sm:space-y-0 sm:justify-between\">\n          <span className=\"text-gray-700 dark:text-neutral-50 \">\n            {t(\"generalSettings.webSearch.provider.label\")}\n          </span>\n          <div>\n            <Select\n              placeholder={t(\"generalSettings.webSearch.provider.placeholder\")}\n              showSearch\n              className=\"w-full mt-4 sm:mt-0 sm:w-[200px]\"\n              options={SUPPORTED_SEARCH_PROVIDERS}\n              filterOption={(input, option) =>\n                option!.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 ||\n                option!.value.toLowerCase().indexOf(input.toLowerCase()) >= 0\n              }\n              {...form.getInputProps(\"searchProvider\")}\n            />\n          </div>\n        </div>\n        {form.values.searchProvider === \"searxng\" && (\n          <>\n            <div className=\"flex sm:flex-row flex-col space-y-4 sm:space-y-0 sm:justify-between\">\n              <span className=\"text-gray-700 dark:text-neutral-50\">\n                {t(\"generalSettings.webSearch.searxng.url.label\")}\n              </span>\n              <div>\n                <Input\n                  placeholder=\"https://searxng.example.com\"\n                  className=\"w-full mt-4 sm:mt-0 sm:w-[200px]\"\n                  required\n                  {...form.getInputProps(\"searxngURL\")}\n                />\n              </div>\n            </div>\n          </>\n        )}\n        {form.values.searchProvider === \"google\" && (\n          <>\n            <div className=\"flex sm:flex-row flex-col space-y-4 sm:space-y-0 sm:justify-between\">\n              <span className=\"text-gray-700 dark:text-neutral-50\">\n                {t(\"generalSettings.webSearch.googleDomain.label\")}\n              </span>\n              <div>\n                <Select\n                  showSearch\n                  className=\"w-full mt-4 sm:mt-0 sm:w-[200px]\"\n                  options={ALL_GOOGLE_DOMAINS.map((e) => ({\n                    label: e,\n                    value: e\n                  }))}\n                  filterOption={(input, option) =>\n                    option!.label.toLowerCase().indexOf(input.toLowerCase()) >=\n                      0 ||\n                    option!.value.toLowerCase().indexOf(input.toLowerCase()) >=\n                      0\n                  }\n                  {...form.getInputProps(\"googleDomain\")}\n                />\n              </div>\n            </div>\n          </>\n        )}\n        {form.values.searchProvider === \"brave-api\" && (\n          <>\n            <div className=\"flex sm:flex-row flex-col space-y-4 sm:space-y-0 sm:justify-between\">\n              <span className=\"text-gray-700 dark:text-neutral-50\">\n                {t(\"generalSettings.webSearch.braveApi.label\")}\n              </span>\n              <div>\n                <Input.Password\n                  placeholder={t(\n                    \"generalSettings.webSearch.braveApi.placeholder\"\n                  )}\n                  required\n                  className=\"w-full mt-4 sm:mt-0 sm:w-[200px]\"\n                  {...form.getInputProps(\"braveApiKey\")}\n                />\n              </div>\n            </div>\n          </>\n        )}\n        {form.values.searchProvider === \"tavily-api\" && (\n          <>\n            <div className=\"flex sm:flex-row flex-col space-y-4 sm:space-y-0 sm:justify-between\">\n              <span className=\"text-gray-700 dark:text-neutral-50\">\n                {t(\"generalSettings.webSearch.tavilyApi.label\")}\n              </span>\n              <div>\n                <Input.Password\n                  placeholder={t(\n                    \"generalSettings.webSearch.tavilyApi.placeholder\"\n                  )}\n                  required\n                  className=\"w-full mt-4 sm:mt-0 sm:w-[200px]\"\n                  {...form.getInputProps(\"tavilyApiKey\")}\n                />\n              </div>\n            </div>\n          </>\n        )}\n\n        {form.values.searchProvider === \"exa\" && (\n          <>\n            <div className=\"flex sm:flex-row flex-col space-y-4 sm:space-y-0 sm:justify-between\">\n              <span className=\"text-gray-700 dark:text-neutral-50\">\n                {t(\"generalSettings.webSearch.exa.label\")}\n              </span>\n              <div>\n                <Input.Password\n                  placeholder={t(\"generalSettings.webSearch.exa.placeholder\")}\n                  required\n                  className=\"w-full mt-4 sm:mt-0 sm:w-[200px]\"\n                  {...form.getInputProps(\"exaAPIKey\")}\n                />\n              </div>\n            </div>\n          </>\n        )}\n\n        {form.values.searchProvider === \"firecrawl\" && (\n          <>\n            <div className=\"flex sm:flex-row flex-col space-y-4 sm:space-y-0 sm:justify-between\">\n              <span className=\"text-gray-700 dark:text-neutral-50\">\n                {t(\"generalSettings.webSearch.firecrawlAPIKey.label\")}\n              </span>\n              <div>\n                <Input.Password\n                  placeholder={t(\n                    \"generalSettings.webSearch.firecrawlAPIKey.placeholder\"\n                  )}\n                  required\n                  className=\"w-full mt-4 sm:mt-0 sm:w-[200px]\"\n                  {...form.getInputProps(\"firecrawlAPIKey\")}\n                />\n              </div>\n            </div>\n          </>\n        )}\n\n        {form.values.searchProvider === \"ollama-search\" && (\n          <>\n            <div className=\"flex sm:flex-row flex-col space-y-4 sm:space-y-0 sm:justify-between\">\n              <span className=\"text-gray-700 dark:text-neutral-50\">\n                {t(\n                  \"generalSettings.webSearch.ollamaSearchApiKey.label\",\n                  \"Ollama Search API Key\"\n                )}\n              </span>\n              <div>\n                <Input.Password\n                  placeholder={t(\n                    \"generalSettings.webSearch.ollamaSearchApiKey.placeholder\",\n                    \"Ollama Search API Key\"\n                  )}\n                  required\n                  className=\"w-full mt-4 sm:mt-0 sm:w-[200px]\"\n                  {...form.getInputProps(\"ollamaSearchApiKey\")}\n                />\n              </div>\n            </div>\n          </>\n        )}\n\n        {form.values.searchProvider === \"kagi-api\" && (\n          <>\n            <div className=\"flex sm:flex-row flex-col space-y-4 sm:space-y-0 sm:justify-between\">\n              <span className=\"text-gray-700 dark:text-neutral-50\">\n                {t(\n                  \"generalSettings.webSearch.kagiApi.label\",\n                  \"Kagi API Key\"\n                )}\n              </span>\n              <div>\n                <Input.Password\n                  placeholder={t(\n                    \"generalSettings.webSearch.kagiApi.placeholder\",\n                    \"Kagi API Key\"\n                  )}\n                  required\n                  className=\"w-full mt-4 sm:mt-0 sm:w-[200px]\"\n                  {...form.getInputProps(\"kagiApiKey\")}\n                />\n              </div>\n            </div>\n          </>\n        )}\n\n        {form.values.searchProvider === \"perplexity-api\" && (\n          <>\n            <div className=\"flex sm:flex-row flex-col space-y-4 sm:space-y-0 sm:justify-between\">\n              <span className=\"text-gray-700 dark:text-neutral-50\">\n                {t(\n                  \"generalSettings.webSearch.perplexityApi.label\",\n                  \"Perplexity API Key\"\n                )}\n              </span>\n              <div>\n                <Input.Password\n                  placeholder={t(\n                    \"generalSettings.webSearch.perplexityApi.placeholder\",\n                    \"Perplexity API Key\"\n                  )}\n                  required\n                  className=\"w-full mt-4 sm:mt-0 sm:w-[200px]\"\n                  {...form.getInputProps(\"perplexityApiKey\")}\n                />\n              </div>\n            </div>\n          </>\n        )}\n\n        <div className=\"flex sm:flex-row flex-col space-y-4 sm:space-y-0 sm:justify-between\">\n          <span className=\"text-gray-700 dark:text-neutral-50 \">\n            {t(\"generalSettings.webSearch.searchMode.label\")}\n          </span>\n          <div>\n            <Switch\n              className=\"mt-4 sm:mt-0\"\n              {...form.getInputProps(\"isSimpleInternetSearch\", {\n                type: \"checkbox\"\n              })}\n            />\n          </div>\n        </div>\n        <div className=\"flex sm:flex-row flex-col space-y-4 sm:space-y-0 sm:justify-between\">\n          <span className=\"text-gray-700 dark:text-neutral-50 \">\n            {t(\"generalSettings.webSearch.totalSearchResults.label\")}\n          </span>\n          <div>\n            <InputNumber\n              placeholder={t(\n                \"generalSettings.webSearch.totalSearchResults.placeholder\"\n              )}\n              {...form.getInputProps(\"totalSearchResults\")}\n              className=\"!w-full mt-4 sm:mt-0 sm:w-[200px]\"\n            />\n          </div>\n        </div>\n\n        <div className=\"flex sm:flex-row flex-col space-y-4 sm:space-y-0 sm:justify-between\">\n          <span className=\"text-gray-700 dark:text-neutral-50 \">\n            {t(\"generalSettings.webSearch.visitSpecificWebsite.label\")}\n          </span>\n          <div>\n            <Switch\n              className=\"mt-4 sm:mt-0\"\n              {...form.getInputProps(\"visitSpecificWebsite\", {\n                type: \"checkbox\"\n              })}\n            />\n          </div>\n        </div>\n\n        <div className=\"flex sm:flex-row flex-col space-y-4 sm:space-y-0 sm:justify-between\">\n          <span className=\"text-gray-700 dark:text-neutral-50 \">\n            {t(\"generalSettings.webSearch.searchOnByDefault.label\")}\n          </span>\n          <div>\n            <Switch\n              className=\"mt-4 sm:mt-0\"\n              {...form.getInputProps(\"defaultInternetSearchOn\", {\n                type: \"checkbox\"\n              })}\n            />\n          </div>\n        </div>\n\n        <div className=\"flex sm:flex-row flex-col space-y-4 sm:space-y-0 sm:justify-between\">\n          <div className=\"flex flex-col space-y-1\">\n            <span className=\"text-gray-700 dark:text-neutral-50\">\n              {t(\"generalSettings.webSearch.domainFilter.label\", \"Domain Filter List\")}\n            </span>\n            <p className=\"text-sm text-gray-500 dark:text-gray-400\">\n              {t(\"generalSettings.webSearch.domainFilter.description\", \"Only show results from these domains\")}\n            </p>\n          </div>\n          <div className=\"w-full mt-4 sm:mt-0 sm:w-[200px]\">\n            <Select\n              mode=\"tags\"\n              placeholder={t(\"generalSettings.webSearch.domainFilter.placeholder\", \"e.g., example.com\")}\n              className=\"w-full\"\n              tokenSeparators={[',', ' ']}\n              {...form.getInputProps(\"domainFilterList\")}\n            />\n          </div>\n        </div>\n\n        <div className=\"flex sm:flex-row flex-col space-y-4 sm:space-y-0 sm:justify-between\">\n          <div className=\"flex flex-col space-y-1\">\n            <span className=\"text-gray-700 dark:text-neutral-50\">\n              {t(\"generalSettings.webSearch.blockedDomains.label\", \"Blocked Domains\")}\n            </span>\n            <p className=\"text-sm text-gray-500 dark:text-gray-400\">\n              {t(\"generalSettings.webSearch.blockedDomains.description\", \"Exclude results from these domains\")}\n            </p>\n          </div>\n          <div className=\"w-full mt-4 sm:mt-0 sm:w-[200px]\">\n            <Select\n              mode=\"tags\"\n              placeholder={t(\"generalSettings.webSearch.blockedDomains.placeholder\", \"e.g., spam.com\")}\n              className=\"w-full\"\n              tokenSeparators={[',', ' ']}\n              {...form.getInputProps(\"blockedDomainList\")}\n            />\n          </div>\n        </div>\n\n        <div className=\"flex justify-end\">\n          <SaveButton btnType=\"submit\" />\n        </div>\n      </form>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Option/Settings/sidepanel-rag.tsx",
    "content": "import { useStorage } from \"@plasmohq/storage/hook\"\nimport { InputNumber, Switch } from \"antd\"\nimport { useTranslation } from \"react-i18next\"\n\nexport const SidepanelRag = ({ hideBorder }: { hideBorder?: boolean }) => {\n  const { t } = useTranslation(\"settings\")\n  const [chatWithWebsiteEmbedding, setChatWithWebsiteEmbedding] = useStorage(\n    \"chatWithWebsiteEmbedding\",\n    false\n  )\n  const [maxWebsiteContext, setMaxWebsiteContext] = useStorage(\n    \"maxWebsiteContext\",\n    7028\n  )\n\n  return (\n    <div>\n      <div className=\"mb-5\">\n        <h2\n          className={`${\n            !hideBorder ? \"text-base font-semibold leading-7\" : \"text-md\"\n          } text-gray-900 dark:text-white`}>\n          {t(\"generalSettings.sidepanelRag.heading\")}\n        </h2>\n        {!hideBorder && (\n          <div className=\"border border-b border-gray-200 dark:border-gray-600 mt-3\"></div>\n        )}\n      </div>\n      <div className={`${\n        !hideBorder ? \"text-sm\" : \"\"\n      } space-y-4`}>\n        <div className=\"flex flex-col sm:flex-row sm:items-center sm:justify-between space-y-4 sm:space-y-0\">\n          <span className=\"text-gray-700  truncate dark:text-neutral-50\">\n            {t(\"generalSettings.sidepanelRag.ragEnabled.label\")}\n          </span>\n          <div>\n            <Switch\n              className=\"mt-4 sm:mt-0\"\n              checked={chatWithWebsiteEmbedding}\n              onChange={(checked) => setChatWithWebsiteEmbedding(checked)}\n            />\n          </div>\n        </div>\n        <div className=\"flex flex-col sm:flex-row sm:items-center sm:justify-between space-y-4 sm:space-y-0\">\n          <span className=\"text-gray-700 truncate dark:text-neutral-50\">\n            {t(\"generalSettings.sidepanelRag.maxWebsiteContext.label\")}\n          </span>\n          <div>\n            <InputNumber\n              className=\"mt-4 sm:mt-0\"\n              value={maxWebsiteContext}\n              onChange={(value) => setMaxWebsiteContext(value)}\n              placeholder={t(\n                \"generalSettings.sidepanelRag.maxWebsiteContext.placeholder\"\n              )}\n            />\n          </div>\n        </div>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Option/Settings/sst-settings.tsx",
    "content": "import { useStorage } from \"@plasmohq/storage/hook\"\nimport { InputNumber, Select, Switch } from \"antd\"\nimport { useTranslation } from \"react-i18next\"\nimport { SUPPORTED_LANGUAGES } from \"~/utils/supported-languages\"\n\nexport const SSTSettings = ({ hideBorder }: { hideBorder?: boolean }) => {\n  const { t } = useTranslation(\"settings\")\n  const [speechToTextLanguage, setSpeechToTextLanguage] = useStorage(\n    \"speechToTextLanguage\",\n    \"en-US\"\n  )\n\n  const [autoSubmitVoiceMessage, setAutoSubmitVoiceMessage] = useStorage(\n    \"autoSubmitVoiceMessage\",\n    false\n  )\n\n  const [autoStopTimeout, setAutoStopTimeout] = useStorage(\n    \"autoStopTimeout\",\n    2000\n  )\n\n  return (\n    <div>\n      <div className=\"mb-5\">\n        <h2\n          className={`${\n            !hideBorder ? \"text-base font-semibold leading-7\" : \"text-md\"\n          } text-gray-900 dark:text-white`}>\n          {t(\"generalSettings.stt.heading\")}\n        </h2>\n        {!hideBorder && (\n          <div className=\"border border-b border-gray-200 dark:border-gray-600 mt-3\"></div>\n        )}\n      </div>\n      <form className=\"space-y-4\">\n        <div className=\"flex flex-row justify-between\">\n          <span className=\"text-gray-700 dark:text-neutral-50\">\n            {t(\"generalSettings.settings.speechRecognitionLang.label\")}\n          </span>\n\n          <Select\n            placeholder={t(\n              \"generalSettings.settings.speechRecognitionLang.placeholder\"\n            )}\n            allowClear\n            showSearch\n            options={SUPPORTED_LANGUAGES}\n            value={speechToTextLanguage}\n            filterOption={(input, option) =>\n              option!.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 ||\n              option!.value.toLowerCase().indexOf(input.toLowerCase()) >= 0\n            }\n            onChange={(value) => {\n              setSpeechToTextLanguage(value)\n            }}\n            className={hideBorder ? \"w-full\" : \"!min-w-[200px]\"}\n          />\n        </div>\n\n        <div className=\"flex flex-row justify-between\">\n          <span className=\"text-gray-700 dark:text-neutral-50\">\n            {t(\"generalSettings.stt.autoSubmitVoiceMessage.label\")}\n          </span>\n          <Switch\n            checked={autoSubmitVoiceMessage}\n            onChange={(checked) => {\n              setAutoSubmitVoiceMessage(checked)\n            }}\n          />\n        </div>\n\n        <div className=\"flex flex-row justify-between\">\n          <span className=\"text-gray-700 dark:text-neutral-50\">\n            {t(\"generalSettings.stt.autoStopTimeout.label\")}\n          </span>\n          <InputNumber\n            className={hideBorder ? \"w-full\" : \"!min-w-[200px]\"}\n            type=\"number\"\n            placeholder={t(\"generalSettings.stt.autoStopTimeout.placeholder\")}\n            value={autoStopTimeout}\n            onChange={(e) => {\n              setAutoStopTimeout(e)\n            }}\n          />\n        </div>\n      </form>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Option/Settings/system-settings.tsx",
    "content": "import { BetaTag } from \"@/components/Common/Beta\"\nimport { useFontSize } from \"@/context/FontSizeProvider\"\nimport { useMessageOption } from \"@/hooks/useMessageOption\"\nimport {\n  exportPageAssistData,\n  importPageAssistData\n} from \"@/libs/export-import\"\nimport { Storage } from \"@plasmohq/storage\"\nimport { useStorage } from \"@plasmohq/storage/hook\"\nimport { useMutation, useQueryClient } from \"@tanstack/react-query\"\nimport { Select, notification, Switch } from \"antd\"\nimport { useTranslation } from \"react-i18next\"\nimport { Loader2, RotateCcw, Upload } from \"lucide-react\"\nimport { toBase64 } from \"@/libs/to-base64\"\nimport { PageAssistDatabase } from \"@/db/dexie/chat\"\nimport { isFireFox, isFireFoxPrivateMode } from \"@/utils/is-private-mode\"\nimport { firefoxSyncDataForPrivateMode } from \"@/db/dexie/firefox-sync\"\n\nexport const SystemSettings = () => {\n  const { t } = useTranslation([\"settings\", \"knowledge\"])\n  const queryClient = useQueryClient()\n  const { clearChat } = useMessageOption()\n  const { increase, decrease, scale } = useFontSize()\n\n  const [webuiBtnSidePanel, setWebuiBtnSidePanel] = useStorage(\n    \"webuiBtnSidePanel\",\n    false\n  )\n\n  const [storageSyncEnabled, setStorageSyncEnabled] = useStorage(\n    {\n      key: \"storageSyncEnabled\",\n      instance: new Storage({\n        area: \"local\"\n      })\n    },\n    true\n  )\n\n  const [actionIconClick, setActionIconClick] = useStorage(\n    {\n      key: \"actionIconClick\",\n      instance: new Storage({\n        area: \"local\"\n      })\n    },\n    \"webui\"\n  )\n\n  const [contextMenuClick, setContextMenuClick] = useStorage(\n    {\n      key: \"contextMenuClick\",\n      instance: new Storage({\n        area: \"local\"\n      })\n    },\n    \"sidePanel\"\n  )\n  const [chatBackgroundImage, setChatBackgroundImage] = useStorage({\n    key: \"chatBackgroundImage\",\n    instance: new Storage({\n      area: \"local\"\n    })\n  })\n\n  const importDataMutation = useMutation({\n    mutationFn: async (file: File) => {\n      await importPageAssistData(file)\n    },\n    onSuccess: () => {\n      queryClient.invalidateQueries({\n        queryKey: [\"fetchChatHistory\"]\n      })\n\n      notification.success({\n        message: \"Imported data successfully\"\n      })\n\n      setTimeout(() => { \n        window.location.reload() \n      }, 1000)   \n    },\n    onError: (error) => {\n      console.error(\"Import error:\", error)\n      notification.error({\n        message: \"Import error\"\n      })\n    }\n  })\n\n  const syncFirefoxData = useMutation({\n    mutationFn: firefoxSyncDataForPrivateMode,\n    onSuccess: () => {\n      notification.success({\n        message:\n          \"Firefox data synced successfully, You don't need to do this again\"\n      })\n    },\n    onError: (error) => {\n      console.log(error)\n      notification.error({\n        message: \"Firefox data sync failed\"\n      })\n    }\n  })\n\n  const handleImageUpload = async (\n    event: React.ChangeEvent<HTMLInputElement>\n  ) => {\n    const file = event.target.files?.[0]\n    if (file) {\n      try {\n        if (!file.type.startsWith(\"image/\")) {\n          notification.error({\n            message: \"Please select a valid image file\"\n          })\n          return\n        }\n\n        const base64String = await toBase64(file)\n        setChatBackgroundImage(base64String)\n      } catch (error) {\n        console.error(\"Error uploading image:\", error)\n        notification.error({\n          message: \"Failed to upload image\"\n        })\n      }\n    }\n  }\n\n  return (\n    <div>\n      <div className=\"mb-5\">\n        <h2 className=\"text-base font-semibold leading-7 text-gray-900 dark:text-white\">\n          {t(\"generalSettings.system.heading\")}\n        </h2>\n        <div className=\"border border-b border-gray-200 dark:border-gray-600 mt-3\"></div>\n      </div>\n      <div className=\"flex flex-col sm:flex-row mb-3 gap-3 sm:gap-0 sm:justify-between sm:items-center\">\n        <span className=\"text-black dark:text-white font-medium\">\n          <BetaTag />\n          {t(\"generalSettings.system.fontSize.label\")}\n        </span>\n        <div className=\"flex flex-row items-center gap-3 justify-center sm:justify-end\">\n          <button\n            onClick={decrease}\n            className=\"bg-black hover:bg-gray-800 dark:bg-white dark:hover:bg-gray-200 text-white dark:text-black px-3 py-1.5 rounded-lg transition-colors duration-200 font-medium text-sm\">\n            A-\n          </button>\n          <span className=\"min-w-[2rem] text-center font-medium text-black dark:text-white\">\n            {scale.toFixed(1)}x\n          </span>\n          <button\n            onClick={increase}\n            className=\"bg-black hover:bg-gray-800 dark:bg-white dark:hover:bg-gray-200 text-white dark:text-black px-3 py-1.5 rounded-lg transition-colors duration-200 font-medium text-sm\">\n            A+\n          </button>{\" \"}\n        </div>\n      </div>\n\n      <div className=\"flex flex-col sm:flex-row mb-3 gap-3 sm:gap-0 sm:justify-between sm:items-center\">\n        <span className=\"text-gray-700 dark:text-neutral-50\">\n          <BetaTag />\n          {t(\"generalSettings.system.actionIcon.label\")}\n        </span>\n        <Select\n          options={[\n            {\n              label: \"Open Web UI\",\n              value: \"webui\"\n            },\n            {\n              label: \"Open SidePanel\",\n              value: \"sidePanel\"\n            }\n          ]}\n          value={actionIconClick}\n          className=\"w-full sm:w-[200px]\"\n          onChange={(value) => {\n            setActionIconClick(value)\n          }}\n        />\n      </div>\n      <div className=\"flex flex-col sm:flex-row mb-3 gap-3 sm:gap-0 sm:justify-between sm:items-center\">\n        <span className=\"text-gray-700 dark:text-neutral-50\">\n          <BetaTag />\n          {t(\"generalSettings.system.contextMenu.label\")}\n        </span>\n        <Select\n          options={[\n            {\n              label: \"Open Web UI\",\n              value: \"webui\"\n            },\n            {\n              label: \"Open SidePanel\",\n              value: \"sidePanel\"\n            }\n          ]}\n          value={contextMenuClick}\n          className=\"w-full sm:w-[200px]\"\n          onChange={(value) => {\n            setContextMenuClick(value)\n          }}\n        />\n      </div>\n      {isFireFox && !isFireFoxPrivateMode && (\n        <div className=\"flex flex-col sm:flex-row mb-3 gap-3 sm:gap-0 sm:justify-between sm:items-center\">\n          <span className=\"text-gray-700 dark:text-neutral-50\">\n            <BetaTag />\n            {t(\"generalSettings.system.firefoxPrivateModeSync.label\", {\n              defaultValue:\n                \"Sync Custom Models, Prompts for Firefox Private Windows (Incognito Mode)\"\n            })}\n          </span>\n          <button\n            onClick={() => {\n              syncFirefoxData.mutate()\n            }}\n            disabled={syncFirefoxData.isPending}\n            className=\"bg-gray-800 dark:bg-white text-white dark:text-gray-900 px-4 py-2 rounded-md cursor-pointer w-full sm:w-auto\">\n            {syncFirefoxData.isPending ? (\n              <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n            ) : (\n              t(\"generalSettings.system.firefoxPrivateModeSync.button\", {\n                defaultValue: \"Sync Data\"\n              })\n            )}\n          </button>\n        </div>\n      )}\n      <div className=\"flex flex-col sm:flex-row mb-3 gap-3 sm:gap-0 sm:justify-between sm:items-center\">\n        <span className=\"text-gray-700 dark:text-neutral-50\">\n          {t(\"generalSettings.system.storageSyncEnabled.label\")}\n        </span>\n        <div>\n          <Switch\n            checked={storageSyncEnabled}\n            onChange={(checked) => {\n              setStorageSyncEnabled(checked)\n            }}\n          />\n        </div>\n      </div>\n\n      <div className=\"flex flex-col sm:flex-row mb-3 gap-3 sm:gap-0 sm:justify-between sm:items-center\">\n        <span className=\"text-gray-700 dark:text-neutral-50\">\n          {t(\"generalSettings.system.webuiBtnSidePanel.label\")}\n        </span>\n         <div>\n          <Switch\n          checked={webuiBtnSidePanel}\n          onChange={(checked) => {\n            setWebuiBtnSidePanel(checked)\n          }}\n        />\n         </div>\n      </div>\n\n      <div className=\"flex flex-col sm:flex-row mb-3 gap-3 sm:gap-0 sm:justify-between sm:items-center\">\n        <span className=\"text-gray-700 dark:text-neutral-50\">\n          <BetaTag />\n          {t(\"generalSettings.system.chatBackgroundImage.label\")}\n        </span>\n        <div className=\"flex items-center gap-2 justify-center sm:justify-end\">\n          {chatBackgroundImage ? (\n            <button\n              onClick={() => {\n                setChatBackgroundImage(null)\n              }}\n              className=\"text-gray-800 dark:text-white\">\n              <RotateCcw className=\"size-4\" />\n            </button>\n          ) : null}\n          <label\n            htmlFor=\"background-image-upload\"\n            className=\"bg-gray-800 inline-flex gap-2 dark:bg-white text-white dark:text-gray-900 px-4 py-2 rounded-md cursor-pointer\">\n            <Upload className=\"size-4\" />\n            {t(\"knowledge:form.uploadFile.label\")}\n          </label>\n          <input\n            type=\"file\"\n            accept=\"image/*\"\n            id=\"background-image-upload\"\n            className=\"hidden\"\n            onChange={handleImageUpload}\n          />\n        </div>\n      </div>\n\n      <div className=\"flex flex-col sm:flex-row mb-3 gap-3 sm:gap-0 sm:justify-between sm:items-center\">\n        <span className=\"text-gray-700 dark:text-neutral-50\">\n          {t(\"generalSettings.system.export.label\")}\n        </span>\n        <button\n          onClick={exportPageAssistData}\n          className=\"bg-gray-800 dark:bg-white text-white dark:text-gray-900 px-4 py-2 rounded-md cursor-pointer w-full sm:w-auto\">\n          {t(\"generalSettings.system.export.button\")}\n        </button>\n      </div>\n      <div className=\"flex flex-col sm:flex-row mb-3 gap-3 sm:gap-0 sm:justify-between sm:items-center\">\n        <span className=\"text-gray-700 dark:text-neutral-50\">\n          {t(\"generalSettings.system.import.label\")}\n        </span>\n        <label\n          htmlFor=\"import\"\n          className=\"bg-gray-800 dark:bg-white text-white dark:text-gray-900 px-4 py-2 rounded-md cursor-pointer flex items-center justify-center w-full sm:w-auto\">\n          {importDataMutation.isPending ? (\n            <>\n              <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n            </>\n          ) : (\n            t(\"generalSettings.system.import.button\")\n          )}\n        </label>\n        <input\n          type=\"file\"\n          accept=\".json\"\n          id=\"import\"\n          className=\"hidden\"\n          disabled={importDataMutation.isPending}\n          onChange={(e) => {\n            if (e.target.files && e.target.files[0]) {\n              importDataMutation.mutate(e.target.files[0])\n            }\n          }}\n        />\n      </div>\n\n      <div className=\"flex flex-col sm:flex-row mb-3 gap-3 sm:gap-0 sm:justify-between sm:items-center\">\n        <span className=\"text-gray-700 dark:text-neutral-50\">\n          {t(\"generalSettings.system.deleteChatHistory.label\")}\n        </span>\n\n        <button\n          onClick={async () => {\n            const confirm = window.confirm(\n              t(\"generalSettings.system.deleteChatHistory.confirm\")\n            )\n\n            if (confirm) {\n              const db = new PageAssistDatabase()\n              await db.clearDB()\n              queryClient.invalidateQueries({\n                queryKey: [\"fetchChatHistory\"]\n              })\n              clearChat()\n              try {\n                if (storageSyncEnabled) {\n                  await browser.storage.sync.clear()\n                }\n                await browser.storage.local.clear()\n                await browser.storage.session.clear()\n              } catch (e) {\n                console.error(\"Error clearing storage:\", e)\n              }\n            }\n          }}\n          className=\"bg-red-500 dark:bg-red-600 text-white dark:text-gray-200 px-4 py-2 rounded-md w-full sm:w-auto\">\n          {t(\"generalSettings.system.deleteChatHistory.button\")}\n        </button>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Option/Settings/title.tsx",
    "content": "import { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\"\nimport { Avatar, Form, Input, Select, Skeleton, Switch } from \"antd\"\nimport { SaveButton } from \"~/components/Common/SaveButton\" \nimport {\n  isTitleGenEnabled,\n  setTitleGenEnabled,\n  getTitleGenerationPrompt,\n  setTitleGenerationPrompt,\n  titleGenerationModel,\n  DEFAULT_TITLE_GEN_PROMPT,\n  setTitleGenerationModel\n} from \"~/services/title\"\nimport { ProviderIcons } from \"@/components/Common/ProviderIcon\"\nimport { useStorage } from \"@plasmohq/storage/hook\"\nimport { fetchChatModels } from \"@/services/ollama\"\n\nexport const SettingTitle = () => {\n  const [form] = Form.useForm()\n  const queryClient = useQueryClient()\n\n \n  const { status, data } = useQuery({\n    queryKey: [\"fetchTitleSettings\"],\n    queryFn: async () => {\n      const [models, enabled, prompt, model] = await Promise.all([\n        fetchChatModels({ returnEmpty: true }),\n        isTitleGenEnabled(),\n        getTitleGenerationPrompt(),\n        titleGenerationModel() \n      ])\n\n      return {\n        models,\n        enabled,\n        prompt,\n        model\n      }\n    }\n  })\n\n  const { mutate: saveTitleSettings, isPending } = useMutation({\n    mutationFn: async (values: {\n      enabled: boolean\n      prompt: string\n      model: string\n    }) => {\n      await setTitleGenEnabled(values.enabled)\n      await setTitleGenerationPrompt(values.prompt)\n      await setTitleGenerationModel(values.model)\n      return true\n    },\n    onSuccess: () => {\n      queryClient.invalidateQueries({\n        queryKey: [\"fetchTitleSettings\"]\n      })\n    }\n  })\n\n  return (\n    <div className=\"flex flex-col gap-3\">\n      {status === \"pending\" && <Skeleton paragraph={{ rows: 4 }} active />}\n\n      {status === \"success\" && (\n        <div>\n          <div>\n            <h2 className=\"text-base font-semibold leading-7 text-gray-900 dark:text-white\">\n              Title Generation Settings\n            </h2>\n            <div className=\"border border-b border-gray-200 dark:border-gray-600 mt-3 mb-6\"></div>\n          </div>\n\n          <Form\n            form={form}\n            layout=\"vertical\"\n            onFinish={(values) => {\n              saveTitleSettings({\n                enabled: values.enabled,\n                prompt: values.prompt,\n                model: values.model\n              })\n            }}\n            initialValues={{\n              enabled: data.enabled,\n              prompt: data.prompt || DEFAULT_TITLE_GEN_PROMPT,\n              model: data.model || \"\"\n            }}>\n            <Form.Item\n              name=\"enabled\"\n              label=\"Enable Title Generation\"\n              valuePropName=\"checked\">\n              <Switch />\n            </Form.Item>\n\n            <Form.Item\n              name=\"model\"\n              label=\"Title Generation Model\"\n              help=\"Select a model for generating chat titles. Leave empty to use the default chat model.\"\n              rules={[\n                {\n                  required: false\n                }\n              ]}>\n              <Select\n                size=\"large\"\n                showSearch\n                allowClear\n                placeholder=\"Select a model (optional)\"\n                style={{ width: \"100%\" }}\n                className=\"mt-4\"\n                filterOption={(input, option) =>\n                  option.label.key\n                    .toLowerCase()\n                    .indexOf(input.toLowerCase()) >= 0\n                }\n                options={data.models?.map((model) => ({\n                  label: (\n                    <span\n                      key={model.model}\n                      className=\"flex flex-row gap-3 items-center truncate\">\n                      {model?.avatar ? (\n                        <Avatar\n                          src={model.avatar}\n                          alt={model.name}\n                          size=\"small\"\n                        />\n                      ) : (\n                        <ProviderIcons\n                          provider={model?.provider}\n                          className=\"w-5 h-5\"\n                        />\n                      )}\n                      <span className=\"truncate\">\n                        {model?.nickname || model?.name}\n                      </span>\n                    </span>\n                  ),\n                  value: model.model\n                }))}\n              />\n            </Form.Item>\n\n            <Form.Item\n              name=\"prompt\"\n              label=\"Title Generation Prompt\"\n              help=\"Use {{query}} as a placeholder for the user's query.\"\n              rules={[\n                {\n                  required: true,\n                  message: \"Enter a title generation prompt.\"\n                }\n              ]}>\n              <Input.TextArea\n                rows={10}\n                placeholder=\"Enter the prompt for generating titles...\"\n              />\n            </Form.Item>\n\n            <Form.Item>\n              <div className=\"flex justify-end\">\n                <SaveButton disabled={isPending} btnType=\"submit\" />\n              </div>\n            </Form.Item>\n          </Form>\n        </div>\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Option/Settings/tts-mode.tsx",
    "content": "import { SaveButton } from \"@/components/Common/SaveButton\"\nimport { getModels, getVoices } from \"@/services/elevenlabs\"\nimport { getTTSSettings, setTTSSettings } from \"@/services/tts\"\nimport { useWebUI } from \"@/store/webui\"\nimport { useForm } from \"@mantine/form\"\nimport { useQuery } from \"@tanstack/react-query\"\nimport { Input, InputNumber, message, Select, Skeleton, Switch } from \"antd\"\nimport { useTranslation } from \"react-i18next\"\n\nexport const TTSModeSettings = ({ hideBorder }: { hideBorder?: boolean }) => {\n  const { t } = useTranslation(\"settings\")\n  const { setTTSEnabled } = useWebUI()\n\n  const form = useForm({\n    initialValues: {\n      ttsEnabled: false,\n      ttsProvider: \"\",\n      voice: \"\",\n      ssmlEnabled: false,\n      removeReasoningTagTTS: true,\n      elevenLabsApiKey: \"\",\n      elevenLabsVoiceId: \"\",\n      elevenLabsModel: \"\",\n      responseSplitting: \"\",\n      openAITTSBaseUrl: \"\",\n      openAITTSApiKey: \"\",\n      openAITTSModel: \"\",\n      openAITTSVoice: \"\",\n      ttsAutoPlay: false,\n      playbackSpeed: 1\n    }\n  })\n\n  const { status, data } = useQuery({\n    queryKey: [\"fetchTTSSettings\"],\n    queryFn: async () => {\n      const data = await getTTSSettings()\n      form.setValues(data)\n      return data\n    }\n  })\n\n  const { data: elevenLabsData } = useQuery({\n    queryKey: [\"fetchElevenLabsData\", form.values.elevenLabsApiKey],\n    queryFn: async () => {\n      try {\n        if (\n          form.values.ttsProvider === \"elevenlabs\" &&\n          form.values.elevenLabsApiKey\n        ) {\n          const voices = await getVoices(form.values.elevenLabsApiKey)\n          const models = await getModels(form.values.elevenLabsApiKey)\n          return { voices, models }\n        }\n      } catch (e) {\n        console.error(e)\n        message.error(\"Error fetching ElevenLabs data\")\n      }\n      return null\n    },\n    enabled:\n      form.values.ttsProvider === \"elevenlabs\" && !!form.values.elevenLabsApiKey\n  })\n  if (status === \"pending\" || status === \"error\") {\n    return <Skeleton active />\n  }\n\n  return (\n    <div>\n      <div className=\"mb-5\">\n        <h2\n          className={`${\n            !hideBorder ? \"text-base font-semibold leading-7\" : \"text-md\"\n          } text-gray-900 dark:text-white`}>\n          {t(\"generalSettings.tts.heading\")}\n        </h2>\n        {!hideBorder && (\n          <div className=\"border border-b border-gray-200 dark:border-gray-600 mt-3\"></div>\n        )}\n      </div>\n      <form\n        onSubmit={form.onSubmit(async (values) => {\n          await setTTSSettings(values)\n          setTTSEnabled(values.ttsEnabled)\n        })}\n        className=\"space-y-4\">\n        <div className=\"flex sm:flex-row flex-col space-y-4 sm:space-y-0 sm:justify-between\">\n          <span className=\"text-gray-700 dark:text-neutral-50 \">\n            {t(\"generalSettings.tts.ttsEnabled.label\")}\n          </span>\n          <div>\n            <Switch\n              className=\"mt-4 sm:mt-0\"\n              {...form.getInputProps(\"ttsEnabled\", {\n                type: \"checkbox\"\n              })}\n            />\n          </div>\n        </div>\n        <div className=\"flex sm:flex-row flex-col space-y-4 sm:space-y-0 sm:justify-between\">\n          <span className=\"text-gray-700 dark:text-neutral-50 \">\n            {t(\"generalSettings.tts.ttsAutoPlay.label\")}\n          </span>\n          <div>\n            <Switch\n              className=\"mt-4 sm:mt-0\"\n              {...form.getInputProps(\"ttsAutoPlay\", {\n                type: \"checkbox\"\n              })}\n            />\n          </div>\n        </div>\n        <div className=\"flex sm:flex-row flex-col space-y-4 sm:space-y-0 sm:justify-between\">\n          <span className=\"text-gray-700 dark:text-neutral-50 \">\n            {t(\"generalSettings.tts.ttsProvider.label\")}\n          </span>\n          <div>\n            <Select\n              placeholder={t(\"generalSettings.tts.ttsProvider.placeholder\")}\n              className=\"w-full mt-4 sm:mt-0 sm:w-[200px]\"\n              options={[\n                { label: \"Browser TTS\", value: \"browser\" },\n                {\n                  label: \"ElevenLabs\",\n                  value: \"elevenlabs\"\n                },\n                {\n                  label: \"OpenAI TTS\",\n                  value: \"openai\"\n                }\n              ]}\n              {...form.getInputProps(\"ttsProvider\")}\n            />\n          </div>\n        </div>\n        {form.values.ttsProvider === \"browser\" && (\n          <div className=\"flex sm:flex-row flex-col space-y-4 sm:space-y-0 sm:justify-between\">\n            <span className=\"text-gray-700 dark:text-neutral-50 \">\n              {t(\"generalSettings.tts.ttsVoice.label\")}\n            </span>\n            <div>\n              <Select\n                placeholder={t(\"generalSettings.tts.ttsVoice.placeholder\")}\n                className=\"w-full mt-4 sm:mt-0 sm:w-[200px]\"\n                options={data?.browserTTSVoices?.map((voice) => ({\n                  label: `${voice.voiceName} - ${voice.lang}`.trim(),\n                  value: voice.voiceName\n                }))}\n                {...form.getInputProps(\"voice\")}\n              />\n            </div>\n          </div>\n        )}\n        {form.values.ttsProvider === \"elevenlabs\" && (\n          <>\n            <div className=\"flex sm:flex-row flex-col space-y-4 sm:space-y-0 sm:justify-between\">\n              <span className=\"text-gray-700 dark:text-neutral-50\">\n                API Key\n              </span>\n              <Input.Password\n                placeholder=\"sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"\n                className=\" mt-4 sm:mt-0 !w-[300px] sm:w-[200px]\"\n                required\n                {...form.getInputProps(\"elevenLabsApiKey\")}\n              />\n            </div>\n\n            {elevenLabsData && (\n              <>\n                <div className=\"flex sm:flex-row flex-col space-y-4 sm:space-y-0 sm:justify-between\">\n                  <span className=\"text-gray-700 dark:text-neutral-50\">\n                    TTS Voice\n                  </span>\n                  <Select\n                    options={elevenLabsData.voices.map((v) => ({\n                      label: v.name,\n                      value: v.voice_id\n                    }))}\n                    className=\"w-full mt-4 sm:mt-0 sm:w-[200px]\"\n                    placeholder=\"Select a voice\"\n                    {...form.getInputProps(\"elevenLabsVoiceId\")}\n                  />\n                </div>\n\n                <div className=\"flex sm:flex-row flex-col space-y-4 sm:space-y-0 sm:justify-between\">\n                  <span className=\"text-gray-700 dark:text-neutral-50\">\n                    TTS Model\n                  </span>\n                  <Select\n                    className=\"w-full mt-4 sm:mt-0 sm:w-[200px]\"\n                    placeholder=\"Select a model\"\n                    options={elevenLabsData.models.map((m) => ({\n                      label: m.name,\n                      value: m.model_id\n                    }))}\n                    {...form.getInputProps(\"elevenLabsModel\")}\n                  />\n                </div>\n                <div className=\"flex sm:flex-row flex-col space-y-4 sm:space-y-0 sm:justify-between\">\n                  <span className=\"text-gray-700 dark:text-neutral-50 \">\n                    {t(\"generalSettings.tts.responseSplitting.label\")}\n                  </span>\n                  <div>\n                    <Select\n                      placeholder={t(\n                        \"generalSettings.tts.responseSplitting.placeholder\"\n                      )}\n                      className=\"w-full mt-4 sm:mt-0 sm:w-[200px]\"\n                      options={[\n                        { label: \"None\", value: \"none\" },\n                        { label: \"Punctuation\", value: \"punctuation\" },\n                        { label: \"Paragraph\", value: \"paragraph\" }\n                      ]}\n                      {...form.getInputProps(\"responseSplitting\")}\n                    />\n                  </div>\n                </div>\n              </>\n            )}\n          </>\n        )}\n        {form.values.ttsProvider === \"openai\" && (\n          <>\n            <div className=\"flex sm:flex-row flex-col space-y-4 sm:space-y-0 sm:justify-between\">\n              <span className=\"text-gray-700 dark:text-neutral-50\">\n                Base URL\n              </span>\n              <Input\n                placeholder=\"http://localhost:5000/v1\"\n                className=\" mt-4 sm:mt-0 !w-[300px] sm:w-[200px]\"\n                required\n                {...form.getInputProps(\"openAITTSBaseUrl\")}\n              />\n            </div>\n\n            <div className=\"flex sm:flex-row flex-col space-y-4 sm:space-y-0 sm:justify-between\">\n              <span className=\"text-gray-700 dark:text-neutral-50\">\n                API Key\n              </span>\n              <Input.Password\n                placeholder=\"sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"\n                className=\" mt-4 sm:mt-0 !w-[300px] sm:w-[200px]\"\n                {...form.getInputProps(\"openAITTSApiKey\")}\n              />\n            </div>\n\n            <div className=\"flex sm:flex-row flex-col space-y-4 sm:space-y-0 sm:justify-between\">\n              <span className=\"text-gray-700 dark:text-neutral-50\">\n                TTS Voice\n              </span>\n              <Input\n                placeholder=\"alloy\"\n                className=\" mt-4 sm:mt-0 !w-[300px] sm:w-[200px]\"\n                required\n                {...form.getInputProps(\"openAITTSVoice\")}\n              />\n            </div>\n\n            <div className=\"flex sm:flex-row flex-col space-y-4 sm:space-y-0 sm:justify-between\">\n              <span className=\"text-gray-700 dark:text-neutral-50\">\n                TTS Model\n              </span>\n              <Input\n                placeholder=\"tts-1\"\n                className=\" mt-4 sm:mt-0 !w-[300px] sm:w-[200px]\"\n                required\n                {...form.getInputProps(\"openAITTSModel\")}\n              />\n            </div>\n          </>\n        )}\n        <div className=\"flex sm:flex-row flex-col space-y-4 sm:space-y-0 sm:justify-between\">\n          <span className=\"text-gray-700 dark:text-neutral-50 \">\n            {t(\"generalSettings.tts.ssmlEnabled.label\")}\n          </span>\n          <div>\n            <Switch\n              className=\"mt-4 sm:mt-0\"\n              {...form.getInputProps(\"ssmlEnabled\", {\n                type: \"checkbox\"\n              })}\n            />\n          </div>\n        </div>\n\n        <div className=\"flex sm:flex-row flex-col space-y-4 sm:space-y-0 sm:justify-between\">\n          <span className=\"text-gray-700 dark:text-neutral-50 \">\n            {t(\"generalSettings.tts.removeReasoningTagTTS.label\")}\n          </span>\n          <div>\n            <Switch\n              className=\"mt-4 sm:mt-0\"\n              {...form.getInputProps(\"removeReasoningTagTTS\", {\n                type: \"checkbox\"\n              })}\n            />\n          </div>\n        </div>\n\n        <div className=\"flex sm:flex-row flex-col space-y-4 sm:space-y-0 sm:justify-between\">\n          <span className=\"text-gray-700 dark:text-neutral-50\">\n            Playback Speed\n          </span>\n          <InputNumber\n            placeholder=\"1\"\n            className=\" mt-4 sm:mt-0 !w-[300px] sm:w-[200px]\"\n            required\n            {...form.getInputProps(\"playbackSpeed\")}\n          />\n        </div>\n\n        <div className=\"flex justify-end\">\n          <SaveButton btnType=\"submit\" />\n        </div>\n      </form>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Option/Share/index.tsx",
    "content": "import { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\"\nimport { Form, Input, Skeleton, Switch, Table, Tooltip, message } from \"antd\"\nimport { Trash2 } from \"lucide-react\"\nimport { Trans, useTranslation } from \"react-i18next\"\nimport { SaveButton } from \"~/components/Common/SaveButton\"\nimport { deleteWebshare, getAllWebshares, getUserId } from \"@/db/dexie/helpers\"\nimport { getPageShareUrl, setPageShareUrl } from \"~/services/ollama\"\nimport { verifyPageShareURL } from \"~/utils/verify-page-share\"\nimport { useStorage } from \"@plasmohq/storage/hook\"\nimport fetcher from \"@/libs/fetcher\"\n\nexport const OptionShareBody = () => {\n  const queryClient = useQueryClient()\n  const { t } = useTranslation([\"settings\"])\n  const [shareModeEnabled, setShareModelEnabled] = useStorage(\n    \"shareMode\",\n    false\n  )\n\n  const { status, data } = useQuery({\n    queryKey: [\"fetchShareInfo\"],\n    queryFn: async () => {\n      const [url, shares] = await Promise.all([\n        getPageShareUrl(),\n        getAllWebshares()\n      ])\n      return { url, shares }\n    }\n  })\n\n  const onSubmit = async (values: { url: string }) => {\n    if (shareModeEnabled) {\n      const isOk = await verifyPageShareURL(values.url)\n      if (isOk) {\n        await setPageShareUrl(values.url)\n      }\n    } else {\n      await setPageShareUrl(values.url)\n    }\n  }\n\n  const onDelete = async ({\n    api_url,\n    share_id,\n    id\n  }: {\n    id: string\n    share_id: string\n    api_url: string\n  }) => {\n    const owner_id = await getUserId()\n    const res = await fetcher(`${api_url}/api/v1/share/delete`, {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\"\n      },\n      body: JSON.stringify({\n        share_id,\n        owner_id\n      })\n    })\n    if (!res.ok) throw new Error(\"Failed to delete share link\")\n    await deleteWebshare(id)\n    return \"ok\"\n  }\n\n  const { mutate: updatePageShareUrl, isPending: isUpdatePending } =\n    useMutation({\n      mutationFn: onSubmit,\n      onSuccess: () => {\n        queryClient.invalidateQueries({\n          queryKey: [\"fetchShareInfo\"]\n        })\n        message.success(t(\"manageShare.notification.pageShareSuccess\"))\n      },\n      onError: (error) => {\n        message.error(error?.message || t(\"manageShare.notification.someError\"))\n      }\n    })\n\n  const { mutate: deleteMutation } = useMutation({\n    mutationFn: onDelete,\n    onSuccess: () => {\n      queryClient.invalidateQueries({\n        queryKey: [\"fetchShareInfo\"]\n      })\n      message.success(t(\"manageShare.notification.webShareDeleteSuccess\"))\n    },\n    onError: (error) => {\n      message.error(error?.message || t(\"manageShare.notification.someError\"))\n    }\n  })\n\n  return (\n    <div className=\"flex flex-col space-y-3\">\n      {status === \"pending\" && <Skeleton paragraph={{ rows: 4 }} active />}\n      {status === \"success\" && (\n        <div>\n          <div>\n            <div>\n              <h2 className=\"text-base font-semibold leading-7 text-gray-900 dark:text-white\">\n                {t(\"manageShare.heading\")}\n              </h2>\n              <div className=\"border border-b border-gray-200 dark:border-gray-600 mt-3 mb-6\"></div>\n            </div>\n            <Form\n              layout=\"vertical\"\n              onFinish={updatePageShareUrl}\n              initialValues={{\n                url: data.url\n              }}>\n              <Form.Item\n                name=\"url\"\n                help={\n                  <Trans\n                    i18nKey=\"settings:manageShare.form.url.help\"\n                    components={{\n                      anchor: (\n                        <a\n                          href=\"https://github.com/n4ze3m/page-assist/blob/main/page-share.md\"\n                          target=\"__blank\"\n                          className=\"text-blue-600 dark:text-blue-400\"></a>\n                      )\n                    }}\n                  />\n                }\n                rules={[\n                  {\n                    required: true,\n                    message: t(\"manageShare.form.url.required\")\n                  }\n                ]}\n                label={t(\"manageShare.form.url.label\")}>\n                <Input\n                  placeholder={t(\"manageShare.form.url.placeholder\")}\n                  size=\"large\"\n                />\n              </Form.Item>\n              <Form.Item>\n                <div className=\"flex justify-end\">\n                  <SaveButton disabled={isUpdatePending} btnType=\"submit\" />\n                </div>\n              </Form.Item>\n            </Form>\n            <div className=\"space-y-2 flex mb-4 flex-row items-center justify-between rounded-lg  dark:border-gray-600 \">\n              <div className=\"space-y-0.5\">\n                <label className=\"text-sm font-semibold leading-5 text-gray-900 dark:text-white\">\n                  {t(\"manageShare.webshare.label\")}\n                </label>\n                <p className=\"text-sm font-normal leading-5 text-gray-700 dark:text-gray-400\">\n                  {t(\"manageShare.webshare.description\")}\n                </p>\n              </div>\n              <Switch\n                checked={shareModeEnabled}\n                onChange={setShareModelEnabled}\n              />\n            </div>\n          </div>\n          <div>\n            <div>\n              <h2 className=\"text-base font-semibold leading-7 text-gray-900 dark:text-white\">\n                {t(\"manageShare.webshare.heading\")}\n              </h2>\n              <div className=\"border border-b border-gray-200 dark:border-gray-600 mt-3 mb-6\"></div>\n            </div>\n            <div>\n              <Table\n                dataSource={data.shares}\n                columns={[\n                  {\n                    title: t(\"manageShare.webshare.columns.title\"),\n                    dataIndex: \"title\",\n                    key: \"title\"\n                  },\n                  {\n                    title: t(\"manageShare.webshare.columns.url\"),\n                    dataIndex: \"url\",\n                    key: \"url\",\n                    render: (url: string) => (\n                      <a\n                        href={url}\n                        target=\"__blank\"\n                        className=\"text-blue-600 dark:text-blue-400\">\n                        {url}\n                      </a>\n                    )\n                  },\n                  {\n                    title: t(\"manageShare.webshare.columns.actions\"),\n                    render: (_, render) => (\n                      <Tooltip title={t(\"manageShare.webshare.tooltip.delete\")}>\n                        <button\n                          onClick={() => {\n                            if (\n                              window.confirm(\n                                t(\"manageShare.webshare.confirm.delete\")\n                              )\n                            ) {\n                              deleteMutation({\n                                id: render.id,\n                                share_id: render.share_id,\n                                api_url: render.api_url\n                              })\n                            }\n                          }}\n                          className=\"text-red-500 dark:text-red-400\">\n                          <Trash2 className=\"w-5 h-5\" />\n                        </button>\n                      </Tooltip>\n                    )\n                  }\n                ]}\n              />\n            </div>\n          </div>\n        </div>\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Option/Sidebar.tsx",
    "content": "import {\n  useMutation,\n  useQueryClient,\n  useInfiniteQuery,\n  useQuery\n} from \"@tanstack/react-query\"\nimport {\n  Empty,\n  Skeleton,\n  Dropdown,\n  Menu,\n  Tooltip,\n  Input,\n  message,\n  Button\n} from \"antd\"\nimport { SaveButton } from \"@/components/Common/SaveButton\"\nimport {\n  PencilIcon,\n  Trash2,\n  MoreVertical,\n  PinIcon,\n  PinOffIcon,\n  BotIcon,\n  SearchIcon,\n  Trash2Icon,\n  Loader2,\n  ChevronDown,\n  GitBranch,\n  Sparkles,\n  FolderPlus,\n  Folder\n} from \"lucide-react\"\nimport { useNavigate } from \"react-router-dom\"\nimport { useTranslation } from \"react-i18next\"\nimport { lastUsedChatModelEnabled } from \"@/services/model-settings\"\nimport { useDebounce } from \"@/hooks/useDebounce\"\nimport { useState, useRef, useEffect } from \"react\"\nimport { PageAssistDatabase } from \"@/db/dexie/chat\"\nimport {\n  deleteByHistoryId,\n  deleteHistoriesByDateRange,\n  formatToChatHistory,\n  formatToConversationHistory,\n  updateHistory,\n  pinHistory,\n  formatToMessage,\n  getSessionFiles,\n  getPromptById,\n  getProjectFolders,\n  addProjectFolder,\n  updateProjectFolder,\n  deleteProjectFolder,\n  assignHistoryToFolder\n} from \"@/db/dexie/helpers\"\nimport { UploadedFile } from \"@/db/dexie/types\"\nimport { isDatabaseClosedError } from \"@/utils/ff-error\"\nimport { updatePageTitle } from \"@/utils/update-page-title\"\nimport { generateTitle } from \"@/services/title\"\n\ntype Props = {\n  onClose: () => void\n  setMessages: (messages: any) => void\n  setHistory: (history: any) => void\n  setHistoryId: (historyId: string) => void\n  setSelectedModel: (model: string) => void\n  setSelectedSystemPrompt: (prompt: string) => void\n  setSystemPrompt: (prompt: string) => void\n  setContext?: (context: UploadedFile[]) => void\n  clearChat: () => void\n  temporaryChat: boolean\n  historyId: string\n  history: any\n  isOpen: boolean\n  selectedModel: string\n}\n\nexport const Sidebar = ({\n  onClose,\n  setMessages,\n  setHistory,\n  setHistoryId,\n  setSelectedModel,\n  setSelectedSystemPrompt,\n  clearChat,\n  historyId,\n  setSystemPrompt,\n  temporaryChat,\n  isOpen,\n  setContext,\n  selectedModel\n}: Props) => {\n  const { t } = useTranslation([\"option\", \"common\"])\n  const client = useQueryClient()\n  const navigate = useNavigate()\n  const [searchQuery, setSearchQuery] = useState(\"\")\n  const debouncedSearchQuery = useDebounce(searchQuery, 300)\n  const [deleteGroup, setDeleteGroup] = useState<string | null>(null)\n  const [dexiePrivateWindowError, setDexiePrivateWindowError] = useState(false)\n  const [editingHistoryId, setEditingHistoryId] = useState<string | null>(null)\n  const [editTitle, setEditTitle] = useState(\"\")\n  const [generatingTitleId, setGeneratingTitleId] = useState<string | null>(\n    null\n  )\n  const [newProjectTitle, setNewProjectTitle] = useState(\"\")\n  const [isCreatingProject, setIsCreatingProject] = useState(false)\n  const [editingProjectId, setEditingProjectId] = useState<string | null>(null)\n  const [editProjectTitle, setEditProjectTitle] = useState(\"\")\n  const [draggingChatId, setDraggingChatId] = useState<string | null>(null)\n  const [dragOverProjectId, setDragOverProjectId] = useState<string | null>(\n    null\n  )\n  const [collapsedFolders, setCollapsedFolders] = useState<Set<string>>(\n    new Set()\n  )\n  const projectCreationCardRef = useRef<HTMLDivElement>(null)\n\n  useEffect(() => {\n    const handleClickOutside = (event: MouseEvent) => {\n      if (\n        isCreatingProject &&\n        projectCreationCardRef.current &&\n        !projectCreationCardRef.current.contains(event.target as Node)\n      ) {\n        setIsCreatingProject(false)\n        setNewProjectTitle(\"\")\n      }\n    }\n\n    if (isCreatingProject) {\n      document.addEventListener(\"mousedown\", handleClickOutside)\n    }\n\n    return () => {\n      document.removeEventListener(\"mousedown\", handleClickOutside)\n    }\n  }, [isCreatingProject])\n\n  // Helper function to group chats by date\n  const groupChatsByDate = (chats: any[]) => {\n    const now = new Date()\n    const today = new Date(now.setHours(0, 0, 0, 0))\n    const yesterday = new Date(today)\n    yesterday.setDate(yesterday.getDate() - 1)\n    const lastWeek = new Date(today)\n    lastWeek.setDate(lastWeek.getDate() - 7)\n\n    const todayItems = chats.filter(\n      (item) => new Date(item?.createdAt) >= today\n    )\n    const yesterdayItems = chats.filter(\n      (item) =>\n        new Date(item?.createdAt) >= yesterday &&\n        new Date(item?.createdAt) < today\n    )\n    const lastWeekItems = chats.filter(\n      (item) =>\n        new Date(item?.createdAt) >= lastWeek &&\n        new Date(item?.createdAt) < yesterday\n    )\n    const olderItems = chats.filter(\n      (item) => new Date(item?.createdAt) < lastWeek\n    )\n\n    const groups = []\n    if (todayItems.length) groups.push({ label: \"today\", items: todayItems })\n    if (yesterdayItems.length)\n      groups.push({ label: \"yesterday\", items: yesterdayItems })\n    if (lastWeekItems.length)\n      groups.push({ label: \"last7Days\", items: lastWeekItems })\n    if (olderItems.length) groups.push({ label: \"older\", items: olderItems })\n\n    return groups\n  }\n\n  const handleEditStart = (chat: any) => {\n    setEditingHistoryId(chat.id)\n    setEditTitle(chat.title)\n  }\n\n  const handleEditCancel = () => {\n    setEditingHistoryId(null)\n    setEditTitle(\"\")\n  }\n\n  const handleGenerateTitle = async (chat: any) => {\n    setGeneratingTitleId(chat.id)\n    try {\n      const db = new PageAssistDatabase()\n      const history = await db.getChatHistory(chat.id)\n      const chatHistory = formatToConversationHistory(history)\n      const model = selectedModel\n\n      const generatedTitle = await generateTitle(model, chatHistory, chat.title)\n      if (generatedTitle && generatedTitle !== chat.title) {\n        setEditTitle(generatedTitle)\n      }\n    } catch (error) {\n      console.error(\"Error generating title:\", error)\n      message.error(\n        t(\"common:generateTitleError\", {\n          defaultValue: \"Failed to generate title\"\n        })\n      )\n    } finally {\n      setGeneratingTitleId(null)\n    }\n  }\n\n  const handleEditSave = (id: string, currentTitle: string) => {\n    if (editTitle.trim() && editTitle.trim() !== currentTitle) {\n      editHistory({ id, title: editTitle.trim() })\n    }\n    setEditingHistoryId(null)\n    setEditTitle(\"\")\n  }\n\n  // Using infinite query for pagination\n  const {\n    data: chatHistoriesData,\n    status,\n    fetchNextPage,\n    hasNextPage,\n    isFetchingNextPage,\n    isLoading\n  } = useInfiniteQuery({\n    queryKey: [\"fetchChatHistory\", debouncedSearchQuery],\n    queryFn: async ({ pageParam = 1 }) => {\n      try {\n        const db = new PageAssistDatabase()\n        const result = await db.getChatHistoriesPaginated(\n          pageParam,\n          debouncedSearchQuery || undefined\n        )\n\n        // If searching, don't group by date - just return all results in a single group\n        if (debouncedSearchQuery) {\n          console.log(\"Search results:\", result.histories)\n          return {\n            groups:\n              result.histories.length > 0\n                ? [{ label: \"searchResults\", items: result.histories }]\n                : [],\n            hasMore: result.hasMore,\n            totalCount: result.totalCount\n          }\n        }\n\n        // Group the histories by date only when not searching\n        const now = new Date()\n        const today = new Date(now.setHours(0, 0, 0, 0))\n        const yesterday = new Date(today)\n        yesterday.setDate(yesterday.getDate() - 1)\n        const lastWeek = new Date(today)\n        lastWeek.setDate(lastWeek.getDate() - 7)\n\n        const pinnedItems = result.histories.filter((item) => item.is_pinned)\n        const todayItems = result.histories.filter(\n          (item) => !item.is_pinned && new Date(item?.createdAt) >= today\n        )\n        const yesterdayItems = result.histories.filter(\n          (item) =>\n            !item.is_pinned &&\n            new Date(item?.createdAt) >= yesterday &&\n            new Date(item?.createdAt) < today\n        )\n        const lastWeekItems = result.histories.filter(\n          (item) =>\n            !item.is_pinned &&\n            new Date(item?.createdAt) >= lastWeek &&\n            new Date(item?.createdAt) < yesterday\n        )\n        const olderItems = result.histories.filter(\n          (item) => !item.is_pinned && new Date(item?.createdAt) < lastWeek\n        )\n\n        const groups = []\n\n        // Always get all pinned items for the first page to ensure they appear at the top\n        if (pageParam === 1) {\n          try {\n            const db = new PageAssistDatabase()\n            const allPinnedItems = await db.getChatHistories()\n            const pinnedOnlyItems = allPinnedItems.filter(\n              (item) => item.is_pinned\n            )\n            if (pinnedOnlyItems.length > 0) {\n              groups.push({ label: \"pinned\", items: pinnedOnlyItems })\n            }\n          } catch (e) {\n            // Fallback to pinned items from current page if db query fails\n            if (pinnedItems.length)\n              groups.push({ label: \"pinned\", items: pinnedItems })\n          }\n        }\n\n        if (todayItems.length)\n          groups.push({ label: \"today\", items: todayItems })\n        if (yesterdayItems.length)\n          groups.push({ label: \"yesterday\", items: yesterdayItems })\n        if (lastWeekItems.length)\n          groups.push({ label: \"last7Days\", items: lastWeekItems })\n        if (olderItems.length)\n          groups.push({ label: \"older\", items: olderItems })\n\n        return {\n          groups,\n          hasMore: result.hasMore,\n          totalCount: result.totalCount\n        }\n      } catch (e) {\n        setDexiePrivateWindowError(isDatabaseClosedError(e))\n        return {\n          groups: [],\n          hasMore: false,\n          totalCount: 0\n        }\n      }\n    },\n    getNextPageParam: (lastPage, allPages) => {\n      return lastPage.hasMore ? allPages.length + 1 : undefined\n    },\n    placeholderData: undefined,\n    enabled: isOpen,\n    initialPageParam: 1\n  })\n\n  // Flatten all groups from all pages\n  const chatHistories =\n    chatHistoriesData?.pages.reduce(\n      (acc, page) => {\n        page.groups.forEach((group) => {\n          const existingGroup = acc.find((g) => g.label === group.label)\n          if (existingGroup) {\n            const newItems = group.items.filter(\n              (newItem) =>\n                !existingGroup.items.some(\n                  (existingItem) => existingItem.id === newItem.id\n                )\n            )\n            existingGroup.items.push(...newItems)\n          } else {\n            acc.push({ ...group })\n          }\n        })\n        return acc\n      },\n      [] as Array<{ label: string; items: any[] }>\n    ) || []\n\n  const orderedChatHistories = chatHistories.sort((a, b) => {\n    const order = [\n      \"pinned\",\n      \"today\",\n      \"yesterday\",\n      \"last7Days\",\n      \"older\",\n      \"searchResults\"\n    ]\n    return order.indexOf(a.label) - order.indexOf(b.label)\n  })\n\n  const { mutate: deleteHistory } = useMutation({\n    mutationKey: [\"deleteHistory\"],\n    mutationFn: deleteByHistoryId,\n    onSuccess: (history_id) => {\n      client.invalidateQueries({\n        queryKey: [\"fetchChatHistory\"]\n      })\n      if (historyId === history_id) {\n        clearChat()\n        updatePageTitle()\n      }\n    }\n  })\n\n  const { mutate: editHistory } = useMutation({\n    mutationKey: [\"editHistory\"],\n    mutationFn: async (data: { id: string; title: string }) => {\n      return await updateHistory(data.id, data.title)\n    },\n    onSuccess: () => {\n      client.invalidateQueries({\n        queryKey: [\"fetchChatHistory\"]\n      })\n    }\n  })\n\n  const { mutate: deleteHistoriesByRange, isPending: deleteRangeLoading } =\n    useMutation({\n      mutationKey: [\"deleteHistoriesByRange\"],\n      mutationFn: async (rangeLabel: string) => {\n        setDeleteGroup(rangeLabel)\n        return await deleteHistoriesByDateRange(rangeLabel)\n      },\n      onSuccess: (deletedIds) => {\n        client.invalidateQueries({\n          queryKey: [\"fetchChatHistory\"]\n        })\n\n        if (deletedIds.includes(historyId)) {\n          clearChat()\n        }\n\n        message.success(\n          t(\"common:historiesDeleted\", { count: deletedIds.length })\n        )\n      },\n      onError: (error) => {\n        console.error(\"Failed to delete histories:\", error)\n        message.error(t(\"common:deleteHistoriesError\"))\n      }\n    })\n\n  const handleDeleteHistoriesByRange = (rangeLabel: string) => {\n    if (!confirm(t(`common:range:deleteConfirm:${rangeLabel}`))) {\n      return\n    }\n    deleteHistoriesByRange(rangeLabel)\n  }\n\n  const { mutate: pinChatHistory, isPending: pinLoading } = useMutation({\n    mutationKey: [\"pinHistory\"],\n    mutationFn: async (data: { id: string; is_pinned: boolean }) => {\n      return await pinHistory(data.id, data.is_pinned)\n    },\n    onSuccess: () => {\n      client.invalidateQueries({\n        queryKey: [\"fetchChatHistory\"]\n      })\n    }\n  })\n\n  const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    setSearchQuery(e.target.value)\n  }\n\n  const { data: projectFoldersData = [] } = useQuery({\n    queryKey: [\"fetchProjectFolders\"],\n    queryFn: async () => {\n      return await getProjectFolders()\n    },\n    enabled: isOpen\n  })\n\n  const projectFolders = projectFoldersData || []\n\n  const { mutate: createProjectFolder, isPending: creatingProject } =\n    useMutation({\n      mutationKey: [\"createProjectFolder\"],\n      mutationFn: async (data: { title: string }) => {\n        return await addProjectFolder(data.title)\n      },\n      onSuccess: () => {\n        client.invalidateQueries({ queryKey: [\"fetchProjectFolders\"] })\n        setNewProjectTitle(\"\")\n        setIsCreatingProject(false)\n      }\n    })\n\n  const { mutate: renameProjectFolder } = useMutation({\n    mutationKey: [\"renameProjectFolder\"],\n    mutationFn: async (data: { id: string; title: string }) => {\n      return await updateProjectFolder(data.id, data.title)\n    },\n    onSuccess: () => {\n      client.invalidateQueries({ queryKey: [\"fetchProjectFolders\"] })\n      setEditingProjectId(null)\n      setEditProjectTitle(\"\")\n    }\n  })\n\n  const { mutate: removeProjectFolder } = useMutation({\n    mutationKey: [\"deleteProjectFolder\"],\n    mutationFn: async (id: string) => {\n      return await deleteProjectFolder(id)\n    },\n    onSuccess: () => {\n      client.invalidateQueries({ queryKey: [\"fetchProjectFolders\"] })\n      client.invalidateQueries({ queryKey: [\"fetchChatHistory\"] })\n    }\n  })\n\n  const { mutate: moveChatToFolder } = useMutation({\n    mutationKey: [\"assignHistoryToFolder\"],\n    mutationFn: async (data: { historyId: string; folderId?: string }) => {\n      return await assignHistoryToFolder(data.historyId, data.folderId)\n    },\n    onSuccess: () => {\n      client.invalidateQueries({ queryKey: [\"fetchChatHistory\"] })\n    }\n  })\n\n  const startProjectEdit = (project: { id: string; title: string }) => {\n    setEditingProjectId(project.id)\n    setEditProjectTitle(project.title)\n  }\n\n  const cancelProjectEdit = () => {\n    setEditingProjectId(null)\n    setEditProjectTitle(\"\")\n  }\n\n  const saveProjectEdit = (projectId: string, currentTitle: string) => {\n    const trimmedTitle = editProjectTitle.trim()\n    if (trimmedTitle && trimmedTitle !== currentTitle) {\n      renameProjectFolder({ id: projectId, title: trimmedTitle })\n    } else {\n      cancelProjectEdit()\n    }\n  }\n\n  const handleCreateProject = () => {\n    const trimmedTitle = newProjectTitle.trim()\n    if (!trimmedTitle) {\n      return\n    }\n    createProjectFolder({ title: trimmedTitle })\n  }\n\n  const handleDeleteProject = (projectId: string) => {\n    if (\n      !confirm(\n        t(\"common:deleteProjectConfirmation\", {\n          defaultValue:\n            \"Delete this project folder? Chats will move back to Your chats.\"\n        })\n      )\n    ) {\n      return\n    }\n    removeProjectFolder(projectId)\n  }\n\n  const clearSearch = () => {\n    setSearchQuery(\"\")\n  }\n\n  const folderMap = projectFolders.reduce<Record<string, any>>(\n    (acc, folder) => {\n      acc[folder.id] = folder\n      return acc\n    },\n    {}\n  )\n\n  const isSearchActive = Boolean(debouncedSearchQuery)\n  const allChats = orderedChatHistories.flatMap((group) => group.items)\n\n  // Separate pinned chats from the rest\n  const pinnedChats = allChats.filter((chat) => chat.is_pinned)\n  const unpinnedChats = allChats.filter((chat) => !chat.is_pinned)\n\n  const projectChatsMap = unpinnedChats.reduce<Record<string, any[]>>(\n    (acc, chat) => {\n      if (!chat.folder_id) {\n        return acc\n      }\n      if (!acc[chat.folder_id]) {\n        acc[chat.folder_id] = []\n      }\n      acc[chat.folder_id].push(chat)\n      return acc\n    },\n    {}\n  )\n\n  const unassignedChats = unpinnedChats.filter((chat) => !chat.folder_id)\n\n  const handleDragStart = (chatId: string) => {\n    setDraggingChatId(chatId)\n  }\n\n  const handleDragEnd = () => {\n    setDraggingChatId(null)\n    setDragOverProjectId(null)\n  }\n\n  const handleDropOnFolder = (folderId?: string) => {\n    if (!draggingChatId) {\n      return\n    }\n    moveChatToFolder({ historyId: draggingChatId, folderId })\n    handleDragEnd()\n  }\n\n  const toggleFolderCollapse = (folderId: string) => {\n    setCollapsedFolders((prev) => {\n      const newSet = new Set(prev)\n      if (newSet.has(folderId)) {\n        newSet.delete(folderId)\n      } else {\n        newSet.add(folderId)\n      }\n      return newSet\n    })\n  }\n\n  const renderChatRow = (chat: any) => {\n    return (\n      <div\n        key={chat.id}\n        draggable\n        onDragStart={() => handleDragStart(chat.id)}\n        onDragEnd={handleDragEnd}\n        className={`flex py-2 px-2 items-center gap-3 relative rounded-md truncate hover:pr-4 group transition-opacity duration-300 ease-in-out border ${\n          historyId === chat.id\n            ? \"bg-gray-200 dark:bg-[#454242] border-gray-400 dark:border-gray-600 text-gray-900 dark:text-gray-100\"\n            : \"bg-gray-50 dark:bg-[#242424] dark:text-gray-100 text-gray-800 border-gray-300 dark:border-[#404040] hover:bg-gray-200 dark:hover:bg-[#2a2a2a]\"\n        }`}\n        data-chat-id={chat.id}>\n        {chat?.message_source === \"copilot\" && (\n          <BotIcon className=\"size-3 text-gray-500 dark:text-gray-400\" />\n        )}\n        {chat?.message_source === \"branch\" && (\n          <GitBranch className=\"size-3 text-gray-500 dark:text-gray-400\" />\n        )}\n        {editingHistoryId === chat.id ? (\n          <div className=\"flex items-center flex-1 gap-1\">\n            <input\n              value={editTitle}\n              onChange={(e) => setEditTitle(e.target.value)}\n              onKeyDown={(e) => {\n                if (e.key === \"Enter\") {\n                  handleEditSave(chat.id, chat.title)\n                } else if (e.key === \"Escape\") {\n                  handleEditCancel()\n                }\n                e.stopPropagation()\n              }}\n              onClick={(e) => e.stopPropagation()}\n              onBlur={(e) => {\n                if (e.relatedTarget?.closest(\"[data-generate-btn]\")) {\n                  return\n                }\n                handleEditSave(chat.id, chat.title)\n              }}\n              autoFocus\n              disabled={generatingTitleId === chat.id}\n              className={`flex-1 h-8 text-sm z-20 px-0 bg-transparent outline-none border-none dark:focus:ring-[#404040] focus:ring-gray-300 focus:rounded-md focus:p-2 caret-current selection:bg-gray-300 dark:selection:bg-gray-600 ${\n                historyId === chat.id\n                  ? \"text-gray-900 dark:text-gray-100 placeholder-gray-500\"\n                  : \"text-gray-800 dark:text-gray-100 placeholder-gray-400\"\n              } ${generatingTitleId === chat.id ? \"opacity-50\" : \"\"}`}\n            />\n            <Tooltip\n              title={t(\"common:generateTitle\", {\n                defaultValue: \"Generate title with AI\"\n              })}>\n              <button\n                data-generate-btn\n                onClick={(e) => {\n                  e.stopPropagation()\n                  handleGenerateTitle(chat)\n                }}\n                disabled={generatingTitleId === chat.id}\n                className={`p-1 rounded-md transition-all duration-200 ${\n                  generatingTitleId === chat.id\n                    ? \"text-purple-500 dark:text-purple-400\"\n                    : \"text-gray-400 hover:text-purple-500 dark:hover:text-purple-400 hover:bg-gray-200 dark:hover:bg-gray-700\"\n                }`}>\n                <Sparkles\n                  className={`w-4 h-4 ${\n                    generatingTitleId === chat.id ? \"animate-pulse\" : \"\"\n                  }`}\n                  style={\n                    generatingTitleId === chat.id\n                      ? {\n                          filter: \"drop-shadow(0 0 4px rgba(168, 85, 247, 0.6))\"\n                        }\n                      : undefined\n                  }\n                />\n              </button>\n            </Tooltip>\n          </div>\n        ) : (\n          <button\n            className=\"flex-1 overflow-hidden break-all text-start truncate w-full\"\n            onClick={async () => {\n              const db = new PageAssistDatabase()\n              const history = await db.getChatHistory(chat.id)\n              const historyDetails = await db.getHistoryInfo(chat.id)\n              setHistoryId(chat.id)\n              setHistory(formatToChatHistory(history))\n              setMessages(formatToMessage(history))\n              const isLastUsedChatModel = await lastUsedChatModelEnabled()\n              if (isLastUsedChatModel) {\n                const currentChatModel = historyDetails?.model_id\n                if (currentChatModel) {\n                  setSelectedModel(currentChatModel)\n                }\n              }\n              const lastUsedPrompt = historyDetails?.last_used_prompt\n              if (lastUsedPrompt) {\n                if (lastUsedPrompt.prompt_id) {\n                  const prompt = await getPromptById(lastUsedPrompt.prompt_id)\n                  if (prompt) {\n                    setSelectedSystemPrompt(lastUsedPrompt.prompt_id)\n                  }\n                }\n                setSystemPrompt(lastUsedPrompt.prompt_content)\n              }\n\n              if (setContext) {\n                const session = await getSessionFiles(chat.id)\n                setContext(session)\n              }\n              updatePageTitle(chat.title)\n              navigate(\"/\")\n              onClose()\n            }}>\n            <span className=\"flex-grow truncate\">{chat.title}</span>\n          </button>\n        )}\n        <div className=\"flex items-center gap-2\">\n          <Dropdown\n            overlay={\n              <Menu>\n                <Menu.Item\n                  key=\"pin\"\n                  icon={\n                    chat.is_pinned ? (\n                      <PinOffIcon className=\"w-4 h-4\" />\n                    ) : (\n                      <PinIcon className=\"w-4 h-4\" />\n                    )\n                  }\n                  onClick={() =>\n                    pinChatHistory({\n                      id: chat.id,\n                      is_pinned: !chat.is_pinned\n                    })\n                  }\n                  disabled={pinLoading}>\n                  {chat.is_pinned ? t(\"common:unpin\") : t(\"common:pin\")}\n                </Menu.Item>\n                <Menu.Item\n                  key=\"edit\"\n                  icon={<PencilIcon className=\"w-4 h-4\" />}\n                  onClick={(e) => {\n                    e.domEvent.stopPropagation()\n                    handleEditStart(chat)\n                  }}>\n                  {t(\"common:edit\")}\n                </Menu.Item>\n                <Menu.Item\n                  key=\"delete\"\n                  icon={<Trash2 className=\"w-4 h-4\" />}\n                  danger\n                  onClick={() => {\n                    if (!confirm(t(\"deleteHistoryConfirmation\"))) return\n                    deleteHistory(chat.id)\n                  }}>\n                  {t(\"common:delete\")}\n                </Menu.Item>\n              </Menu>\n            }\n            trigger={[\"click\"]}\n            placement=\"bottomRight\">\n            <button className=\"text-gray-500 dark:text-gray-400 opacity-80 hover:opacity-100\">\n              <MoreVertical className=\"w-4 h-4\" />\n            </button>\n          </Dropdown>\n        </div>\n      </div>\n    )\n  }\n\n  const handleLoadMore = () => {\n    if (hasNextPage && !isFetchingNextPage) {\n      fetchNextPage()\n    }\n  }\n\n  return (\n    <div\n      className={`overflow-y-auto z-99 ${temporaryChat ? \"pointer-events-none opacity-50\" : \"\"}`}>\n      <div className=\"sticky top-0 z-10 my-3\">\n        <div className=\"relative\">\n          <Input\n            placeholder={t(\"common:search\")}\n            value={searchQuery}\n            onChange={handleSearchChange}\n            prefix={<SearchIcon className=\"w-4 h-4 text-gray-400\" />}\n            suffix={\n              searchQuery ? (\n                <button\n                  onClick={clearSearch}\n                  className=\"text-gray-400 hover:text-gray-600 dark:hover:text-gray-200\">\n                  ✕\n                </button>\n              ) : null\n            }\n            className=\"w-full rounded-md border border-gray-300 dark:border-gray-700 dark:bg-[#232222]\"\n          />\n        </div>\n      </div>\n\n      {status === \"success\" &&\n        orderedChatHistories.length === 0 &&\n        !dexiePrivateWindowError && (\n          <div className=\"flex justify-center items-center mt-20 overflow-hidden\">\n            <Empty description={t(\"common:noHistory\")} />\n          </div>\n        )}\n\n      {dexiePrivateWindowError && (\n        <div className=\"flex justify-center items-center mt-20 overflow-hidden\">\n          <Empty\n            description={t(\"common:privateWindow\", {\n              defaultValue:\n                \"Don't worry, this is a known issue on Firefox: IndexedDB does not work in private mode. Please open the extension in a normal window to view your chat history.\"\n            })}\n          />\n        </div>\n      )}\n\n      {(status === \"pending\" || isLoading) && (\n        <div className=\"flex justify-center items-center mt-5\">\n          <Skeleton active paragraph={{ rows: 8 }} />\n        </div>\n      )}\n\n      {status === \"error\" && (\n        <div className=\"flex justify-center items-center\">\n          <span className=\"text-red-500\">Error loading history</span>\n        </div>\n      )}\n\n      {status === \"success\" && orderedChatHistories.length > 0 && (\n        <div className=\"flex flex-col gap-2\">\n          {/* Pinned Chats Section - Always show at top when not searching */}\n          {!isSearchActive && pinnedChats.length > 0 && (\n            <div className=\"mb-2\">\n              <div className=\"flex items-center justify-between mb-2\">\n                <h3 className=\"px-2 text-sm font-medium text-gray-500\">\n                  {t(\"common:date:pinned\")}\n                </h3>\n              </div>\n              <div className=\"flex flex-col gap-2\">\n                {pinnedChats.map((chat) => renderChatRow(chat))}\n              </div>\n            </div>\n          )}\n\n          {/* Project Folders Section */}\n          {!isSearchActive && (\n            <>\n              <div className=\"flex items-center justify-between\">\n                <h3 className=\"px-2 text-sm font-medium text-gray-500\">\n                  {t(\"common:projects\", { defaultValue: \"Projects\" })}\n                </h3>\n              </div>\n\n              {isCreatingProject ? (\n                <div\n                  ref={projectCreationCardRef}\n                  className=\"rounded-md p-2 mb-2 bg-gray-100 dark:bg-[#2a2a2a] border border-gray-400 dark:border-[#383838]\">\n                  <div className=\"flex flex-col gap-2\">\n                    <input\n                      value={newProjectTitle}\n                      onChange={(e) => setNewProjectTitle(e.target.value)}\n                      onKeyDown={(e) => {\n                        if (e.key === \"Enter\") {\n                          handleCreateProject()\n                        } else if (e.key === \"Escape\") {\n                          setIsCreatingProject(false)\n                          setNewProjectTitle(\"\")\n                        }\n                      }}\n                      placeholder={t(\"common:projectName\", {\n                        defaultValue: \"Project name\"\n                      })}\n                      autoFocus\n                      className=\"px-2 py-1 text-sm bg-white dark:bg-[#1a1a1a] text-gray-800 dark:text-gray-100 border border-gray-300 dark:border-gray-600 rounded outline-none focus:ring-2 focus:ring-gray-400 dark:focus:ring-gray-500\"\n                    />\n                    <div className=\"flex items-center justify-end gap-2\">\n                      <SaveButton\n                        onClick={handleCreateProject}\n                        disabled={creatingProject || !newProjectTitle.trim()}\n                        text=\"create\"\n                        textOnSave=\"created\"\n                        className=\"mt-0\"\n                      />\n                    </div>\n                  </div>\n                </div>\n              ) : (\n                <button\n                  onClick={() => setIsCreatingProject(true)}\n                  className=\"flex items-center gap-2 px-2 py-2 text-sm text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-[#2a2a2a] rounded-md transition-colors w-full border border-transparent hover:border-gray-300 dark:hover:border-[#404040]\">\n                  <FolderPlus className=\"w-4 h-4\" />\n                  {t(\"common:newProject\", { defaultValue: \"New project\" })}\n                </button>\n              )}\n              {/* Project Folders */}\n              {projectFolders.map((folder) => {\n                const folderChats = projectChatsMap[folder.id] || []\n                const isCollapsed = !collapsedFolders.has(folder.id)\n                const groupedFolderChats = groupChatsByDate(folderChats)\n                return (\n                  <div\n                    key={folder.id}\n                    onDragOver={(e) => {\n                      e.preventDefault()\n                      setDragOverProjectId(folder.id)\n                    }}\n                    onDragLeave={() => setDragOverProjectId(null)}\n                    onDrop={() => handleDropOnFolder(folder.id)}\n                    className={`rounded-md p-2 mb-2 border transition-colors ${\n                      dragOverProjectId === folder.id\n                        ? \"bg-blue-100 dark:bg-blue-900/30 border-blue-400 dark:border-blue-600\"\n                        : \"bg-gray-100 dark:bg-[#2a2a2a] border-gray-400 dark:border-[#383838]\"\n                    }`}>\n                    <div className=\"flex items-center justify-between\">\n                      {editingProjectId === folder.id ? (\n                        <input\n                          value={editProjectTitle}\n                          onChange={(e) => setEditProjectTitle(e.target.value)}\n                          onKeyDown={(e) => {\n                            if (e.key === \"Enter\") {\n                              saveProjectEdit(folder.id, folder.title)\n                            } else if (e.key === \"Escape\") {\n                              cancelProjectEdit()\n                            }\n                            e.stopPropagation()\n                          }}\n                          onBlur={() =>\n                            saveProjectEdit(folder.id, folder.title)\n                          }\n                          autoFocus\n                          className=\"flex-1 px-2 py-1 text-sm bg-white dark:bg-[#1a1a1a] text-gray-800 dark:text-gray-100 border border-gray-300 dark:border-gray-600 rounded outline-none focus:ring-2 focus:ring-gray-400 dark:focus:ring-gray-500\"\n                        />\n                      ) : (\n                        <button\n                          onClick={() => toggleFolderCollapse(folder.id)}\n                          className=\"flex items-center gap-2 flex-1 text-left hover:opacity-70 transition-opacity\">\n                          <ChevronDown\n                            className={`w-4 h-4 text-gray-500 dark:text-gray-400 transition-transform ${\n                              isCollapsed ? \"-rotate-90\" : \"\"\n                            }`}\n                          />\n                          <Folder className=\"w-4 h-4 text-gray-600 dark:text-gray-400\" />\n                          <span className=\"text-sm font-medium text-gray-700 dark:text-gray-300\">\n                            {folder.title}\n                          </span>\n                          <span className=\"text-xs text-gray-400\">\n                            ({folderChats.length})\n                          </span>\n                        </button>\n                      )}\n                      <Dropdown\n                        overlay={\n                          <Menu>\n                            <Menu.Item\n                              key=\"rename\"\n                              icon={<PencilIcon className=\"w-4 h-4\" />}\n                              onClick={() => startProjectEdit(folder)}>\n                              {t(\"common:rename\")}\n                            </Menu.Item>\n                            <Menu.Item\n                              key=\"delete\"\n                              icon={<Trash2 className=\"w-4 h-4\" />}\n                              danger\n                              onClick={() => handleDeleteProject(folder.id)}>\n                              {t(\"common:delete\")}\n                            </Menu.Item>\n                          </Menu>\n                        }\n                        trigger={[\"click\"]}\n                        placement=\"bottomRight\">\n                        <button className=\"text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200\">\n                          <MoreVertical className=\"w-4 h-4\" />\n                        </button>\n                      </Dropdown>\n                    </div>\n                    {!isCollapsed && (\n                      <div className=\"flex flex-col gap-2 mt-2\">\n                        {groupedFolderChats.map((group, groupIndex) => (\n                          <div key={groupIndex}>\n                            <h4 className=\"px-2 text-xs font-medium text-gray-400 mb-1\">\n                              {t(`common:date:${group.label}`)}\n                            </h4>\n                            <div className=\"flex flex-col gap-2\">\n                              {group.items.map((chat) => renderChatRow(chat))}\n                            </div>\n                          </div>\n                        ))}\n                      </div>\n                    )}\n                  </div>\n                )\n              })}\n\n              {/* Unassigned Chats Section */}\n              {unassignedChats.length > 0 && (\n                <div\n                  onDragOver={(e) => {\n                    e.preventDefault()\n                    setDragOverProjectId(\"unassigned\")\n                  }}\n                  onDragLeave={() => setDragOverProjectId(null)}\n                  onDrop={() => handleDropOnFolder(undefined)}\n                  className={`rounded-md ${\n                    dragOverProjectId === \"unassigned\"\n                      ? \"bg-blue-50 dark:bg-blue-900/20\"\n                      : \"\"\n                  }`}>\n                  <div className=\"flex items-center justify-between mb-2\">\n                    <h3 className=\"px-2 text-sm font-medium text-gray-500\">\n                      {t(\"common:yourChats\", { defaultValue: \"Your chats\" })}\n                    </h3>\n                  </div>\n                  <div className=\"flex flex-col gap-2\">\n                    {groupChatsByDate(unassignedChats).map(\n                      (group, groupIndex) => (\n                        <div key={groupIndex}>\n                          <h4 className=\"px-2 text-xs font-medium text-gray-400 mb-1\">\n                            {t(`common:date:${group.label}`)}\n                          </h4>\n                          <div className=\"flex flex-col gap-2\">\n                            {group.items.map((chat) => renderChatRow(chat))}\n                          </div>\n                        </div>\n                      )\n                    )}\n                  </div>\n                </div>\n              )}\n            </>\n          )}\n\n          {/* Search Results or Date-grouped History */}\n          {isSearchActive &&\n            orderedChatHistories.map((group, groupIndex) => (\n              <div key={groupIndex}>\n                <div className=\"flex items-center justify-between mt-2\">\n                  <h3 className=\"px-2 text-sm font-medium text-gray-500\">\n                    {group.label === \"searchResults\"\n                      ? t(\"common:searchResults\")\n                      : t(`common:date:${group.label}`)}\n                  </h3>\n                  {group.label !== \"searchResults\" && (\n                    <Tooltip\n                      title={t(`common:range:tooltip:${group.label}`)}\n                      placement=\"top\">\n                      <button\n                        onClick={() =>\n                          handleDeleteHistoriesByRange(group.label)\n                        }>\n                        {deleteRangeLoading && deleteGroup === group.label ? (\n                          <Loader2 className=\"w-4 h-4 text-gray-500 hover:text-gray-700 dark:hover:text-gray-200 animate-spin\" />\n                        ) : (\n                          <Trash2Icon className=\"w-4 h-4 text-gray-500 hover:text-gray-700 dark:hover:text-gray-200\" />\n                        )}\n                      </button>\n                    </Tooltip>\n                  )}\n                </div>\n                <div className=\"flex flex-col gap-2 mt-2\">\n                  {group.items.map((chat) => renderChatRow(chat))}\n                </div>\n              </div>\n            ))}\n\n          {/* Load More Button */}\n          {hasNextPage && (\n            <div className=\"flex justify-center mt-4 mb-2\">\n              <Button\n                type=\"default\"\n                onClick={handleLoadMore}\n                loading={isFetchingNextPage}\n                icon={\n                  !isFetchingNextPage ? (\n                    <ChevronDown className=\"w-4 h-4\" />\n                  ) : undefined\n                }\n                className=\"flex items-center gap-2 text-sm\">\n                {isFetchingNextPage\n                  ? t(\"common:loading\")\n                  : t(\"common:loadMore\")}\n              </Button>\n            </div>\n          )}\n        </div>\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Select/LoadingIndicator.tsx",
    "content": "import React from \"react\"\n\nexport const LoadingIndicator: React.FC<{ className?: string }> = ({\n  className = \"\"\n}) => (\n  <div className={`animate-spin ${className}`}>\n    <svg\n      className=\"w-4 h-4\"\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\">\n      <circle\n        className=\"opacity-25\"\n        cx=\"12\"\n        cy=\"12\"\n        r=\"10\"\n        stroke=\"currentColor\"\n        strokeWidth=\"4\"\n      />\n      <path\n        className=\"opacity-75\"\n        fill=\"currentColor\"\n        d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"\n      />\n    </svg>\n  </div>\n)\n"
  },
  {
    "path": "src/components/Select/index.tsx",
    "content": "import React, { useState, useRef, useEffect, useMemo } from \"react\"\nimport { Search, RotateCw, ChevronDown } from \"lucide-react\"\nimport { LoadingIndicator } from \"./LoadingIndicator\"\nimport { Empty } from \"antd\"\n\nexport interface SelectOption {\n  label: string | JSX.Element\n  value: string\n}\n\ninterface SelectProps {\n  options: SelectOption[]\n  value?: string\n  onChange: (option: SelectOption) => void\n  placeholder?: string\n  onRefresh?: () => void\n  className?: string\n  dropdownClassName?: string\n  optionClassName?: string\n  searchClassName?: string\n  disabled?: boolean\n  isLoading?: boolean\n  loadingText?: string\n  filterOption?: (input: string, option: SelectOption) => boolean\n}\n\nexport const PageAssistSelect: React.FC<SelectProps> = ({\n  options = [],\n  value,\n  onChange,\n  placeholder = \"Select an option\",\n  onRefresh,\n  className = \"\",\n  dropdownClassName = \"\",\n  optionClassName = \"\",\n  searchClassName = \"\",\n  disabled = false,\n  isLoading = false,\n  loadingText = \"Loading...\",\n  filterOption\n}) => {\n  const [isOpen, setIsOpen] = useState(false)\n  const [searchTerm, setSearchTerm] = useState(\"\")\n  const [filteredOptions, setFilteredOptions] = useState<SelectOption[]>([])\n  const containerRef = useRef<HTMLDivElement>(null)\n  const optionsContainerRef = useRef<HTMLDivElement>(null)\n  const [activeIndex, setActiveIndex] = useState(-1)\n\n  useEffect(() => {\n    const handleClickOutside = (event: MouseEvent) => {\n      if (\n        containerRef.current &&\n        !containerRef.current.contains(event.target as Node)\n      ) {\n        setIsOpen(false)\n      }\n    }\n\n    document.addEventListener(\"mousedown\", handleClickOutside)\n    return () => document.removeEventListener(\"mousedown\", handleClickOutside)\n  }, [])\n\n  useEffect(() => {\n    try {\n      if (isOpen && optionsContainerRef.current && value) {\n        const selectedOptionElement = optionsContainerRef.current.querySelector(\n          `[data-value=\"${value}\"]`\n        )\n        if (selectedOptionElement) {\n          selectedOptionElement.scrollIntoView({ block: \"nearest\" })\n        }\n      }\n    } catch (error) {\n      console.error(\"Error scrolling to selected option:\", error)\n    }\n    \n  }, [isOpen, value])\n\n  useEffect(() => {\n    if (!options) return\n\n    const filtered = options.filter((option) => {\n      if (!searchTerm) return true\n\n      if (filterOption) {\n        return filterOption(searchTerm, option)\n      }\n\n      if (typeof option.label === \"string\") {\n        return option.label.toLowerCase().includes(searchTerm.toLowerCase())\n      }\n\n      if (React.isValidElement(option.label)) {\n        const textContent = extractTextFromJSX(option.label)\n        return textContent.toLowerCase().includes(searchTerm.toLowerCase())\n      }\n\n      return false\n    })\n    setFilteredOptions(filtered)\n    setActiveIndex(-1)\n  }, [searchTerm, options, filterOption])\n\n  const extractTextFromJSX = (element: React.ReactElement): string => {\n    if (typeof element.props.children === \"string\") {\n      return element.props.children\n    }\n\n    if (Array.isArray(element.props.children)) {\n      return element.props.children\n        .map((child) => {\n          if (typeof child === \"string\") return child\n          if (React.isValidElement(child)) return extractTextFromJSX(child)\n          return \"\"\n        })\n        .join(\" \")\n    }\n\n    if (React.isValidElement(element.props.children)) {\n      return extractTextFromJSX(element.props.children)\n    }\n\n    return \"\"\n  }\n\n  const handleRefresh = (e: React.MouseEvent) => {\n    e.stopPropagation()\n    onRefresh?.()\n  }\n\n  const handleKeyDown = (e: React.KeyboardEvent) => {\n    if (disabled || isLoading) return\n\n    switch (e.key) {\n      case \"Enter\":\n        if (isOpen && activeIndex >= 0) {\n          e.preventDefault()\n          const selectedOption = filteredOptions[activeIndex]\n          if (selectedOption) {\n            onChange(selectedOption)\n            setIsOpen(false)\n            setSearchTerm(\"\")\n          }\n        } else {\n          setIsOpen(!isOpen)\n        }\n        break\n      case \" \":\n        if (!isOpen) {\n          e.preventDefault()\n          setIsOpen(true)\n        }\n        break\n      case \"Escape\":\n        setIsOpen(false)\n        break\n      case \"ArrowDown\":\n        e.preventDefault()\n        if (!isOpen) {\n          setIsOpen(true)\n        } else {\n          setActiveIndex((prev) =>\n            prev < filteredOptions.length - 1 ? prev + 1 : prev\n          )\n        }\n        break\n      case \"ArrowUp\":\n        e.preventDefault()\n        setActiveIndex((prev) => (prev > 0 ? prev - 1 : prev))\n        break\n    }\n  }\n\n  const defaultSelectClass = `\n    flex items-center justify-between p-2.5 rounded-lg border\n    ${disabled || isLoading ? \"cursor-not-allowed opacity-50\" : \"cursor-pointer\"}\n    ${isOpen ? \"ring-2 ring-blue-500\" : \"\"}\n    bg-transparent border-gray-300 text-gray-900\n    transition-all duration-200\n    dark:text-white\n    dark:border-[#353534]\n    bg-white dark:bg-[#1a1a1a]\n  `\n\n  const defaultDropdownClass = `\n    absolute z-50 w-full mt-1 bg-white dark:bg-[#1e1e1f] dark:text-white rounded-lg shadow-lg \n    border border-gray-200 dark:border-[#353534]\n  `\n\n  const defaultSearchClass = `\n    w-full pl-8 pr-8 py-1.5 rounded-md\n    bg-gray-50 border border-gray-200\n    focus:outline-none focus:ring-2 focus:ring-gray-100\n    text-gray-900\n    dark:bg-[#1e1e1f] dark:text-white\n    dark:border-[#353534]\n    dark:focus:ring-gray-700\n    dark:focus:border-gray-700\n    dark:placeholder-gray-400\n    dark:bg-opacity-90\n    dark:hover:bg-opacity-100\n    dark:focus:bg-opacity-100\n    dark:hover:border-gray-700\n    dark:hover:bg-[#2a2a2b]\n    dark:focus:bg-[#2a2a2b]\n  `\n\n  const defaultOptionClass = `\n    p-2 cursor-pointer transition-colors duration-150\n  `\n\n  const selectedOption = useMemo(() => {\n    if (!value || !options) return null\n    return options?.find((opt) => opt.value === value)\n  }, [value, options])\n\n  if (!options) {\n    return (\n      <div className={`relative w-full ${className}`}>\n        <div className={`${defaultSelectClass} ${className}`}>\n          <LoadingIndicator />\n          <span>{loadingText}</span>\n        </div>\n      </div>\n    )\n  }\n\n  return (\n    <div ref={containerRef} className={`relative w-full ${className}`}>\n      <div\n        role=\"combobox\"\n        aria-expanded={isOpen}\n        aria-haspopup=\"listbox\"\n        aria-controls=\"select-dropdown\"\n        aria-label={placeholder}\n        tabIndex={disabled ? -1 : 0}\n        onClick={() => !disabled && !isLoading && setIsOpen(!isOpen)}\n        onKeyDown={handleKeyDown}\n        className={`${defaultSelectClass} ${className}`}>\n        <span className=\"!truncate flex items-center gap-2 select-none\">\n          {isLoading && <LoadingIndicator  />}\n          {isLoading ? (\n            loadingText\n          ) : selectedOption ? (\n            selectedOption.label\n          ) : (\n            <span className=\"dark:text-gray-500 font-semibold text-[14px]\">\n              {placeholder}\n            </span>\n          )}\n        </span>\n        <ChevronDown\n          aria-hidden=\"true\"\n          className={`w-4 h-4 transition-transform duration-200 ${\n            isOpen ? \"transform rotate-180\" : \"\"\n          }`}\n        />\n      </div>\n\n      {isOpen && (\n        <div\n          id=\"select-dropdown\"\n          role=\"listbox\"\n          className={`${defaultDropdownClass} ${dropdownClassName}`}>\n          <div className=\"p-2 border-b border-gray-200 dark:border-[#353534]\">\n            <div className=\"relative\">\n              <input\n                type=\"text\"\n                value={searchTerm}\n                onChange={(e) => setSearchTerm(e.target.value)}\n                placeholder=\"Search...\"\n                className={`${defaultSearchClass} ${searchClassName}`}\n                disabled={isLoading}\n                aria-label=\"Search options\"\n              />\n              <Search\n                aria-hidden=\"true\"\n                className=\"absolute left-2 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400\"\n              />\n              {onRefresh && (\n                <button\n                  onClick={handleRefresh}\n                  disabled={isLoading}\n                  aria-label=\"Refresh options\"\n                  className={`absolute right-2 top-1/2 transform -translate-y-1/2\n                    hover:text-blue-500 dark:hover:text-blue-400 transition-colors duration-200\n                    ${isLoading ? \"cursor-not-allowed opacity-50\" : \"\"}`}>\n                  <RotateCw\n                    aria-hidden=\"true\"\n                    className={`w-4 h-4 dark:text-gray-400 ${isLoading ? \"animate-spin\" : \"\"}`}\n                  />\n                </button>\n              )}{\" \"}\n            </div>\n          </div>\n\n          <div\n            ref={optionsContainerRef}\n            className=\"max-h-60 overflow-y-auto custom-scrollbar\">\n            {isLoading ? (\n              <div className=\"p-4 text-center text-gray-500 flex items-center justify-center gap-2\">\n                <LoadingIndicator />\n                <span>{loadingText}</span>\n              </div>\n            ) : filteredOptions.length === 0 ? (\n              <div className=\"p-6\">\n                <Empty />\n              </div>\n            ) : (\n              filteredOptions.map((option, index) => (\n                <div\n                  key={option.value}\n                  role=\"option\"\n                  aria-selected={value === option.value}\n                  data-value={option.value}\n                  onClick={() => {\n                    onChange(option)\n                    setIsOpen(false)\n                    setSearchTerm(\"\")\n                  }}\n                  className={`\n                    ${defaultOptionClass}\n                    ${value === option.value ? \"bg-blue-50 dark:bg-[#262627]\" : \"hover:bg-gray-100 dark:hover:bg-[#272728]\"}\n                    ${activeIndex === index ? \"bg-gray-100 dark:bg-[#272728]\" : \"\"}\n                    ${optionClassName}`}>\n                  {option.label}\n                </div>\n              ))\n            )}\n          </div>\n        </div>\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/ShortcutConfig.tsx",
    "content": ""
  },
  {
    "path": "src/components/Sidepanel/Chat/body.tsx",
    "content": "import React from \"react\"\nimport { PlaygroundMessage } from \"~/components/Common/Playground/Message\"\nimport { useMessage } from \"~/hooks/useMessage\"\nimport { EmptySidePanel } from \"../Chat/empty\"\nimport { useWebUI } from \"@/store/webui\"\nimport { MessageSourcePopup } from \"@/components/Common/Playground/MessageSourcePopup\"\nimport { usePlaygroundMessageGroups } from \"@/components/Common/Playground/message-groups\"\n\nconst SidePanelBodyComponent = () => {\n  const {\n    messages,\n    streaming,\n    regenerateLastMessage,\n    editMessage,\n    isSearchingInternet, \n    createChatBranch,\n    temporaryChat,\n    actionInfo\n  } = useMessage()\n  const [isSourceOpen, setIsSourceOpen] = React.useState(false)\n  const [source, setSource] = React.useState<any>(null)\n  const { ttsEnabled } = useWebUI()\n  const messageGroups = usePlaygroundMessageGroups(messages)\n  const lastGroupIndex = messageGroups.length - 1\n\n  const handleEditMessage = React.useCallback(\n    (\n      actionIndex: number,\n      isHuman: boolean,\n      value: string,\n      _isSend: boolean\n    ) => {\n      editMessage(actionIndex, value, isHuman)\n    },\n    [editMessage]\n  )\n\n  const handleSourceClick = React.useCallback((data: any) => {\n    setSource(data)\n    setIsSourceOpen(true)\n  }, [])\n\n  return (\n    <>\n      <div className=\"relative flex w-full flex-col items-center pt-16 pb-4\">\n        {messages.length === 0 && <EmptySidePanel />}\n        {messageGroups.map((message, index) => (\n          <PlaygroundMessage\n            key={message.renderKey}\n            isBot={message.isBot}\n            message={message.message}\n            name={message.name}\n            images={message.images || []}\n            isLastMessage={index === lastGroupIndex}\n            actionIndex={message.actionIndex}\n            onRengerate={\n              index === lastGroupIndex ? regenerateLastMessage : undefined\n            }\n            message_type={message.messageType}\n            isProcessing={streaming && index === lastGroupIndex}\n            isSearchingInternet={\n              index === lastGroupIndex ? isSearchingInternet : false\n            }\n            sources={message.sources}\n            onEditFormSubmit={handleEditMessage}\n            onNewBranch={createChatBranch}\n            onSourceClick={handleSourceClick}\n            isTTSEnabled={ttsEnabled}\n            generationInfo={message?.generationInfo}\n            isStreaming={streaming && index === lastGroupIndex}\n            reasoningTimeTaken={message?.reasoning_time_taken}\n            modelImage={message?.modelImage}\n            modelName={message?.modelName}\n            temporaryChat={temporaryChat}\n            actionInfo={index === lastGroupIndex ? actionInfo : null}\n            messageKind={message?.messageKind}\n            toolCalls={message?.toolCalls}\n            toolCallId={message?.toolCallId}\n            toolName={message?.toolName}\n            toolServerName={message?.toolServerName}\n            toolError={message?.toolError}\n            segments={message.segments}\n          />\n        ))}\n      </div>\n      <div className=\"w-full pb-[157px]\"></div>\n\n      <MessageSourcePopup\n        open={isSourceOpen}\n        setOpen={setIsSourceOpen}\n        source={source}\n      />\n    </>\n  )\n}\n\nexport const SidePanelBody = React.memo(SidePanelBodyComponent)\n"
  },
  {
    "path": "src/components/Sidepanel/Chat/empty.tsx",
    "content": "import { ProviderIcons } from \"@/components/Common/ProviderIcon\"\nimport { cleanUrl } from \"@/libs/clean-url\"\nimport { useStorage } from \"@plasmohq/storage/hook\"\nimport { useQuery, useQueryClient } from \"@tanstack/react-query\"\nimport { Avatar, Select } from \"antd\"\nimport { Loader2, RotateCcw } from \"lucide-react\"\nimport { useEffect, useState } from \"react\"\nimport { Trans, useTranslation } from \"react-i18next\"\nimport { useMessage } from \"~/hooks/useMessage\"\nimport {\n  getOllamaURL,\n  isOllamaRunning,\n  setOllamaURL as saveOllamaURL,\n  fetchChatModels\n} from \"~/services/ollama\"\n\nexport const EmptySidePanel = () => {\n  const [ollamaURL, setOllamaURL] = useState<string>(\"\")\n  const { t } = useTranslation([\"playground\", \"common\"])\n  const queryClient = useQueryClient()\n  const [checkOllamaStatus] = useStorage(\"checkOllamaStatus\", true)\n\n  const {\n    data: ollamaInfo,\n    status: ollamaStatus,\n    refetch,\n    isRefetching\n  } = useQuery({\n    queryKey: [\"ollamaStatus\", checkOllamaStatus],\n    queryFn: async () => {\n      const ollamaURL = await getOllamaURL()\n      const isOk = await isOllamaRunning()\n      const models = await fetchChatModels({ returnEmpty: false })\n      queryClient.invalidateQueries({\n        queryKey: [\"getAllModelsForSelect\"]\n      })\n      return {\n        isOk: checkOllamaStatus ? isOk : true,\n        models,\n        ollamaURL\n      }\n    }\n  })\n\n  useEffect(() => {\n    if (ollamaInfo?.ollamaURL) {\n      setOllamaURL(ollamaInfo.ollamaURL)\n    }\n  }, [ollamaInfo])\n\n  const { setSelectedModel, selectedModel, chatMode, setChatMode } =\n    useMessage()\n  const renderSection = () => {\n    return (\n      <div className=\"mt-4\">\n        <Select\n          onChange={(e) => {\n            setSelectedModel(e)\n            localStorage.setItem(\"selectedModel\", e)\n          }}\n          value={selectedModel}\n          size=\"large\"\n          filterOption={(input, option) => {\n            //@ts-ignore\n            return (\n              option?.label?.props[\"data-title\"]\n                ?.toLowerCase()\n                ?.indexOf(input.toLowerCase()) >= 0\n            )\n          }}\n          showSearch\n          placeholder={t(\"common:selectAModel\")}\n          style={{ width: \"100%\" }}\n          className=\"mt-4 min-w-60 max-w-64\"\n          options={ollamaInfo?.models?.map((model) => ({\n            label: (\n              <span\n                key={model.model}\n                data-title={model.name}\n                className=\"flex flex-row gap-3 items-center \">\n                {model?.avatar ? (\n                  <Avatar src={model.avatar} alt={model.name} size=\"small\" />\n                ) : (\n                  <ProviderIcons\n                    provider={model?.provider}\n                    className=\"w-5 h-5\"\n                  />\n                )}\n                <span className=\"line-clamp-2\">\n                  {model?.nickname || model.model}\n                </span>\n              </span>\n            ),\n            value: model.model\n          }))}\n        />\n\n        <div className=\"mt-4\">\n          <div className=\"inline-flex items-center\">\n            <label\n              className=\"relative flex items-center p-3 rounded-full cursor-pointer\"\n              htmlFor=\"check\">\n              <input\n                type=\"checkbox\"\n                checked={chatMode === \"rag\"}\n                onChange={(e) => {\n                  setChatMode(e.target.checked ? \"rag\" : \"normal\")\n                }}\n                className=\"before:content[''] peer relative h-5 w-5 cursor-pointer appearance-none rounded-md border border-blue-gray-200 transition-all before:absolute before:top-2/4 before:left-2/4 before:block before:h-12 before:w-12 before:-translate-y-2/4 before:-translate-x-2/4 before:rounded-full before:bg-blue-gray-500 before:opacity-0 before:transition-opacity\"\n                id=\"check\"\n              />\n              <span className=\"absolute text-white transition-opacity opacity-0 pointer-events-none top-2/4 left-2/4 -translate-y-2/4 -translate-x-2/4 peer-checked:opacity-100 \">\n                <svg\n                  xmlns=\"http://www.w3.org/2000/svg\"\n                  className=\"h-3.5 w-3.5\"\n                  viewBox=\"0 0 20 20\"\n                  fill=\"currentColor\"\n                  stroke=\"currentColor\"\n                  strokeWidth=\"1\">\n                  <path\n                    fillRule=\"evenodd\"\n                    d=\"M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z\"\n                    clipRule=\"evenodd\"></path>\n                </svg>\n              </span>\n            </label>\n            <label\n              className=\"mt-px font-light  cursor-pointer select-none text-gray-900 dark:text-gray-400\"\n              htmlFor=\"check\">\n              {t(\"common:chatWithCurrentPage\")}\n            </label>\n          </div>\n        </div>\n      </div>\n    )\n  }\n\n  if (!checkOllamaStatus) {\n    return (\n      <div className=\"mx-auto sm:max-w-md px-4 mt-10\">\n        <div className=\"rounded-lg justify-center items-center flex flex-col border dark:border-gray-700 p-8 bg-white dark:bg-[#262626] shadow-sm\">\n          <div className=\"inline-flex items-center space-x-2\">\n            <p className=\"dark:text-gray-400 text-gray-900\">\n              <span>👋</span>\n              {t(\"welcome\")}\n            </p>\n          </div>\n          {ollamaStatus === \"pending\" && (\n            <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n          )}\n          {ollamaStatus === \"success\" && ollamaInfo.isOk && renderSection()}\n        </div>\n      </div>\n    )\n  }\n\n  return (\n    <div className=\"mx-auto sm:max-w-lg px-4 mt-10\">\n      <div className=\"rounded-lg  justify-center items-center flex flex-col border border-gray-300 dark:border-gray-700 p-8 bg-white dark:bg-[#262626] shadow-sm\">\n        {(ollamaStatus === \"pending\" || isRefetching) && (\n          <div className=\"inline-flex items-center space-x-2\">\n            <div className=\"w-3 h-3 bg-blue-500 rounded-full animate-bounce\"></div>\n            <p className=\"dark:text-gray-400 text-gray-900\">\n              {t(\"ollamaState.searching\")}\n            </p>\n          </div>\n        )}\n        {!isRefetching && ollamaStatus === \"success\" ? (\n          ollamaInfo.isOk ? (\n            <div className=\"inline-flex  items-center space-x-2\">\n              <div className=\"w-3 h-3 bg-green-500 rounded-full\"></div>\n              <p className=\"dark:text-gray-400 text-gray-900\">\n                {t(\"ollamaState.running\")}\n              </p>\n            </div>\n          ) : (\n            <div className=\"flex flex-col space-y-2 justify-center items-center\">\n              <div className=\"inline-flex  space-x-2\">\n                <div className=\"w-3 h-3 bg-red-500 rounded-full\"></div>\n                <p className=\"dark:text-gray-400 text-gray-900\">\n                  {t(\"ollamaState.notRunning\")}\n                </p>\n              </div>\n\n              <input\n                className=\"bg-gray-100 dark:bg-black dark:text-gray-100 rounded-md px-4 py-2 mt-2 w-full\"\n                type=\"url\"\n                value={ollamaURL}\n                onChange={(e) => setOllamaURL(e.target.value)}\n              />\n\n              <button\n                onClick={() => {\n                  saveOllamaURL(ollamaURL)\n                  refetch()\n                }}\n                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 \">\n                <RotateCcw className=\"h-4 w-4 mr-3\" />\n                {t(\"common:retry\")}\n              </button>\n              {ollamaURL &&\n                cleanUrl(ollamaURL) !== \"http://127.0.0.1:11434\" && (\n                  <p className=\"text-xs text-gray-700 dark:text-gray-400 mb-4 text-center\">\n                    <Trans\n                      i18nKey=\"playground:ollamaState.connectionError\"\n                      components={{\n                        anchor: (\n                          <a\n                            href=\"https://github.com/n4ze3m/page-assist/blob/main/docs/connection-issue.md\"\n                            target=\"__blank\"\n                            className=\"text-blue-600 dark:text-blue-400\"></a>\n                        )\n                      }}\n                    />\n                  </p>\n                )}\n            </div>\n          )\n        ) : null}\n\n        {ollamaStatus === \"success\" && ollamaInfo.isOk && renderSection()}\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Sidepanel/Chat/form.tsx",
    "content": "import { useForm } from \"@mantine/form\"\nimport { useMutation } from \"@tanstack/react-query\"\nimport React from \"react\"\nimport useDynamicTextareaSize from \"~/hooks/useDynamicTextareaSize\"\nimport { useMessage } from \"~/hooks/useMessage\"\nimport { toBase64 } from \"~/libs/to-base64\"\nimport { Checkbox, Dropdown, Image, Switch, Tooltip, Popover, Radio } from \"antd\"\nimport { useWebUI } from \"~/store/webui\"\nimport { defaultEmbeddingModelForRag } from \"~/services/ollama\"\nimport {\n  ImageIcon,\n  MicIcon,\n  StopCircleIcon,\n  X,\n  EyeIcon,\n  EyeOffIcon,\n  Brain,\n  BrainCircuit,\n  PlusIcon,\n  MinusIcon,\n  PaperclipIcon,\n  ArrowUp\n} from \"lucide-react\"\nimport { useTranslation } from \"react-i18next\"\nimport { ModelSelect } from \"@/components/Common/ModelSelect\"\nimport { useSpeechRecognition } from \"@/hooks/useSpeechRecognition\"\nimport { PiGlobeX, PiGlobe } from \"react-icons/pi\"\nimport { handleChatInputKeyDown } from \"@/utils/key-down\"\nimport { getIsSimpleInternetSearch } from \"@/services/search\"\nimport { useStorage } from \"@plasmohq/storage/hook\"\nimport { useFocusShortcuts } from \"@/hooks/keyboard\"\nimport { isThinkingCapableModel, isGptOssModel } from \"~/libs/model-utils\"\nimport { useStoreChatModelSettings } from \"~/store/model\"\nimport { getVariable } from \"@/utils/select-variable\"\nimport { useMessageQueue } from \"@/hooks/useMessageQueue\"\nimport { QueuedMessagesList } from \"@/components/Common/QueuedMessagesList\"\nimport { McpServerToggle } from \"@/components/Common/McpServerToggle\"\n\ntype Props = {\n  dropedFile: File | undefined\n}\n\nexport const SidepanelForm = ({ dropedFile }: Props) => {\n  const textareaRef = React.useRef<HTMLTextAreaElement>(null)\n  const inputRef = React.useRef<HTMLInputElement>(null)\n  const { sendWhenEnter, setSendWhenEnter } = useWebUI()\n  const [typing, setTyping] = React.useState<boolean>(false)\n  const { t } = useTranslation([\"playground\", \"common\"])\n  const [chatWithWebsiteEmbedding] = useStorage(\n    \"chatWithWebsiteEmbedding\",\n    false\n  )\n  const [persistChatInput] = useStorage(\"persistChatInput\", false)\n  const [persistedMessage, setPersistedMessage] = useStorage(\n    \"sidepanelPersistedMessage\",\n    \"\"\n  )\n  const [enableMessageQueue] = useStorage(\"enableMessageQueue\", false)\n  const [showMcpServersInChat] = useStorage(\"showMcpServersInChat\", true)\n  const [optimizeQueueForSmallScreen] = useStorage(\n    \"optimizeQueueForSmallScreen\",\n    false\n  )\n\n  const form = useForm({\n    initialValues: {\n      message: \"\",\n      image: \"\",\n      images: [] as string[]\n    }\n  })\n  const {\n    transcript,\n    isListening,\n    resetTranscript,\n    start: startListening,\n    stop: stopSpeechRecognition,\n    supported: browserSupportsSpeechRecognition\n  } = useSpeechRecognition()\n\n  const stopListening = async () => {\n    if (isListening) {\n      stopSpeechRecognition()\n    }\n  }\n\n  const onInputChange = async (\n    e: React.ChangeEvent<HTMLInputElement> | File\n  ) => {\n    if (e instanceof File) {\n      const base64 = await toBase64(e)\n      const currentImages = form.values.images || []\n      form.setFieldValue(\"images\", [...currentImages, base64])\n    } else {\n      if (e.target.files && e.target.files.length > 0) {\n        const files = Array.from(e.target.files)\n        for (const file of files) {\n          const base64 = await toBase64(file)\n          const currentImages = form.values.images || []\n          form.setFieldValue(\"images\", [...currentImages, base64])\n        }\n      }\n    }\n  }\n\n  const removeImage = (index: number) => {\n    const currentImages = form.values.images || []\n    const newImages = currentImages.filter((_, i) => i !== index)\n    form.setFieldValue(\"images\", newImages)\n  }\n  const textAreaFocus = () => {\n    if (textareaRef.current) {\n      textareaRef.current.focus()\n    }\n  }\n\n  useFocusShortcuts(textareaRef, true)\n\n  const handleKeyDown = (e: React.KeyboardEvent) => {\n    if (e.key === \"Process\" || e.key === \"229\") return\n    if (\n      handleChatInputKeyDown({\n        e,\n        sendWhenEnter,\n        typing,\n        isSending: streaming && !enableMessageQueue\n      })\n    ) {\n      e.preventDefault()\n      submitForm()\n    }\n  }\n\n  const handlePaste = (e: React.ClipboardEvent) => {\n    if (e.clipboardData.files.length > 0) {\n      onInputChange(e.clipboardData.files[0])\n    }\n  }\n\n  const {\n    onSubmit,\n    selectedModel,\n    chatMode,\n    stopStreamingRequest,\n    streaming,\n    setChatMode,\n    webSearch,\n    setWebSearch,\n    selectedQuickPrompt,\n    setSelectedQuickPrompt,\n    speechToTextLanguage,\n    useOCR,\n    setUseOCR,\n    defaultInternetSearchOn,\n    defaultChatWithWebsite,\n    temporaryChat\n  } = useMessage()\n\n  // Thinking mode state\n  const [defaultThinkingMode] = useStorage(\"defaultThinkingMode\", false)\n  const thinking = useStoreChatModelSettings((state) => state.thinking)\n  const setThinking = useStoreChatModelSettings((state) => state.setThinking)\n\n  React.useEffect(() => {\n    if (dropedFile) {\n      onInputChange(dropedFile)\n    }\n  }, [dropedFile])\n\n  useDynamicTextareaSize(textareaRef, form.values.message, 120)\n\n  React.useEffect(() => {\n    if (isListening) {\n      form.setFieldValue(\"message\", transcript)\n    }\n  }, [transcript])\n\n  React.useEffect(() => {\n    if (selectedQuickPrompt) {\n      const word = getVariable(selectedQuickPrompt)\n      form.setFieldValue(\"message\", selectedQuickPrompt)\n      if (word) {\n        textareaRef.current?.focus()\n        const interval = setTimeout(() => {\n          textareaRef.current?.setSelectionRange(word.start, word.end)\n          setSelectedQuickPrompt(null)\n        }, 100)\n        return () => {\n          clearInterval(interval)\n        }\n      }\n    }\n  }, [selectedQuickPrompt])\n  const { mutateAsync: sendMessage, isPending: isSending } = useMutation({\n    mutationFn: onSubmit,\n    onSuccess: () => {\n      textAreaFocus()\n    },\n    onError: (error) => {\n      textAreaFocus()\n    }\n  })\n  const validateBeforeMessageSend = async () => {\n    if (!selectedModel || selectedModel.length === 0) {\n      form.setFieldError(\"message\", t(\"formError.noModel\"))\n      return false\n    }\n    if (chatMode === \"rag\") {\n      const defaultEM = await defaultEmbeddingModelForRag()\n      if (!defaultEM && chatWithWebsiteEmbedding) {\n        form.setFieldError(\"message\", t(\"formError.noEmbeddingModel\"))\n        return false\n      }\n    }\n    if (webSearch) {\n      const defaultEM = await defaultEmbeddingModelForRag()\n      const simpleSearch = await getIsSimpleInternetSearch()\n      if (!defaultEM && !simpleSearch) {\n        form.setFieldError(\"message\", t(\"formError.noEmbeddingModel\"))\n        return false\n      }\n    }\n\n    return true\n  }\n\n  const sendQueuedTextMessage = async (payload: {\n    message: string\n    images: string[]\n  }) => {\n    const trimmedMessage = payload.message.trim()\n    const hasImages = payload.images.length > 0\n    if (!trimmedMessage && !hasImages) {\n      throw new Error(\"Queue item is empty\")\n    }\n\n    const isValid = await validateBeforeMessageSend()\n    if (!isValid) {\n      throw new Error(\"Validation failed\")\n    }\n\n    await sendMessage({\n      image: payload.images.length > 0 ? payload.images[0] : \"\",\n      images: payload.images,\n      message: trimmedMessage\n    })\n  }\n\n  const {\n    queuedMessages,\n    enqueueMessage,\n    deleteQueuedMessage,\n    takeQueuedMessage,\n    sendQueuedMessageNow\n  } = useMessageQueue({\n    enabled: enableMessageQueue,\n    streaming,\n    onSendMessage: sendQueuedTextMessage,\n    onStopStreaming: stopStreamingRequest\n  })\n  const [isQueuePanelExpanded, setIsQueuePanelExpanded] = React.useState(false)\n  const hasQueuedMessages = queuedMessages.length > 0\n  const useCompactActions = optimizeQueueForSmallScreen\n  const [isCompactActionsPopoverOpen, setIsCompactActionsPopoverOpen] =\n    React.useState(false)\n\n  React.useEffect(() => {\n    if (\n      !enableMessageQueue ||\n      !optimizeQueueForSmallScreen ||\n      !hasQueuedMessages\n    ) {\n      setIsQueuePanelExpanded(false)\n    }\n  }, [enableMessageQueue, hasQueuedMessages, optimizeQueueForSmallScreen])\n\n  React.useEffect(() => {\n    if (!useCompactActions) {\n      setIsCompactActionsPopoverOpen(false)\n    }\n  }, [useCompactActions])\n\n  const sendFormValue = async (value: {\n    message: string\n    image: string\n    images: string[]\n  }) => {\n    if (\n      value.message.trim().length === 0 &&\n      (!value.images || value.images.length === 0)\n    ) {\n      return\n    }\n\n    const isValid = await validateBeforeMessageSend()\n    if (!isValid) {\n      return\n    }\n\n    form.reset()\n    if (persistChatInput) {\n      setPersistedMessage(\"\")\n    }\n    textAreaFocus()\n\n    await sendMessage({\n      image: value.images && value.images.length > 0 ? value.images[0] : \"\",\n      images: value.images,\n      message: value.message.trim()\n    })\n  }\n\n  const handleFormSubmit = async (value: {\n    message: string\n    image: string\n    images: string[]\n  }) => {\n    await stopListening()\n\n    if (enableMessageQueue && streaming) {\n      const enqueued = enqueueMessage({\n        message: value.message,\n        images: value.images || []\n      })\n      if (enqueued) {\n        form.setFieldValue(\"message\", \"\")\n        form.setFieldValue(\"images\", [])\n        if (persistChatInput) {\n          setPersistedMessage(\"\")\n        }\n      }\n      return\n    }\n\n    await sendFormValue(value)\n  }\n\n  const submitForm = () => {\n    form.onSubmit(handleFormSubmit)()\n  }\n\n  React.useEffect(() => {\n    const handleDrop = (e: DragEvent) => {\n      e.preventDefault()\n      if (e.dataTransfer?.items) {\n        for (let i = 0; i < e.dataTransfer.items.length; i++) {\n          if (e.dataTransfer.items[i].type === \"text/plain\") {\n            e.dataTransfer.items[i].getAsString((text) => {\n              form.setFieldValue(\"message\", text)\n            })\n          }\n        }\n      }\n    }\n    const handleDragOver = (e: DragEvent) => {\n      e.preventDefault()\n    }\n    textareaRef.current?.addEventListener(\"drop\", handleDrop)\n    textareaRef.current?.addEventListener(\"dragover\", handleDragOver)\n\n    if (defaultInternetSearchOn) {\n      setWebSearch(true)\n    }\n\n    if (defaultChatWithWebsite) {\n      setChatMode(\"rag\")\n    }\n\n    return () => {\n      textareaRef.current?.removeEventListener(\"drop\", handleDrop)\n      textareaRef.current?.removeEventListener(\"dragover\", handleDragOver)\n    }\n  }, [])\n\n  // Separate effect for restoring persisted message to handle async useStorage\n  React.useEffect(() => {\n    if (persistChatInput && persistedMessage && !form.values.message) {\n      form.setFieldValue(\"message\", persistedMessage)\n    }\n  }, [persistChatInput, persistedMessage])\n\n  React.useEffect(() => {\n    if (defaultInternetSearchOn) {\n      setWebSearch(true)\n    }\n  }, [defaultInternetSearchOn])\n\n  const compactActionsPopoverContent = (\n    <div className=\"w-60 space-y-2\">\n      {chatMode !== \"vision\" && (\n        <div\n          className={`flex items-center justify-between rounded-lg border border-gray-200 px-2 py-1.5 dark:border-[#404040] ${\n            chatMode === \"rag\" ? \"hidden\" : \"flex\"\n          }`}>\n          <span className=\"text-xs text-gray-600 dark:text-gray-300\">\n            {t(\"tooltip.searchInternet\")}\n          </span>\n          <Switch\n            size=\"small\"\n            checked={webSearch}\n            onChange={(enabled) => setWebSearch(enabled)}\n          />\n        </div>\n      )}\n      {defaultThinkingMode && isThinkingCapableModel(selectedModel) && (\n        isGptOssModel(selectedModel) ? (\n          <div className=\"flex items-center justify-between rounded-lg border border-gray-200 px-2 py-1.5 dark:border-[#404040]\">\n            <span className=\"text-xs text-gray-600 dark:text-gray-300\">\n              {t(\"tooltip.thinking\")}\n            </span>\n            <Radio.Group\n              value={thinking || \"medium\"}\n              onChange={(e) => setThinking?.(e.target.value)}\n              optionType=\"button\"\n              size=\"small\"\n              options={[\n                {\n                  label: t(\"common:modelSettings.form.thinking.levels.low\"),\n                  value: \"low\"\n                },\n                {\n                  label: t(\"common:modelSettings.form.thinking.levels.medium\"),\n                  value: \"medium\"\n                },\n                {\n                  label: t(\"common:modelSettings.form.thinking.levels.high\"),\n                  value: \"high\"\n                }\n              ]}\n            />\n          </div>\n        ) : (\n          <div className=\"flex items-center justify-between rounded-lg border border-gray-200 px-2 py-1.5 dark:border-[#404040]\">\n            <span className=\"text-xs text-gray-600 dark:text-gray-300\">\n              {t(\"tooltip.thinking\")}\n            </span>\n            <Switch\n              size=\"small\"\n              checked={!!thinking}\n              onChange={(enabled) => setThinking?.(enabled)}\n              checkedChildren={t(\"form.thinking.on\")}\n              unCheckedChildren={t(\"form.thinking.off\")}\n            />\n          </div>\n        )\n      )}\n      <div className=\"flex items-center justify-between rounded-lg border border-gray-200 px-2 py-1.5 dark:border-[#404040]\">\n        <span className=\"text-xs text-gray-600 dark:text-gray-300\">\n          {t(\"sendWhenEnter\")}\n        </span>\n        <Switch\n          size=\"small\"\n          checked={sendWhenEnter}\n          onChange={(enabled) => setSendWhenEnter(enabled)}\n        />\n      </div>\n      <div className=\"flex items-center justify-between rounded-lg border border-gray-200 px-2 py-1.5 dark:border-[#404040]\">\n        <span className=\"text-xs text-gray-600 dark:text-gray-300\">\n          {t(\"common:chatWithCurrentPage\")}\n        </span>\n        <Switch\n          size=\"small\"\n          checked={chatMode === \"rag\"}\n          onChange={(enabled) => setChatMode(enabled ? \"rag\" : \"normal\")}\n        />\n      </div>\n      <div className=\"flex items-center justify-between rounded-lg border border-gray-200 px-2 py-1.5 dark:border-[#404040]\">\n        <span className=\"text-xs text-gray-600 dark:text-gray-300\">\n          {t(\"useOCR\")}\n        </span>\n        <Switch\n          size=\"small\"\n          checked={useOCR}\n          onChange={(enabled) => setUseOCR(enabled)}\n        />\n      </div>\n      <button\n        type=\"button\"\n        onClick={() => {\n          if (chatMode === \"vision\") {\n            setChatMode(\"normal\")\n          } else {\n            setChatMode(\"vision\")\n          }\n          setIsCompactActionsPopoverOpen(false)\n        }}\n        disabled={chatMode === \"rag\"}\n        className={`flex w-full items-center justify-between rounded-lg border border-gray-200 px-2 py-1.5 text-left dark:border-[#404040] ${\n          chatMode === \"rag\" ? \"hidden\" : \"flex\"\n        } disabled:opacity-50`}>\n        <span className=\"text-xs text-gray-600 dark:text-gray-300\">\n          {t(\"tooltip.vision\")}\n        </span>\n        {chatMode === \"vision\" ? (\n          <EyeIcon className=\"h-4 w-4 text-gray-500 dark:text-gray-300\" />\n        ) : (\n          <EyeOffIcon className=\"h-4 w-4 text-gray-500 dark:text-gray-300\" />\n        )}\n      </button>\n    </div>\n  )\n\n  return (\n    <div className=\"flex w-full flex-col items-center px-2\">\n      <div className=\"relative z-10 flex w-full flex-col items-center justify-center gap-2 text-base\">\n        <div className=\"relative flex w-full flex-row justify-center gap-2 lg:w-4/5\">\n          <div\n            data-istemporary-chat={temporaryChat}\n            className={` bg-neutral-50  dark:bg-[#262626] relative w-full max-w-[48rem] p-1 backdrop-blur-lg duration-100 border border-gray-300 rounded-t-xl  dark:border-[#404040] data-[istemporary-chat='true']:bg-gray-200 data-[istemporary-chat='true']:dark:bg-black`}>\n            {enableMessageQueue &&\n              optimizeQueueForSmallScreen &&\n              hasQueuedMessages && (\n                <div className=\"px-2 pt-2 md:hidden\">\n                  <button\n                    type=\"button\"\n                    onClick={() =>\n                      setIsQueuePanelExpanded((previous) => !previous)\n                    }\n                    className=\"flex w-full items-center justify-between rounded-lg border border-dashed border-gray-300 bg-white/70 px-3 py-2 text-xs text-gray-700 dark:border-[#4a4a4a] dark:bg-[#303030]/70 dark:text-gray-200\"\n                    aria-expanded={isQueuePanelExpanded}\n                    aria-controls=\"sidepanel-queued-messages\">\n                    <span className=\"inline-flex items-center gap-2 font-medium\">\n                      {t(\"form.queue.title\", \"Queued messages\")}\n                      <span className=\"inline-flex min-w-5 items-center justify-center rounded-full bg-gray-200 px-1.5 py-0.5 text-[11px] text-gray-700 dark:bg-[#454545] dark:text-gray-200\">\n                        {queuedMessages.length}\n                      </span>\n                    </span>\n                    {isQueuePanelExpanded ? (\n                      <MinusIcon className=\"h-3.5 w-3.5\" />\n                    ) : (\n                      <PlusIcon className=\"h-3.5 w-3.5\" />\n                    )}\n                  </button>\n                </div>\n              )}\n            {enableMessageQueue && (\n              <div\n                id=\"sidepanel-queued-messages\"\n                className={\n                  optimizeQueueForSmallScreen && !isQueuePanelExpanded\n                    ? \"hidden md:block\"\n                    : \"block\"\n                }>\n                <QueuedMessagesList\n                  queuedMessages={queuedMessages}\n                  onDelete={deleteQueuedMessage}\n                  onEdit={(id) => {\n                    const queuedItem = takeQueuedMessage(id)\n                    if (!queuedItem) {\n                      return\n                    }\n                    form.setFieldValue(\"message\", queuedItem.message)\n                    form.setFieldValue(\"images\", queuedItem.images || [])\n                    if (persistChatInput) {\n                      setPersistedMessage(queuedItem.message)\n                    }\n                    textAreaFocus()\n                  }}\n                  onSend={sendQueuedMessageNow}\n                  title={t(\"form.queue.title\", \"Queued messages\")}\n                />\n              </div>\n            )}\n            {form.values.images && form.values.images.length > 0 && (\n              <div className=\"p-2 border-b border-gray-200 dark:border-[#404040]\">\n                <div className=\"flex flex-wrap gap-2\">\n                  {form.values.images.map((img, index) => (\n                    <div key={index} className=\"relative\">\n                      <button\n                        type=\"button\"\n                        onClick={() => removeImage(index)}\n                        className=\"absolute -top-2 -right-2 flex items-center justify-center z-10 bg-white dark:bg-[#262626] p-0.5 rounded-full hover:bg-gray-100 dark:hover:bg-[#404040] text-black dark:text-gray-100 shadow-md\">\n                        <X className=\"h-3 w-3\" />\n                      </button>\n                      <Image\n                        src={img}\n                        alt={`Uploaded Image ${index + 1}`}\n                        preview={true}\n                        className=\"rounded-md max-h-24 object-cover\"\n                      />\n                    </div>\n                  ))}\n                </div>\n              </div>\n            )}\n            <div>\n              <div className=\"flex\">\n                <form\n                  onSubmit={form.onSubmit(handleFormSubmit)}\n                  className=\"shrink-0 flex-grow  flex flex-col items-center \">\n                  <input\n                    id=\"file-upload\"\n                    name=\"file-upload\"\n                    type=\"file\"\n                    className=\"sr-only\"\n                    ref={inputRef}\n                    accept=\"image/*\"\n                    multiple={true}\n                    onChange={onInputChange}\n                  />\n                  <div className=\"w-full  flex flex-col px-1\">\n                    <textarea\n                      onKeyDown={(e) => handleKeyDown(e)}\n                      ref={textareaRef}\n                      className=\"px-2 py-2 w-full resize-none bg-transparent focus-within:outline-none focus:ring-0 focus-visible:ring-0 ring-0 dark:ring-0 border-0 dark:text-gray-100\"\n                      onPaste={handlePaste}\n                      rows={1}\n                      style={{ minHeight: \"60px\" }}\n                      tabIndex={0}\n                      onCompositionStart={() => {\n                        if (import.meta.env.BROWSER !== \"firefox\") {\n                          setTyping(true)\n                        }\n                      }}\n                      onCompositionEnd={() => {\n                        if (import.meta.env.BROWSER !== \"firefox\") {\n                          setTyping(false)\n                        }\n                      }}\n                      placeholder={t(\"form.textarea.placeholder\")}\n                      {...form.getInputProps(\"message\")}\n                      onChange={(e) => {\n                        form.getInputProps(\"message\").onChange(e)\n                        // Persist message as user types\n                        if (persistChatInput) {\n                          setPersistedMessage(e.target.value)\n                        }\n                      }}\n                    />\n                    <div\n                      className={`flex mt-4 items-center gap-3 ${\n                        useCompactActions\n                          ? \"w-full justify-between md:w-auto md:justify-end\"\n                          : \"justify-end\"\n                      }`}>\n                      {useCompactActions && (\n                        <Popover\n                          trigger=\"click\"\n                          placement=\"topRight\"\n                          open={isCompactActionsPopoverOpen}\n                          onOpenChange={setIsCompactActionsPopoverOpen}\n                          content={compactActionsPopoverContent}>\n                          <Tooltip title={t(\"common:more\", \"More\")}>\n                            <button\n                              type=\"button\"\n                              className=\"inline-flex items-center justify-center rounded-md border border-gray-300 p-1.5 dark:border-[#404040] dark:text-gray-300 md:hidden\">\n                              <PlusIcon className=\"h-4 w-4\" />\n                            </button>\n                          </Tooltip>\n                        </Popover>\n                      )}\n                      <div\n                        className={`flex items-center gap-3 ${\n                          useCompactActions ? \"ml-auto\" : \"\"\n                        }`}>\n                      <div\n                        className={`items-center gap-3 ${\n                          useCompactActions ? \"hidden md:flex\" : \"flex\"\n                        }`}>\n                        {chatMode !== \"vision\" && (\n                          <Tooltip title={t(\"tooltip.searchInternet\")}>\n                            <button\n                              type=\"button\"\n                              onClick={() => setWebSearch(!webSearch)}\n                              className={`inline-flex items-center gap-2   ${\n                                chatMode === \"rag\" ? \"hidden\" : \"block\"\n                              }`}>\n                              {webSearch ? (\n                                <PiGlobe className=\"h-4 w-4 text-blue-600 dark:text-blue-400\" />\n                              ) : (\n                                <PiGlobeX className=\"h-4 w-4 text-[#404040] dark:text-gray-400\" />\n                              )}\n                            </button>\n                          </Tooltip>\n                        )}\n                        {defaultThinkingMode &&\n                          isThinkingCapableModel(selectedModel) &&\n                          (isGptOssModel(selectedModel) ? (\n                            <Popover\n                              content={\n                                <div>\n                                  <Radio.Group\n                                    value={thinking || \"medium\"}\n                                    onChange={(e) =>\n                                      setThinking?.(e.target.value)\n                                    }\n                                    className=\"flex flex-col gap-2\">\n                                    <Radio value=\"low\">\n                                      {t(\n                                        \"common:modelSettings.form.thinking.levels.low\"\n                                      )}\n                                    </Radio>\n                                    <Radio value=\"medium\">\n                                      {t(\n                                        \"common:modelSettings.form.thinking.levels.medium\"\n                                      )}\n                                    </Radio>\n                                    <Radio value=\"high\">\n                                      {t(\n                                        \"common:modelSettings.form.thinking.levels.high\"\n                                      )}\n                                    </Radio>\n                                  </Radio.Group>\n                                  <div className=\"text-xs text-gray-500 dark:text-gray-400 mt-2 px-1 border-t border-gray-200 dark:border-gray-700 pt-2\">\n                                    Note: This model always includes reasoning\n                                  </div>\n                                </div>\n                              }\n                              title=\"Reasoning Level\"\n                              trigger=\"click\">\n                              <Tooltip title=\"Adjust reasoning intensity\">\n                                <button\n                                  type=\"button\"\n                                  className=\"inline-flex items-center gap-2\">\n                                  <Brain className=\"h-4 w-4 text-blue-600 dark:text-blue-400\" />\n                                </button>\n                              </Tooltip>\n                            </Popover>\n                          ) : (\n                            <Tooltip title={t(\"tooltip.thinking\")}>\n                              <button\n                                type=\"button\"\n                                onClick={() => setThinking?.(!thinking)}\n                                className=\"inline-flex items-center gap-2\">\n                                {thinking ?? true ? (\n                                  <Brain className=\"h-4 w-4 text-blue-600 dark:text-blue-400\" />\n                                ) : (\n                                  <BrainCircuit className=\"h-4 w-4 text-[#404040] dark:text-gray-400\" />\n                                )}\n                              </button>\n                            </Tooltip>\n                          ))}\n                        {browserSupportsSpeechRecognition && (\n                          <Tooltip title={t(\"tooltip.speechToText\")}>\n                            <button\n                              type=\"button\"\n                              onClick={async () => {\n                                if (isListening) {\n                                  stopListening()\n                                } else {\n                                  resetTranscript()\n                                  startListening({\n                                    continuous: true,\n                                    lang: speechToTextLanguage\n                                  })\n                                }\n                              }}\n                              className={`flex items-center justify-center dark:text-gray-300`}>\n                              {!isListening ? (\n                                <MicIcon className=\"h-4 w-4\" />\n                              ) : (\n                                <div className=\"relative\">\n                                  <span className=\"animate-ping absolute inline-flex h-2 w-2 rounded-full bg-red-400 opacity-75\"></span>\n                                  <MicIcon className=\"h-4 w-4\" />\n                                </div>\n                              )}\n                            </button>\n                          </Tooltip>\n                        )}\n                        <Tooltip title={t(\"tooltip.vision\")}>\n                          <button\n                            type=\"button\"\n                            onClick={() => {\n                              if (chatMode === \"vision\") {\n                                setChatMode(\"normal\")\n                              } else {\n                                setChatMode(\"vision\")\n                              }\n                            }}\n                            disabled={chatMode === \"rag\"}\n                            className={`flex items-center justify-center dark:text-gray-300 ${\n                              chatMode === \"rag\" ? \"hidden\" : \"block\"\n                            } disabled:opacity-50`}>\n                            {chatMode === \"vision\" ? (\n                              <EyeIcon className=\"h-4 w-4\" />\n                            ) : (\n                              <EyeOffIcon className=\"h-4 w-4\" />\n                            )}\n                          </button>\n                        </Tooltip>\n                        <Tooltip title={t(\"tooltip.uploadImage\")}>\n                          <button\n                            type=\"button\"\n                            onClick={() => {\n                              inputRef.current?.click()\n                            }}\n                            disabled={chatMode === \"vision\"}\n                            className={`flex items-center justify-center disabled:opacity-50 dark:text-gray-300 ${\n                              chatMode === \"rag\" ? \"hidden\" : \"block\"\n                            }`}>\n                            <ImageIcon className=\"h-4 w-4\" />\n                          </button>\n                        </Tooltip>\n                      </div>\n                      {useCompactActions && (\n                        <Tooltip title={t(\"tooltip.uploadImage\")}>\n                          <button\n                            type=\"button\"\n                            onClick={() => {\n                              inputRef.current?.click()\n                            }}\n                            disabled={chatMode === \"vision\"}\n                            className={`inline-flex items-center justify-center p-1.5 dark:text-gray-300 md:hidden ${\n                              chatMode === \"rag\" ? \"hidden\" : \"flex\"\n                            } disabled:opacity-50`}>\n                            <PaperclipIcon className=\"h-4 w-4\" />\n                          </button>\n                        </Tooltip>\n                      )}\n                      {useCompactActions && browserSupportsSpeechRecognition && (\n                        <Tooltip title={t(\"tooltip.speechToText\")}>\n                          <button\n                            type=\"button\"\n                            onClick={async () => {\n                              if (isListening) {\n                                stopListening()\n                              } else {\n                                resetTranscript()\n                                startListening({\n                                  continuous: true,\n                                  lang: speechToTextLanguage\n                                })\n                              }\n                            }}\n                            className=\"inline-flex items-center justify-center p-1.5 dark:text-gray-300 md:hidden\">\n                            {!isListening ? (\n                              <MicIcon className=\"h-4 w-4\" />\n                            ) : (\n                              <div className=\"relative\">\n                                <span className=\"animate-ping absolute inline-flex h-2 w-2 rounded-full bg-red-400 opacity-75\"></span>\n                                <MicIcon className=\"h-4 w-4\" />\n                              </div>\n                            )}\n                          </button>\n                        </Tooltip>\n                      )}\n                      {showMcpServersInChat && <McpServerToggle />}\n                      <ModelSelect iconClassName=\"size-4\" />\n                        {streaming && !enableMessageQueue ? (\n                          <Tooltip title={t(\"tooltip.stopStreaming\")}>\n                            <button\n                              type=\"button\"\n                              onClick={stopStreamingRequest}\n                            className=\"text-gray-800 dark:text-gray-300 border border-gray-300 dark:border-[#404040] rounded-md p-1\">\n                            <StopCircleIcon className=\"h-5 w-5\" />\n                          </button>\n                        </Tooltip>\n                      ) : (\n                        <div className=\"inline-flex items-center gap-2\">\n                            {streaming && (\n                              <Tooltip title={t(\"tooltip.stopStreaming\")}>\n                                <button\n                                  type=\"button\"\n                                  onClick={stopStreamingRequest}\n                                className=\"text-gray-800 dark:text-gray-300 border border-gray-300 dark:border-[#404040] rounded-md p-1\">\n                                  <StopCircleIcon className=\"h-5 w-5\" />\n                                </button>\n                              </Tooltip>\n                            )}\n                            {useCompactActions ? (\n                              <Tooltip\n                                title={\n                                  streaming && enableMessageQueue\n                                    ? t(\"form.queue.add\", \"Queue\")\n                                    : t(\"common:submit\")\n                                }>\n                                <button\n                                  type=\"submit\"\n                                  disabled={isSending && !enableMessageQueue}\n                                  className=\"inline-flex h-9 w-9 items-center justify-center rounded-full bg-gray-900 text-white transition hover:bg-black disabled:opacity-60 dark:bg-white dark:text-black dark:hover:bg-gray-100\">\n                                  <ArrowUp className=\"h-4 w-4\" />\n                                </button>\n                              </Tooltip>\n                            ) : (\n                              <Dropdown.Button\n                                htmlType=\"submit\"\n                                disabled={isSending && !enableMessageQueue}\n                                className=\"!justify-end !w-auto\"\n                                icon={\n                                  <svg\n                                    xmlns=\"http://www.w3.org/2000/svg\"\n                                    fill=\"none\"\n                                    viewBox=\"0 0 24 24\"\n                                    strokeWidth={1.5}\n                                    stroke=\"currentColor\"\n                                    className=\"w-4 h-4\">\n                                    <path\n                                      strokeLinecap=\"round\"\n                                      strokeLinejoin=\"round\"\n                                      d=\"m19.5 8.25-7.5 7.5-7.5-7.5\"\n                                    />\n                                  </svg>\n                                }\n                                menu={{\n                                  items: [\n                                    {\n                                      key: 1,\n                                      label: (\n                                        <Checkbox\n                                          checked={sendWhenEnter}\n                                          onChange={(e) =>\n                                            setSendWhenEnter(e.target.checked)\n                                          }>\n                                          {t(\"sendWhenEnter\")}\n                                        </Checkbox>\n                                      )\n                                    },\n                                    {\n                                      key: 2,\n                                      label: (\n                                        <Checkbox\n                                          checked={chatMode === \"rag\"}\n                                          onChange={(e) => {\n                                            setChatMode(\n                                              e.target.checked ? \"rag\" : \"normal\"\n                                            )\n                                          }}>\n                                          {t(\"common:chatWithCurrentPage\")}\n                                        </Checkbox>\n                                      )\n                                    },\n                                    {\n                                      key: 3,\n                                      label: (\n                                        <Checkbox\n                                          checked={useOCR}\n                                          onChange={(e) =>\n                                            setUseOCR(e.target.checked)\n                                          }>\n                                          {t(\"useOCR\")}\n                                        </Checkbox>\n                                      )\n                                    }\n                                  ]\n                                }}>\n                                <div className=\"inline-flex gap-2\">\n                                  {sendWhenEnter ? (\n                                    <svg\n                                      xmlns=\"http://www.w3.org/2000/svg\"\n                                      fill=\"none\"\n                                      stroke=\"currentColor\"\n                                      strokeLinecap=\"round\"\n                                      strokeLinejoin=\"round\"\n                                      strokeWidth=\"2\"\n                                      className=\"h-4 w-4\"\n                                      viewBox=\"0 0 24 24\">\n                                      <path d=\"M9 10L4 15 9 20\"></path>\n                                      <path d=\"M20 4v7a4 4 0 01-4 4H4\"></path>\n                                    </svg>\n                                  ) : null}\n                                  {streaming && enableMessageQueue\n                                    ? t(\"form.queue.add\", \"Queue\")\n                                    : t(\"common:submit\")}\n                                </div>\n                              </Dropdown.Button>\n                            )}\n                        </div>\n                      )}\n                      </div>\n                    </div>\n                  </div>\n                </form>\n              </div>\n              {form.errors.message && (\n                <div className=\"text-red-500 text-center text-sm mt-1\">\n                  {form.errors.message}\n                </div>\n              )}\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Sidepanel/Chat/header.tsx",
    "content": "import logoImage from \"~/assets/icon.png\"\nimport { useMessage } from \"~/hooks/useMessage\"\nimport { Link } from \"react-router-dom\"\nimport { Tooltip, Drawer, notification } from \"antd\"\nimport {\n  BoxesIcon,\n  BrainCog,\n  CogIcon,\n  EraserIcon,\n  // EraserIcon,\n  HistoryIcon,\n  PlusSquare,\n  XIcon,\n  MessageSquareShareIcon\n} from \"lucide-react\"\nimport { useTranslation } from \"react-i18next\"\nimport { CurrentChatModelSettings } from \"@/components/Common/Settings/CurrentChatModelSettings\"\nimport React from \"react\"\nimport { useStorage } from \"@plasmohq/storage/hook\"\nimport { PromptSelect } from \"@/components/Common/PromptSelect\"\nimport { Sidebar } from \"@/components/Option/Sidebar\"\nimport { BsIncognito } from \"react-icons/bs\"\nimport { isFireFoxPrivateMode } from \"@/utils/is-private-mode\"\n\ntype SidepanelHeaderProps = {\n  sidebarOpen?: boolean\n  setSidebarOpen?: (open: boolean) => void\n}\n\nexport const SidepanelHeader = ({\n  sidebarOpen: propSidebarOpen,\n  setSidebarOpen: propSetSidebarOpen\n}: SidepanelHeaderProps = {}) => {\n  const [hideCurrentChatModelSettings] = useStorage(\n    \"hideCurrentChatModelSettings\",\n    false\n  )\n\n  const {\n    clearChat,\n    isEmbedding,\n    messages,\n    streaming,\n    selectedSystemPrompt,\n    setSelectedSystemPrompt,\n    setSelectedQuickPrompt,\n    setMessages,\n    setHistory,\n    setHistoryId,\n    setSelectedModel,\n    historyId,\n    history,\n    useOCR,\n    temporaryChat,\n    setTemporaryChat,\n    selectedModel\n  } = useMessage()\n  const { t } = useTranslation([\"sidepanel\", \"common\", \"option\"])\n  const [openModelSettings, setOpenModelSettings] = React.useState(false)\n  const [localSidebarOpen, setLocalSidebarOpen] = React.useState(false)\n  const [webuiBtnSidePanel, setWebuiBtnSidePanel] = useStorage(\n    \"webuiBtnSidePanel\",\n    false\n  )\n\n  // Use prop state if provided, otherwise use local state\n  const sidebarOpen =\n    propSidebarOpen !== undefined ? propSidebarOpen : localSidebarOpen\n  const setSidebarOpen = propSetSidebarOpen || setLocalSidebarOpen\n\n  return (\n    <div\n      data-istemporary-chat={temporaryChat}\n      className=\" px-3 justify-between bg-white dark:bg-[#1a1a1a] border-b border-gray-300 dark:border-gray-700 py-4 items-center absolute top-0 z-10 flex h-14 w-full data-[istemporary-chat='true']:bg-gray-200 data-[istemporary-chat='true']:dark:bg-black\">\n      <div className=\"focus:outline-none focus-visible:ring-2 focus-visible:ring-pink-700 flex items-center dark:text-white\">\n        <img\n          className=\"h-6 w-auto\"\n          src={logoImage}\n          alt={t(\"common:pageAssist\")}\n        />\n        <span className=\"ml-1 text-sm \">{t(\"common:pageAssist\")}</span>\n      </div>\n\n      <div className=\"flex items-center space-x-3\">\n        {webuiBtnSidePanel ? (\n          <Tooltip title={t(\"tooltip.openwebui\")}>\n            <button\n              onClick={() => {\n                const url = browser.runtime.getURL(\"/options.html\")\n                browser.tabs.create({ url })\n              }}\n              className=\"flex items-center space-x-1 focus:outline-none focus-visible:ring-2 focus-visible:ring-pink-700\">\n              <MessageSquareShareIcon className=\"size-4 text-gray-500 dark:text-gray-400\" />\n            </button>\n          </Tooltip>\n        ) : null}\n        {isEmbedding ? (\n          <Tooltip title={t(\"tooltip.embed\")}>\n            <BoxesIcon className=\"size-4 text-gray-500 dark:text-gray-400 animate-bounce animate-infinite\" />\n          </Tooltip>\n        ) : null}\n\n        {messages.length > 0 && !streaming && (\n          <button\n            title={t(\"option:newChat\")}\n            onClick={() => {\n              clearChat()\n            }}\n            className=\"flex items-center space-x-1 focus:outline-none focus-visible:ring-2 focus-visible:ring-pink-700\">\n            <PlusSquare className=\"size-4 text-gray-500 dark:text-gray-400\" />\n          </button>\n        )}\n\n        <button\n          title={t(\"option:temporaryChat\")}\n          onClick={() => {\n            if (isFireFoxPrivateMode) {\n              notification.error({\n                message: \"Error\",\n                description:\n                  \"Page Assist can't save chat in Firefox Private Mode. Temporary chat is enabled by default. More fixes coming soon.\"\n              })\n              return\n            }\n\n            setTemporaryChat(!temporaryChat)\n            if (messages.length > 0) {\n              clearChat()\n            }\n          }}\n          data-istemporary-chat={temporaryChat}\n          className=\"flex items-center text-gray-500 dark:text-gray-400 space-x-1 focus:outline-none focus-visible:ring-2 focus-visible:ring-pink-700 rounded-full p-1 data-[istemporary-chat='true']:bg-gray-300 data-[istemporary-chat='true']:dark:bg-gray-800\">\n          <BsIncognito className=\"size-4 \" />\n        </button>\n\n        {history.length > 0 && (\n          <button\n            title={t(\"tooltip.clear\")}\n            onClick={() => {\n              setHistory([])\n            }}\n            className=\"flex items-center space-x-1 focus:outline-none focus-visible:ring-2 focus-visible:ring-pink-700\">\n            <EraserIcon className=\"size-4 text-gray-500 dark:text-gray-400\" />\n          </button>\n        )}\n        <Tooltip title={t(\"tooltip.history\")}>\n          <button\n            onClick={() => {\n              setSidebarOpen(true)\n            }}\n            className=\"flex items-center space-x-1 focus:outline-none focus-visible:ring-2 focus-visible:ring-pink-700\">\n            <HistoryIcon className=\"size-4 text-gray-500 dark:text-gray-400\" />\n          </button>\n        </Tooltip>\n        <PromptSelect\n          selectedSystemPrompt={selectedSystemPrompt}\n          setSelectedSystemPrompt={setSelectedSystemPrompt}\n          setSelectedQuickPrompt={setSelectedQuickPrompt}\n          iconClassName=\"size-4\"\n          className=\"text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors\"\n        />\n        {!hideCurrentChatModelSettings && (\n          <Tooltip title={t(\"common:currentChatModelSettings\")}>\n            <button\n              onClick={() => setOpenModelSettings(true)}\n              className=\"text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors\">\n              <BrainCog className=\"size-4\" />\n            </button>\n          </Tooltip>\n        )}\n        <Link to=\"/settings\">\n          <CogIcon className=\"size-4 text-gray-500 dark:text-gray-400\" />\n        </Link>\n      </div>\n      <CurrentChatModelSettings\n        open={openModelSettings}\n        setOpen={setOpenModelSettings}\n        isOCREnabled={useOCR}\n      />\n\n      <Drawer\n        title={\n          <div className=\"flex items-center justify-between\">\n            <div className=\"flex items-center justify-between\">\n              {t(\"tooltip.history\")}\n            </div>\n\n            <button onClick={() => setSidebarOpen(false)}>\n              <XIcon className=\"size-4 text-gray-500 dark:text-gray-400\" />\n            </button>\n          </div>\n        }\n        placement=\"left\"\n        closeIcon={null}\n        onClose={() => setSidebarOpen(false)}\n        open={sidebarOpen}>\n        <Sidebar\n          isOpen={sidebarOpen}\n          onClose={() => setSidebarOpen(false)}\n          setMessages={setMessages}\n          setHistory={setHistory}\n          setHistoryId={setHistoryId}\n          setSelectedModel={setSelectedModel}\n          setSelectedSystemPrompt={setSelectedSystemPrompt}\n          clearChat={clearChat}\n          historyId={historyId}\n          setSystemPrompt={(e) => {}}\n          temporaryChat={false}\n          history={history}\n          selectedModel={selectedModel}\n        />\n      </Drawer>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Sidepanel/Settings/body.tsx",
    "content": "import { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\"\nimport React from \"react\"\nimport {\n  getOllamaURL,\n  systemPromptForNonRag,\n  promptForRag,\n  setOllamaURL as saveOllamaURL,\n  setPromptForRag,\n  setSystemPromptForNonRag,\n  defaultEmbeddingChunkOverlap,\n  defaultEmbeddingChunkSize,\n  defaultEmbeddingModelForRag,\n  saveForRag,\n  getEmbeddingModels\n} from \"~/services/ollama\"\n\nimport {\n  Skeleton,\n  Radio,\n  Select,\n  Form,\n  InputNumber,\n  Collapse,\n  Switch\n} from \"antd\"\nimport { useDarkMode } from \"~/hooks/useDarkmode\"\nimport { SaveButton } from \"~/components/Common/SaveButton\"\nimport { SUPPORTED_LANGUAGES } from \"~/utils/supported-languages\"\nimport { useMessage } from \"~/hooks/useMessage\"\nimport { MoonIcon, SunIcon } from \"lucide-react\"\nimport { Trans, useTranslation } from \"react-i18next\"\nimport { useI18n } from \"@/hooks/useI18n\"\nimport { TTSModeSettings } from \"@/components/Option/Settings/tts-mode\"\nimport { AdvanceOllamaSettings } from \"@/components/Common/Settings/AdvanceOllamaSettings\"\nimport { useStorage } from \"@plasmohq/storage/hook\"\nimport { getTotalFilePerKB } from \"@/services/app\"\nimport { SidepanelRag } from \"@/components/Option/Settings/sidepanel-rag\"\nimport { SSTSettings } from \"@/components/Option/Settings/sst-settings\"\n\nexport const SettingsBody = () => {\n  const { t } = useTranslation(\"settings\")\n  const [ollamaURL, setOllamaURL] = React.useState<string>(\"\")\n  const [systemPrompt, setSystemPrompt] = React.useState<string>(\"\")\n  const [ragPrompt, setRagPrompt] = React.useState<string>(\"\")\n  const [ragQuestionPrompt, setRagQuestionPrompt] = React.useState<string>(\"\")\n  const [selectedValue, setSelectedValue] = React.useState<\"normal\" | \"rag\">(\n    \"normal\"\n  )\n  const [copilotResumeLastChat, setCopilotResumeLastChat] = useStorage(\n    \"copilotResumeLastChat\",\n    false\n  )\n\n  const [hideCurrentChatModelSettings, setHideCurrentChatModelSettings] =\n    useStorage(\"hideCurrentChatModelSettings\", false)\n\n  const [speechToTextLanguage, setSpeechToTextLanguage] = useStorage(\n    \"speechToTextLanguage\",\n    \"en-US\"\n  )\n  const { mode, toggleDarkMode } = useDarkMode()\n  const queryClient = useQueryClient()\n\n  const { changeLocale, locale, supportLanguage } = useI18n()\n\n  const { data, status } = useQuery({\n    queryKey: [\"sidebarSettings\"],\n    queryFn: async () => {\n      const [\n        ollamaURL,\n        systemPrompt,\n        ragPrompt,\n        allModels,\n        chunkOverlap,\n        chunkSize,\n        defaultEM,\n        totalFilePerKB\n      ] = await Promise.all([\n        getOllamaURL(),\n        systemPromptForNonRag(),\n        promptForRag(),\n        getEmbeddingModels({ returnEmpty: true }),\n        defaultEmbeddingChunkOverlap(),\n        defaultEmbeddingChunkSize(),\n        defaultEmbeddingModelForRag(),\n        getTotalFilePerKB()\n      ])\n      return {\n        url: ollamaURL,\n        normalSystemPrompt: systemPrompt,\n        ragSystemPrompt: ragPrompt.ragPrompt,\n        ragQuestionPrompt: ragPrompt.ragQuestionPrompt,\n        models: allModels,\n        chunkOverlap,\n        chunkSize,\n        defaultEM,\n        totalFilePerKB\n      }\n    }\n  })\n\n  const { mutate: saveRAG, isPending: isSaveRAGPending } = useMutation({\n    mutationFn: async (f: {\n      model: string\n      chunkSize: number\n      overlap: number\n    }) => {\n      await saveForRag(f.model, f.chunkSize, f.overlap, data.totalFilePerKB)\n      await queryClient.invalidateQueries({ queryKey: [\"sidebarSettings\"] })\n    }\n  })\n\n  React.useEffect(() => {\n    if (data) {\n      setOllamaURL(data.url)\n      setSystemPrompt(data.normalSystemPrompt)\n      setRagPrompt(data.ragSystemPrompt)\n      setRagQuestionPrompt(data.ragQuestionPrompt)\n    }\n  }, [data])\n\n  if (status === \"pending\") {\n    return (\n      <div className=\"flex flex-col gap-4 p-4\">\n        <Skeleton active />\n        <Skeleton active />\n        <Skeleton active />\n        <Skeleton active />\n      </div>\n    )\n  }\n\n  if (status === \"error\") {\n    return <div>Error</div>\n  }\n\n  return (\n    <div className=\"flex flex-col gap-4 max-w-2xl mx-auto lg:max-w-3xl xl:max-w-4xl 2xl:max-w-5xl\">\n      <div className=\"border border-gray-300 dark:border-gray-700 rounded p-4 bg-white dark:bg-[#1a1a1a]\">\n        <h2 className=\"text-md font-semibold dark:text-white\">\n          {t(\"managePrompts.title\")}\n        </h2>\n        <div className=\"my-3 flex justify-end\">\n          <Radio.Group\n            defaultValue={selectedValue}\n            onChange={(e) => setSelectedValue(e.target.value)}>\n            <Radio.Button value=\"normal\">\n              {t(\"managePrompts.option1\")}\n            </Radio.Button>\n            <Radio.Button value=\"rag\">\n              {t(\"managePrompts.option2\")}\n            </Radio.Button>\n          </Radio.Group>\n        </div>\n\n        {selectedValue === \"normal\" && (\n          <div>\n            <span className=\"text-md font-thin text-gray-500 dark:text-gray-400\">\n              {t(\"managePrompts.systemPrompt\")}\n            </span>\n            <textarea\n              className=\"w-full border border-gray-300 dark:border-gray-700 rounded p-2 dark:bg-[#1a1a1a] dark:text-white dark:placeholder-gray-400\"\n              value={systemPrompt}\n              onChange={(e) => setSystemPrompt(e.target.value)}\n            />\n            <div className=\"flex justify-end\">\n              <SaveButton\n                onClick={() => {\n                  setSystemPromptForNonRag(systemPrompt)\n                }}\n              />\n            </div>\n          </div>\n        )}\n\n        {selectedValue === \"rag\" && (\n          <div>\n            <div className=\"mb-3\">\n              <span className=\"text-md font-thin text-gray-500 dark:text-gray-400\">\n                {t(\"managePrompts.systemPrompt\")}\n              </span>\n              <textarea\n                className=\"w-full border border-gray-300 dark:border-gray-700 rounded p-2 dark:bg-[#1a1a1a] dark:text-white dark:placeholder-gray-400\"\n                value={ragPrompt}\n                onChange={(e) => setRagPrompt(e.target.value)}\n              />\n            </div>\n            <div className=\"mb-3\">\n              <span className=\"text-md  font-thin text-gray-500 dark:text-gray-400\">\n                {t(\"managePrompts.questionPrompt\")}\n              </span>\n              <textarea\n                className=\"w-full border border-gray-300 dark:border-gray-700 rounded p-2 dark:bg-[#1a1a1a] dark:text-white dark:placeholder-gray-400\"\n                value={ragQuestionPrompt}\n                onChange={(e) => setRagQuestionPrompt(e.target.value)}\n              />\n            </div>\n\n            <div className=\"flex justify-end\">\n              <SaveButton\n                onClick={() => {\n                  setPromptForRag(ragPrompt, ragQuestionPrompt)\n                }}\n              />\n            </div>\n          </div>\n        )}\n      </div>\n      <div className=\"border border-gray-300 dark:border-gray-700 rounded p-4 bg-white dark:bg-[#1a1a1a]\">\n        <SidepanelRag hideBorder />\n      </div>\n      <div className=\"border flex flex-col gap-4 border-gray-300 dark:border-gray-700 rounded p-4 bg-white dark:bg-[#1a1a1a]\">\n        <h2 className=\"text-md font-semibold dark:text-white\">\n          {t(\"ollamaSettings.heading\")}\n        </h2>\n        <input\n          className=\"w-full border border-gray-300 dark:border-gray-700 rounded p-2 dark:bg-[#1a1a1a] dark:text-white dark:placeholder-gray-400\"\n          value={ollamaURL}\n          type=\"url\"\n          onChange={(e) => setOllamaURL(e.target.value)}\n          placeholder={t(\"ollamaSettings.settings.ollamaUrl.placeholder\")}\n        />\n\n        <Collapse\n          size=\"small\"\n          items={[\n            {\n              key: \"1\",\n              label: (\n                <div>\n                  <h2 className=\"text-base font-semibold leading-7 text-gray-900 dark:text-white\">\n                    {t(\"ollamaSettings.settings.advanced.label\")}\n                  </h2>\n                  <p className=\"text-xs text-gray-500 dark:text-gray-400 mb-4\">\n                    <Trans\n                      i18nKey=\"settings:ollamaSettings.settings.advanced.help\"\n                      components={{\n                        anchor: (\n                          <a\n                            href=\"https://github.com/n4ze3m/page-assist/blob/main/docs/connection-issue.md#solutions\"\n                            target=\"__blank\"\n                            className=\"text-blue-600 dark:text-blue-400\"></a>\n                        )\n                      }}\n                    />\n                  </p>\n                </div>\n              ),\n              children: <AdvanceOllamaSettings />\n            }\n          ]}\n        />\n\n        <div className=\"flex justify-end\">\n          <SaveButton\n            onClick={() => {\n              saveOllamaURL(ollamaURL)\n            }}\n          />\n        </div>\n      </div>\n      <div className=\"border border-gray-300 dark:border-gray-700 rounded p-4 bg-white dark:bg-[#1a1a1a]\">\n        <h2 className=\"text-md mb-4 font-semibold dark:text-white\">\n          {t(\"rag.ragSettings.label\")}\n        </h2>\n        <Form\n          onFinish={(data) => {\n            saveRAG({\n              model: data.defaultEM,\n              chunkSize: data.chunkSize,\n              overlap: data.chunkOverlap\n            })\n          }}\n          initialValues={{\n            chunkSize: data.chunkSize,\n            chunkOverlap: data.chunkOverlap,\n            defaultEM: data.defaultEM\n          }}>\n          <Form.Item\n            name=\"defaultEM\"\n            label={t(\"rag.ragSettings.model.label\")}\n            help={t(\"rag.ragSettings.model.help\")}\n            rules={[\n              {\n                required: true,\n                message: t(\"rag.ragSettings.model.required\")\n              }\n            ]}>\n            <Select\n              size=\"large\"\n              filterOption={(input, option) =>\n                option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 ||\n                option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0\n              }\n              showSearch\n              placeholder=\"Select a model\"\n              style={{ width: \"100%\" }}\n              className=\"mt-4\"\n              options={data.models?.map((model) => ({\n                label: model.name,\n                value: model.model\n              }))}\n            />\n          </Form.Item>\n\n          <Form.Item\n            name=\"chunkSize\"\n            label={t(\"rag.ragSettings.chunkSize.label\")}\n            rules={[\n              {\n                required: true,\n                message: t(\"rag.ragSettings.chunkSize.required\")\n              }\n            ]}>\n            <InputNumber\n              style={{ width: \"100%\" }}\n              placeholder={t(\"rag.ragSettings.chunkSize.placeholder\")}\n            />\n          </Form.Item>\n          <Form.Item\n            name=\"chunkOverlap\"\n            label={t(\"rag.ragSettings.chunkOverlap.label\")}\n            rules={[\n              {\n                required: true,\n                message: t(\"rag.ragSettings.chunkOverlap.required\")\n              }\n            ]}>\n            <InputNumber\n              style={{ width: \"100%\" }}\n              placeholder={t(\"rag.ragSettings.chunkOverlap.placeholder\")}\n            />\n          </Form.Item>\n\n          <div className=\"flex justify-end\">\n            <SaveButton disabled={isSaveRAGPending} btnType=\"submit\" />\n          </div>\n        </Form>\n      </div>\n\n      <div className=\"border space-y-3 w-full border-gray-300 dark:border-gray-700 rounded p-4 bg-white dark:bg-[#1a1a1a]\">\n        <h2 className=\"text-base mb-4 font-semibold leading-7 text-gray-900 dark:text-white\">\n          {t(\"generalSettings.title\")}\n        </h2>\n        <div className=\"flex flex-col  space-y-4\">\n          <span className=\"text-gray-500   dark:text-neutral-50\">\n            {t(\"generalSettings.settings.copilotResumeLastChat.label\")}\n          </span>\n\n          <div>\n            <Switch\n              checked={copilotResumeLastChat}\n              onChange={(checked) => setCopilotResumeLastChat(checked)}\n            />\n          </div>\n        </div>\n        <div className=\"flex flex-col space-y-4\">\n          <div className=\"inline-flex items-center gap-2\">\n            <span className=\"text-gray-500   dark:text-neutral-50\">\n              {t(\"generalSettings.settings.hideCurrentChatModelSettings.label\")}\n            </span>\n          </div>\n          <div>\n            <Switch\n              checked={hideCurrentChatModelSettings}\n              onChange={(checked) => setHideCurrentChatModelSettings(checked)}\n            />\n          </div>\n        </div>\n        <div>\n          <div className=\"text-xs mb-2  dark:text-white\">\n            {t(\"generalSettings.settings.speechRecognitionLang.label\")}{\" \"}\n          </div>\n          <Select\n            placeholder={t(\n              \"generalSettings.settings.speechRecognitionLang.placeholder\"\n            )}\n            allowClear\n            showSearch\n            options={SUPPORTED_LANGUAGES}\n            value={speechToTextLanguage}\n            filterOption={(input, option) =>\n              option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 ||\n              option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0\n            }\n            onChange={(value) => {\n              setSpeechToTextLanguage(value)\n            }}\n            style={{\n              width: \"100%\"\n            }}\n          />\n        </div>\n        <div>\n          <div className=\"text-xs mb-2  dark:text-white\">\n            {t(\"generalSettings.settings.language.label\")}{\" \"}\n          </div>\n\n          <Select\n            placeholder={t(\"generalSettings.settings.language.placeholder\")}\n            showSearch\n            options={supportLanguage}\n            value={locale}\n            filterOption={(input, option) =>\n              option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 ||\n              option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0\n            }\n            onChange={(value) => {\n              changeLocale(value)\n            }}\n            style={{\n              width: \"100%\"\n            }}\n          />\n        </div>\n      </div>\n      <div className=\"border border-gray-300 dark:border-gray-700 rounded p-4 bg-white dark:bg-[#1a1a1a]\">\n        <SSTSettings hideBorder />\n      </div>\n      <div className=\"border border-gray-300 dark:border-gray-700 rounded p-4 bg-white dark:bg-[#1a1a1a]\">\n        <TTSModeSettings hideBorder />\n      </div>\n      <div className=\"border border-gray-300 dark:border-gray-700 rounded p-4 bg-white dark:bg-[#1a1a1a]\">\n        <h2 className=\"text-md mb-4 font-semibold dark:text-white\">\n          {t(\"generalSettings.settings.darkMode.label\")}{\" \"}\n        </h2>\n        {mode === \"dark\" ? (\n          <button\n            onClick={toggleDarkMode}\n            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\">\n            <SunIcon className=\"h-4 w-4 mr-2\" />\n            {t(\"generalSettings.settings.darkMode.options.light\")}\n          </button>\n        ) : (\n          <button\n            onClick={toggleDarkMode}\n            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\">\n            <MoonIcon className=\"h-4 w-4 mr-2\" />\n            {t(\"generalSettings.settings.darkMode.options.dark\")}\n          </button>\n        )}\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Sidepanel/Settings/header.tsx",
    "content": "import { ChevronLeft, ChevronRight } from \"lucide-react\"\nimport { useTranslation } from \"react-i18next\"\nimport { Link } from \"react-router-dom\"\nimport logoImage from \"~/assets/icon.png\"\n\nexport const SidepanelSettingsHeader = () => {\n  const { t , i18n} = useTranslation(\"common\")\n  const isRTL = i18n?.dir() === \"rtl\"\n \n  return (\n    <div className=\"flex px-3 justify-start gap-3 bg-white dark:bg-[#1a1a1a] border-b border-gray-300 dark:border-gray-700  py-4 items-center\">\n      <Link to=\"/\">\n      {\n        isRTL ? (\n          <ChevronRight className=\"h-5 w-5 text-gray-500\" />\n        ) : (\n          <ChevronLeft className=\"h-5 w-5 text-gray-500\" />\n        )\n      }\n      </Link>\n      <div className=\"focus:outline-none focus-visible:ring-2 focus-visible:ring-pink-700 flex items-center dark:text-white\">\n        <img className=\"h-6 w-auto\" src={logoImage} alt={t(\"pageAssist\")} />\n        <span className=\"ml-1 text-sm \">{t(\"pageAssist\")}</span>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/context/FontSizeProvider.tsx",
    "content": "import { useStorage } from '@plasmohq/storage/hook';\nimport React, { createContext, useContext, useState, useEffect } from 'react';\n\ninterface FontSizeContextType {\n  scale: number;\n  increase: () => void;\n  decrease: () => void;\n  reset: () => void;\n  minScale: number;\n  maxScale: number;\n}\n\ninterface FontSizeProviderProps {\n  children: React.ReactNode;\n  initialScale?: number;\n  step?: number;\n  minScale?: number;\n  maxScale?: number;\n  storageKey?: string;\n}\n\nconst FontSizeContext = createContext<FontSizeContextType | undefined>(undefined);\n\n\nexport const FontSizeProvider = ({\n  children,\n  initialScale = 1,\n  step = 0.1,\n  minScale = 0.8,\n  maxScale = 2.0,\n  storageKey = 'font-size-scale'\n}: FontSizeProviderProps) => {\n  const [storedScale, setStoredScale] = useStorage<number>(\n    storageKey,\n    initialScale\n  );\n  \n  const validScale = (scale: number) => {\n    if (isNaN(scale) || scale < minScale) return minScale;\n    if (scale > maxScale) return maxScale;\n    return scale;\n  };\n  \n  const scale = validScale(storedScale);\n\n  const increase = () => {\n    setStoredScale(validScale(scale + step));\n  };\n  \n  const decrease = () => {\n    setStoredScale(validScale(scale - step));\n  };\n  \n  const reset = () => {\n    setStoredScale(initialScale);\n  };\n  \n  useEffect(() => {\n    document.documentElement.style.setProperty('--font-scale', scale.toString());\n    \n    let styleElement = document.getElementById('font-scale-styles');\n    if (!styleElement) {\n      styleElement = document.createElement('style');\n      styleElement.id = 'font-scale-styles';\n      document.head.appendChild(styleElement);\n    }\n    \n    const textSizeClasses = [\n      'text-xs', 'text-sm', 'text-base', 'text-lg', \n      'text-xl', 'text-2xl', 'text-3xl', 'text-4xl', \n      'text-5xl', 'text-6xl', 'text-7xl', 'text-8xl', 'text-9xl'\n    ];\n    \n    const tailwindRules = textSizeClasses.map(className => {\n      return `.${className} { font-size: calc(var(--${className}-size, 1rem) * var(--font-scale)) !important; }`;\n    }).join('\\n');\n    \n    const proseRules = `\n      /* Base prose class scaling */\n      .prose {\n        font-size: calc(1rem * var(--font-scale));\n      }\n      \n      /* Prose size variants */\n      .prose-xs {\n        font-size: calc(0.75rem * var(--font-scale));\n      }\n      .prose-sm {\n        font-size: calc(0.875rem * var(--font-scale));\n      }\n      .prose-base {\n        font-size: calc(1rem * var(--font-scale));\n      }\n      .prose-lg {\n        font-size: calc(1.125rem * var(--font-scale));\n      }\n      .prose-xl {\n        font-size: calc(1.25rem * var(--font-scale));\n      }\n      .prose-2xl {\n        font-size: calc(1.5rem * var(--font-scale));\n      }\n      \n      /* Scale all elements within prose */\n      .prose h1 {\n        font-size: calc(2.25em * var(--font-scale));\n      }\n      .prose h2 {\n        font-size: calc(1.5em * var(--font-scale));\n      }\n      .prose h3 {\n        font-size: calc(1.25em * var(--font-scale));\n      }\n      .prose h4 {\n        font-size: calc(1em * var(--font-scale));\n      }\n      .prose p, .prose ul, .prose ol, .prose blockquote {\n        font-size: calc(1em * var(--font-scale));\n      }\n      .prose figcaption {\n        font-size: calc(0.875em * var(--font-scale));\n      }\n      .prose code {\n        font-size: calc(0.875em * var(--font-scale));\n      }\n\n      /* Table content scaling */\n      .prose table,\n      .prose th,\n      .prose td {\n        font-size: calc(1em * var(--font-scale));\n      }\n    `;\n    \n    const sizeDefinitions = `\n      :root {\n        --text-xs-size: 0.75rem;\n        --text-sm-size: 0.875rem;\n        --text-base-size: 1rem;\n        --text-lg-size: 1.125rem;\n        --text-xl-size: 1.25rem;\n        --text-2xl-size: 1.5rem;\n        --text-3xl-size: 1.875rem;\n        --text-4xl-size: 2.25rem;\n        --text-5xl-size: 3rem;\n        --text-6xl-size: 3.75rem;\n        --text-7xl-size: 4.5rem;\n        --text-8xl-size: 6rem;\n        --text-9xl-size: 8rem;\n      }\n    `;\n    \n    styleElement.textContent = sizeDefinitions + tailwindRules + proseRules;\n    \n    return () => {\n      if (styleElement && styleElement.parentNode) {\n        styleElement.parentNode.removeChild(styleElement);\n      }\n    };\n  }, [scale]);\n  const contextValue: FontSizeContextType = {\n    scale,\n    increase,\n    decrease, \n    reset,\n    minScale,\n    maxScale\n  };\n\n  return (\n    <FontSizeContext.Provider value={contextValue}>\n      {children}\n    </FontSizeContext.Provider>\n  );\n};\nexport const useFontSize = (): FontSizeContextType => {\n  const context = useContext(FontSizeContext);\n  if (context === undefined) {\n    throw new Error('useFontSize must be used within a FontSizeProvider');\n  }\n  return context;\n};\n"
  },
  {
    "path": "src/context/index.tsx",
    "content": "import { Message } from \"@/types/message\"\nimport React, { Dispatch, SetStateAction, createContext } from \"react\"\n\ninterface PageAssistContext {\n  messages: Message[]\n  setMessages: Dispatch<SetStateAction<Message[]>>\n\n  controller: AbortController | null\n  setController: Dispatch<SetStateAction<AbortController>>\n\n  embeddingController: AbortController | null\n  setEmbeddingController: Dispatch<SetStateAction<AbortController>>\n}\n\nexport const PageAssistContext = createContext<PageAssistContext>({\n  messages: [],\n  setMessages: () => {},\n\n  controller: null,\n  setController: () => {},\n\n  embeddingController: null,\n  setEmbeddingController: () => {}\n})\n\nexport const usePageAssist = () => {\n  const context = React.useContext(PageAssistContext)\n  if (!context) {\n    throw new Error(\"usePageAssist must be used within a PageAssistContext\")\n  }\n  return context\n}\n"
  },
  {
    "path": "src/data/ocr-language.ts",
    "content": "\n// OCR language list for Tesseract 4.00\nconst baseOcrLanguages = [\n    { label: 'Afrikaans', value: 'afr' },\n    { label: 'Amharic', value: 'amh' },\n    { label: 'Arabic', value: 'ara' },\n    { label: 'Assamese', value: 'asm' },\n    { label: 'Azerbaijani', value: 'aze' },\n    { label: 'Azerbaijani - Cyrillic', value: 'aze_cyrl' },\n    { label: 'Belarusian', value: 'bel' },\n    { label: 'Bengali', value: 'ben' },\n    { label: 'Tibetan', value: 'bod' },\n    { label: 'Bosnian', value: 'bos' },\n    { label: 'Bulgarian', value: 'bul' },\n    { label: 'Catalan; Valencian', value: 'cat' },\n    { label: 'Cebuano', value: 'ceb' },\n    { label: 'Czech', value: 'ces' },\n    { label: 'Chinese - Simplified', value: 'chi_sim' },\n    { label: 'Chinese - Traditional', value: 'chi_tra' },\n    { label: 'Cherokee', value: 'chr' },\n    { label: 'Welsh', value: 'cym' },\n    { label: 'Danish', value: 'dan' },\n    { label: 'German', value: 'deu' },\n    { label: 'Dzongkha', value: 'dzo' },\n    { label: 'Greek, Modern (1453-)', value: 'ell' },\n    { label: 'English', value: 'eng' },\n    { label: 'English, Middle (1100-1500)', value: 'enm' },\n    { label: 'Esperanto', value: 'epo' },\n    { label: 'Estonian', value: 'est' },\n    { label: 'Basque', value: 'eus' },\n    { label: 'Persian', value: 'fas' },\n    { label: 'Finnish', value: 'fin' },\n    { label: 'French', value: 'fra' },\n    { label: 'German Fraktur', value: 'frk' },\n    { label: 'French, Middle (ca. 1400-1600)', value: 'frm' },\n    { label: 'Irish', value: 'gle' },\n    { label: 'Galician', value: 'glg' },\n    { label: 'Greek, Ancient (-1453)', value: 'grc' },\n    { label: 'Gujarati', value: 'guj' },\n    { label: 'Haitian; Haitian Creole', value: 'hat' },\n    { label: 'Hebrew', value: 'heb' },\n    { label: 'Hindi', value: 'hin' },\n    { label: 'Croatian', value: 'hrv' },\n    { label: 'Hungarian', value: 'hun' },\n    { label: 'Inuktitut', value: 'iku' },\n    { label: 'Indonesian', value: 'ind' },\n    { label: 'Icelandic', value: 'isl' },\n    { label: 'Italian', value: 'ita' },\n    { label: 'Italian - Old', value: 'ita_old' },\n    { label: 'Javanese', value: 'jav' },\n    { label: 'Japanese', value: 'jpn' },\n    { label: 'Kannada', value: 'kan' },\n    { label: 'Georgian', value: 'kat' },\n    { label: 'Georgian - Old', value: 'kat_old' },\n    { label: 'Kazakh', value: 'kaz' },\n    { label: 'Central Khmer', value: 'khm' },\n    { label: 'Kirghiz; Kyrgyz', value: 'kir' },\n    { label: 'Korean', value: 'kor' },\n    { label: 'Kurdish', value: 'kur' },\n    { label: 'Lao', value: 'lao' },\n    { label: 'Latin', value: 'lat' },\n    { label: 'Latvian', value: 'lav' },\n    { label: 'Lithuanian', value: 'lit' },\n    { label: 'Malayalam', value: 'mal' },\n    { label: 'Marathi', value: 'mar' },\n    { label: 'Macedonian', value: 'mkd' },\n    { label: 'Maltese', value: 'mlt' },\n    { label: 'Malay', value: 'msa' },\n    { label: 'Burmese', value: 'mya' },\n    { label: 'Nepali', value: 'nep' },\n    { label: 'Dutch; Flemish', value: 'nld' },\n    { label: 'Norwegian', value: 'nor' },\n    { label: 'Oriya', value: 'ori' },\n    { label: 'Panjabi; Punjabi', value: 'pan' },\n    { label: 'Polish', value: 'pol' },\n    { label: 'Portuguese', value: 'por' },\n    { label: 'Pushto; Pashto', value: 'pus' },\n    { label: 'Romanian; Moldavian; Moldovan', value: 'ron' },\n    { label: 'Russian', value: 'rus' },\n    { label: 'Sanskrit', value: 'san' },\n    { label: 'Sinhala; Sinhalese', value: 'sin' },\n    { label: 'Slovak', value: 'slk' },\n    { label: 'Slovenian', value: 'slv' },\n    { label: 'Spanish; Castilian', value: 'spa' },\n    { label: 'Spanish; Castilian - Old', value: 'spa_old' },\n    { label: 'Albanian', value: 'sqi' },\n    { label: 'Serbian', value: 'srp' },\n    { label: 'Serbian - Latin', value: 'srp_latn' },\n    { label: 'Swahili', value: 'swa' },\n    { label: 'Swedish', value: 'swe' },\n    { label: 'Syriac', value: 'syr' },\n    { label: 'Tamil', value: 'tam' },\n    { label: 'Telugu', value: 'tel' },\n    { label: 'Tajik', value: 'tgk' },\n    { label: 'Tagalog', value: 'tgl' },\n    { label: 'Thai', value: 'tha' },\n    { label: 'Tigrinya', value: 'tir' },\n    { label: 'Turkish', value: 'tur' },\n    { label: 'Uighur; Uyghur', value: 'uig' },\n    { label: 'Ukrainian', value: 'ukr' },\n    { label: 'Urdu', value: 'urd' },\n    { label: 'Uzbek', value: 'uzb' },\n    { label: 'Uzbek - Cyrillic', value: 'uzb_cyrl' },\n    { label: 'Vietnamese', value: 'vie' },\n    { label: 'Yiddish', value: 'yid' },\n];\n\nexport const ocrLanguages = (\n    import.meta.env.BROWSER === 'edge'\n        ? baseOcrLanguages\n        : [\n            ...baseOcrLanguages,\n            { label: 'English (Offline)', value: 'eng-fast' },\n        ]\n);\n\n\nexport const getDefaultOcrLanguage = () => {\n    return import.meta.env.BROWSER === 'edge' ? 'eng' : 'eng-fast';\n};"
  },
  {
    "path": "src/db/dexie/branch.ts",
    "content": "import { generateID } from \"./helpers\"\nimport { db } from \"./schema\"\nimport { HistoryInfo } from \"./types\"\n\nexport const generateBranchMessage = async (\n  history_id: string,\n  branchMessageIndex: number\n) => {\n  return await db.transaction(\n    \"rw\",\n    db.messages,\n    db.chatHistories,\n    db.sessionFiles,\n    async () => {\n      const chats = await db.messages\n        .where(\"history_id\")\n        .equals(history_id)\n        .toArray()\n      const historyInfo = await db.chatHistories.get(history_id)\n\n      const sortedMessages = chats.sort((a, b) => a.createdAt - b.createdAt)\n\n      const messages = sortedMessages.slice(0, branchMessageIndex + 1)\n\n      const newHistoryId = generateID()\n\n      const history: HistoryInfo = {\n        ...historyInfo,\n        message_source: \"branch\",\n        id: newHistoryId,\n        createdAt: Date.now(),\n      }\n\n      await db.chatHistories.add(history)\n\n      const newMessages = messages.map((message) => ({\n        ...message,\n        id: generateID(),\n        history_id: newHistoryId\n      }))\n\n      await db.messages.bulkAdd(newMessages)\n\n      const sessionFiles = await db.sessionFiles.get(history_id)\n\n      if (sessionFiles) {\n        const newSessionFiles = {\n          ...sessionFiles,\n          history_id: newHistoryId\n        }\n        await db.sessionFiles.put(newSessionFiles)\n      }\n\n      return {\n        messages: newMessages,\n        history\n      }\n    }\n  )\n}\n"
  },
  {
    "path": "src/db/dexie/chat.ts",
    "content": "import {\n  ChatHistory,\n  HistoryInfo,\n  LastUsedModelType,\n  Message,\n  MessageHistory,\n  Prompt,\n  Prompts,\n  SessionFiles,\n  UploadedFile,\n  Webshare,\n  ProjectFolder,\n  ProjectFolders\n} from \"./types\"\nimport { db } from \"./schema\"\nimport { getAllModelNicknames } from \"./nickname\"\nconst PAGE_SIZE = 30\n\nfunction searchQueryInContent(content: string, query: string): boolean {\n  if (!content || !query) {\n    return false\n  }\n\n  const normalizedContent = content.toLowerCase()\n  const normalizedQuery = query.toLowerCase().trim()\n\n  const wordBoundaryPattern = new RegExp(\n    `\\\\b${normalizedQuery.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")}\\\\b`,\n    \"i\"\n  )\n\n  return wordBoundaryPattern.test(normalizedContent)\n}\n\nfunction fastForward(\n  lastRow: any,\n  idProp: string,\n  otherCriterion?: (item: any) => boolean\n) {\n  let fastForwardComplete = false\n  return (item: any) => {\n    if (fastForwardComplete) return otherCriterion ? otherCriterion(item) : true\n    if (item[idProp] === lastRow[idProp]) {\n      fastForwardComplete = true\n    }\n    return false\n  }\n}\n\nexport class PageAssistDatabase {\n  async getSessionFiles(sessionId: string): Promise<UploadedFile[]> {\n    const sessionFiles = await db.sessionFiles.get(sessionId)\n    return sessionFiles?.files || []\n  }\n\n  async getProjectFolders(): Promise<ProjectFolders> {\n    return await db.projectFolders.orderBy(\"createdAt\").toArray()\n  }\n\n  async addProjectFolder(project: ProjectFolder) {\n    await db.projectFolders.add(project)\n  }\n\n  async updateProjectFolder(id: string, title: string) {\n    await db.projectFolders.update(id, { title })\n  }\n\n  async deleteProjectFolder(id: string) {\n    await db.transaction(\n      \"rw\",\n      [db.projectFolders, db.chatHistories],\n      async () => {\n        await db.projectFolders.delete(id)\n        await db.chatHistories\n          .where(\"folder_id\")\n          .equals(id)\n          .modify({ folder_id: undefined })\n      }\n    )\n  }\n\n  async assignHistoryToFolder(history_id: string, folder_id?: string) {\n    await db.chatHistories.update(history_id, { folder_id })\n  }\n\n  async getSessionFilesInfo(sessionId: string): Promise<SessionFiles | null> {\n    const sessionFiles = await db.sessionFiles.get(sessionId)\n    return sessionFiles || null\n  }\n\n  async addFileToSession(sessionId: string, file: UploadedFile) {\n    const sessionFiles = await this.getSessionFilesInfo(sessionId)\n    const updatedFiles = sessionFiles ? [...sessionFiles.files, file] : [file]\n    const sessionData: SessionFiles = {\n      sessionId,\n      files: updatedFiles,\n      retrievalEnabled: sessionFiles?.retrievalEnabled || false,\n      createdAt: sessionFiles?.createdAt || Date.now()\n    }\n    await db.sessionFiles.put(sessionData)\n  }\n\n  async removeFileFromSession(sessionId: string, fileId: string) {\n    const sessionFiles = await this.getSessionFilesInfo(sessionId)\n    if (sessionFiles) {\n      const updatedFiles = sessionFiles.files.filter((f) => f.id !== fileId)\n      const sessionData: SessionFiles = {\n        ...sessionFiles,\n        files: updatedFiles\n      }\n      await db.sessionFiles.put(sessionData)\n    }\n  }\n\n  async updateFileInSession(\n    sessionId: string,\n    fileId: string,\n    updates: Partial<UploadedFile>\n  ) {\n    const sessionFiles = await this.getSessionFilesInfo(sessionId)\n    if (sessionFiles) {\n      const updatedFiles = sessionFiles.files.map((f) =>\n        f.id === fileId ? { ...f, ...updates } : f\n      )\n      const sessionData: SessionFiles = {\n        ...sessionFiles,\n        files: updatedFiles\n      }\n      await db.sessionFiles.put(sessionData)\n    }\n  }\n\n  async setRetrievalEnabled(sessionId: string, enabled: boolean) {\n    const sessionFiles = await this.getSessionFilesInfo(sessionId)\n    const sessionData: SessionFiles = {\n      sessionId,\n      files: sessionFiles?.files || [],\n      retrievalEnabled: enabled,\n      createdAt: sessionFiles?.createdAt || Date.now()\n    }\n    await db.sessionFiles.put(sessionData)\n  }\n\n  async clearSessionFiles(sessionId: string) {\n    await db.sessionFiles.delete(sessionId)\n  }\n\n  async getChatHistory(id: string): Promise<MessageHistory> {\n    const modelNicknames = await getAllModelNicknames()\n    const messages = await db.messages.where(\"history_id\").equals(id).toArray()\n\n    return messages.map((message) => {\n      return {\n        ...message,\n        modelName: modelNicknames[message.name]?.model_name || message.name,\n        modelImage: modelNicknames[message.name]?.model_avatar || undefined\n      }\n    })\n  }\n\n  async getChatHistories(): Promise<ChatHistory> {\n    return await db.chatHistories.orderBy(\"createdAt\").reverse().toArray()\n  }\n\n  async fullTextSearchChatHistories(query: string): Promise<ChatHistory> {\n    const normalizedQuery = query.toLowerCase().trim()\n    if (!normalizedQuery) {\n      return this.getChatHistories()\n    }\n\n    const titleMatches = await db.chatHistories\n      .where(\"title\")\n      .startsWithIgnoreCase(normalizedQuery)\n      .or(\"title\")\n      .anyOfIgnoreCase(normalizedQuery.split(\" \"))\n      .toArray()\n\n    const messageMatches = await db.messages\n      .filter((message) =>\n        searchQueryInContent(message.content, normalizedQuery)\n      )\n      .toArray()\n\n    const historyIdsFromMessages = [\n      ...new Set(messageMatches.map((msg) => msg.history_id))\n    ]\n\n    const historiesFromMessages = await db.chatHistories\n      .where(\"id\")\n      .anyOf(historyIdsFromMessages)\n      .toArray()\n\n    const allMatches = [...titleMatches, ...historiesFromMessages]\n    const uniqueHistories = allMatches.filter(\n      (history, index, self) =>\n        index === self.findIndex((h) => h.id === history.id)\n    )\n\n    return uniqueHistories.sort((a, b) => b.createdAt - a.createdAt)\n  }\n\n  async getChatHistoryTitleById(id: string): Promise<string> {\n    const chatHistory = await db.chatHistories.get(id)\n    return chatHistory?.title || \"\"\n  }\n\n  async getHistoryInfo(id: string): Promise<HistoryInfo> {\n    return db.chatHistories.get(id)\n  }\n\n  async addChatHistory(history: HistoryInfo) {\n    await db.chatHistories.add(history)\n  }\n\n  async updateChatHistoryCreatedAt(id: string, createdAt: number) {\n    await db.chatHistories.update(id, { createdAt })\n  }\n\n  async addMessage(message: Message) {\n    await db.messages.add(message)\n  }\n\n  async updateMessage(history_id: string, message_id: string, content: string) {\n    await db.messages.where(\"id\").equals(message_id).modify({ content })\n  }\n\n  async removeChatHistory(id: string) {\n    await db.chatHistories.delete(id)\n  }\n\n  async removeMessage(history_id: string, message_id: string) {\n    await db.messages.delete(message_id)\n  }\n\n  async updateLastUsedModel(history_id: string, model_id: string) {\n    await db.chatHistories.update(history_id, { model_id })\n  }\n\n  async updateLastUsedPrompt(\n    history_id: string,\n    usedPrompt: LastUsedModelType\n  ) {\n    await db.chatHistories.update(history_id, { last_used_prompt: usedPrompt })\n  }\n\n  async clear() {\n    await db.delete()\n    await db.open()\n  }\n\n  async deleteChatHistory(id: string) {\n    await db.transaction(\"rw\", [db.chatHistories, db.messages], async () => {\n      await db.chatHistories.delete(id)\n      await db.messages.where(\"history_id\").equals(id).delete()\n    })\n  }\n\n  async deleteAllChatHistory() {\n    await db.transaction(\"rw\", [db.chatHistories, db.messages], async () => {\n      await db.chatHistories.clear()\n      await db.messages.clear()\n    })\n  }\n\n  async clearDB() {\n    await db.delete()\n    await db.open()\n  }\n\n  async deleteMessage(history_id: string) {\n    await db.messages.where(\"history_id\").equals(history_id).delete()\n  }\n  async getChatHistoriesPaginated(\n    page: number = 1,\n    searchQuery?: string\n  ): Promise<{\n    histories: ChatHistory\n    hasMore: boolean\n    totalCount: number\n  }> {\n    const offset = (page - 1) * PAGE_SIZE\n\n    if (searchQuery) {\n      console.log(\"Searching chat histories with query:\", searchQuery)\n      const allResults = await this.fullTextSearchChatHistories(searchQuery)\n      const paginatedResults = allResults.slice(offset, offset + PAGE_SIZE)\n      console.log(\"Paginated search results:\", paginatedResults)\n      return {\n        histories: paginatedResults,\n        hasMore: offset + PAGE_SIZE < allResults.length,\n        totalCount: allResults.length\n      }\n    }\n\n    if (page === 1) {\n      const histories = await db.chatHistories\n        .orderBy(\"createdAt\")\n        .reverse()\n        .limit(PAGE_SIZE)\n        .toArray()\n\n      const totalCount = await db.chatHistories.count()\n\n      return {\n        histories,\n        hasMore: histories.length === PAGE_SIZE,\n        totalCount\n      }\n    } else {\n      const skipCount = offset\n      const histories = await db.chatHistories\n        .orderBy(\"createdAt\")\n        .reverse()\n        .offset(skipCount)\n        .limit(PAGE_SIZE)\n        .toArray()\n\n      const totalCount = await db.chatHistories.count()\n\n      return {\n        histories,\n        hasMore: offset + PAGE_SIZE < totalCount,\n        totalCount\n      }\n    }\n  }\n  async getChatHistoriesPaginatedOptimized(\n    lastEntry?: any,\n    searchQuery?: string\n  ): Promise<{\n    histories: ChatHistory\n    hasMore: boolean\n  }> {\n    if (searchQuery) {\n      const allResults = await this.fullTextSearchChatHistories(searchQuery)\n      return {\n        histories: allResults.slice(0, PAGE_SIZE),\n        hasMore: allResults.length > PAGE_SIZE\n      }\n    }\n\n    if (!lastEntry) {\n      const histories = await db.chatHistories\n        .orderBy(\"createdAt\")\n        .reverse()\n        .limit(PAGE_SIZE)\n        .toArray()\n\n      return {\n        histories,\n        hasMore: histories.length === PAGE_SIZE\n      }\n    } else {\n      const histories = await db.chatHistories\n        .where(\"createdAt\")\n        .belowOrEqual(lastEntry.createdAt)\n        .filter(fastForward(lastEntry, \"id\"))\n        .limit(PAGE_SIZE)\n        .reverse()\n        .toArray()\n\n      return {\n        histories,\n        hasMore: histories.length === PAGE_SIZE\n      }\n    }\n  }\n\n  // Prompts Methods\n  async getAllPrompts(): Promise<Prompts> {\n    return await db.prompts.orderBy(\"createdAt\").reverse().toArray()\n  }\n\n  async addPrompt(prompt: Prompt) {\n    await db.prompts.add(prompt)\n  }\n\n  async deletePrompt(id: string) {\n    await db.prompts.delete(id)\n  }\n\n  async updatePrompt(\n    id: string,\n    title: string,\n    content: string,\n    is_system: boolean\n  ) {\n    await db.prompts.update(id, { title, content, is_system })\n  }\n\n  async getPromptById(id: string): Promise<Prompt | undefined> {\n    return await db.prompts.get(id)\n  }\n\n  // Webshare Methods\n  async getWebshare(id: string) {\n    return await db.webshares.get(id)\n  }\n\n  async getAllWebshares(): Promise<Webshare[]> {\n    return await db.webshares.orderBy(\"createdAt\").reverse().toArray()\n  }\n\n  async addWebshare(webshare: Webshare) {\n    await db.webshares.add(webshare)\n  }\n\n  async deleteWebshare(id: string) {\n    await db.webshares.delete(id)\n  }\n\n  // User Settings Methods\n  async getUserID(): Promise<string> {\n    const userSettings = await db.userSettings.get(\"main\")\n    return userSettings?.user_id || \"\"\n  }\n\n  async setUserID(id: string) {\n    await db.userSettings.put({ id: \"main\", user_id: id })\n  }\n\n  async importChatHistoryV2(\n    data: any[],\n    options: {\n      replaceExisting?: boolean\n      mergeData?: boolean\n    } = {}\n  ) {\n    const { replaceExisting = false, mergeData = true } = options\n\n    if (!mergeData && !replaceExisting) {\n      // Clear existing data\n      await this.deleteAllChatHistory()\n    }\n\n    for (const item of data) {\n      if (item.history) {\n        await db.chatHistories.put(item.history)\n      }\n\n      if (item.messages) {\n        for (const message of item.messages) {\n          const existingMessage = await db.messages.get(message.id)\n\n          if (existingMessage && !replaceExisting) {\n            continue\n          }\n\n          await db.messages.put(message)\n        }\n      }\n    }\n  }\n\n  async importPromptsV2(\n    data: Prompt[],\n    options: {\n      replaceExisting?: boolean\n      mergeData?: boolean\n    } = {}\n  ) {\n    const { replaceExisting = false, mergeData = true } = options\n\n    if (!mergeData && !replaceExisting) {\n      await db.prompts.clear()\n    }\n\n    for (const prompt of data) {\n      const existingPrompt = await db.prompts.get(prompt.id)\n\n      if (existingPrompt && !replaceExisting) {\n        continue\n      }\n\n      await db.prompts.put(prompt)\n    }\n  }\n\n  async importSessionFilesV2(\n    data: SessionFiles[],\n    options: {\n      replaceExisting?: boolean\n      mergeData?: boolean\n    } = {}\n  ) {\n    const { replaceExisting = false, mergeData = true } = options\n\n    if (!mergeData && !replaceExisting) {\n      await db.sessionFiles.clear()\n    }\n\n    for (const sessionFile of data) {\n      const existingSessionFile = await db.sessionFiles.get(\n        sessionFile.sessionId\n      )\n\n      if (existingSessionFile && !replaceExisting) {\n        if (mergeData) {\n          // Merge files arrays\n          const mergedFiles = [...existingSessionFile.files]\n          for (const newFile of sessionFile.files) {\n            if (!mergedFiles.find((f) => f.id === newFile.id)) {\n              mergedFiles.push(newFile)\n            }\n          }\n          await db.sessionFiles.put({\n            ...existingSessionFile,\n            files: mergedFiles\n          })\n        }\n        continue\n      }\n\n      await db.sessionFiles.put(sessionFile)\n    }\n  }\n}\n"
  },
  {
    "path": "src/db/dexie/firefox-sync.ts",
    "content": "import { bulkAddPromptsFB } from \"../index\"\nimport { bulkAddModelsFB } from \"../models\"\nimport { bulkAddOAIFB } from \"../openai\"\nimport { db } from \"./schema\"\n\nexport const firefoxSyncDataForPrivateMode = async () => {\n\n  const allPrompts = await db.prompts.toArray()\n  const customModels = await db.customModels.toArray()\n  const oaiConfigs = await db.openaiConfigs.toArray()\n\n  await bulkAddPromptsFB(allPrompts)\n  await bulkAddModelsFB(customModels)\n  await bulkAddOAIFB(oaiConfigs)\n}\n"
  },
  {
    "path": "src/db/dexie/helpers.ts",
    "content": "import {\n  type ChatHistory as ChatHistoryType,\n  type Message as MessageType\n} from \"~/store/option\"\nimport { ChatDocuments } from \"@/models/ChatTypes\"\nimport { isConversationMessage } from \"@/libs/mcp/utils\"\nimport {\n  type HistoryInfo,\n  type MessageHistory,\n  type Message,\n  type Prompts,\n  type UploadedFile,\n  type SessionFiles,\n  type Webshare,\n  Prompt,\n  LastUsedModelType,\n  OpenAIModelConfigs,\n  ModelNicknames,\n  Models,\n  ProjectFolder,\n  ProjectFolders\n} from \"./types\"\nimport { PageAssistDatabase } from \"./chat\"\nimport { db as chatDB } from \"./schema\"\nimport {\n  deletePromptByIdFB,\n  getAllPromptsFB,\n  getPromptByIdFB,\n  savePromptFB,\n  updatePromptFB\n} from \"..\"\nimport { OpenAIModelDb } from \"./openai\"\nimport { ModelNickname } from \"./nickname\"\nimport { ModelDb } from \"./models\"\nimport { exportMcpServers, importMcpServersV2 } from \"./mcp\"\n\n// Helper function to generate IDs (keeping the same format)\nexport const generateID = () => {\n  return \"pa_xxxx-xxxx-xxx-xxxx\".replace(/[x]/g, () => {\n    const r = Math.floor(Math.random() * 16)\n    return r.toString(16)\n  })\n}\n\n// Chat History Functions\nexport const saveHistory = async (\n  title: string,\n  is_rag?: boolean,\n  message_source?: \"copilot\" | \"web-ui\" | \"branch\",\n  doc_id?: string\n) => {\n  const id = generateID()\n  const createdAt = Date.now()\n  const history: HistoryInfo = {\n    id,\n    title: title?.trim()?.length > 0 ? title : \"Untitled Chat\",\n    createdAt,\n    is_rag: is_rag || false,\n    message_source,\n    doc_id\n  }\n  const db = new PageAssistDatabase()\n  await db.addChatHistory(history)\n  return history\n}\nexport const updateChatHistoryCreatedAt = async (history_id: string) => {\n  const createdAt = Date.now()\n  const db = new PageAssistDatabase()\n  await db.updateChatHistoryCreatedAt(history_id, createdAt)\n}\n\nexport const updateMessage = async (\n  history_id: string,\n  message_id: string,\n  content: string\n) => {\n  const db = new PageAssistDatabase()\n  await db.updateMessage(history_id, message_id, content)\n}\n\nexport const saveMessage = async ({\n  content,\n  history_id,\n  name,\n  role,\n  images,\n  source,\n  generationInfo,\n  message_type,\n  modelImage,\n  modelName,\n  reasoning_time_taken,\n  time,\n  documents,\n  messageKind,\n  toolCalls,\n  toolCallId,\n  toolName,\n  toolServerName,\n  toolError\n}: {\n  history_id: string\n  name: string\n  role: string\n  content: string\n  images: string[]\n  source?: any[]\n  time?: number\n  message_type?: string\n  generationInfo?: any\n  reasoning_time_taken?: number\n  modelName?: string\n  modelImage?: string\n  documents?: ChatDocuments\n  messageKind?: Message[\"messageKind\"]\n  toolCalls?: Message[\"toolCalls\"]\n  toolCallId?: string\n  toolName?: string\n  toolServerName?: string\n  toolError?: boolean\n}) => {\n  const id = generateID()\n  let createdAt = Date.now()\n  if (time) {\n    createdAt += time\n  }\n  const message: Message = {\n    id,\n    history_id,\n    name,\n    role,\n    content,\n    images,\n    createdAt,\n    sources: source,\n    messageType: message_type,\n    generationInfo: generationInfo,\n    reasoning_time_taken,\n    modelName,\n    modelImage,\n    documents,\n    messageKind,\n    toolCalls,\n    toolCallId,\n    toolName,\n    toolServerName,\n    toolError\n  }\n  const db = new PageAssistDatabase()\n  await db.addMessage(message)\n  return message\n}\n\nexport const formatToChatHistory = (\n  messages: MessageHistory\n): ChatHistoryType => {\n  messages.sort((a, b) => a.createdAt - b.createdAt)\n  return messages.map((message) => {\n    return {\n      content: message.content,\n      role: message.role as \"user\" | \"assistant\" | \"system\" | \"tool\",\n      image:\n        message.images && message.images.length > 0\n          ? message.images[0]\n          : undefined,\n      images: message.images,\n      messageType: message.messageType,\n      messageKind: message.messageKind,\n      toolCalls: message.toolCalls,\n      toolCallId: message.toolCallId,\n      toolName: message.toolName,\n      toolServerName: message.toolServerName,\n      toolError: message.toolError\n    }\n  })\n}\n\nexport const formatToConversationHistory = (\n  messages: MessageHistory\n): ChatHistoryType =>\n  formatToChatHistory(\n    messages.filter((message) =>\n      isConversationMessage({\n        role: message.role,\n        messageKind: message.messageKind\n      })\n    )\n  )\n\nexport const formatToMessage = (messages: MessageHistory): MessageType[] => {\n  messages.sort((a, b) => a.createdAt - b.createdAt)\n  return messages.map((message) => {\n    return {\n      isBot: message.role !== \"user\",\n      message: message.content,\n      name: message.name,\n      sources: message?.sources || [],\n      images: message.images || [],\n      generationInfo: message?.generationInfo,\n      reasoning_time_taken: message?.reasoning_time_taken,\n      modelName: message?.modelName,\n      modelImage: message?.modelImage,\n      id: message.id,\n      documents: message?.documents,\n      messageType: message?.messageType,\n      messageKind: message?.messageKind,\n      toolCalls: message?.toolCalls,\n      toolCallId: message?.toolCallId,\n      toolName: message?.toolName,\n      toolServerName: message?.toolServerName,\n      toolError: message?.toolError\n    }\n  })\n}\n\nexport const deleteByHistoryId = async (history_id: string) => {\n  const db = new PageAssistDatabase()\n  await db.deleteMessage(history_id)\n  await db.removeChatHistory(history_id)\n  return history_id\n}\n\nexport const updateHistory = async (id: string, title: string) => {\n  await chatDB.chatHistories.update(id, { title })\n}\n\nexport const pinHistory = async (id: string, is_pinned: boolean) => {\n  await chatDB.chatHistories.update(id, { is_pinned })\n}\n\nexport const removeMessageUsingHistoryId = async (history_id: string) => {\n  const db = new PageAssistDatabase()\n  const chatHistory = await db.getChatHistory(history_id)\n  if (chatHistory.length > 0) {\n    const sortedHistory = chatHistory.sort((a, b) => a.createdAt - b.createdAt)\n    const lastUserIndex = sortedHistory.findLastIndex(\n      (message) => message.role === \"user\"\n    )\n\n    if (lastUserIndex === -1) {\n      return\n    }\n\n    const messagesToDelete = sortedHistory.slice(lastUserIndex + 1)\n    for (const message of messagesToDelete) {\n      await db.removeMessage(history_id, message.id)\n    }\n  }\n}\n\nexport const updateMessageByIndex = async (\n  history_id: string,\n  index: number,\n  message: string\n) => {\n  try {\n    const db = new PageAssistDatabase()\n    const chatHistory = await db.getChatHistory(history_id)\n    const sortedHistory = chatHistory.sort((a, b) => a.createdAt - b.createdAt)\n    if (sortedHistory[index]) {\n      await db.updateMessage(history_id, sortedHistory[index].id, message)\n    }\n  } catch (e) {\n    // temp chat will break\n  }\n}\n\nexport const deleteChatForEdit = async (history_id: string, index: number) => {\n  const db = new PageAssistDatabase()\n  const chatHistory = await db.getChatHistory(history_id)\n  const sortedHistory = chatHistory.sort((a, b) => a.createdAt - b.createdAt)\n\n  // Delete messages after the specified index\n  const messagesToDelete = sortedHistory.slice(index + 1)\n  for (const message of messagesToDelete) {\n    await db.removeMessage(history_id, message.id)\n  }\n}\n\n// Prompt Functions\nexport const getAllPrompts = async () => {\n  try {\n    const db = new PageAssistDatabase()\n    return await db.getAllPrompts()\n  } catch (e) {\n    if (isDatabaseClosedError(e)) {\n      return await getAllPromptsFB()\n    }\n\n    return []\n  }\n}\n\nexport const getAllPromptsSystem = async () => {\n  try {\n    const db = new PageAssistDatabase()\n    return await db.getAllPrompts()\n  } catch (e) {\n    if (isDatabaseClosedError(e)) {\n      return await getAllPromptsFB()\n    }\n\n    return []\n  }\n}\n\nexport const savePrompt = async ({\n  content,\n  title,\n  is_system = false\n}: {\n  title: string\n  content: string\n  is_system: boolean\n}) => {\n  const db = new PageAssistDatabase()\n  const id = generateID()\n  const createdAt = Date.now()\n  const prompt = { id, title, content, is_system, createdAt }\n  await db.addPrompt(prompt)\n  await savePromptFB(prompt)\n  return prompt\n}\n\nexport const deletePromptById = async (id: string) => {\n  const db = new PageAssistDatabase()\n  await db.deletePrompt(id)\n  await deletePromptByIdFB(id)\n  return id\n}\n\nexport const updatePrompt = async ({\n  content,\n  id,\n  title,\n  is_system\n}: {\n  id: string\n  title: string\n  content: string\n  is_system: boolean\n}) => {\n  const db = new PageAssistDatabase()\n  await db.updatePrompt(id, title, content, is_system)\n  await updatePromptFB({\n    id,\n    title,\n    content,\n    is_system\n  })\n  return id\n}\n\nexport const getPromptById = async (id: string) => {\n  try {\n    if (!id || id.trim() === \"\") return null\n    const db = new PageAssistDatabase()\n    return await db.getPromptById(id)\n  } catch (e) {\n    if (isDatabaseClosedError(e)) {\n      return await getPromptByIdFB(id)\n    }\n    return null\n  }\n}\n\n// Webshare Functions\nexport const getAllWebshares = async () => {\n  try {\n    const db = new PageAssistDatabase()\n    return await db.getAllWebshares()\n  } catch (e) {\n    return []\n  }\n}\n\nexport const deleteWebshare = async (id: string) => {\n  const db = new PageAssistDatabase()\n  await db.deleteWebshare(id)\n  return id\n}\n\nexport const saveWebshare = async ({\n  title,\n  url,\n  api_url,\n  share_id\n}: {\n  title: string\n  url: string\n  api_url: string\n  share_id: string\n}) => {\n  const db = new PageAssistDatabase()\n  const id = generateID()\n  const createdAt = Date.now()\n  const webshare: Webshare = { id, title, url, share_id, createdAt, api_url }\n  await db.addWebshare(webshare)\n  return webshare\n}\n\n// User Functions\nexport const getUserId = async () => {\n  const db = new PageAssistDatabase()\n  const id = await db.getUserID()\n  if (!id || id?.trim() === \"\") {\n    const user_id = \"user_xxxx-xxxx-xxx-xxxx-xxxx\".replace(/[x]/g, () => {\n      const r = Math.floor(Math.random() * 16)\n      return r.toString(16)\n    })\n    await db.setUserID(user_id)\n    return user_id\n  }\n  return id\n}\n\n// Export/Import Functions\nexport const exportChatHistory = async () => {\n  const db = new PageAssistDatabase()\n  const chatHistories = await db.getChatHistories()\n  const messages = await Promise.all(\n    chatHistories.map(async (history) => {\n      const messages = await db.getChatHistory(history.id)\n      return { history, messages }\n    })\n  )\n  return messages\n}\n\nexport const importChatHistory = async (\n  data: {\n    history: HistoryInfo\n    messages: MessageHistory\n  }[]\n) => {\n  const db = new PageAssistDatabase()\n  for (const { history, messages } of data) {\n    await db.addChatHistory(history)\n    for (const message of messages) {\n      await db.addMessage(message)\n    }\n  }\n}\n\nexport const exportPrompts = async () => {\n  const db = new PageAssistDatabase()\n  return await db.getAllPrompts()\n}\n\nexport const exportOAIConfigs = async () => {\n  const db = new OpenAIModelDb()\n  return await db.getAll()\n}\n\nexport const exportNicknames = async () => {\n  const modelNickname = new ModelNickname()\n  const data = await modelNickname.getAllModelNicknames()\n  return data\n}\n\nexport const exportModels = async () => {\n  const db = new ModelDb()\n  return db.getAll()\n}\n\nexport const importNicknamesV2 = async (\n  nicknames: ModelNicknames,\n  options: {\n    replaceExisting?: boolean\n    mergeData?: boolean\n  } = {}\n) => {\n  const db = new ModelNickname()\n  await db.importDataV2(nicknames, options)\n}\n\nexport const importModelsV2 = async (\n  models: Models,\n  options: {\n    replaceExisting?: boolean\n    mergeData?: boolean\n  } = {}\n) => {\n  const db = new ModelDb()\n  await db.importDataV2(models, options)\n}\n\nexport const importPrompts = async (prompts: Prompts) => {\n  const db = new PageAssistDatabase()\n  for (const prompt of prompts) {\n    await db.addPrompt(prompt)\n  }\n}\n\nexport const importOAIConfigs = async (configs: OpenAIModelConfigs) => {\n  const db = new OpenAIModelDb()\n  for (const config of configs) {\n    await db.create(config)\n  }\n}\n\n// Utility Functions\nexport const getRecentChatFromCopilot = async () => {\n  const db = new PageAssistDatabase()\n  const chatHistories = await db.getChatHistories()\n  if (chatHistories.length === 0) return null\n  const history = chatHistories.find(\n    (history) => history.message_source === \"copilot\"\n  )\n  if (!history) return null\n\n  const messages = await db.getChatHistory(history.id)\n\n  return { history, messages }\n}\n\nexport const getRecentChatFromWebUI = async () => {\n  const db = new PageAssistDatabase()\n  const chatHistories = await db.getChatHistories()\n  if (chatHistories.length === 0) return null\n  const history = chatHistories.find(\n    (history) => history.message_source === \"web-ui\"\n  )\n  if (!history) return null\n\n  const messages = await db.getChatHistory(history.id)\n\n  return { history, messages }\n}\n\nexport const getTitleById = async (id: string) => {\n  const db = new PageAssistDatabase()\n  const title = await db.getChatHistoryTitleById(id)\n  return title\n}\n\nexport const getLastChatHistory = async (history_id: string) => {\n  const db = new PageAssistDatabase()\n  const messages = await db.getChatHistory(history_id)\n  messages.sort((a, b) => a.createdAt - b.createdAt)\n  const lastAssistantMessage = messages.findLast(\n    (message) =>\n      message.role === \"assistant\" &&\n      (!message.messageKind || message.messageKind === \"text\")\n  )\n\n  return lastAssistantMessage\n}\n\nexport const deleteHistoriesByDateRange = async (\n  rangeLabel: string\n): Promise<string[]> => {\n  const db = new PageAssistDatabase()\n  const allHistories = await db.getChatHistories()\n  const now = new Date()\n  const today = new Date(now.setHours(0, 0, 0, 0))\n  const yesterday = new Date(today)\n  yesterday.setDate(yesterday.getDate() - 1)\n  const lastWeek = new Date(today)\n  lastWeek.setDate(lastWeek.getDate() - 7)\n  let historiesToDelete: HistoryInfo[] = []\n\n  switch (rangeLabel) {\n    case \"today\":\n      historiesToDelete = allHistories.filter(\n        (item) => !item.is_pinned && new Date(item?.createdAt) >= today\n      )\n      break\n    case \"yesterday\":\n      historiesToDelete = allHistories.filter(\n        (item) =>\n          !item.is_pinned &&\n          new Date(item?.createdAt) >= yesterday &&\n          new Date(item?.createdAt) < today\n      )\n      break\n    case \"last7Days\":\n      historiesToDelete = allHistories.filter(\n        (item) =>\n          !item.is_pinned &&\n          new Date(item?.createdAt) >= lastWeek &&\n          new Date(item?.createdAt) < yesterday\n      )\n      break\n    case \"older\":\n      historiesToDelete = allHistories.filter(\n        (item) => !item.is_pinned && new Date(item?.createdAt) < lastWeek\n      )\n      break\n    case \"pinned\":\n      historiesToDelete = allHistories.filter((item) => item.is_pinned)\n      break\n    default:\n      return []\n  }\n\n  const deletedIds: string[] = []\n  for (const history of historiesToDelete) {\n    await db.deleteMessage(history.id)\n    await db.removeChatHistory(history.id)\n    deletedIds.push(history.id)\n  }\n\n  return deletedIds\n}\n\n// Session Files Helper Functions\nexport const getSessionFiles = async (\n  sessionId: string\n): Promise<UploadedFile[]> => {\n  const db = new PageAssistDatabase()\n  return await db.getSessionFiles(sessionId)\n}\n\nexport const addFileToSession = async (\n  sessionId: string,\n  file: UploadedFile\n) => {\n  const db = new PageAssistDatabase()\n  await db.addFileToSession(sessionId, file)\n}\n\nexport const removeFileFromSession = async (\n  sessionId: string,\n  fileId: string\n) => {\n  const db = new PageAssistDatabase()\n  await db.removeFileFromSession(sessionId, fileId)\n}\n\nexport const updateFileInSession = async (\n  sessionId: string,\n  fileId: string,\n  updates: Partial<UploadedFile>\n) => {\n  const db = new PageAssistDatabase()\n  await db.updateFileInSession(sessionId, fileId, updates)\n}\n\nexport const setRetrievalEnabled = async (\n  sessionId: string,\n  enabled: boolean\n) => {\n  const db = new PageAssistDatabase()\n  await db.setRetrievalEnabled(sessionId, enabled)\n}\n\nexport const getSessionFilesInfo = async (\n  sessionId: string\n): Promise<SessionFiles | null> => {\n  const db = new PageAssistDatabase()\n  return await db.getSessionFilesInfo(sessionId)\n}\n\nexport const clearSessionFiles = async (sessionId: string) => {\n  const db = new PageAssistDatabase()\n  await db.clearSessionFiles(sessionId)\n}\nexport const importChatHistoryV2 = async (\n  data: any[],\n  options: {\n    replaceExisting?: boolean\n    mergeData?: boolean\n  } = {}\n) => {\n  const chatDb = new PageAssistDatabase()\n  return chatDb.importChatHistoryV2(data, options)\n}\n\nexport const importPromptsV2 = async (\n  data: Prompt[],\n  options: {\n    replaceExisting?: boolean\n    mergeData?: boolean\n  } = {}\n) => {\n  const chatDb = new PageAssistDatabase()\n  return chatDb.importPromptsV2(data, options)\n}\n\nexport const importOAIConfigsV2 = async (\n  data: OpenAIModelConfigs,\n  options: {\n    replaceExisting?: boolean\n    mergeData?: boolean\n  } = {}\n) => {\n  const db = new OpenAIModelDb()\n  return db.importDataV2(data, options)\n}\n\nexport { exportMcpServers, importMcpServersV2 }\n\nexport const getProjectFolders = async (): Promise<ProjectFolders> => {\n  const db = new PageAssistDatabase()\n  return db.getProjectFolders()\n}\n\nexport const addProjectFolder = async (\n  title: string\n): Promise<ProjectFolder> => {\n  const db = new PageAssistDatabase()\n  const id = generateID()\n  const createdAt = Date.now()\n  const folder = { id, title, createdAt }\n  await db.addProjectFolder(folder)\n  return folder\n}\n\nexport const updateProjectFolder = async (\n  id: string,\n  title: string\n) => {\n  const db = new PageAssistDatabase()\n  await db.updateProjectFolder(id, title)\n}\n\nexport const deleteProjectFolder = async (id: string) => {\n  const db = new PageAssistDatabase()\n  await db.deleteProjectFolder(id)\n}\n\nexport const assignHistoryToFolder = async (\n  history_id: string,\n  folder_id?: string\n) => {\n  const db = new PageAssistDatabase()\n  await db.assignHistoryToFolder(history_id, folder_id)\n}\n\nexport const updateLastUsedModel = async (\n  history_id: string,\n  model_id: string\n) => {\n  const chatDb = new PageAssistDatabase()\n  return chatDb.updateLastUsedModel(history_id, model_id)\n}\n\nexport const updateLastUsedPrompt = async (\n  history_id: string,\n  usedPrompt: LastUsedModelType\n) => {\n  const chatDb = new PageAssistDatabase()\n  return chatDb.updateLastUsedPrompt(history_id, usedPrompt)\n}\n"
  },
  {
    "path": "src/db/dexie/knowledge.ts",
    "content": "import { db } from \"./schema\"\nimport { Knowledge, Source } from \"./types\"\nimport { deleteVector, deleteVectorByFileId } from \"./vector\"\n\nexport const generateID = () => {\n  return \"pa_knowledge_xxxx-xxxx-xxx-xxxx\".replace(/[x]/g, () => {\n    const r = Math.floor(Math.random() * 16)\n    return r.toString(16)\n  })\n}\n\nexport class PageAssistKnowledge {\n  async getAll(): Promise<Knowledge[]> {\n    return await db.knowledge.orderBy(\"createdAt\").reverse().toArray()\n  }\n\n  async getById(id: string): Promise<Knowledge | undefined> {\n    return await db.knowledge.get(id)\n  }\n\n  async create(knowledge: Knowledge): Promise<void> {\n    await db.knowledge.add(knowledge)\n  }\n\n  async update(knowledge: Knowledge): Promise<void> {\n    await db.knowledge.put(knowledge)\n  }\n\n  async delete(id: string): Promise<void> {\n    await db.knowledge.delete(id)\n  }\n\n  async deleteSource(id: string, source_id: string): Promise<void> {\n    const knowledge = await this.getById(id)\n    if (knowledge) {\n      knowledge.source = knowledge.source.filter(\n        (s) => s.source_id !== source_id\n      )\n      await this.update(knowledge)\n    }\n  }\n\n  async importDataV2(\n    data: Knowledge[],\n    options: {\n      replaceExisting?: boolean\n      mergeData?: boolean\n    } = {}\n  ): Promise<void> {\n    const { replaceExisting = false, mergeData = true } = options\n\n    if (!mergeData && !replaceExisting) {\n      await db.knowledge.clear()\n    }\n\n    for (const knowledge of data) {\n      const existingKnowledge = await this.getById(knowledge.id)\n\n      if (existingKnowledge && !replaceExisting) {\n        if (mergeData) {\n          const mergedSources = [...existingKnowledge.source]\n          for (const newSource of knowledge.source) {\n            if (\n              !mergedSources.find((s) => s.source_id === newSource.source_id)\n            ) {\n              mergedSources.push(newSource)\n            }\n          }\n          await this.update({\n            ...existingKnowledge,\n            source: mergedSources\n          })\n        }\n        continue\n      }\n\n      await this.create(knowledge)\n    }\n  }\n}\nexport const importKnowledgeV2 = async (\n  data: Knowledge[],\n  options: {\n    replaceExisting?: boolean\n    mergeData?: boolean\n  } = {}\n) => {\n  try {\n    const { replaceExisting = false, mergeData = true } = options\n    const knowledgeDb = new PageAssistKnowledge()\n\n    if (!mergeData && !replaceExisting) {\n      await db.knowledge.clear()\n    }\n\n    for (const knowledge of data) {\n      const existingKnowledge = await knowledgeDb.getById(knowledge.id)\n\n      if (existingKnowledge && !replaceExisting) {\n        if (mergeData) {\n          // Merge sources arrays, avoiding duplicates\n          const mergedSources = [...existingKnowledge.source]\n          for (const newSource of knowledge.source) {\n            if (\n              !mergedSources.find((s) => s.source_id === newSource.source_id)\n            ) {\n              mergedSources.push(newSource)\n            }\n          }\n          await knowledgeDb.update({\n            ...existingKnowledge,\n            source: mergedSources\n          })\n        }\n        continue\n      }\n\n      await knowledgeDb.create(knowledge)\n    }\n  } catch (e) {\n    console.warn(e)\n  }\n}\n// Helper functions that match the original API\nexport const createKnowledge = async ({\n  source,\n  title,\n  embedding_model\n}: {\n  title: string\n  source: Source[]\n  embedding_model: string\n}) => {\n  const db = new PageAssistKnowledge()\n  const id = generateID()\n  const knowledge: Knowledge = {\n    id,\n    title,\n    db_type: \"knowledge\",\n    source,\n    status: \"pending\",\n    knownledge: {},\n    embedding_model,\n    createdAt: Date.now()\n  }\n  await db.create(knowledge)\n  return knowledge\n}\n\nexport const getKnowledgeById = async (id: string) => {\n  const db = new PageAssistKnowledge()\n  return db.getById(id)\n}\n\nexport const updateKnowledgeStatus = async (id: string, status: string) => {\n  const db = new PageAssistKnowledge()\n  const knowledge = await db.getById(id)\n  if (knowledge) {\n    if (status === \"finished\") {\n      knowledge.source = knowledge?.source?.map((e) => ({\n        ...e,\n        content: undefined\n      }))\n    }\n    await db.update({\n      ...knowledge,\n      status\n    })\n  }\n}\n\nexport const addNewSources = async (id: string, source: Source[]) => {\n  await updateKnowledgeStatus(id, \"processing\")\n  const db = new PageAssistKnowledge()\n  const knowledge = await db.getById(id)\n  if (knowledge) {\n    await db.update({\n      ...knowledge,\n      source: [...knowledge.source, ...source]\n    })\n  }\n}\n\nexport const getAllKnowledge = async (status?: string) => {\n  try {\n    const db = new PageAssistKnowledge()\n    const data = await db.getAll()\n\n    if (status) {\n      return data\n        .filter((d) => d?.db_type === \"knowledge\")\n        .filter((d) => d?.status === status)\n        .map((d) => {\n          d.source.forEach((s) => {\n            delete (s as any).content\n          })\n          return d\n        })\n        .sort((a, b) => b.createdAt - a.createdAt)\n    }\n\n    return data\n      .filter((d) => d?.db_type === \"knowledge\")\n      .map((d) => {\n        d?.source.forEach((s) => {\n          delete (s as any).content\n        })\n        return d\n      })\n      .sort((a, b) => b.createdAt - a.createdAt)\n  } catch (e) {\n    return []\n  }\n}\n\nexport const deleteKnowledge = async (id: string) => {\n  const db = new PageAssistKnowledge()\n  await db.delete(id)\n  await deleteVector(`vector:${id}`)\n}\n\nexport const deleteSource = async (id: string, source_id: string) => {\n  const db = new PageAssistKnowledge()\n  await db.deleteSource(id, source_id)\n  await deleteVectorByFileId(`vector:${id}`, source_id)\n}\n\nexport const exportKnowledge = async () => {\n  const db = new PageAssistKnowledge()\n  const data = await db.getAll()\n  return data\n}\n\nexport const importKnowledge = async (data: Knowledge[]) => {\n  const db = new PageAssistKnowledge()\n  for (const d of data) {\n    await db.create(d)\n  }\n}\n\nexport const updateKnowledgebase = async ({\n  id,\n  title,\n  systemPrompt,\n  followupPrompt\n}: {\n  id: string\n  title: string\n  systemPrompt?: string\n  followupPrompt?: string\n}) => {\n  const kb = new PageAssistKnowledge()\n  const knowledgeBase = await kb.getById(id)\n  if (knowledgeBase) {\n    await kb.update({\n      ...knowledgeBase,\n      title,\n      systemPrompt,\n      followupPrompt,\n    })\n  }\n}\n"
  },
  {
    "path": "src/db/dexie/mcp.ts",
    "content": "import { db } from \"./schema\"\nimport { McpServer } from \"@/libs/mcp/types\"\nimport { buildMcpHeaders, normalizeMcpServerInput } from \"@/libs/mcp/utils\"\n\nexport const generateMcpServerId = () => {\n  return \"mcp-xxxx-xxx-xxxx\".replace(/[x]/g, () => {\n    const r = Math.floor(Math.random() * 16)\n    return r.toString(16)\n  })\n}\n\nexport class McpServerDb {\n  getAll = async (): Promise<McpServer[]> => {\n    return await db.mcpServers.orderBy(\"createdAt\").reverse().toArray()\n  }\n\n  getEnabled = async (): Promise<McpServer[]> => {\n    return await db.mcpServers.filter((server) => server.enabled).toArray()\n  }\n\n  getById = async (id: string): Promise<McpServer | undefined> => {\n    return await db.mcpServers.get(id)\n  }\n\n  create = async (server: McpServer): Promise<void> => {\n    await db.mcpServers.add(server)\n  }\n\n  update = async (server: McpServer): Promise<void> => {\n    await db.mcpServers.put(server)\n  }\n\n  delete = async (id: string): Promise<void> => {\n    await db.mcpServers.delete(id)\n  }\n\n  async importDataV2(\n    data: McpServer[],\n    options: {\n      replaceExisting?: boolean\n      mergeData?: boolean\n    } = {}\n  ): Promise<void> {\n    const { replaceExisting = false, mergeData = true } = options\n\n    for (const server of data) {\n      const existing = await this.getById(server.id)\n\n      if (existing && !replaceExisting) {\n        if (mergeData) {\n          await this.update({\n            ...existing,\n            ...server\n          })\n        }\n        continue\n      }\n\n      await this.update(server)\n    }\n  }\n}\n\nexport const addMcpServer = async (server: Omit<McpServer, \"id\" | \"createdAt\" | \"updatedAt\">) => {\n  const mcpDb = new McpServerDb()\n  const now = Date.now()\n  const data: McpServer = {\n    ...server,\n    ...normalizeMcpServerInput(server),\n    id: generateMcpServerId(),\n    createdAt: now,\n    updatedAt: now\n  }\n\n  await mcpDb.create(data)\n  return data.id\n}\n\nexport const updateMcpServer = async (\n  server: Partial<McpServer> & Pick<McpServer, \"id\">\n) => {\n  const mcpDb = new McpServerDb()\n  const existing = await mcpDb.getById(server.id)\n\n  if (!existing) {\n    throw new Error(\"MCP server not found\")\n  }\n\n  const data: McpServer = {\n    ...existing,\n    ...server,\n    ...normalizeMcpServerInput({\n      ...existing,\n      ...server\n    }),\n    updatedAt: Date.now()\n  }\n\n  await mcpDb.update(data)\n  return data\n}\n\nexport const deleteMcpServer = async (id: string) => {\n  const mcpDb = new McpServerDb()\n  await mcpDb.delete(id)\n}\n\nexport const getAllMcpServers = async () => {\n  const mcpDb = new McpServerDb()\n  return await mcpDb.getAll()\n}\n\nexport const getEnabledMcpServers = async () => {\n  const mcpDb = new McpServerDb()\n  return await mcpDb.getEnabled()\n}\n\nexport const exportMcpServers = async () => {\n  const mcpDb = new McpServerDb()\n  return await mcpDb.getAll()\n}\n\nexport const importMcpServersV2 = async (\n  data: McpServer[],\n  options: {\n    replaceExisting?: boolean\n    mergeData?: boolean\n  } = {}\n) => {\n  const mcpDb = new McpServerDb()\n  return await mcpDb.importDataV2(data, options)\n}\n\nexport const getMcpServerHeaders = (server: McpServer) =>\n  buildMcpHeaders({\n    authType: server.authType,\n    bearerToken: server.bearerToken,\n    headers: server.headers,\n    oauthTokens: server.oauthTokens\n  })\n"
  },
  {
    "path": "src/db/dexie/memory.ts",
    "content": "import { db } from \"./schema\"\nimport { Memory, Memories } from \"./types\"\nimport { generateID } from \"../index\"\n\nexport const getAllMemories = async (): Promise<Memories> => {\n  try {\n    const memories = await db.memories.orderBy(\"createdAt\").reverse().toArray()\n    return memories\n  } catch (error) {\n    console.error(\"Error getting all memories:\", error)\n    return []\n  }\n}\n\nexport const addMemory = async (content: string): Promise<Memory> => {\n  const now = Date.now()\n  const memory: Memory = {\n    id: generateID(),\n    content: content.trim(),\n    createdAt: now,\n    updatedAt: now\n  }\n\n  await db.memories.add(memory)\n  return memory\n}\n\nexport const updateMemory = async (\n  id: string,\n  content: string\n): Promise<Memory | null> => {\n  const memory = await db.memories.get(id)\n\n  if (!memory) {\n    return null\n  }\n\n  const updatedMemory: Memory = {\n    ...memory,\n    content: content.trim(),\n    updatedAt: Date.now()\n  }\n\n  await db.memories.put(updatedMemory)\n  return updatedMemory\n}\n\nexport const deleteMemory = async (id: string): Promise<boolean> => {\n  try {\n    await db.memories.delete(id)\n    return true\n  } catch (error) {\n    console.error(\"Error deleting memory:\", error)\n    return false\n  }\n}\n\nexport const deleteAllMemories = async (): Promise<void> => {\n  await db.memories.clear()\n}\n\nexport const getMemoryById = async (id: string): Promise<Memory | null> => {\n  const memory = await db.memories.get(id)\n  return memory || null\n}\n\nexport const bulkAddMemories = async (memories: Memory[]): Promise<void> => {\n  await db.memories.bulkAdd(memories)\n}\n\nexport const exportMemories = async (): Promise<Memories> => {\n  return await getAllMemories()\n}\n\nexport const importMemories = async (memories: Memories): Promise<void> => {\n  await bulkAddMemories(memories)\n}\n\nexport const getMemoriesAsContext = async (): Promise<string> => {\n  try {\n    const memories = await getAllMemories()\n\n    if (memories.length === 0) {\n      return \"\"\n    }\n\n    const memoryContext = memories\n      .map((memory) => {\n        return `- ${memory.content}`\n      })\n      .join(\"\\n\")\n\n    return `User Context (Personal Memories):\\n${memoryContext}`\n  } catch (error) {\n    console.error(\"Error generating memory context:\", error)\n    return \"\"\n  }\n}\n"
  },
  {
    "path": "src/db/dexie/migration.ts",
    "content": "import { getLastUsedChatModel, getLastUsedChatSystemPrompt } from \"@/services/model-settings\"\nimport { PageAssitDatabase as ChromeDB } from \"../index\"\nimport { getAllKnowledge } from \"../knowledge\"\nimport { getAllVector,  } from \"../vector\"\nimport { PageAssistDatabase as DexieDB, } from \"./chat\"\nimport { PageAssistKnowledge as DexieDBK } from \"./knowledge\"\nimport { PageAssistVectorDb as DexieDBV } from \"./vector\"\nimport { OpenAIModelDb as DexieDBOAI } from \"./openai\"\nimport {ModelNickname as DexieDBNick} from \"./nickname\"\nimport { ModelDb as DexieDBM } from \"./models\"\nimport { getAllOpenAIConfig } from \"../openai\"\nimport { getAllModelsExT } from \"../models\"\nimport { getAllModelNicknamesMig } from \"../nickname\"\nimport { notification } from \"antd\"\n\nexport class DatabaseMigration {\n  private chromeDB: ChromeDB\n  private dexieDB: DexieDB\n  private dexieDBK: DexieDBK\n  private dexieDBV: DexieDBV\n  private dexieDBOAI: DexieDBOAI\n  private dexieDBM: DexieDBM\n  private dexieNick: DexieDBNick\n\n  constructor() {\n    this.chromeDB = new ChromeDB()\n    this.dexieDB = new DexieDB()\n    this.dexieDBK = new DexieDBK()\n    this.dexieDBV = new DexieDBV()\n    this.dexieDBOAI = new DexieDBOAI()\n    this.dexieDBM = new DexieDBM()\n    this.dexieNick = new DexieDBNick()\n  }\n\n  async migrateAllData(): Promise<{\n    success: boolean\n    migratedCounts: {\n      chatHistories: number\n      messages: number\n      prompts: number\n      webshares: number\n      sessionFiles: number\n      userSettings: number\n    }\n    errors: string[]\n  }> {\n    const errors: string[] = []\n    const migratedCounts = {\n      chatHistories: 0,\n      messages: 0,\n      prompts: 0,\n      webshares: 0,\n      sessionFiles: 0,\n      userSettings: 0,\n      knowledge: 0,\n      vector: 0,\n    }\n\n    try {\n      // Migrate user settings\n      try {\n        const userId = await this.chromeDB.getUserID()\n        if (userId && typeof userId === \"string\" && userId.trim() !== \"\") {\n          await this.dexieDB.setUserID(userId)\n          migratedCounts.userSettings = 1\n        }\n      } catch (error) {\n        errors.push(`Failed to migrate user settings: ${error}`)\n      }\n\n\n\n      // Migrate chat histories and messages\n      try {\n        const chatHistories = await this.chromeDB.getChatHistories()\n        for (const history of chatHistories) {\n          try {\n\n            const lastUsedModel = await getLastUsedChatModel(history.id)\n            const lastUsedPrompt = await getLastUsedChatSystemPrompt(history.id)\n\n            await this.dexieDB.addChatHistory({\n              ...history,\n              model_id: lastUsedModel,\n              last_used_prompt: lastUsedPrompt,\n            })\n            migratedCounts.chatHistories++\n\n            // Migrate messages for this history\n            try {\n              const messages = await this.chromeDB.getChatHistory(history.id)\n              for (const message of messages) {\n                try {\n                  await this.dexieDB.addMessage(message)\n                  migratedCounts.messages++\n                } catch (msgError) {\n                  errors.push(\n                    `Failed to migrate message ${message.id}: ${msgError}`\n                  )\n                }\n              }\n\n            } catch (msgHistoryError) {\n              errors.push(\n                `Failed to get messages for history ${history.id}: ${msgHistoryError}`\n              )\n            }\n          } catch (historyError) {\n            errors.push(\n              `Failed to migrate chat history ${history.id}: ${historyError}`\n            )\n          }\n        }\n        await this.chromeDB.deleteAllChatHistory()\n      } catch (error) {\n        errors.push(`Failed to migrate chat histories: ${error}`)\n      }\n\n\n      // Migrate knowledge\n      try {\n        const knowledges = await getAllKnowledge()\n        await this.dexieDBK.importDataV2(knowledges)\n      } catch (error) {\n        errors.push(`Failed to migrate knowledge: ${error}`)\n      }\n\n      // Migrate OpenAI config\n      try {\n        const configs = await getAllOpenAIConfig()\n        await this.dexieDBOAI.importDataV2(configs)\n      } catch(error) {\n        errors.push(`Failed to migrate OAI: ${error}`)\n      }\n\n      // Migrate Custom models\n\n      try {\n        const models = await getAllModelsExT()\n        await this.dexieDBM.importDataV2(models)\n      } catch(error) {\n        errors.push(`Failed to migrate OAI: ${error}`)\n      }\n\n      // Migrate vector\n      try {\n        const vectors = await getAllVector()\n        await this.dexieDBV.saveImportedDataV2(vectors)\n      } catch (error) {\n        errors.push(`Failed to migrate knowledge: ${error}`)\n      }\n\n     // Migrate nickname\n      try {\n        console.log(\"Saving Nickname\")\n        const nicknames = await getAllModelNicknamesMig()\n        await this.dexieNick.importDataV2(nicknames)\n      } catch (error) {\n        errors.push(`Failed to migrate nick: ${error}`)\n      }\n\n\n\n      // Migrate prompts\n      try {\n        const prompts = await this.chromeDB.getAllPrompts()\n        for (const prompt of prompts) {\n          try {\n            await this.dexieDB.addPrompt(prompt)\n            migratedCounts.prompts++\n          } catch (promptError) {\n            errors.push(`Failed to migrate prompt ${prompt.id}: ${promptError}`)\n          }\n        }\n      } catch (error) {\n        errors.push(`Failed to migrate prompts: ${error}`)\n      }\n\n      // Migrate webshares\n      try {\n        const webshares = await this.chromeDB.getAllWebshares()\n        for (const webshare of webshares) {\n          try {\n            await this.dexieDB.addWebshare(webshare)\n            migratedCounts.webshares++\n          } catch (webshareError) {\n            errors.push(\n              `Failed to migrate webshare ${webshare.id}: ${webshareError}`\n            )\n          }\n        }\n      } catch (error) {\n        errors.push(`Failed to migrate webshares: ${error}`)\n      }\n\n      // Migrate session files - this is tricky since Chrome storage doesn't have a way to enumerate all keys\n      // We'll need to handle this separately or ask the user to provide session IDs\n      console.warn(\n        \"Session files migration requires manual handling of session IDs\"\n      )\n\n      return {\n        success: errors.length === 0,\n        migratedCounts,\n        errors\n      }\n    } catch (error) {\n      errors.push(`General migration error: ${error}`)\n      return {\n        success: false,\n        migratedCounts,\n        errors\n      }\n    }\n  }\n\n  async migrateSessionFiles(sessionIds: string[]): Promise<{\n    success: boolean\n    migratedCount: number\n    errors: string[]\n  }> {\n    const errors: string[] = []\n    let migratedCount = 0\n\n    for (const sessionId of sessionIds) {\n      try {\n        const sessionFiles = await this.chromeDB.getSessionFilesInfo(sessionId)\n        if (sessionFiles) {\n          await this.dexieDB.setRetrievalEnabled(\n            sessionId,\n            sessionFiles.retrievalEnabled\n          )\n          for (const file of sessionFiles.files) {\n            await this.dexieDB.addFileToSession(sessionId, file)\n          }\n          migratedCount++\n        }\n      } catch (error) {\n        errors.push(\n          `Failed to migrate session files for ${sessionId}: ${error}`\n        )\n      }\n    }\n\n    return {\n      success: errors.length === 0,\n      migratedCount,\n      errors\n    }\n  }\n\n  async verifyMigration(): Promise<{\n    isValid: boolean\n    counts: {\n      chrome: {\n        chatHistories: number\n        prompts: number\n        webshares: number\n      }\n      dexie: {\n        chatHistories: number\n        prompts: number\n        webshares: number\n      }\n    }\n    issues: string[]\n  }> {\n    const issues: string[] = []\n\n    try {\n      // Count Chrome storage data\n      const chromeChatHistories = await this.chromeDB.getChatHistories()\n      const chromePrompts = await this.chromeDB.getAllPrompts()\n      const chromeWebshares = await this.chromeDB.getAllWebshares()\n\n      // Count Dexie data\n      const dexieChatHistories = await this.dexieDB.getChatHistories()\n      const dexiePrompts = await this.dexieDB.getAllPrompts()\n      const dexieWebshares = await this.dexieDB.getAllWebshares()\n\n      const counts = {\n        chrome: {\n          chatHistories: chromeChatHistories.length,\n          prompts: chromePrompts.length,\n          webshares: chromeWebshares.length\n        },\n        dexie: {\n          chatHistories: dexieChatHistories.length,\n          prompts: dexiePrompts.length,\n          webshares: dexieWebshares.length\n        }\n      }\n\n      // Check for discrepancies\n      if (counts.chrome.chatHistories !== counts.dexie.chatHistories) {\n        issues.push(\n          `Chat histories count mismatch: Chrome(${counts.chrome.chatHistories}) vs Dexie(${counts.dexie.chatHistories})`\n        )\n      }\n\n      if (counts.chrome.prompts !== counts.dexie.prompts) {\n        issues.push(\n          `Prompts count mismatch: Chrome(${counts.chrome.prompts}) vs Dexie(${counts.dexie.prompts})`\n        )\n      }\n\n      if (counts.chrome.webshares !== counts.dexie.webshares) {\n        issues.push(\n          `Webshares count mismatch: Chrome(${counts.chrome.webshares}) vs Dexie(${counts.dexie.webshares})`\n        )\n      }\n\n      return {\n        isValid: issues.length === 0,\n        counts,\n        issues\n      }\n    } catch (error) {\n      issues.push(`Verification failed: ${error}`)\n      return {\n        isValid: false,\n        counts: {\n          chrome: { chatHistories: 0, prompts: 0, webshares: 0 },\n          dexie: { chatHistories: 0, prompts: 0, webshares: 0 }\n        },\n        issues\n      }\n    }\n  }\n\n  async clearDexieDatabase(): Promise<void> {\n    await this.dexieDB.clear()\n  }\n}\n\nexport const runMigration = async (): Promise<void> => {\n  const migration = new DatabaseMigration()\n\n  console.log(\"Starting database migration...\")\n  const result = await migration.migrateAllData()\n\n  if (result.success) {\n    console.log(\"Migration completed successfully!\")\n    console.log(\"Migrated counts:\", result.migratedCounts)\n  } else {\n    console.error(\"Migration completed with errors:\")\n    console.error(\"Errors:\", result.errors)\n    console.log(\"Partial migration counts:\", result.migratedCounts)\n  }\n\n  // Verify migration\n  console.log(\"Verifying migration...\")\n  const verification = await migration.verifyMigration()\n\n  if (verification.isValid) {\n    console.log(\"Migration verification passed!\")\n  } else {\n    console.warn(\"Migration verification found issues:\")\n    console.warn(verification.issues)\n  }\n\n  console.log(\"Data counts:\", verification.counts)\n}\n\n// Helper function to get all session IDs from Chrome storage\nexport const getAllSessionIds = async (): Promise<string[]> => {\n  return new Promise((resolve) => {\n    chrome.storage.local.get(null, (items) => {\n      const sessionIds: string[] = []\n      for (const key in items) {\n        if (key.startsWith(\"session_files_\")) {\n          sessionIds.push(key.replace(\"session_files_\", \"\"))\n        }\n      }\n      resolve(sessionIds)\n    })\n  })\n}\n\nexport const runSessionFilesMigration = async (): Promise<void> => {\n  const migration = new DatabaseMigration()\n  const sessionIds = await getAllSessionIds()\n\n  if (sessionIds.length > 0) {\n    console.log(`Found ${sessionIds.length} session files to migrate`)\n    const result = await migration.migrateSessionFiles(sessionIds)\n\n    if (result.success) {\n      console.log(`Successfully migrated ${result.migratedCount} session files`)\n    } else {\n      console.error(\"Session files migration completed with errors:\")\n      console.error(result.errors)\n    }\n  } else {\n    console.log(\"No session files found to migrate\")\n  }\n}\n\nexport const runAllMigrations = async (): Promise<void> => {\n  await runMigration()\n  await runSessionFilesMigration()\n}\n"
  },
  {
    "path": "src/db/dexie/modelState.ts",
    "content": "import { db } from \"./schema\"\nimport { ModelState, ModelStates } from \"./types\"\n\nexport class ModelStateDb {\n  async getModelState(model_id: string): Promise<ModelState | undefined> {\n    return await db.modelState.get(model_id)\n  }\n\n  async getAllModelStates(): Promise<ModelStates> {\n    return await db.modelState.toArray()\n  }\n\n  async setModelState(model_id: string, is_enabled: boolean): Promise<void> {\n    await db.modelState.put({\n      id: model_id,\n      model_id,\n      is_enabled\n    })\n  }\n\n  async deleteModelState(model_id: string): Promise<void> {\n    await db.modelState.delete(model_id)\n  }\n\n  async importDataV2(\n    data: ModelStates,\n    options: {\n      replaceExisting?: boolean\n      mergeData?: boolean\n    } = {}\n  ): Promise<void> {\n    const { replaceExisting = false, mergeData = true } = options\n\n    for (const state of data) {\n      const existingState = await this.getModelState(state.model_id)\n\n      if (existingState && !replaceExisting) {\n        if (mergeData) {\n          await this.setModelState(state.model_id, state.is_enabled)\n        }\n        continue\n      }\n\n      await this.setModelState(state.model_id, state.is_enabled)\n    }\n  }\n}\n\nexport const getModelState = async (\n  model_id: string\n): Promise<boolean> => {\n  try {\n    const modelStateDb = new ModelStateDb()\n    const state = await modelStateDb.getModelState(model_id)\n    // If no state exists, model is enabled by default\n    return state?.is_enabled ?? true\n  } catch (e) {\n    console.error(\"Error getting model state\", e)\n    // Default to enabled if error\n    return true\n  }\n}\n\nexport const setModelState = async (\n  model_id: string,\n  is_enabled: boolean\n): Promise<void> => {\n  try {\n    const modelStateDb = new ModelStateDb()\n    await modelStateDb.setModelState(model_id, is_enabled)\n  } catch (e) {\n    console.error(\"Error setting model state\", e)\n  }\n}\n\nexport const toggleModelState = async (\n  model_id: string\n): Promise<boolean> => {\n  try {\n    const currentState = await getModelState(model_id)\n    const newState = !currentState\n    await setModelState(model_id, newState)\n    return newState\n  } catch (e) {\n    console.error(\"Error toggling model state\", e)\n    return true\n  }\n}\n\nexport const getAllModelStates = async (): Promise<Record<string, boolean>> => {\n  try {\n    const modelStateDb = new ModelStateDb()\n    const states = await modelStateDb.getAllModelStates()\n    const result: Record<string, boolean> = {}\n    for (const state of states) {\n      result[state.model_id] = state.is_enabled\n    }\n    return result\n  } catch (e) {\n    console.error(\"Error getting all model states\", e)\n    return {}\n  }\n}\n"
  },
  {
    "path": "src/db/dexie/models.ts",
    "content": "import { getAllOpenAIModels } from \"@/libs/openai\"\nimport {\n  getAllOpenAIConfig,\n  getOpenAIConfigById as providerInfo\n} from \"./openai\"\nimport { getAllModelNicknames } from \"./nickname\"\nimport { getAllModelStates } from \"./modelState\"\nimport { getAllProviderStates } from \"./providerState\"\nimport { Model, Models } from \"./types\"\nimport { db } from \"./schema\"\nimport {\n  createModelFB,\n  deleteModelFB,\n  getAllCustomModelsFB,\n  getModelInfoFB\n} from \"../models\"\nimport { isDatabaseClosedError } from \"@/utils/ff-error\"\n\nexport const generateID = () => {\n  return \"model-xxxx-xxxx-xxx-xxxx\".replace(/[x]/g, () => {\n    const r = Math.floor(Math.random() * 16)\n    return r.toString(16)\n  })\n}\n\nexport const removeModelSuffix = (id: string) => {\n  return id\n    .replace(/_model-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{3,4}-[a-f0-9]{4}/, \"\")\n    .replace(/_lmstudio_openai-[a-f0-9]{4}-[a-f0-9]{3}-[a-f0-9]{4}/, \"\")\n    .replace(/_llamafile_openai-[a-f0-9]{4}-[a-f0-9]{3}-[a-f0-9]{4}/, \"\")\n    .replace(/_ollama2_openai-[a-f0-9]{4}-[a-f0-9]{3}-[a-f0-9]{4}/, \"\")\n    .replace(/_llamacpp_openai-[a-f0-9]{4}-[a-f0-9]{3}-[a-f0-9]{4}/, \"\")\n    .replace(/_vllm_openai-[a-f0-9]{4}-[a-f0-9]{3}-[a-f0-9]{4}/, \"\")\n}\nexport const isLMStudioModel = (model: string) => {\n  const lmstudioModelRegex =\n    /_lmstudio_openai-[a-f0-9]{4}-[a-f0-9]{3}-[a-f0-9]{4}/\n  return lmstudioModelRegex.test(model)\n}\n\nexport const isLlamafileModel = (model: string) => {\n  const llamafileModelRegex =\n    /_llamafile_openai-[a-f0-9]{4}-[a-f0-9]{3}-[a-f0-9]{4}/\n  return llamafileModelRegex.test(model)\n}\n\nexport const isLLamaCppModel = (model: string) => {\n  const llamaCppModelRegex =\n    /_llamacpp_openai-[a-f0-9]{4}-[a-f0-9]{3}-[a-f0-9]{4}/\n  return llamaCppModelRegex.test(model)\n}\n\nexport const isVLLMModel = (model: string) => {\n  const vllmModelRegex = /_vllm_openai-[a-f0-9]{4}-[a-f0-9]{3}-[a-f0-9]{4}/\n  return vllmModelRegex.test(model)\n}\n\nexport const isOllamaModel = (model: string) => {\n  const ollamaModelRegex = /_ollama2_openai-[a-f0-9]{4}-[a-f0-9]{3}-[a-f0-9]{4}/\n  return ollamaModelRegex.test(model)\n}\nexport const getLMStudioModelId = (\n  model: string\n): { model_id: string; provider_id: string } => {\n  const lmstudioModelRegex =\n    /_lmstudio_openai-[a-f0-9]{4}-[a-f0-9]{3}-[a-f0-9]{4}/\n  const match = model.match(lmstudioModelRegex)\n  if (match) {\n    const modelId = match[0]\n    const providerId = match[0].replace(\"_lmstudio_openai-\", \"\")\n    return { model_id: modelId, provider_id: providerId }\n  }\n  return null\n}\nexport const getOllamaModelId = (\n  model: string\n): { model_id: string; provider_id: string } => {\n  const ollamaModelRegex = /_ollama2_openai-[a-f0-9]{4}-[a-f0-9]{3}-[a-f0-9]{4}/\n  const match = model.match(ollamaModelRegex)\n  if (match) {\n    const modelId = match[0]\n    const providerId = match[0].replace(\"_ollama2_openai-\", \"\")\n    return { model_id: modelId, provider_id: providerId }\n  }\n  return null\n}\nexport const getLlamafileModelId = (\n  model: string\n): { model_id: string; provider_id: string } => {\n  const llamafileModelRegex =\n    /_llamafile_openai-[a-f0-9]{4}-[a-f0-9]{3}-[a-f0-9]{4}/\n  const match = model.match(llamafileModelRegex)\n  if (match) {\n    const modelId = match[0]\n    const providerId = match[0].replace(\"_llamafile_openai-\", \"\")\n    return { model_id: modelId, provider_id: providerId }\n  }\n  return null\n}\n\nexport const getLLamaCppModelId = (\n  model: string\n): { model_id: string; provider_id: string } => {\n  const llamaCppModelRegex =\n    /_llamacpp_openai-[a-f0-9]{4}-[a-f0-9]{3}-[a-f0-9]{4}/\n  const match = model.match(llamaCppModelRegex)\n  if (match) {\n    const modelId = match[0]\n    const providerId = match[0].replace(\"_llamacpp_openai-\", \"\")\n    return { model_id: modelId, provider_id: providerId }\n  }\n  return null\n}\n\nexport const getVLLMModelId = (\n  model: string\n): { model_id: string; provider_id: string } => {\n  const vllmModelRegex = /_vllm_openai-[a-f0-9]{4}-[a-f0-9]{3}-[a-f0-9]{4}/\n  const match = model.match(vllmModelRegex)\n  if (match) {\n    const modelId = match[0]\n    const providerId = match[0].replace(\"_vllm_openai-\", \"\")\n    return { model_id: modelId, provider_id: providerId }\n  }\n  return null\n}\n\nexport const isCustomModel = (model: string) => {\n  if (isLMStudioModel(model)) {\n    return true\n  }\n\n  if (isLlamafileModel(model)) {\n    return true\n  }\n\n  if (isOllamaModel(model)) {\n    return true\n  }\n\n  if (isLLamaCppModel(model)) {\n    return true\n  }\n\n  if (isVLLMModel(model)) {\n    return true\n  }\n\n  const customModelRegex =\n    /_model-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{3,4}-[a-f0-9]{4}/\n  return customModelRegex.test(model)\n}\nexport class ModelDb {\n  getAll = async (): Promise<Models> => {\n    return await db.customModels.reverse().toArray()\n  }\n\n  create = async (model: Model): Promise<void> => {\n    return await db.customModels.add(model)\n  }\n\n  getById = async (id: string): Promise<Model> => {\n    return await db.customModels.get(id)\n  }\n\n  update = async (model: Model): Promise<void> => {\n    return await db.customModels.put(model)\n  }\n\n  delete = async (id: string): Promise<void> => {\n    return await db.customModels.delete(id)\n  }\n\n  deleteAll = async (): Promise<void> => {\n    return await db.customModels.clear()\n  }\n\n  async importDataV2(\n    data: Models,\n    options: {\n      replaceExisting?: boolean\n      mergeData?: boolean\n    } = {}\n  ): Promise<void> {\n    const { replaceExisting = false, mergeData = true } = options\n\n    for (const oai of data) {\n      const existingKnowledge = await this.getById(oai.id)\n\n      if (existingKnowledge && !replaceExisting) {\n        if (mergeData) {\n          await this.update({\n            ...existingKnowledge\n          })\n        }\n        continue\n      }\n\n      await this.create(oai)\n    }\n  }\n}\n\nexport const createManyModels = async (\n  data: {\n    model_id: string\n    name: string\n    provider_id: string\n    model_type: string\n  }[]\n) => {\n  const db = new ModelDb()\n\n  const models = data.map((item) => {\n    return {\n      ...item,\n      lookup: `${item.model_id}_${item.provider_id}`,\n      id: `${item.model_id}_${generateID()}`,\n      db_type: \"openai_model\",\n      name: item.name.replaceAll(/accounts\\/[^\\/]+\\/models\\//g, \"\")\n    }\n  })\n\n  for (const model of models) {\n    const isExist = await isLookupExist(model.lookup)\n\n    if (isExist) {\n      continue\n    }\n\n    await db.create(model)\n    await createModelFB(model)\n  }\n}\n\nexport const createModel = async (\n  model_id: string,\n  name: string,\n  provider_id: string,\n  model_type: string\n) => {\n  const db = new ModelDb()\n  const id = generateID()\n  const model: Model = {\n    id: `${model_id}_${id}`,\n    model_id,\n    name,\n    provider_id,\n    lookup: `${model_id}_${provider_id}`,\n    db_type: \"openai_model\",\n    model_type: model_type\n  }\n  await db.create(model)\n  await createModelFB(model)\n  return model\n}\n\nexport const getModelInfo = async (id: string) => {\n  try {\n    const db = new ModelDb()\n\n    if (isLMStudioModel(id)) {\n      const lmstudioId = getLMStudioModelId(id)\n      if (!lmstudioId) {\n        throw new Error(\"Invalid LMStudio model ID\")\n      }\n      return {\n        model_id: id.replace(\n          /_lmstudio_openai-[a-f0-9]{4}-[a-f0-9]{3}-[a-f0-9]{4}/,\n          \"\"\n        ),\n        provider_id: `openai-${lmstudioId.provider_id}`,\n        name: id.replace(\n          /_lmstudio_openai-[a-f0-9]{4}-[a-f0-9]{3}-[a-f0-9]{4}/,\n          \"\"\n        )\n      }\n    }\n\n    if (isLlamafileModel(id)) {\n      const llamafileId = getLlamafileModelId(id)\n      if (!llamafileId) {\n        throw new Error(\"Invalid LMStudio model ID\")\n      }\n      return {\n        model_id: id.replace(\n          /_llamafile_openai-[a-f0-9]{4}-[a-f0-9]{3}-[a-f0-9]{4}/,\n          \"\"\n        ),\n        provider_id: `openai-${llamafileId.provider_id}`,\n        name: id.replace(\n          /_llamafile_openai-[a-f0-9]{4}-[a-f0-9]{3}-[a-f0-9]{4}/,\n          \"\"\n        )\n      }\n    }\n\n    if (isLLamaCppModel(id)) {\n      const llamaCppId = getLLamaCppModelId(id)\n      if (!llamaCppId) {\n        throw new Error(\"Invalid llamaCPP model ID\")\n      }\n\n      return {\n        model_id: id.replace(\n          /_llamacpp_openai-[a-f0-9]{4}-[a-f0-9]{3}-[a-f0-9]{4}/,\n          \"\"\n        ),\n        provider_id: `openai-${llamaCppId.provider_id}`,\n        name: id.replace(\n          /_llamacpp_openai-[a-f0-9]{4}-[a-f0-9]{3}-[a-f0-9]{4}/,\n          \"\"\n        )\n      }\n    }\n\n    if (isVLLMModel(id)) {\n      const vllmId = getVLLMModelId(id)\n      if (!vllmId) {\n        throw new Error(\"Invalid Vllm model ID\")\n      }\n      return {\n        model_id: id.replace(\n          /_vllm_openai-[a-f0-9]{4}-[a-f0-9]{3}-[a-f0-9]{4}/,\n          \"\"\n        ),\n        provider_id: `openai-${vllmId.provider_id}`,\n        name: id.replace(/_vllm_openai-[a-f0-9]{4}-[a-f0-9]{3}-[a-f0-9]{4}/, \"\")\n      }\n    }\n\n    if (isOllamaModel(id)) {\n      const ollamaId = getOllamaModelId(id)\n      if (!ollamaId) {\n        throw new Error(\"Invalid LMStudio model ID\")\n      }\n      return {\n        model_id: id.replace(\n          /_ollama2_openai-[a-f0-9]{4}-[a-f0-9]{3}-[a-f0-9]{4}/,\n          \"\"\n        ),\n        provider_id: `openai-${ollamaId.provider_id}`,\n        name: id.replace(\n          /_ollama2_openai-[a-f0-9]{4}-[a-f0-9]{3}-[a-f0-9]{4}/,\n          \"\"\n        )\n      }\n    }\n\n    const model = await db.getById(id)\n    return model\n  } catch (e) {\n    if (isDatabaseClosedError(e)) {\n      return await getModelInfoFB(id)\n    }\n\n    return null\n  }\n}\n\nexport const getAllCustomModels = async () => {\n  try {\n    const db = new ModelDb()\n    const modelNicknames = await getAllModelNicknames()\n    const models = (await db.getAll()).filter(\n      (model) => model?.db_type === \"openai_model\"\n    )\n    const modelsWithProvider = await Promise.all(\n      models.map(async (model) => {\n        const provider = await providerInfo(model.provider_id)\n        return { ...model, provider }\n      })\n    )\n\n    return modelsWithProvider.map((model) => {\n      return {\n        ...model,\n        nickname: modelNicknames[model.id]?.model_name || model.model_id,\n        avatar: modelNicknames[model.id]?.model_avatar || undefined\n      }\n    })\n  } catch (e) {\n    if (isDatabaseClosedError(e)) {\n      return await getAllCustomModelsFB()\n    }\n    return []\n  }\n}\n\nexport const deleteModel = async (id: string) => {\n  const db = new ModelDb()\n  await db.delete(id)\n  await deleteModelFB(id)\n}\n\nexport const deleteAllModelsByProviderId = async (provider_id: string) => {\n  const db = new ModelDb()\n  const models = await db.getAll()\n  const modelsToDelete = models.filter(\n    (model) => model.provider_id === provider_id\n  )\n  for (const model of modelsToDelete) {\n    await db.delete(model.id)\n  }\n}\n\nexport const isLookupExist = async (lookup: string) => {\n  const db = new ModelDb()\n  const models = await db.getAll()\n  const model = models.find((model) => model?.lookup === lookup)\n  return model ? true : false\n}\n\nexport const dynamicFetchLMStudio = async ({\n  baseUrl,\n  providerId,\n  customHeaders = []\n}: {\n  baseUrl: string\n  providerId: string\n  customHeaders?: { key: string; value: string }[]\n}) => {\n  const models = await getAllOpenAIModels({ baseUrl, customHeaders })\n  const lmstudioModels = models.map((e) => {\n    return {\n      name: e?.name || e?.id,\n      id: `${e?.id}_lmstudio_${providerId}`,\n      provider: providerId,\n      lookup: `${e?.id}_${providerId}`,\n      provider_id: providerId\n    }\n  })\n\n  return lmstudioModels\n}\n\nexport const dynamicFetchLLamaCpp = async ({\n  baseUrl,\n  providerId,\n  customHeaders = []\n}: {\n  baseUrl: string\n  providerId: string\n  customHeaders?: { key: string; value: string }[]\n}) => {\n  const models = await getAllOpenAIModels({ baseUrl, customHeaders })\n  const llamaCppModels = models.map((e) => {\n    return {\n      name: e?.name || e?.id,\n      id: `${e?.id}_llamacpp_${providerId}`,\n      provider: providerId,\n      lookup: `${e?.id}_${providerId}`,\n      provider_id: providerId\n    }\n  })\n\n  return llamaCppModels\n}\n\nexport const dynamicFetchVLLM = async ({\n  baseUrl,\n  providerId,\n  customHeaders = []\n}: {\n  baseUrl: string\n  providerId: string\n  customHeaders?: { key: string; value: string }[]\n}) => {\n  const models = await getAllOpenAIModels({ baseUrl, customHeaders })\n  const vllmModels = models.map((e) => {\n    return {\n      name: e?.name || e?.id,\n      id: `${e?.id}_vllm_${providerId}`,\n      provider: providerId,\n      lookup: `${e?.id}_${providerId}`,\n      provider_id: providerId\n    }\n  })\n\n  return vllmModels\n}\n\nexport const dynamicFetchOllama2 = async ({\n  baseUrl,\n  providerId,\n  customHeaders = []\n}: {\n  baseUrl: string\n  providerId: string\n  customHeaders?: { key: string; value: string }[]\n}) => {\n  if (baseUrl.includes(\"ollama.com\")) {\n    const res = await fetch(\"https://ollama.com/api/tags\", {\n      headers: {\n        \"Content-Type\": \"application/json\"\n      }\n    })\n\n    const data = await res.json()\n\n    const models = data.models as { model: string; name: string }[]\n\n    return models.map((e) => {\n      return {\n        name: e?.name || e?.model,\n        id: `${e?.model}_ollama2_${providerId}`,\n        provider: providerId,\n        lookup: `${e?.model}_${providerId}`,\n        provider_id: providerId\n      }\n    })\n  }\n  const models = await getAllOpenAIModels({ baseUrl, customHeaders })\n  const ollama2Models = models.map((e) => {\n    return {\n      name: e?.name || e?.id,\n      id: `${e?.id}_ollama2_${providerId}`,\n      provider: providerId,\n      lookup: `${e?.id}_${providerId}`,\n      provider_id: providerId\n    }\n  })\n\n  return ollama2Models\n}\n\nexport const dynamicFetchLlamafile = async ({\n  baseUrl,\n  providerId,\n  customHeaders = []\n}: {\n  baseUrl: string\n  providerId: string\n  customHeaders?: { key: string; value: string }[]\n}) => {\n  const models = await getAllOpenAIModels({ baseUrl, customHeaders })\n  const llamafileModels = models.map((e) => {\n    return {\n      name: e?.name || e?.id,\n      id: `${e?.id}_llamafile_${providerId}`,\n      provider: providerId,\n      lookup: `${e?.id}_${providerId}`,\n      provider_id: providerId\n    }\n  })\n\n  return llamafileModels\n}\n\nexport const ollamaFormatAllCustomModels = async (\n  modelType: \"all\" | \"chat\" | \"embedding\" = \"all\"\n) => {\n  try {\n    const [allModles, allProviders, modelStates, providerStates] = await Promise.all([\n      getAllCustomModels(),\n      getAllOpenAIConfig(),\n      getAllModelStates(),\n      getAllProviderStates()\n    ])\n    const modelNicknames = await getAllModelNicknames()\n    const lmstudioProviders = allProviders.filter(\n      (provider) => provider.provider === \"lmstudio\"\n    )\n\n    const llamafileProviders = allProviders.filter(\n      (provider) => provider.provider === \"llamafile\"\n    )\n\n    const ollamaProviders = allProviders.filter(\n      (provider) => provider.provider === \"ollama2\"\n    )\n\n    const llamacppProvider = allProviders.filter(\n      (model) => model.provider === \"llamacpp\"\n    )\n\n    const vllmProviders = allProviders.filter(\n      (model) => model.provider === \"vllm\"\n    )\n\n    const lmModelsPromises = lmstudioProviders.map((provider) =>\n      dynamicFetchLMStudio({\n        baseUrl: provider.baseUrl,\n        providerId: provider.id,\n        customHeaders: provider.headers\n      })\n    )\n\n    const llamafileModelsPromises = llamafileProviders.map((provider) =>\n      dynamicFetchLlamafile({\n        baseUrl: provider.baseUrl,\n        providerId: provider.id,\n        customHeaders: provider.headers\n      })\n    )\n\n    const ollamaModelsPromises = ollamaProviders.map((provider) =>\n      dynamicFetchOllama2({\n        baseUrl: provider.baseUrl,\n        providerId: provider.id,\n        customHeaders: provider.headers\n      })\n    )\n\n    const llamacppModelsPromises = llamacppProvider.map((provider) =>\n      dynamicFetchLLamaCpp({\n        baseUrl: provider.baseUrl,\n        providerId: provider.id,\n        customHeaders: provider.headers\n      })\n    )\n\n    const vllmModelsPromises = vllmProviders.map((provider) =>\n      dynamicFetchVLLM({\n        baseUrl: provider.baseUrl,\n        providerId: provider.id,\n        customHeaders: provider.headers\n      })\n    )\n\n    const lmModelsFetch = await Promise.all(lmModelsPromises)\n\n    const llamafileModelsFetch = await Promise.all(llamafileModelsPromises)\n\n    const ollamaModelsFetch = await Promise.all(ollamaModelsPromises)\n\n    const llamacppModelsFetch = await Promise.all(llamacppModelsPromises)\n\n    const vllmModelsFetch = await Promise.all(vllmModelsPromises)\n\n    const lmModels = lmModelsFetch.flat()\n\n    const llamafileModels = llamafileModelsFetch.flat()\n\n    const ollama2Models = ollamaModelsFetch.flat()\n\n    const llamacppModels = llamacppModelsFetch.flat()\n\n    const vllmModels = vllmModelsFetch.flat()\n\n    // merge allModels and lmModels\n    const allModlesWithLMStudio = [\n      ...(modelType !== \"all\"\n        ? allModles.filter((model) => model.model_type === modelType)\n        : allModles),\n      ...lmModels,\n      ...llamafileModels,\n      ...ollama2Models,\n      ...llamacppModels,\n      ...vllmModels\n    ]\n\n    const ollamaModels = allModlesWithLMStudio.map((model) => {\n      const modelEnabled = modelStates[model.id] ?? true // Default to enabled\n      const providerEnabled = providerStates[model.provider_id] ?? true // Default to enabled\n      const isEnabled = modelEnabled && providerEnabled // Both must be enabled\n\n      return {\n        name: model.name,\n        model: model.id,\n        modified_at: \"\",\n        provider:\n          allProviders.find((provider) => provider.id === model.provider_id)\n            ?.provider || \"custom\",\n        size: 0,\n        digest: \"\",\n        is_enabled: isEnabled,\n        provider_id: model.provider_id,\n        details: {\n          parent_model: \"\",\n          format: \"\",\n          family: \"\",\n          families: [],\n          parameter_size: \"\",\n          quantization_level: \"\"\n        }\n      }\n    })\n\n    // Filter out disabled models (either model disabled or provider disabled)\n    const enabledModels = ollamaModels.filter(model => model.is_enabled)\n\n    return enabledModels.map((model) => {\n      return {\n        ...model,\n        nickname: modelNicknames[model.model]?.model_name || model.name,\n        avatar: modelNicknames[model.model]?.model_avatar || undefined\n      }\n    })\n  } catch (e) {\n    console.error(e)\n    return []\n  }\n}\n"
  },
  {
    "path": "src/db/dexie/nickname.ts",
    "content": "import { db } from \"./schema\"\nimport { ModelNicknames, ModelNickname as MNick } from \"./types\"\n\nexport class ModelNickname {\n  async saveModelNickname(\n    id: string,\n    model_name: string,\n    model_avatar?: string\n  ): Promise<void> {\n    await db.modelNickname.put({\n      id,\n      model_name,\n      model_id: id,\n      model_avatar\n    })\n    console.log({\n      id,\n      model_name,\n      model_id: id,\n      model_avatar\n    })\n  }\n\n  async getModelNicknameByID(model_id: string) {\n    return await db.modelNickname.get(model_id)\n  }\n\n  async getAllModelNicknames() {\n    return await db.modelNickname.reverse().toArray()\n  }\n\n  async importDataV2(\n    data: ModelNicknames,\n    options: {\n      replaceExisting?: boolean\n      mergeData?: boolean\n    } = {}\n  ): Promise<void> {\n    const { replaceExisting = false, mergeData = true } = options\n\n    for (const oai of data) {\n      console.log(\"Saving x\")\n\n      await db.modelNickname.put({\n        id: oai.model_id,\n        model_id: oai.model_id,\n        model_name: oai.model_name,\n        model_avatar: oai.model_avatar\n      })\n    }\n  }\n}\n\nexport const getAllModelNicknames = async () => {\n  try {\n    const modelNickname = new ModelNickname()\n    const data = await modelNickname.getAllModelNicknames()\n    const result: Record<string, MNick> = {}\n    for (const d of data) {\n      result[d.model_id] = d\n    }\n    return result\n  } catch (e) {\n    console.error(\"Firefox Private Mode Error\", e)\n    return {}\n  }\n}\n\nexport const getModelNicknameByID = async (\n  model_id: string\n): Promise<{ model_name: string; model_avatar?: string } | null> => {\n  try {\n    const modelNickname = new ModelNickname()\n    return await modelNickname.getModelNicknameByID(model_id)\n  } catch (e) {\n    return null\n  }\n}\n\nexport const saveModelNickname = async ({\n  model_id,\n  model_name,\n  model_avatar\n}: {\n  model_id: string\n  model_name: string\n  model_avatar?: string\n}) => {\n  const modelNickname = new ModelNickname()\n  return await modelNickname.saveModelNickname(\n    model_id,\n    model_name,\n    model_avatar\n  )\n}\n"
  },
  {
    "path": "src/db/dexie/openai.ts",
    "content": "import { cleanUrl } from \"@/libs/clean-url\"\nimport { db } from \"./schema\"\nimport { OpenAIModelConfig, OpenAIModelConfigs } from \"./types\"\nimport { deleteAllModelsByProviderId } from \"./models\"\nimport {\n  addOpenAICofigFB,\n  deleteOpenAIConfigFB,\n  getAllOpenAIConfigFB,\n  getOpenAIConfigByIdFB,\n  updateOpenAIConfigApiKeyFB,\n  updateOpenAIConfigFB\n} from \"../openai\"\n\nexport const generateID = () => {\n  return \"openai-xxxx-xxx-xxxx\".replace(/[x]/g, () => {\n    const r = Math.floor(Math.random() * 16)\n    return r.toString(16)\n  })\n}\n\nexport class OpenAIModelDb {\n  getAll = async (): Promise<OpenAIModelConfigs> => {\n    return await db.openaiConfigs.orderBy(\"createdAt\").reverse().toArray()\n  }\n\n  create = async (config: OpenAIModelConfig): Promise<void> => {\n    return await db.openaiConfigs.add(config)\n  }\n\n  getById = async (id: string): Promise<OpenAIModelConfig> => {\n    return await db.openaiConfigs.get(id)\n  }\n\n  update = async (config: OpenAIModelConfig): Promise<void> => {\n    return await db.openaiConfigs.put(config)\n  }\n\n  delete = async (id: string): Promise<void> => {\n    return await db.openaiConfigs.delete(id)\n  }\n  async importDataV2(\n    data: OpenAIModelConfigs,\n    options: {\n      replaceExisting?: boolean\n      mergeData?: boolean\n    } = {}\n  ): Promise<void> {\n    const { replaceExisting = false, mergeData = true } = options\n\n    for (const oai of data) {\n      const existingKnowledge = await this.getById(oai.id)\n\n      if (existingKnowledge && !replaceExisting) {\n        if (mergeData) {\n          await this.update({\n            ...existingKnowledge\n          })\n        }\n        continue\n      }\n\n      await this.create(oai)\n    }\n  }\n}\n\nexport const addOpenAICofig = async ({\n  name,\n  baseUrl,\n  apiKey,\n  provider,\n  headers,\n  fix_cors\n}: {\n  name: string\n  baseUrl: string\n  apiKey: string\n  provider?: string\n  headers?: { key: string; value: string }[]\n  fix_cors?: boolean\n}) => {\n  const openaiDb = new OpenAIModelDb()\n  const id = generateID()\n  const config: OpenAIModelConfig = {\n    id,\n    name,\n    baseUrl: cleanUrl(baseUrl),\n    apiKey,\n    createdAt: Date.now(),\n    db_type: \"openai\",\n    provider,\n    headers,\n    fix_cors\n  }\n  await openaiDb.create(config)\n  await addOpenAICofigFB(config)\n  return id\n}\n\nexport const getAllOpenAIConfig = async () => {\n  try {\n    const openaiDb = new OpenAIModelDb()\n    const configs = await openaiDb.getAll()\n    return configs.filter((config) => config?.db_type === \"openai\")\n  } catch (e) {\n    if (isDatabaseClosedError(e)) {\n      return await getAllOpenAIConfigFB()\n    }\n    return []\n  }\n}\n\nexport const updateOpenAIConfig = async ({\n  id,\n  name,\n  baseUrl,\n  apiKey,\n  headers,\n  fix_cors\n}: {\n  id: string\n  name: string\n  baseUrl: string\n  apiKey: string\n  headers?: { key: string; value: string }[]\n  fix_cors?: boolean\n}) => {\n  const openaiDb = new OpenAIModelDb()\n  const oldData = await openaiDb.getById(id)\n  const config: OpenAIModelConfig = {\n    ...oldData,\n    id,\n    name,\n    baseUrl: cleanUrl(baseUrl),\n    apiKey,\n    createdAt: Date.now(),\n    db_type: \"openai\",\n    headers: headers || [],\n    fix_cors: fix_cors\n  }\n\n  await openaiDb.update(config)\n  await updateOpenAIConfigFB(config)\n  return config\n}\n\nexport const deleteOpenAIConfig = async (id: string) => {\n  const openaiDb = new OpenAIModelDb()\n  await openaiDb.delete(id)\n  await deleteAllModelsByProviderId(id)\n  await deleteOpenAIConfigFB(id)\n}\n\nexport const updateOpenAIConfigApiKey = async (\n  id: string,\n  { name, baseUrl, apiKey }: { name: string; baseUrl: string; apiKey: string }\n) => {\n  const openaiDb = new OpenAIModelDb()\n  const config: OpenAIModelConfig = {\n    id,\n    name,\n    baseUrl: cleanUrl(baseUrl),\n    apiKey,\n    createdAt: Date.now(),\n    db_type: \"openai\"\n  }\n\n  await openaiDb.update(config)\n  await updateOpenAIConfigApiKeyFB(config)\n}\n\nexport const getOpenAIConfigById = async (id: string) => {\n  try {\n    const openaiDb = new OpenAIModelDb()\n    const config = await openaiDb.getById(id)\n    return config\n  } catch (e) {\n    if (isDatabaseClosedError(e)) {\n      return await getOpenAIConfigByIdFB(id)\n    }\n    return null\n  }\n}\n"
  },
  {
    "path": "src/db/dexie/providerState.ts",
    "content": "import { db } from \"./schema\"\nimport { ProviderState, ProviderStates } from \"./types\"\n\nexport class ProviderStateDb {\n  async getProviderState(provider_id: string): Promise<ProviderState | undefined> {\n    return await db.providerState.get(provider_id)\n  }\n\n  async getAllProviderStates(): Promise<ProviderStates> {\n    return await db.providerState.toArray()\n  }\n\n  async setProviderState(provider_id: string, is_enabled: boolean): Promise<void> {\n    await db.providerState.put({\n      id: provider_id,\n      provider_id,\n      is_enabled\n    })\n  }\n\n  async deleteProviderState(provider_id: string): Promise<void> {\n    await db.providerState.delete(provider_id)\n  }\n\n  async importDataV2(\n    data: ProviderStates,\n    options: {\n      replaceExisting?: boolean\n      mergeData?: boolean\n    } = {}\n  ): Promise<void> {\n    const { replaceExisting = false, mergeData = true } = options\n\n    for (const state of data) {\n      const existingState = await this.getProviderState(state.provider_id)\n\n      if (existingState && !replaceExisting) {\n        if (mergeData) {\n          await this.setProviderState(state.provider_id, state.is_enabled)\n        }\n        continue\n      }\n\n      await this.setProviderState(state.provider_id, state.is_enabled)\n    }\n  }\n}\n\nexport const getProviderState = async (\n  provider_id: string\n): Promise<boolean> => {\n  try {\n    const providerStateDb = new ProviderStateDb()\n    const state = await providerStateDb.getProviderState(provider_id)\n    // If no state exists, provider is enabled by default\n    return state?.is_enabled ?? true\n  } catch (e) {\n    console.error(\"Error getting provider state\", e)\n    // Default to enabled if error\n    return true\n  }\n}\n\nexport const setProviderState = async (\n  provider_id: string,\n  is_enabled: boolean\n): Promise<void> => {\n  try {\n    const providerStateDb = new ProviderStateDb()\n    await providerStateDb.setProviderState(provider_id, is_enabled)\n  } catch (e) {\n    console.error(\"Error setting provider state\", e)\n  }\n}\n\nexport const toggleProviderState = async (\n  provider_id: string\n): Promise<boolean> => {\n  try {\n    const currentState = await getProviderState(provider_id)\n    const newState = !currentState\n    await setProviderState(provider_id, newState)\n    return newState\n  } catch (e) {\n    console.error(\"Error toggling provider state\", e)\n    return true\n  }\n}\n\nexport const getAllProviderStates = async (): Promise<Record<string, boolean>> => {\n  try {\n    const providerStateDb = new ProviderStateDb()\n    const states = await providerStateDb.getAllProviderStates()\n    const result: Record<string, boolean> = {}\n    for (const state of states) {\n      result[state.provider_id] = state.is_enabled\n    }\n    return result\n  } catch (e) {\n    console.error(\"Error getting all provider states\", e)\n    return {}\n  }\n}\n"
  },
  {
    "path": "src/db/dexie/schema.ts",
    "content": "import Dexie, { type Table } from \"dexie\"\nimport {\n  HistoryInfo,\n  Message,\n  Prompt,\n  SessionFiles,\n  UserSettings,\n  Webshare,\n  Knowledge,\n  VectorData,\n  Document,\n  OpenAIModelConfig,\n  Model,\n  ModelNickname,\n  ModelState,\n  ProviderState,\n  Memory,\n  ProjectFolder,\n  McpServerConfig\n} from \"./types\"\n\nexport class PageAssistDexieDB extends Dexie {\n  chatHistories!: Table<HistoryInfo>\n  messages!: Table<Message>\n  prompts!: Table<Prompt>\n  webshares!: Table<Webshare>\n  sessionFiles!: Table<SessionFiles>\n  userSettings!: Table<UserSettings>\n\n  // Knowledge management tables\n  knowledge!: Table<Knowledge>\n  documents!: Table<Document>\n  vectors!: Table<VectorData>\n\n  // Openai config\n  openaiConfigs!: Table<OpenAIModelConfig>\n  customModels!: Table<Model>\n  modelNickname!: Table<ModelNickname>\n  modelState!: Table<ModelState>\n  providerState!: Table<ProviderState>\n\n  // Memory management\n  memories!: Table<Memory>\n\n  // Project folders\n  projectFolders!: Table<ProjectFolder>\n  mcpServers!: Table<McpServerConfig>\n\n  constructor() {\n    super(\"PageAssistDatabase\")\n\n    this.version(1).stores({\n      chatHistories:\n        \"id, title, is_rag, message_source, is_pinned, createdAt, doc_id, last_used_prompt, model_id\",\n      messages:\n        \"id, history_id, name, role, content, createdAt, messageType, modelName\",\n      prompts: \"id, title, content, is_system, createdBy, createdAt\",\n      webshares: \"id, title, url, api_url, share_id, createdAt\",\n      sessionFiles: \"sessionId, retrievalEnabled, createdAt\",\n      userSettings: \"id, user_id\",\n      // Knowledge management tables\n      knowledge:\n        \"id, db_type, title, status, embedding_model, systemPrompt, followupPrompt, createdAt\",\n      documents: \"id, db_type, title, status, embedding_model, createdAt\",\n      vectors: \"id, vectors\",\n      // OpenAI Configs\n      openaiConfigs:\n        \"id, name, baseUrl, apiKey, createdAt, provider, db_type, headers\",\n      customModels:\n        \"id, model_id, name, model_name, model_image, provider_id, lookup, model_type, db_type\",\n      modelNickname: \"id, model_id, model_name, model_avatar\",\n      modelState: \"id, model_id, is_enabled\",\n      providerState: \"id, provider_id, is_enabled\",\n      // Memory management\n      memories: \"id, content, createdAt, updatedAt\"\n    })\n\n    this.version(2).stores({\n      chatHistories:\n        \"id, title, is_rag, message_source, is_pinned, createdAt, doc_id, last_used_prompt, model_id, folder_id\",\n      messages:\n        \"id, history_id, name, role, content, createdAt, messageType, modelName\",\n      prompts: \"id, title, content, is_system, createdBy, createdAt\",\n      webshares: \"id, title, url, api_url, share_id, createdAt\",\n      sessionFiles: \"sessionId, retrievalEnabled, createdAt\",\n      userSettings: \"id, user_id\",\n      knowledge:\n        \"id, db_type, title, status, embedding_model, systemPrompt, followupPrompt, createdAt\",\n      documents: \"id, db_type, title, status, embedding_model, createdAt\",\n      vectors: \"id, vectors\",\n      openaiConfigs:\n        \"id, name, baseUrl, apiKey, createdAt, provider, db_type, headers\",\n      customModels:\n        \"id, model_id, name, model_name, model_image, provider_id, lookup, model_type, db_type\",\n      modelNickname: \"id, model_id, model_name, model_avatar\",\n      modelState: \"id, model_id, is_enabled\",\n      providerState: \"id, provider_id, is_enabled\",\n      memories: \"id, content, createdAt, updatedAt\",\n      projectFolders: \"id, title, createdAt\"\n    })\n\n    this.version(3).stores({\n      chatHistories:\n        \"id, title, is_rag, message_source, is_pinned, createdAt, doc_id, last_used_prompt, model_id, folder_id\",\n      messages:\n        \"id, history_id, name, role, content, createdAt, messageType, modelName\",\n      prompts: \"id, title, content, is_system, createdBy, createdAt\",\n      webshares: \"id, title, url, api_url, share_id, createdAt\",\n      sessionFiles: \"sessionId, retrievalEnabled, createdAt\",\n      userSettings: \"id, user_id\",\n      knowledge:\n        \"id, db_type, title, status, embedding_model, systemPrompt, followupPrompt, createdAt\",\n      documents: \"id, db_type, title, status, embedding_model, createdAt\",\n      vectors: \"id, vectors\",\n      openaiConfigs:\n        \"id, name, baseUrl, apiKey, createdAt, provider, db_type, headers\",\n      customModels:\n        \"id, model_id, name, model_name, model_image, provider_id, lookup, model_type, db_type\",\n      modelNickname: \"id, model_id, model_name, model_avatar\",\n      modelState: \"id, model_id, is_enabled\",\n      providerState: \"id, provider_id, is_enabled\",\n      memories: \"id, content, createdAt, updatedAt\",\n      projectFolders: \"id, title, color, createdAt\"\n    })\n\n    this.version(4).stores({\n      chatHistories:\n        \"id, title, is_rag, message_source, is_pinned, createdAt, doc_id, last_used_prompt, model_id, folder_id\",\n      messages:\n        \"id, history_id, name, role, content, createdAt, messageType, modelName, messageKind, toolCallId, toolName, toolServerName\",\n      prompts: \"id, title, content, is_system, createdBy, createdAt\",\n      webshares: \"id, title, url, api_url, share_id, createdAt\",\n      sessionFiles: \"sessionId, retrievalEnabled, createdAt\",\n      userSettings: \"id, user_id\",\n      knowledge:\n        \"id, db_type, title, status, embedding_model, systemPrompt, followupPrompt, createdAt\",\n      documents: \"id, db_type, title, status, embedding_model, createdAt\",\n      vectors: \"id, vectors\",\n      openaiConfigs:\n        \"id, name, baseUrl, apiKey, createdAt, provider, db_type, headers\",\n      customModels:\n        \"id, model_id, name, model_name, model_image, provider_id, lookup, model_type, db_type\",\n      modelNickname: \"id, model_id, model_name, model_avatar\",\n      modelState: \"id, model_id, is_enabled\",\n      providerState: \"id, provider_id, is_enabled\",\n      memories: \"id, content, createdAt, updatedAt\",\n      projectFolders: \"id, title, color, createdAt\",\n      mcpServers: \"id, name, url, enabled, transport, updatedAt, createdAt\"\n    })\n\n    this.version(5).stores({\n      chatHistories:\n        \"id, title, is_rag, message_source, is_pinned, createdAt, doc_id, last_used_prompt, model_id, folder_id\",\n      messages:\n        \"id, history_id, name, role, content, createdAt, messageType, modelName, messageKind, toolCallId, toolName, toolServerName\",\n      prompts: \"id, title, content, is_system, createdBy, createdAt\",\n      webshares: \"id, title, url, api_url, share_id, createdAt\",\n      sessionFiles: \"sessionId, retrievalEnabled, createdAt\",\n      userSettings: \"id, user_id\",\n      knowledge:\n        \"id, db_type, title, status, embedding_model, systemPrompt, followupPrompt, createdAt\",\n      documents: \"id, db_type, title, status, embedding_model, createdAt\",\n      vectors: \"id, vectors\",\n      openaiConfigs:\n        \"id, name, baseUrl, apiKey, createdAt, provider, db_type, headers\",\n      customModels:\n        \"id, model_id, name, model_name, model_image, provider_id, lookup, model_type, db_type\",\n      modelNickname: \"id, model_id, model_name, model_avatar\",\n      modelState: \"id, model_id, is_enabled\",\n      providerState: \"id, provider_id, is_enabled\",\n      memories: \"id, content, createdAt, updatedAt\",\n      projectFolders: \"id, title, color, createdAt\",\n      mcpServers:\n        \"id, name, url, enabled, transport, updatedAt, createdAt, toolsLastSyncedAt\"\n    })\n  }\n}\n\nexport const db = new PageAssistDexieDB()\n"
  },
  {
    "path": "src/db/dexie/types.ts",
    "content": "import { ChatDocuments } from \"@/models/ChatTypes\"\nimport { ChatMessageKind, McpHeader, McpServer, McpToolCall } from \"@/libs/mcp/types\"\n\nexport type LastUsedModelType = { prompt_id?: string; prompt_content?: string }\n\nexport type HistoryInfo = {\n  id: string\n  title: string\n  is_rag: boolean\n  message_source?: \"copilot\" | \"web-ui\" | \"branch\"\n  is_pinned?: boolean\n  createdAt: number\n  doc_id?: string\n  last_used_prompt?: LastUsedModelType\n  model_id?: string\n  folder_id?: string\n}\n\nexport type WebSearch = {\n  search_engine: string\n  search_url: string\n  search_query: string\n  search_results: {\n    title: string\n    link: string\n  }[]\n}\n\nexport type UploadedFile = {\n  id: string\n  filename: string\n  type: string\n  content: string\n  size: number\n  uploadedAt: number\n  embedding?: number[]\n  processed: boolean\n}\n\nexport type SessionFiles = {\n  id?: any\n  sessionId: string\n  files: UploadedFile[]\n  retrievalEnabled: boolean\n  createdAt: number\n}\n\nexport type Message = {\n  id: string\n  history_id: string\n  name: string\n  role: string\n  content: string\n  images?: string[]\n  sources?: string[]\n  search?: WebSearch\n  createdAt: number\n  reasoning_time_taken?: number\n  messageType?: string\n  messageKind?: ChatMessageKind\n  toolCalls?: McpToolCall[]\n  toolCallId?: string\n  toolName?: string\n  toolServerName?: string\n  toolError?: boolean\n  generationInfo?: any\n  modelName?: string\n  modelImage?: string\n  documents?: ChatDocuments\n}\n\nexport type Webshare = {\n  id: string\n  title: string\n  url: string\n  api_url: string\n  share_id: string\n  createdAt: number\n}\n\nexport type Prompt = {\n  id: string\n  title: string\n  content: string\n  is_system: boolean\n  createdBy?: string\n  createdAt: number\n}\n\nexport type UserSettings = {\n  id: string\n  user_id: string\n}\n\nexport type Source = {\n  source_id: string\n  type: string\n  filename?: string\n  content: string\n}\n\nexport type Knowledge = {\n  id: string\n  db_type: string\n  title: string\n  status: string\n  embedding_model: string\n  source: Source[]\n  knownledge: any\n  createdAt: number\n  systemPrompt?: string\n  followupPrompt?: string\n}\n\nexport interface PageAssistVector {\n  file_id: string\n  content: string\n  embedding: number[]\n  metadata: Record<string, any>\n}\n\nexport type VectorData = {\n  id: string\n  vectors: PageAssistVector[]\n}\n\n// Types for Document\nexport type DocumentSource = {\n  source_id: string\n  type: string\n  filename?: string\n  content: string\n}\n\nexport type Document = {\n  id: string\n  db_type: string\n  title: string\n  status: string\n  embedding_model: string\n  source: DocumentSource[]\n  document: any\n  createdAt: number\n  systemPrompt?: string\n  followupPrompt?: string\n  compressedContent?: string\n}\n\nexport type OpenAIModelConfig = {\n  id: string\n  name: string\n  baseUrl: string\n  apiKey?: string\n  createdAt: number\n  provider?: string\n  db_type: string\n  fix_cors?: boolean\n  headers?: { key: string; value: string }[]\n}\n\nexport type McpServerConfig = McpServer\n\nexport type Model = {\n  id: string\n  model_id: string\n  name: string\n  model_name?: string\n  model_image?: string\n  provider_id: string\n  lookup: string\n  model_type: string\n  db_type: string\n}\n\nexport type ModelNickname = {\n  id: string\n  model_id: string\n  model_name: string\n  model_avatar?: string\n}\n\nexport type ModelState = {\n  id: string\n  model_id: string\n  is_enabled: boolean\n}\n\nexport type ProviderState = {\n  id: string\n  provider_id: string\n  is_enabled: boolean\n}\n\nexport type Memory = {\n  id: string\n  content: string\n  createdAt: number\n  updatedAt: number\n}\n\nexport type ProjectFolder = {\n  id: string\n  title: string\n  createdAt: number\n}\n\nexport type MessageHistory = Message[]\nexport type ChatHistory = HistoryInfo[]\nexport type Prompts = Prompt[]\nexport type OpenAIModelConfigs = OpenAIModelConfig[]\nexport type McpServerConfigs = McpServerConfig[]\nexport type Models = Model[]\nexport type ModelNicknames = ModelNickname[]\nexport type ModelStates = ModelState[]\nexport type ProviderStates = ProviderState[]\nexport type Memories = Memory[]\nexport type ProjectFolders = ProjectFolder[]\n"
  },
  {
    "path": "src/db/dexie/vector.ts",
    "content": " \nimport { db } from \"./schema\";\nimport { PageAssistVector, VectorData } from \"./types\";\n\nexport class PageAssistVectorDb {\n  async insertVector(id: string, vector: PageAssistVector[]): Promise<void> {\n    const existingData = await db.vectors.get(id);\n    \n    if (!existingData) {\n      await db.vectors.add({ id, vectors: vector });\n    } else {\n      const updatedData = {\n        ...existingData,\n        vectors: existingData.vectors.concat(vector)\n      };\n      await db.vectors.put(updatedData);\n    }\n  }\n\n  async deleteVector(id: string): Promise<void> {\n    await db.vectors.delete(id);\n  }\n\n  async deleteVectorByFileId(id: string, file_id: string): Promise<void> {\n    const data = await db.vectors.get(id);\n    if (data) {\n      data.vectors = data.vectors.filter((v) => v.file_id !== file_id);\n      await db.vectors.put(data);\n    }\n  }\n\n  async getVector(id: string): Promise<VectorData | undefined> {\n    return await db.vectors.get(id);  \n  }\n\n  async getAll(): Promise<VectorData[]> {\n    return await db.vectors.toArray();\n  }\n\n  async saveImportedData(data: VectorData[]): Promise<void> {\n    await db.vectors.bulkPut(data);\n  }\nasync saveImportedDataV2(data: VectorData[], options: {\n  replaceExisting?: boolean;\n  mergeData?: boolean;\n} = {}): Promise<void> {\n  const { replaceExisting = false, mergeData = true } = options;\n  \n  if (!mergeData && !replaceExisting) {\n    await db.vectors.clear();\n  }\n  \n  for (const vectorData of data) {\n    const existingVector = await db.vectors.get(vectorData.id);\n    \n    if (existingVector && !replaceExisting) {\n      if (mergeData) {\n        const mergedVectors = [...existingVector.vectors];\n        for (const newVector of vectorData.vectors) {\n          if (!mergedVectors.find(v => v.file_id === newVector.file_id && v.content === newVector.content)) {\n            mergedVectors.push(newVector);\n          }\n        }\n        await db.vectors.put({\n          ...existingVector,\n          vectors: mergedVectors\n        });\n      }\n      continue;\n    }\n    \n    await db.vectors.put(vectorData);\n  }\n}\n  \n}\nexport const importVectorsV2 = async (data: VectorData[], options: {\n  replaceExisting?: boolean;\n  mergeData?: boolean;\n} = {}) => {\n  const { replaceExisting = false, mergeData = true } = options;\n  \n  if (!mergeData && !replaceExisting) {\n    await db.vectors.clear();\n  }\n  \n  for (const vectorData of data) {\n    const existingVector = await db.vectors.get(vectorData.id);\n    \n    if (existingVector && !replaceExisting) {\n      if (mergeData) {\n        // Merge vectors arrays, avoiding duplicates\n        const mergedVectors = [...existingVector.vectors];\n        for (const newVector of vectorData.vectors) {\n          if (!mergedVectors.find(v => v.file_id === newVector.file_id && v.content === newVector.content)) {\n            mergedVectors.push(newVector);\n          }\n        }\n        await db.vectors.put({\n          ...existingVector,\n          vectors: mergedVectors\n        });\n      }\n      continue;\n    }\n    \n    await db.vectors.put(vectorData);\n  }\n  \n}\n\nexport const saveImportedDataV2 = async (data: VectorData[], options: {\n  replaceExisting?: boolean;\n  mergeData?: boolean;\n} = {}) => {\n  const vectorDb = new PageAssistVectorDb();\n  return vectorDb.saveImportedDataV2(data, options);\n}\n\n// Helper functions that match the original API\nexport const insertVector = async (\n  id: string,\n  vector: PageAssistVector[]\n): Promise<void> => {\n  const db = new PageAssistVectorDb();\n  return db.insertVector(id, vector);\n};\n\nexport const getVector = async (id: string): Promise<VectorData | undefined> => {\n  const db = new PageAssistVectorDb();\n  return db.getVector(id);\n};\n\nexport const deleteVector = async (id: string): Promise<void> => {\n  const db = new PageAssistVectorDb();\n  return db.deleteVector(id);\n};\n\nexport const deleteVectorByFileId = async (\n  id: string,\n  file_id: string\n): Promise<void> => {\n  const db = new PageAssistVectorDb();\n  return db.deleteVectorByFileId(id, file_id);\n};\n\nexport const exportVectors = async () => {\n  const db = new PageAssistVectorDb();\n  const data = await db.getAll();\n  return data;\n};\n\nexport const importVectors = async (data: VectorData[]) => {\n  const db = new PageAssistVectorDb();\n  return db.saveImportedData(data);\n};\n"
  },
  {
    "path": "src/db/document.ts",
    "content": "import { deleteVector, deleteVectorByFileId } from \"./vector\"\nimport { compressText, decompressData, arrayBufferToBase64, base64ToArrayBuffer } from \"@/utils/compress\"\n\nexport type Source = {\n    source_id: string\n    type: string\n    filename?: string\n    content: string\n}\n\nexport type Document = {\n    id: string\n    db_type: string\n    title: string\n    status: string\n    embedding_model: string\n    source: Source[]\n    document: any\n    createdAt: number\n    systemPrompt?: string,\n    followupPrompt?: string\n}\n\nexport const generateID = () => {\n    return \"pa_document_xxxx-xxxx-xxx-xxxx\".replace(/[x]/g, () => {\n        const r = Math.floor(Math.random() * 16)\n        return r.toString(16)\n    })\n}\n\nexport class PageAssistDocument {\n    db: chrome.storage.StorageArea\n\n    constructor() {\n        this.db = chrome.storage.local\n    }\n\n    getAll = async (): Promise<Document[]> => {\n        return new Promise((resolve, reject) => {\n            this.db.get(null, async (result) => {\n                if (chrome.runtime.lastError) {\n                    reject(chrome.runtime.lastError)\n                } else {\n                    const data = await Promise.all(\n                        Object.keys(result)\n                            .filter(key => key.startsWith('pa_document_'))\n                            .map(async (key) => {\n                                const doc = result[key];\n                                if (doc.compressedContent) {\n                                    const decompressedContent = await decompressData(\n                                        base64ToArrayBuffer(doc.compressedContent)\n                                    );\n                                    return JSON.parse(decompressedContent);\n                                }\n                                return doc;\n                            })\n                    );\n                    resolve(data);\n                }\n            })\n        })\n    }\n\n    getById = async (id: string): Promise<Document> => {\n        return new Promise((resolve, reject) => {\n            this.db.get(id, async (result) => {\n                if (chrome.runtime.lastError) {\n                    reject(chrome.runtime.lastError)\n                } else {\n                    if (result[id].compressedContent) {\n                        const decompressedContent = await decompressData(\n                            base64ToArrayBuffer(result[id].compressedContent)\n                        );\n                        resolve(JSON.parse(decompressedContent));\n                    } else {\n                        resolve(result[id]);\n                    }\n                }\n            })\n        })\n    }\n\n    create = async (document: Document): Promise<void> => {\n        return new Promise(async (resolve, reject) => {\n            const documentString = JSON.stringify(document);\n            const compressedData = await compressText(documentString);\n            const base64Data = arrayBufferToBase64(compressedData);\n\n            this.db.set({\n                [document.id]: {\n                    id: document.id,\n                    title: document.title,\n                    status: document.status,\n                    db_type: document.db_type,\n                    createdAt: document.createdAt,\n                    compressedContent: base64Data\n                }\n            }, () => {\n                if (chrome.runtime.lastError) {\n                    reject(chrome.runtime.lastError)\n                } else {\n                    resolve()\n                }\n            })\n        })\n    }\n\n    update = async (document: Document): Promise<void> => {\n        return new Promise(async (resolve, reject) => {\n            const documentString = JSON.stringify(document);\n            const compressedData = await compressText(documentString);\n            const base64Data = arrayBufferToBase64(compressedData);\n\n            this.db.set({\n                [document.id]: {\n                    id: document.id,\n                    title: document.title,\n                    status: document.status,\n                    db_type: document.db_type,\n                    createdAt: document.createdAt,\n                    compressedContent: base64Data\n                }\n            }, () => {\n                if (chrome.runtime.lastError) {\n                    reject(chrome.runtime.lastError)\n                } else {\n                    resolve()\n                }\n            })\n        })\n    }\n\n    delete = async (id: string): Promise<void> => {\n        return new Promise((resolve, reject) => {\n            this.db.remove(id, () => {\n                if (chrome.runtime.lastError) {\n                    reject(chrome.runtime.lastError)\n                } else {\n                    resolve()\n                }\n            })\n        })\n    }\n\n    deleteSource = async (id: string, source_id: string): Promise<void> => {\n        return new Promise(async (resolve, reject) => {\n            try {\n                const data = await this.getById(id) as Document;\n                data.source = data.source.filter((s) => s.source_id !== source_id);\n                await this.update(data);\n                resolve();\n            } catch (error) {\n                reject(error);\n            }\n        })\n    }\n}\n\nexport const createDocument = async ({\n    source,\n    title,\n    embedding_model\n}: {\n    title: string\n    source: Source[]\n    embedding_model: string\n}) => {\n    const db = new PageAssistDocument()\n    const id = generateID()\n    const document: Document = {\n        id,\n        title,\n        db_type: \"document\",\n        source,\n        status: \"pending\",\n        document: {},\n        embedding_model,\n        createdAt: Date.now()\n    }\n    await db.create(document)\n    return document\n}\n\nexport const getDocumentById = async (id: string) => {\n    const db = new PageAssistDocument()\n    return db.getById(id)\n}\n\nexport const updateDocumentStatus = async (id: string, status: string) => {\n    const db = new PageAssistDocument()\n    const document = await db.getById(id)\n    if (status === \"finished\") {\n        document.source = document?.source?.map(e => ({\n            ...e,\n            content: undefined,\n        }))\n    }\n    await db.update({\n        ...document,\n        status\n    })\n}\n\nexport const addNewSources = async (id: string, source: Source[]) => {\n    await updateDocumentStatus(id, \"processing\")\n    const db = new PageAssistDocument()\n    const document = await db.getById(id)\n    await db.update({\n        ...document,\n        source: [...document.source, ...source]\n    })\n}\n\nexport const getAllDocuments = async (status?: string) => {\n    const db = new PageAssistDocument()\n    const data = await db.getAll()\n\n    if (status) {\n        return data\n            .filter((d) => d?.db_type === \"document\")\n            .filter((d) => d?.status === status)\n            .map((d) => {\n                d.source.forEach((s) => {\n                    delete s.content\n                })\n                return d\n            })\n            .sort((a, b) => b.createdAt - a.createdAt)\n    }\n\n    return data\n        .filter((d) => d?.db_type === \"document\")\n        .map((d) => {\n            d?.source.forEach((s) => {\n                delete s.content\n            })\n            return d\n        })\n        .sort((a, b) => b.createdAt - a.createdAt)\n}\n\nexport const deleteDocument = async (id: string) => {\n    const db = new PageAssistDocument()\n    await db.delete(id)\n    await deleteVector(`vector:${id}`)\n}\n\nexport const deleteSource = async (id: string, source_id: string) => {\n    const db = new PageAssistDocument()\n    await db.deleteSource(id, source_id)\n    await deleteVectorByFileId(`vector:${id}`, source_id)\n}\n\nexport const exportDocuments = async () => {\n    const db = new PageAssistDocument()\n    const data = await db.getAll()\n    return data\n}\n\nexport const importDocuments = async (data: Document[]) => {\n    const db = new PageAssistDocument()\n    for (const d of data) {\n        await db.create(d)\n    }\n}\n\nexport const updateDocumentbase = async ({ id, systemPrompt, title }: {\n    id: string,\n    title: string,\n    systemPrompt: string\n}) => {\n    const db = new PageAssistDocument()\n    const documentBase = await db.getById(id)\n    await db.update({\n        ...documentBase,\n        title,\n        systemPrompt\n    })\n}\n"
  },
  {
    "path": "src/db/index.ts",
    "content": "import {\n  type ChatHistory as ChatHistoryType,\n  type Message as MessageType\n} from \"~/store/option\"\nimport { getAllModelNicknames } from \"./nickname\"\nimport { ChatDocuments } from \"@/models/ChatTypes\"\ntype HistoryInfo = {\n  id: string\n  title: string\n  is_rag: boolean\n  message_source?: \"copilot\" | \"web-ui\"\n  is_pinned?: boolean\n  createdAt: number\n  doc_id?: string\n}\n\ntype WebSearch = {\n  search_engine: string\n  search_url: string\n  search_query: string\n  search_results: {\n    title: string\n    link: string\n  }[]\n}\n\ntype UploadedFile = {\n  id: string\n  filename: string\n  type: string\n  content: string\n  size: number\n  uploadedAt: number\n  embedding?: number[]\n  processed: boolean\n}\n\ntype SessionFiles = {\n  sessionId: string\n  files: UploadedFile[]\n  retrievalEnabled: boolean\n  createdAt: number\n}\n\ntype Message = {\n  id: string\n  history_id: string\n  name: string\n  role: string\n  content: string\n  images?: string[]\n  sources?: string[]\n  search?: WebSearch\n  createdAt: number\n  reasoning_time_taken?: number\n  messageType?: string\n  generationInfo?: any\n  modelName?: string\n  modelImage?: string\n  documents?: ChatDocuments\n}\nfunction simpleFuzzyMatch(text: string, query: string): boolean {\n  if (!text || !query) {\n    return false\n  }\n\n  const lowerText = text.toLowerCase()\n  const lowerQuery = query.toLowerCase().trim()\n\n  if (lowerQuery === \"\") {\n    return true\n  }\n\n  if (lowerText.includes(lowerQuery)) {\n    return true\n  }\n\n  const queryWords = lowerQuery.split(/\\s+/).filter((word) => word.length > 2)\n\n  if (queryWords.length > 1) {\n    const matchedWords = queryWords.filter((word) => lowerText.includes(word))\n    return matchedWords.length >= Math.ceil(queryWords.length * 0.7)\n  }\n\n  if (lowerQuery.length > 3) {\n    const maxDistance = Math.floor(lowerQuery.length * 0.3)\n\n    const textWords = lowerText.split(/\\s+/)\n    return textWords.some((word) => {\n      if (Math.abs(word.length - lowerQuery.length) > maxDistance) {\n        return false\n      }\n      let matches = 0\n      for (let i = 0; i < lowerQuery.length; i++) {\n        if (word.includes(lowerQuery[i])) {\n          matches++\n        }\n      }\n\n      return matches >= lowerQuery.length - maxDistance\n    })\n  }\n\n  return false\n}\n\ntype Webshare = {\n  id: string\n  title: string\n  url: string\n  api_url: string\n  share_id: string\n  createdAt: number\n}\n\ntype Prompt = {\n  id: string\n  title: string\n  content: string\n  is_system: boolean\n  createdBy?: string\n  createdAt: number\n}\n\ntype MessageHistory = Message[]\n\ntype ChatHistory = HistoryInfo[]\n\ntype Prompts = Prompt[]\n\nexport class PageAssitDatabase {\n  db: chrome.storage.StorageArea\n\n  constructor() {\n    this.db = chrome.storage.local\n  }\n\n  async getSessionFiles(sessionId: string): Promise<UploadedFile[]> {\n    return new Promise((resolve) => {\n      this.db.get(`session_files_${sessionId}`, (result) => {\n        const sessionFiles = result[\n          `session_files_${sessionId}`\n        ] as SessionFiles\n        resolve(sessionFiles?.files || [])\n      })\n    })\n  }\n\n  async getSessionFilesInfo(sessionId: string): Promise<SessionFiles | null> {\n    return new Promise((resolve) => {\n      this.db.get(`session_files_${sessionId}`, (result) => {\n        resolve(result[`session_files_${sessionId}`] || null)\n      })\n    })\n  }\n\n  async addFileToSession(sessionId: string, file: UploadedFile) {\n    const sessionFiles = await this.getSessionFilesInfo(sessionId)\n    const updatedFiles = sessionFiles ? [...sessionFiles.files, file] : [file]\n    const sessionData: SessionFiles = {\n      sessionId,\n      files: updatedFiles,\n      retrievalEnabled: sessionFiles?.retrievalEnabled || false,\n      createdAt: sessionFiles?.createdAt || Date.now()\n    }\n    this.db.set({ [`session_files_${sessionId}`]: sessionData })\n  }\n\n  async removeFileFromSession(sessionId: string, fileId: string) {\n    const sessionFiles = await this.getSessionFilesInfo(sessionId)\n    if (sessionFiles) {\n      const updatedFiles = sessionFiles.files.filter((f) => f.id !== fileId)\n      const sessionData: SessionFiles = {\n        ...sessionFiles,\n        files: updatedFiles\n      }\n      this.db.set({ [`session_files_${sessionId}`]: sessionData })\n    }\n  }\n\n  async updateFileInSession(\n    sessionId: string,\n    fileId: string,\n    updates: Partial<UploadedFile>\n  ) {\n    const sessionFiles = await this.getSessionFilesInfo(sessionId)\n    if (sessionFiles) {\n      const updatedFiles = sessionFiles.files.map((f) =>\n        f.id === fileId ? { ...f, ...updates } : f\n      )\n      const sessionData: SessionFiles = {\n        ...sessionFiles,\n        files: updatedFiles\n      }\n      this.db.set({ [`session_files_${sessionId}`]: sessionData })\n    }\n  }\n\n  async setRetrievalEnabled(sessionId: string, enabled: boolean) {\n    const sessionFiles = await this.getSessionFilesInfo(sessionId)\n    const sessionData: SessionFiles = {\n      sessionId,\n      files: sessionFiles?.files || [],\n      retrievalEnabled: enabled,\n      createdAt: sessionFiles?.createdAt || Date.now()\n    }\n    this.db.set({ [`session_files_${sessionId}`]: sessionData })\n  }\n\n  async clearSessionFiles(sessionId: string) {\n    this.db.remove(`session_files_${sessionId}`)\n  }\n\n  async getChatHistory(id: string): Promise<MessageHistory> {\n    const modelNicknames = await getAllModelNicknames()\n    return new Promise((resolve, reject) => {\n      this.db.get(id, (result) => {\n        resolve(\n          (result[id] || []).map((message: any) => {\n            return {\n              ...message,\n              modelName:\n                modelNicknames[message.name]?.model_name || message.name,\n              modelImage:\n                modelNicknames[message.name]?.model_avatar || undefined\n            }\n          })\n        )\n      })\n    })\n  }\n\n  async getChatHistories(): Promise<ChatHistory> {\n    return new Promise((resolve, reject) => {\n      this.db.get(\"chatHistories\", (result) => {\n        resolve(result.chatHistories || [])\n      })\n    })\n  }\n\n  async getChatHistoryTitleById(id: string): Promise<string> {\n    const chatHistories = await this.getChatHistories()\n    const chatHistory = chatHistories.find((history) => history.id === id)\n    return chatHistory?.title || \"\"\n  }\n\n  async addChatHistory(history: HistoryInfo) {\n    const chatHistories = await this.getChatHistories()\n    const newChatHistories = [history, ...chatHistories]\n    this.db.set({ chatHistories: newChatHistories })\n  }\n\n  async addMessage(message: Message) {\n    const history_id = message.history_id\n    const chatHistory = await this.getChatHistory(history_id)\n    const newChatHistory = [message, ...chatHistory]\n    await this.db.set({ [history_id]: newChatHistory })\n  }\n\n  async updateMessage(history_id: string, message_id: string, content: string) {\n    const chatHistory = await this.getChatHistory(history_id)\n    const newChatHistory = chatHistory.map((message) => {\n      if (message.id === message_id) {\n        message.content = content\n      }\n      return message\n    })\n    this.db.set({ [history_id]: newChatHistory })\n  }\n\n  async removeChatHistory(id: string) {\n    const chatHistories = await this.getChatHistories()\n    const newChatHistories = chatHistories.filter(\n      (history) => history.id !== id\n    )\n    this.db.set({ chatHistories: newChatHistories })\n  }\n\n  async removeMessage(history_id: string, message_id: string) {\n    const chatHistory = await this.getChatHistory(history_id)\n    const newChatHistory = chatHistory.filter(\n      (message) => message.id !== message_id\n    )\n    this.db.set({ [history_id]: newChatHistory })\n  }\n\n  async clear() {\n    this.db.clear()\n  }\n\n  async deleteChatHistory(id: string) {\n    const chatHistories = await this.getChatHistories()\n    const newChatHistories = chatHistories.filter(\n      (history) => history.id !== id\n    )\n    this.db.set({ chatHistories: newChatHistories })\n    this.db.remove(id)\n  }\n\n  async deleteAllChatHistory() {\n    const chatHistories = await this.getChatHistories()\n    chatHistories.forEach((history) => {\n      this.db.remove(history.id)\n    })\n    this.db.set({ chatHistories: [] })\n  }\n\n  async deleteMessage(history_id: string) {\n    await this.db.remove(history_id)\n  }\n\n  async getAllPrompts(): Promise<Prompts> {\n    return new Promise((resolve, reject) => {\n      this.db.get(\"prompts\", (result) => {\n        resolve(result.prompts || [])\n      })\n    })\n  }\n\n  async bulkAddPrompts(prompts: Prompt[]) {\n    await this.db.set({ prompts: [] })\n    await this.db.set({ prompts: prompts })\n  }\n\n  async addPrompt(prompt: Prompt) {\n    const prompts = await this.getAllPrompts()\n    const newPrompts = [prompt, ...prompts]\n    this.db.set({ prompts: newPrompts })\n  }\n\n  async deletePrompt(id: string) {\n    const prompts = await this.getAllPrompts()\n    const newPrompts = prompts.filter((prompt) => prompt.id !== id)\n    this.db.set({ prompts: newPrompts })\n  }\n\n  async updatePrompt(\n    id: string,\n    title: string,\n    content: string,\n    is_system: boolean\n  ) {\n    const prompts = await this.getAllPrompts()\n    const newPrompts = prompts.map((prompt) => {\n      if (prompt.id === id) {\n        prompt.title = title\n        prompt.content = content\n        prompt.is_system = is_system\n      }\n      return prompt\n    })\n    this.db.set({ prompts: newPrompts })\n  }\n\n  async getPromptById(id: string) {\n    const prompts = await this.getAllPrompts()\n    return prompts.find((prompt) => prompt.id === id)\n  }\n\n  async getWebshare(id: string) {\n    return new Promise((resolve, reject) => {\n      this.db.get(id, (result) => {\n        resolve(result[id] || [])\n      })\n    })\n  }\n\n  async getAllWebshares(): Promise<Webshare[]> {\n    return new Promise((resolve, reject) => {\n      this.db.get(\"webshares\", (result) => {\n        resolve(result.webshares || [])\n      })\n    })\n  }\n\n  async addWebshare(webshare: Webshare) {\n    const webshares = await this.getAllWebshares()\n    const newWebshares = [webshare, ...webshares]\n    this.db.set({ webshares: newWebshares })\n  }\n\n  async deleteWebshare(id: string) {\n    const webshares = await this.getAllWebshares()\n    const newWebshares = webshares.filter((webshare) => webshare.id !== id)\n    this.db.set({ webshares: newWebshares })\n  }\n\n  async getUserID() {\n    return new Promise((resolve, reject) => {\n      this.db.get(\"user_id\", (result) => {\n        resolve(result.user_id || \"\")\n      })\n    })\n  }\n\n  async setUserID(id: string) {\n    this.db.set({ user_id: id })\n  }\n\n  async searchChatHistories(query: string): Promise<ChatHistory> {\n    const normalizedQuery = query.toLowerCase().trim()\n    if (!normalizedQuery) {\n      return this.getChatHistories()\n    }\n\n    const allHistories = await this.getChatHistories()\n    const matchedHistories: ChatHistory = []\n    const matchedHistoryIds = new Set<string>()\n\n    for (const history of allHistories) {\n      if (simpleFuzzyMatch(history.title, normalizedQuery)) {\n        if (!matchedHistoryIds.has(history.id)) {\n          matchedHistories.push(history)\n          matchedHistoryIds.add(history.id)\n        }\n        continue\n      }\n\n      try {\n        const messages = await this.getChatHistory(history.id)\n        for (const message of messages) {\n          if (\n            message.content &&\n            simpleFuzzyMatch(message.content, normalizedQuery)\n          ) {\n            if (!matchedHistoryIds.has(history.id)) {\n              matchedHistories.push(history)\n              matchedHistoryIds.add(history.id)\n            }\n            break\n          }\n        }\n      } catch (error) {\n        console.error(\n          `Error fetching messages for history ${history.id}:`,\n          error\n        )\n      }\n    }\n\n    return matchedHistories\n  }\n}\n\nexport const generateID = () => {\n  return \"pa_xxxx-xxxx-xxx-xxxx\".replace(/[x]/g, () => {\n    const r = Math.floor(Math.random() * 16)\n    return r.toString(16)\n  })\n}\n\nexport const saveHistory = async (\n  title: string,\n  is_rag?: boolean,\n  message_source?: \"copilot\" | \"web-ui\",\n  doc_id?: string\n) => {\n  const id = generateID()\n  const createdAt = Date.now()\n  const history = { id, title, createdAt, is_rag, message_source, doc_id }\n  const db = new PageAssitDatabase()\n  await db.addChatHistory(history)\n  return history\n}\n\nexport const updateMessage = async (\n  history_id: string,\n  message_id: string,\n  content: string\n) => {\n  const db = new PageAssitDatabase()\n  await db.updateMessage(history_id, message_id, content)\n}\n\nexport const saveMessage = async ({\n  content,\n  history_id,\n  name,\n  role,\n  images,\n  source,\n  generationInfo,\n  message_type,\n  modelImage,\n  modelName,\n  reasoning_time_taken,\n  time,\n  documents\n}: {\n  history_id: string\n  name: string\n  role: string\n  content: string\n  images: string[]\n  source?: any[]\n  time?: number\n  message_type?: string\n  generationInfo?: any\n  reasoning_time_taken?: number\n  modelName?: string\n  modelImage?: string\n  documents?: ChatDocuments\n}) => {\n  const id = generateID()\n  let createdAt = Date.now()\n  if (time) {\n    createdAt += time\n  }\n  const message = {\n    id,\n    history_id,\n    name,\n    role,\n    content,\n    images,\n    createdAt,\n    sources: source,\n    messageType: message_type,\n    generationInfo: generationInfo,\n    reasoning_time_taken,\n    modelName,\n    modelImage,\n    documents\n  }\n  const db = new PageAssitDatabase()\n  await db.addMessage(message)\n  return message\n}\n\nexport const formatToChatHistory = (\n  messages: MessageHistory\n): ChatHistoryType => {\n  messages.sort((a, b) => a.createdAt - b.createdAt)\n  return messages.map((message) => {\n    return {\n      content: message.content,\n      role: message.role as \"user\" | \"assistant\" | \"system\",\n      images: message.images\n    }\n  })\n}\n\nexport const formatToMessage = (messages: MessageHistory): MessageType[] => {\n  messages.sort((a, b) => a.createdAt - b.createdAt)\n  return messages.map((message) => {\n    return {\n      isBot: message.role === \"assistant\",\n      message: message.content,\n      name: message.name,\n      sources: message?.sources || [],\n      images: message.images || [],\n      generationInfo: message?.generationInfo,\n      reasoning_time_taken: message?.reasoning_time_taken,\n      modelName: message?.modelName,\n      modelImage: message?.modelImage,\n      id: message.id,\n      documents: message?.documents\n    }\n  })\n}\n\nexport const deleteByHistoryId = async (history_id: string) => {\n  const db = new PageAssitDatabase()\n  await db.deleteMessage(history_id)\n  await db.removeChatHistory(history_id)\n  return history_id\n}\n\nexport const updateHistory = async (id: string, title: string) => {\n  const db = new PageAssitDatabase()\n  const chatHistories = await db.getChatHistories()\n  const newChatHistories = chatHistories.map((history) => {\n    if (history.id === id) {\n      history.title = title\n    }\n    return history\n  })\n  db.db.set({ chatHistories: newChatHistories })\n}\n\nexport const pinHistory = async (id: string, is_pinned: boolean) => {\n  const db = new PageAssitDatabase()\n  const chatHistories = await db.getChatHistories()\n  const newChatHistories = chatHistories.map((history) => {\n    if (history.id === id) {\n      history.is_pinned = is_pinned\n    }\n    return history\n  })\n  db.db.set({ chatHistories: newChatHistories })\n}\n\nexport const removeMessageUsingHistoryId = async (history_id: string) => {\n  const db = new PageAssitDatabase()\n  const chatHistory = await db.getChatHistory(history_id)\n  chatHistory.shift()\n  await db.db.set({ [history_id]: chatHistory })\n}\n\nexport const getAllPromptsFB = async () => {\n  const db = new PageAssitDatabase()\n  return await db.getAllPrompts()\n}\n\nexport const updateMessageByIndex = async (\n  history_id: string,\n  index: number,\n  message: string\n) => {\n  try {\n    const db = new PageAssitDatabase()\n    const chatHistory = (await db.getChatHistory(history_id)).reverse()\n    chatHistory[index].content = message\n    await db.db.set({ [history_id]: chatHistory.reverse() })\n  } catch (e) {\n    // temp chat will break\n  }\n}\n\nexport const deleteChatForEdit = async (history_id: string, index: number) => {\n  const db = new PageAssitDatabase()\n  const chatHistory = (await db.getChatHistory(history_id)).reverse()\n  const previousHistory = chatHistory.slice(0, index + 1)\n  await db.db.set({ [history_id]: previousHistory.reverse() })\n}\n\nexport const savePromptFB = async (prompt: any) => {\n  const db = new PageAssitDatabase()\n  await db.addPrompt(prompt)\n  return prompt\n}\n\nexport const deletePromptByIdFB = async (id: string) => {\n  const db = new PageAssitDatabase()\n  await db.deletePrompt(id)\n  return id\n}\n\nexport const updatePromptFB = async ({\n  content,\n  id,\n  title,\n  is_system\n}: {\n  id: string\n  title: string\n  content: string\n  is_system: boolean\n}) => {\n  const db = new PageAssitDatabase()\n  await db.updatePrompt(id, title, content, is_system)\n  return id\n}\n\nexport const getPromptById = async (id: string) => {\n  if (!id || id.trim() === \"\") return null\n  const db = new PageAssitDatabase()\n  return await db.getPromptById(id)\n}\n\nexport const getPromptByIdFB = async (id: string) => getPromptById(id)\n\nexport const getAllWebshares = async () => {\n  const db = new PageAssitDatabase()\n  return await db.getAllWebshares()\n}\n\nexport const deleteWebshare = async (id: string) => {\n  const db = new PageAssitDatabase()\n  await db.deleteWebshare(id)\n  return id\n}\n\nexport const saveWebshare = async ({\n  title,\n  url,\n  api_url,\n  share_id\n}: {\n  title: string\n  url: string\n  api_url: string\n  share_id: string\n}) => {\n  const db = new PageAssitDatabase()\n  const id = generateID()\n  const createdAt = Date.now()\n  const webshare = { id, title, url, share_id, createdAt, api_url }\n  await db.addWebshare(webshare)\n  return webshare\n}\n\nexport const getUserId = async () => {\n  const db = new PageAssitDatabase()\n  const id = (await db.getUserID()) as string\n  if (!id || id?.trim() === \"\") {\n    const user_id = \"user_xxxx-xxxx-xxx-xxxx-xxxx\".replace(/[x]/g, () => {\n      const r = Math.floor(Math.random() * 16)\n      return r.toString(16)\n    })\n    db.setUserID(user_id)\n    return user_id\n  }\n  return id\n}\n\nexport const exportChatHistory = async () => {\n  const db = new PageAssitDatabase()\n  const chatHistories = await db.getChatHistories()\n  const messages = await Promise.all(\n    chatHistories.map(async (history) => {\n      const messages = await db.getChatHistory(history.id)\n      return { history, messages }\n    })\n  )\n  return messages\n}\n\nexport const importChatHistory = async (\n  data: {\n    history: HistoryInfo\n    messages: MessageHistory\n  }[]\n) => {\n  const db = new PageAssitDatabase()\n  for (const { history, messages } of data) {\n    await db.addChatHistory(history)\n    for (const message of messages) {\n      await db.addMessage(message)\n    }\n  }\n}\n\nexport const exportPrompts = async () => {\n  const db = new PageAssitDatabase()\n  return await db.getAllPrompts()\n}\n\nexport const importPrompts = async (prompts: Prompts) => {\n  const db = new PageAssitDatabase()\n  for (const prompt of prompts) {\n    await db.addPrompt(prompt)\n  }\n}\n\nexport const getRecentChatFromCopilot = async () => {\n  const db = new PageAssitDatabase()\n  const chatHistories = await db.getChatHistories()\n  if (chatHistories.length === 0) return null\n  const history = chatHistories.find(\n    (history) => history.message_source === \"copilot\"\n  )\n  if (!history) return null\n\n  const messages = await db.getChatHistory(history.id)\n\n  return { history, messages }\n}\n\nexport const getRecentChatFromWebUI = async () => {\n  const db = new PageAssitDatabase()\n  const chatHistories = await db.getChatHistories()\n  if (chatHistories.length === 0) return null\n  const history = chatHistories.find(\n    (history) => history.message_source === \"web-ui\"\n  )\n  if (!history) return null\n\n  const messages = await db.getChatHistory(history.id)\n\n  return { history, messages }\n}\n\nexport const getTitleById = async (id: string) => {\n  const db = new PageAssitDatabase()\n  const title = await db.getChatHistoryTitleById(id)\n  return title\n}\n\nexport const getLastChatHistory = async (history_id: string) => {\n  const db = new PageAssitDatabase()\n  const messages = await db.getChatHistory(history_id)\n  messages.sort((a, b) => a.createdAt - b.createdAt)\n  const lastMessage = messages[messages.length - 1]\n  return lastMessage?.role === \"assistant\"\n    ? lastMessage\n    : messages.findLast((m) => m.role === \"assistant\")\n}\n\nexport const deleteHistoriesByDateRange = async (\n  rangeLabel: string\n): Promise<string[]> => {\n  const db = new PageAssitDatabase()\n  const allHistories = await db.getChatHistories()\n  const now = new Date()\n  const today = new Date(now.setHours(0, 0, 0, 0))\n  const yesterday = new Date(today)\n  yesterday.setDate(yesterday.getDate() - 1)\n  const lastWeek = new Date(today)\n  lastWeek.setDate(lastWeek.getDate() - 7)\n  let historiesToDelete: HistoryInfo[] = []\n  switch (rangeLabel) {\n    case \"today\":\n      historiesToDelete = allHistories.filter(\n        (item) => !item.is_pinned && new Date(item?.createdAt) >= today\n      )\n      break\n    case \"yesterday\":\n      historiesToDelete = allHistories.filter(\n        (item) =>\n          !item.is_pinned &&\n          new Date(item?.createdAt) >= yesterday &&\n          new Date(item?.createdAt) < today\n      )\n      break\n    case \"last7Days\":\n      historiesToDelete = allHistories.filter(\n        (item) =>\n          !item.is_pinned &&\n          new Date(item?.createdAt) >= lastWeek &&\n          new Date(item?.createdAt) < yesterday\n      )\n      break\n    case \"older\":\n      historiesToDelete = allHistories.filter(\n        (item) => !item.is_pinned && new Date(item?.createdAt) < lastWeek\n      )\n      break\n    case \"pinned\":\n      historiesToDelete = allHistories.filter((item) => item.is_pinned)\n      break\n    default:\n      return []\n  }\n\n  const deletedIds: string[] = []\n  for (const history of historiesToDelete) {\n    await db.deleteMessage(history.id)\n    await db.removeChatHistory(history.id)\n    deletedIds.push(history.id)\n  }\n\n  return deletedIds\n}\n\n// Session files helper functions\nexport const getSessionFiles = async (\n  sessionId: string\n): Promise<UploadedFile[]> => {\n  const db = new PageAssitDatabase()\n  return await db.getSessionFiles(sessionId)\n}\n\nexport const addFileToSession = async (\n  sessionId: string,\n  file: UploadedFile\n) => {\n  const db = new PageAssitDatabase()\n  await db.addFileToSession(sessionId, file)\n}\n\nexport const removeFileFromSession = async (\n  sessionId: string,\n  fileId: string\n) => {\n  const db = new PageAssitDatabase()\n  await db.removeFileFromSession(sessionId, fileId)\n}\n\nexport const updateFileInSession = async (\n  sessionId: string,\n  fileId: string,\n  updates: Partial<UploadedFile>\n) => {\n  const db = new PageAssitDatabase()\n  await db.updateFileInSession(sessionId, fileId, updates)\n}\n\nexport const setRetrievalEnabled = async (\n  sessionId: string,\n  enabled: boolean\n) => {\n  const db = new PageAssitDatabase()\n  await db.setRetrievalEnabled(sessionId, enabled)\n}\n\nexport const getSessionFilesInfo = async (\n  sessionId: string\n): Promise<SessionFiles | null> => {\n  const db = new PageAssitDatabase()\n  return await db.getSessionFilesInfo(sessionId)\n}\n\nexport const clearSessionFiles = async (sessionId: string) => {\n  const db = new PageAssitDatabase()\n  await db.clearSessionFiles(sessionId)\n}\n\nexport const bulkAddPromptsFB = async (prompts: Prompt[]) => {\n  const db = new PageAssitDatabase()\n  await db.bulkAddPrompts(prompts)\n}\n\nexport type { UploadedFile, SessionFiles }\n"
  },
  {
    "path": "src/db/knowledge.ts",
    "content": "import { deleteVector, deleteVectorByFileId } from \"./vector\"\n\nexport type Source = {\n  source_id: string\n  type: string\n  filename?: string\n  content: string\n  // Optional metadata to indicate how this source was added (e.g., 'text_input' or 'file_upload')\n  sourceType?: string\n}\n\nexport type Knowledge = {\n  id: string\n  db_type: string\n  title: string\n  status: string\n  embedding_model: string\n  source: Source[]\n  knownledge: any\n  createdAt: number\n  systemPrompt?: string,\n  followupPrompt?: string\n}\nexport const generateID = () => {\n  return \"pa_knowledge_xxxx-xxxx-xxx-xxxx\".replace(/[x]/g, () => {\n    const r = Math.floor(Math.random() * 16)\n    return r.toString(16)\n  })\n}\nexport class PageAssistKnowledge {\n  db: chrome.storage.StorageArea\n\n  constructor() {\n    this.db = chrome.storage.local\n  }\n\n  getAll = async (): Promise<Knowledge[]> => {\n    return new Promise((resolve, reject) => {\n      this.db.get(null, (result) => {\n        if (chrome.runtime.lastError) {\n          reject(chrome.runtime.lastError)\n        } else {\n          const data = Object.keys(result).map((key) => result[key])\n          resolve(data)\n        }\n      })\n    })\n  }\n\n  getById = async (id: string): Promise<Knowledge> => {\n    return new Promise((resolve, reject) => {\n      this.db.get(id, (result) => {\n        if (chrome.runtime.lastError) {\n          reject(chrome.runtime.lastError)\n        } else {\n          resolve(result[id])\n        }\n      })\n    })\n  }\n\n  create = async (knowledge: Knowledge): Promise<void> => {\n    return new Promise((resolve, reject) => {\n      this.db.set({ [knowledge.id]: knowledge }, () => {\n        if (chrome.runtime.lastError) {\n          reject(chrome.runtime.lastError)\n        } else {\n          resolve()\n        }\n      })\n    })\n  }\n\n  update = async (knowledge: Knowledge): Promise<void> => {\n    return new Promise((resolve, reject) => {\n      this.db.set({ [knowledge.id]: knowledge }, () => {\n        if (chrome.runtime.lastError) {\n          reject(chrome.runtime.lastError)\n        } else {\n          resolve()\n        }\n      })\n    })\n  }\n\n  delete = async (id: string): Promise<void> => {\n    return new Promise((resolve, reject) => {\n      this.db.remove(id, () => {\n        if (chrome.runtime.lastError) {\n          reject(chrome.runtime.lastError)\n        } else {\n          resolve()\n        }\n      })\n    })\n  }\n\n  deleteSource = async (id: string, source_id: string): Promise<void> => {\n    return new Promise((resolve, reject) => {\n      this.db.get(id, (result) => {\n        if (chrome.runtime.lastError) {\n          reject(chrome.runtime.lastError)\n        } else {\n          const data = result[id] as Knowledge\n          data.source = data.source.filter((s) => s.source_id !== source_id)\n          this.db.set({ [id]: data }, () => {\n            if (chrome.runtime.lastError) {\n              reject(chrome.runtime.lastError)\n            } else {\n              resolve()\n            }\n          })\n        }\n      })\n    })\n  }\n}\n\nexport const createKnowledge = async ({\n  source,\n  title,\n  embedding_model\n}: {\n  title: string\n  source: Source[]\n  embedding_model: string\n}) => {\n  const db = new PageAssistKnowledge()\n  const id = generateID()\n  const knowledge: Knowledge = {\n    id,\n    title,\n    db_type: \"knowledge\",\n    source,\n    status: \"pending\",\n    knownledge: {},\n    embedding_model,\n    createdAt: Date.now()\n  }\n  await db.create(knowledge)\n  return knowledge\n}\n\nexport const getKnowledgeById = async (id: string) => {\n  const db = new PageAssistKnowledge()\n  return db.getById(id)\n}\n\nexport const updateKnowledgeStatus = async (id: string, status: string) => {\n  const db = new PageAssistKnowledge()\n  const knowledge = await db.getById(id)\n  if (status === \"finished\") {\n    knowledge.source = knowledge?.source?.map(e => ({\n      ...e,\n      content: undefined,\n    }))\n  }\n  await db.update({\n    ...knowledge,\n    status\n  })\n}\n\n\nexport const addNewSources = async (id: string, source: Source[]) => {\n  await updateKnowledgeStatus(id, \"processing\")\n  const db = new PageAssistKnowledge()\n  const knowledge = await db.getById(id)\n  await db.update({\n    ...knowledge,\n    source: [...knowledge.source, ...source]\n  })\n}\n\n\nexport const getAllKnowledge = async (status?: string) => {\n  const db = new PageAssistKnowledge()\n  const data = await db.getAll()\n\n  if (status) {\n    return data\n      .filter((d) => d?.db_type === \"knowledge\")\n      .filter((d) => d?.status === status)\n      .map((d) => {\n        d.source.forEach((s) => {\n          delete s.content\n        })\n        return d\n      })\n      .sort((a, b) => b.createdAt - a.createdAt)\n  }\n\n  return data\n    .filter((d) => d?.db_type === \"knowledge\")\n    .map((d) => {\n      d?.source.forEach((s) => {\n        delete s.content\n      })\n      return d\n    })\n    .sort((a, b) => b.createdAt - a.createdAt)\n}\n\nexport const deleteKnowledge = async (id: string) => {\n  const db = new PageAssistKnowledge()\n  await db.delete(id)\n  await deleteVector(`vector:${id}`)\n}\n\nexport const deleteSource = async (id: string, source_id: string) => {\n  const db = new PageAssistKnowledge()\n  await db.deleteSource(id, source_id)\n  await deleteVectorByFileId(`vector:${id}`, source_id)\n}\n\nexport const exportKnowledge = async () => {\n  const db = new PageAssistKnowledge()\n  const data = await db.getAll()\n  return data\n}\n\nexport const importKnowledge = async (data: Knowledge[]) => {\n  const db = new PageAssistKnowledge()\n  for (const d of data) {\n    await db.create(d)\n  }\n}\n\nexport const updateKnowledgebase = async ({ id, systemPrompt, title }: {\n  id: string,\n  title: string,\n  systemPrompt: string\n}) => {\n  const kb = new PageAssistKnowledge()\n  const knowledgeBase = await kb.getById(id)\n  await kb.update({\n    ...knowledgeBase,\n    title,\n    systemPrompt\n  })\n}"
  },
  {
    "path": "src/db/models.ts",
    "content": "import { getAllOpenAIModels } from \"@/libs/openai\"\nimport {\n  getAllOpenAIConfigFB,\n  getOpenAIConfigById as providerInfo\n} from \"./openai\"\nimport { getAllModelNicknames } from \"./nickname\"\n\ntype Model = {\n  id: string\n  model_id: string\n  name: string\n  model_name?: string\n  model_image?: string\n  provider_id: string\n  lookup: string\n  model_type: string\n  db_type: string\n}\nexport const generateID = () => {\n  return \"model-xxxx-xxxx-xxx-xxxx\".replace(/[x]/g, () => {\n    const r = Math.floor(Math.random() * 16)\n    return r.toString(16)\n  })\n}\n\nexport const removeModelSuffix = (id: string) => {\n  return id\n    .replace(/_model-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{3,4}-[a-f0-9]{4}/, \"\")\n    .replace(/_lmstudio_openai-[a-f0-9]{4}-[a-f0-9]{3}-[a-f0-9]{4}/, \"\")\n    .replace(/_llamafile_openai-[a-f0-9]{4}-[a-f0-9]{3}-[a-f0-9]{4}/, \"\")\n    .replace(/_ollama2_openai-[a-f0-9]{4}-[a-f0-9]{3}-[a-f0-9]{4}/, \"\")\n    .replace(/_llamacpp_openai-[a-f0-9]{4}-[a-f0-9]{3}-[a-f0-9]{4}/, \"\")\n}\nexport const isLMStudioModel = (model: string) => {\n  const lmstudioModelRegex =\n    /_lmstudio_openai-[a-f0-9]{4}-[a-f0-9]{3}-[a-f0-9]{4}/\n  return lmstudioModelRegex.test(model)\n}\n\nexport const isLlamafileModel = (model: string) => {\n  const llamafileModelRegex =\n    /_llamafile_openai-[a-f0-9]{4}-[a-f0-9]{3}-[a-f0-9]{4}/\n  return llamafileModelRegex.test(model)\n}\n\nexport const isLLamaCppModel = (model: string) => {\n  const llamaCppModelRegex =\n    /_llamacpp_openai-[a-f0-9]{4}-[a-f0-9]{3}-[a-f0-9]{4}/\n  return llamaCppModelRegex.test(model)\n}\n\nexport const isOllamaModel = (model: string) => {\n  const ollamaModelRegex = /_ollama2_openai-[a-f0-9]{4}-[a-f0-9]{3}-[a-f0-9]{4}/\n  return ollamaModelRegex.test(model)\n}\nexport const getLMStudioModelId = (\n  model: string\n): { model_id: string; provider_id: string } => {\n  const lmstudioModelRegex =\n    /_lmstudio_openai-[a-f0-9]{4}-[a-f0-9]{3}-[a-f0-9]{4}/\n  const match = model.match(lmstudioModelRegex)\n  if (match) {\n    const modelId = match[0]\n    const providerId = match[0].replace(\"_lmstudio_openai-\", \"\")\n    return { model_id: modelId, provider_id: providerId }\n  }\n  return null\n}\nexport const getOllamaModelId = (\n  model: string\n): { model_id: string; provider_id: string } => {\n  const ollamaModelRegex = /_ollama2_openai-[a-f0-9]{4}-[a-f0-9]{3}-[a-f0-9]{4}/\n  const match = model.match(ollamaModelRegex)\n  if (match) {\n    const modelId = match[0]\n    const providerId = match[0].replace(\"_ollama2_openai-\", \"\")\n    return { model_id: modelId, provider_id: providerId }\n  }\n  return null\n}\nexport const getLlamafileModelId = (\n  model: string\n): { model_id: string; provider_id: string } => {\n  const llamafileModelRegex =\n    /_llamafile_openai-[a-f0-9]{4}-[a-f0-9]{3}-[a-f0-9]{4}/\n  const match = model.match(llamafileModelRegex)\n  if (match) {\n    const modelId = match[0]\n    const providerId = match[0].replace(\"_llamafile_openai-\", \"\")\n    return { model_id: modelId, provider_id: providerId }\n  }\n  return null\n}\n\nexport const getLLamaCppModelId = (\n  model: string\n): { model_id: string; provider_id: string } => {\n  const llamaCppModelRegex =\n    /_llamacpp_openai-[a-f0-9]{4}-[a-f0-9]{3}-[a-f0-9]{4}/\n  const match = model.match(llamaCppModelRegex)\n  if (match) {\n    const modelId = match[0]\n    const providerId = match[0].replace(\"_llamacpp_openai-\", \"\")\n    return { model_id: modelId, provider_id: providerId }\n  }\n  return null\n}\n\nexport const isCustomModel = (model: string) => {\n  if (isLMStudioModel(model)) {\n    return true\n  }\n\n  if (isLlamafileModel(model)) {\n    return true\n  }\n\n  if (isOllamaModel(model)) {\n    return true\n  }\n\n  if (isLLamaCppModel(model)) {\n    return true\n  }\n\n  const customModelRegex =\n    /_model-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{3,4}-[a-f0-9]{4}/\n  return customModelRegex.test(model)\n}\nexport class ModelDb {\n  db: chrome.storage.StorageArea\n\n  constructor() {\n    this.db = chrome.storage.local\n  }\n\n  getAll = async (): Promise<Model[]> => {\n    return new Promise((resolve, reject) => {\n      this.db.get(null, (result) => {\n        if (chrome.runtime.lastError) {\n          reject(chrome.runtime.lastError)\n        } else {\n          const data = Object.keys(result).map((key) => result[key])\n          resolve(data)\n        }\n      })\n    })\n  }\n\n  create = async (model: Model): Promise<void> => {\n    return new Promise((resolve, reject) => {\n      this.db.set({ [model.id]: model }, () => {\n        if (chrome.runtime.lastError) {\n          reject(chrome.runtime.lastError)\n        } else {\n          resolve()\n        }\n      })\n    })\n  }\n\n  getById = async (id: string): Promise<Model> => {\n    return new Promise((resolve, reject) => {\n      this.db.get(id, (result) => {\n        if (chrome.runtime.lastError) {\n          reject(chrome.runtime.lastError)\n        } else {\n          resolve(result[id])\n        }\n      })\n    })\n  }\n\n  update = async (model: Model): Promise<void> => {\n    return new Promise((resolve, reject) => {\n      this.db.set({ [model.id]: model }, () => {\n        if (chrome.runtime.lastError) {\n          reject(chrome.runtime.lastError)\n        } else {\n          resolve()\n        }\n      })\n    })\n  }\n\n  delete = async (id: string): Promise<void> => {\n    return new Promise((resolve, reject) => {\n      this.db.remove(id, () => {\n        if (chrome.runtime.lastError) {\n          reject(chrome.runtime.lastError)\n        } else {\n          resolve()\n        }\n      })\n    })\n  }\n\n  deleteAll = async (): Promise<void> => {\n    return new Promise((resolve, reject) => {\n      this.db.clear(() => {\n        if (chrome.runtime.lastError) {\n          reject(chrome.runtime.lastError)\n        } else {\n          resolve()\n        }\n      })\n    })\n  }\n}\n\nexport const createManyModels = async (\n  data: {\n    model_id: string\n    name: string\n    provider_id: string\n    model_type: string\n  }[]\n) => {\n  const db = new ModelDb()\n\n  const models = data.map((item) => {\n    return {\n      ...item,\n      lookup: `${item.model_id}_${item.provider_id}`,\n      id: `${item.model_id}_${generateID()}`,\n      db_type: \"openai_model\",\n      name: item.name.replaceAll(/accounts\\/[^\\/]+\\/models\\//g, \"\")\n    }\n  })\n\n  for (const model of models) {\n    const isExist = await isLookupExist(model.lookup)\n\n    if (isExist) {\n      continue\n    }\n\n    await db.create(model)\n  }\n}\n\nexport const createModelFB = async (model: any) => {\n  try {\n    const db = new ModelDb()\n    await db.create(model)\n  } catch (e) {}\n}\n\nexport const getAllModelsExT = async () => {\n  const db = new ModelDb()\n  const allData = await db.getAll()\n  return allData?.filter((d) => d?.db_type === \"openai_model\") || []\n}\n\nexport const getModelInfoFB = async (id: string) => {\n  const db = new ModelDb()\n\n  if (isLMStudioModel(id)) {\n    const lmstudioId = getLMStudioModelId(id)\n    if (!lmstudioId) {\n      throw new Error(\"Invalid LMStudio model ID\")\n    }\n    return {\n      model_id: id.replace(\n        /_lmstudio_openai-[a-f0-9]{4}-[a-f0-9]{3}-[a-f0-9]{4}/,\n        \"\"\n      ),\n      provider_id: `openai-${lmstudioId.provider_id}`,\n      name: id.replace(\n        /_lmstudio_openai-[a-f0-9]{4}-[a-f0-9]{3}-[a-f0-9]{4}/,\n        \"\"\n      )\n    }\n  }\n\n  if (isLlamafileModel(id)) {\n    const llamafileId = getLlamafileModelId(id)\n    if (!llamafileId) {\n      throw new Error(\"Invalid LMStudio model ID\")\n    }\n    return {\n      model_id: id.replace(\n        /_llamafile_openai-[a-f0-9]{4}-[a-f0-9]{3}-[a-f0-9]{4}/,\n        \"\"\n      ),\n      provider_id: `openai-${llamafileId.provider_id}`,\n      name: id.replace(\n        /_llamafile_openai-[a-f0-9]{4}-[a-f0-9]{3}-[a-f0-9]{4}/,\n        \"\"\n      )\n    }\n  }\n\n  if (isLLamaCppModel(id)) {\n    const llamaCppId = getLLamaCppModelId(id)\n    if (!llamaCppId) {\n      throw new Error(\"Invalid LMStudio model ID\")\n    }\n\n    return {\n      model_id: id.replace(\n        /_llamacpp_openai-[a-f0-9]{4}-[a-f0-9]{3}-[a-f0-9]{4}/,\n        \"\"\n      ),\n      provider_id: `openai-${llamaCppId.provider_id}`,\n      name: id.replace(\n        /_llamacpp_openai-[a-f0-9]{4}-[a-f0-9]{3}-[a-f0-9]{4}/,\n        \"\"\n      )\n    }\n  }\n\n  if (isOllamaModel(id)) {\n    const ollamaId = getOllamaModelId(id)\n    if (!ollamaId) {\n      throw new Error(\"Invalid LMStudio model ID\")\n    }\n    return {\n      model_id: id.replace(\n        /_ollama2_openai-[a-f0-9]{4}-[a-f0-9]{3}-[a-f0-9]{4}/,\n        \"\"\n      ),\n      provider_id: `openai-${ollamaId.provider_id}`,\n      name: id.replace(\n        /_ollama2_openai-[a-f0-9]{4}-[a-f0-9]{3}-[a-f0-9]{4}/,\n        \"\"\n      )\n    }\n  }\n\n  const model = await db.getById(id)\n  return model\n}\n\nexport const getAllCustomModelsFB = async () => {\n  const db = new ModelDb()\n  const modelNicknames = await getAllModelNicknames()\n  const models = (await db.getAll()).filter(\n    (model) => model?.db_type === \"openai_model\"\n  )\n  const modelsWithProvider = await Promise.all(\n    models.map(async (model) => {\n      const provider = await providerInfo(model.provider_id)\n      return { ...model, provider }\n    })\n  )\n\n  return modelsWithProvider.map((model) => {\n    return {\n      ...model,\n      nickname: modelNicknames[model.id]?.model_name || model.model_id,\n      avatar: modelNicknames[model.id]?.model_avatar || undefined\n    }\n  })\n}\n\nexport const deleteModelFB = async (id: string) => {\n  const db = new ModelDb()\n  await db.delete(id)\n}\n\nexport const deleteAllModelsByProviderId = async (provider_id: string) => {\n  const db = new ModelDb()\n  const models = await db.getAll()\n  const modelsToDelete = models.filter(\n    (model) => model.provider_id === provider_id\n  )\n  for (const model of modelsToDelete) {\n    await db.delete(model.id)\n  }\n}\n\nexport const bulkAddModelsFB = async (models: Model[]) => {\n  // delete all exist models\n  const db = new ModelDb()\n  const modelsToDelete = (await db.getAll()).filter(\n    (model) => model?.db_type === \"openai_model\"\n  )\n  for (const model of modelsToDelete) {\n    await db.delete(model.id)\n  }\n  // add new models\n  for (const model of models) {\n    await db.create(model)\n  }\n}\n\nexport const isLookupExist = async (lookup: string) => {\n  const db = new ModelDb()\n  const models = await db.getAll()\n  const model = models.find((model) => model?.lookup === lookup)\n  return model ? true : false\n}\n\nexport const dynamicFetchLMStudio = async ({\n  baseUrl,\n  providerId,\n  customHeaders = []\n}: {\n  baseUrl: string\n  providerId: string\n  customHeaders?: { key: string; value: string }[]\n}) => {\n  const models = await getAllOpenAIModels({ baseUrl, customHeaders })\n  const lmstudioModels = models.map((e) => {\n    return {\n      name: e?.name || e?.id,\n      id: `${e?.id}_lmstudio_${providerId}`,\n      provider: providerId,\n      lookup: `${e?.id}_${providerId}`,\n      provider_id: providerId\n    }\n  })\n\n  return lmstudioModels\n}\n\nexport const dynamicFetchLLamaCpp = async ({\n  baseUrl,\n  providerId,\n  customHeaders = []\n}: {\n  baseUrl: string\n  providerId: string\n  customHeaders?: { key: string; value: string }[]\n}) => {\n  const models = await getAllOpenAIModels({ baseUrl, customHeaders })\n  const llamaCppModels = models.map((e) => {\n    return {\n      name: e?.name || e?.id,\n      id: `${e?.id}_llamacpp_${providerId}`,\n      provider: providerId,\n      lookup: `${e?.id}_${providerId}`,\n      provider_id: providerId\n    }\n  })\n\n  return llamaCppModels\n}\n\nexport const dynamicFetchOllama2 = async ({\n  baseUrl,\n  providerId,\n  customHeaders = []\n}: {\n  baseUrl: string\n  providerId: string\n  customHeaders?: { key: string; value: string }[]\n}) => {\n \n\n  const models = await getAllOpenAIModels({ baseUrl, customHeaders })\n  const ollama2Models = models.map((e) => {\n    return {\n      name: e?.name || e?.id,\n      id: `${e?.id}_ollama2_${providerId}`,\n      provider: providerId,\n      lookup: `${e?.id}_${providerId}`,\n      provider_id: providerId\n    }\n  })\n\n  return ollama2Models\n}\n\nexport const dynamicFetchLlamafile = async ({\n  baseUrl,\n  providerId,\n  customHeaders = []\n}: {\n  baseUrl: string\n  providerId: string\n  customHeaders?: { key: string; value: string }[]\n}) => {\n  const models = await getAllOpenAIModels({ baseUrl, customHeaders })\n  const llamafileModels = models.map((e) => {\n    return {\n      name: e?.name || e?.id,\n      id: `${e?.id}_llamafile_${providerId}`,\n      provider: providerId,\n      lookup: `${e?.id}_${providerId}`,\n      provider_id: providerId\n    }\n  })\n\n  return llamafileModels\n}\n\nexport const ollamaFormatAllCustomModelsFallback = async (\n  modelType: \"all\" | \"chat\" | \"embedding\" = \"all\"\n) => {\n  try {\n    const [allModles, allProviders] = await Promise.all([\n      getAllCustomModelsFB(),\n      getAllOpenAIConfigFB()\n    ])\n    const modelNicknames = await getAllModelNicknames()\n    const lmstudioProviders = allProviders.filter(\n      (provider) => provider.provider === \"lmstudio\"\n    )\n\n    const llamafileProviders = allProviders.filter(\n      (provider) => provider.provider === \"llamafile\"\n    )\n\n    const ollamaProviders = allProviders.filter(\n      (provider) => provider.provider === \"ollama2\"\n    )\n\n    const llamacppProvider = allProviders.filter(\n      (model) => model.provider === \"llamacpp\"\n    )\n\n    const lmModelsPromises = lmstudioProviders.map((provider) =>\n      dynamicFetchLMStudio({\n        baseUrl: provider.baseUrl,\n        providerId: provider.id,\n        customHeaders: provider.headers\n      })\n    )\n\n    const llamafileModelsPromises = llamafileProviders.map((provider) =>\n      dynamicFetchLlamafile({\n        baseUrl: provider.baseUrl,\n        providerId: provider.id,\n        customHeaders: provider.headers\n      })\n    )\n\n    const ollamaModelsPromises = ollamaProviders.map((provider) =>\n      dynamicFetchOllama2({\n        baseUrl: provider.baseUrl,\n        providerId: provider.id,\n        customHeaders: provider.headers\n      })\n    )\n\n    const llamacppModelsPromises = llamacppProvider.map((provider) =>\n      dynamicFetchLLamaCpp({\n        baseUrl: provider.baseUrl,\n        providerId: provider.id,\n        customHeaders: provider.headers\n      })\n    )\n\n    const lmModelsFetch = await Promise.all(lmModelsPromises)\n\n    const llamafileModelsFetch = await Promise.all(llamafileModelsPromises)\n\n    const ollamaModelsFetch = await Promise.all(ollamaModelsPromises)\n\n    const llamacppModelsFetch = await Promise.all(llamacppModelsPromises)\n\n    const lmModels = lmModelsFetch.flat()\n\n    const llamafileModels = llamafileModelsFetch.flat()\n\n    const ollama2Models = ollamaModelsFetch.flat()\n\n    const llamacppModels = llamacppModelsFetch.flat()\n\n    // merge allModels and lmModels\n    const allModlesWithLMStudio = [\n      ...(modelType !== \"all\"\n        ? allModles.filter((model) => model.model_type === modelType)\n        : allModles),\n      ...lmModels,\n      ...llamafileModels,\n      ...ollama2Models,\n      ...llamacppModels\n    ]\n\n    const ollamaModels = allModlesWithLMStudio.map((model) => {\n      return {\n        name: model.name,\n        model: model.id,\n        modified_at: \"\",\n        provider:\n          allProviders.find((provider) => provider.id === model.provider_id)\n            ?.provider || \"custom\",\n        size: 0,\n        digest: \"\",\n        details: {\n          parent_model: \"\",\n          format: \"\",\n          family: \"\",\n          families: [],\n          parameter_size: \"\",\n          quantization_level: \"\"\n        }\n      }\n    })\n\n    return ollamaModels.map((model) => {\n      return {\n        ...model,\n        nickname: modelNicknames[model.model]?.model_name || model.name,\n        avatar: modelNicknames[model.model]?.model_avatar || undefined\n      }\n    })\n  } catch (e) {\n    console.error(e)\n    return []\n  }\n}\n"
  },
  {
    "path": "src/db/nickname.ts",
    "content": "import { Storage } from \"@plasmohq/storage\"\nimport { ModelNicknames } from \"./dexie/types\"\n\nexport class ModelNickname {\n    db: Storage\n    private KEY = \"modelNickname\"\n\n    constructor() {\n        this.db = new Storage({\n            area: \"local\"\n        })\n    }\n\n    async saveModelNickname(\n        model_id: string,\n        model_name: string,\n        model_avatar?: string\n    ): Promise<void> {\n        const modelNames = (await this.db.get(this.KEY)) || {}\n\n        modelNames[model_id] = {\n            model_name,\n            ...(model_avatar && { model_avatar })\n        }\n\n        await this.db.set(this.KEY, modelNames)\n    }\n\n    async getModelNicknameByID(model_id: string) {\n        const data = (await this.db.get(this.KEY)) || {}\n        return data[model_id]\n    }\n\n    async getAllModelNicknames() {\n        const data = (await this.db.get(this.KEY)) || {}\n        return data\n    }\n}\n\nexport const getAllModelNicknames = async () => {\n    const modelNickname = new ModelNickname()\n    return await modelNickname.getAllModelNicknames()\n}\nexport const getAllModelNicknamesMig = async () => {\n    const modelNickname = new ModelNickname()\n    const data = await modelNickname.getAllModelNicknames()\n    const result = []\n    for (const [model_id, value] of Object.entries(data)) {\n        result.push({\n            model_id,\n            //@ts-ignore\n            model_avatar: value?.model_avatar,\n            //@ts-ignore\n            model_name: value?.model_name\n        })\n    }\n    return result as ModelNicknames\n}\nexport const getModelNicknameByID = async (\n    model_id: string\n): Promise<{ model_name: string; model_avatar?: string } | null> => {\n    const modelNickname = new ModelNickname()\n    return await modelNickname.getModelNicknameByID(model_id)\n}\n\n\nexport const saveModelNickname = async (\n    {\n        model_id,\n        model_name,\n        model_avatar\n    }: {\n        model_id: string,\n        model_name: string,\n        model_avatar?: string\n    }\n) => {\n\n    const modelNickname = new ModelNickname()\n    return await modelNickname.saveModelNickname(model_id, model_name, model_avatar)\n}\n\n\n"
  },
  {
    "path": "src/db/openai.ts",
    "content": "import { cleanUrl } from \"@/libs/clean-url\"\nimport { deleteAllModelsByProviderId } from \"./models\"\nimport { OpenAIModelConfig } from \"./dexie/types\"\n\nexport const generateID = () => {\n  return \"openai-xxxx-xxx-xxxx\".replace(/[x]/g, () => {\n    const r = Math.floor(Math.random() * 16)\n    return r.toString(16)\n  })\n}\n\nexport class OpenAIModelDb {\n  db: chrome.storage.StorageArea\n\n  constructor() {\n    this.db = chrome.storage.local\n  }\n\n  getAll = async (): Promise<OpenAIModelConfig[]> => {\n    return new Promise((resolve, reject) => {\n      this.db.get(null, (result) => {\n        if (chrome.runtime.lastError) {\n          reject(chrome.runtime.lastError)\n        } else {\n          const data = Object.keys(result).map((key) => result[key])\n          resolve(data)\n        }\n      })\n    })\n  }\n\n  create = async (config: OpenAIModelConfig): Promise<void> => {\n    return new Promise((resolve, reject) => {\n      this.db.set({ [config.id]: config }, () => {\n        if (chrome.runtime.lastError) {\n          reject(chrome.runtime.lastError)\n        } else {\n          resolve()\n        }\n      })\n    })\n  }\n\n  getById = async (id: string): Promise<OpenAIModelConfig> => {\n    return new Promise((resolve, reject) => {\n      this.db.get(id, (result) => {\n        if (chrome.runtime.lastError) {\n          reject(chrome.runtime.lastError)\n        } else {\n          resolve(result[id])\n        }\n      })\n    })\n  }\n\n  update = async (config: OpenAIModelConfig): Promise<void> => {\n    return new Promise((resolve, reject) => {\n      this.db.set({ [config.id]: config }, () => {\n        if (chrome.runtime.lastError) {\n          reject(chrome.runtime.lastError)\n        } else {\n          resolve()\n        }\n      })\n    })\n  }\n\n  delete = async (id: string): Promise<void> => {\n    return new Promise((resolve, reject) => {\n      this.db.remove(id, () => {\n        if (chrome.runtime.lastError) {\n          reject(chrome.runtime.lastError)\n        } else {\n          resolve()\n        }\n      })\n    })\n  }\n}\n\nexport const addOpenAICofigFB = async (config: OpenAIModelConfig) => {\n  try {\n    const openaiDb = new OpenAIModelDb()\n    await openaiDb.create(config)\n    return config.id\n  } catch (e) {\n    console.error(\"Error adding OpenAI config:\", e)\n  }\n}\n\nexport const getAllOpenAIConfig = async () => {\n  const openaiDb = new OpenAIModelDb()\n  const configs = await openaiDb.getAll()\n  return configs.filter((config) => config?.db_type === \"openai\")\n}\n\nexport const getAllOpenAIConfigFB = async () => await getAllOpenAIConfig()\n\nexport const updateOpenAIConfigFB = async (config: OpenAIModelConfig) => {\n  const openaiDb = new OpenAIModelDb()\n  await openaiDb.update(config)\n  return config\n}\n\nexport const deleteOpenAIConfigFB = async (id: string) => {\n  const openaiDb = new OpenAIModelDb()\n  await openaiDb.delete(id)\n  await deleteAllModelsByProviderId(id)\n}\n\nexport const bulkAddOAIFB = async (configs: OpenAIModelConfig[]) => {\n  const openaiDb = new OpenAIModelDb()\n\n  const oaiToDelete = await getAllOpenAIConfigFB()\n\n  for (const config of oaiToDelete) {\n    await openaiDb.delete(config.id)\n  }\n\n  for (const config of configs) {\n    await openaiDb.create(config)\n  }\n}\n\nexport const updateOpenAIConfigApiKeyFB = async (config: OpenAIModelConfig) => {\n  const openaiDb = new OpenAIModelDb()\n  await openaiDb.update(config)\n}\n\nexport const getOpenAIConfigById = async (id: string) => {\n  const openaiDb = new OpenAIModelDb()\n  const config = await openaiDb.getById(id)\n  return config\n}\n\nexport const getOpenAIConfigByIdFB = async (id: string) =>\n  getOpenAIConfigById(id)\n"
  },
  {
    "path": "src/db/vector.ts",
    "content": "import { formatVector } from \"@/libs/export-import\"\n\ninterface PageAssistVector {\n  file_id: string\n  content: string\n  embedding: number[]\n  metadata: Record<string, any>\n}\n\nexport type VectorData = {\n  id: string\n  vectors: PageAssistVector[]\n}\n\nexport class PageAssistVectorDb {\n  db: chrome.storage.StorageArea\n\n  constructor() {\n    this.db = chrome.storage.local\n  }\n\n  insertVector = async (\n    id: string,\n    vector: PageAssistVector[]\n  ): Promise<void> => {\n    return new Promise((resolve, reject) => {\n      this.db.get(id, (result) => {\n        if (chrome.runtime.lastError) {\n          reject(chrome.runtime.lastError)\n        } else {\n          const data = result[id] as VectorData\n          if (!data) {\n            this.db.set({ [id]: { id, vectors: vector } }, () => {\n              if (chrome.runtime.lastError) {\n                reject(chrome.runtime.lastError)\n              } else {\n                resolve()\n              }\n            })\n          } else {\n            this.db.set(\n              {\n                [id]: {\n                  ...data,\n                  vectors: data.vectors.concat(vector)\n                }\n              },\n              () => {\n                if (chrome.runtime.lastError) {\n                  reject(chrome.runtime.lastError)\n                } else {\n                  resolve()\n                }\n              }\n            )\n          }\n        }\n      })\n    })\n  }\n\n  deleteVector = async (id: string): Promise<void> => {\n    return new Promise((resolve, reject) => {\n      this.db.remove(id, () => {\n        if (chrome.runtime.lastError) {\n          reject(chrome.runtime.lastError)\n        } else {\n          resolve()\n        }\n      })\n    })\n  }\n\n  deleteVectorByFileId = async (id: string, file_id: string): Promise<void> => {\n    return new Promise((resolve, reject) => {\n      this.db.get(id, (result) => {\n        if (chrome.runtime.lastError) {\n          reject(chrome.runtime.lastError)\n        } else {\n          const data = result[id] as VectorData\n          data.vectors = data.vectors.filter((v) => v.file_id !== file_id)\n          this.db.set({ [id]: data }, () => {\n            if (chrome.runtime.lastError) {\n              reject(chrome.runtime.lastError)\n            } else {\n              resolve()\n            }\n          })\n        }\n      })\n    })\n  }\n\n  getVector = async (id: string): Promise<VectorData> => {\n    return new Promise((resolve, reject) => {\n      this.db.get(id, (result) => {\n        if (chrome.runtime.lastError) {\n          reject(chrome.runtime.lastError)\n        } else {\n          resolve(result[id] as VectorData)\n        }\n      })\n    })\n  }\n\n  getAll = async (): Promise<VectorData[]> => {\n    return new Promise((resolve, reject) => {\n      this.db.get(null, (result) => {\n        if (chrome.runtime.lastError) {\n          reject(chrome.runtime.lastError)\n        } else {\n          resolve(Object.values(result))\n        }\n      })\n    })\n  }\n\n  saveImportedData = async (data: VectorData[]): Promise<void> => {\n    return new Promise((resolve, reject) => {\n      const obj: Record<string, VectorData> = {}\n      data.forEach((d) => {\n        obj[d.id] = d\n      })\n      this.db.set(obj, () => {\n        if (chrome.runtime.lastError) {\n          reject(chrome.runtime.lastError)\n        } else {\n          resolve()\n        }\n      })\n    })\n  }\n}\n\nexport const insertVector = async (\n  id: string,\n  vector: PageAssistVector[]\n): Promise<void> => {\n  const db = new PageAssistVectorDb()\n  return db.insertVector(id, vector)\n}\n\nexport const getAllVector = async () => {\n  const db = new PageAssistVectorDb()\n  const data =  await db.getAll()\n  return formatVector(data)\n}\n\nexport const getVector = async (id: string): Promise<VectorData> => {\n  const db = new PageAssistVectorDb()\n  return db.getVector(id)\n}\n\nexport const deleteVector = async (id: string): Promise<void> => {\n  const db = new PageAssistVectorDb()\n  return db.deleteVector(id)\n}\n\nexport const deleteVectorByFileId = async (\n  id: string,\n  file_id: string\n): Promise<void> => {\n  const db = new PageAssistVectorDb()\n  return db.deleteVectorByFileId(id, file_id)\n}\n\nexport const exportVectors = async () => {\n  const db = new PageAssistVectorDb()\n  const data = await db.getAll()\n  return data\n}\n\nexport const importVectors = async (data: VectorData[]) => {\n  const db = new PageAssistVectorDb()\n  return db.saveImportedData(data) \n}"
  },
  {
    "path": "src/entries/background.ts",
    "content": "import { getOllamaURL, isOllamaRunning } from \"../services/ollama\"\nimport { browser } from \"wxt/browser\"\nimport { clearBadge, streamDownload, cancelDownload } from \"@/utils/pull-ollama\"\nimport { Storage } from \"@plasmohq/storage\"\nimport { getInitialConfig } from \"@/services/action\"\nimport { getCustomCopilotPrompts, getCopilotPromptsEnabledState, type CustomCopilotPrompt } from \"@/services/application\"\nimport { startMcpOAuthFlow, disconnectMcpOAuth } from \"@/libs/mcp/oauth-flow\"\nimport { McpServerDb } from \"@/db/dexie/mcp\"\n\nexport default defineBackground({\n  main() {\n    const storage = new Storage({\n      area: \"local\"\n    })\n    let isCopilotRunning: boolean = false\n    let actionIconClick: string = \"webui\"\n    let contextMenuClick: string = \"sidePanel\"\n    const contextMenuId = {\n      webui: \"open-web-ui-pa\",\n      sidePanel: \"open-side-panel-pa\"\n    }\n\n    let customCopilotMenuIds: string[] = []\n    const builtinCopilotMenus = [\n      { id: \"summarize-pa\", key: \"summary\", title: \"Summarize\" },\n      { id: \"explain-pa\", key: \"explain\", title: \"Explain\" },\n      { id: \"rephrase-pa\", key: \"rephrase\", title: \"Rephrase\" },\n      { id: \"translate-pg\", key: \"translate\", title: \"Translate\" },\n      { id: \"custom-pg\", key: \"custom\", title: \"Custom\" }\n    ]\n\n    const createBuiltinCopilotMenus = async () => {\n      const enabledState = await getCopilotPromptsEnabledState()\n\n      for (const menu of builtinCopilotMenus) {\n        // Remove existing menu\n        try {\n          await browser.contextMenus.remove(menu.id)\n        } catch (e) {\n          // Menu might not exist, ignore\n        }\n\n        // Create menu only if enabled\n        if (enabledState[menu.key]) {\n          browser.contextMenus.create({\n            id: menu.id,\n            title: menu.title,\n            contexts: [\"selection\"]\n          })\n        }\n      }\n    }\n\n    const createCustomCopilotMenus = async () => {\n      // Remove existing custom copilot menus\n      for (const menuId of customCopilotMenuIds) {\n        try {\n          await browser.contextMenus.remove(menuId)\n        } catch (e) {\n          // Menu might not exist, ignore\n        }\n      }\n      customCopilotMenuIds = []\n\n      // Create new custom copilot menus\n      const customPrompts = await getCustomCopilotPrompts()\n      const enabledPrompts = customPrompts.filter(p => p.enabled)\n\n      for (const prompt of enabledPrompts) {\n        const menuId = `custom_copilot_${prompt.id}`\n        customCopilotMenuIds.push(menuId)\n        browser.contextMenus.create({\n          id: menuId,\n          title: prompt.title,\n          contexts: [\"selection\"]\n        })\n      }\n    }\n\n    const initialize = async () => {\n      try {\n        storage.watch({\n          actionIconClick: (value) => {\n            const oldValue = value?.oldValue || \"webui\"\n            const newValue = value?.newValue || \"webui\"\n            if (oldValue !== newValue) {\n              actionIconClick = newValue\n            }\n          },\n          contextMenuClick: (value) => {\n            const oldValue = value?.oldValue || \"sidePanel\"\n            const newValue = value?.newValue || \"sidePanel\"\n            if (oldValue !== newValue) {\n              contextMenuClick = newValue\n              browser.contextMenus.remove(contextMenuId[oldValue])\n              browser.contextMenus.create({\n                id: contextMenuId[newValue],\n                title: contextMenuTitle[newValue],\n                contexts: [\"page\", \"selection\"]\n              })\n            }\n          },\n          customCopilotPrompts: async () => {\n            // Recreate custom copilot menus when prompts change\n            await createCustomCopilotMenus()\n          },\n          youtubeAutoSummarize: async (value) => {\n            const newValue = value?.newValue || false\n            const tabs = await browser.tabs.query({\n              url: \"*://www.youtube.com/watch*\"\n            })\n            tabs.forEach((tab) => {\n              if (tab.id) {\n                browser.tabs\n                  .sendMessage(tab.id, {\n                    type: \"youtube_summarize_setting_changed\",\n                    enabled: newValue\n                  })\n                  .catch(() => {})\n              }\n            })\n          }\n        })\n        const data = await getInitialConfig()\n        contextMenuClick = data.contextMenuClick\n        actionIconClick = data.actionIconClick\n\n        browser.contextMenus.create({\n          id: contextMenuId[contextMenuClick],\n          title: contextMenuTitle[contextMenuClick],\n          contexts: [\"page\", \"selection\"]\n        })\n\n        // Create built-in copilot menus\n        await createBuiltinCopilotMenus()\n\n        // Create custom copilot menus\n        await createCustomCopilotMenus()\n      } catch (error) {\n        console.error(\"Error in initLogic:\", error)\n      }\n    }\n\n    browser.runtime.onMessage.addListener(async (message, sender) => {\n      if (message.type === \"refresh_custom_copilot_menus\") {\n        await createCustomCopilotMenus()\n        return Promise.resolve({ success: true })\n      } else if (message.type === \"refresh_builtin_copilot_menus\") {\n        await createBuiltinCopilotMenus()\n        return Promise.resolve({ success: true })\n      } else if (message.type === \"check_youtube_summarize_enabled\") {\n        const enabled = await storage.get(\"youtubeAutoSummarize\")\n        return Promise.resolve({ enabled: enabled || false })\n      } else if (message.type === \"sidepanel\") {\n        await browser.sidebarAction.open()\n      } else if (message.type === \"pull_model\") {\n        const ollamaURL = await getOllamaURL()\n\n        const isRunning = await isOllamaRunning()\n\n        if (!isRunning) {\n          setBadgeText({ text: \"E\" })\n          setBadgeBackgroundColor({ color: \"#FF0000\" })\n          setTitle({ title: \"Ollama is not running\" })\n          setTimeout(() => {\n            clearBadge()\n          }, 5000)\n          return\n        }\n\n        await streamDownload(ollamaURL, message.modelName)\n      } else if (message.type === \"cancel_download\") {\n        cancelDownload()\n      } else if (message.type === \"mcp_oauth_start\") {\n        const mcpDb = new McpServerDb()\n        const server = await mcpDb.getById(message.serverId)\n        if (!server) {\n          return Promise.resolve({ success: false, error: \"Server not found\" })\n        }\n        const result = await startMcpOAuthFlow(server)\n        return Promise.resolve(result)\n      } else if (message.type === \"mcp_oauth_disconnect\") {\n        await disconnectMcpOAuth(message.serverId)\n        return Promise.resolve({ success: true })\n      } else if (message.type === \"youtube_summarize\") {\n        if (sender.tab?.id) {\n          chrome.sidePanel.open({\n            tabId: sender.tab.id\n          })\n        }\n\n        setTimeout(\n          async () => {\n            const prompt = `Summarize this YouTube video: \"${message.videoTitle}\".\\n\\nPlease provide a comprehensive summary of the video content.`\n\n            await browser.runtime.sendMessage({\n              from: \"background\",\n              type: \"yt_summarize\",\n              text: prompt\n            })\n          },\n          isCopilotRunning ? 0 : 5000\n        )\n      }\n    })\n\n    browser.runtime.onConnect.addListener((port) => {\n      if (port.name === \"pgCopilot\") {\n        isCopilotRunning = true\n        port.onDisconnect.addListener(() => {\n          isCopilotRunning = false\n        })\n      }\n    })\n\n    chrome.action.onClicked.addListener((tab) => {\n      if (actionIconClick === \"webui\") {\n        chrome.tabs.create({ url: chrome.runtime.getURL(\"/options.html\") })\n      } else {\n        chrome.sidePanel.open({\n          tabId: tab.id!\n        })\n      }\n    })\n\n    const contextMenuTitle = {\n      webui: browser.i18n.getMessage(\"openOptionToChat\"),\n      sidePanel: browser.i18n.getMessage(\"openSidePanelToChat\")\n    }\n\n    browser.contextMenus.onClicked.addListener(async (info, tab) => {\n      if (info.menuItemId === \"open-side-panel-pa\") {\n        chrome.sidePanel.open({\n          tabId: tab.id!\n        })\n      } else if (info.menuItemId === \"open-web-ui-pa\") {\n        browser.tabs.create({\n          url: browser.runtime.getURL(\"/options.html\")\n        })\n      } else if (info.menuItemId === \"summarize-pa\") {\n        chrome.sidePanel.open({\n          tabId: tab.id!\n        })\n        // this is a bad method hope somone can fix it :)\n        setTimeout(\n          async () => {\n            await browser.runtime.sendMessage({\n              from: \"background\",\n              type: \"summary\",\n              text: info.selectionText\n            })\n          },\n          isCopilotRunning ? 0 : 5000\n        )\n      } else if (info.menuItemId === \"rephrase-pa\") {\n        chrome.sidePanel.open({\n          tabId: tab.id!\n        })\n        setTimeout(\n          async () => {\n            await browser.runtime.sendMessage({\n              type: \"rephrase\",\n              from: \"background\",\n              text: info.selectionText\n            })\n          },\n          isCopilotRunning ? 0 : 5000\n        )\n      } else if (info.menuItemId === \"translate-pg\") {\n        chrome.sidePanel.open({\n          tabId: tab.id!\n        })\n\n        setTimeout(\n          async () => {\n            await browser.runtime.sendMessage({\n              type: \"translate\",\n              from: \"background\",\n              text: info.selectionText\n            })\n          },\n          isCopilotRunning ? 0 : 5000\n        )\n      } else if (info.menuItemId === \"explain-pa\") {\n        chrome.sidePanel.open({\n          tabId: tab.id!\n        })\n\n        setTimeout(\n          async () => {\n            await browser.runtime.sendMessage({\n              type: \"explain\",\n              from: \"background\",\n              text: info.selectionText\n            })\n          },\n          isCopilotRunning ? 0 : 5000\n        )\n      } else if (info.menuItemId === \"custom-pg\") {\n        chrome.sidePanel.open({\n          tabId: tab.id!\n        })\n\n        setTimeout(\n          async () => {\n            await browser.runtime.sendMessage({\n              type: \"custom\",\n              from: \"background\",\n              text: info.selectionText\n            })\n          },\n          isCopilotRunning ? 0 : 5000\n        )\n      } else if (typeof info.menuItemId === \"string\" && info.menuItemId.startsWith(\"custom_copilot_\")) {\n        // Handle custom copilot prompts\n        chrome.sidePanel.open({\n          tabId: tab.id!\n        })\n\n        setTimeout(\n          async () => {\n            await browser.runtime.sendMessage({\n              type: info.menuItemId,\n              from: \"background\",\n              text: info.selectionText\n            })\n          },\n          isCopilotRunning ? 0 : 5000\n        )\n      }\n    })\n\n    browser.commands.onCommand.addListener((command) => {\n      switch (command) {\n        case \"execute_side_panel\":\n          chrome.tabs.query(\n            { active: true, currentWindow: true },\n            async (tabs) => {\n              const tab = tabs[0]\n              chrome.sidePanel.open({\n                tabId: tab.id!\n              })\n            }\n          )\n          break\n        default:\n          break\n      }\n    })\n\n    initialize()\n  },\n  persistent: true\n})\n"
  },
  {
    "path": "src/entries/hf-pull.content.ts",
    "content": "export default defineContentScript({\n  main(ctx) {\n    const downloadModel = async (modelName: string) => {\n      const ok = confirm(\n        `[Page Assist Extension] Do you want to pull the ${modelName} model? This has nothing to do with the huggingface.co website. The model will be pulled locally once you confirm. Make sure Ollama is running.`\n      )\n      if (ok) {\n        alert(\n          `[Page Assist Extension] Pulling ${modelName} model. For more details, check the extension icon.`\n        )\n\n        await browser.runtime.sendMessage({\n          type: \"pull_model\",\n          modelName\n        })\n        return true\n      }\n      return false\n    }\n\n    const downloadSVG = `\n        <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 24 24\" width=\"16\" height=\"16\">\n          <path d=\"M12 16l-6-6h4V4h4v6h4l-6 6z\"/>\n          <path d=\"M4 20h16v-2H4v2z\"/>\n        </svg>\n      `\n\n    const injectDownloadButton = (modal: HTMLElement) => {\n      const copyButton = modal.querySelector(\n        'button[title=\"Copy snippet to clipboard\"]'\n      )\n      \n      if (!copyButton && !modal.querySelector(\".pageassist-download-button\")) {\n        const downloadButton = document.createElement(\"button\")\n        downloadButton.classList.add(\"pageassist-download-button\", \"focus:outline-hidden\", \"inline-flex\", \"cursor-pointer\", \"items-center\", \"text-sm\", \"bg-white\", \"shadow-xs\", \"rounded-md\", \"border\", \"px-2\", \"py-1\", \"text-gray-600\")\n        downloadButton.title = \"Pull from Page Assist\"\n        downloadButton.innerHTML = `${downloadSVG} <span class=\"ml-1.5\">Pull from Page Assist</span>`\n        \n        downloadButton.addEventListener(\"click\", async () => {\n          const preElement = modal.querySelector(\"pre\")\n          if (preElement) {\n            const modelCommand = preElement.textContent?.trim() || \"\"\n            \n            if (modelCommand.includes(\"ollama run\") || modelCommand.includes(\"ollama pull\")) {\n              const lines = modelCommand.split('\\n')\n              const ollamaLine = lines.find(line => \n                line.trim().startsWith(\"ollama run\") || line.trim().startsWith(\"ollama pull\")\n              )\n              \n              if (ollamaLine) {\n                await downloadModel(\n                  ollamaLine\n                    .trim()\n                    .replaceAll(\"ollama run\", \"\")\n                    .replaceAll(\"ollama pull\", \"\")\n                    .trim()\n                )\n              }\n            }\n          }\n        })\n        \n        modal.appendChild(downloadButton)\n        return\n      }\n      \n      // Original logic for complex modals\n      if (copyButton && !modal.querySelector(\".pageassist-download-button\")) {\n        const downloadButton = copyButton.cloneNode(true) as HTMLElement\n        downloadButton.classList.add(\"pageassist-download-button\")\n        downloadButton.querySelector(\"svg\")!.outerHTML = downloadSVG\n        downloadButton.querySelector(\"span\")!.textContent =\n          \"Pull from Page Assist\"\n        downloadButton.addEventListener(\"click\", async () => {\n          const preElement = modal.querySelector(\"pre\")\n          if (preElement) {\n            let modelCommand = \"\"\n            preElement.childNodes.forEach((node) => {\n              if (node.nodeType === Node.TEXT_NODE) {\n                modelCommand += node.textContent\n              } else if (node instanceof HTMLSelectElement) {\n                modelCommand += node.value\n              } else if (node instanceof HTMLElement) {\n                const selectElement = node.querySelector(\n                  \"select\"\n                ) as HTMLSelectElement\n                if (selectElement) {\n                  modelCommand += selectElement.value\n                } else {\n                  modelCommand += node.textContent\n                }\n              }\n            })\n\n            modelCommand = modelCommand.trim()\n\n            if (modelCommand.includes(\"ollama run\") || modelCommand.includes(\"ollama pull\")) {\n              const lines = modelCommand.split('\\n')\n              const ollamaLine = lines.find(line => \n                line.trim().startsWith(\"ollama run\") || line.trim().startsWith(\"ollama pull\")\n              )\n              \n              if (ollamaLine) {\n                await downloadModel(\n                  ollamaLine\n                    .trim()\n                    .replaceAll(\"ollama run\", \"\")\n                    .replaceAll(\"ollama pull\", \"\")\n                    .trim()\n                )\n              }\n            }\n          }\n        })\n        const buttonContainer = document.createElement('div')\n        buttonContainer.classList.add(\"mb-3\")\n        buttonContainer.style.display = 'flex'\n        buttonContainer.style.justifyContent = 'flex-end'\n        buttonContainer.appendChild(downloadButton)\n        modal.querySelector(\"pre\")!.insertAdjacentElement(\"afterend\", buttonContainer)\n      }\n    }\n\n    const checkForOllamaCommands = (element: HTMLElement) => {\n      const modal = element.querySelector(\".shadow-alternate\") as HTMLElement\n      if (modal) {\n        injectDownloadButton(modal)\n        return\n      }\n      const preElements = element.querySelectorAll(\"pre\")\n      preElements.forEach((preElement) => {\n        const text = preElement.textContent || \"\"\n        if ((text.includes(\"ollama run\") || text.includes(\"ollama pull\")) && \n            !preElement.parentElement?.querySelector(\".pageassist-download-button\")) {\n          const container = preElement.closest(\"div\")\n          const copyButton = container?.querySelector('button[title=\"Copy snippet to clipboard\"]')\n          \n          if (copyButton) {\n            const mockModal = document.createElement(\"div\")\n            mockModal.appendChild(preElement.cloneNode(true))\n            \n            injectDownloadButton(mockModal)\n            \n            const downloadButton = mockModal.querySelector(\".pageassist-download-button\")\n            if (downloadButton) {\n              const buttonContainer = document.createElement('div')\n              buttonContainer.classList.add(\"mb-3\")\n              buttonContainer.style.display = 'flex'\n              buttonContainer.style.justifyContent = 'flex-end'\n              buttonContainer.appendChild(downloadButton)\n              \n              preElement.insertAdjacentElement(\"afterend\", buttonContainer)\n            }\n          }\n        }\n      })\n    }\n\n    const observer = new MutationObserver((mutations) => {\n      for (const mutation of mutations) {\n        mutation.addedNodes.forEach((node) => {\n          if (node instanceof HTMLElement) {\n            checkForOllamaCommands(node)\n          }\n        })\n      }\n    })\n\n    observer.observe(document.body, { childList: true, subtree: true })\n    \n    checkForOllamaCommands(document.body)\n  },\n  allFrames: true,\n  matches: [\"*://huggingface.co/*\"]\n})\n"
  },
  {
    "path": "src/entries/ollama-pull.content.ts",
    "content": "import { injectOllamaPullButtons } from \"@/utils/ollama-pull-inject\"\n\nexport default defineContentScript({\n  main(ctx) {\n    const sendMessage = async (modelName: string) => {\n      await browser.runtime.sendMessage({\n        type: \"pull_model\",\n        modelName\n      })\n    }\n\n    injectOllamaPullButtons(sendMessage)\n  },\n  allFrames: true,\n  matches: [\"*://ollama.com/*\"],\n\n})"
  },
  {
    "path": "src/entries/options/App.tsx",
    "content": "import { QueryClient, QueryClientProvider } from \"@tanstack/react-query\"\nimport { MemoryRouter } from \"react-router-dom\"\nimport { useEffect, useState } from \"react\"\nconst queryClient = new QueryClient()\nimport { ConfigProvider, Empty, theme } from \"antd\"\nimport { StyleProvider } from \"@ant-design/cssinjs\"\nimport { useDarkMode } from \"~/hooks/useDarkmode\"\nimport { OptionRouting } from \"@/routes/chrome-route\"\nimport \"~/i18n\"\nimport { useTranslation } from \"react-i18next\"\nimport { PageAssistProvider } from \"@/components/Common/PageAssistProvider\"\nimport { FontSizeProvider } from \"@/context/FontSizeProvider\"\nimport { runAllMigrations } from \"@/db/dexie/migration\"\n\nfunction IndexOption() {\n  const { mode } = useDarkMode()\n  const { t, i18n } = useTranslation()\n  const [direction, setDirection] = useState<\"ltr\" | \"rtl\">(\"ltr\")\n\n  useEffect(() => {\n    if (i18n.resolvedLanguage) {\n      document.documentElement.lang = i18n.resolvedLanguage\n      document.documentElement.dir = i18n.dir(i18n.resolvedLanguage)\n      setDirection(i18n.dir(i18n.resolvedLanguage))\n    }\n  }, [i18n, i18n.resolvedLanguage])\n\n  return (\n    <MemoryRouter>\n      <ConfigProvider\n        theme={{\n          algorithm:\n            mode === \"dark\" ? theme.darkAlgorithm : theme.defaultAlgorithm,\n          token: {\n            fontFamily: \"Arimo\"\n          }\n        }}\n        renderEmpty={() => (\n          <Empty\n            imageStyle={{\n              height: 60\n            }}\n            description={t(\"common:noData\")}\n          />\n        )}\n        direction={direction}>\n        <StyleProvider hashPriority=\"high\">\n          <QueryClientProvider client={queryClient}>\n            <PageAssistProvider>\n              <FontSizeProvider>\n                <OptionRouting />\n              </FontSizeProvider>\n            </PageAssistProvider>\n          </QueryClientProvider>\n        </StyleProvider>\n      </ConfigProvider>\n    </MemoryRouter>\n  )\n}\n\nexport default IndexOption\n"
  },
  {
    "path": "src/entries/options/index.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <title>Page Assist - A Web UI for Local AI Models</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <meta name=\"manifest.open_in_tab\" content=\"true\" />\n    <meta name=\"manifest.browser_style\" content=\"false\" />\n    <link href=\"~/assets/tailwind.css\" rel=\"stylesheet\" />\n    <meta charset=\"utf-8\" />\n  </head>\n  <body class=\"bg-white dark:bg-[#1a1a1a]\">\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"./main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "src/entries/options/main.tsx",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom/client';\nimport IndexOption from './App';\n\n\nReactDOM.createRoot(document.getElementById('root')!).render(\n  <React.StrictMode>\n    <IndexOption />\n  </React.StrictMode>,\n);\n"
  },
  {
    "path": "src/entries/sidepanel/App.tsx",
    "content": "import { QueryClient, QueryClientProvider } from \"@tanstack/react-query\"\nimport { MemoryRouter } from \"react-router-dom\"\nimport { useEffect } from \"react\"\nimport { SidepanelRouting } from \"@/routes/chrome-route\"\nconst queryClient = new QueryClient()\nimport { ConfigProvider, Empty, theme } from \"antd\"\nimport { StyleProvider } from \"@ant-design/cssinjs\"\nimport { useDarkMode } from \"~/hooks/useDarkmode\"\nimport \"~/i18n\"\nimport { useTranslation } from \"react-i18next\"\nimport { PageAssistProvider } from \"@/components/Common/PageAssistProvider\"\nimport { FontSizeProvider } from \"@/context/FontSizeProvider\"\n\nfunction IndexSidepanel() {\n  const { mode } = useDarkMode()\n  const { t, i18n } = useTranslation()\n\n  useEffect(() => {\n    if (i18n.resolvedLanguage) {\n      document.documentElement.lang = i18n.resolvedLanguage\n      document.documentElement.dir = i18n.dir(i18n.resolvedLanguage)\n    }\n  }, [i18n, i18n.resolvedLanguage])\n\n  return (\n    <MemoryRouter>\n      <ConfigProvider\n        theme={{\n          algorithm:\n            mode === \"dark\" ? theme.darkAlgorithm : theme.defaultAlgorithm,\n          token: {\n            fontFamily: \"Arimo\"\n          }\n        }}\n        renderEmpty={() => (\n          <Empty\n            imageStyle={{\n              height: 60\n            }}\n            description={t(\"common:noData\")}\n          />\n        )}>\n        <StyleProvider hashPriority=\"high\">\n          <QueryClientProvider client={queryClient}>\n            <PageAssistProvider>\n              <FontSizeProvider>\n                <SidepanelRouting />\n              </FontSizeProvider>\n            </PageAssistProvider>\n          </QueryClientProvider>\n        </StyleProvider>\n      </ConfigProvider>\n    </MemoryRouter>\n  )\n}\n\nexport default IndexSidepanel\n"
  },
  {
    "path": "src/entries/sidepanel/index.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <title>Page Assist - A Web UI for Local AI Models</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <meta name=\"manifest.type\" content=\"browser_action\" />\n    <meta name=\"manifest.open_at_install\" content=\"false\" />\n    <meta name=\"manifest.browser_style\" content=\"false\" />\n    <meta name=\"manifest.default_icon\" content=\"'/icon.png'\" />\n    <link href=\"~/assets/tailwind.css\" rel=\"stylesheet\" />\n    <meta charset=\"utf-8\" />\n  </head>\n  <body class=\"bg-white dark:bg-[#1a1a1a]\">\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"./main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "src/entries/sidepanel/main.tsx",
    "content": "import React from \"react\"\nimport ReactDOM from \"react-dom/client\"\nimport IndexSidepanel from \"./App\"\n\nReactDOM.createRoot(document.getElementById(\"root\")!).render(\n  <React.StrictMode>\n    <IndexSidepanel />\n  </React.StrictMode>\n)\n"
  },
  {
    "path": "src/entries/youtube-summarize.content.ts",
    "content": "export default defineContentScript({\n  async main(ctx) {\n    // Check if YouTube summarization is enabled\n    const checkEnabled = async () => {\n      try {\n        const response = await browser.runtime.sendMessage({\n          type: \"check_youtube_summarize_enabled\"\n        })\n        return response?.enabled || false\n      } catch (error) {\n        console.error(\"Failed to check YouTube summarize setting:\", error)\n        return false\n      }\n    }\n\n    const summarizeIconSVG = `\n      <svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" focusable=\"false\" aria-hidden=\"true\" style=\"pointer-events: none; display: inherit; width: 100%; height: 100%;\">\n        <path d=\"M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z\"/>\n      </svg>\n    `\n\n    const createSummarizeButton = (): HTMLElement => {\n      const buttonContainer = document.createElement(\"yt-button-view-model\")\n      buttonContainer.className =\n        \"ytd-menu-renderer pageassist-youtube-summarize-container\"\n\n      buttonContainer.innerHTML = `\n        <button-view-model class=\"ytSpecButtonViewModelHost style-scope ytd-menu-renderer\">\n          <button class=\"yt-spec-button-shape-next yt-spec-button-shape-next--tonal yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m yt-spec-button-shape-next--icon-leading yt-spec-button-shape-next--enable-backdrop-filter-experiment pageassist-youtube-summarize\"\n                  title=\"Summarize with Page Assist\"\n                  aria-label=\"Summarize with Page Assist\"\n                  aria-disabled=\"false\"\n                  style=\"\">\n            <div aria-hidden=\"true\" class=\"yt-spec-button-shape-next__icon\">\n              <span class=\"ytIconWrapperHost\" style=\"width: 24px; height: 24px;\">\n                <span class=\"yt-icon-shape ytSpecIconShapeHost\">\n                  <div style=\"width: 100%; height: 100%; display: block; fill: currentcolor;\">\n                    ${summarizeIconSVG}\n                  </div>\n                </span>\n              </span>\n            </div>\n            <div class=\"yt-spec-button-shape-next__button-text-content\">Summarize</div>\n            <yt-touch-feedback-shape aria-hidden=\"true\" class=\"yt-spec-touch-feedback-shape yt-spec-touch-feedback-shape--touch-response\">\n              <div class=\"yt-spec-touch-feedback-shape__stroke\"></div>\n              <div class=\"yt-spec-touch-feedback-shape__fill\"></div>\n            </yt-touch-feedback-shape>\n          </button>\n        </button-view-model>\n      `\n\n      const button = buttonContainer.querySelector(\"button\")\n      button?.addEventListener(\"click\", async () => {\n        // Get video title for context\n        const videoTitle =\n          document\n            .querySelector(\"h1.ytd-video-primary-info-renderer\")\n            ?.textContent?.trim() ||\n          document\n            .querySelector(\"h1 yt-formatted-string\")\n            ?.textContent?.trim() ||\n          document.title.replace(\" - YouTube\", \"\")\n\n        await browser.runtime.sendMessage({\n          type: \"youtube_summarize\",\n          videoTitle,\n          videoUrl: window.location.href\n        })\n      })\n\n      return buttonContainer\n    }\n\n    const injectSummarizeButton = async () => {\n      // Check if feature is enabled first\n      const isEnabled = await checkEnabled()\n      if (!isEnabled) {\n        return\n      }\n\n      // Target the top-level buttons container\n      const topLevelButtons = document.querySelector(\n        \"#top-level-buttons-computed\"\n      )\n\n      if (\n        topLevelButtons &&\n        !document.querySelector(\".pageassist-youtube-summarize-container\")\n      ) {\n        const button = createSummarizeButton()\n        const likeDislikeButton = topLevelButtons.querySelector(\n          \"segmented-like-dislike-button-view-model\"\n        )\n\n        const shareButton = topLevelButtons.querySelector(\n          \"yt-button-view-model\"\n        )\n\n        if (likeDislikeButton) {\n          if (likeDislikeButton.nextSibling) {\n            topLevelButtons.insertBefore(button, likeDislikeButton.nextSibling)\n          } else {\n            topLevelButtons.appendChild(button)\n          }\n        } else if (shareButton) {\n          if (shareButton.nextSibling) {\n            topLevelButtons.insertBefore(button, shareButton.nextSibling)\n          } else {\n            topLevelButtons.appendChild(button)\n          }\n        } else {\n          // Last resort: just append to the container\n          topLevelButtons.appendChild(button)\n        }\n      }\n    }\n\n    const removeSummarizeButton = () => {\n      const button = document.querySelector(\n        \".pageassist-youtube-summarize-container\"\n      )\n      if (button) {\n        button.remove()\n      }\n    }\n\n    // Listen for storage changes from background\n    browser.runtime.onMessage.addListener((message) => {\n      if (message.type === \"youtube_summarize_setting_changed\") {\n        if (message.enabled) {\n          injectSummarizeButton()\n        } else {\n          removeSummarizeButton()\n        }\n      }\n    })\n\n    // Observer to detect when top-level buttons are loaded\n    const observer = new MutationObserver(() => {\n      const topLevelButtons = document.querySelector(\"#top-level-buttons-computed\")\n      const existingButton = document.querySelector(\".pageassist-youtube-summarize-container\")\n\n      // Inject button if container exists but button doesn't\n      if (topLevelButtons && !existingButton) {\n        injectSummarizeButton()\n      }\n    })\n\n    observer.observe(document.body, { childList: true, subtree: true })\n\n    injectSummarizeButton()\n    setTimeout(() => {\n      injectSummarizeButton()\n    }, 500)\n\n    setTimeout(() => {\n      injectSummarizeButton()\n    }, 2000)\n\n    let lastUrl = location.href\n    new MutationObserver(() => {\n      const url = location.href\n      if (url !== lastUrl) {\n        lastUrl = url\n        removeSummarizeButton()\n        setTimeout(() => {\n          injectSummarizeButton()\n        }, 1000)\n      }\n    }).observe(document.body, { childList: true, subtree: true })\n  },\n  matches: [\"*://www.youtube.com/watch*\"],\n  runAt: \"document_end\"\n})\n"
  },
  {
    "path": "src/entries-firefox/background.ts",
    "content": "import { getOllamaURL, isOllamaRunning } from \"../services/ollama\"\nimport { browser } from \"wxt/browser\"\nimport { clearBadge, streamDownload, cancelDownload } from \"@/utils/pull-ollama\"\nimport { Storage } from \"@plasmohq/storage\"\nimport { getInitialConfig } from \"@/services/action\"\nimport { getCustomCopilotPrompts, getCopilotPromptsEnabledState, type CustomCopilotPrompt } from \"@/services/application\"\nimport { startMcpOAuthFlow, disconnectMcpOAuth } from \"@/libs/mcp/oauth-flow\"\nimport { McpServerDb } from \"@/db/dexie/mcp\"\n\nexport default defineBackground({\n  main() {\n    const storage = new Storage({\n      area: \"local\"\n    })\n    let isCopilotRunning: boolean = false\n    let actionIconClick: string = \"webui\"\n    let contextMenuClick: string = \"sidePanel\"\n\n    let customCopilotMenuIds: string[] = []\n    const builtinCopilotMenus = [\n      { id: \"summarize-pa\", key: \"summary\", title: \"Summarize\" },\n      { id: \"explain-pa\", key: \"explain\", title: \"Explain\" },\n      { id: \"rephrase-pa\", key: \"rephrase\", title: \"Rephrase\" },\n      { id: \"translate-pg\", key: \"translate\", title: \"Translate\" },\n      { id: \"custom-pg\", key: \"custom\", title: \"Custom\" }\n    ]\n\n    const createBuiltinCopilotMenus = async () => {\n      const enabledState = await getCopilotPromptsEnabledState()\n\n      for (const menu of builtinCopilotMenus) {\n        // Remove existing menu\n        try {\n          await browser.contextMenus.remove(menu.id)\n        } catch (e) {\n          // Menu might not exist, ignore\n        }\n\n        // Create menu only if enabled\n        if (enabledState[menu.key]) {\n          browser.contextMenus.create({\n            id: menu.id,\n            title: menu.title,\n            contexts: [\"selection\"]\n          })\n        }\n      }\n    }\n\n    const createCustomCopilotMenus = async () => {\n      // Remove existing custom copilot menus\n      for (const menuId of customCopilotMenuIds) {\n        try {\n          await browser.contextMenus.remove(menuId)\n        } catch (e) {\n          // Menu might not exist, ignore\n        }\n      }\n      customCopilotMenuIds = []\n\n      // Create new custom copilot menus\n      const customPrompts = await getCustomCopilotPrompts()\n      const enabledPrompts = customPrompts.filter(p => p.enabled)\n\n      for (const prompt of enabledPrompts) {\n        const menuId = `custom_copilot_${prompt.id}`\n        customCopilotMenuIds.push(menuId)\n        browser.contextMenus.create({\n          id: menuId,\n          title: prompt.title,\n          contexts: [\"selection\"]\n        })\n      }\n    }\n\n    const initialize = async () => {\n      try {\n        storage.watch({\n          actionIconClick: (value) => {\n            const oldValue = value?.oldValue || \"webui\"\n            const newValue = value?.newValue || \"webui\"\n            if (oldValue !== newValue) {\n              actionIconClick = newValue\n            }\n          },\n          contextMenuClick: (value) => {\n            const oldValue = value?.oldValue || \"sidePanel\"\n            const newValue = value?.newValue || \"sidePanel\"\n            if (oldValue !== newValue) {\n              contextMenuClick = newValue\n              browser.contextMenus.remove(contextMenuId[oldValue])\n              browser.contextMenus.create({\n                id: contextMenuId[newValue],\n                title: contextMenuTitle[newValue],\n                contexts: [\"page\", \"selection\"]\n              })\n            }\n          },\n          customCopilotPrompts: async () => {\n            // Recreate custom copilot menus when prompts change\n            await createCustomCopilotMenus()\n          },\n          youtubeAutoSummarize: async (value) => {\n            const newValue = value?.newValue || false\n            const tabs = await browser.tabs.query({\n              url: \"*://www.youtube.com/watch*\"\n            })\n            tabs.forEach((tab) => {\n              if (tab.id) {\n                browser.tabs\n                  .sendMessage(tab.id, {\n                    type: \"youtube_summarize_setting_changed\",\n                    enabled: newValue\n                  })\n                  .catch(() => {})\n              }\n            })\n          }\n        })\n        const data = await getInitialConfig()\n        contextMenuClick = data.contextMenuClick\n        actionIconClick = data.actionIconClick\n        browser.contextMenus.create({\n          id: contextMenuId[contextMenuClick],\n          title: contextMenuTitle[contextMenuClick],\n          contexts: [\"page\", \"selection\"]\n        })\n\n        // Create built-in copilot menus\n        await createBuiltinCopilotMenus()\n\n        // Create custom copilot menus\n        await createCustomCopilotMenus()\n      } catch (error) {\n        console.error(\"Error in initLogic:\", error)\n      }\n    }\n\n    browser.runtime.onMessage.addListener(async (message, sender) => {\n      if (message.type === \"refresh_custom_copilot_menus\") {\n        await createCustomCopilotMenus()\n        return Promise.resolve({ success: true })\n      } else if (message.type === \"refresh_builtin_copilot_menus\") {\n        await createBuiltinCopilotMenus()\n        return Promise.resolve({ success: true })\n      } else if (message.type === \"check_youtube_summarize_enabled\") {\n        const enabled = await storage.get(\"youtubeAutoSummarize\")\n        return Promise.resolve({ enabled: enabled || false })\n      } else if (message.type === \"sidepanel\") {\n        await browser.sidebarAction.open()\n      } else if (message.type === \"pull_model\") {\n        const ollamaURL = await getOllamaURL()\n\n        const isRunning = await isOllamaRunning()\n\n        if (!isRunning) {\n          setBadgeText({ text: \"E\" })\n          setBadgeBackgroundColor({ color: \"#FF0000\" })\n          setTitle({ title: \"Ollama is not running\" })\n          setTimeout(() => {\n            clearBadge()\n          }, 5000)\n          return\n        }\n\n        await streamDownload(ollamaURL, message.modelName)\n      } else if (message.type === \"cancel_download\") {\n        cancelDownload()\n      } else if (message.type === \"mcp_oauth_start\") {\n        const mcpDb = new McpServerDb()\n        const server = await mcpDb.getById(message.serverId)\n        if (!server) {\n          return Promise.resolve({ success: false, error: \"Server not found\" })\n        }\n        const result = await startMcpOAuthFlow(server)\n        return Promise.resolve(result)\n      } else if (message.type === \"mcp_oauth_disconnect\") {\n        await disconnectMcpOAuth(message.serverId)\n        return Promise.resolve({ success: true })\n      } else if (message.type === \"youtube_summarize\") {\n        if (sender.tab?.id) {\n          await browser.sidebarAction.open()\n        }\n\n        setTimeout(\n          async () => {\n            const prompt = `Summarize this YouTube video: \"${message.videoTitle}\".\\n\\nPlease provide a comprehensive summary of the video content.`\n\n            await browser.runtime.sendMessage({\n              from: \"background\",\n              type: \"yt_summarize\",\n              text: prompt\n            })\n          },\n          isCopilotRunning ? 0 : 5000\n        )\n      }\n    })\n\n    browser.runtime.onConnect.addListener((port) => {\n      if (port.name === \"pgCopilot\") {\n        isCopilotRunning = true\n        port.onDisconnect.addListener(() => {\n          isCopilotRunning = false\n        })\n      }\n    })\n\n    browser.browserAction.onClicked.addListener((tab) => {\n      if (actionIconClick === \"webui\") {\n        browser.tabs.create({ url: browser.runtime.getURL(\"/options.html\") })\n      } else {\n        browser.sidebarAction.toggle()\n      }\n    })\n\n    const contextMenuTitle = {\n      webui: browser.i18n.getMessage(\"openOptionToChat\"),\n      sidePanel: browser.i18n.getMessage(\"openSidePanelToChat\")\n    }\n\n    const contextMenuId = {\n      webui: \"open-web-ui-pa\",\n      sidePanel: \"open-side-panel-pa\"\n    }\n\n    browser.contextMenus.onClicked.addListener((info, tab) => {\n      if (info.menuItemId === \"open-side-panel-pa\") {\n        browser.sidebarAction.toggle()\n      } else if (info.menuItemId === \"open-web-ui-pa\") {\n        browser.tabs.create({\n          url: browser.runtime.getURL(\"/options.html\")\n        })\n      } else if (info.menuItemId === \"summarize-pa\") {\n        if (!isCopilotRunning) {\n          browser.sidebarAction.toggle()\n        }\n        setTimeout(\n          async () => {\n            await browser.runtime.sendMessage({\n              from: \"background\",\n              type: \"summary\",\n              text: info.selectionText\n            })\n          },\n          isCopilotRunning ? 0 : 5000\n        )\n      } else if (info.menuItemId === \"rephrase-pa\") {\n        if (!isCopilotRunning) {\n          browser.sidebarAction.toggle()\n        }\n        setTimeout(\n          async () => {\n            await browser.runtime.sendMessage({\n              type: \"rephrase\",\n              from: \"background\",\n              text: info.selectionText\n            })\n          },\n          isCopilotRunning ? 0 : 5000\n        )\n      } else if (info.menuItemId === \"translate-pg\") {\n        if (!isCopilotRunning) {\n          browser.sidebarAction.toggle()\n        }\n        setTimeout(\n          async () => {\n            await browser.runtime.sendMessage({\n              type: \"translate\",\n              from: \"background\",\n              text: info.selectionText\n            })\n          },\n          isCopilotRunning ? 0 : 5000\n        )\n      } else if (info.menuItemId === \"explain-pa\") {\n        if (!isCopilotRunning) {\n          browser.sidebarAction.toggle()\n        }\n        setTimeout(\n          async () => {\n            await browser.runtime.sendMessage({\n              type: \"explain\",\n              from: \"background\",\n              text: info.selectionText\n            })\n          },\n          isCopilotRunning ? 0 : 5000\n        )\n      } else if (info.menuItemId === \"custom-pg\") {\n        if (!isCopilotRunning) {\n          browser.sidebarAction.toggle()\n        }\n        setTimeout(\n          async () => {\n            await browser.runtime.sendMessage({\n              type: \"custom\",\n              from: \"background\",\n              text: info.selectionText\n            })\n          },\n          isCopilotRunning ? 0 : 5000\n        )\n      } else if (typeof info.menuItemId === \"string\" && info.menuItemId.startsWith(\"custom_copilot_\")) {\n        // Handle custom copilot prompts\n        if (!isCopilotRunning) {\n          browser.sidebarAction.toggle()\n        }\n        setTimeout(\n          async () => {\n            await browser.runtime.sendMessage({\n              type: info.menuItemId,\n              from: \"background\",\n              text: info.selectionText\n            })\n          },\n          isCopilotRunning ? 0 : 5000\n        )\n      }\n    })\n\n    browser.commands.onCommand.addListener((command) => {\n      switch (command) {\n        case \"execute_side_panel\":\n          browser.sidebarAction.toggle()\n          break\n        default:\n          break\n      }\n    })\n\n    initialize()\n  },\n  persistent: true\n})\n"
  },
  {
    "path": "src/entries-firefox/hf-pull.content.ts",
    "content": "export default defineContentScript({\n  main(ctx) {\n    const downloadModel = async (modelName: string) => {\n      const ok = confirm(\n        `[Page Assist Extension] Do you want to pull the ${modelName} model? This has nothing to do with the huggingface.co website. The model will be pulled locally once you confirm. Make sure Ollama is running.`\n      )\n      if (ok) {\n        alert(\n          `[Page Assist Extension] Pulling ${modelName} model. For more details, check the extension icon.`\n        )\n\n        await browser.runtime.sendMessage({\n          type: \"pull_model\",\n          modelName\n        })\n        return true\n      }\n      return false\n    }\n\n    const downloadSVG = `\n        <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 24 24\" width=\"16\" height=\"16\">\n          <path d=\"M12 16l-6-6h4V4h4v6h4l-6 6z\"/>\n          <path d=\"M4 20h16v-2H4v2z\"/>\n        </svg>\n      `\n\n    const injectDownloadButton = (modal: HTMLElement) => {\n      const copyButton = modal.querySelector(\n        'button[title=\"Copy snippet to clipboard\"]'\n      )\n      \n      if (!copyButton && !modal.querySelector(\".pageassist-download-button\")) {\n        const downloadButton = document.createElement(\"button\")\n        downloadButton.classList.add(\"pageassist-download-button\", \"focus:outline-hidden\", \"inline-flex\", \"cursor-pointer\", \"items-center\", \"text-sm\", \"bg-white\", \"shadow-xs\", \"rounded-md\", \"border\", \"px-2\", \"py-1\", \"text-gray-600\")\n        downloadButton.title = \"Pull from Page Assist\"\n        downloadButton.innerHTML = `${downloadSVG} <span class=\"ml-1.5\">Pull from Page Assist</span>`\n        \n        downloadButton.addEventListener(\"click\", async () => {\n          const preElement = modal.querySelector(\"pre\")\n          if (preElement) {\n            const modelCommand = preElement.textContent?.trim() || \"\"\n            \n            if (modelCommand.includes(\"ollama run\") || modelCommand.includes(\"ollama pull\")) {\n              const lines = modelCommand.split('\\n')\n              const ollamaLine = lines.find(line => \n                line.trim().startsWith(\"ollama run\") || line.trim().startsWith(\"ollama pull\")\n              )\n              \n              if (ollamaLine) {\n                await downloadModel(\n                  ollamaLine\n                    .trim()\n                    .replaceAll(\"ollama run\", \"\")\n                    .replaceAll(\"ollama pull\", \"\")\n                    .trim()\n                )\n              }\n            }\n          }\n        })\n        \n        modal.appendChild(downloadButton)\n        return\n      }\n      \n      // Original logic for complex modals\n      if (copyButton && !modal.querySelector(\".pageassist-download-button\")) {\n        const downloadButton = copyButton.cloneNode(true) as HTMLElement\n        downloadButton.classList.add(\"pageassist-download-button\")\n        downloadButton.querySelector(\"svg\")!.outerHTML = downloadSVG\n        downloadButton.querySelector(\"span\")!.textContent =\n          \"Pull from Page Assist\"\n        downloadButton.addEventListener(\"click\", async () => {\n          const preElement = modal.querySelector(\"pre\")\n          if (preElement) {\n            let modelCommand = \"\"\n            preElement.childNodes.forEach((node) => {\n              if (node.nodeType === Node.TEXT_NODE) {\n                modelCommand += node.textContent\n              } else if (node instanceof HTMLSelectElement) {\n                modelCommand += node.value\n              } else if (node instanceof HTMLElement) {\n                const selectElement = node.querySelector(\n                  \"select\"\n                ) as HTMLSelectElement\n                if (selectElement) {\n                  modelCommand += selectElement.value\n                } else {\n                  modelCommand += node.textContent\n                }\n              }\n            })\n\n            modelCommand = modelCommand.trim()\n\n            if (modelCommand.includes(\"ollama run\") || modelCommand.includes(\"ollama pull\")) {\n              const lines = modelCommand.split('\\n')\n              const ollamaLine = lines.find(line => \n                line.trim().startsWith(\"ollama run\") || line.trim().startsWith(\"ollama pull\")\n              )\n              \n              if (ollamaLine) {\n                await downloadModel(\n                  ollamaLine\n                    .trim()\n                    .replaceAll(\"ollama run\", \"\")\n                    .replaceAll(\"ollama pull\", \"\")\n                    .trim()\n                )\n              }\n            }\n          }\n        })\n        const buttonContainer = document.createElement('div')\n        buttonContainer.classList.add(\"mb-3\")\n        buttonContainer.style.display = 'flex'\n        buttonContainer.style.justifyContent = 'flex-end'\n        buttonContainer.appendChild(downloadButton)\n        modal.querySelector(\"pre\")!.insertAdjacentElement(\"afterend\", buttonContainer)\n      }\n    }\n\n    const checkForOllamaCommands = (element: HTMLElement) => {\n      const modal = element.querySelector(\".shadow-alternate\") as HTMLElement\n      if (modal) {\n        injectDownloadButton(modal)\n        return\n      }\n      const preElements = element.querySelectorAll(\"pre\")\n      preElements.forEach((preElement) => {\n        const text = preElement.textContent || \"\"\n        if ((text.includes(\"ollama run\") || text.includes(\"ollama pull\")) && \n            !preElement.parentElement?.querySelector(\".pageassist-download-button\")) {\n          const container = preElement.closest(\"div\")\n          const copyButton = container?.querySelector('button[title=\"Copy snippet to clipboard\"]')\n          \n          if (copyButton) {\n            const mockModal = document.createElement(\"div\")\n            mockModal.appendChild(preElement.cloneNode(true))\n            \n            injectDownloadButton(mockModal)\n            \n            const downloadButton = mockModal.querySelector(\".pageassist-download-button\")\n            if (downloadButton) {\n              const buttonContainer = document.createElement('div')\n              buttonContainer.classList.add(\"mb-3\")\n              buttonContainer.style.display = 'flex'\n              buttonContainer.style.justifyContent = 'flex-end'\n              buttonContainer.appendChild(downloadButton)\n              \n              preElement.insertAdjacentElement(\"afterend\", buttonContainer)\n            }\n          }\n        }\n      })\n    }\n\n    const observer = new MutationObserver((mutations) => {\n      for (const mutation of mutations) {\n        mutation.addedNodes.forEach((node) => {\n          if (node instanceof HTMLElement) {\n            checkForOllamaCommands(node)\n          }\n        })\n      }\n    })\n\n    observer.observe(document.body, { childList: true, subtree: true })\n    \n    checkForOllamaCommands(document.body)\n  },\n  allFrames: true,\n  matches: [\"*://huggingface.co/*\"]\n})\n"
  },
  {
    "path": "src/entries-firefox/ollama-pull.content.ts",
    "content": "import { injectOllamaPullButtons } from \"@/utils/ollama-pull-inject\"\n\nexport default defineContentScript({\n  main(ctx) {\n    const sendMessage = async (modelName: string) => {\n      await browser.runtime.sendMessage({\n        type: \"pull_model\",\n        modelName\n      })\n    }\n\n    injectOllamaPullButtons(sendMessage)\n  },\n  allFrames: true,\n  matches: [\"*://ollama.com/*\"],\n\n})"
  },
  {
    "path": "src/entries-firefox/options/App.tsx",
    "content": "import { QueryClient, QueryClientProvider } from \"@tanstack/react-query\"\nimport { MemoryRouter } from \"react-router-dom\"\nimport { useEffect, useState } from \"react\"\nconst queryClient = new QueryClient()\nimport { ConfigProvider, Empty, theme } from \"antd\"\nimport { StyleProvider } from \"@ant-design/cssinjs\"\nimport { useDarkMode } from \"~/hooks/useDarkmode\"\nimport { OptionRouting } from \"@/routes/firefox-route\"\nimport \"~/i18n\"\nimport { useTranslation } from \"react-i18next\"\nimport { PageAssistProvider } from \"@/components/Common/PageAssistProvider\"\nimport { FontSizeProvider } from \"@/context/FontSizeProvider\"\n\nfunction IndexOption() {\n  const { mode } = useDarkMode()\n  const { t, i18n } = useTranslation()\n  const [direction, setDirection] = useState<\"ltr\" | \"rtl\">(\"ltr\")\n\n  useEffect(() => {\n    if (i18n.resolvedLanguage) {\n      document.documentElement.lang = i18n.resolvedLanguage\n      document.documentElement.dir = i18n.dir(i18n.resolvedLanguage)\n      setDirection(i18n.dir(i18n.resolvedLanguage))\n    }\n  }, [i18n, i18n.resolvedLanguage])\n\n  return (\n    <MemoryRouter>\n      <ConfigProvider\n        theme={{\n          algorithm:\n            mode === \"dark\" ? theme.darkAlgorithm : theme.defaultAlgorithm,\n          token: {\n            fontFamily: \"Arimo\"\n          }\n        }}\n        renderEmpty={() => (\n          <Empty\n            imageStyle={{\n              height: 60\n            }}\n            description={t(\"common:noData\")}\n          />\n        )}\n        direction={direction}>\n        <StyleProvider hashPriority=\"high\">\n          <QueryClientProvider client={queryClient}>\n            <PageAssistProvider>\n              <FontSizeProvider>\n                <OptionRouting />\n              </FontSizeProvider>\n            </PageAssistProvider>\n          </QueryClientProvider>\n        </StyleProvider>\n      </ConfigProvider>\n    </MemoryRouter>\n  )\n}\n\nexport default IndexOption\n"
  },
  {
    "path": "src/entries-firefox/options/index.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <title>Page Assist - A Web UI for Local AI Models</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <meta name=\"manifest.open_in_tab\" content=\"true\" />\n    <meta name=\"manifest.browser_style\" content=\"false\" />\n    <link href=\"~/assets/tailwind.css\" rel=\"stylesheet\" />\n    <meta charset=\"utf-8\" />\n  </head>\n  <body class=\"bg-white dark:bg-[#1a1a1a]\">\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"./main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "src/entries-firefox/options/main.tsx",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom/client';\nimport IndexOption from './App';\n\n\nReactDOM.createRoot(document.getElementById('root')!).render(\n  <React.StrictMode>\n    <IndexOption />\n  </React.StrictMode>,\n);\n"
  },
  {
    "path": "src/entries-firefox/sidepanel/App.tsx",
    "content": "import { QueryClient, QueryClientProvider } from \"@tanstack/react-query\"\nimport { MemoryRouter } from \"react-router-dom\"\nimport { useEffect } from \"react\"\nimport { SidepanelRouting } from \"@/routes/firefox-route\"\nconst queryClient = new QueryClient()\nimport { ConfigProvider, Empty, theme } from \"antd\"\nimport { StyleProvider } from \"@ant-design/cssinjs\"\nimport { useDarkMode } from \"~/hooks/useDarkmode\"\nimport \"~/i18n\"\nimport { useTranslation } from \"react-i18next\"\nimport { PageAssistProvider } from \"@/components/Common/PageAssistProvider\"\nimport { FontSizeProvider } from \"@/context/FontSizeProvider\"\n\nfunction IndexSidepanel() {\n  const { mode } = useDarkMode()\n  const { t, i18n } = useTranslation()\n\n  useEffect(() => {\n    if (i18n.resolvedLanguage) {\n      document.documentElement.lang = i18n.resolvedLanguage\n      document.documentElement.dir = i18n.dir(i18n.resolvedLanguage)\n    }\n  }, [i18n, i18n.resolvedLanguage])\n\n  return (\n    <MemoryRouter>\n      <ConfigProvider\n        theme={{\n          algorithm:\n            mode === \"dark\" ? theme.darkAlgorithm : theme.defaultAlgorithm,\n          token: {\n            fontFamily: \"Arimo\"\n          }\n        }}\n        renderEmpty={() => (\n          <Empty\n            imageStyle={{\n              height: 60\n            }}\n            description={t(\"common:noData\")}\n          />\n        )}>\n        <StyleProvider hashPriority=\"high\">\n          <QueryClientProvider client={queryClient}>\n            <PageAssistProvider>\n              <FontSizeProvider>\n                <SidepanelRouting />\n              </FontSizeProvider>\n            </PageAssistProvider>\n          </QueryClientProvider>\n        </StyleProvider>\n      </ConfigProvider>\n    </MemoryRouter>\n  )\n}\n\nexport default IndexSidepanel\n"
  },
  {
    "path": "src/entries-firefox/sidepanel/index.html",
    "content": "<!doctype html>\n<html>\n  <head>\n    <title>Page Assist - A Web UI for Local AI Models</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <meta name=\"manifest.type\" content=\"browser_action\" />\n    <meta name=\"manifest.open_at_install\" content=\"false\" />\n    <meta name=\"manifest.browser_style\" content=\"false\" />\n    <meta name=\"manifest.default_icon\" content=\"'/icon.png'\" />\n    <link href=\"~/assets/tailwind.css\" rel=\"stylesheet\" />\n    <meta charset=\"utf-8\" />\n  </head>\n  <body class=\"bg-white dark:bg-[#1a1a1a]\">\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"./main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "src/entries-firefox/sidepanel/main.tsx",
    "content": "import React from \"react\"\nimport ReactDOM from \"react-dom/client\"\nimport IndexSidepanel from \"./App\"\n\nReactDOM.createRoot(document.getElementById(\"root\")!).render(\n  <React.StrictMode>\n    <IndexSidepanel />\n  </React.StrictMode>\n)\n"
  },
  {
    "path": "src/entries-firefox/youtube-summarize.content.ts",
    "content": "export default defineContentScript({\n  async main(ctx) {\n    // Check if YouTube summarization is enabled\n    const checkEnabled = async () => {\n      try {\n        const response = await browser.runtime.sendMessage({\n          type: \"check_youtube_summarize_enabled\"\n        })\n        return response?.enabled || false\n      } catch (error) {\n        console.error(\"Failed to check YouTube summarize setting:\", error)\n        return false\n      }\n    }\n\n    const summarizeIconSVG = `\n      <svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24\" viewBox=\"0 0 24 24\" width=\"24\" focusable=\"false\" aria-hidden=\"true\" style=\"pointer-events: none; display: inherit; width: 100%; height: 100%;\">\n        <path d=\"M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z\"/>\n      </svg>\n    `\n\n    const createSummarizeButton = (): HTMLElement => {\n      const buttonContainer = document.createElement(\"yt-button-view-model\")\n      buttonContainer.className =\n        \"ytd-menu-renderer pageassist-youtube-summarize-container\"\n\n      buttonContainer.innerHTML = `\n        <button-view-model class=\"ytSpecButtonViewModelHost style-scope ytd-menu-renderer\">\n          <button class=\"yt-spec-button-shape-next yt-spec-button-shape-next--tonal yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m yt-spec-button-shape-next--icon-leading yt-spec-button-shape-next--enable-backdrop-filter-experiment pageassist-youtube-summarize\"\n                  title=\"Summarize with Page Assist\"\n                  aria-label=\"Summarize with Page Assist\"\n                  aria-disabled=\"false\"\n                  style=\"\">\n            <div aria-hidden=\"true\" class=\"yt-spec-button-shape-next__icon\">\n              <span class=\"ytIconWrapperHost\" style=\"width: 24px; height: 24px;\">\n                <span class=\"yt-icon-shape ytSpecIconShapeHost\">\n                  <div style=\"width: 100%; height: 100%; display: block; fill: currentcolor;\">\n                    ${summarizeIconSVG}\n                  </div>\n                </span>\n              </span>\n            </div>\n            <div class=\"yt-spec-button-shape-next__button-text-content\">Summarize</div>\n            <yt-touch-feedback-shape aria-hidden=\"true\" class=\"yt-spec-touch-feedback-shape yt-spec-touch-feedback-shape--touch-response\">\n              <div class=\"yt-spec-touch-feedback-shape__stroke\"></div>\n              <div class=\"yt-spec-touch-feedback-shape__fill\"></div>\n            </yt-touch-feedback-shape>\n          </button>\n        </button-view-model>\n      `\n\n      const button = buttonContainer.querySelector(\"button\")\n      button?.addEventListener(\"click\", async () => {\n        // Get video title for context\n        const videoTitle =\n          document\n            .querySelector(\"h1.ytd-video-primary-info-renderer\")\n            ?.textContent?.trim() ||\n          document\n            .querySelector(\"h1 yt-formatted-string\")\n            ?.textContent?.trim() ||\n          document.title.replace(\" - YouTube\", \"\")\n\n        // Open sidebar and send summarize message\n        await browser.runtime.sendMessage({\n          type: \"youtube_summarize\",\n          videoTitle,\n          videoUrl: window.location.href\n        })\n      })\n\n      return buttonContainer\n    }\n\n    const injectSummarizeButton = async () => {\n      // Check if feature is enabled first\n      const isEnabled = await checkEnabled()\n      if (!isEnabled) {\n        return\n      }\n\n      // Target the top-level buttons container\n      const topLevelButtons = document.querySelector(\n        \"#top-level-buttons-computed\"\n      )\n\n      if (\n        topLevelButtons &&\n        !document.querySelector(\".pageassist-youtube-summarize-container\")\n      ) {\n        const button = createSummarizeButton()\n\n        // Try multiple insertion strategies for better compatibility\n        // Strategy 1: Insert after like/dislike button (new UI)\n        const likeDislikeButton = topLevelButtons.querySelector(\n          \"segmented-like-dislike-button-view-model\"\n        )\n\n        // Strategy 2: Insert after share button as fallback\n        const shareButton = topLevelButtons.querySelector(\n          \"yt-button-view-model\"\n        )\n\n        if (likeDislikeButton) {\n          // Insert after like/dislike button\n          if (likeDislikeButton.nextSibling) {\n            topLevelButtons.insertBefore(button, likeDislikeButton.nextSibling)\n          } else {\n            topLevelButtons.appendChild(button)\n          }\n        } else if (shareButton) {\n          // Fallback: insert after share button\n          if (shareButton.nextSibling) {\n            topLevelButtons.insertBefore(button, shareButton.nextSibling)\n          } else {\n            topLevelButtons.appendChild(button)\n          }\n        } else {\n          // Last resort: just append to the container\n          topLevelButtons.appendChild(button)\n        }\n      }\n    }\n\n    const removeSummarizeButton = () => {\n      const button = document.querySelector(\n        \".pageassist-youtube-summarize-container\"\n      )\n      if (button) {\n        button.remove()\n      }\n    }\n\n    // Listen for storage changes from background\n    browser.runtime.onMessage.addListener((message) => {\n      if (message.type === \"youtube_summarize_setting_changed\") {\n        if (message.enabled) {\n          injectSummarizeButton()\n        } else {\n          removeSummarizeButton()\n        }\n      }\n    })\n\n    // Observer to detect when top-level buttons are loaded\n    const observer = new MutationObserver(() => {\n      const topLevelButtons = document.querySelector(\"#top-level-buttons-computed\")\n      const existingButton = document.querySelector(\".pageassist-youtube-summarize-container\")\n\n      // Inject button if container exists but button doesn't\n      if (topLevelButtons && !existingButton) {\n        injectSummarizeButton()\n      }\n    })\n\n    observer.observe(document.body, { childList: true, subtree: true })\n\n    // Multiple injection attempts with different timings for better reliability\n    // Immediate attempt\n    injectSummarizeButton()\n\n    // Short delay for quick page loads\n    setTimeout(() => {\n      injectSummarizeButton()\n    }, 500)\n\n    // Longer delay for slower connections\n    setTimeout(() => {\n      injectSummarizeButton()\n    }, 2000)\n\n    // Handle YouTube's SPA navigation\n    let lastUrl = location.href\n    new MutationObserver(() => {\n      const url = location.href\n      if (url !== lastUrl) {\n        lastUrl = url\n        // Remove old button and re-inject on navigation\n        removeSummarizeButton()\n        setTimeout(() => {\n          injectSummarizeButton()\n        }, 1000)\n      }\n    }).observe(document.body, { childList: true, subtree: true })\n  },\n  matches: [\"*://www.youtube.com/watch*\"],\n  runAt: \"document_end\"\n})\n"
  },
  {
    "path": "src/hooks/chat-helper/index.ts",
    "content": "import {\n  getLastChatHistory,\n  saveHistory,\n  saveMessage,\n  updateMessage,\n  updateLastUsedModel as setLastUsedChatModel,\n  updateLastUsedPrompt as setLastUsedChatSystemPrompt,\n  updateChatHistoryCreatedAt\n} from \"@/db/dexie/helpers\"\nimport { ChatDocuments } from \"@/models/ChatTypes\"\nimport { generateTitle } from \"@/services/title\"\nimport { ChatHistory } from \"@/store/option\"\nimport { updatePageTitle } from \"@/utils/update-page-title\"\n\nexport const saveMessageOnError = async ({\n  e,\n  history,\n  setHistory,\n  image,\n  images,\n  userMessage,\n  botMessage,\n  historyId,\n  selectedModel,\n  setHistoryId,\n  isRegenerating,\n  message_source = \"web-ui\",\n  message_type,\n  prompt_content,\n  prompt_id,\n  isContinue,\n  documents = []\n}: {\n  e: any\n  setHistory: (history: ChatHistory) => void\n  history: ChatHistory\n  userMessage: string\n  image: string\n  images?: string[]\n  botMessage: string\n  historyId: string | null\n  selectedModel: string\n  setHistoryId: (historyId: string) => void\n  isRegenerating: boolean\n  message_source?: \"copilot\" | \"web-ui\"\n  message_type?: string\n  prompt_id?: string\n  prompt_content?: string\n  isContinue?: boolean\n  documents?: ChatDocuments\n}) => {\n  // Use images array if available, otherwise wrap single image\n  const imagesToSave = images && images.length > 0 ? images : (image ? [image] : [])\n\n  if (\n    e?.name === \"AbortError\" ||\n    e?.message === \"AbortError\" ||\n    e?.name?.includes(\"AbortError\") ||\n    e?.message?.includes(\"AbortError\")\n  ) {\n    setHistory([\n      ...history,\n      {\n        role: \"user\",\n        content: userMessage,\n        image,\n        images\n      },\n      {\n        role: \"assistant\",\n        content: botMessage\n      }\n    ])\n\n    if (historyId) {\n      if (!isRegenerating && !isContinue) {\n        await saveMessage({\n          history_id: historyId,\n          name: selectedModel,\n          role: \"user\",\n          content: userMessage,\n          images: imagesToSave,\n          time: 1,\n          message_type,\n          documents\n        })\n      }\n\n      if (isContinue) {\n        console.log(\"Saving Last Message\")\n        const lastMessage = await getLastChatHistory(historyId)\n        await updateMessage(historyId, lastMessage.id, botMessage)\n      } else {\n        await saveMessage({\n          history_id: historyId,\n          name: selectedModel,\n          role: \"assistant\",\n          content: botMessage,\n          images: [],\n          source: [],\n          time: 2,\n          message_type\n        })\n      }\n      await setLastUsedChatModel(historyId, selectedModel)\n      if (prompt_id || prompt_content) {\n        await setLastUsedChatSystemPrompt(historyId, {\n          prompt_content,\n          prompt_id\n        })\n      }\n\n      return historyId\n    } else {\n      const title = await generateTitle(selectedModel, [\n        ...history,\n        {\n          role: \"user\",\n          content: userMessage,\n          image,\n          images\n        },\n        {\n          role: \"assistant\",\n          content: botMessage\n        }\n      ], userMessage)\n      const newHistoryId = await saveHistory(title, false, message_source)\n      updatePageTitle(title)\n      if (!isRegenerating) {\n        await saveMessage({\n          history_id: newHistoryId.id,\n          name: selectedModel,\n          role: \"user\",\n          content: userMessage,\n          images: imagesToSave,\n          time: 1,\n          message_type,\n          documents\n        })\n      }\n\n      await saveMessage({\n        history_id: newHistoryId.id,\n        name: selectedModel,\n        role: \"assistant\",\n        content: botMessage,\n        images: [],\n        source: [],\n        time: 2,\n        message_type\n      })\n      setHistoryId(newHistoryId.id)\n      await setLastUsedChatModel(newHistoryId.id, selectedModel)\n      if (prompt_id || prompt_content) {\n        await setLastUsedChatSystemPrompt(newHistoryId.id, {\n          prompt_content,\n          prompt_id\n        })\n      }\n\n      return newHistoryId.id\n    }\n  }\n\n  return historyId\n}\n\nexport const saveMessageOnSuccess = async ({\n  historyId,\n  setHistoryId,\n  isRegenerate,\n  selectedModel,\n  message,\n  image,\n  images,\n  fullText,\n  source,\n  message_source = \"web-ui\",\n  message_type,\n  generationInfo,\n  prompt_id,\n  prompt_content,\n  reasoning_time_taken = 0,\n  isContinue,\n  documents = []\n}: {\n  historyId: string | null\n  setHistoryId: (historyId: string) => void\n  isRegenerate: boolean\n  selectedModel: string | null\n  message: string\n  image: string\n  images?: string[]\n  fullText: string\n  source: any[]\n  message_source?: \"copilot\" | \"web-ui\"\n  message_type?: string\n  generationInfo?: any\n  prompt_id?: string\n  prompt_content?: string\n  reasoning_time_taken?: number\n  isContinue?: boolean\n  documents?: ChatDocuments\n}) => {\n  // Use images array if available, otherwise wrap single image\n  const imagesToSave = images && images.length > 0 ? images : (image ? [image] : [])\n  if (historyId) {\n    if (!isRegenerate && !isContinue) {\n      await saveMessage({\n        history_id: historyId,\n        name: selectedModel,\n        role: \"user\",\n        content: message,\n        images: imagesToSave,\n        time: 1,\n        message_type,\n        generationInfo,\n        reasoning_time_taken,\n        documents\n      })\n    }\n\n    if (isContinue) {\n      console.log(\"Saving Last Message\")\n      const lastMessage = await getLastChatHistory(historyId)\n      console.log(\"lastMessage\", lastMessage)\n      await updateMessage(historyId, lastMessage.id, fullText)\n    } else {\n      await saveMessage(\n        {\n          history_id: historyId,\n          name: selectedModel,\n          role: \"assistant\",\n          content: fullText,\n          images: [],\n          source,\n          time: 2,\n          message_type,\n          generationInfo,\n          reasoning_time_taken\n        }\n        // historyId,\n        // selectedModel!,\n        // \"assistant\",\n        // fullText,\n        // [],\n        // source,\n        // 2,\n        // message_type,\n        // generationInfo,\n        // reasoning_time_taken\n      )\n    }\n\n    await setLastUsedChatModel(historyId, selectedModel!)\n    if (prompt_id || prompt_content) {\n      await setLastUsedChatSystemPrompt(historyId, {\n        prompt_content,\n        prompt_id\n      })\n    }\n\n    await updateChatHistoryCreatedAt(historyId)\n\n    return historyId\n  } else {\n    const title = await generateTitle(selectedModel, [\n      {\n        role: \"user\",\n        content: message,\n        image,\n        images\n      },\n      {\n        role: \"assistant\",\n        content: fullText\n      }\n    ], message)\n    updatePageTitle(title)\n    const newHistoryId = await saveHistory(title, false, message_source)\n\n    await saveMessage(\n      {\n        history_id: newHistoryId.id,\n        name: selectedModel,\n        role: \"user\",\n        content: message,\n        images: imagesToSave,\n        time: 1,\n        message_type,\n        generationInfo,\n        reasoning_time_taken,\n        documents\n      }\n      // newHistoryId.id,\n      // selectedModel,\n      // \"user\",\n      // message,\n      // [image],\n      // [],\n      // 1,\n      // message_type,\n      // generationInfo,\n      // reasoning_time_taken\n    )\n\n    await saveMessage(\n      {\n        history_id: newHistoryId.id,\n        name: selectedModel,\n        role: \"assistant\",\n        content: fullText,\n        images: [],\n        source,\n        time: 2,\n        message_type,\n        generationInfo,\n        reasoning_time_taken\n      }\n      // newHistoryId.id,\n      // selectedModel!,\n      // \"assistant\",\n      // fullText,\n      // [],\n      // source,\n      // 2,\n      // message_type,\n      // generationInfo,\n      // reasoning_time_taken\n    )\n    setHistoryId(newHistoryId.id)\n    await setLastUsedChatModel(newHistoryId.id, selectedModel!)\n    if (prompt_id || prompt_content) {\n      await setLastUsedChatSystemPrompt(newHistoryId.id, {\n        prompt_content,\n        prompt_id\n      })\n    }\n\n    return newHistoryId.id\n  }\n}\n"
  },
  {
    "path": "src/hooks/chat-modes/continueChatMode.ts",
    "content": "import { cleanUrl } from \"~/libs/clean-url\"\nimport {\n  getOllamaURL,\n  systemPromptForNonRagOption\n} from \"~/services/ollama\"\nimport { type ChatHistory, type Message } from \"~/store/option\"\nimport { getPromptById } from \"@/db/dexie/helpers\"\nimport { generateHistory } from \"@/utils/generate-history\"\nimport { pageAssistModel } from \"@/models\"\nimport {\n  isReasoningEnded,\n  isReasoningStarted,\n  mergeReasoningContent\n} from \"@/libs/reasoning\"\nimport { systemPromptFormatter } from \"@/utils/system-message\"\n\nexport const continueChatMode = async (\n  messages: Message[],\n  history: ChatHistory,\n  signal: AbortSignal,\n  {\n    selectedModel,\n    selectedSystemPrompt,\n    currentChatModelSettings,\n    setMessages,\n    saveMessageOnSuccess,\n    saveMessageOnError,\n    setHistory,\n    setIsProcessing,\n    setStreaming,\n    setAbortController,\n    historyId,\n    setHistoryId\n  }: {\n    selectedModel: string\n    selectedSystemPrompt: string\n    currentChatModelSettings: any\n    setMessages: (messages: Message[] | ((prev: Message[]) => Message[])) => void\n    saveMessageOnSuccess: (data: any) => Promise<string | null>\n    saveMessageOnError: (data: any) => Promise<string | null>\n    setHistory: (history: ChatHistory) => void\n    setIsProcessing: (value: boolean) => void\n    setStreaming: (value: boolean) => void\n    setAbortController: (controller: AbortController | null) => void\n    historyId: string | null\n    setHistoryId: (id: string) => void\n  }\n) => {\n  console.log(\"Using continueChatMode\")\n  const url = await getOllamaURL()\n  let promptId: string | undefined = selectedSystemPrompt\n  let promptContent: string | undefined = undefined\n\n  const ollama = await pageAssistModel({\n    model: selectedModel!,\n    baseUrl: cleanUrl(url)\n  })\n\n  let newMessage: Message[] = []\n\n  const lastMessage = messages[messages.length - 1]\n  let generateMessageId = lastMessage.id\n  newMessage = [...messages]\n  newMessage[newMessage.length - 1] = {\n    ...lastMessage,\n    message: lastMessage.message + \"▋\"\n  }\n  setMessages(newMessage)\n  let fullText = lastMessage.message\n  let contentToSave = \"\"\n  let timetaken = 0\n\n  try {\n    const prompt = await systemPromptForNonRagOption()\n    const selectedPrompt = await getPromptById(selectedSystemPrompt)\n\n    const applicationChatHistory = generateHistory(history, selectedModel)\n\n    if (prompt && !selectedPrompt) {\n      applicationChatHistory.unshift(\n        await systemPromptFormatter({\n          content: prompt\n        })\n      )\n    }\n\n    const isTempSystemprompt =\n      currentChatModelSettings.systemPrompt &&\n      currentChatModelSettings.systemPrompt?.trim().length > 0\n\n    if (!isTempSystemprompt && selectedPrompt) {\n      applicationChatHistory.unshift(\n        await systemPromptFormatter({\n          content: selectedPrompt.content\n        })\n      )\n      promptContent = selectedPrompt.content\n    }\n\n    if (isTempSystemprompt) {\n      applicationChatHistory.unshift(\n        await systemPromptFormatter({\n          content: currentChatModelSettings.systemPrompt\n        })\n      )\n      promptContent = currentChatModelSettings.systemPrompt\n    }\n\n    let generationInfo: any | undefined = undefined\n\n    const chunks = await ollama.stream([...applicationChatHistory], {\n      signal: signal,\n      callbacks: [\n        {\n          handleLLMEnd(output: any): any {\n            try {\n              generationInfo = output?.generations?.[0][0]?.generationInfo\n            } catch (e) {\n              console.error(\"handleLLMEnd error\", e)\n            }\n          }\n        }\n      ]\n    })\n\n    let count = 0\n    let reasoningStartTime: Date | null = null\n    let reasoningEndTime: Date | null = null\n    let apiReasoning: boolean = false\n    for await (const chunk of chunks) {\n      if (chunk?.additional_kwargs?.reasoning_content) {\n        const reasoningContent = mergeReasoningContent(\n          fullText,\n          chunk?.additional_kwargs?.reasoning_content || \"\"\n        )\n        contentToSave = reasoningContent\n        fullText = reasoningContent\n        apiReasoning = true\n      }\n\n      if (apiReasoning && chunk?.content) {\n        fullText += \"</think>\"\n        contentToSave += \"</think>\"\n        apiReasoning = false\n      }\n\n      contentToSave += chunk?.content\n      fullText += chunk?.content\n\n      if (isReasoningStarted(fullText) && !reasoningStartTime) {\n        reasoningStartTime = new Date()\n      }\n\n      if (\n        reasoningStartTime &&\n        !reasoningEndTime &&\n        isReasoningEnded(fullText)\n      ) {\n        reasoningEndTime = new Date()\n        const reasoningTime =\n          reasoningEndTime.getTime() - reasoningStartTime.getTime()\n        timetaken = reasoningTime\n      }\n\n      if (count === 0) {\n        setIsProcessing(true)\n      }\n\n      setMessages((prev) => {\n        return prev.map((message) => {\n          if (message.id === generateMessageId) {\n            return {\n              ...message,\n              message: fullText + \"▋\",\n              reasoning_time_taken: timetaken\n            }\n          }\n          return message\n        })\n      })\n      count++\n    }\n\n    setMessages((prev) => {\n      return prev.map((message) => {\n        if (message.id === generateMessageId) {\n          return {\n            ...message,\n            message: fullText,\n            generationInfo,\n            reasoning_time_taken: timetaken\n          }\n        }\n        return message\n      })\n    })\n\n    let newHistory = [...history]\n\n    newHistory[newHistory.length - 1] = {\n      ...newHistory[newHistory.length - 1],\n      content: fullText\n    }\n    setHistory(newHistory)\n    await saveMessageOnSuccess({\n      historyId,\n      setHistoryId,\n      isRegenerate: false,\n      selectedModel: selectedModel,\n      message: \"\",\n      image: \"\",\n      fullText,\n      source: [],\n      generationInfo,\n      prompt_content: promptContent,\n      prompt_id: promptId,\n      reasoning_time_taken: timetaken,\n      isContinue: true\n    })\n\n    setIsProcessing(false)\n    setStreaming(false)\n  } catch (e) {\n    const errorSave = await saveMessageOnError({\n      e,\n      botMessage: fullText,\n      history,\n      historyId,\n      image: \"\",\n      selectedModel,\n      setHistory,\n      setHistoryId,\n      userMessage: \"\",\n      isRegenerating: false,\n      isContinue: true,\n      prompt_content: promptContent,\n      prompt_id: promptId\n    })\n\n    if (!errorSave) {\n      throw e // Re-throw to be handled by the calling function\n    }\n    setIsProcessing(false)\n    setStreaming(false)\n  } finally {\n    setAbortController(null)\n  }\n}\n"
  },
  {
    "path": "src/hooks/chat-modes/documentChatMode.ts",
    "content": "import { cleanUrl } from \"~/libs/clean-url\"\nimport {\n  defaultEmbeddingModelForRag,\n  getOllamaURL,\n  geWebSearchFollowUpPrompt,\n  promptForRag\n} from \"~/services/ollama\"\nimport { type ChatHistory, type Message } from \"~/store/option\"\nimport { addFileToSession, generateID, getSessionFiles } from \"@/db/dexie/helpers\"\nimport { generateHistory } from \"@/utils/generate-history\"\nimport { pageAssistModel } from \"@/models\"\nimport { humanMessageFormatter } from \"@/utils/human-message\"\nimport {\n  isReasoningEnded,\n  isReasoningStarted,\n  mergeReasoningContent,\n  removeReasoning\n} from \"@/libs/reasoning\"\nimport { getModelNicknameByID } from \"@/db/dexie/nickname\"\nimport { formatDocs } from \"@/chain/chat-with-x\"\nimport { getAllDefaultModelSettings } from \"@/services/model-settings\"\nimport { getNoOfRetrievedDocs } from \"@/services/app\"\nimport { pageAssistEmbeddingModel } from \"@/models/embedding\"\nimport { UploadedFile } from \"@/db/dexie/types\"\nimport { getSystemPromptForWeb, isQueryHaveWebsite } from \"@/web/web\"\nimport { PAMemoryVectorStore } from \"@/libs/PAMemoryVectorStore\"\nimport { getMaxContextSize } from \"@/services/kb\"\n\nexport const documentChatMode = async (\n  message: string,\n  image: string,\n  isRegenerate: boolean,\n  messages: Message[],\n  history: ChatHistory,\n  signal: AbortSignal,\n  uploadedFiles: UploadedFile[],\n  {\n    selectedModel,\n    useOCR,\n    currentChatModelSettings,\n    setMessages,\n    saveMessageOnSuccess,\n    saveMessageOnError,\n    setHistory,\n    setIsProcessing,\n    setStreaming,\n    setAbortController,\n    historyId,\n    setHistoryId,\n    fileRetrievalEnabled,\n    setActionInfo,\n    webSearch\n  }: {\n    selectedModel: string\n    useOCR: boolean\n    currentChatModelSettings: any\n    setMessages: (\n      messages: Message[] | ((prev: Message[]) => Message[])\n    ) => void\n    saveMessageOnSuccess: (data: any) => Promise<string | null>\n    saveMessageOnError: (data: any) => Promise<string | null>\n    setHistory: (history: ChatHistory) => void\n    setIsProcessing: (value: boolean) => void\n    setStreaming: (value: boolean) => void\n    setAbortController: (controller: AbortController | null) => void\n    historyId: string | null\n    setHistoryId: (id: string) => void\n    fileRetrievalEnabled: boolean\n    setActionInfo: (actionInfo: string | null) => void\n    webSearch: boolean\n  }\n) => {\n  const url = await getOllamaURL()\n  const userDefaultModelSettings = await getAllDefaultModelSettings()\n\n  let sessionFiles: UploadedFile[] = []\n  const currentFiles: UploadedFile[] = uploadedFiles\n\n  if (historyId) {\n    sessionFiles = await getSessionFiles(historyId)\n  }\n\n  const newFiles = currentFiles.filter(\n    (f) => !sessionFiles.some((sf) => sf.id === f.id)\n  )\n\n  const allFiles = [...sessionFiles, ...newFiles]\n  const ollama = await pageAssistModel({\n    model: selectedModel!,\n    baseUrl: cleanUrl(url)\n  })\n\n  let newMessage: Message[] = []\n  let generateMessageId = generateID()\n  const modelInfo = await getModelNicknameByID(selectedModel)\n\n  if (!isRegenerate) {\n    newMessage = [\n      ...messages,\n      {\n        isBot: false,\n        name: \"You\",\n        message,\n        sources: [],\n        images: image ? [image] : [],\n        documents: newFiles.map((f) => ({\n          type: \"file\",\n          filename: f.filename,\n          fileSize: f.size\n        }))\n      },\n      {\n        isBot: true,\n        name: selectedModel,\n        message: \"▋\",\n        sources: [],\n        id: generateMessageId,\n        modelImage: modelInfo?.model_avatar,\n        modelName: modelInfo?.model_name || selectedModel\n      }\n    ]\n  } else {\n    newMessage = [\n      ...messages,\n      {\n        isBot: true,\n        name: selectedModel,\n        message: \"▋\",\n        sources: [],\n        id: generateMessageId,\n        modelImage: modelInfo?.model_avatar,\n        modelName: modelInfo?.model_name || selectedModel\n      }\n    ]\n  }\n  setMessages(newMessage)\n  let fullText = \"\"\n  let contentToSave = \"\"\n\n  const embeddingModel = await defaultEmbeddingModelForRag()\n  const ollamaUrl = await getOllamaURL()\n  const ollamaEmbedding = await pageAssistEmbeddingModel({\n    model: embeddingModel || selectedModel,\n    baseUrl: cleanUrl(ollamaUrl),\n    keepAlive:\n      currentChatModelSettings?.keepAlive ?? userDefaultModelSettings?.keepAlive\n  })\n\n  let timetaken = 0\n  try {\n    let query = message\n    const { ragPrompt: systemPrompt, ragQuestionPrompt: questionPrompt } =\n      await promptForRag()\n\n    let context: string = \"\"\n    let source: any[] = []\n    const docSize = await getNoOfRetrievedDocs()\n\n    if (webSearch) {\n      //  setIsSearchingInternet(true)\n      setActionInfo(\"webSearch\")\n\n      let query = message\n\n      // if (newMessage.length > 2) {\n      let questionPrompt = await geWebSearchFollowUpPrompt()\n      const lastTenMessages = newMessage.slice(-10)\n      lastTenMessages.pop()\n      const chat_history = lastTenMessages\n        .map((message) => {\n          return `${message.isBot ? \"Assistant: \" : \"Human: \"}${message.message}`\n        })\n        .join(\"\\n\")\n      const promptForQuestion = questionPrompt\n        .replaceAll(\"{chat_history}\", chat_history)\n        .replaceAll(\"{question}\", message)\n      const questionModel = await pageAssistModel({\n        model: selectedModel!,\n        baseUrl: cleanUrl(url)\n      })\n\n      let questionMessage = await humanMessageFormatter({\n        content: [\n          {\n            text: promptForQuestion,\n            type: \"text\"\n          }\n        ],\n        model: selectedModel,\n        useOCR: useOCR\n      })\n\n      if (image.length > 0) {\n        questionMessage = await humanMessageFormatter({\n          content: [\n            {\n              text: promptForQuestion,\n              type: \"text\"\n            },\n            {\n              image_url: image,\n              type: \"image_url\"\n            }\n          ],\n          model: selectedModel,\n          useOCR: useOCR\n        })\n      }\n      try {\n        const isWebQuery = await isQueryHaveWebsite(query)\n        if (!isWebQuery) {\n          const response = await questionModel.invoke([questionMessage])\n          query = response?.content?.toString() || message\n          query = removeReasoning(query)\n        }\n      } catch (error) {\n        console.error(\"Error in questionModel.invoke:\", error)\n      }\n\n      const { prompt, source: webSource } = await getSystemPromptForWeb(\n        query,\n        true\n      )\n\n      context += prompt + \"\\n\"\n      source = [\n        ...source,\n        ...webSource.map((source) => {\n          return {\n            ...source,\n            type: \"url\"\n          }\n        })\n      ]\n\n      setActionInfo(null)\n    }\n    if (newMessage.length > 2) {\n      const lastTenMessages = newMessage.slice(-10)\n      lastTenMessages.pop()\n      const chat_history = lastTenMessages\n        .map((message) => {\n          return `${message.isBot ? \"Assistant: \" : \"Human: \"}${message.message}`\n        })\n        .join(\"\\n\")\n      const promptForQuestion = questionPrompt\n        .replaceAll(\"{chat_history}\", chat_history)\n        .replaceAll(\"{question}\", message)\n      const questionOllama = await pageAssistModel({\n        model: selectedModel!,\n        baseUrl: cleanUrl(url)\n      })\n      const response = await questionOllama.invoke(promptForQuestion)\n      query = response.content.toString()\n      query = removeReasoning(query)\n    }\n    if (uploadedFiles.length > 0) {\n      if (fileRetrievalEnabled) {\n        if (!embeddingModel?.length) {\n          throw new Error(\"No embedding model selected\")\n        }\n        setActionInfo(\"embeddingGen\")\n        const documents = allFiles.map((file) => ({\n          pageContent: file.content,\n          metadata: {\n            source: file.filename,\n            type: file.type,\n            size: file.size,\n            uploadedAt: file.uploadedAt\n          }\n        }))\n\n        const textSplitter = await getPageAssistTextSplitter()\n        const chunks = await textSplitter.splitDocuments(documents)\n\n        const vectorstore = await PAMemoryVectorStore.fromDocuments(\n          chunks,\n          ollamaEmbedding\n        )\n        setActionInfo(\"semanticSearch\")\n        const docs = await vectorstore.similaritySearch(query, docSize)\n        context += formatDocs(docs)\n        source = [\n          ...source,\n          ...docs.map((doc) => {\n            return {\n              ...doc,\n              name: doc?.metadata?.source || \"untitled\",\n              type: doc?.metadata?.type || \"unknown\",\n              mode: \"rag\",\n              url: \"\"\n            }\n          })\n        ]\n\n        setActionInfo(null)\n      } else {\n        const maxContextSize = await getMaxContextSize()\n\n        context += allFiles\n          .map((f) => `File: ${f.filename}\\nContent: ${f.content}\\n---\\n`)\n          .join(\"\")\n          .substring(0, maxContextSize)\n        source = [\n          ...source,\n          ...allFiles.map((file) => ({\n            pageContent: file.content.substring(0, 200) + \"...\",\n            metadata: {\n              source: file.filename,\n              type: file.type,\n              mode: \"rag\"\n            },\n            name: file.filename,\n            type: file.type,\n            mode: \"rag\",\n            url: \"\"\n          }))\n        ]\n      }\n    } else {\n      context += \"No documents uploaded for this conversation.\"\n    }\n\n    let humanMessage = await humanMessageFormatter({\n      content: [\n        {\n          text: systemPrompt\n            .replace(\"{context}\", context)\n            .replace(\"{question}\", message),\n          type: \"text\"\n        }\n      ],\n      model: selectedModel,\n      useOCR: useOCR\n    })\n\n    const applicationChatHistory = generateHistory(history, selectedModel)\n\n    let generationInfo: any | undefined = undefined\n\n    const chunks = await ollama.stream(\n      [...applicationChatHistory, humanMessage],\n      {\n        signal: signal,\n        callbacks: [\n          {\n            handleLLMEnd(output: any): any {\n              try {\n                generationInfo = output?.generations?.[0][0]?.generationInfo\n              } catch (e) {\n                console.error(\"handleLLMEnd error\", e)\n              }\n            }\n          }\n        ]\n      }\n    )\n    let count = 0\n    let reasoningStartTime: Date | undefined = undefined\n    let reasoningEndTime: Date | undefined = undefined\n    let apiReasoning = false\n\n    for await (const chunk of chunks) {\n      if (chunk?.additional_kwargs?.reasoning_content) {\n        const reasoningContent = mergeReasoningContent(\n          fullText,\n          chunk?.additional_kwargs?.reasoning_content || \"\"\n        )\n        contentToSave = reasoningContent\n        fullText = reasoningContent\n        apiReasoning = true\n      }\n\n      if (apiReasoning && chunk?.content) {\n        fullText += \"</think>\"\n        contentToSave += \"</think>\"\n        apiReasoning = false\n      }\n\n      contentToSave += chunk?.content\n      fullText += chunk?.content\n      if (count === 0) {\n        setIsProcessing(true)\n      }\n      if (isReasoningStarted(fullText) && !reasoningStartTime) {\n        reasoningStartTime = new Date()\n      }\n\n      if (\n        reasoningStartTime &&\n        !reasoningEndTime &&\n        isReasoningEnded(fullText)\n      ) {\n        reasoningEndTime = new Date()\n        const reasoningTime =\n          reasoningEndTime.getTime() - reasoningStartTime.getTime()\n        timetaken = reasoningTime\n      }\n      setMessages((prev) => {\n        return prev.map((message) => {\n          if (message.id === generateMessageId) {\n            return {\n              ...message,\n              message: fullText + \"▋\",\n              reasoning_time_taken: timetaken\n            }\n          }\n          return message\n        })\n      })\n      count++\n    }\n    // update the message with the full text\n    setMessages((prev) => {\n      return prev.map((message) => {\n        if (message.id === generateMessageId) {\n          return {\n            ...message,\n            message: fullText,\n            sources: source,\n            generationInfo,\n            reasoning_time_taken: timetaken\n          }\n        }\n        return message\n      })\n    })\n\n    setHistory([\n      ...history,\n      {\n        role: \"user\",\n        content: message,\n        image\n      },\n      {\n        role: \"assistant\",\n        content: fullText\n      }\n    ])\n\n    const chatHistoryId = await saveMessageOnSuccess({\n      historyId,\n      setHistoryId,\n      isRegenerate,\n      selectedModel: selectedModel,\n      message,\n      image,\n      fullText,\n      source,\n      generationInfo,\n      reasoning_time_taken: timetaken,\n      documents: uploadedFiles.map((f) => ({\n        type: \"file\",\n        filename: f.filename,\n        fileSize: f.size,\n        processed: f.processed\n      }))\n    })\n\n    if (chatHistoryId) {\n      for (const file of newFiles) {\n        await addFileToSession(chatHistoryId, file)\n      }\n    }\n\n    setIsProcessing(false)\n    setStreaming(false)\n  } catch (e) {\n    console.log(e)\n    const errorSave = await saveMessageOnError({\n      e,\n      botMessage: fullText,\n      history,\n      historyId,\n      image,\n      selectedModel,\n      setHistory,\n      setHistoryId,\n      userMessage: message,\n      isRegenerating: isRegenerate\n    })\n\n    if (!errorSave) {\n      throw e // Re-throw to be handled by the calling function\n    }\n    setIsProcessing(false)\n    setStreaming(false)\n  } finally {\n    setAbortController(null)\n  }\n}\n"
  },
  {
    "path": "src/hooks/chat-modes/normalChatMode.ts",
    "content": "import { cleanUrl } from \"~/libs/clean-url\"\nimport {\n  getOllamaURL,\n  systemPromptForNonRagOption\n} from \"~/services/ollama\"\nimport { type ChatHistory, type Message } from \"~/store/option\"\nimport { generateID, getPromptById } from \"@/db/dexie/helpers\"\nimport { generateHistory } from \"@/utils/generate-history\"\nimport { pageAssistModel } from \"@/models\"\nimport { humanMessageFormatter } from \"@/utils/human-message\"\nimport {\n  isReasoningEnded,\n  isReasoningStarted,\n  mergeReasoningContent\n} from \"@/libs/reasoning\"\nimport { runMcpNormalChatMode } from \"@/libs/mcp/normal-chat\"\nimport { McpBootstrapError } from \"@/libs/mcp/errors\"\nimport { getModelNicknameByID } from \"@/db/dexie/nickname\"\nimport { systemPromptFormatter } from \"@/utils/system-message\"\n\nexport const normalChatMode = async (\n  message: string,\n  image: string,\n  isRegenerate: boolean,\n  messages: Message[],\n  history: ChatHistory,\n  signal: AbortSignal,\n  {\n    selectedModel,\n    useOCR,\n    selectedSystemPrompt,\n    currentChatModelSettings,\n    setMessages,\n    saveMessageOnSuccess,\n    saveMessageOnError,\n    setHistory,\n    setIsProcessing,\n    setStreaming,\n    setAbortController,\n    historyId,\n    setHistoryId,\n    uploadedFiles,\n    images,\n    setActionInfo,\n    temporaryChat,\n    messageSource\n  }: {\n    selectedModel: string\n    useOCR: boolean\n    selectedSystemPrompt: string\n    currentChatModelSettings: any\n    setMessages: (messages: Message[] | ((prev: Message[]) => Message[])) => void\n    saveMessageOnSuccess: (data: any) => Promise<string | null>\n    saveMessageOnError: (data: any) => Promise<string | null>\n    setHistory: (history: ChatHistory) => void\n    setIsProcessing: (value: boolean) => void\n    setStreaming: (value: boolean) => void\n    setAbortController: (controller: AbortController | null) => void\n    historyId: string | null\n    setHistoryId: (id: string) => void\n    uploadedFiles?: any[]\n    images?: string[]\n    setActionInfo?: (value: any) => void\n    temporaryChat?: boolean\n    messageSource?: \"copilot\" | \"web-ui\"\n  }\n) => {\n  console.log(\"Using normalChatMode\")\n  setStreaming(true)\n  try {\n    const handledByMcp = await runMcpNormalChatMode(\n      message,\n      image,\n      isRegenerate,\n      messages,\n      history,\n      signal,\n      {\n        selectedModel,\n        useOCR,\n        selectedSystemPrompt,\n        currentChatModelSettings,\n        setMessages,\n        setHistory,\n        setIsProcessing,\n        setStreaming,\n        setActionInfo: setActionInfo || (() => {}),\n        historyId,\n        setHistoryId,\n        uploadedFiles,\n        images,\n        temporaryChat,\n        messageSource\n      }\n    )\n\n    if (handledByMcp) {\n      return\n    }\n  } catch (error) {\n    if (error instanceof McpBootstrapError) {\n      const processedImages = (images || []).map((currentImage) => {\n        if (currentImage.length > 0 && !currentImage.startsWith(\"data:image\")) {\n          return `data:image/jpeg;base64,${currentImage.split(\",\")[1]}`\n        }\n\n        return currentImage\n      })\n      const imagesToSave =\n        processedImages.length > 0 ? processedImages : image ? [image] : []\n\n      const errorSave = await saveMessageOnError({\n        e: error,\n        botMessage: \"\",\n        history,\n        historyId,\n        image: imagesToSave.length > 0 ? imagesToSave[0] : \"\",\n        images: imagesToSave,\n        selectedModel,\n        setHistory,\n        setHistoryId,\n        userMessage: message,\n        isRegenerating: isRegenerate,\n        message_source: messageSource\n      })\n\n      if (!errorSave) {\n        throw error\n      }\n\n      setIsProcessing(false)\n      setStreaming(false)\n      return\n    }\n\n    throw error\n  }\n  const url = await getOllamaURL()\n  let promptId: string | undefined = selectedSystemPrompt\n  let promptContent: string | undefined = undefined\n\n  // Process images array for base64 formatting\n  const processedImages = (images || []).map((img) => {\n    if (img.length > 0 && !img.startsWith(\"data:image\")) {\n      return `data:image/jpeg;base64,${img.split(\",\")[1]}`\n    }\n    return img\n  })\n\n  // Keep backward compatibility with single image\n  if (image.length > 0) {\n    image = `data:image/jpeg;base64,${image.split(\",\")[1]}`\n  }\n\n  const ollama = await pageAssistModel({\n    model: selectedModel!,\n    baseUrl: cleanUrl(url)\n  })\n\n  let newMessage: Message[] = []\n  let generateMessageId = generateID()\n  const modelInfo = await getModelNicknameByID(selectedModel)\n\n  if (!isRegenerate) {\n    // Use images array if available, otherwise fall back to single image\n    const userImages = processedImages.length > 0 ? processedImages : (image ? [image] : [])\n\n    newMessage = [\n      ...messages,\n      {\n        isBot: false,\n        name: \"You\",\n        message,\n        sources: [],\n        images: userImages,\n        modelImage: modelInfo?.model_avatar,\n        modelName: modelInfo?.model_name || selectedModel,\n        documents: uploadedFiles?.map(f => ({\n          type: \"file\",\n          filename: f.filename,\n          fileSize: f.size,\n          processed: f.processed\n        })) || []\n      },\n      {\n        isBot: true,\n        name: selectedModel,\n        message: \"▋\",\n        sources: [],\n        id: generateMessageId,\n        modelImage: modelInfo?.model_avatar,\n        modelName: modelInfo?.model_name || selectedModel\n      }\n    ]\n  } else {\n    newMessage = [\n      ...messages,\n      {\n        isBot: true,\n        name: selectedModel,\n        message: \"▋\",\n        sources: [],\n        id: generateMessageId,\n        modelImage: modelInfo?.model_avatar,\n        modelName: modelInfo?.model_name || selectedModel\n      }\n    ]\n  }\n  setMessages(newMessage)\n  let fullText = \"\"\n  let contentToSave = \"\"\n  let timetaken = 0\n\n  try {\n    const prompt = await systemPromptForNonRagOption()\n    const selectedPrompt = await getPromptById(selectedSystemPrompt)\n\n    // Build content array with text and multiple images\n    const contentArray: any[] = [\n      {\n        text: message,\n        type: \"text\"\n      }\n    ]\n\n    // Add all images to content array (use processedImages if available, otherwise single image)\n    const imagesToUse = processedImages.length > 0 ? processedImages : (image.length > 0 ? [image] : [])\n\n    imagesToUse.forEach((img) => {\n      contentArray.push({\n        image_url: img,\n        type: \"image_url\"\n      })\n    })\n\n    let humanMessage = await humanMessageFormatter({\n      content: contentArray,\n      model: selectedModel,\n      useOCR: useOCR\n    })\n\n    const applicationChatHistory = generateHistory(history, selectedModel)\n\n    if (prompt && !selectedPrompt) {\n      applicationChatHistory.unshift(\n        await systemPromptFormatter({\n          content: prompt\n        })\n      )\n    }\n\n    const isTempSystemprompt =\n      currentChatModelSettings.systemPrompt &&\n      currentChatModelSettings.systemPrompt?.trim().length > 0\n\n    if (!isTempSystemprompt && selectedPrompt) {\n      applicationChatHistory.unshift(\n        await systemPromptFormatter({\n          content: selectedPrompt.content\n        })\n      )\n      promptContent = selectedPrompt.content\n    }\n\n    if (isTempSystemprompt) {\n      applicationChatHistory.unshift(\n        await systemPromptFormatter({\n          content: currentChatModelSettings.systemPrompt\n        })\n      )\n      promptContent = currentChatModelSettings.systemPrompt\n    }\n\n    let generationInfo: any | undefined = undefined\n\n    const chunks = await ollama.stream(\n      [...applicationChatHistory, humanMessage],\n      {\n        signal: signal,\n        callbacks: [\n          {\n            handleLLMEnd(output: any): any {\n              try {\n                generationInfo = output?.generations?.[0][0]?.generationInfo\n              } catch (e) {\n                console.error(\"handleLLMEnd error\", e)\n              }\n            }\n          }\n        ]\n      }\n    )\n\n    let count = 0\n    let reasoningStartTime: Date | null = null\n    let reasoningEndTime: Date | null = null\n    let apiReasoning: boolean = false\n\n    for await (const chunk of chunks) {\n      if (chunk?.additional_kwargs?.reasoning_content) {\n        const reasoningContent = mergeReasoningContent(\n          fullText,\n          chunk?.additional_kwargs?.reasoning_content || \"\"\n        )\n        contentToSave = reasoningContent\n        fullText = reasoningContent\n        apiReasoning = true\n      }\n\n      if (apiReasoning && chunk?.content) {\n        fullText += \"</think>\"\n        contentToSave += \"</think>\"\n        apiReasoning = false\n      }\n\n      contentToSave += chunk?.content\n      fullText += chunk?.content\n\n      if (isReasoningStarted(fullText) && !reasoningStartTime) {\n        reasoningStartTime = new Date()\n      }\n\n      if (\n        reasoningStartTime &&\n        !reasoningEndTime &&\n        isReasoningEnded(fullText)\n      ) {\n        reasoningEndTime = new Date()\n        const reasoningTime =\n          reasoningEndTime.getTime() - reasoningStartTime.getTime()\n        timetaken = reasoningTime\n      }\n\n      if (count === 0) {\n        setIsProcessing(true)\n      }\n      setMessages((prev) => {\n        return prev.map((message) => {\n          if (message.id === generateMessageId) {\n            return {\n              ...message,\n              message: fullText + \"▋\",\n              reasoning_time_taken: timetaken\n            }\n          }\n          return message\n        })\n      })\n      count++\n    }\n\n    setMessages((prev) => {\n      return prev.map((message) => {\n        if (message.id === generateMessageId) {\n          return {\n            ...message,\n            message: fullText,\n            generationInfo,\n            reasoning_time_taken: timetaken\n          }\n        }\n        return message\n      })\n    })\n\n    const imagesToSave = processedImages.length > 0 ? processedImages : (image ? [image] : [])\n\n    setHistory([\n      ...history,\n      {\n        role: \"user\",\n        content: message,\n        image: imagesToSave.length > 0 ? imagesToSave[0] : undefined,\n        images: imagesToSave.length > 0 ? imagesToSave : undefined\n      },\n      {\n        role: \"assistant\",\n        content: fullText\n      }\n    ])\n\n    await saveMessageOnSuccess({\n      historyId,\n      setHistoryId,\n      isRegenerate,\n      selectedModel: selectedModel,\n      message,\n      image: imagesToSave.length > 0 ? imagesToSave[0] : \"\",\n      images: imagesToSave,\n      fullText,\n      source: [],\n      generationInfo,\n      prompt_content: promptContent,\n      prompt_id: promptId,\n      reasoning_time_taken: timetaken\n    })\n\n    setIsProcessing(false)\n    setStreaming(false)\n  } catch (e) {\n\n    console.log(e)\n\n    const imagesToSave = processedImages.length > 0 ? processedImages : (image ? [image] : [])\n\n    const errorSave = await saveMessageOnError({\n      e,\n      botMessage: fullText,\n      history,\n      historyId,\n      image: imagesToSave.length > 0 ? imagesToSave[0] : \"\",\n      images: imagesToSave,\n      selectedModel,\n      setHistory,\n      setHistoryId,\n      userMessage: message,\n      isRegenerating: isRegenerate,\n      prompt_content: promptContent,\n      prompt_id: promptId\n    })\n\n    if (!errorSave) {\n      throw e // Re-throw to be handled by the calling function\n    }\n    setIsProcessing(false)\n    setStreaming(false)\n  } finally {\n    setAbortController(null)\n  }\n}\n"
  },
  {
    "path": "src/hooks/chat-modes/ragMode.ts",
    "content": "import { cleanUrl } from \"~/libs/clean-url\"\nimport {\n  defaultEmbeddingModelForRag,\n  getOllamaURL,\n  promptForRag\n} from \"~/services/ollama\"\nimport { type ChatHistory, type Message } from \"~/store/option\"\nimport { generateID } from \"@/db/dexie/helpers\"\nimport { generateHistory } from \"@/utils/generate-history\"\nimport { pageAssistModel } from \"@/models\"\nimport { humanMessageFormatter } from \"@/utils/human-message\"\nimport {\n  isReasoningEnded,\n  isReasoningStarted,\n  mergeReasoningContent,\n  removeReasoning\n} from \"@/libs/reasoning\"\nimport { getModelNicknameByID } from \"@/db/dexie/nickname\"\nimport { PageAssistVectorStore } from \"@/libs/PageAssistVectorStore\"\nimport { formatDocs } from \"@/chain/chat-with-x\"\nimport { getAllDefaultModelSettings } from \"@/services/model-settings\"\nimport { getNoOfRetrievedDocs } from \"@/services/app\"\nimport { pageAssistEmbeddingModel } from \"@/models/embedding\"\nimport { isChatWithWebsiteEnabled } from \"@/services/kb\"\nimport { getKnowledgeById } from \"@/db/dexie/knowledge\"\n\nexport const ragMode = async (\n  message: string,\n  image: string,\n  isRegenerate: boolean,\n  messages: Message[],\n  history: ChatHistory,\n  signal: AbortSignal,\n  {\n    selectedModel,\n    useOCR,\n    selectedKnowledge,\n    currentChatModelSettings,\n    setMessages,\n    saveMessageOnSuccess,\n    saveMessageOnError,\n    setHistory,\n    setIsProcessing,\n    setStreaming,\n    setAbortController,\n    historyId,\n    setHistoryId\n  }: {\n    selectedModel: string\n    useOCR: boolean\n    selectedKnowledge: any\n    currentChatModelSettings: any\n    setMessages: (messages: Message[] | ((prev: Message[]) => Message[])) => void\n    saveMessageOnSuccess: (data: any) => Promise<string | null>\n    saveMessageOnError: (data: any) => Promise<string | null>\n    setHistory: (history: ChatHistory) => void\n    setIsProcessing: (value: boolean) => void\n    setStreaming: (value: boolean) => void\n    setAbortController: (controller: AbortController | null) => void\n    historyId: string | null\n    setHistoryId: (id: string) => void\n  }\n) => {\n  console.log(\"Using ragMode\")\n  const url = await getOllamaURL()\n  const userDefaultModelSettings = await getAllDefaultModelSettings()\n\n  const ollama = await pageAssistModel({\n    model: selectedModel!,\n    baseUrl: cleanUrl(url)\n  })\n\n  let newMessage: Message[] = []\n  let generateMessageId = generateID()\n  const modelInfo = await getModelNicknameByID(selectedModel)\n\n  if (!isRegenerate) {\n    newMessage = [\n      ...messages,\n      {\n        isBot: false,\n        name: \"You\",\n        message,\n        sources: [],\n        images: []\n      },\n      {\n        isBot: true,\n        name: selectedModel,\n        message: \"▋\",\n        sources: [],\n        id: generateMessageId,\n        modelImage: modelInfo?.model_avatar,\n        modelName: modelInfo?.model_name || selectedModel\n      }\n    ]\n  } else {\n    newMessage = [\n      ...messages,\n      {\n        isBot: true,\n        name: selectedModel,\n        message: \"▋\",\n        sources: [],\n        id: generateMessageId,\n        modelImage: modelInfo?.model_avatar,\n        modelName: modelInfo?.model_name || selectedModel\n      }\n    ]\n  }\n  setMessages(newMessage)\n  let fullText = \"\"\n  let contentToSave = \"\"\n\n  const embeddingModle = await defaultEmbeddingModelForRag()\n  const ollamaUrl = await getOllamaURL()\n  const ollamaEmbedding = await pageAssistEmbeddingModel({\n    model: embeddingModle || selectedModel,\n    baseUrl: cleanUrl(ollamaUrl),\n    keepAlive:\n      currentChatModelSettings?.keepAlive ??\n      userDefaultModelSettings?.keepAlive\n  })\n\n  const kbInfo = await getKnowledgeById(selectedKnowledge.id)\n\n  let vectorstore = await PageAssistVectorStore.fromExistingIndex(\n    ollamaEmbedding,\n    {\n      file_id: null,\n      knownledge_id: selectedKnowledge.id\n    }\n  )\n  let timetaken = 0\n  try {\n    let query = message\n    let { ragPrompt: systemPrompt, ragQuestionPrompt: questionPrompt } =\n      await promptForRag()\n\n    console.log(kbInfo, \"kbInfo\")\n    if (kbInfo?.systemPrompt?.trim()) {\n      systemPrompt = kbInfo.systemPrompt\n    }\n\n    if (kbInfo?.followupPrompt?.trim()) {\n      questionPrompt = kbInfo.followupPrompt\n    }\n\n\n    if (newMessage.length > 2) {\n      const lastTenMessages = newMessage.slice(-10)\n      lastTenMessages.pop()\n      const chat_history = lastTenMessages\n        .map((message) => {\n          return `${message.isBot ? \"Assistant: \" : \"Human: \"}${message.message}`\n        })\n        .join(\"\\n\")\n      const promptForQuestion = questionPrompt\n        .replaceAll(\"{chat_history}\", chat_history)\n        .replaceAll(\"{question}\", message)\n      const questionOllama = await pageAssistModel({\n        model: selectedModel!,\n        baseUrl: cleanUrl(url)\n      })\n      const response = await questionOllama.invoke(promptForQuestion)\n      query = response.content.toString()\n      query = removeReasoning(query)\n    }\n    const docSize = await getNoOfRetrievedDocs()\n    // const useVS = await isChatWithWebsiteEnabled()\n    let context: string = \"\"\n    let source: any[] = []\n    // if (useVS) {\n    const docs = await vectorstore.similaritySearchKB(query, docSize)\n    context = formatDocs(docs)\n    source = docs.map((doc) => {\n      return {\n        ...doc,\n        name: doc?.metadata?.source || \"untitled\",\n        type: doc?.metadata?.type || \"unknown\",\n        mode: \"rag\",\n        url: \"\"\n      }\n    })\n    // } else {\n    //   const docs = await vectorstore.getAllPageContent()\n    //   context = docs.pageContent\n    //   source = docs.metadata.map((doc) => {\n    //     return {\n    //       ...doc,\n    //       name: doc?.source || \"untitled\",\n    //       type: doc?.type || \"unknown\",\n    //       mode: \"rag\",\n    //       url: \"\"\n    //     }\n    //   })\n    // }\n    //  message = message.trim().replaceAll(\"\\n\", \" \")\n\n    let humanMessage = await humanMessageFormatter({\n      content: [\n        {\n          text: systemPrompt\n            .replace(\"{context}\", context)\n            .replace(\"{question}\", message),\n          type: \"text\"\n        }\n      ],\n      model: selectedModel,\n      useOCR: useOCR\n    })\n\n    const applicationChatHistory = generateHistory(history, selectedModel)\n\n    let generationInfo: any | undefined = undefined\n\n    const chunks = await ollama.stream(\n      [...applicationChatHistory, humanMessage],\n      {\n        signal: signal,\n        callbacks: [\n          {\n            handleLLMEnd(output: any): any {\n              try {\n                generationInfo = output?.generations?.[0][0]?.generationInfo\n              } catch (e) {\n                console.error(\"handleLLMEnd error\", e)\n              }\n            }\n          }\n        ]\n      }\n    )\n    let count = 0\n    let reasoningStartTime: Date | undefined = undefined\n    let reasoningEndTime: Date | undefined = undefined\n    let apiReasoning = false\n\n    for await (const chunk of chunks) {\n      if (chunk?.additional_kwargs?.reasoning_content) {\n        const reasoningContent = mergeReasoningContent(\n          fullText,\n          chunk?.additional_kwargs?.reasoning_content || \"\"\n        )\n        contentToSave = reasoningContent\n        fullText = reasoningContent\n        apiReasoning = true\n      }\n\n      if (apiReasoning && chunk?.content) {\n        fullText += \"</think>\"\n        contentToSave += \"</think>\"\n        apiReasoning = false\n      }\n\n      contentToSave += chunk?.content\n      fullText += chunk?.content\n      if (count === 0) {\n        setIsProcessing(true)\n      }\n      if (isReasoningStarted(fullText) && !reasoningStartTime) {\n        reasoningStartTime = new Date()\n      }\n\n      if (\n        reasoningStartTime &&\n        !reasoningEndTime &&\n        isReasoningEnded(fullText)\n      ) {\n        reasoningEndTime = new Date()\n        const reasoningTime =\n          reasoningEndTime.getTime() - reasoningStartTime.getTime()\n        timetaken = reasoningTime\n      }\n      setMessages((prev) => {\n        return prev.map((message) => {\n          if (message.id === generateMessageId) {\n            return {\n              ...message,\n              message: fullText + \"▋\",\n              reasoning_time_taken: timetaken\n            }\n          }\n          return message\n        })\n      })\n      count++\n    }\n    // update the message with the full text\n    setMessages((prev) => {\n      return prev.map((message) => {\n        if (message.id === generateMessageId) {\n          return {\n            ...message,\n            message: fullText,\n            sources: source,\n            generationInfo,\n            reasoning_time_taken: timetaken\n          }\n        }\n        return message\n      })\n    })\n\n    setHistory([\n      ...history,\n      {\n        role: \"user\",\n        content: message,\n        image\n      },\n      {\n        role: \"assistant\",\n        content: fullText\n      }\n    ])\n\n    await saveMessageOnSuccess({\n      historyId,\n      setHistoryId,\n      isRegenerate,\n      selectedModel: selectedModel,\n      message,\n      image,\n      fullText,\n      source,\n      generationInfo,\n      reasoning_time_taken: timetaken\n    })\n\n    setIsProcessing(false)\n    setStreaming(false)\n  } catch (e) {\n    console.log(e)\n    const errorSave = await saveMessageOnError({\n      e,\n      botMessage: fullText,\n      history,\n      historyId,\n      image,\n      selectedModel,\n      setHistory,\n      setHistoryId,\n      userMessage: message,\n      isRegenerating: isRegenerate\n    })\n\n    if (!errorSave) {\n      throw e // Re-throw to be handled by the calling function\n    }\n    setIsProcessing(false)\n    setStreaming(false)\n  } finally {\n    setAbortController(null)\n  }\n}\n"
  },
  {
    "path": "src/hooks/chat-modes/searchChatMode.ts",
    "content": "import { cleanUrl } from \"~/libs/clean-url\"\nimport {\n  geWebSearchFollowUpPrompt,\n  getOllamaURL\n} from \"~/services/ollama\"\nimport { type ChatHistory, type Message } from \"~/store/option\"\nimport { generateID } from \"@/db/dexie/helpers\"\nimport { getSystemPromptForWeb, isQueryHaveWebsite } from \"~/web/web\"\nimport { generateHistory } from \"@/utils/generate-history\"\nimport { pageAssistModel } from \"@/models\"\nimport { humanMessageFormatter } from \"@/utils/human-message\"\nimport {\n  isReasoningEnded,\n  isReasoningStarted,\n  mergeReasoningContent,\n  removeReasoning\n} from \"@/libs/reasoning\"\nimport { getModelNicknameByID } from \"@/db/dexie/nickname\"\nimport { systemPromptFormatter } from \"@/utils/system-message\"\n\nexport const searchChatMode = async (\n  message: string,\n  image: string,\n  isRegenerate: boolean,\n  messages: Message[],\n  history: ChatHistory,\n  signal: AbortSignal,\n  {\n    selectedModel,\n    useOCR,\n    setMessages,\n    setIsSearchingInternet,\n    saveMessageOnSuccess,\n    saveMessageOnError,\n    setHistory,\n    setIsProcessing,\n    setStreaming,\n    setAbortController,\n    historyId,\n    setHistoryId,\n    images\n  }: {\n    selectedModel: string\n    useOCR: boolean\n    setMessages: (messages: Message[] | ((prev: Message[]) => Message[])) => void\n    setIsSearchingInternet: (value: boolean) => void\n    saveMessageOnSuccess: (data: any) => Promise<string | null>\n    saveMessageOnError: (data: any) => Promise<string | null>\n    setHistory: (history: ChatHistory) => void\n    setIsProcessing: (value: boolean) => void\n    setStreaming: (value: boolean) => void\n    setAbortController: (controller: AbortController | null) => void\n    historyId: string | null\n    setHistoryId: (id: string) => void\n    images?: string[]\n  }\n) => {\n  console.log(\"Using searchChatMode\")\n  const url = await getOllamaURL()\n\n  // Process images array for base64 formatting\n  const processedImages = (images || []).map((img) => {\n    if (img.length > 0 && !img.startsWith(\"data:image\")) {\n      return `data:image/jpeg;base64,${img.split(\",\")[1]}`\n    }\n    return img\n  })\n\n  // Keep backward compatibility with single image\n  if (image.length > 0) {\n    image = `data:image/jpeg;base64,${image.split(\",\")[1]}`\n  }\n\n  const ollama = await pageAssistModel({\n    model: selectedModel!,\n    baseUrl: cleanUrl(url)\n  })\n\n  let newMessage: Message[] = []\n  let generateMessageId = generateID()\n\n  const modelInfo = await getModelNicknameByID(selectedModel)\n  if (!isRegenerate) {\n    // Use images array if available, otherwise fall back to single image\n    const userImages = processedImages.length > 0 ? processedImages : (image ? [image] : [])\n\n    newMessage = [\n      ...messages,\n      {\n        isBot: false,\n        name: \"You\",\n        message,\n        sources: [],\n        images: userImages\n      },\n      {\n        isBot: true,\n        name: selectedModel,\n        message: \"▋\",\n        sources: [],\n        id: generateMessageId,\n        modelImage: modelInfo?.model_avatar,\n        modelName: modelInfo?.model_name || selectedModel\n      }\n    ]\n  } else {\n    newMessage = [\n      ...messages,\n      {\n        isBot: true,\n        name: selectedModel,\n        message: \"▋\",\n        sources: [],\n        id: generateMessageId,\n        modelImage: modelInfo?.model_avatar,\n        modelName: modelInfo?.model_name || selectedModel\n      }\n    ]\n  }\n  setMessages(newMessage)\n  let fullText = \"\"\n  let contentToSave = \"\"\n  let timetaken = 0\n\n  try {\n    setIsSearchingInternet(true)\n\n    let query = message\n\n    let questionPrompt = await geWebSearchFollowUpPrompt()\n    const lastTenMessages = newMessage.slice(-10)\n    lastTenMessages.pop()\n    const chat_history = lastTenMessages\n      .map((message) => {\n        return `${message.isBot ? \"Assistant: \" : \"Human: \"}${message.message}`\n      })\n      .join(\"\\n\")\n    const promptForQuestion = questionPrompt\n      .replaceAll(\"{chat_history}\", chat_history)\n      .replaceAll(\"{question}\", message)\n    const questionModel = await pageAssistModel({\n      model: selectedModel!,\n      baseUrl: cleanUrl(url)\n    })\n\n    // Build content array with text and images for question message\n    const questionContentArray: any[] = [\n      {\n        text: promptForQuestion,\n        type: \"text\"\n      }\n    ]\n\n    // Add all images to content array (use processedImages if available, otherwise single image)\n    const imagesToUse = processedImages.length > 0 ? processedImages : (image.length > 0 ? [image] : [])\n\n    imagesToUse.forEach((img) => {\n      questionContentArray.push({\n        image_url: img,\n        type: \"image_url\"\n      })\n    })\n\n    let questionMessage = await humanMessageFormatter({\n      content: questionContentArray,\n      model: selectedModel,\n      useOCR: useOCR\n    })\n    try {\n      const isWebQuery = await isQueryHaveWebsite(query)\n      if (!isWebQuery) {\n        const response = await questionModel.invoke([questionMessage])\n        query = response?.content?.toString() || message\n        query = removeReasoning(query)\n      }\n    } catch (error) {\n      console.error(\"Error in questionModel.invoke:\", error)\n    }\n    // }\n\n    const { prompt, source } = await getSystemPromptForWeb(query)\n    setIsSearchingInternet(false)\n\n    // Build content array with text and all images\n    const contentArray: any[] = [\n      {\n        text: message,\n        type: \"text\"\n      }\n    ]\n\n    imagesToUse.forEach((img) => {\n      contentArray.push({\n        image_url: img,\n        type: \"image_url\"\n      })\n    })\n\n    let humanMessage = await humanMessageFormatter({\n      content: contentArray,\n      model: selectedModel,\n      useOCR: useOCR\n    })\n\n    const applicationChatHistory = generateHistory(history, selectedModel)\n\n    if (prompt) {\n      applicationChatHistory.unshift(\n        await systemPromptFormatter({\n          content: prompt\n        })\n      )\n    }\n\n    let generationInfo: any | undefined = undefined\n\n    const chunks = await ollama.stream(\n      [...applicationChatHistory, humanMessage],\n      {\n        signal: signal,\n        callbacks: [\n          {\n            handleLLMEnd(output: any): any {\n              try {\n                generationInfo = output?.generations?.[0][0]?.generationInfo\n              } catch (e) {\n                console.error(\"handleLLMEnd error\", e)\n              }\n            }\n          }\n        ]\n      }\n    )\n    let count = 0\n    let reasoningStartTime: Date | undefined = undefined\n    let reasoningEndTime: Date | undefined = undefined\n    let apiReasoning = false\n    for await (const chunk of chunks) {\n      if (chunk?.additional_kwargs?.reasoning_content) {\n        const reasoningContent = mergeReasoningContent(\n          fullText,\n          chunk?.additional_kwargs?.reasoning_content || \"\"\n        )\n        contentToSave = reasoningContent\n        fullText = reasoningContent\n        apiReasoning = true\n      }\n\n      if (apiReasoning && chunk?.content) {\n        fullText += \"</think>\"\n        contentToSave += \"</think>\"\n        apiReasoning = false\n      }\n\n      contentToSave += chunk?.content\n      fullText += chunk?.content\n      if (count === 0) {\n        setIsProcessing(true)\n      }\n      if (isReasoningStarted(fullText) && !reasoningStartTime) {\n        reasoningStartTime = new Date()\n      }\n\n      if (\n        reasoningStartTime &&\n        !reasoningEndTime &&\n        isReasoningEnded(fullText)\n      ) {\n        reasoningEndTime = new Date()\n        const reasoningTime =\n          reasoningEndTime.getTime() - reasoningStartTime.getTime()\n        timetaken = reasoningTime\n      }\n      setMessages((prev) => {\n        return prev.map((message) => {\n          if (message.id === generateMessageId) {\n            return {\n              ...message,\n              message: fullText + \"▋\",\n              reasoning_time_taken: timetaken\n            }\n          }\n          return message\n        })\n      })\n      count++\n    }\n    // update the message with the full text\n    setMessages((prev) => {\n      return prev.map((message) => {\n        if (message.id === generateMessageId) {\n          return {\n            ...message,\n            message: fullText,\n            sources: source,\n            generationInfo,\n            reasoning_time_taken: timetaken\n          }\n        }\n        return message\n      })\n    })\n\n    const imagesToSave = processedImages.length > 0 ? processedImages : (image ? [image] : [])\n\n    setHistory([\n      ...history,\n      {\n        role: \"user\",\n        content: message,\n        image: imagesToSave.length > 0 ? imagesToSave[0] : undefined,\n        images: imagesToSave.length > 0 ? imagesToSave : undefined\n      },\n      {\n        role: \"assistant\",\n        content: fullText\n      }\n    ])\n\n    await saveMessageOnSuccess({\n      historyId,\n      setHistoryId,\n      isRegenerate,\n      selectedModel: selectedModel,\n      message,\n      image: imagesToSave.length > 0 ? imagesToSave[0] : \"\",\n      images: imagesToSave,\n      fullText,\n      source,\n      generationInfo,\n      reasoning_time_taken: timetaken\n    })\n\n    setIsProcessing(false)\n    setStreaming(false)\n  } catch (e) {\n    const imagesToSave = processedImages.length > 0 ? processedImages : (image ? [image] : [])\n\n    const errorSave = await saveMessageOnError({\n      e,\n      botMessage: fullText,\n      history,\n      historyId,\n      image: imagesToSave.length > 0 ? imagesToSave[0] : \"\",\n      images: imagesToSave,\n      selectedModel,\n      setHistory,\n      setHistoryId,\n      userMessage: message,\n      isRegenerating: isRegenerate\n    })\n\n    if (!errorSave) {\n      throw e // Re-throw to be handled by the calling function\n    }\n    setIsProcessing(false)\n    setStreaming(false)\n  } finally {\n    setAbortController(null)\n  }\n}\n"
  },
  {
    "path": "src/hooks/chat-modes/tabChatMode.ts",
    "content": "import { cleanUrl } from \"~/libs/clean-url\"\nimport {\n  getOllamaURL,\n  promptForRag,\n} from \"~/services/ollama\"\nimport { type ChatHistory, type Message } from \"~/store/option\"\nimport { generateID } from \"@/db/dexie/helpers\"\nimport { generateHistory } from \"@/utils/generate-history\"\nimport { pageAssistModel } from \"@/models\"\nimport { humanMessageFormatter } from \"@/utils/human-message\"\nimport {\n  isReasoningEnded,\n  isReasoningStarted,\n  mergeReasoningContent,\n  removeReasoning\n} from \"@/libs/reasoning\"\nimport { getModelNicknameByID } from \"@/db/dexie/nickname\"\nimport { ChatDocuments } from \"@/models/ChatTypes\"\nimport { getTabContents } from \"@/libs/get-tab-contents\"\n\nexport const tabChatMode = async (\n  message: string,\n  image: string,\n  documents: ChatDocuments,\n  isRegenerate: boolean,\n  messages: Message[],\n  history: ChatHistory,\n  signal: AbortSignal,\n  {\n    selectedModel,\n    useOCR,\n    selectedSystemPrompt,\n    currentChatModelSettings,\n    setMessages,\n    saveMessageOnSuccess,\n    saveMessageOnError,\n    setHistory,\n    setIsProcessing,\n    setStreaming,\n    setAbortController,\n    historyId,\n    setHistoryId\n  }: {\n    selectedModel: string\n    useOCR: boolean\n    selectedSystemPrompt: string\n    currentChatModelSettings: any\n    setMessages: (messages: Message[] | ((prev: Message[]) => Message[])) => void\n    saveMessageOnSuccess: (data: any) => Promise<string | null>\n    saveMessageOnError: (data: any) => Promise<string | null>\n    setHistory: (history: ChatHistory) => void\n    setIsProcessing: (value: boolean) => void\n    setStreaming: (value: boolean) => void\n    setAbortController: (controller: AbortController | null) => void\n    historyId: string | null\n    setHistoryId: (id: string) => void\n  }\n) => {\n  console.log(\"Using tabChatMode\")\n  const url = await getOllamaURL()\n\n  const ollama = await pageAssistModel({\n    model: selectedModel!,\n    baseUrl: cleanUrl(url)\n  })\n\n  let newMessage: Message[] = []\n  let generateMessageId = generateID()\n  const modelInfo = await getModelNicknameByID(selectedModel)\n\n  if (!isRegenerate) {\n    newMessage = [\n      ...messages,\n      {\n        isBot: false,\n        name: \"You\",\n        message,\n        sources: [],\n        images: [image],\n        documents\n      },\n      {\n        isBot: true,\n        name: selectedModel,\n        message: \"▋\",\n        sources: [],\n        id: generateMessageId,\n        modelImage: modelInfo?.model_avatar,\n        modelName: modelInfo?.model_name || selectedModel\n      }\n    ]\n  } else {\n    newMessage = [\n      ...messages,\n      {\n        isBot: true,\n        name: selectedModel,\n        message: \"▋\",\n        sources: [],\n        id: generateMessageId,\n        modelImage: modelInfo?.model_avatar,\n        modelName: modelInfo?.model_name || selectedModel\n      }\n    ]\n  }\n  setMessages(newMessage)\n  let fullText = \"\"\n  let contentToSave = \"\"\n\n\n  let timetaken = 0\n  try {\n    let query = message\n    const { ragPrompt: systemPrompt, ragQuestionPrompt: questionPrompt } =\n      await promptForRag()\n    let context = await getTabContents(documents)\n\n    let humanMessage = await humanMessageFormatter({\n      content: [\n        {\n          text: systemPrompt\n            .replace(\"{context}\", context)\n            .replace(\"{question}\", message),\n          type: \"text\"\n        }\n      ],\n      model: selectedModel,\n      useOCR: useOCR\n    })\n    if (image.length > 0) {\n      humanMessage = await humanMessageFormatter({\n        content: [\n          {\n            text: message,\n            type: \"text\"\n          },\n          {\n            image_url: image,\n            type: \"image_url\"\n          }\n        ],\n        model: selectedModel,\n        useOCR: useOCR\n      })\n    }\n    // console.log(context)\n    if (newMessage.length > 2) {\n      const lastTenMessages = newMessage.slice(-10)\n      lastTenMessages.pop()\n      const chat_history = lastTenMessages\n        .map((message) => {\n          return `${message.isBot ? \"Assistant: \" : \"Human: \"}${message.message}`\n        })\n        .join(\"\\n\")\n      const promptForQuestion = questionPrompt\n        .replaceAll(\"{chat_history}\", chat_history)\n        .replaceAll(\"{question}\", message)\n      const questionOllama = await pageAssistModel({\n        model: selectedModel!,\n        baseUrl: cleanUrl(url)\n      })\n      const response = await questionOllama.invoke(promptForQuestion)\n      query = response.content.toString()\n      query = removeReasoning(query)\n    }\n    let source: any[] = []\n\n\n    const applicationChatHistory = generateHistory(history, selectedModel)\n\n    let generationInfo: any | undefined = undefined\n\n    const chunks = await ollama.stream(\n      [...applicationChatHistory, humanMessage],\n      {\n        signal: signal,\n        callbacks: [\n          {\n            handleLLMEnd(output: any): any {\n              try {\n                generationInfo = output?.generations?.[0][0]?.generationInfo\n              } catch (e) {\n                console.error(\"handleLLMEnd error\", e)\n              }\n            }\n          }\n        ]\n      }\n    )\n    let count = 0\n    let reasoningStartTime: Date | undefined = undefined\n    let reasoningEndTime: Date | undefined = undefined\n    let apiReasoning = false\n\n    for await (const chunk of chunks) {\n      if (chunk?.additional_kwargs?.reasoning_content) {\n        const reasoningContent = mergeReasoningContent(\n          fullText,\n          chunk?.additional_kwargs?.reasoning_content || \"\"\n        )\n        contentToSave = reasoningContent\n        fullText = reasoningContent\n        apiReasoning = true\n      }\n\n      if (apiReasoning && chunk?.content) {\n        fullText += \"</think>\"\n        contentToSave += \"</think>\"\n        apiReasoning = false\n      }\n\n      contentToSave += chunk?.content\n      fullText += chunk?.content\n      if (count === 0) {\n        setIsProcessing(true)\n      }\n      if (isReasoningStarted(fullText) && !reasoningStartTime) {\n        reasoningStartTime = new Date()\n      }\n\n      if (\n        reasoningStartTime &&\n        !reasoningEndTime &&\n        isReasoningEnded(fullText)\n      ) {\n        reasoningEndTime = new Date()\n        const reasoningTime =\n          reasoningEndTime.getTime() - reasoningStartTime.getTime()\n        timetaken = reasoningTime\n      }\n      setMessages((prev) => {\n        return prev.map((message) => {\n          if (message.id === generateMessageId) {\n            return {\n              ...message,\n              message: fullText + \"▋\",\n              reasoning_time_taken: timetaken\n            }\n          }\n          return message\n        })\n      })\n      count++\n    }\n    // update the message with the full text\n    setMessages((prev) => {\n      return prev.map((message) => {\n        if (message.id === generateMessageId) {\n          return {\n            ...message,\n            message: fullText,\n            sources: source,\n            generationInfo,\n            reasoning_time_taken: timetaken\n          }\n        }\n        return message\n      })\n    })\n\n    setHistory([\n      ...history,\n      {\n        role: \"user\",\n        content: message,\n        image\n      },\n      {\n        role: \"assistant\",\n        content: fullText\n      }\n    ])\n\n    await saveMessageOnSuccess({\n      historyId,\n      setHistoryId,\n      isRegenerate,\n      selectedModel: selectedModel,\n      message,\n      image,\n      fullText,\n      source,\n      generationInfo,\n      reasoning_time_taken: timetaken,\n      documents,\n    })\n\n    setIsProcessing(false)\n    setStreaming(false)\n  } catch (e) {\n    console.log(e)\n    const errorSave = await saveMessageOnError({\n      e,\n      botMessage: fullText,\n      history,\n      historyId,\n      image,\n      selectedModel,\n      setHistory,\n      setHistoryId,\n      userMessage: message,\n      isRegenerating: isRegenerate,\n      documents,\n    })\n\n    if (!errorSave) {\n      throw e // Re-throw to be handled by the calling function\n    }\n    setIsProcessing(false)\n    setStreaming(false)\n  } finally {\n    setAbortController(null)\n  }\n}\n"
  },
  {
    "path": "src/hooks/handlers/messageHandlers.ts",
    "content": "import { type ChatHistory, type Message } from \"~/store/option\"\nimport {\n  deleteChatForEdit,\n  formatToChatHistory,\n  formatToMessage,\n  updateMessageByIndex\n} from \"@/db/dexie/helpers\"\nimport { validateBeforeSubmit } from \"../utils/messageHelpers\"\nimport { generateBranchMessage } from \"@/db/dexie/branch\"\nimport { getPromptById, getSessionFiles, UploadedFile } from \"@/db\"\n\nexport const createRegenerateLastMessage = ({\n  validateBeforeSubmitFn,\n  history,\n  messages,\n  setHistory,\n  setMessages,\n  historyId,\n  removeMessageUsingHistoryIdFn,\n  onSubmit\n}: {\n  validateBeforeSubmitFn: () => boolean\n  history: ChatHistory | (() => ChatHistory)\n  messages: Message[] | (() => Message[])\n  setHistory: (history: ChatHistory) => void\n  setMessages: (messages: Message[]) => void\n  historyId: string | null | (() => string | null)\n  removeMessageUsingHistoryIdFn: (id: string | null) => Promise<void>\n  onSubmit: (params: any) => Promise<void>\n}) => {\n  return async () => {\n    const currentHistory =\n      typeof history === \"function\" ? history() : history\n    const currentMessages =\n      typeof messages === \"function\" ? messages() : messages\n    const currentHistoryId =\n      typeof historyId === \"function\" ? historyId() : historyId\n    const isOk = validateBeforeSubmitFn()\n\n    if (!isOk) {\n      return\n    }\n    if (currentHistory.length > 0) {\n      const lastUserIndex = currentHistory.findLastIndex(\n        (message) => message.role === \"user\"\n      )\n\n      if (lastUserIndex === -1) {\n        return\n      }\n\n      const lastMessage = currentHistory[lastUserIndex]\n      const newHistory = currentHistory.slice(0, lastUserIndex)\n      const newMessages = currentMessages.slice(0, lastUserIndex + 1)\n\n      setHistory(newHistory)\n      setMessages(newMessages)\n      await removeMessageUsingHistoryIdFn(currentHistoryId)\n\n      if (lastMessage.role === \"user\") {\n        const newController = new AbortController()\n        await onSubmit({\n          message: lastMessage.content,\n          image: lastMessage.image || \"\",\n          images: lastMessage.images || [],\n          isRegenerate: true,\n          messages: newMessages,\n          memory: newHistory,\n          controller: newController\n        })\n      }\n    }\n  }\n}\n\nexport const createEditMessage = ({\n  messages,\n  history,\n  setMessages,\n  setHistory,\n  historyId,\n  validateBeforeSubmitFn,\n  onSubmit\n}: {\n  messages: Message[] | (() => Message[])\n  history: ChatHistory | (() => ChatHistory)\n  setMessages: (messages: Message[]) => void\n  setHistory: (history: ChatHistory) => void\n  historyId: string | null | (() => string | null)\n  validateBeforeSubmitFn: () => boolean\n  onSubmit: (params: any) => Promise<void>\n}) => {\n  return async (\n    index: number,\n    message: string,\n    isHuman: boolean,\n    isSend: boolean\n  ) => {\n    const currentMessages =\n      typeof messages === \"function\" ? messages() : messages\n    const currentHistory =\n      typeof history === \"function\" ? history() : history\n    const currentHistoryId =\n      typeof historyId === \"function\" ? historyId() : historyId\n    const newMessages = currentMessages.map((currentMessage, currentIndex) =>\n      currentIndex === index\n        ? {\n            ...currentMessage,\n            message\n          }\n        : currentMessage\n    )\n    const newHistory = currentHistory.map((currentMessage, currentIndex) =>\n      currentIndex === index\n        ? {\n            ...currentMessage,\n            content: message\n          }\n        : currentMessage\n    )\n\n    // if human message and send then only trigger the submit\n    if (isHuman && isSend) {\n      const isOk = validateBeforeSubmitFn()\n\n      if (!isOk) {\n        return\n      }\n\n      const currentHumanMessage = newMessages[index]\n      const previousMessages = newMessages.slice(0, index + 1)\n      setMessages(previousMessages)\n      const previousHistory = newHistory.slice(0, index)\n      setHistory(previousHistory)\n      await updateMessageByIndex(currentHistoryId, index, message)\n      await deleteChatForEdit(currentHistoryId, index)\n      const abortController = new AbortController()\n      await onSubmit({\n        message: message,\n        image: currentHumanMessage.images[0] || \"\",\n        isRegenerate: true,\n        messages: previousMessages,\n        memory: previousHistory,\n        controller: abortController,\n        images: currentHumanMessage.images || []\n      })\n      return\n    }\n\n    setMessages(newMessages)\n    setHistory(newHistory)\n    await updateMessageByIndex(currentHistoryId, index, message)\n  }\n}\n\nexport const createBranchMessage = ({\n  setMessages,\n  setHistory,\n  historyId,\n  getHistoryId,\n  setHistoryId,\n  setContext,\n  setSelectedSystemPrompt,\n  setSystemPrompt\n}: {\n  setMessages: (messages: Message[]) => void\n  setHistory: (history: ChatHistory) => void\n  historyId: string | null\n  getHistoryId?: () => string | null\n  setHistoryId: (id: string | null) => void\n  setSelectedSystemPrompt?: (prompt: string) => void\n  setSystemPrompt?: (prompt: string) => void\n  setContext?: (context: UploadedFile[]) => void\n}) => {\n  return async (index: number) => {\n    try {\n      const activeHistoryId = getHistoryId?.() ?? historyId\n      const newBranch = await generateBranchMessage(activeHistoryId, index)\n      setHistory(formatToChatHistory(newBranch.messages))\n      setMessages(formatToMessage(newBranch.messages))\n      setHistoryId(newBranch.history.id)\n      const systemFiles = await getSessionFiles(newBranch.history.id)\n      if (setContext) {\n        setContext(systemFiles)\n      }\n\n      const lastUsedPrompt = newBranch?.history?.last_used_prompt\n      if (lastUsedPrompt) {\n        if (lastUsedPrompt.prompt_id) {\n          const prompt = await getPromptById(lastUsedPrompt.prompt_id)\n          if (prompt) {\n            setSelectedSystemPrompt(lastUsedPrompt.prompt_id)\n          }\n        }\n        setSystemPrompt(lastUsedPrompt.prompt_content)\n      }\n    } catch (e) {\n      console.log(`[branch] ${e}`)\n    }\n  }\n}\n\nexport const createStopStreamingRequest = (\n  abortController: AbortController | null,\n  setAbortController: (controller: AbortController | null) => void\n) => {\n  return () => {\n    if (abortController) {\n      abortController.abort()\n      setAbortController(null)\n    }\n  }\n}\n"
  },
  {
    "path": "src/hooks/keyboard/index.ts",
    "content": "export {\n  useKeyboardShortcuts,\n  useFocusShortcuts,\n  type KeyboardShortcut,\n  type KeyboardShortcutConfig\n} from './useKeyboardShortcuts'\n\nexport {\n  useShortcutConfig,\n  formatShortcut,\n  defaultShortcuts,\n  type ShortcutConfig\n} from './useShortcutConfig'\n"
  },
  {
    "path": "src/hooks/keyboard/useKeyboardShortcuts.ts",
    "content": "import { useEffect, useCallback } from 'react'\nimport { useShortcutConfig } from './useShortcutConfig'\n\nexport interface KeyboardShortcut {\n  key: string\n  ctrlKey?: boolean\n  altKey?: boolean\n  shiftKey?: boolean\n  metaKey?: boolean\n  preventDefault?: boolean\n  stopPropagation?: boolean\n}\n\nexport interface KeyboardShortcutConfig {\n  shortcut: KeyboardShortcut\n  action: () => void\n  enabled?: boolean\n  description?: string\n}\n\n/**\n * Hook for managing configurable keyboard shortcuts\n * @param shortcuts Array of keyboard shortcut configurations\n * @param target Target element to attach listeners to (defaults to document)\n */\nexport const useKeyboardShortcuts = (\n  shortcuts: KeyboardShortcutConfig[],\n  target: Document | HTMLElement | null = document\n) => {\n  const handleKeyDown = useCallback(\n    (event: KeyboardEvent) => {\n      shortcuts.forEach(({ shortcut, action, enabled = true }) => {\n        if (!enabled) return\n\n        const {\n          key,\n          ctrlKey = false,\n          altKey = false,\n          shiftKey = false,\n          metaKey = false,\n          preventDefault = true,\n          stopPropagation = true\n        } = shortcut\n\n        // Check if the key combination matches\n        const keyMatches = event.key.toLowerCase() === key.toLowerCase()\n        const ctrlMatches = event.ctrlKey === ctrlKey\n        const altMatches = event.altKey === altKey\n        const shiftMatches = event.shiftKey === shiftKey\n        const metaMatches = event.metaKey === metaKey\n\n        if (keyMatches && ctrlMatches && altMatches && shiftMatches && metaMatches) {\n          if (preventDefault) {\n            event.preventDefault()\n          }\n          if (stopPropagation) {\n            event.stopPropagation()\n          }\n          action()\n        }\n      })\n    },\n    [shortcuts]\n  )\n\n  useEffect(() => {\n    if (!target) return\n\n    target.addEventListener('keydown', handleKeyDown)\n\n    return () => {\n      target.removeEventListener('keydown', handleKeyDown)\n    }\n  }, [target, handleKeyDown])\n}\n\n/**\n * Hook specifically for focus shortcuts in forms\n * @param textareaRef Reference to the textarea element to focus\n * @param enabled Whether the shortcuts are enabled\n */\nexport const useFocusShortcuts = (\n  textareaRef: React.RefObject<HTMLTextAreaElement>,\n  enabled: boolean = true\n) => {\n  const { shortcuts: configuredShortcuts } = useShortcutConfig()\n\n  const focusTextarea = useCallback(() => {\n    if (textareaRef.current) {\n      textareaRef.current.focus()\n      // Place cursor at the end of the text\n      const textLength = textareaRef.current.value.length\n      textareaRef.current.setSelectionRange(textLength, textLength)\n    }\n  }, [textareaRef])\n\n  const shortcuts: KeyboardShortcutConfig[] = [\n    {\n      shortcut: configuredShortcuts.focusTextarea,\n      action: focusTextarea,\n      enabled,\n      description: 'Focus textarea'\n    }\n  ]\n\n  useKeyboardShortcuts(shortcuts)\n\n  return {\n    focusTextarea,\n    shortcuts\n  }\n}\n\n/**\n * Hook specifically for chat shortcuts\n * @param clearChat Function to clear/start new chat\n * @param enabled Whether the shortcuts are enabled\n */\nexport const useChatShortcuts = (\n  clearChat: () => void,\n  enabled: boolean = true\n) => {\n  const { shortcuts: configuredShortcuts } = useShortcutConfig()\n\n  const newChat = useCallback(() => {\n    clearChat()\n  }, [clearChat])\n\n  const shortcuts: KeyboardShortcutConfig[] = [\n    {\n      shortcut: configuredShortcuts.newChat,\n      action: newChat,\n      enabled,\n      description: 'Start new chat'\n    }\n  ]\n\n  useKeyboardShortcuts(shortcuts)\n\n  return {\n    newChat,\n    shortcuts\n  }\n}\n\n/**\n * Hook specifically for sidebar shortcuts\n * @param toggleSidebar Function to toggle sidebar\n * @param enabled Whether the shortcuts are enabled\n */\nexport const useSidebarShortcuts = (\n  toggleSidebar: () => void,\n  enabled: boolean = true\n) => {\n  const { shortcuts: configuredShortcuts } = useShortcutConfig()\n\n  const toggleSidebarAction = useCallback(() => {\n    toggleSidebar()\n  }, [toggleSidebar])\n\n  const shortcuts: KeyboardShortcutConfig[] = [\n    {\n      shortcut: configuredShortcuts.toggleSidebar,\n      action: toggleSidebarAction,\n      enabled,\n      description: 'Toggle sidebar'\n    }\n  ]\n\n  useKeyboardShortcuts(shortcuts)\n\n  return {\n    toggleSidebar: toggleSidebarAction,\n    shortcuts\n  }\n}\n\n/**\n * Hook specifically for chat mode shortcuts\n * @param toggleChatMode Function to toggle chat mode between normal and rag\n * @param enabled Whether the shortcuts are enabled\n */\nexport const useChatModeShortcuts = (\n  toggleChatMode: () => void,\n  enabled: boolean = true\n) => {\n  const { shortcuts: configuredShortcuts } = useShortcutConfig()\n\n  const toggleChatModeAction = useCallback(() => {\n    toggleChatMode()\n  }, [toggleChatMode])\n\n  const shortcuts: KeyboardShortcutConfig[] = [\n    {\n      shortcut: configuredShortcuts.toggleChatMode,\n      action: toggleChatModeAction,\n      enabled,\n      description: 'Toggle chat with current page'\n    }\n  ]\n\n  useKeyboardShortcuts(shortcuts)\n\n  return {\n    toggleChatMode: toggleChatModeAction,\n    shortcuts\n  }\n}\n"
  },
  {
    "path": "src/hooks/keyboard/useShortcutConfig.ts",
    "content": "import { useStorage } from \"@plasmohq/storage/hook\"\nimport { KeyboardShortcut } from \"./useKeyboardShortcuts\"\n\nexport interface ShortcutConfig {\n  focusTextarea: KeyboardShortcut\n  newChat: KeyboardShortcut\n  toggleSidebar: KeyboardShortcut\n  toggleChatMode: KeyboardShortcut\n}\n\nexport const defaultShortcuts: ShortcutConfig = {\n  focusTextarea: {\n    key: 'Escape',\n    shiftKey: true,\n    preventDefault: true,\n    stopPropagation: true\n  },\n  newChat: {\n    key: 'o',\n    ctrlKey: true,\n    shiftKey: true,\n    preventDefault: true,\n    stopPropagation: true\n  },\n  toggleSidebar: {\n    key: 'b',\n    ctrlKey: true,\n    preventDefault: true,\n    stopPropagation: true\n  },\n  toggleChatMode: {\n    key: 'e',\n    ctrlKey: true,\n    preventDefault: true,\n    stopPropagation: true\n  }\n}\n\n/**\n * Hook for managing keyboard shortcut configurations\n * Allows users to customize their keyboard shortcuts\n */\nexport const useShortcutConfig = () => {\n  const [shortcuts, setShortcuts] = useStorage<ShortcutConfig>(\n    \"keyboardShortcuts\",\n    defaultShortcuts\n  )\n\n  const updateShortcut = (\n    shortcutName: keyof ShortcutConfig,\n    newShortcut: KeyboardShortcut\n  ) => {\n    setShortcuts(prev => ({\n      ...prev,\n      [shortcutName]: newShortcut\n    }))\n  }\n\n  const resetShortcuts = () => {\n    setShortcuts(defaultShortcuts)\n  }\n\n  const resetShortcut = (shortcutName: keyof ShortcutConfig) => {\n    setShortcuts(prev => ({\n      ...prev,\n      [shortcutName]: defaultShortcuts[shortcutName]\n    }))\n  }\n\n  return {\n    shortcuts,\n    updateShortcut,\n    resetShortcuts,\n    resetShortcut\n  }\n}\n\n/**\n * Utility function to format shortcut for display\n */\nexport const formatShortcut = (shortcut: KeyboardShortcut): string => {\n  const parts: string[] = []\n  \n  if (shortcut.ctrlKey) parts.push('Ctrl')\n  if (shortcut.altKey) parts.push('Alt')\n  if (shortcut.shiftKey) parts.push('Shift')\n  if (shortcut.metaKey) parts.push('⌘')\n  \n  parts.push(shortcut.key)\n  \n  return parts.join(' + ')\n}\n"
  },
  {
    "path": "src/hooks/useBackgroundMessage.tsx",
    "content": "import { useState, useEffect } from \"react\"\n\ninterface Message {\n  from: string\n  type: string\n  text: string\n}\n\nfunction useBackgroundMessage() {\n  const [message, setMessage] = useState<Message | null>(null)\n\n  useEffect(() => {\n    const messageListener = (request: Message) => {\n      if (request.from === \"background\") {\n        setMessage(request)\n      }\n    }\n    browser.runtime.connect({ name: 'pgCopilot' })\n    browser.runtime.onMessage.addListener(messageListener)\n\n    return () => {\n      browser.runtime.onMessage.removeListener(messageListener)\n    }\n  }, [])\n\n  return message\n}\n\nexport default useBackgroundMessage"
  },
  {
    "path": "src/hooks/useDarkmode.tsx",
    "content": "import React from \"react\";\nimport { create } from \"zustand\";\n\ntype DarkModeState = {\n  mode: \"system\" | \"dark\" | \"light\";\n  setMode: (mode: \"system\" | \"dark\" | \"light\") => void;\n};\n\nexport const useDarkModeStore = create<DarkModeState>((set) => ({\n  mode: \"system\",\n  setMode: (mode) => set({ mode }),\n}));\n\nexport const useDarkMode = () => {\n  const { mode, setMode } = useDarkModeStore();\n\n  const getSystemTheme = () => {\n    const darkModeMediaQuery = window.matchMedia(\n      \"(prefers-color-scheme: dark)\"\n    );\n    const isDarkMode = darkModeMediaQuery.matches;\n    return isDarkMode ? \"dark\" : \"light\";\n  };\n\n  const handleDarkModeChange = (e: MediaQueryListEvent) => {\n    document.documentElement.classList.remove(\"dark\", \"light\");\n    const mode = e.matches ? \"dark\" : \"light\";\n    document.documentElement.classList.add(mode);\n    setMode(mode);\n  };\n\n  React.useEffect(() => {\n    const theme = localStorage.getItem(\"theme\") as \"system\" | \"dark\" | \"light\";\n    if (theme) {\n      if (theme !== \"system\") {\n        document.documentElement.classList.add(theme);\n        setMode(theme);\n      } else {\n        const systemTheme = getSystemTheme();\n        document.documentElement.classList.add(systemTheme);\n        setMode(systemTheme);\n      }\n    } else {\n      setMode(getSystemTheme());\n      localStorage.setItem(\"theme\", getSystemTheme());\n    }\n  }, []);\n\n  React.useEffect(() => {\n    const darkModeMediaQuery = window.matchMedia(\n      \"(prefers-color-scheme: dark)\"\n    );\n    darkModeMediaQuery.addEventListener(\"change\", handleDarkModeChange);\n    return () =>\n      darkModeMediaQuery.removeEventListener(\"change\", handleDarkModeChange);\n  }, []);\n\n  const toggleDarkMode = () => {\n    const newMode = mode === \"dark\" ? \"light\" : \"dark\";\n    document.documentElement.classList.remove(\"dark\", \"light\");\n    document.documentElement.classList.add(newMode);\n    setMode(newMode);\n    localStorage.setItem(\"theme\", newMode);\n  };\n\n  return { mode, toggleDarkMode };\n};"
  },
  {
    "path": "src/hooks/useDebounce.tsx",
    "content": "import { useEffect, useState } from \"react\"\n\nexport const useDebounce = (value: string, delay: number) => {\n  const [debouncedValue, setDebouncedValue] = useState(value)\n\n  useEffect(() => {\n    const handler = setTimeout(() => {\n      setDebouncedValue(value)\n    }, delay)\n\n    return () => {\n      clearTimeout(handler)\n    }\n  }, [value, delay])\n\n  return debouncedValue\n}\n"
  },
  {
    "path": "src/hooks/useDynamicTextareaSize.tsx",
    "content": "// copied from https://gist.github.com/KristofferEriksson/87ea5b8195339577151a236a9e9b46ff\n/**\n * Custom hook for dynamically resizing a textarea to fit its content.\n * @param {React.RefObject<HTMLTextAreaElement>} textareaRef - Reference to the textarea element.\n * @param {string} textContent - Current text content of the textarea.\n * @param {number} maxHeight - Optional: maxHeight of the textarea in pixels.\n */\n\nimport { useEffect } from \"react\";\nconst useDynamicTextareaSize = (\n  textareaRef: React.RefObject<HTMLTextAreaElement>,\n  textContent: string,\n  // optional maximum height after which textarea becomes scrollable\n  maxHeight?: number\n): void => {\n  useEffect(() => {\n    const currentTextarea = textareaRef.current;\n    if (currentTextarea) {\n      // Temporarily collapse the textarea to calculate the required height\n      currentTextarea.style.height = \"0px\";\n      const contentHeight = currentTextarea.scrollHeight;\n\n      if (maxHeight) {\n        \n        // Set max-height and adjust overflow behavior if maxHeight is provided\n        currentTextarea.style.maxHeight = `${maxHeight}px`;\n        currentTextarea.style.overflowY = contentHeight > maxHeight ? \"scroll\" : \"hidden\";\n        currentTextarea.style.height = `${Math.min(contentHeight, maxHeight)}px`;\n      } else {\n        \n        // Adjust height without max height constraint\n        currentTextarea.style.height = `${contentHeight}px`;\n      }\n    }\n  }, [textareaRef, textContent, maxHeight]);\n};\n\nexport default useDynamicTextareaSize;"
  },
  {
    "path": "src/hooks/useI18n.tsx",
    "content": "import { supportLanguage } from \"@/i18n/support-language\"\nimport { useState } from \"react\"\nimport { useTranslation } from \"react-i18next\"\n\nexport const useI18n = () => {\n  const { i18n } = useTranslation()\n  const [locale, setLocale] = useState<string>(\n    localStorage.getItem(\"i18nextLng\") || \"en\"\n  )\n\n  const changeLocale = (lang: string) => {\n    setLocale(lang)\n    i18n.changeLanguage(lang)\n    localStorage.setItem(\"i18nextLng\", lang)\n  }\n\n  return { locale, changeLocale, supportLanguage }\n}\n"
  },
  {
    "path": "src/hooks/useLocal.tsx",
    "content": "import React from \"react\"\n\nexport default function useLocal(key: string) {\n  const [value, setValue] = React.useState<string | null>(null)\n\n  React.useEffect(() => {\n    chrome.storage.local.get(key, (result) => {\n      setValue(result[key])\n    })\n  }, [key])\n\n  const update = (newValue: string) => {\n    chrome.storage.local.set({ [key]: newValue }, () => {\n      setValue(newValue)\n    })\n  }\n\n  const remove = () => {\n    chrome.storage.local.remove(key)\n    setValue(null)\n  }\n\n  return { value, update, remove }\n}\n\nexport function useChatWidget() {\n  const { value, update } = useLocal(\"chat-widget\")\n  const [active, setActive] = React.useState<boolean>(value === \"show\")\n\n  const setActiveValue = (newValue: boolean) => {\n    if (newValue) {\n      update(\"show\")\n    } else {\n      update(\"hide\")\n    }\n    setActive(newValue)\n  }\n\n  return { active, setActiveValue }\n}\n"
  },
  {
    "path": "src/hooks/useMessage.tsx",
    "content": "import React from \"react\"\nimport { cleanUrl } from \"~/libs/clean-url\"\nimport {\n  defaultEmbeddingModelForRag,\n  geWebSearchFollowUpPrompt,\n  getOllamaURL,\n  promptForRag,\n  systemPromptForNonRag\n} from \"~/services/ollama\"\nimport { useStoreMessageOption, type Message } from \"~/store/option\"\nimport { useStoreMessage } from \"~/store\"\nimport { getContentFromCurrentTab } from \"~/libs/get-html\"\nimport { memoryEmbedding } from \"@/utils/memory-embeddings\"\nimport { ChatHistory } from \"@/store/option\"\nimport {\n  deleteChatForEdit,\n  generateID,\n  getPromptById,\n  removeMessageUsingHistoryId,\n  updateMessageByIndex\n} from \"@/db/dexie/helpers\"\nimport { notification } from \"antd\"\nimport { useTranslation } from \"react-i18next\"\nimport { usePageAssist } from \"@/context\"\nimport { formatDocs } from \"@/chain/chat-with-x\"\nimport { useStorage } from \"@plasmohq/storage/hook\"\nimport { useStoreChatModelSettings } from \"@/store/model\"\nimport { getAllDefaultModelSettings } from \"@/services/model-settings\"\nimport { getSystemPromptForWeb, isQueryHaveWebsite } from \"@/web/web\"\nimport { pageAssistModel } from \"@/models\"\nimport { getPrompt } from \"@/services/application\"\nimport { humanMessageFormatter } from \"@/utils/human-message\"\nimport { pageAssistEmbeddingModel } from \"@/models/embedding\"\nimport { PAMemoryVectorStore } from \"@/libs/PAMemoryVectorStore\"\nimport { getScreenshotFromCurrentTab } from \"@/libs/get-screenshot\"\nimport {\n  isReasoningEnded,\n  isReasoningStarted,\n  mergeReasoningContent,\n  removeReasoning\n} from \"@/libs/reasoning\"\nimport { getModelNicknameByID } from \"@/db/dexie/nickname\"\nimport { systemPromptFormatter } from \"@/utils/system-message\"\nimport {\n  createBranchMessage,\n  createRegenerateLastMessage\n} from \"./handlers/messageHandlers\"\nimport {\n  createSaveMessageOnError,\n  createSaveMessageOnSuccess\n} from \"./utils/messageHelpers\"\nimport { updatePageTitle } from \"@/utils/update-page-title\"\nimport { getNoOfRetrievedDocs } from \"@/services/app\"\nimport { normalChatMode as sharedNormalChatMode } from \"./chat-modes/normalChatMode\"\n\nexport const useMessage = () => {\n  const {\n    controller: abortController,\n    setController: setAbortController,\n    messages,\n    setMessages,\n    embeddingController,\n    setEmbeddingController\n  } = usePageAssist()\n  const { t } = useTranslation(\"option\")\n  const [selectedModel, setSelectedModel] = useStorage(\"selectedModel\")\n  const currentChatModelSettings = useStoreChatModelSettings()\n  const {\n    setIsSearchingInternet,\n    webSearch,\n    setWebSearch,\n    isSearchingInternet,\n    temporaryChat,\n    setTemporaryChat,\n    actionInfo,\n    setActionInfo\n  } = useStoreMessageOption()\n  const [defaultInternetSearchOn] = useStorage(\"defaultInternetSearchOn\", false)\n\n  const [defaultChatWithWebsite] = useStorage(\"defaultChatWithWebsite\", false)\n\n  const [chatWithWebsiteEmbedding] = useStorage(\n    \"chatWithWebsiteEmbedding\",\n    false\n  )\n  const [maxWebsiteContext] = useStorage(\"maxWebsiteContext\", 4028)\n\n  const {\n    history,\n    setHistory,\n    setStreaming,\n    streaming,\n    setIsFirstMessage,\n    historyId,\n    setHistoryId,\n    isLoading,\n    setIsLoading,\n    isProcessing,\n    setIsProcessing,\n    chatMode,\n    setChatMode,\n    setIsEmbedding,\n    isEmbedding,\n    currentURL,\n    setCurrentURL,\n    selectedQuickPrompt,\n    setSelectedQuickPrompt,\n    selectedSystemPrompt,\n    setSelectedSystemPrompt,\n    useOCR,\n    setUseOCR\n  } = useStoreMessage()\n  const [sidepanelTemporaryChat] = useStorage(\"sidepanelTemporaryChat\", false)\n  const [speechToTextLanguage, setSpeechToTextLanguage] = useStorage(\n    \"speechToTextLanguage\",\n    \"en-US\"\n  )\n\n  const [keepTrackOfEmbedding, setKeepTrackOfEmbedding] = React.useState<{\n    [key: string]: PAMemoryVectorStore\n  }>({})\n\n  const clearChat = () => {\n    stopStreamingRequest()\n    setMessages([])\n    setHistory([])\n    setHistoryId(null)\n    setIsFirstMessage(true)\n    setIsLoading(false)\n    setIsProcessing(false)\n    setStreaming(false)\n    updatePageTitle()\n    currentChatModelSettings.reset()\n    if (defaultInternetSearchOn) {\n      setWebSearch(true)\n    }\n    if (defaultChatWithWebsite) {\n      setChatMode(\"rag\")\n    }\n    if (sidepanelTemporaryChat) {\n      setTemporaryChat(true)\n    }\n    setActionInfo(null)\n  }\n\n  const saveMessageOnSuccess = createSaveMessageOnSuccess(\n    temporaryChat,\n    setHistoryId as (id: string) => void\n  )\n  const saveMessageOnError = createSaveMessageOnError(\n    temporaryChat,\n    history,\n    setHistory,\n    setHistoryId as (id: string) => void\n  )\n\n  const messagesRef = React.useRef(messages)\n  const historyRef = React.useRef(history)\n  const historyIdRef = React.useRef(historyId)\n  const onSubmitRef = React.useRef<(params: any) => Promise<void>>(\n    async () => {}\n  )\n\n  React.useEffect(() => {\n    messagesRef.current = messages\n  }, [messages])\n\n  React.useEffect(() => {\n    historyRef.current = history\n  }, [history])\n\n  React.useEffect(() => {\n    historyIdRef.current = historyId\n  }, [historyId])\n\n  const chatWithWebsiteMode = async (\n    message: string,\n    image: string,\n    isRegenerate: boolean,\n    messages: Message[],\n    history: ChatHistory,\n    signal: AbortSignal,\n    embeddingSignal: AbortSignal\n  ) => {\n    setStreaming(true)\n    const url = await getOllamaURL()\n    const userDefaultModelSettings = await getAllDefaultModelSettings()\n\n    const ollama = await pageAssistModel({\n      model: selectedModel!,\n      baseUrl: cleanUrl(url)\n    })\n\n    let newMessage: Message[] = []\n    let generateMessageId = generateID()\n    const modelInfo = await getModelNicknameByID(selectedModel)\n\n    if (!isRegenerate) {\n      newMessage = [\n        ...messages,\n        {\n          isBot: false,\n          name: \"You\",\n          message,\n          sources: [],\n          images: []\n        },\n        {\n          isBot: true,\n          name: selectedModel,\n          message: \"▋\",\n          sources: [],\n          id: generateMessageId,\n          modelImage: modelInfo?.model_avatar,\n          modelName: modelInfo?.model_name || selectedModel\n        }\n      ]\n    } else {\n      newMessage = [\n        ...messages,\n        {\n          isBot: true,\n          name: selectedModel,\n          message: \"▋\",\n          sources: [],\n          id: generateMessageId,\n          modelImage: modelInfo?.model_avatar,\n          modelName: modelInfo?.model_name || selectedModel\n        }\n      ]\n    }\n\n    setMessages(newMessage)\n    let fullText = \"\"\n    let contentToSave = \"\"\n    let embedURL: string, embedHTML: string, embedType: string\n    let embedPDF: { content: string; page: number }[] = []\n\n    let isAlreadyExistEmbedding: PAMemoryVectorStore\n    const {\n      content: html,\n      url: websiteUrl,\n      type,\n      pdf\n    } = await getContentFromCurrentTab(chatWithWebsiteEmbedding)\n\n    embedHTML = html\n    embedURL = websiteUrl\n    embedType = type\n    embedPDF = pdf\n    if (messages.length === 0) {\n      setCurrentURL(websiteUrl)\n      isAlreadyExistEmbedding = keepTrackOfEmbedding[currentURL]\n    } else {\n      if (currentURL !== websiteUrl) {\n        setCurrentURL(websiteUrl)\n      } else {\n        embedURL = currentURL\n      }\n      isAlreadyExistEmbedding = keepTrackOfEmbedding[websiteUrl]\n    }\n    setMessages(newMessage)\n    const ollamaUrl = await getOllamaURL()\n    const embeddingModle = await defaultEmbeddingModelForRag()\n\n    const ollamaEmbedding = await pageAssistEmbeddingModel({\n      model: embeddingModle || selectedModel,\n      baseUrl: cleanUrl(ollamaUrl),\n      signal: embeddingSignal,\n      keepAlive:\n        currentChatModelSettings?.keepAlive ??\n        userDefaultModelSettings?.keepAlive\n    })\n    let vectorstore: PAMemoryVectorStore\n\n    try {\n      if (isAlreadyExistEmbedding) {\n        vectorstore = isAlreadyExistEmbedding\n      } else {\n        if (chatWithWebsiteEmbedding) {\n          vectorstore = await memoryEmbedding({\n            html: embedHTML,\n            keepTrackOfEmbedding: keepTrackOfEmbedding,\n            ollamaEmbedding: ollamaEmbedding,\n            pdf: embedPDF,\n            setIsEmbedding: setIsEmbedding,\n            setKeepTrackOfEmbedding: setKeepTrackOfEmbedding,\n            type: embedType,\n            url: embedURL\n          })\n        }\n      }\n      let query = message\n      const { ragPrompt: systemPrompt, ragQuestionPrompt: questionPrompt } =\n        await promptForRag()\n      if (newMessage.length > 2) {\n        const lastTenMessages = newMessage.slice(-10)\n        lastTenMessages.pop()\n        const chat_history = lastTenMessages\n          .map((message) => {\n            return `${message.isBot ? \"Assistant: \" : \"Human: \"}${message.message}`\n          })\n          .join(\"\\n\")\n        const promptForQuestion = questionPrompt\n          .replaceAll(\"{chat_history}\", chat_history)\n          .replaceAll(\"{question}\", message)\n        const questionOllama = await pageAssistModel({\n          model: selectedModel!,\n          baseUrl: cleanUrl(url)\n        })\n        const response = await questionOllama.invoke(promptForQuestion)\n        query = response.content.toString()\n        query = removeReasoning(query)\n      }\n\n      let context: string = \"\"\n      let source: {\n        name: any\n        type: any\n        mode: string\n        url: string\n        pageContent: string\n        metadata: Record<string, any>\n      }[] = []\n\n      if (chatWithWebsiteEmbedding) {\n        const docSize = await getNoOfRetrievedDocs()\n\n        const docs = await vectorstore.similaritySearch(query, docSize)\n        context = formatDocs(docs)\n        source = docs.map((doc) => {\n          return {\n            ...doc,\n            name: doc?.metadata?.source || \"untitled\",\n            type: doc?.metadata?.type || \"unknown\",\n            mode: \"chat\",\n            url: \"\"\n          }\n        })\n      } else {\n        if (type === \"html\") {\n          context = embedHTML.slice(0, maxWebsiteContext)\n        } else {\n          context = embedPDF\n            .map((pdf) => pdf.content)\n            .join(\" \")\n            .slice(0, maxWebsiteContext)\n        }\n\n        source = [\n          {\n            name: embedURL,\n            type: type,\n            mode: \"chat\",\n            url: embedURL,\n            pageContent: context,\n            metadata: {\n              source: embedURL,\n              url: embedURL\n            }\n          }\n        ]\n      }\n\n      let humanMessage = await humanMessageFormatter({\n        content: [\n          {\n            text: systemPrompt\n              .replace(\"{context}\", context)\n              .replace(\"{question}\", query),\n            type: \"text\"\n          }\n        ],\n        model: selectedModel,\n        useOCR\n      })\n\n      const applicationChatHistory = generateHistory(history, selectedModel)\n\n      let generationInfo: any | undefined = undefined\n\n      const chunks = await ollama.stream(\n        [...applicationChatHistory, humanMessage],\n        {\n          signal: signal,\n          callbacks: [\n            {\n              handleLLMEnd(output: any): any {\n                try {\n                  generationInfo = output?.generations?.[0][0]?.generationInfo\n                } catch (e) {\n                  console.error(\"handleLLMEnd error\", e)\n                }\n              }\n            }\n          ]\n        }\n      )\n      let count = 0\n      let reasoningStartTime: Date | null = null\n      let reasoningEndTime: Date | null = null\n      let timetaken = 0\n      let apiReasoning = false\n      for await (const chunk of chunks) {\n        if (chunk?.additional_kwargs?.reasoning_content) {\n          const reasoningContent = mergeReasoningContent(\n            fullText,\n            chunk?.additional_kwargs?.reasoning_content || \"\"\n          )\n          contentToSave = reasoningContent\n          fullText = reasoningContent\n          apiReasoning = true\n        }\n\n        if (apiReasoning && chunk?.content) {\n          fullText += \"</think>\"\n          contentToSave += \"</think>\"\n          apiReasoning = false\n        }\n\n        contentToSave += chunk?.content\n        fullText += chunk?.content\n        if (count === 0) {\n          setIsProcessing(true)\n        }\n        if (isReasoningStarted(fullText) && !reasoningStartTime) {\n          reasoningStartTime = new Date()\n        }\n\n        if (\n          reasoningStartTime &&\n          !reasoningEndTime &&\n          isReasoningEnded(fullText)\n        ) {\n          reasoningEndTime = new Date()\n          const reasoningTime =\n            reasoningEndTime.getTime() - reasoningStartTime.getTime()\n          timetaken = reasoningTime\n        }\n        setMessages((prev) => {\n          return prev.map((message) => {\n            if (message.id === generateMessageId) {\n              return {\n                ...message,\n                message: fullText + \"▋\",\n                reasoning_time_taken: timetaken\n              }\n            }\n            return message\n          })\n        })\n        count++\n      }\n\n      setMessages((prev) => {\n        return prev.map((message) => {\n          if (message.id === generateMessageId) {\n            return {\n              ...message,\n              message: fullText,\n              sources: source,\n              generationInfo,\n              reasoning_time_taken: timetaken\n            }\n          }\n          return message\n        })\n      })\n\n      setHistory([\n        ...history,\n        {\n          role: \"user\",\n          content: message,\n          image,\n        },\n        {\n          role: \"assistant\",\n          content: fullText\n        }\n      ])\n\n      await saveMessageOnSuccess({\n        historyId,\n        setHistoryId,\n        isRegenerate,\n        selectedModel: selectedModel,\n        message,\n        image,\n        fullText,\n        source,\n        message_source: \"copilot\",\n        generationInfo,\n        reasoning_time_taken: timetaken\n      })\n\n      setIsProcessing(false)\n      setStreaming(false)\n    } catch (e) {\n      console.log(e)\n      const errorSave = await saveMessageOnError({\n        e,\n        botMessage: fullText,\n        history,\n        historyId,\n        image,\n        selectedModel,\n        setHistory,\n        setHistoryId,\n        userMessage: message,\n        isRegenerating: isRegenerate,\n        message_source: \"copilot\"\n      })\n\n      if (!errorSave) {\n        notification.error({\n          message: t(\"error\"),\n          description: e?.message || t(\"somethingWentWrong\")\n        })\n      }\n      setIsProcessing(false)\n      setStreaming(false)\n      setIsProcessing(false)\n      setStreaming(false)\n      setIsEmbedding(false)\n    } finally {\n      setAbortController(null)\n      setEmbeddingController(null)\n    }\n  }\n\n  const visionChatMode = async (\n    message: string,\n    image: string,\n    isRegenerate: boolean,\n    messages: Message[],\n    history: ChatHistory,\n    signal: AbortSignal\n  ) => {\n    setStreaming(true)\n    const url = await getOllamaURL()\n\n    const ollama = await pageAssistModel({\n      model: selectedModel!,\n      baseUrl: cleanUrl(url)\n    })\n\n    let newMessage: Message[] = []\n    let generateMessageId = generateID()\n    const modelInfo = await getModelNicknameByID(selectedModel)\n\n    if (!isRegenerate) {\n      newMessage = [\n        ...messages,\n        {\n          isBot: false,\n          name: \"You\",\n          message,\n          sources: [],\n          images: []\n        },\n        {\n          isBot: true,\n          name: selectedModel,\n          message: \"▋\",\n          sources: [],\n          id: generateMessageId,\n          modelImage: modelInfo?.model_avatar,\n          modelName: modelInfo?.model_name || selectedModel\n        }\n      ]\n    } else {\n      newMessage = [\n        ...messages,\n        {\n          isBot: true,\n          name: selectedModel,\n          message: \"▋\",\n          sources: [],\n          id: generateMessageId,\n          modelImage: modelInfo?.model_avatar,\n          modelName: modelInfo?.model_name || selectedModel\n        }\n      ]\n    }\n    setMessages(newMessage)\n    let fullText = \"\"\n    let contentToSave = \"\"\n\n    try {\n      const prompt = await systemPromptForNonRag()\n      const selectedPrompt = await getPromptById(selectedSystemPrompt)\n\n      const applicationChatHistory = []\n\n      const data = await getScreenshotFromCurrentTab()\n\n      const visionImage = data?.screenshot || \"\"\n\n      if (visionImage === \"\") {\n        throw new Error(\n          data?.error ||\n            \"Please close and reopen the side panel. This is a bug that will be fixed soon.\"\n        )\n      }\n\n      if (prompt && !selectedPrompt) {\n        applicationChatHistory.unshift(\n          await systemPromptFormatter({\n            content: prompt\n          })\n        )\n      }\n      if (selectedPrompt) {\n        applicationChatHistory.unshift(\n          await systemPromptFormatter({\n            content: selectedPrompt.content\n          })\n        )\n      }\n\n      let humanMessage = await humanMessageFormatter({\n        content: [\n          {\n            text: message,\n            type: \"text\"\n          },\n          {\n            image_url: visionImage,\n            type: \"image_url\"\n          }\n        ],\n        model: selectedModel,\n        useOCR\n      })\n\n      let generationInfo: any | undefined = undefined\n\n      const chunks = await ollama.stream(\n        [...applicationChatHistory, humanMessage],\n        {\n          signal: signal,\n          callbacks: [\n            {\n              handleLLMEnd(output: any): any {\n                try {\n                  generationInfo = output?.generations?.[0][0]?.generationInfo\n                } catch (e) {\n                  console.error(\"handleLLMEnd error\", e)\n                }\n              }\n            }\n          ]\n        }\n      )\n      let count = 0\n      let reasoningStartTime: Date | undefined = undefined\n      let reasoningEndTime: Date | undefined = undefined\n      let timetaken = 0\n      let apiReasoning = false\n      for await (const chunk of chunks) {\n        if (chunk?.additional_kwargs?.reasoning_content) {\n          const reasoningContent = mergeReasoningContent(\n            fullText,\n            chunk?.additional_kwargs?.reasoning_content || \"\"\n          )\n          contentToSave = reasoningContent\n          fullText = reasoningContent\n          apiReasoning = true\n        }\n\n        if (apiReasoning && chunk?.content) {\n          fullText += \"</think>\"\n          contentToSave += \"</think>\"\n          apiReasoning = false\n        }\n\n        contentToSave += chunk?.content\n        fullText += chunk?.content\n        if (count === 0) {\n          setIsProcessing(true)\n        }\n        if (isReasoningStarted(fullText) && !reasoningStartTime) {\n          reasoningStartTime = new Date()\n        }\n\n        if (\n          reasoningStartTime &&\n          !reasoningEndTime &&\n          isReasoningEnded(fullText)\n        ) {\n          reasoningEndTime = new Date()\n          const reasoningTime =\n            reasoningEndTime.getTime() - reasoningStartTime.getTime()\n          timetaken = reasoningTime\n        }\n        setMessages((prev) => {\n          return prev.map((message) => {\n            if (message.id === generateMessageId) {\n              return {\n                ...message,\n                message: fullText + \"▋\",\n                reasoning_time_taken: timetaken\n              }\n            }\n            return message\n          })\n        })\n        count++\n      }\n      setMessages((prev) => {\n        return prev.map((message) => {\n          if (message.id === generateMessageId) {\n            return {\n              ...message,\n              message: fullText,\n              generationInfo,\n              reasoning_time_taken: timetaken\n            }\n          }\n          return message\n        })\n      })\n\n      setHistory([\n        ...history,\n        {\n          role: \"user\",\n          content: message\n        },\n        {\n          role: \"assistant\",\n          content: fullText\n        }\n      ])\n\n      await saveMessageOnSuccess({\n        historyId,\n        setHistoryId,\n        isRegenerate,\n        selectedModel: selectedModel,\n        message,\n        image,\n        fullText,\n        source: [],\n        message_source: \"copilot\",\n        generationInfo,\n        reasoning_time_taken: timetaken\n      })\n\n      setIsProcessing(false)\n      setStreaming(false)\n    } catch (e) {\n      const errorSave = await saveMessageOnError({\n        e,\n        botMessage: fullText,\n        history,\n        historyId,\n        image,\n        selectedModel,\n        setHistory,\n        setHistoryId,\n        userMessage: message,\n        isRegenerating: isRegenerate,\n        message_source: \"copilot\"\n      })\n\n      if (!errorSave) {\n        notification.error({\n          message: t(\"error\"),\n          description: e?.message || t(\"somethingWentWrong\")\n        })\n      }\n      setIsProcessing(false)\n      setStreaming(false)\n      setIsProcessing(false)\n      setStreaming(false)\n      setIsEmbedding(false)\n    } finally {\n      setAbortController(null)\n      setEmbeddingController(null)\n    }\n  }\n\n  const normalChatMode = async (\n    message: string,\n    image: string,\n    isRegenerate: boolean,\n    messages: Message[],\n    history: ChatHistory,\n    signal: AbortSignal,\n    images?: string[]\n  ) => {\n    setStreaming(true)\n    const url = await getOllamaURL()\n\n    if (image.length > 0) {\n      image = `data:image/jpeg;base64,${image.split(\",\")[1]}`\n    }\n\n    // Process multiple images if provided\n    const processedImages = images?.length > 0\n      ? images.map(img => {\n          if (img.length > 0 && !img.startsWith('data:')) {\n            return `data:image/jpeg;base64,${img.split(\",\")[1]}`\n          }\n          return img\n        }).filter(img => img.length > 0)\n      : image.length > 0 ? [image] : []\n\n    const ollama = await pageAssistModel({\n      model: selectedModel!,\n      baseUrl: cleanUrl(url)\n    })\n\n    let newMessage: Message[] = []\n    let generateMessageId = generateID()\n    const modelInfo = await getModelNicknameByID(selectedModel)\n\n    if (!isRegenerate) {\n      newMessage = [\n        ...messages,\n        {\n          isBot: false,\n          name: \"You\",\n          message,\n          sources: [],\n          images: processedImages\n        },\n        {\n          isBot: true,\n          name: selectedModel,\n          message: \"▋\",\n          sources: [],\n          id: generateMessageId,\n          modelImage: modelInfo?.model_avatar,\n          modelName: modelInfo?.model_name || selectedModel\n        }\n      ]\n    } else {\n      newMessage = [\n        ...messages,\n        {\n          isBot: true,\n          name: selectedModel,\n          message: \"▋\",\n          sources: [],\n          id: generateMessageId,\n          modelImage: modelInfo?.model_avatar,\n          modelName: modelInfo?.model_name || selectedModel\n        }\n      ]\n    }\n    setMessages(newMessage)\n    let fullText = \"\"\n    let contentToSave = \"\"\n\n    try {\n      const prompt = await systemPromptForNonRag()\n      const selectedPrompt = await getPromptById(selectedSystemPrompt)\n\n      let humanMessage = await humanMessageFormatter({\n        content: [\n          {\n            text: message,\n            type: \"text\"\n          }\n        ],\n        model: selectedModel,\n        useOCR\n      })\n      if (processedImages.length > 0) {\n        humanMessage = await humanMessageFormatter({\n          content: [\n            {\n              text: message,\n              type: \"text\"\n            },\n            ...processedImages.map(img => ({\n              image_url: img,\n              type: \"image_url\" as const\n            }))\n          ],\n          model: selectedModel,\n          useOCR\n        })\n      }\n\n      const applicationChatHistory = generateHistory(history, selectedModel)\n\n      if (prompt && !selectedPrompt) {\n        applicationChatHistory.unshift(\n          await systemPromptFormatter({\n            content: prompt\n          })\n        )\n      }\n      if (selectedPrompt) {\n        applicationChatHistory.unshift(\n          await systemPromptFormatter({\n            content: selectedPrompt.content\n          })\n        )\n      }\n\n      let generationInfo: any | undefined = undefined\n\n      const chunks = await ollama.stream(\n        [...applicationChatHistory, humanMessage],\n        {\n          signal: signal,\n          callbacks: [\n            {\n              handleLLMEnd(output: any): any {\n                try {\n                  generationInfo = output?.generations?.[0][0]?.generationInfo\n                } catch (e) {\n                  console.error(\"handleLLMEnd error\", e)\n                }\n              }\n            }\n          ]\n        }\n      )\n      let count = 0\n      let reasoningStartTime: Date | null = null\n      let reasoningEndTime: Date | null = null\n      let timetaken = 0\n      let apiReasoning = false\n\n      for await (const chunk of chunks) {\n        if (chunk?.additional_kwargs?.reasoning_content) {\n          const reasoningContent = mergeReasoningContent(\n            fullText,\n            chunk?.additional_kwargs?.reasoning_content || \"\"\n          )\n          contentToSave = reasoningContent\n          fullText = reasoningContent\n          apiReasoning = true\n        }\n\n        if (apiReasoning && chunk?.content) {\n          fullText += \"</think>\"\n          contentToSave += \"</think>\"\n          apiReasoning = false\n        }\n\n        contentToSave += chunk?.content\n        fullText += chunk?.content\n        if (count === 0) {\n          setIsProcessing(true)\n        }\n        if (isReasoningStarted(fullText) && !reasoningStartTime) {\n          reasoningStartTime = new Date()\n        }\n\n        if (\n          reasoningStartTime &&\n          !reasoningEndTime &&\n          isReasoningEnded(fullText)\n        ) {\n          reasoningEndTime = new Date()\n          const reasoningTime =\n            reasoningEndTime.getTime() - reasoningStartTime.getTime()\n          timetaken = reasoningTime\n        }\n        setMessages((prev) => {\n          return prev.map((message) => {\n            if (message.id === generateMessageId) {\n              return {\n                ...message,\n                message: fullText + \"▋\",\n                reasoning_time_taken: timetaken\n              }\n            }\n            return message\n          })\n        })\n        count++\n      }\n\n      setMessages((prev) => {\n        return prev.map((message) => {\n          if (message.id === generateMessageId) {\n            return {\n              ...message,\n              message: fullText,\n              generationInfo,\n              reasoning_time_taken: timetaken\n            }\n          }\n          return message\n        })\n      })\n\n      setHistory([\n        ...history,\n        {\n          role: \"user\",\n          content: message,\n          image,\n          images: processedImages\n        },\n        {\n          role: \"assistant\",\n          content: fullText\n        }\n      ])\n\n      await saveMessageOnSuccess({\n        historyId,\n        setHistoryId,\n        isRegenerate,\n        selectedModel: selectedModel,\n        message,\n        image,\n        fullText,\n        source: [],\n        message_source: \"copilot\",\n        generationInfo,\n        reasoning_time_taken: timetaken\n      })\n\n      setIsProcessing(false)\n      setStreaming(false)\n    } catch (e) {\n      const errorSave = await saveMessageOnError({\n        e,\n        botMessage: fullText,\n        history,\n        historyId,\n        image,\n        selectedModel,\n        setHistory,\n        setHistoryId,\n        userMessage: message,\n        isRegenerating: isRegenerate,\n        message_source: \"copilot\"\n      })\n\n      if (!errorSave) {\n        notification.error({\n          message: t(\"error\"),\n          description: e?.message || t(\"somethingWentWrong\")\n        })\n      }\n      setIsProcessing(false)\n      setStreaming(false)\n    } finally {\n      setAbortController(null)\n    }\n  }\n\n  const searchChatMode = async (\n    message: string,\n    image: string,\n    isRegenerate: boolean,\n    messages: Message[],\n    history: ChatHistory,\n    signal: AbortSignal,\n    images?: string[]\n  ) => {\n    const url = await getOllamaURL()\n    setStreaming(true)\n    if (image.length > 0) {\n      image = `data:image/jpeg;base64,${image.split(\",\")[1]}`\n    }\n\n    // Process multiple images if provided\n    const processedImages = images?.length > 0\n      ? images.map(img => {\n          if (img.length > 0 && !img.startsWith('data:')) {\n            return `data:image/jpeg;base64,${img.split(\",\")[1]}`\n          }\n          return img\n        }).filter(img => img.length > 0)\n      : image.length > 0 ? [image] : []\n\n    const ollama = await pageAssistModel({\n      model: selectedModel!,\n      baseUrl: cleanUrl(url)\n    })\n\n    let newMessage: Message[] = []\n    let generateMessageId = generateID()\n    const modelInfo = await getModelNicknameByID(selectedModel)\n\n    if (!isRegenerate) {\n      newMessage = [\n        ...messages,\n        {\n          isBot: false,\n          name: \"You\",\n          message,\n          sources: [],\n          images: processedImages\n        },\n        {\n          isBot: true,\n          name: selectedModel,\n          message: \"▋\",\n          sources: [],\n          id: generateMessageId,\n          modelImage: modelInfo?.model_avatar,\n          modelName: modelInfo?.model_name || selectedModel\n        }\n      ]\n    } else {\n      newMessage = [\n        ...messages,\n        {\n          isBot: true,\n          name: selectedModel,\n          message: \"▋\",\n          sources: [],\n          id: generateMessageId,\n          modelImage: modelInfo?.model_avatar,\n          modelName: modelInfo?.model_name || selectedModel\n        }\n      ]\n    }\n    setMessages(newMessage)\n    let fullText = \"\"\n    let contentToSave = \"\"\n\n    try {\n      setIsSearchingInternet(true)\n\n      let query = message\n\n      // if (newMessage.length > 2) {\n      let questionPrompt = await geWebSearchFollowUpPrompt()\n      const lastTenMessages = newMessage.slice(-10)\n      lastTenMessages.pop()\n      const chat_history = lastTenMessages\n        .map((message) => {\n          return `${message.isBot ? \"Assistant: \" : \"Human: \"}${message.message}`\n        })\n        .join(\"\\n\")\n      const promptForQuestion = questionPrompt\n        .replaceAll(\"{chat_history}\", chat_history)\n        .replaceAll(\"{question}\", message)\n      const questionModel = await pageAssistModel({\n        model: selectedModel!,\n        baseUrl: cleanUrl(url)\n      })\n\n      let questionMessage = await humanMessageFormatter({\n        content: [\n          {\n            text: promptForQuestion,\n            type: \"text\"\n          }\n        ],\n        model: selectedModel,\n        useOCR: useOCR\n      })\n\n      if (processedImages.length > 0) {\n        questionMessage = await humanMessageFormatter({\n          content: [\n            {\n              text: promptForQuestion,\n              type: \"text\"\n            },\n            ...processedImages.map(img => ({\n              image_url: img,\n              type: \"image_url\" as const\n            }))\n          ],\n          model: selectedModel,\n          useOCR: useOCR\n        })\n      }\n      try {\n        const isWebQuery = await isQueryHaveWebsite(query)\n        if (!isWebQuery) {\n          const response = await questionModel.invoke([questionMessage])\n          query = response?.content?.toString() || message\n          query = removeReasoning(query)\n        }\n      } catch (error) {\n        console.error(\"Error in questionModel.invoke:\", error)\n      }\n\n      const { prompt, source } = await getSystemPromptForWeb(query)\n      setIsSearchingInternet(false)\n\n      //  message = message.trim().replaceAll(\"\\n\", \" \")\n\n      let humanMessage = await humanMessageFormatter({\n        content: [\n          {\n            text: message,\n            type: \"text\"\n          }\n        ],\n        model: selectedModel,\n        useOCR\n      })\n      if (processedImages.length > 0) {\n        humanMessage = await humanMessageFormatter({\n          content: [\n            {\n              text: message,\n              type: \"text\"\n            },\n            ...processedImages.map(img => ({\n              image_url: img,\n              type: \"image_url\" as const\n            }))\n          ],\n          model: selectedModel,\n          useOCR\n        })\n      }\n\n      const applicationChatHistory = generateHistory(history, selectedModel)\n\n      if (prompt) {\n        applicationChatHistory.unshift(\n          await systemPromptFormatter({\n            content: prompt\n          })\n        )\n      }\n\n      let generationInfo: any | undefined = undefined\n      const chunks = await ollama.stream(\n        [...applicationChatHistory, humanMessage],\n        {\n          signal: signal,\n          callbacks: [\n            {\n              handleLLMEnd(output: any): any {\n                try {\n                  generationInfo = output?.generations?.[0][0]?.generationInfo\n                } catch (e) {\n                  console.error(\"handleLLMEnd error\", e)\n                }\n              }\n            }\n          ]\n        }\n      )\n      let count = 0\n      let timetaken = 0\n      let reasoningStartTime: Date | undefined = undefined\n      let reasoningEndTime: Date | undefined = undefined\n      let apiReasoning = false\n      for await (const chunk of chunks) {\n        if (chunk?.additional_kwargs?.reasoning_content) {\n          const reasoningContent = mergeReasoningContent(\n            fullText,\n            chunk?.additional_kwargs?.reasoning_content || \"\"\n          )\n          contentToSave = reasoningContent\n          fullText = reasoningContent\n          apiReasoning = true\n        }\n\n        if (apiReasoning && chunk?.content) {\n          fullText += \"</think>\"\n          contentToSave += \"</think>\"\n          apiReasoning = false\n        }\n\n        contentToSave += chunk?.content\n        fullText += chunk?.content\n        if (count === 0) {\n          setIsProcessing(true)\n        }\n\n        if (isReasoningStarted(fullText) && !reasoningStartTime) {\n          reasoningStartTime = new Date()\n        }\n\n        if (\n          reasoningStartTime &&\n          !reasoningEndTime &&\n          isReasoningEnded(fullText)\n        ) {\n          reasoningEndTime = new Date()\n          const reasoningTime =\n            reasoningEndTime.getTime() - reasoningStartTime.getTime()\n          timetaken = reasoningTime\n        }\n        setMessages((prev) => {\n          return prev.map((message) => {\n            if (message.id === generateMessageId) {\n              return {\n                ...message,\n                message: fullText + \"▋\",\n                reasoning_time_taken: timetaken\n              }\n            }\n            return message\n          })\n        })\n        count++\n      }\n      // update the message with the full text\n      setMessages((prev) => {\n        return prev.map((message) => {\n          if (message.id === generateMessageId) {\n            return {\n              ...message,\n              message: fullText,\n              sources: source,\n              generationInfo,\n              reasoning_time_taken: timetaken\n            }\n          }\n          return message\n        })\n      })\n\n      setHistory([\n        ...history,\n        {\n          role: \"user\",\n          content: message,\n          image,\n          images: processedImages\n        },\n        {\n          role: \"assistant\",\n          content: fullText\n        }\n      ])\n\n      await saveMessageOnSuccess({\n        historyId,\n        setHistoryId,\n        isRegenerate,\n        selectedModel: selectedModel,\n        message,\n        image,\n        fullText,\n        source,\n        generationInfo,\n        reasoning_time_taken: timetaken\n      })\n\n      setIsProcessing(false)\n      setStreaming(false)\n    } catch (e) {\n      const errorSave = await saveMessageOnError({\n        e,\n        botMessage: fullText,\n        history,\n        historyId,\n        image,\n        selectedModel,\n        setHistory,\n        setHistoryId,\n        userMessage: message,\n        isRegenerating: isRegenerate\n      })\n\n      if (!errorSave) {\n        notification.error({\n          message: t(\"error\"),\n          description: e?.message || t(\"somethingWentWrong\")\n        })\n      }\n      setIsProcessing(false)\n      setStreaming(false)\n    } finally {\n      setAbortController(null)\n    }\n  }\n\n  const presetChatMode = async (\n    message: string,\n    image: string,\n    isRegenerate: boolean,\n    messages: Message[],\n    history: ChatHistory,\n    signal: AbortSignal,\n    messageType: string,\n    images?: string[]\n  ) => {\n    setStreaming(true)\n    const url = await getOllamaURL()\n\n    if (image.length > 0) {\n      image = `data:image/jpeg;base64,${image.split(\",\")[1]}`\n    }\n\n    // Process multiple images if provided\n    const processedImages = images?.length > 0\n      ? images.map(img => {\n          if (img.length > 0 && !img.startsWith('data:')) {\n            return `data:image/jpeg;base64,${img.split(\",\")[1]}`\n          }\n          return img\n        }).filter(img => img.length > 0)\n      : image.length > 0 ? [image] : []\n\n    const ollama = await pageAssistModel({\n      model: selectedModel!,\n      baseUrl: cleanUrl(url)\n    })\n\n    let newMessage: Message[] = []\n    let generateMessageId = generateID()\n    const modelInfo = await getModelNicknameByID(selectedModel)\n\n    if (!isRegenerate) {\n      newMessage = [\n        ...messages,\n        {\n          isBot: false,\n          name: \"You\",\n          message,\n          sources: [],\n          images: processedImages,\n          messageType: messageType\n        },\n        {\n          isBot: true,\n          name: selectedModel,\n          message: \"▋\",\n          sources: [],\n          id: generateMessageId,\n          modelImage: modelInfo?.model_avatar,\n          modelName: modelInfo?.model_name || selectedModel\n        }\n      ]\n    } else {\n      newMessage = [\n        ...messages,\n        {\n          isBot: true,\n          name: selectedModel,\n          message: \"▋\",\n          sources: [],\n          id: generateMessageId,\n          modelImage: modelInfo?.model_avatar,\n          modelName: modelInfo?.model_name || selectedModel\n        }\n      ]\n    }\n    setMessages(newMessage)\n    let fullText = \"\"\n    let contentToSave = \"\"\n\n    try {\n      const prompt = await getPrompt(messageType)\n      let humanMessage = await humanMessageFormatter({\n        content: [\n          {\n            text: prompt.replace(\"{text}\", message),\n            type: \"text\"\n          }\n        ],\n        model: selectedModel,\n        useOCR\n      })\n      if (processedImages.length > 0) {\n        humanMessage = await humanMessageFormatter({\n          content: [\n            {\n              text: prompt.replace(\"{text}\", message),\n              type: \"text\"\n            },\n            ...processedImages.map(img => ({\n              image_url: img,\n              type: \"image_url\" as const\n            }))\n          ],\n          model: selectedModel,\n          useOCR\n        })\n      }\n\n      let generationInfo: any | undefined = undefined\n\n      const chunks = await ollama.stream([humanMessage], {\n        signal: signal,\n        callbacks: [\n          {\n            handleLLMEnd(output: any): any {\n              try {\n                generationInfo = output?.generations?.[0][0]?.generationInfo\n              } catch (e) {\n                console.error(\"handleLLMEnd error\", e)\n              }\n            }\n          }\n        ]\n      })\n      let count = 0\n      let reasoningStartTime: Date | null = null\n      let reasoningEndTime: Date | null = null\n      let timetaken = 0\n      let apiReasoning = false\n      for await (const chunk of chunks) {\n        if (chunk?.additional_kwargs?.reasoning_content) {\n          const reasoningContent = mergeReasoningContent(\n            fullText,\n            chunk?.additional_kwargs?.reasoning_content || \"\"\n          )\n          contentToSave = reasoningContent\n          fullText = reasoningContent\n          apiReasoning = true\n        }\n\n        if (apiReasoning && chunk?.content) {\n          fullText += \"</think>\"\n          contentToSave += \"</think>\"\n          apiReasoning = false\n        }\n\n        contentToSave += chunk?.content\n        fullText += chunk?.content\n        if (count === 0) {\n          setIsProcessing(true)\n        }\n        if (isReasoningStarted(fullText) && !reasoningStartTime) {\n          reasoningStartTime = new Date()\n        }\n\n        if (\n          reasoningStartTime &&\n          !reasoningEndTime &&\n          isReasoningEnded(fullText)\n        ) {\n          reasoningEndTime = new Date()\n          const reasoningTime =\n            reasoningEndTime.getTime() - reasoningStartTime.getTime()\n          timetaken = reasoningTime\n        }\n        setMessages((prev) => {\n          return prev.map((message) => {\n            if (message.id === generateMessageId) {\n              return {\n                ...message,\n                message: fullText + \"▋\",\n                reasoning_time_taken: timetaken\n              }\n            }\n            return message\n          })\n        })\n        count++\n      }\n\n      setMessages((prev) => {\n        return prev.map((message) => {\n          if (message.id === generateMessageId) {\n            return {\n              ...message,\n              message: fullText,\n              generationInfo,\n              reasoning_time_taken: timetaken\n            }\n          }\n          return message\n        })\n      })\n\n      setHistory([\n        ...history,\n        {\n          role: \"user\",\n          content: message,\n          image,\n          messageType,\n          images: processedImages\n        },\n        {\n          role: \"assistant\",\n          content: fullText\n        }\n      ])\n\n      await saveMessageOnSuccess({\n        historyId,\n        setHistoryId,\n        isRegenerate,\n        selectedModel: selectedModel,\n        message,\n        image,\n        fullText,\n        source: [],\n        message_source: \"copilot\",\n        message_type: messageType,\n        generationInfo,\n        reasoning_time_taken: timetaken\n      })\n\n      setIsProcessing(false)\n      setStreaming(false)\n    } catch (e) {\n      const errorSave = await saveMessageOnError({\n        e,\n        botMessage: fullText,\n        history,\n        historyId,\n        image,\n        selectedModel,\n        setHistory,\n        setHistoryId,\n        userMessage: message,\n        isRegenerating: isRegenerate,\n        message_source: \"copilot\",\n        message_type: messageType\n      })\n\n      if (!errorSave) {\n        notification.error({\n          message: t(\"error\"),\n          description: e?.message || t(\"somethingWentWrong\")\n        })\n      }\n      setIsProcessing(false)\n      setStreaming(false)\n    } finally {\n      setAbortController(null)\n    }\n  }\n\n  const onSubmit = async ({\n    message,\n    image,\n    images,\n    isRegenerate,\n    controller,\n    memory,\n    messages: chatHistory,\n    messageType,\n    chatType\n  }: {\n    message: string\n    image: string\n    images?: string[]\n    isRegenerate?: boolean\n    messages?: Message[]\n    memory?: ChatHistory\n    controller?: AbortController\n    messageType?: string\n    chatType?: string\n  }) => {\n    let signal: AbortSignal\n    if (!controller) {\n      const newController = new AbortController()\n      signal = newController.signal\n      setAbortController(newController)\n    } else {\n      setAbortController(controller)\n      signal = controller.signal\n    }\n\n    if (chatType === \"youtube\") {\n      setChatMode(\"rag\")\n      const newEmbeddingController = new AbortController()\n      let embeddingSignal = newEmbeddingController.signal\n      setEmbeddingController(newEmbeddingController)\n      await chatWithWebsiteMode(\n        message,\n        image,\n        isRegenerate,\n        chatHistory || messages,\n        memory || history,\n        signal,\n        embeddingSignal\n      )\n      return\n    }\n\n    if (messageType) {\n      await presetChatMode(\n        message,\n        image,\n        isRegenerate,\n        chatHistory || messages,\n        memory || history,\n        signal,\n        messageType,\n        images\n      )\n    } else {\n      if (chatMode === \"normal\") {\n        if (webSearch) {\n          await searchChatMode(\n            message,\n            image,\n            isRegenerate || false,\n            messages,\n            memory || history,\n            signal,\n            images\n          )\n        } else {\n          await sharedNormalChatMode(\n            message,\n            image,\n            isRegenerate,\n            chatHistory || messages,\n            memory || history,\n            signal,\n            {\n              selectedModel,\n              useOCR,\n              selectedSystemPrompt,\n              currentChatModelSettings,\n              setMessages,\n              saveMessageOnSuccess,\n              saveMessageOnError,\n              setHistory,\n              setIsProcessing,\n              setStreaming,\n              setAbortController,\n              historyId,\n              setHistoryId,\n              images,\n              setActionInfo,\n              temporaryChat,\n              messageSource: \"copilot\"\n            }\n          )\n        }\n      } else if (chatMode === \"vision\") {\n        await visionChatMode(\n          message,\n          image,\n          isRegenerate,\n          chatHistory || messages,\n          memory || history,\n          signal\n        )\n      } else {\n        const newEmbeddingController = new AbortController()\n        let embeddingSignal = newEmbeddingController.signal\n        setEmbeddingController(newEmbeddingController)\n        await chatWithWebsiteMode(\n          message,\n          image,\n          isRegenerate,\n          chatHistory || messages,\n          memory || history,\n          signal,\n          embeddingSignal\n        )\n      }\n    }\n  }\n\n  React.useEffect(() => {\n    onSubmitRef.current = onSubmit\n  }, [onSubmit])\n\n  const stopStreamingRequest = () => {\n    if (isEmbedding) {\n      if (embeddingController) {\n        embeddingController.abort()\n        setEmbeddingController(null)\n      }\n    }\n    if (abortController) {\n      abortController.abort()\n      setAbortController(null)\n    }\n  }\n\n  const editMessage = React.useCallback(async (\n    index: number,\n    message: string,\n    isHuman: boolean\n  ) => {\n    const currentMessages = messagesRef.current\n    const currentHistory = historyRef.current\n    const currentHistoryId = historyIdRef.current\n    const nextMessages = currentMessages.map((currentMessage, currentIndex) =>\n      currentIndex === index\n        ? {\n            ...currentMessage,\n            message\n          }\n        : currentMessage\n    )\n    const nextHistory = currentHistory.map((currentMessage, currentIndex) =>\n      currentIndex === index\n        ? {\n            ...currentMessage,\n            content: message\n          }\n        : currentMessage\n    )\n\n    if (isHuman) {\n      const currentHumanMessage = nextMessages[index]\n      const previousMessages = nextMessages.slice(0, index + 1)\n      setMessages(previousMessages)\n      const previousHistory = nextHistory.slice(0, index)\n      setHistory(previousHistory)\n      await updateMessageByIndex(currentHistoryId, index, message)\n      await deleteChatForEdit(currentHistoryId, index)\n      const abortController = new AbortController()\n      await onSubmitRef.current({\n        message: message,\n        image: currentHumanMessage.images?.[0] || \"\",\n        images: currentHumanMessage.images || [],\n        isRegenerate: true,\n        messages: previousMessages,\n        memory: previousHistory,\n        controller: abortController\n      })\n    } else {\n      setMessages(nextMessages)\n      setHistory(nextHistory)\n      await updateMessageByIndex(currentHistoryId, index, message)\n    }\n  }, [setMessages, setHistory])\n\n  const getMessages = React.useCallback(() => messagesRef.current, [])\n  const getHistory = React.useCallback(() => historyRef.current, [])\n  const getHistoryId = React.useCallback(() => historyIdRef.current, [])\n  const submitWithCurrentState = React.useCallback(\n    (params: any) => onSubmitRef.current(params),\n    []\n  )\n\n  const regenerateLastMessage = React.useMemo(\n    () =>\n      createRegenerateLastMessage({\n        validateBeforeSubmitFn: () => true,\n        history: getHistory,\n        messages: getMessages,\n        setHistory,\n        setMessages,\n        historyId: getHistoryId,\n        removeMessageUsingHistoryIdFn: removeMessageUsingHistoryId,\n        onSubmit: submitWithCurrentState\n      }),\n    [\n      getHistory,\n      getMessages,\n      setHistory,\n      setMessages,\n      getHistoryId,\n      submitWithCurrentState\n    ]\n  )\n  const createChatBranch = React.useMemo(\n    () =>\n      createBranchMessage({\n        historyId: null,\n        getHistoryId,\n        setHistory,\n        setHistoryId,\n        setMessages,\n        setSelectedSystemPrompt,\n        setSystemPrompt: currentChatModelSettings.setSystemPrompt\n      }),\n    [\n      setHistory,\n      setHistoryId,\n      setMessages,\n      getHistoryId,\n      setSelectedSystemPrompt,\n      currentChatModelSettings.setSystemPrompt\n    ]\n  )\n  return {\n    messages,\n    setMessages,\n    editMessage,\n    onSubmit,\n    setStreaming,\n    streaming,\n    setHistory,\n    historyId,\n    setHistoryId,\n    setIsFirstMessage,\n    isLoading,\n    setIsLoading,\n    isProcessing,\n    stopStreamingRequest,\n    clearChat,\n    selectedModel,\n    setSelectedModel,\n    chatMode,\n    setChatMode,\n    isEmbedding,\n    regenerateLastMessage,\n    webSearch,\n    setWebSearch,\n    isSearchingInternet,\n    selectedQuickPrompt,\n    setSelectedQuickPrompt,\n    selectedSystemPrompt,\n    setSelectedSystemPrompt,\n    speechToTextLanguage,\n    setSpeechToTextLanguage,\n    useOCR,\n    setUseOCR,\n    defaultInternetSearchOn,\n    defaultChatWithWebsite,\n    history,\n    createChatBranch,\n    temporaryChat,\n    setTemporaryChat,\n    sidepanelTemporaryChat,\n    actionInfo\n  }\n}\n"
  },
  {
    "path": "src/hooks/useMessageOption.tsx",
    "content": "import React from \"react\"\nimport { type ChatHistory, type Message } from \"~/store/option\"\nimport { useStoreMessageOption } from \"~/store/option\"\nimport { removeMessageUsingHistoryId, saveHistory, saveMessage } from \"@/db/dexie/helpers\"\nimport { useNavigate } from \"react-router-dom\"\nimport { notification } from \"antd\"\nimport { useTranslation } from \"react-i18next\"\nimport { usePageAssist } from \"@/context\"\nimport { useWebUI } from \"@/store/webui\"\nimport { useStorage } from \"@plasmohq/storage/hook\"\nimport { useStoreChatModelSettings } from \"@/store/model\"\nimport { ChatDocuments } from \"@/models/ChatTypes\"\nimport { searchChatMode } from \"./chat-modes/searchChatMode\"\nimport { normalChatMode } from \"./chat-modes/normalChatMode\"\nimport { continueChatMode } from \"./chat-modes/continueChatMode\"\nimport { ragMode } from \"./chat-modes/ragMode\"\nimport {\n  focusTextArea,\n  validateBeforeSubmit,\n  createSaveMessageOnSuccess,\n  createSaveMessageOnError\n} from \"./utils/messageHelpers\"\nimport {\n  createRegenerateLastMessage,\n  createEditMessage,\n  createStopStreamingRequest,\n  createBranchMessage\n} from \"./handlers/messageHandlers\"\nimport { tabChatMode } from \"./chat-modes/tabChatMode\"\nimport { documentChatMode } from \"./chat-modes/documentChatMode\"\nimport { generateID } from \"@/db/dexie/helpers\"\nimport { UploadedFile } from \"@/db/dexie/types\"\nimport { updatePageTitle } from \"@/utils/update-page-title\"\nimport { generateTitle } from \"@/services/title\"\n\nexport const useMessageOption = () => {\n  const {\n    controller: abortController,\n    setController: setAbortController,\n    messages,\n    setMessages\n  } = usePageAssist()\n  const {\n    history,\n    setHistory,\n    setStreaming,\n    streaming,\n    setIsFirstMessage,\n    historyId,\n    setHistoryId,\n    isLoading,\n    setIsLoading,\n    isProcessing,\n    setIsProcessing,\n    chatMode,\n    setChatMode,\n    webSearch,\n    setWebSearch,\n    isSearchingInternet,\n    setIsSearchingInternet,\n    selectedQuickPrompt,\n    setSelectedQuickPrompt,\n    selectedSystemPrompt,\n    setSelectedSystemPrompt,\n    selectedKnowledge,\n    setSelectedKnowledge,\n    temporaryChat,\n    setTemporaryChat,\n    useOCR,\n    setUseOCR,\n    documentContext,\n    setDocumentContext,\n    uploadedFiles,\n    setUploadedFiles,\n    contextFiles,\n    setContextFiles,\n    actionInfo,\n    setActionInfo,\n    setFileRetrievalEnabled,\n    fileRetrievalEnabled\n  } = useStoreMessageOption()\n  const [webuiTemporaryChat] = useStorage(\"webuiTemporaryChat\", false)\n\n  const currentChatModelSettings = useStoreChatModelSettings()\n  const [selectedModel, setSelectedModel] = useStorage(\"selectedModel\")\n  const [defaultInternetSearchOn] = useStorage(\"defaultInternetSearchOn\", false)\n  const [speechToTextLanguage, setSpeechToTextLanguage] = useStorage(\n    \"speechToTextLanguage\",\n    \"en-US\"\n  )\n  const { ttsEnabled } = useWebUI()\n\n  const { t } = useTranslation(\"option\")\n\n  const navigate = useNavigate()\n  const textareaRef = React.useRef<HTMLTextAreaElement>(null)\n\n  const handleFocusTextArea = () => focusTextArea(textareaRef)\n\n  const handleFileUpload = async (file: File) => {\n    try {\n      const isImage = file.type.startsWith(\"image/\")\n\n      if (isImage) {\n        return file\n      }\n\n      const maxSize = 10 * 1024 * 1024\n      if (file.size > maxSize) {\n        notification.error({\n          message: \"File Too Large\",\n          description: \"File size must be less than 10MB\"\n        })\n        return\n      }\n\n      const fileId = generateID()\n\n      const { processFileUpload } = await import(\"~/utils/file-processor\")\n      const source = await processFileUpload(file)\n\n      const uploadedFile: UploadedFile = {\n        id: fileId,\n        filename: file.name,\n        type: file.type,\n        content: source.content,\n        size: file.size,\n        uploadedAt: Date.now(),\n        processed: false\n      }\n\n      setUploadedFiles([...uploadedFiles, uploadedFile])\n      setContextFiles([...contextFiles, uploadedFile])\n\n      return file\n    } catch (error) {\n      console.error(\"Error uploading file:\", error)\n      notification.error({\n        message: \"Upload Failed\",\n        description: \"Failed to upload file. Please try again.\"\n      })\n      throw error\n    }\n  }\n\n  const removeUploadedFile = async (fileId: string) => {\n    setUploadedFiles(uploadedFiles.filter((f) => f.id !== fileId))\n    setContextFiles(contextFiles.filter((f) => f.id !== fileId))\n  }\n\n  const clearUploadedFiles = () => {\n    setUploadedFiles([])\n  }\n\n  const handleSetFileRetrievalEnabled = async (enabled: boolean) => {\n    setFileRetrievalEnabled(enabled)\n  }\n\n  const clearChat = () => {\n    navigate(\"/\")\n    setMessages([])\n    setHistory([])\n    setHistoryId(null)\n    setIsFirstMessage(true)\n    setIsLoading(false)\n    setIsProcessing(false)\n    setStreaming(false)\n    setContextFiles([])\n    updatePageTitle()\n    currentChatModelSettings.reset()\n    // textareaRef?.current?.focus()\n    if (defaultInternetSearchOn) {\n      setWebSearch(true)\n    }\n    handleFocusTextArea()\n    setDocumentContext(null)\n    // Clear uploaded files\n    setUploadedFiles([])\n    setFileRetrievalEnabled(false)\n    setActionInfo(null)\n    if (webuiTemporaryChat) {\n      setTemporaryChat(true)\n    }\n  }\n\n  const saveMessageOnSuccess = createSaveMessageOnSuccess(\n    temporaryChat,\n    setHistoryId as (id: string) => void\n  )\n  const saveMessageOnError = createSaveMessageOnError(\n    temporaryChat,\n    history,\n    setHistory,\n    setHistoryId as (id: string) => void\n  )\n\n  const validateBeforeSubmitFn = React.useCallback(\n    () => validateBeforeSubmit(selectedModel, t),\n    [selectedModel, t]\n  )\n\n  const onSubmit = async ({\n    message,\n    image,\n    images,\n    isRegenerate = false,\n    messages: chatHistory,\n    memory,\n    controller,\n    isContinue,\n    docs\n  }: {\n    message: string\n    image: string\n    images?: string[]\n    isRegenerate?: boolean\n    isContinue?: boolean\n    messages?: Message[]\n    memory?: ChatHistory\n    controller?: AbortController\n    docs?: ChatDocuments\n  }) => {\n    setStreaming(true)\n    let signal: AbortSignal\n    if (!controller) {\n      const newController = new AbortController()\n      signal = newController.signal\n      setAbortController(newController)\n    } else {\n      setAbortController(controller)\n      signal = controller.signal\n    }\n\n    const chatModeParams = {\n      selectedModel,\n      useOCR,\n      selectedSystemPrompt,\n      selectedKnowledge,\n      currentChatModelSettings,\n      setMessages,\n      setIsSearchingInternet,\n      saveMessageOnSuccess,\n      saveMessageOnError,\n      setHistory,\n      setIsProcessing,\n      setStreaming,\n      setAbortController,\n      historyId,\n      setHistoryId,\n      fileRetrievalEnabled,\n      setActionInfo,\n      webSearch,\n      temporaryChat,\n      messageSource: \"web-ui\" as const\n    }\n\n    try {\n      if (isContinue) {\n        await continueChatMode(\n          chatHistory || messages,\n          memory || history,\n          signal,\n          chatModeParams\n        )\n        return\n      }\n      // console.log(\"contextFiles\", contextFiles)\n      if (contextFiles.length > 0) {\n        await documentChatMode(\n          message,\n          image,\n          isRegenerate,\n          chatHistory || messages,\n          memory || history,\n          signal,\n          contextFiles,\n          chatModeParams\n        )\n        // setFileRetrievalEnabled(false)\n        return\n      }\n\n      if (docs?.length > 0 || documentContext?.length > 0) {\n        const processingTabs = docs || documentContext || []\n\n        if (docs?.length > 0) {\n          setDocumentContext(\n            Array.from(new Set([...(documentContext || []), ...docs]))\n          )\n        }\n        await tabChatMode(\n          message,\n          image,\n          processingTabs,\n          isRegenerate,\n          chatHistory || messages,\n          memory || history,\n          signal,\n          chatModeParams\n        )\n        return\n      }\n\n      if (selectedKnowledge) {\n        await ragMode(\n          message,\n          image,\n          isRegenerate,\n          chatHistory || messages,\n          memory || history,\n          signal,\n          chatModeParams\n        )\n      } else {\n        if (webSearch) {\n          // Include images array in search mode\n          const enhancedSearchChatModeParams = {\n            ...chatModeParams,\n            images: images\n          }\n\n          await searchChatMode(\n            message,\n            image,\n            isRegenerate,\n            chatHistory || messages,\n            memory || history,\n            signal,\n            enhancedSearchChatModeParams\n          )\n        } else {\n          // Include uploaded files info and images array in normal mode\n          const enhancedChatModeParams = {\n            ...chatModeParams,\n            uploadedFiles: uploadedFiles,\n            images: images\n          }\n\n          await normalChatMode(\n            message,\n            image,\n            isRegenerate,\n            chatHistory || messages,\n            memory || history,\n            signal,\n            enhancedChatModeParams\n          )\n        }\n      }\n    } catch (e: any) {\n      notification.error({\n        message: t(\"error\"),\n        description: e?.message || t(\"somethingWentWrong\")\n      })\n      setIsProcessing(false)\n      setStreaming(false)\n    }\n  }\n\n  const messagesRef = React.useRef(messages)\n  const historyRef = React.useRef(history)\n  const historyIdRef = React.useRef(historyId)\n  const onSubmitRef = React.useRef(onSubmit)\n\n  React.useEffect(() => {\n    messagesRef.current = messages\n  }, [messages])\n\n  React.useEffect(() => {\n    historyRef.current = history\n  }, [history])\n\n  React.useEffect(() => {\n    historyIdRef.current = historyId\n  }, [historyId])\n\n  React.useEffect(() => {\n    onSubmitRef.current = onSubmit\n  }, [onSubmit])\n\n  const getMessages = React.useCallback(() => messagesRef.current, [])\n  const getHistory = React.useCallback(() => historyRef.current, [])\n  const getHistoryId = React.useCallback(() => historyIdRef.current, [])\n  const submitWithCurrentState = React.useCallback(\n    (params: any) => onSubmitRef.current(params),\n    []\n  )\n\n  const regenerateLastMessage = React.useMemo(\n    () =>\n      createRegenerateLastMessage({\n        validateBeforeSubmitFn,\n        history: getHistory,\n        messages: getMessages,\n        setHistory,\n        setMessages,\n        historyId: getHistoryId,\n        removeMessageUsingHistoryIdFn: removeMessageUsingHistoryId,\n        onSubmit: submitWithCurrentState\n      }),\n    [\n      validateBeforeSubmitFn,\n      getHistory,\n      getMessages,\n      setHistory,\n      setMessages,\n      getHistoryId,\n      submitWithCurrentState\n    ]\n  )\n\n  const stopStreamingRequest = createStopStreamingRequest(\n    abortController,\n    setAbortController\n  )\n\n  const editMessage = React.useMemo(\n    () =>\n      createEditMessage({\n        messages: getMessages,\n        history: getHistory,\n        setMessages,\n        setHistory,\n        historyId: getHistoryId,\n        validateBeforeSubmitFn,\n        onSubmit: submitWithCurrentState\n      }),\n    [\n      getMessages,\n      getHistory,\n      setMessages,\n      setHistory,\n      getHistoryId,\n      validateBeforeSubmitFn,\n      submitWithCurrentState\n    ]\n  )\n\n  const createChatBranch = React.useMemo(\n    () =>\n      createBranchMessage({\n        historyId: null,\n        getHistoryId,\n        setHistory,\n        setHistoryId,\n        setMessages,\n        setContext: setContextFiles,\n        setSelectedSystemPrompt,\n        setSystemPrompt: currentChatModelSettings.setSystemPrompt\n      }),\n    [\n      getHistoryId,\n      setHistory,\n      setHistoryId,\n      setMessages,\n      setContextFiles,\n      setSelectedSystemPrompt,\n      currentChatModelSettings.setSystemPrompt\n    ]\n  )\n\n  const saveTemporaryChat = async () => {\n    if (!temporaryChat || messages.length === 0) {\n      return\n    }\n\n    try {\n      // Generate title from the first user message\n      const firstUserMessage = messages.find((msg) => !msg.isBot)?.message || \"Untitled Chat\"\n      const title = await generateTitle(selectedModel, history, firstUserMessage)\n\n      // Determine message source based on context\n      const messageSource = \"web-ui\" as const\n\n      // Create new history record\n      const newHistory = await saveHistory(\n        title,\n        chatMode === \"rag\",\n        messageSource\n      )\n\n      let timeOffset = 0\n\n      // Save all messages to the new history\n      for (const msg of messages) {\n        await saveMessage({\n          history_id: newHistory.id,\n          name: selectedModel,\n          role:\n            msg.messageKind === \"tool_result\"\n              ? \"tool\"\n              : msg.isBot\n                ? \"assistant\"\n                : \"user\",\n          content: msg.message,\n          images: msg.images || [],\n          source: msg.sources || [],\n          time: ++timeOffset,\n          message_type: \"normal\",\n          generationInfo: msg.generationInfo,\n          reasoning_time_taken: msg.reasoning_time_taken || 0,\n          documents: msg.documents,\n          messageKind: msg.messageKind,\n          toolCalls: msg.toolCalls,\n          toolCallId: msg.toolCallId,\n          toolName: msg.toolName,\n          toolServerName: msg.toolServerName,\n          toolError: msg.toolError\n        })\n      }\n\n      // Update state to convert temporary chat to permanent\n      setHistoryId(newHistory.id)\n      setTemporaryChat(false)\n      updatePageTitle(title)\n\n      // Show success notification\n      notification.success({\n        message: t(\"chatSaved\"),\n        description: t(\"temporaryChatSavedSuccessfully\")\n      })\n\n      return newHistory.id\n    } catch (error) {\n      console.error(\"Error saving temporary chat:\", error)\n      notification.error({\n        message: t(\"error\"),\n        description: t(\"failedToSaveTemporaryChat\")\n      })\n    }\n  }\n\n  return {\n    editMessage,\n    messages,\n    setMessages,\n    onSubmit,\n    setStreaming,\n    streaming,\n    setHistory,\n    historyId,\n    setHistoryId,\n    setIsFirstMessage,\n    isLoading,\n    setIsLoading,\n    isProcessing,\n    stopStreamingRequest,\n    clearChat,\n    selectedModel,\n    setSelectedModel,\n    chatMode,\n    setChatMode,\n    speechToTextLanguage,\n    setSpeechToTextLanguage,\n    regenerateLastMessage,\n    webSearch,\n    setWebSearch,\n    isSearchingInternet,\n    setIsSearchingInternet,\n    selectedQuickPrompt,\n    setSelectedQuickPrompt,\n    selectedSystemPrompt,\n    setSelectedSystemPrompt,\n    textareaRef,\n    selectedKnowledge,\n    setSelectedKnowledge,\n    ttsEnabled,\n    temporaryChat,\n    setTemporaryChat,\n    useOCR,\n    setUseOCR,\n    defaultInternetSearchOn,\n    history,\n    uploadedFiles,\n    fileRetrievalEnabled,\n    setFileRetrievalEnabled: handleSetFileRetrievalEnabled,\n    handleFileUpload,\n    removeUploadedFile,\n    clearUploadedFiles,\n    actionInfo,\n    setActionInfo,\n    setContextFiles,\n    createChatBranch,\n    webuiTemporaryChat,\n    saveTemporaryChat\n  }\n}\n"
  },
  {
    "path": "src/hooks/useMessageQueue.ts",
    "content": "import React from \"react\"\n\nexport type QueuedMessage = {\n  id: string\n  message: string\n  images: string[]\n}\n\nconst createQueueId = () => {\n  return `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`\n}\n\nexport const useMessageQueue = ({\n  enabled,\n  streaming,\n  onSendMessage,\n  onStopStreaming\n}: {\n  enabled: boolean\n  streaming: boolean\n  onSendMessage: (payload: { message: string; images: string[] }) => Promise<void>\n  onStopStreaming: () => void\n}) => {\n  const [queuedMessages, setQueuedMessages] = React.useState<QueuedMessage[]>([\n\n  ])\n  const isDispatchingRef = React.useRef(false)\n  const queueRef = React.useRef<QueuedMessage[]>([])\n  const priorityMessageIdRef = React.useRef<string | null>(null)\n\n  React.useEffect(() => {\n    queueRef.current = queuedMessages\n  }, [queuedMessages])\n\n  React.useEffect(() => {\n    if (!enabled) {\n      setQueuedMessages([])\n      priorityMessageIdRef.current = null\n    }\n  }, [enabled])\n\n  const flushNextMessage = React.useCallback(async () => {\n\n    if (!enabled || streaming || isDispatchingRef.current) {\n      return\n    }\n\n    const queue = queueRef.current\n    if (queue.length === 0) {\n      return\n    }\n\n    const priorityId = priorityMessageIdRef.current\n    let next = queue[0]\n\n    if (priorityId) {\n      const priorityMessage = queue.find((item) => item.id === priorityId)\n      if (priorityMessage) {\n        next = priorityMessage\n      }\n      priorityMessageIdRef.current = null\n    }\n\n    isDispatchingRef.current = true\n    setQueuedMessages((prev) => prev.filter((item) => item.id !== next.id))\n\n    try {\n      await onSendMessage({\n        message: next.message,\n        images: next.images\n      })\n    } catch (error) {\n      setQueuedMessages((prev) => [next, ...prev])\n      console.error(\"Failed to send queued message\", error)\n    } finally {\n      isDispatchingRef.current = false\n    }\n  }, [enabled, onSendMessage, streaming])\n\n  React.useEffect(() => {\n    if (!enabled || streaming || queuedMessages.length === 0) {\n      return\n    }\n    void flushNextMessage()\n  }, [enabled, flushNextMessage, queuedMessages.length, streaming])\n\n  const enqueueMessage = React.useCallback(\n    ({ message, images = [] }: { message: string; images?: string[] }) => {\n      if (!enabled) {\n        return false\n      }\n\n      const trimmedMessage = message.trim()\n      const hasImages = images.length > 0\n      if (!trimmedMessage && !hasImages) {\n        return false\n      }\n\n      setQueuedMessages((prev) => {\n        return [\n          ...prev,\n          {\n            id: createQueueId(),\n            message: trimmedMessage,\n            images\n          }\n        ]\n      })\n\n      return true\n    },\n    [enabled, streaming]\n  )\n\n  const deleteQueuedMessage = React.useCallback((id: string) => {\n    setQueuedMessages((prev) => prev.filter((item) => item.id !== id))\n  }, [])\n\n  const updateQueuedMessage = React.useCallback(\n    (\n      id: string,\n      payload: {\n        message: string\n        images?: string[]\n      }\n    ) => {\n      const trimmedMessage = payload.message.trim()\n      const nextImages = payload.images || []\n      const hasImages = nextImages.length > 0\n\n      setQueuedMessages((prev) => {\n        if (!trimmedMessage && !hasImages) {\n          return prev.filter((item) => item.id !== id)\n        }\n\n        return prev.map((item) =>\n          item.id === id\n            ? {\n              ...item,\n              message: trimmedMessage,\n              images: nextImages\n            }\n            : item\n        )\n      })\n    },\n    []\n  )\n\n  const takeQueuedMessage = React.useCallback((id: string) => {\n    let queuedMessage: QueuedMessage | null = null\n    setQueuedMessages((prev) => {\n      const target = prev.find((item) => item.id === id)\n      if (target) {\n        queuedMessage = target\n      }\n      return prev.filter((item) => item.id !== id)\n    })\n    return queuedMessage\n  }, [])\n\n  const sendQueuedMessageNow = React.useCallback(\n    (id: string) => {\n      if (!queueRef.current.find((item) => item.id === id)) {\n        return\n      }\n\n      priorityMessageIdRef.current = id\n\n      if (streaming) {\n        onStopStreaming()\n        return\n      }\n\n      void flushNextMessage()\n    },\n    [flushNextMessage, onStopStreaming, streaming]\n  )\n\n  return {\n    queuedMessages,\n    enqueueMessage,\n    deleteQueuedMessage,\n    updateQueuedMessage,\n    takeQueuedMessage,\n    sendQueuedMessageNow\n  }\n}\n"
  },
  {
    "path": "src/hooks/useMigration.tsx",
    "content": "import { useEffect } from \"react\"\nimport { useMutation } from \"@tanstack/react-query\"\nimport { runAllMigrations } from \"~/db/dexie/migration\"\nimport { Storage } from \"@plasmohq/storage\"\nimport { message, notification } from \"antd\"\n\nconst storage = new Storage()\n\nexport const getIsMigrated = async () => {\n  const isMigrated = await storage.get(\"isMigrated\")\n  return isMigrated || false\n}\n\nexport const setIsMigrated = async (isMigrated: boolean) => {\n  await storage.set(\"isMigrated\", isMigrated)\n}\n\ninterface MigrationResult {\n  success: boolean\n  error?: string\n}\n\nexport const useMigration = () => {\n  const migrationMutation = useMutation<MigrationResult, Error>({\n    mutationFn: async () => {\n      try {\n        const isMigrated = await getIsMigrated()\n        if (isMigrated) {\n          return { success: false }\n        }\n        message.loading(\n          \"Sorry for the interruption. This is a one-time update that won't occur again. This is for a better optimized chat. The page will refresh after the update.\",\n          30_000\n        )\n        console.log(\"Starting background migration...\")\n        await runAllMigrations()\n        console.log(\"Background migration completed successfully\")\n        return { success: true }\n      } catch (error) {\n        console.error(\"Background migration failed:\", error)\n        return {\n          success: false,\n          error: error instanceof Error ? error.message : \"Unknown error\"\n        }\n      }\n  },\n    onSuccess: async (result) => {\n      if (result.success) {\n        await setIsMigrated(true)\n        notification.success({\n          message: \"Migration completed successfully\"\n        })\n        window.location.reload()\n      }\n    }\n  })\n\n  useEffect(() => {\n    migrationMutation.mutate()\n  }, [])\n\n  return {\n    isLoading: migrationMutation.isPending,\n    isSuccess: migrationMutation.isSuccess && migrationMutation.data?.success,\n    isError:\n      migrationMutation.isError ||\n      (migrationMutation.data && !migrationMutation.data.success),\n    error: migrationMutation.data?.error || migrationMutation.error?.message,\n    retry: () => migrationMutation.mutate()\n  }\n}\n"
  },
  {
    "path": "src/hooks/useSmartScroll.tsx",
    "content": "import { useRef, useEffect, useState, useCallback } from \"react\"\n\nexport const useSmartScroll = (\n  messages: any[],\n  streaming: boolean,\n  threshold: number = 100,\n  resetKey?: string | null\n) => {\n  const containerRef = useRef<HTMLDivElement>(null)\n  const [isAutoScrollEnabled, setIsAutoScrollEnabled] = useState(true)\n  const scrollTimeout = useRef<ReturnType<typeof setTimeout> | null>(null)\n  const lastScrollTop = useRef(0)\n  const lastScrollHeight = useRef(0)\n  const isScrollingProgrammatically = useRef(false)\n  const previousResetKey = useRef<string | null | undefined>(resetKey)\n  const pendingResetScroll = useRef(false)\n\n  const isAtBottom = useCallback(() => {\n    const container = containerRef.current\n    if (!container) return false\n\n    const { scrollTop, scrollHeight, clientHeight } = container\n    return scrollHeight - scrollTop - clientHeight <= threshold\n  }, [threshold])\n\n  const scrollToBottom = useCallback((smooth: boolean = false) => {\n    const container = containerRef.current\n    if (!container) return\n\n    isScrollingProgrammatically.current = true\n\n    container.scrollTo({\n      top: container.scrollHeight,\n      behavior: smooth ? \"smooth\" : \"auto\"\n    })\n\n    lastScrollTop.current = container.scrollTop\n    lastScrollHeight.current = container.scrollHeight\n\n    setTimeout(\n      () => {\n        isScrollingProgrammatically.current = false\n      },\n      smooth ? 300 : 50\n    )\n  }, [])\n\n  useEffect(() => {\n    const container = containerRef.current\n    if (!container) return\n\n    const handleScroll = () => {\n      if (isScrollingProgrammatically.current) return\n\n      const { scrollTop, scrollHeight } = container\n      const isScrollingUp = scrollTop < lastScrollTop.current\n\n      lastScrollTop.current = scrollTop\n      lastScrollHeight.current = scrollHeight\n\n      if (isScrollingUp) {\n        setIsAutoScrollEnabled(false)\n      }\n\n      if (scrollTimeout.current) {\n        clearTimeout(scrollTimeout.current)\n      }\n\n      scrollTimeout.current = setTimeout(() => {\n        if (isAtBottom()) {\n          setIsAutoScrollEnabled(true)\n        }\n      }, 300)\n    }\n\n    container.addEventListener(\"scroll\", handleScroll, { passive: true })\n\n    return () => {\n      container.removeEventListener(\"scroll\", handleScroll)\n      if (scrollTimeout.current) {\n        clearTimeout(scrollTimeout.current)\n      }\n    }\n  }, [isAtBottom])\n\n  useEffect(() => {\n    if (streaming && isAutoScrollEnabled) {\n      requestAnimationFrame(() => {\n        scrollToBottom(false)\n      })\n    }\n  }, [streaming, isAutoScrollEnabled, scrollToBottom])\n\n  useEffect(() => {\n    if (previousResetKey.current !== resetKey) {\n      previousResetKey.current = resetKey\n      pendingResetScroll.current = true\n      setIsAutoScrollEnabled(true)\n    }\n  }, [resetKey])\n\n  useEffect(() => {\n    if (!pendingResetScroll.current || messages.length === 0) {\n      return\n    }\n\n    requestAnimationFrame(() => {\n      requestAnimationFrame(() => {\n        scrollToBottom(false)\n        pendingResetScroll.current = false\n      })\n    })\n  }, [messages, scrollToBottom])\n\n  useEffect(() => {\n    if (messages.length === 0) {\n      setIsAutoScrollEnabled(true)\n      return\n    }\n\n    if (isAutoScrollEnabled && !isAtBottom()) {\n      requestAnimationFrame(() => {\n        scrollToBottom(!streaming)\n      })\n    }\n  }, [messages, isAutoScrollEnabled, scrollToBottom, streaming, isAtBottom])\n\n  const autoScrollToBottom = useCallback(() => {\n    setIsAutoScrollEnabled(true)\n    scrollToBottom(true)\n  }, [scrollToBottom])\n\n  return {\n    containerRef,\n    isAutoScrollToBottom: isAutoScrollEnabled && isAtBottom(),\n    autoScrollToBottom\n  }\n}\n"
  },
  {
    "path": "src/hooks/useSmartScroll2.tsx",
    "content": "/*\n* This is old code i just wanted to keep for reference.\n*/\n\nimport { useRef, useEffect, useState, useCallback } from \"react\"\n\nexport const useSmartScroll = (messages: any[], streaming: boolean, threshold: number = 50) => {\n  const containerRef = useRef<HTMLDivElement>(null)\n  const [isAtBottom, setIsAtBottom] = useState(true)\n  const [shouldAutoScroll, setShouldAutoScroll] = useState(true)\n  const lastMessageCount = useRef(0)\n  const scrollTimeout = useRef<NodeJS.Timeout | null>(null)\n\n  const checkIfAtBottom = useCallback(() => {\n    if (!containerRef.current) return false\n    const { scrollTop, scrollHeight, clientHeight } = containerRef.current\n    return scrollHeight - scrollTop - clientHeight <= threshold\n  }, [threshold])\n\n  const updateScrollStates = useCallback(() => {\n    const atBottom = checkIfAtBottom()\n    setIsAtBottom(atBottom)\n    \n    if (atBottom) {\n      setShouldAutoScroll(true)\n    }\n  }, [checkIfAtBottom])\n\n  const scrollToBottom = useCallback((smooth: boolean = true) => {\n    if (!containerRef.current) return\n    \n    containerRef.current.scrollTo({\n      top: containerRef.current.scrollHeight,\n      behavior: smooth ? \"smooth\" : \"auto\"\n    })\n  }, [])\n\n  // Handle scroll events\n  useEffect(() => {\n    const container = containerRef.current\n    if (!container) return\n\n    const handleScroll = () => {\n      // Clear existing timeout\n      if (scrollTimeout.current) {\n        clearTimeout(scrollTimeout.current)\n      }\n\n      // Debounce scroll events\n      scrollTimeout.current = setTimeout(() => {\n        const atBottom = checkIfAtBottom()\n        setIsAtBottom(atBottom)\n        \n        if (atBottom) {\n          setShouldAutoScroll(true)\n        } else {\n          // Only disable auto-scroll if user actively scrolled up\n          setShouldAutoScroll(false)\n        }\n      }, 100)\n    }\n\n    container.addEventListener(\"scroll\", handleScroll, { passive: true })\n    \n    // Initial check\n    updateScrollStates()\n\n    return () => {\n      container.removeEventListener(\"scroll\", handleScroll)\n      if (scrollTimeout.current) {\n        clearTimeout(scrollTimeout.current)\n      }\n    }\n  }, [checkIfAtBottom, updateScrollStates])\n\n  // Auto-scroll when new messages arrive\n  useEffect(() => {\n    const hasNewMessages = messages.length > lastMessageCount.current\n    lastMessageCount.current = messages.length\n\n    if (messages.length === 0) {\n      setShouldAutoScroll(true)\n      setIsAtBottom(true)\n      return\n    }\n\n    if (shouldAutoScroll && hasNewMessages) {\n      // Use setTimeout to ensure DOM is updated\n      setTimeout(() => {\n        scrollToBottom(false)\n        // Update states after scroll\n        setTimeout(() => {\n          updateScrollStates()\n        }, 50)\n      }, 0)\n    } else {\n      // Still update states even if not auto-scrolling\n      setTimeout(updateScrollStates, 50)\n    }\n  }, [messages, shouldAutoScroll, scrollToBottom, updateScrollStates])\n\n  // Enable auto-scroll when streaming starts if at bottom\n  useEffect(() => {\n    if (streaming && isAtBottom) {\n      setShouldAutoScroll(true)\n    }\n  }, [streaming, isAtBottom])\n\n  const autoScrollToBottom = useCallback(() => {\n    setShouldAutoScroll(true)\n    setIsAtBottom(true)\n    scrollToBottom(true)\n  }, [scrollToBottom])\n\n  return { \n    containerRef, \n    isAutoScrollToBottom: isAtBottom && shouldAutoScroll, \n    autoScrollToBottom \n  }\n}"
  },
  {
    "path": "src/hooks/useSpeechRecognition.tsx",
    "content": "import { useRef, useEffect, useState, useCallback } from \"react\"\n\ntype SpeechRecognitionEvent = {\n  results: SpeechRecognitionResultList\n  resultIndex: number\n}\n\ndeclare global {\n  interface SpeechRecognitionErrorEvent extends Event {\n    //@ts-ignore\n    error: any\n  }\n  interface Window {\n    SpeechRecognition: any\n    webkitSpeechRecognition: any\n  }\n}\n\ntype SpeechRecognition = {\n  lang: string\n  interimResults: boolean\n  continuous: boolean\n  maxAlternatives: number\n  grammars: any\n  onresult: (event: SpeechRecognitionEvent) => void\n  onerror: (event: Event) => void\n  onend: () => void\n  start: () => void\n  stop: () => void\n}\n\ntype SpeechRecognitionProps = {\n  onEnd?: () => void\n  onResult?: (transcript: string) => void\n  onError?: (event: Event) => void\n  autoStop?: boolean\n  autoStopTimeout?: number\n  autoSubmit?: boolean\n}\n\ntype ListenArgs = {\n  lang?: string\n  interimResults?: boolean\n  continuous?: boolean\n  maxAlternatives?: number\n  grammars?: any\n  autoStop?: boolean\n  autoStopTimeout?: number\n  autoSubmit?: boolean\n}\n\ntype SpeechRecognitionHook = {\n  start: (args?: ListenArgs) => void\n  isListening: boolean\n  stop: () => void\n  supported: boolean\n  transcript: string\n  resetTranscript: () => void\n}\n\nconst useEventCallback = <T extends (...args: any[]) => any>(\n  fn: T,\n  dependencies: any[]\n) => {\n  const ref = useRef<T>()\n\n  useEffect(() => {\n    ref.current = fn\n  }, [fn, ...dependencies])\n\n  return useCallback(\n    (...args: Parameters<T>) => {\n      const fn = ref.current\n      return fn!(...args)\n    },\n    [ref]\n  )\n}\n\nexport const useSpeechRecognition = (\n  props: SpeechRecognitionProps = {}\n): SpeechRecognitionHook => {\n  const {\n    onEnd = () => {},\n    onResult = () => {},\n    onError = () => {},\n    autoStop = false,\n    autoStopTimeout = 5000,\n    autoSubmit = false\n  } = props\n\n  const recognition = useRef<SpeechRecognition | null>(null)\n  const [listening, setListening] = useState<boolean>(false)\n  const [supported, setSupported] = useState<boolean>(false)\n  const [liveTranscript, setLiveTranscript] = useState<string>(\"\")\n  const silenceTimer = useRef<NodeJS.Timeout | null>(null)\n  const lastTranscriptRef = useRef<string>(\"\")\n\n  useEffect(() => {\n    if (typeof window === \"undefined\") return\n    window.SpeechRecognition =\n      window.SpeechRecognition || window.webkitSpeechRecognition\n    if (window.SpeechRecognition) {\n      setSupported(true)\n      recognition.current = new window.SpeechRecognition()\n    }\n  }, [])\n\n  const resetTranscript = () => {\n    setLiveTranscript(\"\")\n    lastTranscriptRef.current = \"\"\n  }\n\n  const processResult = (\n    event: SpeechRecognitionEvent,\n    shouldAutoStop: boolean,\n    shouldAutoSubmit: boolean\n  ) => {\n    const transcript = Array.from(event.results)\n      .map((result) => result[0])\n      .map((result) => result.transcript)\n      .join(\"\")\n\n    onResult(transcript)\n\n    // Reset silence timer if transcript changed\n    if (shouldAutoStop && transcript !== lastTranscriptRef.current) {\n      lastTranscriptRef.current = transcript\n\n      if (silenceTimer.current) {\n        clearTimeout(silenceTimer.current)\n      }\n\n      silenceTimer.current = setTimeout(() => {\n        stop()\n        if (shouldAutoSubmit) {\n          // Submit the final transcript\n          onResult(transcript)\n        }\n      }, autoStopTimeout)\n    }\n  }\n\n  const handleError = (event: Event) => {\n    if ((event as SpeechRecognitionErrorEvent).error === \"not-allowed\") {\n      if (recognition.current) {\n        recognition.current.onend = null\n      }\n      setListening(false)\n    }\n    onError(event)\n  }\n\n  const listen = useEventCallback(\n    (args: ListenArgs = {}) => {\n      if (listening || !supported) return\n      const {\n        lang = \"\",\n        interimResults = true,\n        continuous = false,\n        maxAlternatives = 1,\n        grammars,\n        autoStop: argAutoStop = autoStop,\n        autoStopTimeout: argAutoStopTimeout = autoStopTimeout,\n        autoSubmit: argAutoSubmit = autoSubmit\n      } = args\n\n      setListening(true)\n      setLiveTranscript(\"\")\n      lastTranscriptRef.current = \"\"\n\n      if (recognition.current) {\n        recognition.current.lang = lang\n        recognition.current.interimResults = interimResults\n        recognition.current.onresult = (event) => {\n          processResult(event, argAutoStop, argAutoSubmit)\n          const transcript = Array.from(event.results)\n            .map((result) => result[0])\n            .map((result) => result.transcript)\n            .join(\"\")\n          setLiveTranscript(transcript)\n        }\n        recognition.current.onerror = handleError\n        recognition.current.continuous = continuous\n        recognition.current.maxAlternatives = maxAlternatives\n\n        if (grammars) {\n          recognition.current.grammars = grammars\n        }\n        recognition.current.onend = () => {\n          if (recognition.current && !argAutoStop) {\n            recognition.current.start()\n          } else {\n            onEnd()\n          }\n        }\n        if (recognition.current) {\n          recognition.current.start()\n        }\n      }\n    },\n    [listening, supported, recognition, autoStop, autoStopTimeout, autoSubmit]\n  )\n\n  const stop = useEventCallback(() => {\n    if (!listening || !supported) return\n\n    if (silenceTimer.current) {\n      clearTimeout(silenceTimer.current)\n      silenceTimer.current = null\n    }\n\n    if (recognition.current) {\n      recognition.current.onresult = null\n      recognition.current.onend = null\n      recognition.current.onerror = null\n      setListening(false)\n      recognition.current.stop()\n    }\n    onEnd()\n  }, [listening, supported, recognition, onEnd])\n\n  // Clean up timer on unmount\n  useEffect(() => {\n    return () => {\n      if (silenceTimer.current) {\n        clearTimeout(silenceTimer.current)\n      }\n    }\n  }, [])\n\n  return {\n    start: listen,\n    isListening: listening,\n    stop,\n    supported,\n    transcript: liveTranscript,\n    resetTranscript\n  }\n}\n"
  },
  {
    "path": "src/hooks/useTTS.tsx",
    "content": "import { useEffect, useState } from \"react\"\nimport { notification } from \"antd\"\nimport {\n  getElevenLabsApiKey,\n  getElevenLabsModel,\n  getElevenLabsVoiceId,\n  getRemoveReasoningTagTTS,\n  getTTSProvider,\n  getVoice,\n  isSSMLEnabled,\n  getSpeechPlaybackSpeed\n} from \"@/services/tts\"\nimport { markdownToSSML } from \"@/utils/markdown-to-ssml\"\nimport { generateSpeech } from \"@/services/elevenlabs\"\nimport { splitMessageContent } from \"@/utils/tts\"\nimport { removeReasoning } from \"@/libs/reasoning\"\nimport { markdownToText } from \"@/utils/markdown-to-text\"\nimport { generateOpenAITTS } from \"@/services/openai-tts\"\n\nexport interface VoiceOptions {\n  utterance: string\n}\n\nexport const useTTS = () => {\n  const [isSpeaking, setIsSpeaking] = useState(false)\n  const [audioElement, setAudioElement] = useState<HTMLAudioElement | null>(\n    null\n  )\n\n  const speak = async ({ utterance }: VoiceOptions) => {\n    try {\n      const voice = await getVoice()\n      const provider = await getTTSProvider()\n      const isRemoveReasoning = await getRemoveReasoningTagTTS()\n      const playbackSpeed = await getSpeechPlaybackSpeed()\n\n      if (isRemoveReasoning) {\n        utterance = removeReasoning(utterance)\n      }\n      const isSSML = await isSSMLEnabled()\n      if (isSSML) {\n        utterance = markdownToSSML(utterance)\n      } else {\n        utterance = markdownToText(utterance)\n      }\n      if (provider === \"browser\") {\n        if (\n          import.meta.env.BROWSER === \"chrome\" ||\n          import.meta.env.BROWSER === \"edge\"\n        ) {\n          chrome.tts.speak(utterance, {\n            voiceName: voice,\n            rate: playbackSpeed,\n            onEvent(event) {\n              if (event.type === \"start\") {\n                setIsSpeaking(true)\n              } else if (event.type === \"end\") {\n                setIsSpeaking(false)\n              }\n            }\n          })\n        } else {\n          const synthesisUtterance = new SpeechSynthesisUtterance(utterance)\n          synthesisUtterance.rate = playbackSpeed\n          synthesisUtterance.onstart = () => {\n            setIsSpeaking(true)\n          }\n          synthesisUtterance.onend = () => {\n            setIsSpeaking(false)\n          }\n          const voices = window.speechSynthesis.getVoices()\n          const selectedVoice = voices.find((v) => v.name === voice)\n          if (selectedVoice) {\n            synthesisUtterance.voice = selectedVoice\n          } else {\n            window.speechSynthesis.onvoiceschanged = () => {\n              const updatedVoices = window.speechSynthesis.getVoices()\n              const newVoice = updatedVoices.find((v) => v.name === voice)\n              if (newVoice) {\n                synthesisUtterance.voice = newVoice\n              }\n            }\n          }\n          window.speechSynthesis.speak(synthesisUtterance)\n        }\n      } else if (provider === \"elevenlabs\") {\n        const apiKey = await getElevenLabsApiKey()\n        const modelId = await getElevenLabsModel()\n        const voiceId = await getElevenLabsVoiceId()\n        const sentences = splitMessageContent(utterance)\n        \n        if (!apiKey || !modelId || !voiceId) {\n          throw new Error(\"Missing ElevenLabs configuration\")\n        }\n\n        let nextAudioData: ArrayBuffer | null = null\n        let nextAudioPromise: Promise<ArrayBuffer> | null = null\n\n        for (let i = 0; i < sentences.length; i++) {\n          setIsSpeaking(true)\n\n          let currentAudioData: ArrayBuffer\n          if (nextAudioData) {\n            currentAudioData = nextAudioData\n            nextAudioData = null\n          } else {\n            currentAudioData = await generateSpeech(apiKey, sentences[i], voiceId, modelId)\n          }\n\n          if (i < sentences.length - 1) {\n            nextAudioPromise = generateSpeech(apiKey, sentences[i + 1], voiceId, modelId)\n          }\n\n          const blob = new Blob([currentAudioData], { type: \"audio/mpeg\" })\n          const url = URL.createObjectURL(blob)\n          const audio = new Audio(url)\n          audio.playbackRate = playbackSpeed\n          setAudioElement(audio)\n\n          await Promise.all([\n            new Promise((resolve) => {\n              audio.onended = resolve\n              audio.play()\n            }),\n            nextAudioPromise?.then((data) => {\n              nextAudioData = data\n            }).catch(console.error) || Promise.resolve()\n          ])\n\n          URL.revokeObjectURL(url)\n        }\n\n        setIsSpeaking(false)\n        setAudioElement(null)\n      } else if (provider === \"openai\") {\n        const sentences = splitMessageContent(utterance)\n        \n        let nextAudioData: ArrayBuffer | null = null\n        let nextAudioPromise: Promise<ArrayBuffer> | null = null\n\n        for (let i = 0; i < sentences.length; i++) {\n          setIsSpeaking(true)\n\n          let currentAudioData: ArrayBuffer\n          if (nextAudioData) {\n            currentAudioData = nextAudioData\n            nextAudioData = null\n          } else {\n            currentAudioData = await generateOpenAITTS({\n              text: sentences[i]\n            })\n          }\n\n          // Start fetching next audio in parallel (if there's a next sentence)\n          if (i < sentences.length - 1) {\n            nextAudioPromise = generateOpenAITTS({\n              text: sentences[i + 1]\n            })\n          }\n\n          // Play current audio\n          const blob = new Blob([currentAudioData], { type: \"audio/mpeg\" })\n          const url = URL.createObjectURL(blob)\n          const audio = new Audio(url)\n          audio.playbackRate = playbackSpeed\n          setAudioElement(audio)\n\n          await Promise.all([\n            new Promise((resolve) => {\n              audio.onended = resolve\n              audio.play()\n            }),\n            nextAudioPromise?.then((data) => {\n              nextAudioData = data\n            }).catch(console.error) || Promise.resolve()\n          ])\n\n          URL.revokeObjectURL(url)\n        }\n\n        setIsSpeaking(false)\n        setAudioElement(null)\n      }\n    } catch (error) {\n      setIsSpeaking(false)\n      setAudioElement(null)\n      notification.error({\n        message: \"Error\",\n        description: \"Something went wrong while trying to play the audio\"\n      })\n    }\n  }\n\n  const cancel = () => {\n    if (audioElement) {\n      audioElement.pause()\n      audioElement.currentTime = 0\n      setAudioElement(null)\n      setIsSpeaking(false)\n      return\n    }\n\n    if (\n      import.meta.env.BROWSER === \"chrome\" ||\n      import.meta.env.BROWSER === \"edge\"\n    ) {\n      chrome.tts.stop()\n    } else {\n      window.speechSynthesis.cancel()\n    }\n    setIsSpeaking(false)\n  }\n\n  useEffect(() => {\n    return () => {\n      cancel()\n    }\n  }, [])\n\n  return {\n    speak,\n    cancel,\n    isSpeaking\n  }\n}\n"
  },
  {
    "path": "src/hooks/useTabMentions.ts",
    "content": "import React from \"react\"\nimport { useStorage } from \"@plasmohq/storage/hook\"\n\nexport interface TabInfo {\n  id: number\n  title: string\n  url: string\n  favIconUrl?: string\n}\n\nexport interface MentionPosition {\n  start: number\n  end: number\n  query: string\n}\n\nexport const useTabMentions = (textareaRef: React.RefObject<HTMLTextAreaElement>) => {\n  const [tabMentionsEnabled] = useStorage(\"tabMentionsEnabled\", false)\n  const [showMentions, setShowMentions] = React.useState(false)\n  const [mentionPosition, setMentionPosition] = React.useState<MentionPosition | null>(null)\n  const [availableTabs, setAvailableTabs] = React.useState<TabInfo[]>([])\n  const [filteredTabs, setFilteredTabs] = React.useState<TabInfo[]>([])\n  const [selectedDocuments, setSelectedDocuments] = React.useState<TabInfo[]>([])\n\n  const fetchTabs = React.useCallback(async () => {\n    try {\n      const tabs = await browser.tabs.query({})\n      const tabInfos: TabInfo[] = tabs\n        .filter(tab => tab.id && tab.title && tab.url)\n        .filter(tab => !tab.active)\n        .filter(tab => tab?.status === 'complete') \n        .filter(tab => {\n          const url = tab.url!.toLowerCase()\n          return !url.startsWith('chrome://') &&\n            !url.startsWith('edge://') &&\n            !url.startsWith('brave://') &&\n            !url.startsWith('firefox://') &&\n            !url.startsWith('chrome-extension://') &&\n            !url.startsWith('moz-extension://')\n        })\n        .map(tab => ({\n          id: tab.id!,\n          title: tab.title!,\n          url: tab.url!,\n          favIconUrl: tab.favIconUrl\n        }))\n      setAvailableTabs(tabInfos)\n      return tabInfos\n    } catch (error) {\n      console.error(\"Failed to fetch tabs:\", error)\n      return []\n    }\n  }, [])\n\n  const detectMention = React.useCallback((text: string, cursorPosition: number) => {\n    if (!tabMentionsEnabled) return null\n\n    // Find the last @ before cursor position\n    const beforeCursor = text.substring(0, cursorPosition)\n    const lastAtIndex = beforeCursor.lastIndexOf(\"@\")\n\n    if (lastAtIndex === -1) return null\n\n    const charBeforeAt = lastAtIndex > 0 ? beforeCursor[lastAtIndex - 1] : \" \"\n    if (charBeforeAt !== \" \" && lastAtIndex !== 0) return null\n\n    const afterAt = text.substring(lastAtIndex + 1, cursorPosition)\n\n    if (afterAt.includes(\" \")) return null\n\n    return {\n      start: lastAtIndex,\n      end: cursorPosition,\n      query: afterAt.toLowerCase()\n    }\n  }, [tabMentionsEnabled])\n\n  const handleTextChange = React.useCallback(async (text: string, cursorPosition: number) => {\n    if (!tabMentionsEnabled) {\n      setShowMentions(false)\n      return\n    }\n\n    const mention = detectMention(text, cursorPosition)\n\n    if (mention) {\n      setMentionPosition(mention)\n\n      let tabs = availableTabs\n      if (tabs.length === 0) {\n        tabs = await fetchTabs()\n      }\n\n      const filtered = tabs.filter(tab =>\n        tab.title.toLowerCase().includes(mention.query) ||\n        tab.url.toLowerCase().includes(mention.query)\n      )\n\n      setFilteredTabs(filtered)\n      setShowMentions(true)\n    } else {\n      setShowMentions(false)\n      setMentionPosition(null)\n    }\n  }, [tabMentionsEnabled, availableTabs, detectMention, fetchTabs])\n\n  // New function to handle when mentions dropdown opens\n  const handleMentionsOpen = React.useCallback(async () => {\n    if (tabMentionsEnabled && showMentions) {\n      // Always fetch fresh tabs when dropdown opens\n      await fetchTabs()\n    }\n  }, [tabMentionsEnabled, showMentions, fetchTabs])\n\n  const insertMention = React.useCallback((tab: TabInfo, currentText: string, setValue: (value: string) => void) => {\n    if (!mentionPosition || !textareaRef.current) return\n\n    if (selectedDocuments.find(doc => doc.id === tab.id)) {\n      setShowMentions(false)\n      setMentionPosition(null)\n      return\n    }\n\n    setSelectedDocuments(prev => [...prev, tab])\n\n    const before = currentText.substring(0, mentionPosition.start)\n    const after = currentText.substring(mentionPosition.end)\n    const newText = before + after\n    setValue(newText)\n\n    setTimeout(() => {\n      if (textareaRef.current) {\n        textareaRef.current.focus()\n        textareaRef.current.setSelectionRange(mentionPosition.start, mentionPosition.start)\n      }\n    }, 0)\n\n    setShowMentions(false)\n    setMentionPosition(null)\n  }, [mentionPosition, textareaRef, selectedDocuments])\n\n  const closeMentions = React.useCallback(() => {\n    setShowMentions(false)\n    setMentionPosition(null)\n  }, [])\n\n  const removeDocument = React.useCallback((id: number) => {\n    setSelectedDocuments(prev => prev.filter(doc => doc.id !== id))\n  }, [])\n\n  const clearSelectedDocuments = React.useCallback(() => {\n    setSelectedDocuments([])\n  }, [])\n\n  return {\n    tabMentionsEnabled,\n    showMentions,\n    mentionPosition,\n    filteredTabs,\n    selectedDocuments,\n    handleTextChange,\n    insertMention,\n    closeMentions,\n    removeDocument,\n    clearSelectedDocuments,\n    reloadTabs: fetchTabs,\n    handleMentionsOpen  \n  }\n}\n"
  },
  {
    "path": "src/hooks/utils/messageHelpers.ts",
    "content": "import { notification } from \"antd\"\nimport { useTranslation } from \"react-i18next\"\nimport {\n  saveMessageOnError as saveError,\n  saveMessageOnSuccess as saveSuccess\n} from \"../chat-helper\"\n\nexport const focusTextArea = (textareaRef?: React.RefObject<HTMLTextAreaElement>) => {\n  try {\n    if (textareaRef?.current) {\n      textareaRef.current.focus()\n    } else {\n      const textareaElement = document.getElementById(\n        \"textarea-message\"\n      ) as HTMLTextAreaElement\n      if (textareaElement) {\n        textareaElement.focus()\n      }\n    }\n  } catch (e) { }\n}\n\nexport const validateBeforeSubmit = (selectedModel: string, t: any) => {\n  if (!selectedModel || selectedModel?.trim()?.length === 0) {\n    notification.error({\n      message: t(\"error\"),\n      description: t(\"validationSelectModel\")\n    })\n    return false\n  }\n\n  return true\n}\n\nexport const createSaveMessageOnSuccess = (temporaryChat: boolean, setHistoryId: (id: string) => void) => {\n  return async (e: any): Promise<string | null> => {\n    if (!temporaryChat) {\n      return await saveSuccess(e)\n    } else {\n      setHistoryId(\"temp\")\n      return null\n    }\n  }\n}\n\nexport const createSaveMessageOnError = (\n  temporaryChat: boolean,\n  history: any,\n  setHistory: (history: any) => void,\n  setHistoryId: (id: string) => void\n) => {\n  return async (e: any): Promise<string | null> => {\n    if (!temporaryChat) {\n      return await saveError(e)\n    } else {\n      setHistory([\n        ...history,\n        {\n          role: \"user\",\n          content: e.userMessage,\n          image: e.image,\n          images: e.images\n        },\n        {\n          role: \"assistant\",\n          content: e.botMessage\n        }\n      ])\n\n      setHistoryId(\"temp\")\n      return null\n    }\n  }\n}\n"
  },
  {
    "path": "src/i18n/index.ts",
    "content": "import i18n from \"i18next\";\nimport { initReactI18next } from \"react-i18next\";\nimport { en } from \"./lang/en\";\nimport { pt } from \"./lang/pt\";\nimport { fr } from \"./lang/fr\";\nimport { uk } from \"./lang/uk\";\nimport { ru } from \"./lang/ru\";\nimport { ml } from \"./lang/ml\";\nimport { zh } from \"./lang/zh\";\nimport { zhTW } from \"./lang/zh-TW\";\nimport { ja } from \"./lang/ja\";\nimport { it } from \"./lang/it\";\nimport { es } from \"./lang/es\";\nimport { fa } from \"./lang/fa\";\nimport { de } from \"./lang/de\";\nimport { da } from \"./lang/da\";\nimport { no } from \"./lang/no\";\nimport { sv } from \"./lang/sv\";\nimport { ko } from \"./lang/ko\";\nimport { ar } from \"./lang/ar\"\n\n\ni18n\n    .use(initReactI18next)\n    .init({\n        resources: {\n            en: en,\n            es: es,\n            fr: fr,\n            \"it\": it,\n            ml: ml,\n            \"pt-BR\": pt,\n            \"zh-CN\": zh,\n            uk: uk,\n            \"uk-UA\": uk,\n            ru: ru,\n            \"ru-RU\": ru,\n            zh: zh,\n            \"zh-TW\": zhTW,\n            ja: ja,\n            \"ja-JP\": ja,\n            fa: fa,\n            \"fa-IR\": fa,\n            da: da,\n            no: no,\n            de: de,\n            sv: sv,\n            ko: ko,\n            ar: ar\n        },\n        fallbackLng: \"en\",\n        lng: localStorage.getItem(\"i18nextLng\") || \"en\",\n    });\n\nexport default i18n;"
  },
  {
    "path": "src/i18n/lang/ar.ts",
    "content": "import option from \"@/assets/locale/ar/option.json\";\nimport playground from \"@/assets/locale/ar/playground.json\";\nimport common from \"@/assets/locale/ar/common.json\";\nimport sidepanel from \"@/assets/locale/ar/sidepanel.json\";\nimport settings from \"@/assets/locale/ar/settings.json\";\nimport knowledge from \"@/assets/locale/ar/knowledge.json\";\nimport chrome from \"@/assets/locale/ar/chrome.json\";\nimport openai from \"@/assets/locale/ar/openai.json\";\n\nexport const ar = {\n    option,\n    playground,\n    common,\n    sidepanel,\n    settings,\n    knowledge,\n    chrome,\n    openai\n}"
  },
  {
    "path": "src/i18n/lang/da.ts",
    "content": "import option from \"@/assets/locale/da/option.json\";\nimport playground from \"@/assets/locale/da/playground.json\";\nimport common from \"@/assets/locale/da/common.json\";\nimport sidepanel from \"@/assets/locale/da/sidepanel.json\";\nimport settings from \"@/assets/locale/da/settings.json\";\nimport knowledge from \"@/assets/locale/da/knowledge.json\";\nimport chrome from \"@/assets/locale/da/chrome.json\";\nimport openai from \"@/assets/locale/da/openai.json\";\n\nexport const da = {\n    option,\n    playground,\n    common,\n    sidepanel,\n    settings,\n    knowledge,\n    chrome,\n    openai\n}"
  },
  {
    "path": "src/i18n/lang/de.ts",
    "content": "import option from \"@/assets/locale/de/option.json\";\nimport playground from \"@/assets/locale/de/playground.json\";\nimport common from \"@/assets/locale/de/common.json\";\nimport sidepanel from \"@/assets/locale/de/sidepanel.json\";\nimport settings from \"@/assets/locale/de/settings.json\";\nimport knowledge from \"@/assets/locale/de/knowledge.json\";\nimport chrome from \"@/assets/locale/de/chrome.json\";\nimport openai from \"@/assets/locale/de/openai.json\";\n\nexport const de = {\n    option,\n    playground,\n    common,\n    sidepanel,\n    settings,\n    knowledge,\n    chrome,\n    openai\n}"
  },
  {
    "path": "src/i18n/lang/en.ts",
    "content": "import option from \"@/assets/locale/en/option.json\";\nimport playground from \"@/assets/locale/en/playground.json\";\nimport common from \"@/assets/locale/en/common.json\";\nimport sidepanel from \"@/assets/locale/en/sidepanel.json\";\nimport settings from \"@/assets/locale/en/settings.json\";\nimport knowledge from \"@/assets/locale/en/knowledge.json\";\nimport chrome from \"@/assets/locale/en/chrome.json\";\nimport openai from \"@/assets/locale/en/openai.json\";\n\nexport const en = {\n    option,\n    playground,\n    common,\n    sidepanel,\n    settings,\n    knowledge,\n    chrome,\n    openai\n}"
  },
  {
    "path": "src/i18n/lang/es.ts",
    "content": "import option from \"@/assets/locale/es/option.json\";\nimport playground from \"@/assets/locale/es/playground.json\";\nimport common from \"@/assets/locale/es/common.json\";\nimport sidepanel from \"@/assets/locale/es/sidepanel.json\";\nimport settings from \"@/assets/locale/es/settings.json\";\nimport knowledge from \"@/assets/locale/es/knowledge.json\";\nimport chrome from \"@/assets/locale/es/chrome.json\";\nimport openai from \"@/assets/locale/es/openai.json\";\n\nexport const es = {\n    option,\n    playground,\n    common,\n    sidepanel,\n    settings,\n    knowledge,\n    chrome,\n    openai\n}\n"
  },
  {
    "path": "src/i18n/lang/fa.ts",
    "content": "import option from \"@/assets/locale/fa/option.json\"\nimport playground from \"@/assets/locale/fa/playground.json\"\nimport common from \"@/assets/locale/fa/common.json\"\nimport sidepanel from \"@/assets/locale/fa/sidepanel.json\"\nimport settings from \"@/assets/locale/fa/settings.json\"\nimport knowledge from \"@/assets/locale/fa/knowledge.json\"\nimport chrome from \"@/assets/locale/fa/chrome.json\"\nimport openai from \"@/assets/locale/fa/openai.json\";\n\nexport const fa = {\n    option,\n    playground,\n    common,\n    sidepanel,\n    settings,\n    knowledge,\n    chrome,\n    openai\n}\n"
  },
  {
    "path": "src/i18n/lang/fr.ts",
    "content": "import option from \"@/assets/locale/fr/option.json\";\nimport playground from \"@/assets/locale/fr/playground.json\";\nimport common from \"@/assets/locale/fr/common.json\";\nimport sidepanel from \"@/assets/locale/fr/sidepanel.json\";\nimport settings from \"@/assets/locale/fr/settings.json\";\nimport knowledge from \"@/assets/locale/fr/knowledge.json\";\nimport chrome from \"@/assets/locale/fr/chrome.json\";\nimport openai from \"@/assets/locale/fr/openai.json\";\n\nexport const fr = {\n    option,\n    playground,\n    common,\n    sidepanel,\n    settings,\n    knowledge,\n    chrome,\n    openai\n}"
  },
  {
    "path": "src/i18n/lang/it.ts",
    "content": "import option from \"@/assets/locale/it/option.json\";\nimport playground from \"@/assets/locale/it/playground.json\";\nimport common from \"@/assets/locale/it/common.json\";\nimport sidepanel from \"@/assets/locale/it/sidepanel.json\";\nimport settings from \"@/assets/locale/it/settings.json\";\nimport knowledge from \"@/assets/locale/it/knowledge.json\";\nimport chrome from \"@/assets/locale/it/chrome.json\";\nimport openai from \"@/assets/locale/it/openai.json\";\n\nexport const it = {\n    option,\n    playground,\n    common,\n    sidepanel,\n    settings,\n    knowledge,\n    chrome,\n    openai\n}"
  },
  {
    "path": "src/i18n/lang/ja.ts",
    "content": "import option from \"@/assets/locale/ja-JP/option.json\";\nimport playground from \"@/assets/locale/ja-JP/playground.json\";\nimport common from \"@/assets/locale/ja-JP/common.json\";\nimport sidepanel from \"@/assets/locale/ja-JP/sidepanel.json\";\nimport settings from \"@/assets/locale/ja-JP/settings.json\";\nimport knowledge from \"@/assets/locale/ja-JP/knowledge.json\";\nimport chrome from \"@/assets/locale/ja-JP/chrome.json\";\nimport openai from \"@/assets/locale/ja-JP/openai.json\";\n\n\nexport const ja = {\n    option,\n    playground,\n    common,\n    sidepanel,\n    settings,\n    knowledge,\n    chrome,\n    openai\n}"
  },
  {
    "path": "src/i18n/lang/ko.ts",
    "content": "import option from \"@/assets/locale/ko/option.json\";\nimport playground from \"@/assets/locale/ko/playground.json\";\nimport common from \"@/assets/locale/ko/common.json\";\nimport sidepanel from \"@/assets/locale/ko/sidepanel.json\";\nimport settings from \"@/assets/locale/ko/settings.json\";\nimport knowledge from \"@/assets/locale/ko/knowledge.json\";\nimport chrome from \"@/assets/locale/ko/chrome.json\";\nimport openai from \"@/assets/locale/ko/openai.json\";\n\nexport const ko = {\n    option,\n    playground,\n    common,\n    sidepanel,\n    settings,\n    knowledge,\n    chrome,\n    openai\n}"
  },
  {
    "path": "src/i18n/lang/ml.ts",
    "content": "import option from \"@/assets/locale/ml/option.json\";\nimport playground from \"@/assets/locale/ml/playground.json\";\nimport common from \"@/assets/locale/ml/common.json\";\nimport sidepanel from \"@/assets/locale/ml/sidepanel.json\";\nimport settings from \"@/assets/locale/ml/settings.json\";\nimport knowledge from \"@/assets/locale/ml/knowledge.json\";\nimport chrome from \"@/assets/locale/ml/chrome.json\";\nimport openai from \"@/assets/locale/ml/openai.json\";\n\nexport const ml = {\n    option,\n    playground,\n    common,\n    sidepanel,\n    settings,\n    knowledge,\n    chrome,\n    openai\n}"
  },
  {
    "path": "src/i18n/lang/no.ts",
    "content": "import option from \"@/assets/locale/no/option.json\";\nimport playground from \"@/assets/locale/no/playground.json\";\nimport common from \"@/assets/locale/no/common.json\";\nimport sidepanel from \"@/assets/locale/no/sidepanel.json\";\nimport settings from \"@/assets/locale/no/settings.json\";\nimport knowledge from \"@/assets/locale/no/knowledge.json\";\nimport chrome from \"@/assets/locale/no/chrome.json\";\nimport openai from \"@/assets/locale/no/openai.json\";\n\nexport const no = {\n    option,\n    playground,\n    common,\n    sidepanel,\n    settings,\n    knowledge,\n    chrome,\n    openai\n}"
  },
  {
    "path": "src/i18n/lang/pt.ts",
    "content": "import option from \"@/assets/locale/pt-BR/option.json\";\nimport playground from \"@/assets/locale/pt-BR/playground.json\";\nimport common from \"@/assets/locale/pt-BR/common.json\";\nimport sidepanel from \"@/assets/locale/pt-BR/sidepanel.json\";\nimport settings from \"@/assets/locale/pt-BR/settings.json\";\nimport knowledge from \"@/assets/locale/pt-BR/knowledge.json\";\nimport chrome from \"@/assets/locale/pt-BR/chrome.json\";\nimport openai from \"@/assets/locale/pt-BR/openai.json\";\n\nexport const pt = {\n    option,\n    playground,\n    common,\n    sidepanel,\n    settings,\n    knowledge,\n    chrome,\n    openai\n}"
  },
  {
    "path": "src/i18n/lang/ru.ts",
    "content": "import option from \"@/assets/locale/ru/option.json\";\nimport playground from \"@/assets/locale/ru/playground.json\";\nimport common from \"@/assets/locale/ru/common.json\";\nimport sidepanel from \"@/assets/locale/ru/sidepanel.json\";\nimport settings from \"@/assets/locale/ru/settings.json\";\nimport knowledge from \"@/assets/locale/ru/knowledge.json\";\nimport chrome from \"@/assets/locale/ru/chrome.json\";\nimport openai from \"@/assets/locale/ru/openai.json\";\n\nexport const ru = {\n    option,\n    playground,\n    common,\n    sidepanel,\n    settings,\n    knowledge,\n    chrome,\n    openai\n}"
  },
  {
    "path": "src/i18n/lang/sv.ts",
    "content": "import option from \"@/assets/locale/sv/option.json\";\nimport playground from \"@/assets/locale/sv/playground.json\";\nimport common from \"@/assets/locale/sv/common.json\";\nimport sidepanel from \"@/assets/locale/sv/sidepanel.json\";\nimport settings from \"@/assets/locale/sv/settings.json\";\nimport knowledge from \"@/assets/locale/sv/knowledge.json\";\nimport chrome from \"@/assets/locale/sv/chrome.json\";\nimport openai from \"@/assets/locale/sv/openai.json\";\n\nexport const sv = {\n    option,\n    playground,\n    common,\n    sidepanel,\n    settings,\n    knowledge,\n    chrome,\n    openai\n}\n"
  },
  {
    "path": "src/i18n/lang/uk.ts",
    "content": "import option from \"@/assets/locale/uk/option.json\";\nimport playground from \"@/assets/locale/uk/playground.json\";\nimport common from \"@/assets/locale/uk/common.json\";\nimport sidepanel from \"@/assets/locale/uk/sidepanel.json\";\nimport settings from \"@/assets/locale/uk/settings.json\";\nimport knowledge from \"@/assets/locale/uk/knowledge.json\";\nimport chrome from \"@/assets/locale/uk/chrome.json\";\nimport openai from \"@/assets/locale/uk/openai.json\";\n\nexport const uk = {\n    option,\n    playground,\n    common,\n    sidepanel,\n    settings,\n    knowledge,\n    chrome,\n    openai\n}"
  },
  {
    "path": "src/i18n/lang/zh-TW.ts",
    "content": "import option from \"@/assets/locale/zh-TW/option.json\";\nimport playground from \"@/assets/locale/zh-TW/playground.json\";\nimport common from \"@/assets/locale/zh-TW/common.json\";\nimport sidepanel from \"@/assets/locale/zh-TW/sidepanel.json\";\nimport settings from \"@/assets/locale/zh-TW/settings.json\";\nimport knowledge from \"@/assets/locale/zh-TW/knowledge.json\";\nimport chrome from \"@/assets/locale/zh-TW/chrome.json\";\nimport openai from \"@/assets/locale/zh-TW/openai.json\";\n\n\nexport const zhTW = {\n    option,\n    playground,\n    common,\n    sidepanel,\n    settings,\n    knowledge,\n    chrome,\n    openai\n}"
  },
  {
    "path": "src/i18n/lang/zh.ts",
    "content": "import option from \"@/assets/locale/zh/option.json\";\nimport playground from \"@/assets/locale/zh/playground.json\";\nimport common from \"@/assets/locale/zh/common.json\";\nimport sidepanel from \"@/assets/locale/zh/sidepanel.json\";\nimport settings from \"@/assets/locale/zh/settings.json\";\nimport knowledge from \"@/assets/locale/zh/knowledge.json\";\nimport chrome from \"@/assets/locale/zh/chrome.json\";\nimport openai from \"@/assets/locale/zh/openai.json\";\n\n\nexport const zh = {\n    option,\n    playground,\n    common,\n    sidepanel,\n    settings,\n    knowledge,\n    chrome,\n    openai\n}"
  },
  {
    "path": "src/i18n/support-language.ts",
    "content": "// Please add new language code to supportLanguage array\nexport const supportLanguage = [\n    {\n        label: \"English\",\n        value: \"en\"\n    },\n    {\n        label: \"Español\",\n        value: \"es\"\n    },\n    {\n        label: \"Français\",\n        value: \"fr\"\n    },\n    {\n        label: \"Italiano\",\n        value: \"it\"\n    },\n    {\n        label: \"Ukrainian\",\n        value: \"uk\"\n    },\n    {\n        label: \"Russian\",\n        value: \"ru\"\n    },\n    {\n        label: \"Português (Brasil)\",\n        value: \"pt-BR\"\n    },\n    {\n        label: \"മലയാളം\",\n        value: \"ml\"\n    },\n    {\n        label: \"简体中文\",\n        value: \"zh-CN\"\n    },\n    {\n        label: \"繁體中文\",\n        value: \"zh-TW\"\n    },\n    {\n        label: \"日本語\",\n        value: \"ja-JP\"\n    },\n    {\n        label: \"فارسی\",\n        value: \"fa\"\n    },\n    {\n        label: \"Deutsch\",\n        value: \"de\"\n    },\n    {\n        label: \"Dansk\",\n        value: \"da\"\n    },\n    {\n        label: \"Norsk\",\n        value: \"no\"\n    },\n    {\n        value: \"sv\",\n        label: \"Svenska\"\n    },\n    {\n        value: \"ko\",\n        label: \"한국어\"\n    },\n    {\n        value: \"ar\",\n        label: \"العربية\"\n    }\n]\n"
  },
  {
    "path": "src/libs/PAMemoryVectorStore.ts",
    "content": "\nimport { similarity as ml_distance_similarity } from \"ml-distance\"\nimport { VectorStore } from \"@langchain/core/vectorstores\"\nimport type { EmbeddingsInterface } from \"@langchain/core/embeddings\"\nimport { Document, DocumentInterface } from \"@langchain/core/documents\"\n\ninterface MemoryVector {\n    content: string\n    embedding: number[]\n    metadata: Record<string, any>\n}\n\ninterface MemoryVectorStoreArgs {\n    similarity?: typeof ml_distance_similarity.cosine\n}\n\nexport class PAMemoryVectorStore extends VectorStore {\n\n\n    declare FilterType: (doc: Document) => boolean\n\n    private memoryVectors: MemoryVector[] = []\n    private similarity: typeof ml_distance_similarity.cosine\n\n    constructor(embeddings: EmbeddingsInterface, args?: MemoryVectorStoreArgs) {\n        super(embeddings, args)\n        this.similarity = args?.similarity ?? ml_distance_similarity.cosine\n    }\n\n    _vectorstoreType(): string {\n        return \"memory\"\n    }\n\n    async addVectors(vectors: number[][], documents: DocumentInterface[], options?: { [x: string]: any }): Promise<void> {\n        const memoryVectors = documents.map((doc, index) => ({\n            content: doc.pageContent,\n            embedding: vectors[index],\n            metadata: doc.metadata\n        }))\n\n        this.memoryVectors.push(...memoryVectors)\n    }\n    similaritySearchVectorWithScore(query: number[], k: number, filter?: this[\"FilterType\"]): Promise<[DocumentInterface, number][]> {\n        throw new Error(\"Method not implemented.\")\n    }\n\n    async addDocuments(documents: Document[]): Promise<void> {\n        const texts = documents.map((doc) => doc.pageContent)\n        const embeddings = await this.embeddings.embedDocuments(texts)\n        await this.addVectors(embeddings, documents)\n    }\n\n    async similaritySearch(query: string, k = 4): Promise<Document[]> {\n        const queryEmbedding = await this.embeddings.embedQuery(query)\n\n        const similarities = this.memoryVectors.map((vector) => ({\n            similarity: this.similarity(queryEmbedding, vector.embedding),\n            document: vector\n        }))\n\n        similarities.sort((a, b) => b.similarity - a.similarity)\n        const topK = similarities.slice(0, k)\n\n        const docs = topK.map(({ document }) =>\n            new Document({\n                pageContent: document.content,\n                metadata: document.metadata\n            })\n        )\n\n        return docs\n    }\n\n    async similaritySearchWithScore(query: string, k = 4): Promise<[Document, number][]> {\n        const queryEmbedding = await this.embeddings.embedQuery(query)\n\n        const similarities = this.memoryVectors.map((vector) => ({\n            similarity: this.similarity(queryEmbedding, vector.embedding),\n            document: vector\n        }))\n\n        similarities.sort((a, b) => b.similarity - a.similarity)\n        const topK = similarities.slice(0, k)\n\n        return topK.map(({ document, similarity }) => [\n            new Document({\n                pageContent: document.content,\n                metadata: document.metadata\n            }),\n            similarity\n        ])\n    }\n\n    static async fromDocuments(\n        docs: Document[],\n        embeddings: EmbeddingsInterface,\n        args?: MemoryVectorStoreArgs\n    ): Promise<PAMemoryVectorStore> {\n        const store = new PAMemoryVectorStore(embeddings, args)\n        await store.addDocuments(docs)\n        return store\n    }\n}\n"
  },
  {
    "path": "src/libs/PageAssistVectorStore.ts",
    "content": "import { similarity as ml_distance_similarity } from \"ml-distance\"\nimport { VectorStore } from \"@langchain/core/vectorstores\"\nimport type { EmbeddingsInterface } from \"@langchain/core/embeddings\"\nimport { Document } from \"@langchain/core/documents\"\nimport { getVector, insertVector } from \"@/db/dexie/vector\"\nimport { getMaxContextSize } from \"@/services/kb\"\n/**\n * Interface representing a vector in memory. It includes the content\n * (text), the corresponding embedding (vector), and any associated\n * metadata.\n */\ninterface PageAssistVector {\n  content: string\n  embedding: number[]\n  metadata: Record<string, any>\n}\n\n/**\n * Interface for the arguments that can be passed to the\n * `MemoryVectorStore` constructor. It includes an optional `similarity`\n * function.\n */\nexport interface MemoryVectorStoreArgs {\n  knownledge_id: string\n  file_id?: string\n  similarity?: typeof ml_distance_similarity.cosine\n}\n\n/**\n * Class that extends `VectorStore` to store vectors in memory. Provides\n * methods for adding documents, performing similarity searches, and\n * creating instances from texts, documents, or an existing index.\n */\nexport class PageAssistVectorStore extends VectorStore {\n  declare FilterType: (doc: Document) => boolean\n\n  knownledge_id: string\n\n  file_id?: string\n\n  // In-memory storage for temp uploaded files\n  memoryVectors: PageAssistVector[] = []\n\n  similarity: typeof ml_distance_similarity.cosine\n\n  _vectorstoreType(): string {\n    return \"memory\"\n  }\n\n  constructor(embeddings: EmbeddingsInterface, args: MemoryVectorStoreArgs) {\n    super(embeddings, args)\n\n    this.similarity = args?.similarity ?? ml_distance_similarity.cosine\n\n    this.knownledge_id = args?.knownledge_id!\n\n    this.file_id = args?.file_id\n  }\n\n  /**\n   * Method to add documents to the memory vector store. It extracts the\n   * text from each document, generates embeddings for them, and adds the\n   * resulting vectors to the store.\n   * @param documents Array of `Document` instances to be added to the store.\n   * @returns Promise that resolves when all documents have been added.\n   */\n  async addDocuments(documents: Document[]): Promise<void> {\n    const texts = documents.map(({ pageContent }) => pageContent)\n    return this.addVectors(\n      await this.embeddings.embedDocuments(texts),\n      documents\n    )\n  }\n\n  /**\n   * Method to add vectors to the memory vector store. It creates\n   * `PageAssistVector` instances for each vector and document pair and adds\n   * them to the store.\n   * @param vectors Array of vectors to be added to the store.\n   * @param documents Array of `Document` instances corresponding to the vectors.\n   * @returns Promise that resolves when all vectors have been added.\n   */\n  async addVectors(vectors: number[][], documents: Document[]): Promise<void> {\n    const memoryVectors = vectors.map((embedding, idx) => ({\n      content: documents[idx].pageContent,\n      embedding,\n      metadata: documents[idx].metadata,\n      file_id: this.file_id\n    }))\n\n    // If file_id is \"temp_uploaded_files\", store in memory instead of database\n    if (this.file_id === \"temp_uploaded_files\") {\n      this.memoryVectors.push(...memoryVectors)\n    } else {\n      await insertVector(`vector:${this.knownledge_id}`, memoryVectors)\n    }\n  }\n\n  /**\n   * Method to perform a similarity search in the memory vector store. It\n   * calculates the similarity between the query vector and each vector in\n   * the store, sorts the results by similarity, and returns the top `k`\n   * results along with their scores.\n   * @param query Query vector to compare against the vectors in the store.\n   * @param k Number of top results to return.\n   * @param filter Optional filter function to apply to the vectors before performing the search.\n   * @returns Promise that resolves with an array of tuples, each containing a `Document` and its similarity score.\n   */\n  async similaritySearchVectorWithScore(\n    query: number[],\n    k: number,\n    filter?: this[\"FilterType\"]\n  ): Promise<[Document, number][]> {\n    const filterFunction = (memoryVector: PageAssistVector) => {\n      if (!filter) {\n        return true\n      }\n\n      const doc = new Document({\n        metadata: memoryVector.metadata,\n        pageContent: memoryVector.content\n      })\n      return filter(doc)\n    }\n\n    let pgVector: PageAssistVector[]\n\n    // Use memory vectors for temp uploaded files, otherwise get from database\n    if (this.file_id === \"temp_uploaded_files\") {\n      pgVector = [...this.memoryVectors]\n    } else {\n      const data = await getVector(`vector:${this.knownledge_id}`)\n      pgVector = [...data.vectors]\n    }\n\n    if (!pgVector.length) {\n      return []\n    }\n\n    const filteredMemoryVectors = pgVector.filter(filterFunction)\n    const searches = filteredMemoryVectors\n      .map((vector, index) => ({\n        similarity: this.similarity(query, vector.embedding),\n        index\n      }))\n      .sort((a, b) => (a.similarity > b.similarity ? -1 : 0))\n      .slice(0, k)\n    const result: [Document, number][] = searches.map((search) => [\n      new Document({\n        metadata: filteredMemoryVectors[search.index].metadata,\n        pageContent: filteredMemoryVectors[search.index].content\n      }),\n      search.similarity\n    ])\n    return result\n  }\n\n  async getAllPageContent() {\n    let pgVector: PageAssistVector[]\n\n    // Use memory vectors for temp uploaded files, otherwise get from database\n    if (this.file_id === \"temp_uploaded_files\") {\n      pgVector = [...this.memoryVectors]\n    } else {\n      const data = await getVector(`vector:${this.knownledge_id}`)\n      pgVector = [...data.vectors]\n    }\n\n    const maxContext = await getMaxContextSize()\n\n    let contextLength = 0\n    const pageContent: string[] = []\n    const metadata: Record<string, any>[] = []\n\n    for (let i = 0; i < pgVector.length; i++) {\n      const memoryVector = pgVector[i]\n      contextLength += memoryVector.content.length\n      if (contextLength > maxContext) {\n        break\n      }\n      pageContent.push(memoryVector.content)\n      metadata.push({\n        ...memoryVector.metadata,\n        metadata: memoryVector?.metadata,\n        pageContent: memoryVector.content\n      })\n    }\n\n    return {\n      pageContent: pageContent.join(\"\\n\\n\"),\n      metadata\n    }\n  }\n\n  /**\n   * Static method to create a `MemoryVectorStore` instance from an array of\n   * texts. It creates a `Document` for each text and metadata pair, and\n   * adds them to the store.\n   * @param texts Array of texts to be added to the store.\n   * @param metadatas Array or single object of metadata corresponding to the texts.\n   * @param embeddings `Embeddings` instance used to generate embeddings for the texts.\n   * @param dbConfig Optional `MemoryVectorStoreArgs` to configure the `MemoryVectorStore` instance.\n   * @returns Promise that resolves with a new `MemoryVectorStore` instance.\n   */\n  static async fromTexts(\n    texts: string[],\n    metadatas: object[] | object,\n    embeddings: EmbeddingsInterface,\n    dbConfig?: MemoryVectorStoreArgs\n  ): Promise<PageAssistVectorStore> {\n    const docs: Document[] = []\n    for (let i = 0; i < texts.length; i += 1) {\n      const metadata = Array.isArray(metadatas) ? metadatas[i] : metadatas\n      const newDoc = new Document({\n        pageContent: texts[i],\n        metadata\n      })\n      docs.push(newDoc)\n    }\n    return PageAssistVectorStore.fromDocuments(docs, embeddings, dbConfig)\n  }\n\n  /**\n   * Static method to create a `MemoryVectorStore` instance from an array of\n   * `Document` instances. It adds the documents to the store.\n   * @param docs Array of `Document` instances to be added to the store.\n   * @param embeddings `Embeddings` instance used to generate embeddings for the documents.\n   * @param dbConfig Optional `MemoryVectorStoreArgs` to configure the `MemoryVectorStore` instance.\n   * @returns Promise that resolves with a new `MemoryVectorStore` instance.\n   */\n  static async fromDocuments(\n    docs: Document[],\n    embeddings: EmbeddingsInterface,\n    dbConfig?: MemoryVectorStoreArgs\n  ): Promise<PageAssistVectorStore> {\n    const instance = new this(embeddings, dbConfig)\n    await instance.addDocuments(docs)\n    return instance\n  }\n\n  /**\n   * Static method to create a `MemoryVectorStore` instance from an existing\n   * index. It creates a new `MemoryVectorStore` instance without adding any\n   * documents or vectors.\n   * @param embeddings `Embeddings` instance used to generate embeddings for the documents.\n   * @param dbConfig Optional `MemoryVectorStoreArgs` to configure the `MemoryVectorStore` instance.\n   * @returns Promise that resolves with a new `MemoryVectorStore` instance.\n   */\n  static async fromExistingIndex(\n    embeddings: EmbeddingsInterface,\n    dbConfig?: MemoryVectorStoreArgs\n  ): Promise<PageAssistVectorStore> {\n    const instance = new this(embeddings, dbConfig)\n    return instance\n  }\n\n  clearMemory() {\n    this.memoryVectors = []\n  }\n\n  async similaritySearchKB(queryTxt: string, k = 4, filter = undefined) {\n    const filterFunction = (memoryVector: PageAssistVector) => {\n      if (!filter) {\n        return true\n      }\n\n      const doc = new Document({\n        metadata: memoryVector.metadata,\n        pageContent: memoryVector.content\n      })\n      return filter(doc)\n    }\n\n    let pgVector: PageAssistVector[]\n\n    // Use memory vectors for temp uploaded files, otherwise get from database\n    if (this.file_id === \"temp_uploaded_files\") {\n      pgVector = [...this.memoryVectors]\n    } else {\n      const data = await getVector(`vector:${this.knownledge_id}`)\n      pgVector = [...data.vectors]\n    }\n\n    if (!pgVector.length) {\n      return []\n    }\n\n    const query = await this.embeddings.embedQuery(queryTxt)\n\n    const filteredMemoryVectors = pgVector.filter(filterFunction)\n    const searches = filteredMemoryVectors\n      .map((vector, index) => ({\n        similarity: this.similarity(query, vector.embedding),\n        index\n      }))\n      .sort((a, b) => (a.similarity > b.similarity ? -1 : 0))\n      .slice(0, k)\n    const result: [Document, number][] = searches.map((search) => [\n      new Document({\n        metadata: filteredMemoryVectors[search.index].metadata,\n        pageContent: filteredMemoryVectors[search.index].content\n      }),\n      search.similarity\n    ])\n    return result.map((result) => result[0])\n  }\n}\n"
  },
  {
    "path": "src/libs/byte-formater.ts",
    "content": "const UNITS = [\n  \"byte\",\n  \"kilobyte\",\n  \"megabyte\",\n  \"gigabyte\",\n  \"terabyte\",\n  \"petabyte\"\n]\n\nconst getValueAndUnit = (n: number) => {\n  const i = n == 0 ? 0 : Math.floor(Math.log(n) / Math.log(1024))\n  const value = n / Math.pow(1024, i)\n  return { value, unit: UNITS[i] }\n}\n\nexport const bytePerSecondFormatter = (n: number) => {\n  const { unit, value } = getValueAndUnit(n)\n  return new Intl.NumberFormat(\"en\", {\n    notation: \"compact\",\n    style: \"unit\",\n    unit\n  }).format(value)\n}\n"
  },
  {
    "path": "src/libs/class-name.tsx",
    "content": "export const classNames = (...classes: string[]) => {\n  return classes.filter(Boolean).join(\" \")\n}\n"
  },
  {
    "path": "src/libs/clean-url.ts",
    "content": "// clean url ending if it with /\nexport const cleanUrl = (url: string) => {\n  if (url.endsWith(\"/\")) {\n    return url.slice(0, -1)\n  }\n  return url\n}\n"
  },
  {
    "path": "src/libs/export-import.ts",
    "content": "import { importChatHistory, importPrompts } from \"@/db\"\nimport {\n  exportChatHistory,\n  exportMcpServers,\n  exportModels,\n  exportNicknames,\n  exportOAIConfigs,\n  exportPrompts,\n  importChatHistoryV2,\n  importMcpServersV2,\n  importModelsV2,\n  importNicknamesV2,\n  importOAIConfigsV2,\n  importPromptsV2\n} from \"@/db/dexie/helpers\"\nimport { exportKnowledge, importKnowledgeV2 } from \"@/db/dexie/knowledge\"\nimport { db } from \"@/db/dexie/schema\"\nimport { exportVectors, importVectorsV2 } from \"@/db/dexie/vector\"\nimport { importKnowledge } from \"@/db/knowledge\"\nimport { importVectors } from \"@/db/vector\"\nimport { getStorageSyncEnabled } from \"@/services/app\"\n\nexport const formatKnowledge = (knowledge: any[]) => {\n  const kb = []\n  for (const k of knowledge) {\n    if (Array.isArray(k)) {\n      kb.push(...formatKnowledge(k))\n    } else {\n      if (k?.db_type === \"knowledge\") {\n        kb.push(k)\n      }\n    }\n  }\n\n  return kb\n}\nexport const formatVector = (vector: any[]) => {\n  const vec = []\n  for (const v of vector) {\n    if (Array.isArray(v)) {\n      vector.push(...formatVector(v))\n    } else {\n      if (v?.vectors) {\n        vec.push(v)\n      }\n    }\n  }\n  return vec\n}\nexport const exportPageAssistData = async () => {\n  const knowledge = await exportKnowledge()\n  const chat = await exportChatHistory()\n  const vector = await exportVectors()\n  const prompts = await exportPrompts()\n  const oaiConfigs = await exportOAIConfigs()\n  const nicknames = await exportNicknames()\n  const models = await exportModels()\n  const mcpServers = await exportMcpServers()\n\n  const storageLocal = await chrome.storage.local.get()\n  const storageSyncEnabled = await getStorageSyncEnabled()\n  const storageSync = storageSyncEnabled\n    ? await chrome.storage.sync.get()\n    : {}\n\n  const data = {\n    knowledge,\n    chat,\n    vector,\n    prompts,\n    oaiConfigs,\n    nicknames,\n    models,\n    mcpServers,\n    storageLocal,\n    storageSync\n  }\n\n  const dataStr = JSON.stringify(data, null, 2)\n\n  const blob = new Blob([dataStr], { type: \"application/json\" })\n  const url = URL.createObjectURL(blob)\n\n  const a = document.createElement(\"a\")\n  a.href = url\n  a.download = `page-assist-${new Date().toISOString()}.json`\n  a.click()\n  URL.revokeObjectURL(url)\n}\nexport const importPageAssistData = async (file: File) => {\n  return new Promise((resolve, reject) => {\n    const reader = new FileReader()\n    reader.onload = async () => {\n      try {\n        const data = JSON.parse(reader.result as string)\n        const options = {}\n        await db.transaction(\n          \"rw\",\n          [\n            db.chatHistories,\n            db.messages,\n            db.prompts,\n            db.knowledge,\n            db.vectors,\n            db.sessionFiles,\n            db.openaiConfigs,\n            db.modelNickname,\n            db.customModels,\n            db.mcpServers\n          ],\n          async () => {\n            if (data?.knowledge && Array.isArray(data.knowledge)) {\n              await importKnowledgeV2(formatKnowledge(data?.knowledge), options)\n            }\n\n            if (data?.chat && Array.isArray(data.chat)) {\n              await importChatHistoryV2(data.chat, options)\n            }\n\n            if (data?.vector && Array.isArray(data.vector)) {\n              await importVectorsV2(formatVector(data.vector), options)\n            }\n\n            if (data?.prompts && Array.isArray(data.prompts)) {\n              await importPromptsV2(data.prompts, options)\n            }\n            if (data?.oaiConfigs && Array.isArray(data.oaiConfigs)) {\n              await importOAIConfigsV2(data.oaiConfigs, options)\n            }\n\n            if (data?.nicknames && Array.isArray(data.nicknames)) {\n              await importNicknamesV2(data.nicknames, options)\n            }\n\n            if (data?.models && Array.isArray(data.models)) {\n              await importModelsV2(data.models, options)\n            }\n\n            if (data?.mcpServers && Array.isArray(data.mcpServers)) {\n              await importMcpServersV2(data.mcpServers, options)\n            }\n          }\n        )\n\n        if (data?.storageLocal && typeof data.storageLocal === \"object\") {\n          await chrome.storage.local.set(data.storageLocal)\n        }\n\n        const storageSyncEnabled = await getStorageSyncEnabled()\n        if (\n          storageSyncEnabled &&\n          data?.storageSync &&\n          typeof data.storageSync === \"object\"\n        ) {\n          await chrome.storage.sync.set(data.storageSync)\n        }\n\n        resolve(true)\n      } catch (e) {\n        console.error(e)\n        reject(e)\n      }\n    }\n\n    reader.onerror = () => reject(reader.error)\n    reader.readAsText(file)\n  })\n}\n\n// @deprecated don't use this\nexport const importPageAssistDataOld = async (file: File) => {\n  return new Promise((resolve, reject) => {\n    const reader = new FileReader()\n    reader.onload = async () => {\n      try {\n        const data = JSON.parse(reader.result as string)\n\n        if (data?.knowledge) {\n          await importKnowledge(data.knowledge)\n        }\n\n        if (data?.chat) {\n          await importChatHistory(data.chat)\n        }\n\n        if (data?.vector) {\n          await importVectors(data.vector)\n        }\n\n        if (data?.prompts) {\n          await importPrompts(data.prompts)\n        }\n\n        resolve(true)\n      } catch (e) {\n        console.error(e)\n        reject(e)\n      }\n    }\n\n    reader.onerror = () => reject(reader.error)\n    reader.readAsText(file)\n  })\n}\n"
  },
  {
    "path": "src/libs/fetcher.ts",
    "content": "import { getCustomOllamaHeaders } from \"@/services/app\"\n\n\nconst fetcher = async (input: string | URL | globalThis.Request, init?: RequestInit) : Promise<Response> => {\n    const update = {...init} || {}\n    const customHeaders = await getCustomOllamaHeaders()\n    update.headers = {\n        ...customHeaders,\n        ...update?.headers\n    }\n    return fetch(input, update)\n}\n\nexport default fetcher"
  },
  {
    "path": "src/libs/get-html.ts",
    "content": "import { defaultExtractContent } from \"@/parser/default\"\nimport { getPdf } from \"./pdf\"\nimport {\n  isTweet,\n  isTwitterTimeline,\n  parseTweet,\n  parseTwitterTimeline\n} from \"@/parser/twitter\"\nimport { isGoogleDocs, parseGoogleDocs } from \"@/parser/google-docs\"\nimport { cleanUnwantedUnicode } from \"@/utils/clean\"\nimport { isYoutubeLink } from \"@/utils/is-youtube\"\n\nconst _getHtml = () => {\n  const url = window.location.href\n  if (document.contentType === \"application/pdf\") {\n    return { url, content: \"\", type: \"pdf\" }\n  }\n\n  return {\n    content: document.documentElement.outerHTML,\n    url,\n    type: \"html\"\n  }\n}\n\n// this is a function that fetches youtube transcript from the current tab\nconst _fetchTranscriptYT = async () => {\n  const url = window.location.href\n  const response = await fetch(url)\n  const content = await response.text()\n  console.log(\"fetching transcript from youtube html\", content.length)\n  function extractYouTubeData(htmlContent: string) {\n    const results: any = {}\n\n    try {\n      // Extract INNERTUBE_CONTEXT for client info\n      const innertubePattern =\n        /[\"']?INNERTUBE_CONTEXT[\"']?\\s*:\\s*({[\\s\\S]*?\"client\"[\\s\\S]*?})/\n      const contextMatch = htmlContent.match(innertubePattern)\n\n      if (contextMatch) {\n        try {\n          // Find complete client object with proper brace matching\n          let startIdx = htmlContent.indexOf(contextMatch[1])\n          let braceCount = 0\n          let inString = false\n          let escapeNext = false\n          let endIdx = startIdx\n\n          for (\n            let i = startIdx;\n            i < htmlContent.length && i < startIdx + 50000;\n            i++\n          ) {\n            const char = htmlContent[i]\n\n            if (!escapeNext) {\n              if (char === '\"' || char === \"'\") {\n                inString = !inString\n              } else if (char === \"\\\\\") {\n                escapeNext = true\n              } else if (!inString) {\n                if (char === \"{\") braceCount++\n                else if (char === \"}\") {\n                  braceCount--\n                  if (braceCount === 0) {\n                    endIdx = i + 1\n                    break\n                  }\n                }\n              }\n            } else {\n              escapeNext = false\n            }\n          }\n\n          const contextStr = htmlContent.substring(startIdx, endIdx)\n          const contextObj = JSON.parse(contextStr)\n\n          if (contextObj.client) {\n            results.clientName = contextObj.client.clientName\n            results.clientVersion = contextObj.client.clientVersion\n          }\n        } catch (e) {\n          // Fallback to regex\n        }\n      }\n\n      // Direct search for clientName and clientVersion as fallback\n      if (!results.clientName) {\n        const nameMatch = htmlContent.match(\n          /[\"']?clientName[\"']?\\s*:\\s*[\"']([^\"']+)[\"']/\n        )\n        if (nameMatch) results.clientName = nameMatch[1]\n      }\n\n      if (!results.clientVersion) {\n        const versionMatch = htmlContent.match(\n          /[\"']?clientVersion[\"']?\\s*:\\s*[\"']([^\"']+)[\"']/\n        )\n        if (versionMatch) results.clientVersion = versionMatch[1]\n      }\n\n      // Extract getTranscriptEndpoint params - multiple patterns to catch different formats\n      const transcriptPatterns = [\n        /getTranscriptEndpoint\\s*:\\s*{[^{}]*params\\s*:\\s*[\"']([^\"']+)[\"']/,\n        /[\"']?getTranscriptEndpoint[\"']?\\s*:\\s*{[^{}]*[\"']?params[\"']?\\s*:\\s*[\"']([^\"']+)[\"']/,\n        /getTranscriptEndpoint[\"']?\\s*:\\s*{[\\s\\S]*?params[\"']?\\s*:\\s*[\"']([^\"']+)[\"']/,\n        // Look for the pattern in a broader context\n        /getTranscriptEndpoint[^{]*{[^}]*params[^:]*:\\s*[\"']([^\"']+)[\"']/\n      ]\n\n      for (const pattern of transcriptPatterns) {\n        const match = htmlContent.match(pattern)\n        if (match) {\n          results.transcriptParams = match[1]\n          break\n        }\n      }\n\n      // If not found, try to find it in a more complex nested structure\n      if (!results.transcriptParams) {\n        // Search for the endpoint object and extract params\n        const endpointMatch = htmlContent.match(\n          /getTranscriptEndpoint[^{]*({[\\s\\S]*?})(?=\\s*[,}])/\n        )\n        if (endpointMatch) {\n          const paramsMatch = endpointMatch[1].match(\n            /params\\s*:\\s*[\"']([^\"']+)[\"']/\n          )\n          if (paramsMatch) {\n            results.transcriptParams = paramsMatch[1]\n          }\n        }\n      }\n\n      // Extract commandMetadata\n      const cmdPatterns = [\n        /[\"']?commandMetadata[\"']?\\s*:\\s*{\\s*[\"']?webCommandMetadata[\"']?\\s*:\\s*{([^}]+)}/,\n        /commandMetadata\\s*:\\s*{\\s*webCommandMetadata\\s*:\\s*{([^}]+)}/\n      ]\n\n      for (const pattern of cmdPatterns) {\n        const cmdMatch = htmlContent.match(pattern)\n        if (cmdMatch) {\n          const webCmd = cmdMatch[1]\n\n          // Extract sendPost\n          const sendPostMatch = webCmd.match(\n            /[\"']?sendPost[\"']?\\s*:\\s*(true|false)/\n          )\n          if (sendPostMatch) {\n            results.sendPost = sendPostMatch[1] === \"true\"\n          }\n\n          // Extract apiUrl\n          const apiUrlMatch = webCmd.match(\n            /[\"']?apiUrl[\"']?\\s*:\\s*[\"']([^\"']+)[\"']/\n          )\n          if (apiUrlMatch) {\n            results.apiUrl = apiUrlMatch[1]\n          }\n\n          break\n        }\n      }\n\n      // Direct search for apiUrl if not found in commandMetadata\n      if (!results.apiUrl) {\n        const apiUrlMatch = htmlContent.match(\n          /[\"']?apiUrl[\"']?\\s*:\\s*[\"'](\\/youtubei\\/v1\\/get_transcript)[\"']/\n        )\n        if (apiUrlMatch) {\n          results.apiUrl = apiUrlMatch[1]\n        }\n      }\n    } catch (error) {\n      console.error(\"Parsing error:\", error)\n      results.error = error.message\n    }\n\n    return results\n  }\n\n  async function fetchTranscript(extractedData: any) {\n    if (!extractedData.transcriptParams) {\n      throw new Error(\"No transcript params found\")\n    }\n\n    const apiUrl = \"/youtubei/v1/get_transcript\"\n    const url = `https://www.youtube.com${apiUrl}`\n\n    const requestBody = {\n      context: {\n        client: {\n          clientName: extractedData.clientName || \"WEB\",\n          clientVersion: extractedData.clientVersion || \"2.0\"\n        }\n      },\n      params: extractedData.transcriptParams\n    }\n\n    try {\n      const response = await fetch(url, {\n        method: \"POST\",\n        headers: {\n          \"Content-Type\": \"application/json\"\n        },\n        body: JSON.stringify(requestBody)\n      })\n\n      if (!response.ok) {\n        throw new Error(`HTTP error! status: ${response.status}`)\n      }\n\n      const data = await response.json()\n      return parseTranscriptResponse(data)\n    } catch (error) {\n      console.error(\"Failed to fetch transcript:\", error)\n      throw error\n    }\n  }\n\n  function parseTranscriptResponse(data: any) {\n    const transcript = []\n\n    try {\n      // Navigate through the YouTube API response structure\n      const actions = data?.actions\n      if (!actions) return transcript\n\n      for (const action of actions) {\n        const results =\n          action?.updateEngagementPanelAction?.content?.transcriptRenderer\n            ?.content?.transcriptSearchPanelRenderer?.body\n            ?.transcriptSegmentListRenderer?.initialSegments ||\n          action?.appendContinuationItemsAction?.continuationItems\n\n        if (!results) continue\n\n        for (const segment of results) {\n          const cueGroup = segment?.transcriptSegmentRenderer?.snippet?.runs\n          if (!cueGroup) continue\n\n          const text = cueGroup.map((run) => run.text).join(\"\")\n          const startMs = segment?.transcriptSegmentRenderer?.startMs\n          const endMs = segment?.transcriptSegmentRenderer?.endMs\n\n          if (text) {\n            transcript.push({\n              text: text.trim(),\n              start: startMs ? parseInt(startMs) / 1000 : null,\n              end: endMs ? parseInt(endMs) / 1000 : null,\n              startMs: startMs ? parseInt(startMs) : null,\n              endMs: endMs ? parseInt(endMs) : null\n            })\n          }\n        }\n      }\n    } catch (error) {\n      console.error(\"Error parsing transcript response:\", error)\n    }\n\n    return transcript\n  }\n\n  const getTranscriptYoutubeFromHTML = async (htmlContent: string) => {\n    try {\n      const extractedData = extractYouTubeData(htmlContent) as any\n      if (!extractedData?.transcriptParams) {\n        console.log(\"[YouTube Transcript] No transcript params found.\")\n        return \"\"\n      }\n      const transcript = await fetchTranscript(extractedData)\n      return transcript?.map((t) => `[${t?.start}] ${t?.text}`).join(\"\\n\")\n    } catch (e) {\n      console.log(\"[YouTube Transcript] Error extracting transcript:\", e)\n      return \"\"\n    }\n  }\n  const transcript = await getTranscriptYoutubeFromHTML(content)\n\n  console.log(transcript)\n\n  return transcript\n}\n\nexport const fetchTranscriptYT = async () => {\n  return new Promise<string>((resolve) => {\n    browser.tabs\n      .query({ active: true, currentWindow: true })\n      .then(async (tabs) => {\n        const tab = tabs[0]\n        try {\n          const data = await browser.scripting.executeScript({\n            target: { tabId: tab.id },\n            func: _fetchTranscriptYT\n          })\n\n          if (data.length > 0) {\n            resolve(data[0].result)\n          }\n        } catch (e) {\n          console.error(\"error\", e)\n          resolve(\"\")\n        }\n      })\n  })\n}\n\nexport const getDataFromCurrentTab = async () => {\n  const result = new Promise((resolve) => {\n    if (\n      import.meta.env.BROWSER === \"chrome\" ||\n      import.meta.env.BROWSER === \"edge\"\n    ) {\n      chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => {\n        const tab = tabs[0]\n\n        const data = await chrome.scripting.executeScript({\n          target: { tabId: tab.id },\n          func: _getHtml\n        })\n\n        if (data.length > 0) {\n          resolve(data[0].result)\n        }\n      })\n    } else {\n      browser.tabs\n        .query({ active: true, currentWindow: true })\n        .then(async (tabs) => {\n          const tab = tabs[0]\n          try {\n            const data = await browser.scripting.executeScript({\n              target: { tabId: tab.id },\n              func: _getHtml\n            })\n\n            if (data.length > 0) {\n              resolve(data[0].result)\n            }\n          } catch (e) {\n            console.error(\"error\", e)\n            // this is a weird method but it works\n            if (import.meta.env.BROWSER === \"firefox\") {\n              // all I need is to get the pdf url but somehow\n              // firefox won't allow extensions to run content scripts on pdf https://bugzilla.mozilla.org/show_bug.cgi?id=1454760\n              // so I set up a weird method to fix this issue by asking tab to give the url\n              // and then I can get the pdf url\n              const result = {\n                url: tab.url,\n                content: \"\",\n                type: \"pdf\"\n              }\n              resolve(result)\n            }\n          }\n        })\n    }\n  }) as Promise<{\n    url: string\n    content: string\n    type: string\n  }>\n\n  const { content, type, url } = await result\n\n  if (type === \"pdf\") {\n    const res = await fetch(url)\n    const data = await res.arrayBuffer()\n    let pdfHtml: {\n      content: string\n      page: number\n    }[] = []\n    const pdf = await getPdf(data)\n\n    for (let i = 1; i <= pdf.numPages; i += 1) {\n      const page = await pdf.getPage(i)\n      const content = await page.getTextContent()\n\n      if (content?.items.length === 0) {\n        continue\n      }\n\n      const text = content?.items\n        .map((item: any) => item.str)\n        .join(\"\\n\")\n        .replace(/\\x00/g, \"\")\n        .trim()\n      pdfHtml.push({\n        content: text,\n        page: i\n      })\n    }\n\n    return {\n      url,\n      content: \"\",\n      pdf: pdfHtml,\n      type: \"pdf\"\n    }\n  }\n  if (isTwitterTimeline(url)) {\n    const data = parseTwitterTimeline(content)\n    return {\n      url,\n      content: data,\n      type: \"html\",\n      pdf: [],\n      html: content\n    }\n  } else if (isTweet(url)) {\n    const data = parseTweet(content)\n    return {\n      url,\n      content: data,\n      type: \"html\",\n      pdf: [],\n      html: content\n    }\n  } else if (isGoogleDocs(url)) {\n    const data = await parseGoogleDocs()\n    if (data) {\n      return {\n        url,\n        content: cleanUnwantedUnicode(data),\n        type: \"html\",\n        pdf: [],\n        html: content\n      }\n    }\n  }\n  const data = defaultExtractContent(content)\n  return { url, content: data, type, pdf: [], html: content }\n}\n\nexport const getContentFromCurrentTab = async (isUsingVS: boolean) => {\n  const data = await getDataFromCurrentTab()\n\n \n  if (isYoutubeLink(data.url)) {\n    console.log(\"Youtube link detected\")\n    const transcript = await fetchTranscriptYT()\n    console.log(transcript)\n    return {\n      ...data,\n      content: transcript\n    }\n  }\n\n\n  return data\n}\n"
  },
  {
    "path": "src/libs/get-screenshot.ts",
    "content": "const captureVisibleTab = () => {\n  const result = new Promise<string>((resolve) => {\n    if (import.meta.env.BROWSER === \"chrome\" || import.meta.env.BROWSER === \"edge\") {\n      chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => {\n        const tab = tabs[0]\n        chrome.tabs.captureVisibleTab(null, { format: \"png\" }, (dataUrl) => {\n          resolve(dataUrl)\n        })\n      })\n    } else {\n      browser.tabs\n        .query({ active: true, currentWindow: true })\n        .then(async (tabs) => {\n          const dataUrl = (await Promise.race([\n            browser.tabs.captureVisibleTab(null, { format: \"png\" }),\n            new Promise((_, reject) =>\n              setTimeout(\n                () => reject(new Error(\"Screenshot capture timed out\")),\n                10000\n              )\n            )\n          ])) as string\n          resolve(dataUrl)\n        })\n    }\n  })\n  return result\n}\n\nexport const getScreenshotFromCurrentTab = async () => {\n  try {\n    const screenshotDataUrl = await captureVisibleTab()\n    return {\n      success: true,\n      screenshot: screenshotDataUrl,\n      error: null\n    }\n  } catch (error) {\n    return {\n      success: false,\n      screenshot: null,\n      error:\n        error instanceof Error ? error.message : \"Failed to capture screenshot\"\n    }\n  }\n}\n"
  },
  {
    "path": "src/libs/get-tab-contents.ts",
    "content": "import { ChatDocuments } from \"@/models/ChatTypes\"\nimport { isAmazonURL, parseAmazonWebsite } from \"@/parser/amazon\"\nimport { defaultExtractContent } from \"@/parser/default\"\nimport { isTwitterProfile, isTwitterTimeline, parseTweetProfile, parseTwitterTimeline } from \"@/parser/twitter\"\nimport { isWikipedia, parseWikipedia } from \"@/parser/wiki\"\nimport { getMaxContextSize } from \"@/services/kb\"\nimport { YtTranscript } from \"yt-transcript\"\nimport { processPDFFromURL } from \"./pdf\"\n\nconst getTranscript = async (url: string) => {\n    const ytTranscript = new YtTranscript({ url })\n    return await ytTranscript.getTranscript()\n}\n\nconst formatTranscriptText = (transcript: any[]) => {\n    return transcript\n        ?.map((item) => {\n            const timestamp = `[${item.start}s]`\n            const transcriptText = item.text\n            return `${timestamp} ${transcriptText}`\n        })\n        ?.join(\" \")\n}\n\nconst formatDocumentHeader = (title: string, url: string) => {\n    const hostname = new URL(url).hostname\n    return `# ${title} (${hostname}) \\n\\n`\n}\n\nconst truncateContent = (content: string, maxLength: number): string => {\n    if (content.length <= maxLength) {\n        return content\n    }\n\n    // Try to truncate at word boundary\n    const truncated = content.substring(0, maxLength)\n    const lastSpaceIndex = truncated.lastIndexOf(' ')\n\n    if (lastSpaceIndex > maxLength * 0.8) {\n        return truncated.substring(0, lastSpaceIndex) + '...'\n    }\n\n    return truncated + '...'\n}\n\nexport const getTabContents = async (documents: ChatDocuments) => {\n    const result: string[] = []\n    const maxContextSize = await getMaxContextSize()\n\n    // Calculate available space per document\n    const contextPerDocument = Math.floor(maxContextSize / documents.length)\n    let remainingContext = maxContextSize\n\n    for (const doc of documents) {\n        try {\n            if (remainingContext <= 0) break\n\n            const pageContent = await browser.scripting.executeScript({\n                target: { tabId: doc.tabId! },\n                func: () => ({\n                    html: document.documentElement.outerHTML,\n                    title: document.title,\n                    url: window.location.href,\n                    isPDF: document.contentType === 'application/pdf'\n                })\n            })\n            const content = pageContent[0].result\n            const header = formatDocumentHeader(doc.title, doc.url)\n            let extractedContent = \"\"\n\n            if (isYoutubeLink(doc.url)) {\n                const transcript = await getTranscript(doc.url)\n                if (transcript) {\n                    extractedContent = formatTranscriptText(transcript)\n                }\n            } else if (isWikipedia(doc.url)) {\n                extractedContent = parseWikipedia(content)\n            } else if (isAmazonURL(doc.url)) {\n                extractedContent = parseAmazonWebsite(content.html)\n            } else if (isTwitterProfile(doc.url)) {\n                extractedContent = parseTweetProfile(content.html)\n            } else if (isTwitterTimeline(doc.url)) {\n                extractedContent = parseTwitterTimeline(content.html)\n            } else if (content.isPDF) {\n                extractedContent = await processPDFFromURL(doc.url)\n            } else {\n                extractedContent = defaultExtractContent(content.html)\n            }\n\n            // Calculate available space for this document's content\n            const headerLength = header.length\n            const availableSpace = Math.min(\n                contextPerDocument - headerLength,\n                remainingContext - headerLength\n            )\n\n            if (availableSpace > 0) {\n                const truncatedContent = truncateContent(extractedContent, availableSpace)\n                const documentContent = `${header}${truncatedContent}`\n\n                result.push(documentContent)\n                remainingContext -= documentContent.length\n            }\n        } catch (e) {\n            console.error(\"Error processing document:\", e)\n            continue\n        }\n    }\n\n    return result.join(\"\\n\\n\")\n}\n"
  },
  {
    "path": "src/libs/mcp/client.ts",
    "content": "import { getEnabledMcpServers } from \"@/db/dexie/mcp\"\nimport { McpServer } from \"./types\"\nimport { HttpOnlyMcpClient } from \"./http-client\"\n\nexport const getConfiguredMcpServers = async () => {\n  return await getEnabledMcpServers()\n}\n\nexport const createMcpClient = (\n  servers: McpServer[],\n  callbacks?: {\n    onProgress?: (...args: any[]) => void\n    beforeToolCall?: (...args: any[]) => any\n    afterToolCall?: (...args: any[]) => any\n  }\n) => {\n  return new HttpOnlyMcpClient(servers, callbacks)\n}\n"
  },
  {
    "path": "src/libs/mcp/errors.ts",
    "content": "export class McpBootstrapError extends Error {\n  constructor(message: string, public readonly cause?: unknown) {\n    super(message)\n    this.name = \"McpBootstrapError\"\n  }\n}\n\nconst LEGACY_MCP_ERROR_PATTERNS = [\n  \"Missing sessionId parameter\",\n  \"No transport found for sessionId\",\n  \"Error POSTing to endpoint\"\n]\n\nconst getFriendlyMcpErrorMessage = (message: string) => {\n  if (\n    LEGACY_MCP_ERROR_PATTERNS.some((pattern) => message.includes(pattern))\n  ) {\n    return \"Legacy MCP SSE endpoints are not supported. Use a Streamable HTTP MCP endpoint instead.\"\n  }\n\n  return message\n}\n\nexport const getMcpErrorMessage = (error: unknown) => {\n  if (error instanceof Error) {\n    return getFriendlyMcpErrorMessage(error.message)\n  }\n\n  if (typeof error === \"string\") {\n    return getFriendlyMcpErrorMessage(error)\n  }\n\n  return \"Unknown MCP error\"\n}\n\nexport const isAbortLikeError = (error: unknown) => {\n  const message = getMcpErrorMessage(error)\n\n  return (\n    error instanceof DOMException ||\n    message === \"AbortError\" ||\n    message.includes(\"AbortError\")\n  )\n}\n"
  },
  {
    "path": "src/libs/mcp/http-client.ts",
    "content": "import { DynamicStructuredTool } from \"@langchain/core/tools\"\nimport type { Client } from \"@modelcontextprotocol/sdk/client/index.js\"\nimport { normalizeMcpToolSchema } from \"./tool-schema\"\nimport { McpServer } from \"./types\"\nimport { createMcpActionInfo, MCP_TOOL_NAME_SEPARATOR } from \"./utils\"\nimport {\n  closeMcpServerConnection,\n  listRemoteMcpTools,\n  openMcpServerConnection\n} from \"./remote-tools\"\nimport { ensureFreshOAuthTokens } from \"./oauth-flow\"\n\ntype McpClientCallbacks = {\n  onProgress?: (...args: any[]) => void\n  beforeToolCall?: (...args: any[]) => any\n  afterToolCall?: (...args: any[]) => any\n}\n\ntype ConnectedServer = {\n  server: McpServer\n  client: Awaited<ReturnType<typeof openMcpServerConnection>>[\"client\"]\n  transport: Awaited<ReturnType<typeof openMcpServerConnection>>[\"transport\"]\n  toolsPromise?: Promise<DynamicStructuredTool[]>\n}\n\nconst serializeResultValue = (value: unknown) => {\n  try {\n    return JSON.stringify(value, null, 2)\n  } catch (error) {\n    return String(value ?? \"\")\n  }\n}\n\nconst toArgumentRecord = (value: unknown) =>\n  value && typeof value === \"object\" && !Array.isArray(value)\n    ? (value as Record<string, unknown>)\n    : {}\n\nconst readToolContent = (content: any) => {\n  if (!content || typeof content !== \"object\") {\n    return String(content ?? \"\")\n  }\n\n  switch (content.type) {\n    case \"text\":\n      return typeof content.text === \"string\" ? content.text : \"\"\n    case \"image\":\n      return `[Image output${content.mimeType ? `: ${content.mimeType}` : \"\"}]`\n    case \"audio\":\n      return `[Audio output${content.mimeType ? `: ${content.mimeType}` : \"\"}]`\n    case \"resource\":\n      if (content.resource?.text) {\n        return content.resource.text\n      }\n\n      if (content.resource?.uri) {\n        return `[Resource: ${content.resource.uri}]`\n      }\n\n      return serializeResultValue(content.resource)\n    case \"resource_link\":\n      return content.uri ? `[Resource link: ${content.uri}]` : serializeResultValue(content)\n    default:\n      return serializeResultValue(content)\n  }\n}\n\nconst formatToolResponse = (result: any) => {\n  const textParts = Array.isArray(result?.content)\n    ? result.content.map((item: unknown) => readToolContent(item)).filter(Boolean)\n    : []\n\n  if (result?.structuredContent != null) {\n    textParts.push(serializeResultValue(result.structuredContent))\n  }\n\n  const content = textParts.join(\"\\n\\n\").trim() || \"Tool executed successfully.\"\n\n  const artifact =\n    result?.structuredContent != null || result?._meta != null\n      ? {\n          structuredContent: result?.structuredContent,\n          meta: result?._meta\n        }\n      : undefined\n\n  return [content, artifact] as const\n}\n\nconst buildToolErrorMessage = (toolName: string, serverName: string, result: any) => {\n  const detail = formatToolResponse(result)[0] || \"Unknown MCP tool error\"\n  return `MCP tool \"${toolName}\" on server \"${serverName}\" failed: ${detail}`\n}\n\nconst buildRequestOptions = ({\n  signal,\n  timeout,\n  metadata,\n  onProgress\n}: {\n  signal?: AbortSignal\n  timeout?: number\n  metadata?: Record<string, any>\n  onProgress?: (...args: any[]) => void\n}) => {\n  const timeoutMs =\n    typeof metadata?.timeoutMs === \"number\" && metadata.timeoutMs > 0\n      ? metadata.timeoutMs\n      : typeof timeout === \"number\" && timeout > 0\n        ? timeout\n        : undefined\n\n  return {\n    ...(signal ? { signal } : {}),\n    ...(timeoutMs ? { timeout: timeoutMs } : {}),\n    ...(onProgress\n      ? {\n          onprogress: (progress: any) => onProgress(progress)\n        }\n      : {})\n  }\n}\n\nconst createLangChainTool = ({\n  client,\n  getClient,\n  server,\n  remoteTool,\n  callbacks\n}: {\n  client?: Client\n  getClient?: () => Promise<Client>\n  server: McpServer\n  remoteTool: any\n  callbacks?: McpClientCallbacks\n}) => {\n  const sanitizedServerName = server.name.replace(/\\s+/g, \"_\")\n  const prefixedToolName = `${sanitizedServerName}${MCP_TOOL_NAME_SEPARATOR}${remoteTool.name}`\n\n  const resolveClient = async () => {\n    if (client) return client\n    if (getClient) return await getClient()\n    throw new Error(`No MCP client available for server \"${server.name}\"`)\n  }\n\n  return new DynamicStructuredTool({\n    name: prefixedToolName,\n    description: remoteTool.description || \"\",\n    schema: normalizeMcpToolSchema(remoteTool.inputSchema),\n    responseFormat: \"content_and_artifact\",\n    metadata: {\n      serverName: server.name,\n      toolName: remoteTool.name\n    },\n    func: async (args, _runManager, config) => {\n      const resolvedClient = await resolveClient()\n\n      const interception = await callbacks?.beforeToolCall?.(\n        {\n          name: remoteTool.name,\n          args,\n          serverName: server.name\n        },\n        {},\n        config ?? {}\n      )\n\n      const normalizedArgs = toArgumentRecord(args)\n      const finalArgs = {\n        ...normalizedArgs,\n        ...toArgumentRecord(interception?.args)\n      }\n\n      const response = await resolvedClient.callTool(\n        {\n          name: remoteTool.name,\n          arguments: finalArgs\n        },\n        undefined,\n        buildRequestOptions({\n          signal: config?.signal,\n          timeout: config?.timeout,\n          metadata: config?.metadata,\n          onProgress: callbacks?.onProgress\n            ? (progress: any) =>\n                callbacks.onProgress?.(\n                  progress,\n                  createMcpActionInfo(\"waiting_result\", {\n                    serverName: server.name,\n                    toolName: remoteTool.name\n                  })\n                )\n            : undefined\n        })\n      )\n\n      if (response?.isError) {\n        throw new Error(buildToolErrorMessage(remoteTool.name, server.name, response))\n      }\n\n      const resultTuple = formatToolResponse(response)\n      const interceptedResult = await callbacks?.afterToolCall?.(\n        {\n          name: remoteTool.name,\n          args: finalArgs,\n          result: resultTuple,\n          serverName: server.name\n        },\n        {},\n        config ?? {}\n      )\n\n      if (Array.isArray(interceptedResult?.result)) {\n        return interceptedResult.result\n      }\n\n      return resultTuple\n    }\n  })\n}\n\nexport class HttpOnlyMcpClient {\n  private readonly callbacks?: McpClientCallbacks\n  private readonly connections = new Map<string, ConnectedServer>()\n  private toolsPromise?: Promise<DynamicStructuredTool[]>\n\n  constructor(servers: McpServer[], callbacks?: McpClientCallbacks) {\n    this.servers = servers\n    this.callbacks = callbacks\n  }\n\n  private readonly servers: McpServer[]\n\n  async getTools() {\n    if (!this.toolsPromise) {\n      this.toolsPromise = this.loadTools()\n    }\n\n    try {\n      return await this.toolsPromise\n    } catch (error) {\n      this.toolsPromise = undefined\n      throw error\n    }\n  }\n\n  async close() {\n    this.toolsPromise = undefined\n\n    const activeConnections = Array.from(this.connections.values())\n    this.connections.clear()\n\n    await Promise.allSettled(\n      activeConnections.map(async ({ client, transport }) =>\n        closeMcpServerConnection({\n          client,\n          transport\n        })\n      )\n    )\n  }\n\n  private async loadTools() {\n    const toolGroups = await Promise.all(\n      this.servers.map(async (server) => {\n        const hasCachedSchemas =\n          server.cachedTools &&\n          server.cachedTools.length > 0 &&\n          server.cachedTools.every((t) => t.inputSchema != null)\n\n        if (hasCachedSchemas) {\n          return this.buildToolsFromCache(server)\n        }\n\n        const connection = await this.getOrCreateConnection(server)\n        return await this.loadToolsForConnection(connection)\n      })\n    )\n\n    return toolGroups.flat()\n  }\n\n  private buildToolsFromCache(server: McpServer) {\n    return (server.cachedTools || []).map((cachedTool) =>\n      createLangChainTool({\n        getClient: async () => {\n          const conn = await this.getOrCreateConnection(server)\n          return conn.client\n        },\n        server,\n        remoteTool: cachedTool,\n        callbacks: this.callbacks\n      })\n    )\n  }\n\n  private async getOrCreateConnection(server: McpServer) {\n    const existingConnection = this.connections.get(server.id)\n    if (existingConnection) {\n      return existingConnection\n    }\n\n    let connectServer = server\n    if (server.authType === \"oauth\" && server.oauthTokens) {\n      const refreshed = await ensureFreshOAuthTokens(server)\n      if (refreshed) {\n        connectServer = refreshed\n      }\n    }\n\n    const { client, transport } = await openMcpServerConnection(connectServer)\n\n    const connection: ConnectedServer = {\n      server: connectServer,\n      client,\n      transport\n    }\n\n    this.connections.set(server.id, connection)\n    return connection\n  }\n\n  private async loadToolsForConnection(connection: ConnectedServer) {\n    if (!connection.toolsPromise) {\n      connection.toolsPromise = this.fetchTools(connection)\n    }\n\n    try {\n      return await connection.toolsPromise\n    } catch (error) {\n      connection.toolsPromise = undefined\n      throw error\n    }\n  }\n\n  private async fetchTools(connection: ConnectedServer) {\n    const remoteTools = await listRemoteMcpTools(\n      connection.client,\n      connection.server.name\n    )\n\n    return remoteTools.map((remoteTool) =>\n      createLangChainTool({\n        client: connection.client,\n        server: connection.server,\n        remoteTool,\n        callbacks: this.callbacks\n      })\n    )\n  }\n}\n"
  },
  {
    "path": "src/libs/mcp/normal-chat.ts",
    "content": "import { cleanUrl } from \"@/libs/clean-url\"\nimport {\n  normalizeToolContent,\n  parseMcpToolName,\n  toStoredToolCalls\n} from \"@/libs/mcp/utils\"\nimport { pageAssistModel } from \"@/models\"\nimport { ChatDocuments } from \"@/models/ChatTypes\"\nimport { getOllamaURL, systemPromptForNonRagOption } from \"@/services/ollama\"\nimport { generateTitle } from \"@/services/title\"\nimport { type ChatHistory, type Message } from \"@/store/option\"\nimport { generateHistory } from \"@/utils/generate-history\"\nimport { humanMessageFormatter } from \"@/utils/human-message\"\nimport { systemPromptFormatter } from \"@/utils/system-message\"\nimport { AIMessage, ToolMessage } from \"@langchain/core/messages\"\nimport { concat } from \"@langchain/core/utils/stream\"\nimport { getConfiguredMcpServers, createMcpClient } from \"./client\"\nimport {\n  McpBootstrapError,\n  getMcpErrorMessage,\n  isAbortLikeError\n} from \"./errors\"\nimport {\n  generateID,\n  getPromptById,\n  saveHistory,\n  saveMessage,\n  updateChatHistoryCreatedAt,\n  updateHistory,\n  updateLastUsedModel,\n  updateLastUsedPrompt\n} from \"@/db/dexie/helpers\"\nimport { getModelNicknameByID } from \"@/db/dexie/nickname\"\nimport { updatePageTitle } from \"@/utils/update-page-title\"\nimport {\n  isReasoningEnded,\n  isReasoningStarted,\n  mergeReasoningContent\n} from \"@/libs/reasoning\"\n\ntype SetMessages = (messages: Message[] | ((prev: Message[]) => Message[])) => void\ntype SetHistory = (history: ChatHistory) => void\n\ntype RunMcpNormalChatParams = {\n  selectedModel: string\n  useOCR: boolean\n  selectedSystemPrompt: string\n  currentChatModelSettings: any\n  setMessages: SetMessages\n  setHistory: SetHistory\n  setIsProcessing: (value: boolean) => void\n  setStreaming: (value: boolean) => void\n  setActionInfo: (value: any) => void\n  historyId: string | null\n  setHistoryId: (id: string) => void\n  uploadedFiles?: any[]\n  images?: string[]\n  temporaryChat?: boolean\n  messageSource?: \"copilot\" | \"web-ui\"\n}\n\nconst createAssistantMessage = ({\n  id,\n  selectedModel,\n  modelName,\n  modelImage\n}: {\n  id: string\n  selectedModel: string\n  modelName?: string\n  modelImage?: string\n}): Message => ({\n  isBot: true,\n  name: selectedModel,\n  message: \"▋\",\n  sources: [],\n  id,\n  modelImage,\n  modelName\n})\n\nconst createUserDocuments = (uploadedFiles?: any[]): ChatDocuments =>\n  uploadedFiles?.map((file) => ({\n    type: \"file\",\n    filename: file.filename,\n    fileSize: file.size,\n    processed: file.processed\n  })) || []\n\nconst STREAM_THROTTLE_MS = 50\n\nconst streamModelResponse = async ({\n  runnable,\n  messages,\n  signal,\n  onToken\n}: {\n  runnable: any\n  messages: any[]\n  signal: AbortSignal\n  onToken: (payload: { text: string; reasoningTimeTaken: number }) => void\n}) => {\n  let generationInfo: any | undefined = undefined\n  let fullText = \"\"\n  let timetaken = 0\n  let count = 0\n  let reasoningStartTime: Date | null = null\n  let reasoningEndTime: Date | null = null\n  let apiReasoning = false\n  let finalChunk: any\n  let lastFlushTime = 0\n  let pendingFlush = false\n\n  const chunks = await runnable.stream(messages, {\n    signal,\n    callbacks: [\n      {\n        handleLLMEnd(output: any) {\n          try {\n            generationInfo = output?.generations?.[0][0]?.generationInfo\n          } catch (error) {\n            console.error(\"handleLLMEnd error\", error)\n          }\n        }\n      }\n    ]\n  })\n\n  for await (const chunk of chunks) {\n    finalChunk = finalChunk ? concat(finalChunk, chunk) : chunk\n\n    if (chunk?.additional_kwargs?.reasoning_content) {\n      const reasoningContent = mergeReasoningContent(\n        fullText,\n        chunk?.additional_kwargs?.reasoning_content || \"\"\n      )\n      fullText = reasoningContent\n      apiReasoning = true\n    }\n\n    if (apiReasoning && chunk?.content) {\n      fullText += \"</think>\"\n      apiReasoning = false\n    }\n\n    fullText += chunk?.content || \"\"\n\n    if (isReasoningStarted(fullText) && !reasoningStartTime) {\n      reasoningStartTime = new Date()\n    }\n\n    if (reasoningStartTime && !reasoningEndTime && isReasoningEnded(fullText)) {\n      reasoningEndTime = new Date()\n      timetaken = reasoningEndTime.getTime() - reasoningStartTime.getTime()\n    }\n\n    const now = Date.now()\n    if (count === 0 || now - lastFlushTime >= STREAM_THROTTLE_MS) {\n      onToken({\n        text: fullText,\n        reasoningTimeTaken: timetaken\n      })\n      lastFlushTime = now\n      pendingFlush = false\n    } else {\n      pendingFlush = true\n    }\n    count++\n  }\n\n  if (apiReasoning) {\n    fullText += \"</think>\"\n    apiReasoning = false\n  }\n\n  if (pendingFlush) {\n    onToken({\n      text: fullText,\n      reasoningTimeTaken: timetaken\n    })\n  }\n\n  if (count === 0) {\n    throw new Error(\"The model did not return any response.\")\n  }\n\n  const aiMessage = new AIMessage({\n    content: finalChunk?.content ?? fullText,\n    additional_kwargs: finalChunk?.additional_kwargs,\n    tool_calls: finalChunk?.tool_calls,\n    response_metadata: finalChunk?.response_metadata,\n    usage_metadata: finalChunk?.usage_metadata\n  })\n\n  return {\n    aiMessage,\n    generationInfo,\n    fullText,\n    reasoningTimeTaken: timetaken\n  }\n}\n\nexport const runMcpNormalChatMode = async (\n  message: string,\n  image: string,\n  isRegenerate: boolean,\n  messages: Message[],\n  history: ChatHistory,\n  signal: AbortSignal,\n  {\n    selectedModel,\n    useOCR,\n    selectedSystemPrompt,\n    currentChatModelSettings,\n    setMessages,\n    setHistory,\n    setIsProcessing,\n    setStreaming,\n    setActionInfo,\n    historyId,\n    setHistoryId,\n    uploadedFiles,\n    images: inputImages,\n    temporaryChat = false,\n    messageSource = \"web-ui\"\n  }: RunMcpNormalChatParams\n) => {\n  const configuredServers = await getConfiguredMcpServers()\n  if (configuredServers.length === 0) {\n    return false\n  }\n\n  const url = await getOllamaURL()\n  const ollama = await pageAssistModel({\n    model: selectedModel,\n    baseUrl: cleanUrl(url)\n  })\n\n  if (\n    typeof ollama?.bindTools !== \"function\" ||\n    ollama?._llmType?.() === \"chrome-ai\"\n  ) {\n    throw new McpBootstrapError(\n      \"The selected model does not support MCP tools. Choose an Ollama or OpenAI-compatible tool-calling model.\"\n    )\n  }\n\n  const prompt = await systemPromptForNonRagOption()\n  const selectedPrompt = await getPromptById(selectedSystemPrompt)\n  let promptId: string | undefined = selectedSystemPrompt\n  let promptContent: string | undefined = undefined\n\n  const processedImages = (inputImages || []).map((currentImage) => {\n    if (currentImage.length > 0 && !currentImage.startsWith(\"data:image\")) {\n      return `data:image/jpeg;base64,${currentImage.split(\",\")[1]}`\n    }\n\n    return currentImage\n  })\n\n  if (image.length > 0 && !image.startsWith(\"data:image\")) {\n    image = `data:image/jpeg;base64,${image.split(\",\")[1]}`\n  }\n\n  const userImages =\n    processedImages.length > 0 ? processedImages : image ? [image] : []\n\n  const modelInfo = await getModelNicknameByID(selectedModel)\n  const userEntry = {\n    role: \"user\" as const,\n    content: message,\n    image: userImages[0],\n    images: userImages\n  }\n  const userDocuments = createUserDocuments(uploadedFiles)\n\n  let uiMessages = isRegenerate\n    ? [...messages]\n    : [\n      ...messages,\n      {\n        isBot: false,\n        name: \"You\",\n        message,\n        sources: [],\n        images: userImages,\n        modelImage: modelInfo?.model_avatar,\n        modelName: modelInfo?.model_name || selectedModel,\n        documents: userDocuments\n      }\n    ]\n  let uiHistory = [...history]\n  let historyWithUser = [...history, userEntry]\n  let nextTimeOffset = 0\n  let activeHistoryId = historyId\n  let currentAssistantId = generateID()\n  let finalAssistantText = \"\"\n\n  const syncMessages = (nextMessages: Message[]) => {\n    uiMessages = nextMessages\n    setMessages(nextMessages)\n  }\n\n  const syncHistory = (nextHistory: ChatHistory) => {\n    uiHistory = nextHistory\n    setHistory(nextHistory)\n  }\n\n  const appendAssistantPlaceholder = () => {\n    currentAssistantId = generateID()\n    syncMessages([\n      ...uiMessages,\n      createAssistantMessage({\n        id: currentAssistantId,\n        selectedModel,\n        modelImage: modelInfo?.model_avatar,\n        modelName: modelInfo?.model_name || selectedModel\n      })\n    ])\n  }\n\n  const updateAssistantRow = (updater: (message: Message) => Message) => {\n    syncMessages(\n      uiMessages.map((currentMessage) =>\n        currentMessage.id === currentAssistantId\n          ? updater(currentMessage)\n          : currentMessage\n      )\n    )\n  }\n\n  const contentArray: any[] = [\n    {\n      text: message,\n      type: \"text\"\n    }\n  ]\n\n  userImages.forEach((currentImage) => {\n    contentArray.push({\n      image_url: currentImage,\n      type: \"image_url\"\n    })\n  })\n\n  let humanMessage = await humanMessageFormatter({\n    content: contentArray,\n    model: selectedModel,\n    useOCR\n  })\n\n  const applicationChatHistory = generateHistory(history, selectedModel)\n\n  if (prompt && !selectedPrompt) {\n    applicationChatHistory.unshift(\n      await systemPromptFormatter({\n        content: prompt\n      })\n    )\n  }\n\n  const isTempSystemprompt =\n    currentChatModelSettings.systemPrompt &&\n    currentChatModelSettings.systemPrompt?.trim().length > 0\n\n  if (!isTempSystemprompt && selectedPrompt) {\n    applicationChatHistory.unshift(\n      await systemPromptFormatter({\n        content: selectedPrompt.content\n      })\n    )\n    promptContent = selectedPrompt.content\n  }\n\n  if (isTempSystemprompt) {\n    applicationChatHistory.unshift(\n      await systemPromptFormatter({\n        content: currentChatModelSettings.systemPrompt\n      })\n    )\n    promptContent = currentChatModelSettings.systemPrompt\n  }\n\n\n  const client = createMcpClient(configuredServers)\n  let boundModel: any\n\n  try {\n    const tools = await client.getTools()\n\n    if (tools.length === 0) {\n      throw new McpBootstrapError(\n        \"No MCP tools were loaded from the configured servers.\"\n      )\n    }\n\n    try {\n      boundModel = ollama.bindTools(tools)\n    } catch (error) {\n      throw new McpBootstrapError(\n        \"The selected model could not bind MCP tools.\",\n        error\n      )\n    }\n\n    if (!temporaryChat) {\n      if (!activeHistoryId) {\n        const provisionalTitle = message?.trim() || \"Untitled Chat\"\n        const createdHistory = await saveHistory(\n          provisionalTitle,\n          false,\n          messageSource\n        )\n        activeHistoryId = createdHistory.id\n        setHistoryId(createdHistory.id)\n        updatePageTitle(provisionalTitle)\n      }\n\n      if (!isRegenerate && activeHistoryId) {\n        await saveMessage({\n          history_id: activeHistoryId,\n          name: selectedModel,\n          role: \"user\",\n          content: message,\n          images: userImages,\n          time: ++nextTimeOffset,\n          message_type: \"normal\",\n          documents: userDocuments\n        })\n      }\n\n      if (activeHistoryId) {\n        await updateLastUsedModel(activeHistoryId, selectedModel)\n        if (promptId || promptContent) {\n          await updateLastUsedPrompt(activeHistoryId, {\n            prompt_content: promptContent,\n            prompt_id: promptId\n          })\n        }\n      }\n    }\n\n    appendAssistantPlaceholder()\n\n    let lcConversation: any[] = [...applicationChatHistory, humanMessage]\n\n    while (true) {\n      setIsProcessing(true)\n      const {\n        aiMessage,\n        fullText,\n        generationInfo,\n        reasoningTimeTaken\n      } = await streamModelResponse({\n        runnable: boundModel,\n        messages: lcConversation,\n        signal,\n        onToken: ({ text, reasoningTimeTaken: currentReasoningTimeTaken }) => {\n          if (!text.trim()) {\n            return\n          }\n\n          updateAssistantRow((currentMessage) => ({\n            ...currentMessage,\n            message: `${text}▋`,\n            reasoning_time_taken: currentReasoningTimeTaken\n          }))\n        }\n      })\n\n      finalAssistantText = fullText\n      const storedToolCalls = toStoredToolCalls((aiMessage as any).tool_calls || [])\n\n      if (storedToolCalls.length === 0) {\n        const assistantHistoryEntry = {\n          role: \"assistant\" as const,\n          content: fullText\n        }\n\n        updateAssistantRow((currentMessage) => ({\n          ...currentMessage,\n          message: fullText,\n          generationInfo,\n          reasoning_time_taken: reasoningTimeTaken\n        }))\n\n        syncHistory([...historyWithUser, assistantHistoryEntry])\n\n        if (!temporaryChat && activeHistoryId) {\n          await saveMessage({\n            history_id: activeHistoryId,\n            name: selectedModel,\n            role: \"assistant\",\n            content: fullText,\n            images: [],\n            source: [],\n            time: ++nextTimeOffset,\n            message_type: \"normal\",\n            generationInfo,\n            reasoning_time_taken: reasoningTimeTaken\n          })\n\n          await updateChatHistoryCreatedAt(activeHistoryId)\n\n          if (!historyId) {\n            const generatedTitle = await generateTitle(\n              selectedModel,\n              [...historyWithUser, assistantHistoryEntry],\n              message\n            )\n            await updateHistory(activeHistoryId, generatedTitle)\n            updatePageTitle(generatedTitle)\n          }\n        }\n\n        break\n      }\n\n      const assistantToolCallEntry = {\n        role: \"assistant\" as const,\n        content: fullText,\n        messageKind: \"assistant_tool_calls\" as const,\n        toolCalls: storedToolCalls\n      }\n\n      updateAssistantRow((currentMessage) => ({\n        ...currentMessage,\n        message: fullText,\n        messageKind: \"assistant_tool_calls\",\n        toolCalls: storedToolCalls,\n        reasoning_time_taken: reasoningTimeTaken,\n        generationInfo: undefined,\n        sources: []\n      }))\n\n      historyWithUser = [...historyWithUser, assistantToolCallEntry]\n      syncHistory(historyWithUser)\n\n      const pendingDbWrites: Promise<any>[] = []\n\n      if (!temporaryChat && activeHistoryId) {\n        pendingDbWrites.push(\n          saveMessage({\n            history_id: activeHistoryId,\n            name: selectedModel,\n            role: \"assistant\",\n            content: fullText,\n            images: [],\n            source: [],\n            time: ++nextTimeOffset,\n            message_type: \"normal\",\n            messageKind: \"assistant_tool_calls\",\n            toolCalls: storedToolCalls,\n            reasoning_time_taken: reasoningTimeTaken\n          })\n        )\n      }\n\n      const toolMessages: ToolMessage[] = []\n\n      for (const toolCall of storedToolCalls) {\n        const parsedTool = parseMcpToolName(toolCall.name)\n\n\n        try {\n          const lowerCallName = toolCall.name.toLowerCase()\n          const tool = tools.find((currentTool) => currentTool.name === toolCall.name)\n            ?? tools.find((currentTool) => currentTool.name.toLowerCase() === lowerCallName)\n            ?? tools.find((currentTool) => parseMcpToolName(currentTool.name).displayName === toolCall.name)\n            ?? tools.find((currentTool) => parseMcpToolName(currentTool.name).displayName.toLowerCase() === lowerCallName)\n          if (!tool) {\n            throw new Error(`Tool \"${toolCall.name}\" is no longer available.`)\n          }\n\n\n          const result = await tool.invoke(toolCall, { signal })\n          const toolMessage =\n            result instanceof ToolMessage\n              ? result\n              : new ToolMessage({\n                content: normalizeToolContent(result),\n                tool_call_id: toolCall.id,\n                status: \"success\"\n              })\n\n          toolMessages.push(toolMessage)\n\n          const toolResultEntry = {\n            role: \"tool\" as const,\n            content: normalizeToolContent(toolMessage.content),\n            messageKind: \"tool_result\" as const,\n            toolCallId: toolMessage.tool_call_id || toolCall.id,\n            toolName: parsedTool.displayName,\n            toolServerName: toolCall.serverName || parsedTool.serverName,\n            toolError: toolMessage.status === \"error\"\n          }\n\n          historyWithUser = [...historyWithUser, toolResultEntry]\n          syncHistory(historyWithUser)\n          syncMessages([\n            ...uiMessages,\n            {\n              isBot: true,\n              name: selectedModel,\n              message: toolResultEntry.content,\n              sources: [],\n              messageKind: \"tool_result\",\n              toolCallId: toolResultEntry.toolCallId,\n              toolName: toolResultEntry.toolName,\n              toolServerName: toolResultEntry.toolServerName,\n              toolError: toolResultEntry.toolError,\n              modelImage: modelInfo?.model_avatar,\n              modelName: modelInfo?.model_name || selectedModel\n            }\n          ])\n\n          if (!temporaryChat && activeHistoryId) {\n            pendingDbWrites.push(\n              saveMessage({\n                history_id: activeHistoryId,\n                name: selectedModel,\n                role: \"tool\",\n                content: toolResultEntry.content,\n                images: [],\n                time: ++nextTimeOffset,\n                message_type: \"normal\",\n                messageKind: \"tool_result\",\n                toolCallId: toolResultEntry.toolCallId,\n                toolName: toolResultEntry.toolName,\n                toolServerName: toolResultEntry.toolServerName,\n                toolError: toolResultEntry.toolError\n              })\n            )\n          }\n        } catch (error) {\n          const toolErrorMessage = getMcpErrorMessage(error)\n          const toolResultEntry = {\n            role: \"tool\" as const,\n            content: toolErrorMessage,\n            messageKind: \"tool_result\" as const,\n            toolCallId: toolCall.id,\n            toolName: parsedTool.displayName,\n            toolServerName: toolCall.serverName || parsedTool.serverName,\n            toolError: true\n          }\n\n          toolMessages.push(\n            new ToolMessage({\n              content: toolErrorMessage,\n              tool_call_id: toolCall.id,\n              status: \"error\"\n            })\n          )\n\n          historyWithUser = [...historyWithUser, toolResultEntry]\n          syncHistory(historyWithUser)\n          syncMessages([\n            ...uiMessages,\n            {\n              isBot: true,\n              name: selectedModel,\n              message: toolErrorMessage,\n              sources: [],\n              messageKind: \"tool_result\",\n              toolCallId: toolCall.id,\n              toolName: parsedTool.displayName,\n              toolServerName: toolCall.serverName || parsedTool.serverName,\n              toolError: true,\n              modelImage: modelInfo?.model_avatar,\n              modelName: modelInfo?.model_name || selectedModel\n            }\n          ])\n\n          if (!temporaryChat && activeHistoryId) {\n            pendingDbWrites.push(\n              saveMessage({\n                history_id: activeHistoryId,\n                name: selectedModel,\n                role: \"tool\",\n                content: toolErrorMessage,\n                images: [],\n                time: ++nextTimeOffset,\n                message_type: \"normal\",\n                messageKind: \"tool_result\",\n                toolCallId: toolCall.id,\n                toolName: parsedTool.displayName,\n                toolServerName: toolCall.serverName || parsedTool.serverName,\n                toolError: true\n              })\n            )\n          }\n        }\n      }\n\n      await Promise.all(pendingDbWrites)\n\n      lcConversation = [...lcConversation, aiMessage, ...toolMessages]\n      appendAssistantPlaceholder()\n    }\n\n    setIsProcessing(false)\n    setStreaming(false)\n    return true\n  } catch (error) {\n    if (isAbortLikeError(error)) {\n      if (finalAssistantText.trim()) {\n        updateAssistantRow((currentMessage) => ({\n          ...currentMessage,\n          message: finalAssistantText\n        }))\n      } else {\n        syncMessages(\n          uiMessages.filter((currentMessage) => currentMessage.id !== currentAssistantId)\n        )\n      }\n\n      setIsProcessing(false)\n      setStreaming(false)\n      return true\n    }\n\n    if (error instanceof McpBootstrapError) {\n      throw error\n    }\n\n    throw error\n  } finally {\n    setActionInfo(null)\n    await client.close()\n  }\n}\n"
  },
  {
    "path": "src/libs/mcp/oauth-flow.ts",
    "content": "import { browser } from \"wxt/browser\"\nimport { McpServerDb, updateMcpServer } from \"@/db/dexie/mcp\"\nimport {\n  generatePkce,\n  discoverOAuthFromMcpServer,\n  getOAuthRedirectUri,\n  registerOAuthClient,\n  buildAuthorizationUrl,\n  exchangeCodeForTokens,\n  refreshOAuthTokens,\n  isOAuthTokenExpired\n} from \"./oauth\"\nimport { inspectMcpServerTools } from \"./remote-tools\"\nimport type { McpServer } from \"./types\"\n\ntype PendingOAuthFlow = {\n  serverId: string\n  state: string\n  codeVerifier: string\n  redirectUri: string\n  tabId: number\n  metadata: McpServer[\"oauthMetadata\"]\n  clientRegistration: McpServer[\"oauthClientRegistration\"]\n}\n\nlet pendingFlow: PendingOAuthFlow | null = null\n\nconst generateState = (): string => {\n  const array = new Uint8Array(32)\n  crypto.getRandomValues(array)\n  return Array.from(array, (b) => b.toString(16).padStart(2, \"0\")).join(\"\")\n}\n\n\nexport const startMcpOAuthFlow = async (\n  server: McpServer\n): Promise<{ success: boolean; error?: string }> => {\n  try {\n    const discovery = await discoverOAuthFromMcpServer(server.url)\n    const metadata = discovery.metadata\n    const redirectUri = await getOAuthRedirectUri()\n    let clientRegistration = server.oauthClientRegistration\n    if (!clientRegistration && metadata.registrationEndpoint) {\n      clientRegistration = await registerOAuthClient(\n        metadata.registrationEndpoint,\n        redirectUri,\n        `Page Assist - ${server.name}`\n      )\n      await updateMcpServer({\n        id: server.id,\n        oauthMetadata: metadata,\n        oauthClientRegistration: clientRegistration,\n        authType: \"oauth\"\n      })\n    }\n\n    if (!clientRegistration) {\n      return {\n        success: false,\n        error:\n          \"No client registration available. The server does not support dynamic client registration. Please configure OAuth client credentials manually.\"\n      }\n    }\n\n    const { codeVerifier, codeChallenge } = await generatePkce()\n    const state = generateState()\n    const authUrl = buildAuthorizationUrl({\n      metadata,\n      clientRegistration,\n      redirectUri,\n      codeChallenge,\n      state,\n      scopes: metadata.scopesSupported\n    })\n\n    const tab = await browser.tabs.create({ url: authUrl })\n\n    pendingFlow = {\n      serverId: server.id,\n      state,\n      codeVerifier,\n      redirectUri,\n      tabId: tab.id!,\n      metadata,\n      clientRegistration\n    }\n\n    startTabMonitoring(redirectUri)\n\n    return { success: true }\n  } catch (error) {\n    return {\n      success: false,\n      error: error instanceof Error ? error.message : String(error)\n    }\n  }\n}\n\n\nconst startTabMonitoring = (redirectUri: string) => {\n  const listener = async (\n    tabId: number,\n    changeInfo: { url?: string; status?: string }\n  ) => {\n    if (!pendingFlow) {\n      browser.tabs.onUpdated.removeListener(listener)\n      return\n    }\n\n    if (tabId !== pendingFlow.tabId) return\n    if (!changeInfo.url) return\n\n    if (!changeInfo.url.startsWith(redirectUri)) return\n\n    const url = new URL(changeInfo.url)\n    const code = url.searchParams.get(\"code\")\n    const state = url.searchParams.get(\"state\")\n    const error = url.searchParams.get(\"error\")\n\n    browser.tabs.onUpdated.removeListener(listener)\n\n    if (error) {\n      const errorDesc = url.searchParams.get(\"error_description\") || error\n      pendingFlow = null\n      console.error(\"MCP OAuth error:\", errorDesc)\n      return\n    }\n\n    if (!code || state !== pendingFlow.state) {\n      pendingFlow = null\n      return\n    }\n\n    try {\n      const tokens = await exchangeCodeForTokens({\n        metadata: pendingFlow.metadata!,\n        clientRegistration: pendingFlow.clientRegistration!,\n        code,\n        redirectUri: pendingFlow.redirectUri,\n        codeVerifier: pendingFlow.codeVerifier\n      })\n\n      const updatedServer = await updateMcpServer({\n        id: pendingFlow.serverId,\n        authType: \"oauth\",\n        oauthTokens: tokens,\n        oauthMetadata: pendingFlow.metadata,\n        oauthClientRegistration: pendingFlow.clientRegistration\n      })\n\n      try {\n        await browser.tabs.remove(tabId)\n      } catch {\n      }\n      try {\n        const validation = await inspectMcpServerTools(updatedServer)\n        await updateMcpServer({\n          id: updatedServer.id,\n          cachedTools: validation.cachedTools,\n          toolsLastSyncedAt: validation.toolsLastSyncedAt,\n          toolsSyncError: undefined\n        })\n      } catch (toolErr) {\n        console.error(\"MCP OAuth: tools fetch after auth failed:\", toolErr)\n      }\n    } catch (err) {\n      console.error(\"MCP OAuth token exchange failed:\", err)\n    } finally {\n      pendingFlow = null\n    }\n  }\n\n  browser.tabs.onUpdated.addListener(listener)\n  const closeListener = (closedTabId: number) => {\n    if (pendingFlow && closedTabId === pendingFlow.tabId) {\n      pendingFlow = null\n      browser.tabs.onUpdated.removeListener(listener)\n      browser.tabs.onRemoved.removeListener(closeListener)\n    }\n  }\n  browser.tabs.onRemoved.addListener(closeListener)\n}\n\n\nexport const ensureFreshOAuthTokens = async (\n  server: McpServer\n): Promise<McpServer | null> => {\n  if (server.authType !== \"oauth\" || !server.oauthTokens) return null\n\n  if (!isOAuthTokenExpired(server.oauthTokens)) {\n    return server\n  }\n\n  if (!server.oauthTokens.refreshToken || !server.oauthMetadata || !server.oauthClientRegistration) {\n    return null\n  }\n\n  try {\n    const newTokens = await refreshOAuthTokens({\n      metadata: server.oauthMetadata,\n      clientRegistration: server.oauthClientRegistration,\n      refreshToken: server.oauthTokens.refreshToken\n    })\n\n    const updated = await updateMcpServer({\n      id: server.id,\n      oauthTokens: newTokens\n    })\n\n    return updated\n  } catch {\n    return null\n  }\n}\n\n\nexport const disconnectMcpOAuth = async (serverId: string) => {\n  await updateMcpServer({\n    id: serverId,\n    authType: \"none\",\n    oauthTokens: undefined\n  })\n}\n"
  },
  {
    "path": "src/libs/mcp/oauth.ts",
    "content": "import { cleanUrl } from \"~/libs/clean-url\"\nimport { getPageShareUrl } from \"~/services/ollama\"\nimport type {\n  McpOAuthClientRegistration,\n  McpOAuthMetadata,\n  McpOAuthTokens\n} from \"./types\"\n\nconst generateRandomString = (length: number): string => {\n  const array = new Uint8Array(length)\n  crypto.getRandomValues(array)\n  return Array.from(array, (b) => b.toString(16).padStart(2, \"0\")).join(\"\")\n}\n\nconst base64UrlEncode = (buffer: ArrayBuffer): string => {\n  const bytes = new Uint8Array(buffer)\n  let binary = \"\"\n  for (const byte of bytes) {\n    binary += String.fromCharCode(byte)\n  }\n  return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\")\n}\n\nexport const generatePkce = async () => {\n  const codeVerifier = generateRandomString(64)\n  const encoded = new TextEncoder().encode(codeVerifier)\n  const digest = await crypto.subtle.digest(\"SHA-256\", encoded)\n  const codeChallenge = base64UrlEncode(digest)\n\n  return { codeVerifier, codeChallenge }\n}\n\n// --- OAuth Discovery ---\n\nexport type OAuthDiscoveryResult = {\n  metadata: McpOAuthMetadata\n}\n\nconst fetchJson = async (url: string): Promise<any> => {\n  const response = await fetch(url)\n  if (!response.ok) {\n    throw new Error(`Failed to fetch ${url}: ${response.status}`)\n  }\n  return response.json()\n}\n\nconst discoverFromPrmUrl = async (\n  prmUrl: string\n): Promise<OAuthDiscoveryResult> => {\n  const prm = await fetchJson(prmUrl)\n\n  const authServers: string[] = prm.authorization_servers || []\n  if (authServers.length === 0) {\n    throw new Error(\"No authorization servers found in protected resource metadata\")\n  }\n\n  const authServerBase = cleanUrl(authServers[0])\n\n  let authMeta: any\n  try {\n    authMeta = await fetchJson(\n      `${authServerBase}/.well-known/openid-configuration`\n    )\n  } catch {\n    authMeta = await fetchJson(\n      `${authServerBase}/.well-known/oauth-authorization-server`\n    )\n  }\n\n  if (!authMeta.authorization_endpoint || !authMeta.token_endpoint) {\n    throw new Error(\"Authorization server metadata missing required endpoints\")\n  }\n\n  return {\n    metadata: {\n      authorizationEndpoint: authMeta.authorization_endpoint,\n      tokenEndpoint: authMeta.token_endpoint,\n      registrationEndpoint: authMeta.registration_endpoint || undefined,\n      issuer: authMeta.issuer || undefined,\n      resourceMetadataUrl: prmUrl,\n      scopesSupported: prm.scopes_supported || authMeta.scopes_supported\n    }\n  }\n}\n\nexport const discoverOAuthFromMcpServer = async (\n  mcpServerUrl: string\n): Promise<OAuthDiscoveryResult> => {\n  const serverUrl = cleanUrl(mcpServerUrl)\n  try {\n    const probeResponse = await fetch(serverUrl, { method: \"POST\" })\n\n    if (probeResponse.status === 401) {\n      const wwwAuth = probeResponse.headers.get(\"WWW-Authenticate\") || \"\"\n      const resourceMetadataMatch = wwwAuth.match(\n        /resource_metadata=\"([^\"]+)\"/\n      )\n\n      if (resourceMetadataMatch) {\n        return await discoverFromPrmUrl(resourceMetadataMatch[1])\n      }\n    }\n  } catch {\n  }\n\n  const origin = new URL(serverUrl).origin\n  const prmUrl = `${origin}/.well-known/oauth-protected-resource`\n\n  return await discoverFromPrmUrl(prmUrl)\n}\n\nexport const discoverOAuthFrom401 = async (\n  wwwAuthenticate: string\n): Promise<OAuthDiscoveryResult | null> => {\n  const resourceMetadataMatch = wwwAuthenticate.match(\n    /resource_metadata=\"([^\"]+)\"/\n  )\n  if (!resourceMetadataMatch) {\n    return null\n  }\n\n  const prmUrl = resourceMetadataMatch[1]\n  const prm = await fetchJson(prmUrl)\n\n  const authServers: string[] = prm.authorization_servers || []\n  if (authServers.length === 0) {\n    return null\n  }\n\n  const authServerBase = cleanUrl(authServers[0])\n\n  let authMeta: any\n  try {\n    authMeta = await fetchJson(\n      `${authServerBase}/.well-known/openid-configuration`\n    )\n  } catch {\n    authMeta = await fetchJson(\n      `${authServerBase}/.well-known/oauth-authorization-server`\n    )\n  }\n\n  if (!authMeta.authorization_endpoint || !authMeta.token_endpoint) {\n    return null\n  }\n\n  return {\n    metadata: {\n      authorizationEndpoint: authMeta.authorization_endpoint,\n      tokenEndpoint: authMeta.token_endpoint,\n      registrationEndpoint: authMeta.registration_endpoint || undefined,\n      issuer: authMeta.issuer || undefined,\n      resourceMetadataUrl: prmUrl,\n      scopesSupported: prm.scopes_supported || authMeta.scopes_supported\n    }\n  }\n}\n\nexport const getOAuthRedirectUri = async (): Promise<string> => {\n  const pageShareUrl = await getPageShareUrl()\n  const base = cleanUrl(pageShareUrl).replace(/\\/+$/, \"\")\n  return `${base}/mcp/oauth/callback`\n}\n\nexport const registerOAuthClient = async (\n  registrationEndpoint: string,\n  redirectUri: string,\n  clientName: string = \"Page Assist\"\n): Promise<McpOAuthClientRegistration> => {\n  const response = await fetch(registrationEndpoint, {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/json\" },\n    body: JSON.stringify({\n      client_name: clientName,\n      redirect_uris: [redirectUri],\n      grant_types: [\"authorization_code\", \"refresh_token\"],\n      response_types: [\"code\"],\n      token_endpoint_auth_method: \"none\"\n    })\n  })\n\n  if (!response.ok) {\n    const text = await response.text()\n    throw new Error(`Dynamic client registration failed: ${text}`)\n  }\n\n  const data = await response.json()\n  console.log(\"[MCP OAuth] DCR response:\", JSON.stringify(data, null, 2))\n\n  return {\n    clientId: data.client_id,\n    clientSecret: data.client_secret || undefined,\n    registrationAccessToken: data.registration_access_token || undefined,\n    redirectUris: data.redirect_uris\n  }\n}\n\nexport const buildAuthorizationUrl = ({\n  metadata,\n  clientRegistration,\n  redirectUri,\n  codeChallenge,\n  state,\n  scopes\n}: {\n  metadata: McpOAuthMetadata\n  clientRegistration: McpOAuthClientRegistration\n  redirectUri: string\n  codeChallenge: string\n  state: string\n  scopes?: string[]\n}): string => {\n  const params = new URLSearchParams({\n    response_type: \"code\",\n    client_id: clientRegistration.clientId,\n    redirect_uri: redirectUri,\n    code_challenge: codeChallenge,\n    code_challenge_method: \"S256\",\n    state\n  })\n\n  if (scopes && scopes.length > 0) {\n    params.set(\"scope\", scopes.join(\" \"))\n  }\n\n  return `${metadata.authorizationEndpoint}?${params.toString()}`\n}\n\nexport const exchangeCodeForTokens = async ({\n  metadata,\n  clientRegistration,\n  code,\n  redirectUri,\n  codeVerifier\n}: {\n  metadata: McpOAuthMetadata\n  clientRegistration: McpOAuthClientRegistration\n  code: string\n  redirectUri: string\n  codeVerifier: string\n}): Promise<McpOAuthTokens> => {\n  console.log(\"[MCP OAuth] Token exchange redirect_uri:\", redirectUri)\n  console.log(\"[MCP OAuth] Token exchange client_id:\", clientRegistration.clientId)\n  const body = new URLSearchParams({\n    grant_type: \"authorization_code\",\n    code,\n    redirect_uri: redirectUri,\n    code_verifier: codeVerifier,\n    client_id: clientRegistration.clientId\n  })\n\n  if (clientRegistration.clientSecret) {\n    body.set(\"client_secret\", clientRegistration.clientSecret)\n  }\n\n  const response = await fetch(metadata.tokenEndpoint, {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n    body: body.toString()\n  })\n\n  if (!response.ok) {\n    const text = await response.text()\n    throw new Error(`Token exchange failed: ${text}`)\n  }\n\n  const data = await response.json()\n\n  return {\n    accessToken: data.access_token,\n    refreshToken: data.refresh_token || undefined,\n    tokenType: data.token_type || \"Bearer\",\n    expiresAt: data.expires_in\n      ? Date.now() + data.expires_in * 1000\n      : undefined,\n    scope: data.scope || undefined\n  }\n}\n\nexport const refreshOAuthTokens = async ({\n  metadata,\n  clientRegistration,\n  refreshToken\n}: {\n  metadata: McpOAuthMetadata\n  clientRegistration: McpOAuthClientRegistration\n  refreshToken: string\n}): Promise<McpOAuthTokens> => {\n  const body = new URLSearchParams({\n    grant_type: \"refresh_token\",\n    refresh_token: refreshToken,\n    client_id: clientRegistration.clientId\n  })\n\n  if (clientRegistration.clientSecret) {\n    body.set(\"client_secret\", clientRegistration.clientSecret)\n  }\n\n  const response = await fetch(metadata.tokenEndpoint, {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n    body: body.toString()\n  })\n\n  if (!response.ok) {\n    throw new Error(`Token refresh failed: ${response.status}`)\n  }\n\n  const data = await response.json()\n\n  return {\n    accessToken: data.access_token,\n    refreshToken: data.refresh_token || refreshToken,\n    tokenType: data.token_type || \"Bearer\",\n    expiresAt: data.expires_in\n      ? Date.now() + data.expires_in * 1000\n      : undefined,\n    scope: data.scope || undefined\n  }\n}\n\nexport const isOAuthTokenExpired = (tokens?: McpOAuthTokens): boolean => {\n  if (!tokens?.accessToken) return true\n  if (!tokens.expiresAt) return false\n  return Date.now() > tokens.expiresAt - 60_000\n}\n\nexport const hasValidOAuthTokens = (tokens?: McpOAuthTokens): boolean => {\n  if (!tokens?.accessToken) return false\n  return !isOAuthTokenExpired(tokens)\n}\n"
  },
  {
    "path": "src/libs/mcp/remote-tools.ts",
    "content": "import { Client } from \"@modelcontextprotocol/sdk/client/index.js\"\nimport { StreamableHTTPClientTransport } from \"@modelcontextprotocol/sdk/client/streamableHttp.js\"\nimport { CfWorkerJsonSchemaValidator } from \"@modelcontextprotocol/sdk/validation/cfworker\"\nimport { getMcpErrorMessage } from \"./errors\"\nimport { McpAvailableTool, McpServer, McpServerInput } from \"./types\"\nimport { buildMcpHeaders } from \"./utils\"\nimport { Implementation } from \"@modelcontextprotocol/sdk/types.js\"\n\nexport type McpConnectableServer = Pick<\n  McpServerInput,\n  \"name\" | \"url\" | \"authType\" | \"bearerToken\" | \"headers\" | \"oauthTokens\"\n>\n\nexport type McpRemoteTool = {\n  name: string\n  description?: string\n  inputSchema?: unknown\n}\n\nexport type McpServerConnection = {\n  client: Client\n  transport: StreamableHTTPClientTransport\n}\n\nexport type McpToolValidationResult = {\n  cachedTools: McpAvailableTool[]\n  toolsLastSyncedAt: number\n  toolsSyncError?: string\n}\n\nconst MCP_CLIENT_INFO: Implementation = {\n  name: \"page-assist\",\n  version: \"1\",\n  description: \"Use your locally running AI models to assist you in your web browsing\",\n  title: \"Page Assist\",\n  websiteUrl: \"https://pageassist.xyz\",\n  icons: [\n    {\n      src: \"https://pageassist.xyz/favicon.ico\",\n      mimeType: \"image/x-icon\",\n      theme: \"dark\"\n    }\n  ]\n}\n\nexport const openMcpServerConnection = async (\n  server: McpConnectableServer\n): Promise<McpServerConnection> => {\n  let url: URL\n  try {\n    url = new URL(server.url)\n  } catch (error) {\n    throw new Error(`Invalid MCP URL for server \"${server.name}\"`)\n  }\n\n  const headers = buildMcpHeaders({\n    authType: server.authType,\n    bearerToken: server.bearerToken,\n    headers: server.headers,\n    oauthTokens: server.oauthTokens\n  })\n\n  const transport = new StreamableHTTPClientTransport(url, {\n    ...(Object.keys(headers).length > 0\n      ? { requestInit: { headers } }\n      : {})\n  })\n\n  const client = new Client(MCP_CLIENT_INFO, {\n    jsonSchemaValidator: new CfWorkerJsonSchemaValidator()\n  })\n\n  try {\n    await client.connect(transport)\n  } catch (error) {\n    throw new Error(\n      `Failed to connect to MCP server \"${server.name}\": ${getMcpErrorMessage(error)}`\n    )\n  }\n\n  return {\n    client,\n    transport\n  }\n}\n\nexport const closeMcpServerConnection = async (\n  connection: McpServerConnection\n) => {\n  try {\n    await connection.transport.terminateSession?.()\n  } catch (error) {\n    // Ignore session termination errors during cleanup.\n  }\n\n  await connection.client.close()\n}\n\nexport const listRemoteMcpTools = async (\n  client: Client,\n  serverName: string\n): Promise<McpRemoteTool[]> => {\n  const tools: McpRemoteTool[] = []\n  let cursor: string | undefined = undefined\n\n  try {\n    do {\n      const response = await client.listTools(cursor ? { cursor } : undefined)\n\n      for (const remoteTool of response.tools || []) {\n        if (!remoteTool?.name) {\n          continue\n        }\n\n        tools.push({\n          name: remoteTool.name,\n          description: remoteTool.description || undefined,\n          inputSchema: remoteTool.inputSchema\n        })\n      }\n\n      cursor = response.nextCursor\n    } while (cursor)\n  } catch (error) {\n    throw new Error(\n      `Failed to load MCP tools from \"${serverName}\": ${getMcpErrorMessage(error)}`\n    )\n  }\n\n  return tools\n}\n\nexport const toCachedMcpTools = (\n  tools: McpRemoteTool[]\n): McpAvailableTool[] =>\n  tools.map((tool) => ({\n    name: tool.name,\n    description: tool.description,\n    inputSchema: tool.inputSchema\n  }))\n\nexport const inspectMcpServerTools = async (\n  server: McpConnectableServer\n): Promise<McpToolValidationResult> => {\n  const connection = await openMcpServerConnection(server)\n\n  try {\n    const cachedTools = toCachedMcpTools(\n      await listRemoteMcpTools(connection.client, server.name)\n    )\n\n    if (cachedTools.length === 0) {\n      throw new Error(`No MCP tools were loaded from \"${server.name}\".`)\n    }\n\n    return {\n      cachedTools,\n      toolsLastSyncedAt: Date.now(),\n      toolsSyncError: undefined\n    }\n  } finally {\n    await closeMcpServerConnection(connection)\n  }\n}\n"
  },
  {
    "path": "src/libs/mcp/tool-schema.ts",
    "content": "const isPlainObject = (value: unknown): value is Record<string, any> =>\n  typeof value === \"object\" && value !== null && !Array.isArray(value)\n\nconst cloneValue = <T>(value: T): T => {\n  if (Array.isArray(value)) {\n    return value.map((item) => cloneValue(item)) as T\n  }\n\n  if (isPlainObject(value)) {\n    return Object.fromEntries(\n      Object.entries(value).map(([key, item]) => [key, cloneValue(item)])\n    ) as T\n  }\n\n  return value\n}\n\nconst deepMergeSchemas = (\n  target: Record<string, any>,\n  source: Record<string, any>\n) => {\n  const merged = { ...target }\n\n  for (const [key, value] of Object.entries(source)) {\n    const currentValue = merged[key]\n\n    if (key === \"required\" && Array.isArray(value)) {\n      merged[key] = Array.from(\n        new Set([...(Array.isArray(currentValue) ? currentValue : []), ...value])\n      )\n      continue\n    }\n\n    if (key === \"enum\" && Array.isArray(value)) {\n      merged[key] = Array.from(\n        new Set([...(Array.isArray(currentValue) ? currentValue : []), ...value])\n      )\n      continue\n    }\n\n    if (key === \"properties\" && isPlainObject(currentValue) && isPlainObject(value)) {\n      merged[key] = { ...currentValue }\n\n      for (const [propertyKey, propertyValue] of Object.entries(value)) {\n        if (\n          isPlainObject(merged[key][propertyKey]) &&\n          isPlainObject(propertyValue)\n        ) {\n          merged[key][propertyKey] = deepMergeSchemas(\n            merged[key][propertyKey],\n            propertyValue\n          )\n        } else {\n          merged[key][propertyKey] = propertyValue\n        }\n      }\n\n      continue\n    }\n\n    if (isPlainObject(currentValue) && isPlainObject(value)) {\n      merged[key] = deepMergeSchemas(currentValue, value)\n      continue\n    }\n\n    merged[key] = value\n  }\n\n  return merged\n}\n\nconst resolveSchemaRefs = (\n  schema: unknown,\n  definitions: Record<string, any>,\n  visitedRefs = new Set<string>()\n): any => {\n  if (!isPlainObject(schema) && !Array.isArray(schema)) {\n    return schema\n  }\n\n  if (Array.isArray(schema)) {\n    return schema.map((item) => resolveSchemaRefs(item, definitions, visitedRefs))\n  }\n\n  const ref = schema.$ref\n  if (typeof ref === \"string\") {\n    const defsMatch = ref.match(/^#\\/\\$defs\\/(.+)$/)\n    const definitionsMatch = ref.match(/^#\\/definitions\\/(.+)$/)\n    const match = defsMatch || definitionsMatch\n\n    if (match) {\n      const definition = definitions[match[1]]\n\n      if (definition && !visitedRefs.has(ref)) {\n        const nextVisitedRefs = new Set(visitedRefs)\n        nextVisitedRefs.add(ref)\n\n        const { $ref, ...rest } = schema\n        return deepMergeSchemas(\n          resolveSchemaRefs(definition, definitions, nextVisitedRefs) || {},\n          Object.fromEntries(\n            Object.entries(rest).map(([key, value]) => [\n              key,\n              resolveSchemaRefs(value, definitions, nextVisitedRefs)\n            ])\n          )\n        )\n      }\n    }\n  }\n\n  return Object.fromEntries(\n    Object.entries(schema).map(([key, value]) => [\n      key,\n      resolveSchemaRefs(value, definitions, visitedRefs)\n    ])\n  )\n}\n\nconst simplifyUnionSchemas = (schemas: Record<string, any>[]) => {\n  const objectSchemas = schemas.filter(\n    (schema) =>\n      isPlainObject(schema) &&\n      (schema.type === \"object\" || isPlainObject(schema.properties))\n  )\n\n  if (objectSchemas.length === 0) {\n    return undefined\n  }\n\n  const merged = objectSchemas.reduce<Record<string, any>>(\n    (accumulator, schema) => deepMergeSchemas(accumulator, schema),\n    {}\n  )\n\n  const requiredLists = objectSchemas\n    .map((schema) => schema.required)\n    .filter(Array.isArray) as string[][]\n\n  if (requiredLists.length > 1) {\n    const [first, ...rest] = requiredLists\n    merged.required = first.filter((key) =>\n      rest.every((required) => required.includes(key))\n    )\n  }\n\n  return merged\n}\n\nconst simplifySchema = (schema: unknown): any => {\n  if (!isPlainObject(schema) && !Array.isArray(schema)) {\n    return schema\n  }\n\n  if (Array.isArray(schema)) {\n    return schema.map((item) => simplifySchema(item))\n  }\n\n  let nextSchema = { ...schema }\n\n  const conditionalBranches = [schema.then, schema.else].filter(isPlainObject)\n  for (const branch of conditionalBranches) {\n    nextSchema = deepMergeSchemas(nextSchema, branch)\n  }\n\n  if (Array.isArray(schema.allOf)) {\n    nextSchema = schema.allOf.reduce<Record<string, any>>(\n      (accumulator, childSchema) =>\n        deepMergeSchemas(accumulator, simplifySchema(childSchema) || {}),\n      nextSchema\n    )\n  }\n\n  const unionSchemas = [schema.anyOf, schema.oneOf].find(Array.isArray) as\n    | Record<string, any>[]\n    | undefined\n\n  if (unionSchemas?.length) {\n    const mergedUnionSchema = simplifyUnionSchemas(\n      unionSchemas\n        .map((item) => simplifySchema(item))\n        .filter(isPlainObject)\n    )\n\n    if (mergedUnionSchema) {\n      nextSchema = deepMergeSchemas(nextSchema, mergedUnionSchema)\n    }\n  }\n\n  delete nextSchema.$schema\n  delete nextSchema.$defs\n  delete nextSchema.definitions\n  delete nextSchema.$ref\n  delete nextSchema.allOf\n  delete nextSchema.anyOf\n  delete nextSchema.oneOf\n  delete nextSchema.not\n  delete nextSchema.if\n  delete nextSchema.then\n  delete nextSchema.else\n  delete nextSchema.unevaluatedProperties\n\n  if (isPlainObject(nextSchema.properties)) {\n    nextSchema.properties = Object.fromEntries(\n      Object.entries(nextSchema.properties).map(([key, value]) => [\n        key,\n        simplifySchema(value)\n      ])\n    )\n  }\n\n  if (Array.isArray(nextSchema.items)) {\n    nextSchema.items = nextSchema.items.map((item: unknown) => simplifySchema(item))\n  } else if (isPlainObject(nextSchema.items)) {\n    nextSchema.items = simplifySchema(nextSchema.items)\n  }\n\n  if (isPlainObject(nextSchema.additionalProperties)) {\n    nextSchema.additionalProperties = simplifySchema(\n      nextSchema.additionalProperties\n    )\n  }\n\n  if (isPlainObject(nextSchema.properties) && !nextSchema.type) {\n    nextSchema.type = \"object\"\n  }\n\n  if (nextSchema.type === \"object\" && !isPlainObject(nextSchema.properties)) {\n    nextSchema.properties = {}\n  }\n\n  return nextSchema\n}\n\nexport const normalizeMcpToolSchema = (schema: unknown) => {\n  const normalizedInput = isPlainObject(schema)\n    ? cloneValue(schema)\n    : { type: \"object\", properties: {} }\n\n  const definitions = {\n    ...(isPlainObject(normalizedInput.$defs) ? normalizedInput.$defs : {}),\n    ...(isPlainObject(normalizedInput.definitions)\n      ? normalizedInput.definitions\n      : {})\n  }\n\n  const resolvedSchema = resolveSchemaRefs(normalizedInput, definitions)\n  const simplifiedSchema = simplifySchema(resolvedSchema)\n\n  if (!isPlainObject(simplifiedSchema)) {\n    return {\n      type: \"object\",\n      properties: {}\n    }\n  }\n\n  if (\n    simplifiedSchema.type === \"object\" &&\n    !isPlainObject(simplifiedSchema.properties)\n  ) {\n    simplifiedSchema.properties = {}\n  }\n\n  return simplifiedSchema\n}\n"
  },
  {
    "path": "src/libs/mcp/types.ts",
    "content": "export type ChatMessageKind = \"text\" | \"assistant_tool_calls\" | \"tool_result\"\n\nexport type McpHeader = {\n  key: string\n  value: string\n}\n\nexport type McpAvailableTool = {\n  name: string\n  description?: string\n  inputSchema?: unknown\n  enabled?: boolean\n}\n\nexport type McpOAuthTokens = {\n  accessToken: string\n  refreshToken?: string\n  tokenType: string\n  expiresAt?: number\n  scope?: string\n}\n\nexport type McpOAuthClientRegistration = {\n  clientId: string\n  clientSecret?: string\n  registrationAccessToken?: string\n  redirectUris?: string[]\n}\n\nexport type McpOAuthMetadata = {\n  authorizationEndpoint: string\n  tokenEndpoint: string\n  registrationEndpoint?: string\n  issuer?: string\n  resourceMetadataUrl?: string\n  scopesSupported?: string[]\n}\n\nexport type McpServer = {\n  id: string\n  name: string\n  transport: \"http\"\n  url: string\n  enabled: boolean\n  authType: \"none\" | \"bearer\" | \"oauth\"\n  bearerToken?: string\n  headers?: McpHeader[]\n  cachedTools?: McpAvailableTool[]\n  toolsLastSyncedAt?: number\n  toolsSyncError?: string\n  oauthTokens?: McpOAuthTokens\n  oauthClientRegistration?: McpOAuthClientRegistration\n  oauthMetadata?: McpOAuthMetadata\n  createdAt: number\n  updatedAt: number\n}\n\nexport type McpServerInput = Pick<\n  McpServer,\n  | \"name\"\n  | \"transport\"\n  | \"url\"\n  | \"enabled\"\n  | \"authType\"\n  | \"bearerToken\"\n  | \"headers\"\n  | \"oauthTokens\"\n  | \"oauthClientRegistration\"\n  | \"oauthMetadata\"\n>\n\nexport type McpToolCall = {\n  id: string\n  name: string\n  args?: unknown\n  type?: \"tool_call\"\n  serverName?: string\n  displayName?: string\n}\n\nexport type ChatActionInfo =\n  | string\n  | {\n    type: \"mcp\"\n    phase: \"connecting\" | \"loading_tools\" | \"calling_tool\" | \"waiting_result\"\n    serverName?: string\n    toolName?: string\n    toolCount?: number\n  }\n"
  },
  {
    "path": "src/libs/mcp/utils.ts",
    "content": "import {\n  ChatActionInfo,\n  ChatMessageKind,\n  McpHeader,\n  McpOAuthTokens,\n  McpServerInput,\n  McpToolCall\n} from \"./types\"\n\nexport const MCP_TOOL_NAME_SEPARATOR = \"__\"\n\nexport const isTraceMessageKind = (messageKind?: ChatMessageKind) =>\n  messageKind === \"assistant_tool_calls\" || messageKind === \"tool_result\"\n\nexport const isTextMessageKind = (messageKind?: ChatMessageKind) =>\n  !messageKind || messageKind === \"text\"\n\nexport const isConversationMessage = ({\n  role,\n  messageKind\n}: {\n  role?: string\n  messageKind?: ChatMessageKind\n}) => role !== \"tool\" && !isTraceMessageKind(messageKind)\n\nexport const parseMcpToolName = (rawName: string) => {\n  const separatorIndex = rawName.indexOf(MCP_TOOL_NAME_SEPARATOR)\n\n  if (separatorIndex === -1) {\n    return {\n      rawName,\n      serverName: undefined,\n      displayName: rawName\n    }\n  }\n\n  return {\n    rawName,\n    serverName: rawName.slice(0, separatorIndex),\n    displayName: rawName.slice(separatorIndex + MCP_TOOL_NAME_SEPARATOR.length)\n  }\n}\n\nexport const sanitizeHeaders = (headers?: McpHeader[]) =>\n  (headers || []).filter(\n    (header) =>\n      header &&\n      typeof header.key === \"string\" &&\n      typeof header.value === \"string\" &&\n      header.key.trim().length > 0 &&\n      header.value.trim().length > 0\n  )\n\nexport const normalizeMcpServerInput = (\n  server: Partial<McpServerInput>\n): McpServerInput => {\n  const authType = server.authType ?? \"none\"\n\n  return {\n    name: server.name?.trim() ?? \"\",\n    transport: \"http\",\n    url: server.url?.trim() ?? \"\",\n    enabled: server.enabled ?? true,\n    authType,\n    bearerToken:\n      authType === \"bearer\" ? server.bearerToken?.trim() || undefined : undefined,\n    headers: sanitizeHeaders(server.headers),\n    oauthTokens: authType === \"oauth\" ? server.oauthTokens : undefined,\n    oauthClientRegistration:\n      authType === \"oauth\" ? server.oauthClientRegistration : undefined,\n    oauthMetadata: authType === \"oauth\" ? server.oauthMetadata : undefined\n  }\n}\n\nexport const getMcpServerConfigFingerprint = (\n  server: Partial<McpServerInput>\n) => {\n  const normalized = normalizeMcpServerInput(server)\n\n  return JSON.stringify({\n    name: normalized.name,\n    transport: normalized.transport,\n    url: normalized.url,\n    authType: normalized.authType,\n    bearerToken: normalized.bearerToken ?? \"\",\n    headers: [...normalized.headers].sort((left, right) =>\n      left.key.localeCompare(right.key)\n    )\n  })\n}\n\nexport const buildMcpHeaders = ({\n  authType,\n  bearerToken,\n  headers,\n  oauthTokens\n}: {\n  authType: \"none\" | \"bearer\" | \"oauth\"\n  bearerToken?: string\n  headers?: McpHeader[]\n  oauthTokens?: McpOAuthTokens\n}) => {\n  const defaultHeaders: Record<string, string> = {}\n\n  if (authType === \"bearer\" && bearerToken?.trim()) {\n    defaultHeaders.Authorization = `Bearer ${bearerToken.trim()}`\n  } else if (authType === \"oauth\" && oauthTokens?.accessToken) {\n    defaultHeaders.Authorization = `Bearer ${oauthTokens.accessToken}`\n  }\n\n  for (const header of sanitizeHeaders(headers)) {\n    defaultHeaders[header.key.trim()] = header.value.trim()\n  }\n\n  return defaultHeaders\n}\n\nexport const toStoredToolCalls = (toolCalls: McpToolCall[] = []): McpToolCall[] =>\n  toolCalls.map((toolCall) => {\n    const parsed = parseMcpToolName(toolCall.name)\n\n    return {\n      id: toolCall.id,\n      name: toolCall.name,\n      args: toolCall.args ?? {},\n      type: \"tool_call\",\n      serverName: toolCall.serverName || parsed.serverName,\n      displayName: toolCall.displayName || parsed.displayName\n    }\n  })\n\nexport const summarizeToolCalls = (toolCalls: McpToolCall[] = []) => {\n  if (toolCalls.length === 0) {\n    return \"\"\n  }\n\n  return toolCalls\n    .map((toolCall) => toolCall.displayName || parseMcpToolName(toolCall.name).displayName)\n    .join(\", \")\n}\n\nexport const stringifyToolArgs = (args: unknown) => {\n  try {\n    return JSON.stringify(args ?? {}, null, 2)\n  } catch (error) {\n    return String(args ?? \"\")\n  }\n}\n\nexport const normalizeToolContent = (content: unknown): string => {\n  if (typeof content === \"string\") {\n    return content\n  }\n\n  if (Array.isArray(content)) {\n    const flattened = content\n      .map((item) => {\n        if (typeof item === \"string\") {\n          return item\n        }\n\n        if (item && typeof item === \"object\") {\n          if (\"text\" in item && typeof item.text === \"string\") {\n            return item.text\n          }\n\n          if (\"type\" in item && typeof item.type === \"string\") {\n            return JSON.stringify(item, null, 2)\n          }\n        }\n\n        return String(item ?? \"\")\n      })\n      .filter(Boolean)\n\n    return flattened.join(\"\\n\\n\")\n  }\n\n  if (content && typeof content === \"object\") {\n    try {\n      return JSON.stringify(content, null, 2)\n    } catch (error) {\n      return String(content)\n    }\n  }\n\n  return String(content ?? \"\")\n}\n\nexport const createMcpActionInfo = (\n  phase: \"connecting\" | \"loading_tools\" | \"calling_tool\" | \"waiting_result\",\n  details?: Omit<Extract<ChatActionInfo, { type: \"mcp\" }>, \"type\" | \"phase\">\n): ChatActionInfo => ({\n  type: \"mcp\",\n  phase,\n  ...details\n})\n"
  },
  {
    "path": "src/libs/model-utils.ts",
    "content": "/**\n * Utility functions for model capabilities and detection\n */\n\n/**\n * Check if a model supports thinking/reasoning mode\n * Based on Ollama's documentation, models like DeepSeek R1, Qwen, and others support thinking\n *\n * @param modelId - The model identifier (e.g., \"deepseek-r1:7b\", \"qwen2.5:14b\")\n * @returns true if the model supports thinking mode\n */\nexport function isThinkingCapableModel(modelId: string | null | undefined): boolean {\n  if (!modelId) return false;\n\n  const normalizedModel = modelId.toLowerCase();\n\n  // List of model name patterns that support thinking mode\n  const thinkingPatterns = [\n    'deepseek-r1',      // DeepSeek R1 family\n    'deepseek-v3',      // DeepSeek v3.1\n    'qwen',             // Qwen 3 family (text and vision)\n    'gpt-oss',          // GPT-OSS\n    'o1',               // OpenAI o1 models (if used via compatible API)\n    'thinking',         // Generic thinking models\n    'reason',           // Models with reasoning in the name\n  ];\n\n  return thinkingPatterns.some(pattern => normalizedModel.includes(pattern));\n}\n\n/**\n * Check if a model is GPT-OSS (which requires level-based thinking instead of boolean)\n *\n * @param modelId - The model identifier\n * @returns true if the model is gpt-oss\n */\nexport function isGptOssModel(modelId: string | null | undefined): boolean {\n  if (!modelId) return false;\n  return modelId.toLowerCase().includes('gpt-oss');\n}\n\n/**\n * Determine what type of thinking parameter a model uses\n *\n * @param modelId - The model identifier\n * @returns \"levels\" for gpt-oss (uses \"low\"|\"medium\"|\"high\"), \"boolean\" for others (uses true|false)\n */\nexport function getThinkingType(modelId: string | null | undefined): \"boolean\" | \"levels\" {\n  if (isGptOssModel(modelId)) {\n    return \"levels\";\n  }\n  return \"boolean\";\n}\n"
  },
  {
    "path": "src/libs/openai.ts",
    "content": "\nimport { getCustomHeaders } from \"@/utils/clean-headers\"\n\ntype Model = {\n  id: string\n  name?: string\n  display_name?: string\n  type: string\n}\n\nexport const isAnthropicAPI = (baseUrl: string) => {\n  return baseUrl === \"https://api.anthropic.com/v1\"\n}\n\n\nexport const getAllAnthropicModels = async ({\n  apiKey,\n  customHeaders = []\n}: {\n  apiKey: string\n  customHeaders?: { key: string; value: string }[]\n}) => {\n\n  try {\n    const url = \"https://api.anthropic.com/v1/models\"\n    const headers = {\n      'x-api-key': apiKey,\n      'Content-Type': 'application/json',\n      \"anthropic-dangerous-direct-browser-access\": \"true\",\n      \"anthropic-version\": \"2023-06-01\",\n      ...getCustomHeaders({ headers: customHeaders }),\n\n    }\n    const controller = new AbortController()\n    const timeoutId = setTimeout(() => controller.abort(), 10000)\n\n    const res = await fetch(url, {\n      headers,\n      signal: controller.signal\n    })\n\n    clearTimeout(timeoutId)\n\n    if (!res.ok) {\n      throw new Error(\"Failed to fetch models\")\n    }\n\n    const data = await res.json()\n    return data.data.map(e=> {\n      return {\n        ...e,\n        name: e?.display_name\n      }\n    }) as Model[]\n\n  } catch (e) {\n    return []\n  }\n}\n\n\nexport const getAllOpenAIModels = async ({\n  baseUrl,\n  apiKey,\n  customHeaders = []\n}: {\n  baseUrl: string\n  apiKey?: string\n  customHeaders?: { key: string; value: string }[]\n}) => {\n  try {\n\n    if (isAnthropicAPI(baseUrl)) {\n      return getAllAnthropicModels({ apiKey, customHeaders })\n    }\n\n    const url = `${baseUrl}/models`\n    const headers = apiKey\n      ? {\n        Authorization: `Bearer ${apiKey}`,\n        ...getCustomHeaders({ headers: customHeaders })\n      }\n      : {\n        ...getCustomHeaders({ headers: customHeaders })\n      }\n\n    const controller = new AbortController()\n    const timeoutId = setTimeout(() => controller.abort(), 10000)\n\n    const res = await fetch(url, {\n      headers,\n      signal: controller.signal\n    })\n\n    clearTimeout(timeoutId)\n\n    // if Google API fails to return models, try another approach\n    if (res.url == 'https://generativelanguage.googleapis.com/v1beta/openai/models') {\n      const urlGoogle = `https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`\n      const resGoogle = await fetch(urlGoogle, {\n        signal: controller.signal\n      })\n\n      const data = await resGoogle.json()\n      return data.models.map(model => ({\n        id: model.name.replace(/^models\\//, \"\"),\n        name: model.name.replace(/^models\\//, \"\"),\n      })) as Model[]\n    }\n\n    if (!res.ok) {\n      return []\n    }\n\n    if (baseUrl === \"https://api.together.xyz/v1\") {\n      const data = (await res.json()) as Model[]\n      return data.map(model => ({\n        id: model.id,\n        name: model.display_name,\n      })) as Model[]\n    }\n\n    const data = (await res.json()) as { data: Model[] }\n\n    return data.data\n  } catch (e) {\n    if (e instanceof DOMException && e.name === 'AbortError') {\n      console.error('Request timed out')\n    } else {\n      console.error(e)\n    }\n    return []\n  }\n}\n"
  },
  {
    "path": "src/libs/pdf.ts",
    "content": "import { pdfDist } from \"./pdfjs\"\n\nexport const getPdf = async (data: ArrayBuffer) => {\n  const pdf = pdfDist.getDocument({\n    data,\n    useWorkerFetch: false,\n    isEvalSupported: false,\n    useSystemFonts: true\n  })\n\n  pdf.onPassword = (callback: any) => {\n    const password = prompt(\"Enter the password: \")\n    if (!password) {\n      throw new Error(\"Password required to open the PDF.\")\n    }\n    callback(password)\n  }\n\n  const pdfDocument = await pdf.promise\n\n  return pdfDocument\n}\n\nexport const processPdf = async (base64: string) => {\n  const res = await fetch(base64)\n  const data = await res.arrayBuffer()\n  const pdf = await getPdf(data)\n  return pdf\n}\n\n\nexport const processPDFFromURL = async (url: string) => {\n  const res = await fetch(url)\n  const data = await res.arrayBuffer()\n  const pdf = await getPdf(data)\n  let pdfText = '';\n\n  for (let i = 1; i <= pdf.numPages; i += 1) {\n    const page = await pdf.getPage(i)\n    const content = await page.getTextContent()\n\n    if (content?.items.length === 0) {\n      continue\n    }\n\n    const text = content?.items\n      .map((item: any) => item.str)\n      .join(\"\\n\")\n      .replace(/\\x00/g, \"\")\n      .trim()\n\n    pdfText += text;\n  }\n\n  return pdfText;\n\n}"
  },
  {
    "path": "src/libs/pdfjs.ts",
    "content": "import * as pdfDist from \"pdfjs-dist\"\nimport * as pdfWorker from \"pdfjs-dist/build/pdf.worker.mjs\";\n\npdfDist.GlobalWorkerOptions.workerSrc = pdfWorker\n\nexport {\n    pdfDist\n}\n"
  },
  {
    "path": "src/libs/process-knowledge.ts",
    "content": "import { getKnowledgeById, updateKnowledgeStatus } from \"@/db/dexie/knowledge\"\nimport { PageAssistPDFUrlLoader } from \"@/loader/pdf-url\"\nimport { getOllamaURL } from \"@/services/ollama\"\nimport { PageAssistVectorStore } from \"./PageAssistVectorStore\"\nimport { PageAssisCSVUrlLoader } from \"@/loader/csv\"\nimport { PageAssisTXTUrlLoader } from \"@/loader/txt\"\nimport { PageAssistDocxLoader } from \"@/loader/docx\"\nimport { cleanUrl } from \"./clean-url\"\nimport { sendEmbeddingCompleteNotification } from \"./send-notification\"\nimport { pageAssistEmbeddingModel } from \"@/models/embedding\"\nimport { getPageAssistTextSplitter } from \"@/utils/text-splitter\"\n\nexport const processKnowledge = async (msg: any, id: string): Promise<void> => {\n  console.log(`Processing knowledge with id: ${id}`)\n  try {\n    const knowledge = await getKnowledgeById(id)\n    const ollamaUrl = await getOllamaURL()\n\n    if (!knowledge) {\n      console.error(`Knowledge with id ${id} not found`)\n      return\n    }\n\n    await updateKnowledgeStatus(id, \"processing\")\n\n    const ollamaEmbedding = await pageAssistEmbeddingModel({\n      baseUrl: cleanUrl(ollamaUrl),\n      model: knowledge.embedding_model\n    })\n\n    const textSplitter = await getPageAssistTextSplitter()\n\n    for (const doc of knowledge.source) {\n\n      // skip if there is no doc.content\n      if (!doc?.content || doc?.content === null) {\n        console.log(`Skipping document with id ${doc.source_id}`)\n        continue\n      }\n\n      if (doc.type === \"pdf\" || doc.type === \"application/pdf\") {\n        const loader = new PageAssistPDFUrlLoader({\n          name: doc.filename,\n          url: doc.content\n        })\n        let docs = await loader.load()\n        const chunks = await textSplitter.splitDocuments(docs)\n        await PageAssistVectorStore.fromDocuments(chunks, ollamaEmbedding, {\n          knownledge_id: knowledge.id,\n          file_id: doc.source_id\n        })\n      } else if (doc.type === \"csv\" || doc.type === \"text/csv\") {\n        const loader = new PageAssisCSVUrlLoader({\n          name: doc.filename,\n          url: doc.content,\n          options: {}\n        })\n\n        let docs = await loader.load()\n\n        const chunks = await textSplitter.splitDocuments(docs)\n        await PageAssistVectorStore.fromDocuments(chunks, ollamaEmbedding, {\n          knownledge_id: knowledge.id,\n          file_id: doc.source_id\n        })\n      } else if (\n        doc.type === \"docx\" ||\n        doc.type ===\n          \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\"\n      ) {\n        try {\n          const loader = new PageAssistDocxLoader({\n            fileName: doc.filename,\n            buffer: await toArrayBufferFromBase64(doc.content)\n          })\n\n          let docs = await loader.load()\n\n          const chunks = await textSplitter.splitDocuments(docs)\n\n          await PageAssistVectorStore.fromDocuments(chunks, ollamaEmbedding, {\n            knownledge_id: knowledge.id,\n            file_id: doc.source_id\n          })\n        } catch (error) {\n          console.error(`Error processing knowledge with id: ${id}`, error)\n        }\n      } else {\n        const loader = new PageAssisTXTUrlLoader({\n          name: doc.filename,\n          url: doc.content\n        })\n\n        let docs = await loader.load()\n\n        const chunks = await textSplitter.splitDocuments(docs)\n\n        await PageAssistVectorStore.fromDocuments(chunks, ollamaEmbedding, {\n          knownledge_id: knowledge.id,\n          file_id: doc.source_id\n        })\n      }\n    }\n\n    await updateKnowledgeStatus(id, \"finished\")\n\n    await sendEmbeddingCompleteNotification()\n  } catch (error) {\n    console.error(`Error processing knowledge with id: ${id}`, error)\n    await updateKnowledgeStatus(id, \"failed\")\n  } finally {\n    console.log(`Finished processing knowledge with id: ${id}`)\n  }\n}\n"
  },
  {
    "path": "src/libs/process-source.ts",
    "content": "import { PageAssisCSVUrlLoader } from \"@/loader/csv\"\nimport { PageAssistDocxLoader } from \"@/loader/docx\"\nimport { PageAssistPDFUrlLoader } from \"@/loader/pdf-url\"\nimport { PageAssisTXTUrlLoader } from \"@/loader/txt\"\nimport { toArrayBufferFromBase64 } from \"~/utils/to-source\"\n\nexport const processSource = async ({\n  type,\n  url,\n  filename\n}: {\n  url: string\n  type: string\n  filename: string\n}) => {\n  if (type === \"pdf\" || type === \"application/pdf\") {\n    const loader = new PageAssistPDFUrlLoader({\n      name: filename,\n      url: url\n    })\n    let docs = await loader.load()\n    return docs.map((e) => e.pageContent).join(\"\\n\")\n  } else if (type === \"csv\" || type === \"text/csv\") {\n    const loader = new PageAssisCSVUrlLoader({\n      name: filename,\n      url: url,\n      options: {}\n    })\n\n    let docs = await loader.load()\n\n    return docs.map((e) => e.pageContent).join(\"\\n\")\n  } else if (\n    type === \"docx\" ||\n    type ===\n      \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\"\n  ) {\n    try {\n      const loader = new PageAssistDocxLoader({\n        fileName: filename,\n        buffer: await toArrayBufferFromBase64(url)\n      })\n\n      let docs = await loader.load()\n\n      return docs.map((e) => e.pageContent).join(\"\\n\")\n    } catch (error) {\n      console.error(`Error loading docx file: ${error}`)\n    }\n  } else {\n    const loader = new PageAssisTXTUrlLoader({\n      name: filename,\n      url: url\n    })\n\n    let docs = await loader.load()\n\n    return docs.map((e) => e.pageContent).join(\"\\n\")\n  }\n}\n"
  },
  {
    "path": "src/libs/reasoning.ts",
    "content": "const tags = [\"think\", \"reason\", \"reasoning\", \"thought\"]\nexport function parseReasoning(text: string): {\n  type: \"reasoning\" | \"text\"\n  content: string\n  reasoning_running?: boolean\n}[] {\n  try {\n    const result: {\n      type: \"reasoning\" | \"text\"\n      content: string\n      reasoning_running?: boolean\n    }[] = []\n    const tagPattern = new RegExp(`<(${tags.join(\"|\")})>`, \"i\")\n    const closeTagPattern = new RegExp(`</(${tags.join(\"|\")})>`, \"i\")\n\n    let currentIndex = 0\n    let isReasoning = false\n\n    while (currentIndex < text.length) {\n      const openTagMatch = text.slice(currentIndex).match(tagPattern)\n      const closeTagMatch = text.slice(currentIndex).match(closeTagPattern)\n\n      if (!isReasoning && openTagMatch) {\n        const beforeText = text.slice(\n          currentIndex,\n          currentIndex + openTagMatch.index\n        )\n        if (beforeText.trim()) {\n          result.push({ type: \"text\", content: beforeText.trim() })\n        }\n\n        isReasoning = true\n        currentIndex += openTagMatch.index! + openTagMatch[0].length\n        continue\n      }\n\n      if (isReasoning && closeTagMatch) {\n        const reasoningContent = text.slice(\n          currentIndex,\n          currentIndex + closeTagMatch.index\n        )\n        if (reasoningContent.trim()) {\n          result.push({ type: \"reasoning\", content: reasoningContent.trim() })\n        }\n\n        isReasoning = false\n        currentIndex += closeTagMatch.index! + closeTagMatch[0].length\n        continue\n      }\n\n      if (currentIndex < text.length) {\n        const remainingText = text.slice(currentIndex)\n        result.push({\n          type: isReasoning ? \"reasoning\" : \"text\",\n          content: remainingText.trim(),\n          reasoning_running: isReasoning\n        })\n        break\n      }\n    }\n\n    return result\n  } catch (e) {\n    console.error(`Error parsing reasoning: ${e}`)\n    return [\n      {\n        type: \"text\",\n        content: text\n      }\n    ]\n  }\n}\n\nexport function isReasoningStarted(text: string): boolean {\n  const tagPattern = new RegExp(`<(${tags.join(\"|\")})>`, \"i\")\n  return tagPattern.test(text)\n}\n\nexport function isReasoningEnded(text: string): boolean {\n  const closeTagPattern = new RegExp(`</(${tags.join(\"|\")})>`, \"i\")\n  return closeTagPattern.test(text)\n}\n\nexport function removeReasoning(text: string): string {\n  const tagPattern = new RegExp(\n    `<(${tags.join(\"|\")})>.*?</(${tags.join(\"|\")})>`,\n    \"gis\"\n  )\n  return text.replace(tagPattern, \"\").trim()\n}\nexport function mergeReasoningContent(\n  originalText: string,\n  reasoning: string\n): string {\n  const reasoningTag = \"<think>\"\n\n  originalText = originalText.replace(reasoningTag, \"\")\n\n  return `${reasoningTag}${originalText + reasoning}`\n}\n\nexport function replaceThinkTagToEM(text: string): string {\n  const tagPattern = new RegExp(\n    `<(${tags.join(\"|\")})>.*?</(${tags.join(\"|\")})>`,\n    \"gis\"\n  )\n  const emStyle = \"font-style: italic; font-size: 0.9em; margin-bottom: 1em;\"\n  return text\n    .replace(tagPattern, (match) => {\n      return `<em style=\"${emStyle}\">${match.replace(/<(\\/)?(${tags.join(\"|\")})>/gi, \"\")}</em>\\n\\n`\n    })\n    .replaceAll(\"<think>\", \"\")\n    .replaceAll(\"</think>\", \"\")\n}\n"
  },
  {
    "path": "src/libs/runtime.ts",
    "content": "/**\n * Rewrites the URL of a request to set the 'Origin' header based on the user's Ollama settings.\n *\n * This function is used to handle CORS issues that may arise when making requests to certain domains.\n * It checks the user's Ollama settings to determine if URL rewriting is enabled, and if so, it updates the\n * 'Origin' header of the request to the specified rewrite URL.\n *\n * @param domain - The domain of the request to be rewritten.\n * @param type - The type of request, defaults to 'ollama'.\n * @returns - A Promise that resolves when the URL rewriting is complete.\n */\nimport { getAdvancedOllamaSettings } from \"@/services/app\"\n\nexport const urlRewriteRuntime = async function (\n  domain: string,\n  type = \"ollama\"\n) {\n  if (browser.runtime && browser.runtime.id) {\n    const { isEnableRewriteUrl, rewriteUrl, autoCORSFix } =\n      await getAdvancedOllamaSettings()\n\n    if (!autoCORSFix) {\n      if (\n        import.meta.env.BROWSER === \"chrome\" ||\n        import.meta.env.BROWSER === \"edge\"\n      ) {\n        try {\n          await browser.declarativeNetRequest.updateDynamicRules({\n            removeRuleIds: [1],\n            addRules: []\n          })\n        } catch (e) {}\n      }\n\n      if (import.meta.env.BROWSER === \"firefox\") {\n        try {\n          browser.webRequest.onBeforeSendHeaders.removeListener(() => {})\n        } catch (e) {}\n      }\n\n      return\n    }\n\n    if (\n      import.meta.env.BROWSER === \"chrome\" ||\n      import.meta.env.BROWSER === \"edge\"\n    ) {\n      const url = new URL(domain)\n      const domains = [url.hostname]\n      let origin = `${url.protocol}//${url.hostname}`\n      if (isEnableRewriteUrl && rewriteUrl && type === \"ollama\") {\n        origin = rewriteUrl\n      }\n      const rules = [\n        {\n          id: 1,\n          priority: 1,\n          condition: {\n            requestDomains: domains\n          },\n          action: {\n            type: \"modifyHeaders\",\n            requestHeaders: [\n              {\n                header: \"Origin\",\n                operation: \"set\",\n                value: origin\n              }\n            ]\n          }\n        }\n      ]\n      await browser.declarativeNetRequest.updateDynamicRules({\n        removeRuleIds: rules.map((r) => r.id),\n        // @ts-ignore\n        addRules: rules\n      })\n    }\n\n    if (import.meta.env.BROWSER === \"firefox\") {\n      const url = new URL(domain)\n      const domains = [`*://${url.hostname}/*`]\n      browser.webRequest.onBeforeSendHeaders.addListener(\n        (details) => {\n          let origin = `${url.protocol}//${url.hostname}`\n          if (isEnableRewriteUrl && rewriteUrl && type === \"ollama\") {\n            origin = rewriteUrl\n          }\n          for (let i = 0; i < details.requestHeaders.length; i++) {\n            if (details.requestHeaders[i].name === \"Origin\") {\n              details.requestHeaders[i].value = origin\n            }\n          }\n          return { requestHeaders: details.requestHeaders }\n        },\n        { urls: domains },\n        [\"blocking\", \"requestHeaders\"]\n      )\n    }\n  }\n}\n"
  },
  {
    "path": "src/libs/send-notification.ts",
    "content": "import { Storage } from \"@plasmohq/storage\"\nconst storage = new Storage()\n\nexport const sendNotification = async (title: string, message: string) => {\n  try {\n    const sendNotificationAfterIndexing = await storage.get<boolean>(\n      \"sendNotificationAfterIndexing\"\n    )\n    if (sendNotificationAfterIndexing) {\n      browser.notifications.create({\n        type: \"basic\",\n        iconUrl: browser.runtime.getURL(\"/icon/128.png\"),\n        title,\n        message\n      })\n    }\n  } catch (error) {\n    console.error(error)\n  }\n}\n\nexport const sendEmbeddingCompleteNotification = async () => {\n  await sendNotification(\n    \"Page Assist - Embedding Completed\",\n    \"The knowledge base embedding process is complete. You can now use the knowledge base for chatting.\"\n  )\n}\n"
  },
  {
    "path": "src/libs/to-base64.ts",
    "content": "export const toBase64 = (file: File) =>\n  new Promise<string>((resolve, reject) => {\n    const reader = new FileReader()\n    reader.readAsDataURL(file)\n    reader.onload = () => resolve(reader.result as string)\n    reader.onerror = (error) => reject(error)\n  })\n"
  },
  {
    "path": "src/loader/csv.ts",
    "content": "import { dsvFormat } from \"d3-dsv\"\n\nimport { BaseDocumentLoader } from \"@langchain/core/document_loaders/base\"\nimport { Document } from \"@langchain/core/documents\"\nexport interface WebLoaderParams {\n  url: string\n  name: string\n  options: {\n    column?: string\n    separator?: string\n  }\n}\n\nexport class PageAssisCSVUrlLoader\n  extends BaseDocumentLoader\n  implements WebLoaderParams\n{\n  pdf: { content: string; page: number }[]\n  url: string\n  name: string\n  options: { column?: string; separator?: string }\n\n  constructor({ url, name }: WebLoaderParams) {\n    super()\n    this.url = url\n    this.name = name\n    this.options = {}\n  }\n\n  public async parse(raw: string): Promise<string[]> {\n    const { column, separator = \",\" } = this.options\n    const psv = dsvFormat(separator)\n\n    let parsed = psv.parseRows(raw.trim())\n\n    if (column !== undefined) {\n      if (!parsed[0].includes(column)) {\n        throw new Error(`ColumnNotFoundError: Column ${column} not found`)\n      }\n\n      const columnIndex = parsed[0].indexOf(column)\n      return parsed.map((row) => row[columnIndex]!)\n    }\n\n    const headers = parsed[0]\n    parsed = parsed.slice(1)\n\n    return parsed.map((row) =>\n      row.map((value, index) => `${headers[index]}: ${value}`).join(\"\\n\")\n    )\n  }\n  async load(): Promise<Document<Record<string, any>>[]> {\n    const res = await fetch(this.url)\n\n    if (!res.ok) {\n      throw new Error(`Failed to fetch ${this.url}`)\n    }\n\n    const raw = await res.text()\n\n    const parsed = await this.parse(raw)\n    let metadata = { source: this.name, type: \"csv\" }\n    parsed.forEach((pageContent, i) => {\n      if (typeof pageContent !== \"string\") {\n        throw new Error(\n          `Expected string, at position ${i} got ${typeof pageContent}`\n        )\n      }\n    })\n    return parsed.map(\n      (pageContent, i) =>\n        new Document({\n          pageContent,\n          metadata:\n            parsed.length === 1\n              ? metadata\n              : {\n                  ...metadata,\n                  line: i + 1\n                }\n        })\n    )\n  }\n}\n"
  },
  {
    "path": "src/loader/docx.ts",
    "content": "import { BaseDocumentLoader } from \"@langchain/core/document_loaders/base\"\nimport { Document } from \"@langchain/core/documents\"\nimport * as mammoth from \"mammoth\"\n\nexport interface WebLoaderParams {\n    fileName: string\n    buffer: ArrayBuffer\n}\n\nexport class PageAssistDocxLoader\n    extends BaseDocumentLoader\n    implements WebLoaderParams {\n    fileName: string\n    buffer: ArrayBuffer\n\n    constructor({ fileName, buffer }: WebLoaderParams) {\n        super()\n        this.fileName = fileName\n        this.buffer = buffer\n    }\n\n    public async load(): Promise<Document[]> {\n        const data = await mammoth.extractRawText({\n            arrayBuffer: this.buffer\n        })\n        const text = data.value\n        const meta = { source: this.fileName }\n        if (text) {\n            return [new Document({ pageContent: text, metadata: meta })]\n        }\n        return []\n    }\n}\n"
  },
  {
    "path": "src/loader/html.ts",
    "content": "import { BaseDocumentLoader } from \"@langchain/core/document_loaders/base\"\nimport { Document } from \"@langchain/core/documents\"\nimport { YtTranscript } from \"yt-transcript\"\nimport { isWikipedia, parseWikipedia } from \"@/parser/wiki\"\nimport { extractReadabilityContent } from \"@/parser/reader\"\nimport { isYoutubeLink } from \"@/utils/is-youtube\"\n\n\n\nconst getTranscript = async (url: string) => {\n  const ytTranscript = new YtTranscript({ url })\n  return await ytTranscript.getTranscript()\n}\n\nexport interface WebLoaderParams {\n  html: string\n  url: string\n}\n\nexport class PageAssistHtmlLoader\n  extends BaseDocumentLoader\n  implements WebLoaderParams {\n  html: string\n  url: string\n\n  constructor({ html, url }: WebLoaderParams) {\n    super()\n    this.html = html\n    this.url = url\n  }\n\n  async load(): Promise<Document<Record<string, any>>[]> {\n    console.log(\"Loading HTML...\", this.url)\n   \n    if (isYoutubeLink(this.url) && this.html) {\n      console.log(\"[SidePanel] Youtube link with HTML detected, skipping transcript\")\n      const metadata = { source: this.url, url: this.url, }\n      return [new Document({ pageContent: this.html, metadata })]\n    } \n   \n    if (isYoutubeLink(this.url)) {\n      console.log(\"Youtube link detected\")\n      const transcript = await getTranscript(this.url)\n      if (!transcript) {\n        throw new Error(\"Transcript not found for this video.\")\n      }\n\n      let text = \"\"\n\n      transcript.forEach((item) => {\n        text += `[${item?.start}] ${item?.text}\\n`\n      })\n\n      return [\n        {\n          metadata: {\n            source: this.url,\n            url: this.url,\n            audio: { chunks: transcript }\n          },\n          pageContent: text\n        }\n      ]\n    }\n    const metadata = { source: this.url, url: this.url, }\n    return [new Document({ pageContent: this.html, metadata })]\n  }\n\n  async loadByURL(): Promise<Document<Record<string, any>>[]> {\n    try {\n      console.log(\"Loading HTML...\", this.url)\n      if (isYoutubeLink(this.url)) {\n        console.log(\"Youtube link detected\")\n        const transcript = await getTranscript(this.url)\n        if (!transcript) {\n          throw new Error(\"Transcript not found for this video.\")\n        }\n\n        let text = \"\"\n\n        transcript?.forEach((item) => {\n          text += `[${item?.start}] ${item?.text}\\n`\n        })\n\n        return [\n          {\n            metadata: {\n              url: this.url,\n              source: this.url,\n              audio: { chunks: transcript }\n            },\n            pageContent: text\n          }\n        ]\n      }\n      // await urlRewriteRuntime(this.url, \"web\")\n      let text = \"\";\n      if (isWikipedia(this.url)) {\n        const fetchHTML = await fetch(this.url)\n        text = parseWikipedia(await fetchHTML.text())\n      } else {\n        text = await extractReadabilityContent(this.url)\n      }\n\n      const metadata = { url: this.url }\n      return [new Document({ pageContent: text, metadata })]\n    } catch (e) {\n      console.log(\"[PageAssistHtmlLoader] loadByURL\", e)\n      return []\n    }\n  }\n}\n"
  },
  {
    "path": "src/loader/pdf-url.ts",
    "content": "import { BaseDocumentLoader } from \"@langchain/core/document_loaders/base\"\nimport { Document } from \"@langchain/core/documents\"\nimport { processPdf } from \"@/libs/pdf\"\nexport interface WebLoaderParams {\n  url: string\n  name: string\n}\n\nexport class PageAssistPDFUrlLoader\n  extends BaseDocumentLoader\n  implements WebLoaderParams\n{\n  pdf: { content: string; page: number }[]\n  url: string\n  name: string\n\n  constructor({ url, name }: WebLoaderParams) {\n    super()\n    this.url = url\n    this.name = name\n  }\n\n  async load(): Promise<Document<Record<string, any>>[]> {\n    const documents: Document[] = []\n\n    const data = await processPdf(this.url)\n\n    for (let i = 1; i <= data.numPages; i += 1) {\n      const page = await data.getPage(i)\n      const content = await page.getTextContent()\n\n      if (content?.items.length === 0) {\n        continue\n      }\n\n      const text = content?.items\n        .map((item: any) => item.str)\n        .join(\"\\n\")\n        .replace(/\\x00/g, \"\")\n        .trim()\n      documents.push({\n        pageContent: text,\n        metadata: { source: this.name, page: i, type: \"pdf\" }\n      })\n    }\n\n    return documents\n  }\n}\n"
  },
  {
    "path": "src/loader/pdf.ts",
    "content": "import { BaseDocumentLoader } from \"@langchain/core/document_loaders/base\"\nimport { Document } from \"@langchain/core/documents\"\nexport interface WebLoaderParams {\n  pdf: { content: string; page: number }[]\n  url: string\n}\n\nexport class PageAssistPDFLoader\n  extends BaseDocumentLoader\n  implements WebLoaderParams\n{\n  pdf: { content: string; page: number }[]\n  url: string\n\n  constructor({ pdf, url }: WebLoaderParams) {\n    super()\n    this.pdf = pdf\n    this.url = url\n  }\n\n  async load(): Promise<Document<Record<string, any>>[]> {\n    const documents: Document[] = []\n\n    for (const page of this.pdf) {\n      const metadata = { source: this.url, page: page.page }\n      documents.push(new Document({ pageContent: page.content, metadata }))\n    }\n\n    return [\n      new Document({\n        pageContent: documents.map((doc) => doc.pageContent).join(\"\\n\\n\"),\n        metadata: documents.map((doc) => doc.metadata)\n      })\n    ]\n  }\n}\n"
  },
  {
    "path": "src/loader/txt.ts",
    "content": "import { BaseDocumentLoader } from \"@langchain/core/document_loaders/base\"\nimport { Document } from \"@langchain/core/documents\"\nexport interface WebLoaderParams {\n  url: string\n  name: string\n}\n\nexport class PageAssisTXTUrlLoader\n  extends BaseDocumentLoader\n  implements WebLoaderParams\n{\n  pdf: { content: string; page: number }[]\n  url: string\n  name: string\n\n  constructor({ url, name }: WebLoaderParams) {\n    super()\n    this.url = url\n    this.name = name\n  }\n\n  public async parse(raw: string): Promise<string[]> {\n    return [raw]\n  }\n  async load(): Promise<Document<Record<string, any>>[]> {\n    const res = await fetch(this.url)\n\n    if (!res.ok) {\n      throw new Error(`Failed to fetch ${this.url}`)\n    }\n\n    const raw = await res.text()\n\n    const parsed = await this.parse(raw)\n    let metadata = { source: this.name, type: \"txt\" }\n    parsed.forEach((pageContent, i) => {\n      if (typeof pageContent !== \"string\") {\n        throw new Error(\n          `Expected string, at position ${i} got ${typeof pageContent}`\n        )\n      }\n    })\n    return parsed.map(\n      (pageContent, i) =>\n        new Document({\n          pageContent,\n          metadata:\n            parsed.length === 1\n              ? metadata\n              : {\n                  ...metadata,\n                  line: i + 1\n                }\n        })\n    )\n  }\n}\n"
  },
  {
    "path": "src/models/ChatChromeAi.ts",
    "content": "import {\n  SimpleChatModel,\n  type BaseChatModelParams\n} from \"@langchain/core/language_models/chat_models\"\nimport type { BaseLanguageModelCallOptions } from \"@langchain/core/language_models/base\"\nimport {\n  CallbackManagerForLLMRun,\n  Callbacks\n} from \"@langchain/core/callbacks/manager\"\nimport { BaseMessage, AIMessageChunk } from \"@langchain/core/messages\"\nimport { ChatGenerationChunk } from \"@langchain/core/outputs\"\nimport { IterableReadableStream } from \"@langchain/core/utils/stream\"\nimport { AITextSession, checkChromeAIAvailability, createAITextSession } from \"./utils/chrome\"\n\nexport interface AITextSessionOptions {\n  topK: number\n  temperature: number\n}\n\nexport const enum AIModelAvailability {\n  Readily = \"readily\",\n  AfterDownload = \"after-download\",\n  No = \"no\"\n}\n\nexport interface ChromeAIInputs extends BaseChatModelParams {\n  topK?: number\n  temperature?: number\n  /**\n   * An optional function to format the prompt before sending it to the model.\n   */\n  promptFormatter?: (messages: BaseMessage[]) => string\n}\n\nexport interface ChromeAICallOptions extends BaseLanguageModelCallOptions {}\n\nfunction formatPrompt(messages: BaseMessage[]): string {\n  return messages\n    .map((message) => {\n      if (typeof message.content !== \"string\") {\n        if (message.content.length > 0) {\n          //@ts-ignore\n          return message.content[0]?.text || \"\"\n        }\n\n        return \"\"\n      }\n      return `${message._getType()}: ${message.content}`\n    })\n    .join(\"\\n\")\n}\n\nexport class ChatChromeAI extends SimpleChatModel<ChromeAICallOptions> {\n  session?: AITextSession\n\n  temperature = 0.8\n\n  topK = 120\n\n  promptFormatter: (messages: BaseMessage[]) => string\n\n  static lc_name() {\n    return \"ChatChromeAI\"\n  }\n\n  constructor(inputs?: ChromeAIInputs) {\n    super({\n      callbacks: {} as Callbacks,\n      ...inputs\n    })\n    this.temperature = inputs?.temperature ?? this.temperature\n    this.topK = inputs?.topK ?? this.topK\n    this.promptFormatter = inputs?.promptFormatter ?? formatPrompt\n  }\n\n  _llmType() {\n    return \"chrome-ai\"\n  }\n\n  /**\n   * Initialize the model. This method must be called before calling `.invoke()`.\n   */\n  async initialize() {\n    if (typeof window === \"undefined\") {\n      throw new Error(\"ChatChromeAI can only be used in the browser.\")\n    }\n\n    const canCreateTextSession = await checkChromeAIAvailability()\n    if (canCreateTextSession === AIModelAvailability.No) {\n      throw new Error(\"The AI model is not available.\")\n    } else if (canCreateTextSession === AIModelAvailability.AfterDownload) {\n      throw new Error(\"The AI model is not yet downloaded.\")\n    }\n\n    this.session = await createAITextSession({\n      topK: this.topK,\n      temperature: this.temperature,\n    })\n  }\n\n  /**\n   * Call `.destroy()` to free resources if you no longer need a session.\n   * When a session is destroyed, it can no longer be used, and any ongoing\n   * execution will be aborted. You may want to keep the session around if\n   * you intend to prompt the model often since creating a session can take\n   * some time.\n   */\n  destroy() {\n    if (!this.session) {\n      return console.error(\"No session found. Returning.\")\n    }\n    this.session.destroy()\n  }\n\n  async *_streamResponseChunks(\n    messages: BaseMessage[],\n    _options: this[\"ParsedCallOptions\"],\n    runManager?: CallbackManagerForLLMRun\n  ): AsyncGenerator<ChatGenerationChunk> {\n    if (!this.session) {\n      await this.initialize()\n    }\n    const textPrompt = this.promptFormatter(messages)\n    const stream = this.session.promptStreaming(textPrompt)\n    const iterableStream = IterableReadableStream.fromReadableStream(stream)\n\n    let previousContent = \"\"\n    for await (const chunk of iterableStream) {\n      const newContent =\n        typeof (globalThis as any).LanguageModel !== \"undefined\"\n          ? chunk\n          : chunk.slice(previousContent.length)\n      previousContent += newContent\n      yield new ChatGenerationChunk({\n        text: newContent,\n        message: new AIMessageChunk({\n          content: newContent,\n          additional_kwargs: {}\n        })\n      })\n      await runManager?.handleLLMNewToken(newContent)\n    }\n  }\n\n  async _call(\n    messages: BaseMessage[],\n    options: this[\"ParsedCallOptions\"],\n    runManager?: CallbackManagerForLLMRun\n  ): Promise<string> {\n    const chunks = []\n    for await (const chunk of this._streamResponseChunks(\n      messages,\n      options,\n      runManager\n    )) {\n      chunks.push(chunk.text)\n    }\n    return chunks.join(\"\")\n  }\n}\n"
  },
  {
    "path": "src/models/ChatGoogleAI.ts",
    "content": "import { ChatOpenAI } from \"@langchain/openai\";\n\nexport class ChatGoogleAI extends ChatOpenAI {\n\n    frequencyPenalty: number = undefined;\n    presencePenalty: number = undefined;\n\n    static lc_name() {\n        return \"ChatGoogleAI\";\n    }\n}"
  },
  {
    "path": "src/models/ChatOllama.ts",
    "content": "import { BaseLanguageModelInput } from \"@langchain/core/language_models/base\"\nimport {\n    BaseChatModel,\n    type BaseChatModelParams,\n    type BaseChatModelCallOptions,\n    type LangSmithParams,\n    type BindToolsInput,\n} from \"@langchain/core/language_models/chat_models\"\nimport { CallbackManagerForLLMRun } from \"@langchain/core/callbacks/manager\"\nimport {\n    AIMessage,\n    AIMessageChunk,\n    BaseMessage,\n    ChatMessage,\n    ToolMessage,\n} from \"@langchain/core/messages\"\nimport { ChatGenerationChunk, ChatResult } from \"@langchain/core/outputs\"\nimport type { StringWithAutocomplete } from \"@langchain/core/utils/types\"\nimport { convertToOpenAITool } from \"@langchain/core/utils/function_calling\"\nimport { concat } from \"@langchain/core/utils/stream\"\nimport { Runnable } from \"@langchain/core/runnables\"\n\nimport {\n    createOllamaChatStream,\n    createOllamaGenerateStream,\n    parseKeepAlive,\n    type OllamaInput,\n    type OllamaMessage,\n} from \"./utils/ollama\"\n\nexport interface ChatOllamaCallOptions extends BaseChatModelCallOptions {\n    tools?: BindToolsInput[]\n}\n\nexport interface ChatOllamaInput extends OllamaInput { }\n\nexport class ChatOllama\n    extends BaseChatModel<ChatOllamaCallOptions, AIMessageChunk>\n    implements ChatOllamaInput {\n    static lc_name() {\n        return \"ChatOllama\"\n    }\n\n    lc_serializable = true\n\n    model = \"llama2\"\n\n    baseUrl = \"http://localhost:11434\"\n\n    keepAlive?: string\n\n    thinking?: boolean | \"low\" | \"medium\" | \"high\"\n\n    embeddingOnly?: boolean\n\n    f16KV?: boolean\n\n    frequencyPenalty?: number\n\n    headers?: Record<string, string>\n\n    logitsAll?: boolean\n\n    lowVram?: boolean\n\n    mainGpu?: number\n\n    mirostat?: number\n\n    mirostatEta?: number\n\n    mirostatTau?: number\n\n    numBatch?: number\n\n    numCtx?: number\n\n    numGpu?: number\n\n    numGqa?: number\n\n    numKeep?: number\n\n    numPredict?: number\n\n    numThread?: number\n\n    penalizeNewline?: boolean\n\n    presencePenalty?: number\n\n    repeatLastN?: number\n\n    repeatPenalty?: number\n\n    ropeFrequencyBase?: number\n\n    ropeFrequencyScale?: number\n\n    temperature?: number\n\n    stop?: string[]\n\n    tfsZ?: number\n\n    topK?: number\n\n    topP?: number\n\n    minP?: number\n\n    typicalP?: number\n\n    useMLock?: boolean\n\n    useMMap?: boolean\n\n    useMlock?: boolean\n\n    vocabOnly?: boolean\n\n    seed?: number\n\n    format?: StringWithAutocomplete<\"json\">\n\n    constructor(fields: OllamaInput & BaseChatModelParams) {\n        super(fields)\n        this.model = fields.model ?? this.model\n        this.baseUrl = fields.baseUrl?.endsWith(\"/\")\n            ? fields.baseUrl.slice(0, -1)\n            : fields.baseUrl ?? this.baseUrl\n        this.keepAlive = parseKeepAlive(fields.keepAlive)\n        this.embeddingOnly = fields.embeddingOnly\n        this.f16KV = fields.f16KV\n        this.frequencyPenalty = fields.frequencyPenalty\n        this.headers = fields.headers\n        this.logitsAll = fields.logitsAll\n        this.lowVram = fields.lowVram\n        this.mainGpu = fields.mainGpu\n        this.mirostat = fields.mirostat\n        this.mirostatEta = fields.mirostatEta\n        this.mirostatTau = fields.mirostatTau\n        this.numBatch = fields.numBatch\n        this.numCtx = fields.numCtx\n        this.numGpu = fields.numGpu === null ? undefined : fields.numGpu\n        this.numGqa = fields.numGqa\n        this.numKeep = fields.numKeep\n        this.numPredict = fields.numPredict\n        this.numThread = fields.numThread\n        this.penalizeNewline = fields.penalizeNewline\n        this.presencePenalty = fields.presencePenalty\n        this.repeatLastN = fields.repeatLastN\n        this.repeatPenalty = fields.repeatPenalty\n        this.ropeFrequencyBase = fields.ropeFrequencyBase\n        this.ropeFrequencyScale = fields.ropeFrequencyScale\n        this.temperature = fields.temperature\n        this.stop = fields.stop\n        this.tfsZ = fields.tfsZ\n        this.topK = fields.topK\n        this.topP = fields.topP\n        this.minP = fields.minP\n        this.typicalP = fields.typicalP\n        this.useMLock = fields.useMLock\n        this.useMMap = fields.useMMap\n        this.useMlock = fields.useMlock\n        this.vocabOnly = fields.vocabOnly\n        this.format = fields.format\n        this.seed = fields.seed\n        this.thinking = fields.thinking\n    }\n\n    getLsParams(options: this[\"ParsedCallOptions\"]): LangSmithParams {\n        const params = this.invocationParams(options)\n        return {\n            ls_provider: \"ollama\",\n            ls_model_name: this.model,\n            ls_model_type: \"chat\" as const,\n            ls_temperature: this.temperature ?? undefined,\n            ls_stop: this.stop,\n            ls_max_tokens: params.options.num_predict,\n        }\n    }\n\n    _llmType() {\n        return \"ollama\"\n    }\n\n    override bindTools(\n        tools: BindToolsInput[],\n        kwargs?: Partial<this[\"ParsedCallOptions\"]>\n    ): Runnable<BaseLanguageModelInput, AIMessageChunk, ChatOllamaCallOptions> {\n        return this.withConfig({\n            tools: tools.map((tool) => convertToOpenAITool(tool)),\n            ...kwargs,\n        } as Partial<ChatOllamaCallOptions>)\n    }\n\n    /**\n     * A method that returns the parameters for an Ollama API call. It\n     * includes model and options parameters.\n     * @param options Optional parsed call options.\n     * @returns An object containing the parameters for an Ollama API call.\n     */\n    invocationParams(options?: this[\"ParsedCallOptions\"]) {\n        return {\n            model: this.model,\n            format: this.format,\n            keep_alive: this.keepAlive,\n            think: this.thinking,\n            options: {\n                embedding_only: this.embeddingOnly,\n                f16_kv: this.f16KV,\n                frequency_penalty: this.frequencyPenalty,\n                logits_all: this.logitsAll,\n                low_vram: this.lowVram,\n                main_gpu: this.mainGpu,\n                mirostat: this.mirostat,\n                mirostat_eta: this.mirostatEta,\n                mirostat_tau: this.mirostatTau,\n                num_batch: this.numBatch,\n                num_ctx: this.numCtx,\n                num_gpu: this.numGpu,\n                num_gqa: this.numGqa,\n                num_keep: this.numKeep,\n                num_predict: this.numPredict,\n                num_thread: this.numThread,\n                penalize_newline: this.penalizeNewline,\n                presence_penalty: this.presencePenalty,\n                repeat_last_n: this.repeatLastN,\n                repeat_penalty: this.repeatPenalty,\n                rope_frequency_base: this.ropeFrequencyBase,\n                rope_frequency_scale: this.ropeFrequencyScale,\n                temperature: this.temperature,\n                stop: options?.stop ?? this.stop,\n                tfs_z: this.tfsZ,\n                top_k: this.topK,\n                top_p: this.topP,\n                min_p: this.minP,\n                typical_p: this.typicalP,\n                use_mlock: this.useMlock,\n                use_mmap: this.useMMap,\n                vocab_only: this.vocabOnly,\n                seed: this.seed,\n            },\n            // eslint-disable-next-line @typescript-eslint/no-explicit-any\n            tools: options?.tools?.length\n                ? options.tools.map((tool) => convertToOpenAITool(tool))\n                : undefined,\n        }\n    }\n\n    _combineLLMOutput() {\n        return {}\n    }\n\n    async _generate(\n        messages: BaseMessage[],\n        options: this[\"ParsedCallOptions\"],\n        runManager?: CallbackManagerForLLMRun\n    ): Promise<ChatResult> {\n        let finalChunk: AIMessageChunk | undefined\n        for await (const chunk of this._streamResponseChunks(\n            messages,\n            options,\n            runManager\n        )) {\n            if (!finalChunk) {\n                finalChunk = chunk.message as AIMessageChunk\n            } else {\n                finalChunk = concat(finalChunk, chunk.message as AIMessageChunk)\n            }\n        }\n\n        const nonChunkMessage = new AIMessage({\n            content: finalChunk?.content ?? \"\",\n            additional_kwargs: finalChunk?.additional_kwargs,\n            tool_calls: finalChunk?.tool_calls,\n            response_metadata: finalChunk?.response_metadata,\n            usage_metadata: finalChunk?.usage_metadata,\n        })\n        return {\n            generations: [\n                {\n                    text:\n                        typeof nonChunkMessage.content === \"string\"\n                            ? nonChunkMessage.content\n                            : \"\",\n                    message: nonChunkMessage,\n                },\n            ],\n        }\n    }\n\n    /** @deprecated */\n    async *_streamResponseChunksLegacy(\n        input: BaseMessage[],\n        options: this[\"ParsedCallOptions\"],\n        runManager?: CallbackManagerForLLMRun\n    ): AsyncGenerator<ChatGenerationChunk> {\n        const stream = createOllamaGenerateStream(\n            this.baseUrl,\n            {\n                ...this.invocationParams(options),\n                prompt: this._formatMessagesAsPrompt(input),\n            },\n            {\n                ...options,\n                headers: this.headers,\n            }\n        )\n        for await (const chunk of stream) {\n            if (!chunk.done) {\n                yield new ChatGenerationChunk({\n                    text: chunk.response,\n                    message: new AIMessageChunk({ content: chunk.response }),\n                })\n                await runManager?.handleLLMNewToken(chunk.response ?? \"\")\n            } else {\n                yield new ChatGenerationChunk({\n                    text: \"\",\n                    message: new AIMessageChunk({ content: \"\" }),\n                    generationInfo: {\n                        model: chunk.model,\n                        total_duration: chunk.total_duration,\n                        load_duration: chunk.load_duration,\n                        prompt_eval_count: chunk.prompt_eval_count,\n                        prompt_eval_duration: chunk.prompt_eval_duration,\n                        eval_count: chunk.eval_count,\n                        eval_duration: chunk.eval_duration,\n                    },\n                })\n            }\n        }\n    }\n\n    async *_streamResponseChunks(\n        input: BaseMessage[],\n        options: this[\"ParsedCallOptions\"],\n        runManager?: CallbackManagerForLLMRun\n    ): AsyncGenerator<ChatGenerationChunk> {\n        try {\n            const stream = await this.caller.call(async () =>\n                createOllamaChatStream(\n                    this.baseUrl,\n                    {\n                        ...this.invocationParams(options),\n                        messages: this._convertMessagesToOllamaMessages(input),\n                    },\n                    {\n                        ...options,\n                        headers: this.headers,\n                    }\n                )\n            )\n            for await (const chunk of stream) {\n                if (!chunk.done) {\n                    const responseMessage = chunk.message\n\n                    // Build tool_call_chunks if Ollama returned tool calls\n                    const toolCallChunks = responseMessage.tool_calls?.map(\n                        (tc, i) => ({\n                            name: tc.function.name,\n                            args: JSON.stringify(tc.function.arguments),\n                            type: \"tool_call_chunk\" as const,\n                            index: i,\n                            id: crypto.randomUUID(),\n                        })\n                    )\n\n                    const content = responseMessage.content ?? \"\"\n\n                    yield new ChatGenerationChunk({\n                        text: content,\n                        message: new AIMessageChunk({\n                            content,\n                            additional_kwargs: responseMessage?.thinking\n                                ? { reasoning_content: responseMessage.thinking }\n                                : {},\n                            tool_call_chunks: toolCallChunks,\n                        }),\n                    })\n                    await runManager?.handleLLMNewToken(content)\n                } else {\n                    yield new ChatGenerationChunk({\n                        text: \"\",\n                        message: new AIMessageChunk({ content: \"\" }),\n                        generationInfo: {\n                            model: chunk.model,\n                            total_duration: chunk.total_duration,\n                            load_duration: chunk.load_duration,\n                            prompt_eval_count: chunk.prompt_eval_count,\n                            prompt_eval_duration: chunk.prompt_eval_duration,\n                            eval_count: chunk.eval_count,\n                            eval_duration: chunk.eval_duration,\n                        },\n                    })\n                }\n            }\n            // eslint-disable-next-line @typescript-eslint/no-explicit-any\n        } catch (e: any) {\n            if (e.response?.status === 404) {\n                console.warn(\n                    \"[WARNING]: It seems you are using a legacy version of Ollama. Please upgrade to a newer version for better chat support.\"\n                )\n                yield* this._streamResponseChunksLegacy(input, options, runManager)\n            } else {\n                throw e\n            }\n        }\n    }\n\n    protected _convertMessagesToOllamaMessages(\n        messages: BaseMessage[]\n    ): OllamaMessage[] {\n        return messages.map((message) => {\n            let role: string\n            if (message._getType() === \"human\") {\n                role = \"user\"\n            } else if (message._getType() === \"ai\") {\n                role = \"assistant\"\n            } else if (message._getType() === \"system\") {\n                role = \"system\"\n            } else if (message._getType() === \"tool\") {\n                role = \"tool\"\n            } else {\n                throw new Error(\n                    `Unsupported message type for Ollama: ${message._getType()}`\n                )\n            }\n\n            // Handle ToolMessage\n            if (message._getType() === \"tool\") {\n                const toolMsg = message as ToolMessage\n                return {\n                    role: \"tool\",\n                    content:\n                        typeof toolMsg.content === \"string\" ? toolMsg.content : \"\",\n                }\n            }\n\n            // Handle AI message with tool calls\n            if (message._getType() === \"ai\") {\n                const aiMsg = message as AIMessage\n                if (aiMsg.tool_calls?.length) {\n                    return {\n                        role: \"assistant\",\n                        content:\n                            typeof aiMsg.content === \"string\" ? aiMsg.content : \"\",\n                        tool_calls: aiMsg.tool_calls.map((tc) => ({\n                            function: {\n                                name: tc.name,\n                                arguments: tc.args,\n                            },\n                        })),\n                    }\n                }\n            }\n\n            let content = \"\"\n            const images: string[] = []\n            if (typeof message.content === \"string\") {\n                content = message.content\n            } else {\n                for (const contentPart of message.content) {\n                    if (contentPart.type === \"text\") {\n                        content = `${content}\\n${contentPart.text}`\n                    } else if (\n                        contentPart.type === \"image_url\" &&\n                        typeof contentPart.image_url === \"string\"\n                    ) {\n                        const imageUrlComponents = contentPart.image_url.split(\",\")\n                        // Support both data:image/jpeg;base64,<image> format as well\n                        images.push(imageUrlComponents[1] ?? imageUrlComponents[0])\n                    } else {\n                        throw new Error(\n                            `Unsupported message content type. Must either have type \"text\" or type \"image_url\" with a string \"image_url\" field.`\n                        )\n                    }\n                }\n            }\n            return {\n                role,\n                content,\n                images,\n            }\n        })\n    }\n\n    /** @deprecated */\n    protected _formatMessagesAsPrompt(messages: BaseMessage[]): string {\n        const formattedMessages = messages\n            .map((message) => {\n                let messageText\n                if (message._getType() === \"human\") {\n                    messageText = `[INST] ${message.content} [/INST]`\n                } else if (message._getType() === \"ai\") {\n                    messageText = message.content\n                } else if (message._getType() === \"system\") {\n                    messageText = `<<SYS>> ${message.content} <</SYS>>`\n                } else if (ChatMessage.isInstance(message)) {\n                    messageText = `\\n\\n${message.role[0].toUpperCase()}${message.role.slice(\n                        1\n                    )}: ${message.content}`\n                } else {\n                    console.warn(\n                        `Unsupported message type passed to Ollama: \"${message._getType()}\"`\n                    )\n                    messageText = \"\"\n                }\n                return messageText\n            })\n            .join(\"\\n\")\n        return formattedMessages\n    }\n}\n"
  },
  {
    "path": "src/models/ChatTypes.ts",
    "content": "export type ChatDocument  = {\n    title?: string, \n    url?: string,\n    type: \"tab\" | \"file\",\n    tabId?: number,\n    favIconUrl?: string,\n    filename?: string,\n    fileSize?: number,\n}\n\nexport type ChatDocuments = ChatDocument[]"
  },
  {
    "path": "src/models/CustomAIMessageChunk.ts",
    "content": "interface BaseMessageFields {\n    content: string;\n    name?: string;\n    additional_kwargs?: {\n        [key: string]: unknown;\n    };\n}\n\nexport class CustomAIMessageChunk {\n    /** The text of the message. */\n    content: string;\n\n    /** The name of the message sender in a multi-user chat. */\n    name?: string;\n\n    /** Additional keyword arguments */\n    additional_kwargs: NonNullable<BaseMessageFields[\"additional_kwargs\"]>;\n\n    constructor(fields: BaseMessageFields) {\n        // Make sure the default value for additional_kwargs is passed into super() for serialization\n        if (!fields.additional_kwargs) {\n            // eslint-disable-next-line no-param-reassign\n            fields.additional_kwargs = {};\n        }\n\n        this.name = fields.name;\n        this.content = fields.content;\n        this.additional_kwargs = fields.additional_kwargs;\n    }\n\n    static _mergeAdditionalKwargs(\n        left: NonNullable<BaseMessageFields[\"additional_kwargs\"]>,\n        right: NonNullable<BaseMessageFields[\"additional_kwargs\"]>\n    ): NonNullable<BaseMessageFields[\"additional_kwargs\"]> {\n        const merged = { ...left };\n        for (const [key, value] of Object.entries(right)) {\n            if (merged[key] === undefined) {\n                merged[key] = value;\n            } else if (typeof merged[key] === \"string\" && typeof value === \"string\") {\n                merged[key] = (merged[key] as string) + value;\n            } else if (Array.isArray(merged[key]) && Array.isArray(value)) {\n                merged[key] = [...(merged[key] as unknown[]), ...value];\n            } else if (\n                !Array.isArray(merged[key]) &&\n                typeof merged[key] === \"object\" &&\n                !Array.isArray(value) &&\n                typeof value === \"object\"\n            ) {\n                merged[key] = this._mergeAdditionalKwargs(\n                    merged[key] as NonNullable<BaseMessageFields[\"additional_kwargs\"]>,\n                    value as NonNullable<BaseMessageFields[\"additional_kwargs\"]>\n                );\n            } else {\n                console.warn(\n                    `additional_kwargs[${key}] already exists in this message chunk and cannot be merged.`\n                );\n            }\n        }\n        return merged;\n    }\n\n    _getType(): string {\n        return \"ai\";\n    }\n\n    _updateId(value: string | undefined) {\n        // no-op: CustomAIMessageChunk does not track id\n    }\n\n    concat(chunk: CustomAIMessageChunk) {\n        return new CustomAIMessageChunk({\n            content: this.content + chunk.content,\n            additional_kwargs: CustomAIMessageChunk._mergeAdditionalKwargs(\n                this.additional_kwargs,\n                chunk.additional_kwargs\n            ),\n        });\n    }\n}\n\nfunction isAiMessageChunkFields(value: unknown): value is BaseMessageFields {\n    if (typeof value !== \"object\" || value == null) return false;\n    return \"content\" in value && typeof value[\"content\"] === \"string\";\n}\n\nfunction isAiMessageChunkFieldsList(\n    value: unknown[]\n): value is BaseMessageFields[] {\n    return value.length > 0 && value.every((x) => isAiMessageChunkFields(x));\n}\n"
  },
  {
    "path": "src/models/CustomChatAnthropic.ts",
    "content": "import {\n    BaseChatModel,\n    type BaseChatModelCallOptions,\n    type BaseChatModelParams,\n    type BindToolsInput,\n    type LangSmithParams,\n} from \"@langchain/core/language_models/chat_models\"\nimport { BaseLanguageModelInput } from \"@langchain/core/language_models/base\"\nimport {\n    CallbackManagerForLLMRun,\n    Callbacks\n} from \"@langchain/core/callbacks/manager\"\nimport {\n    AIMessage,\n    AIMessageChunk,\n    BaseMessage,\n    ToolMessage,\n} from \"@langchain/core/messages\"\nimport { ChatGenerationChunk, ChatResult } from \"@langchain/core/outputs\"\nimport { convertToOpenAITool } from \"@langchain/core/utils/function_calling\"\nimport { concat } from \"@langchain/core/utils/stream\"\nimport { Runnable } from \"@langchain/core/runnables\"\n\nexport interface AnthropicMessageOptions {\n    apiKey?: string\n    temperature?: number\n    maxTokens?: number\n    topP?: number\n    topK?: number\n    modelName?: string\n    stopSequences?: string[]\n    budgetTokens?: number\n}\n\nexport interface AnthropicCallOptions extends BaseChatModelCallOptions {\n    tools?: BindToolsInput[]\n}\n\nfunction detectImageMediaType(base64Data: string): string | null {\n    try {\n        const binaryString = atob(base64Data.slice(0, 32))\n        const bytes = new Uint8Array(binaryString.length)\n        for (let i = 0; i < binaryString.length; i++) {\n            bytes[i] = binaryString.charCodeAt(i)\n        }\n\n        if (bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) {\n            return \"image/jpeg\"\n        }\n        if (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47) {\n            return \"image/png\"\n        }\n        if (bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46) {\n            return \"image/gif\"\n        }\n        if (bytes[0] === 0x52 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x46) {\n            if (bytes[8] === 0x57 && bytes[9] === 0x45 && bytes[10] === 0x42 && bytes[11] === 0x50) {\n                return \"image/webp\"\n            }\n        }\n    } catch (e) {\n    }\n    return null\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction convertToAnthropicTools(tools: BindToolsInput[]): any[] {\n    return tools.map(tool => {\n        const openAITool = convertToOpenAITool(tool)\n        return {\n            name: openAITool.function.name,\n            description: openAITool.function.description || \"\",\n            input_schema: openAITool.function.parameters || { type: \"object\", properties: {} }\n        }\n    })\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nfunction convertMessagesToAnthropicFormat(messages: BaseMessage[]): Array<any> {\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    const result: Array<any> = []\n\n    for (const message of messages) {\n        const type = message._getType()\n\n        if (type === \"system\") {\n            // handled separately as body.system\n        } else if (type === \"human\") {\n            if (typeof message.content === \"string\") {\n                result.push({ role: \"user\", content: message.content })\n            } else if (Array.isArray(message.content)) {\n                // eslint-disable-next-line @typescript-eslint/no-explicit-any\n                const anthropicContent: Array<any> = []\n\n                for (const block of message.content) {\n                    if (typeof block === \"object\") {\n                        if (\"text\" in block && block.type === \"text\") {\n                            anthropicContent.push({\n                                type: \"text\",\n                                text: block.text as string\n                            })\n                        } else if (\"image_url\" in block && block.type === \"image_url\") {\n                            const imageUrl = block.image_url as string | { url: string } | undefined\n                            if (typeof imageUrl === \"string\" && imageUrl.startsWith(\"data:image/\")) {\n                                const [header, data] = imageUrl.split(\",\")\n                                const mediaType = header.match(/data:([^;]+)/)?.[1] || \"image/jpeg\"\n                                const actualMediaType = detectImageMediaType(data) || mediaType\n                                anthropicContent.push({\n                                    type: \"image\",\n                                    source: {\n                                        type: \"base64\",\n                                        media_type: actualMediaType,\n                                        data: data\n                                    }\n                                })\n                            } else if (typeof imageUrl === \"string\") {\n                                anthropicContent.push({\n                                    type: \"image\",\n                                    source: { type: \"url\", url: imageUrl }\n                                })\n                            } else if (typeof imageUrl === \"object\" && imageUrl && \"url\" in imageUrl) {\n                                const url = imageUrl.url\n                                if (url.startsWith(\"data:image/\")) {\n                                    const [header, data] = url.split(\",\")\n                                    const mediaType = header.match(/data:([^;]+)/)?.[1] || \"image/jpeg\"\n                                    const actualMediaType = detectImageMediaType(data) || mediaType\n                                    anthropicContent.push({\n                                        type: \"image\",\n                                        source: {\n                                            type: \"base64\",\n                                            media_type: actualMediaType,\n                                            data: data\n                                        }\n                                    })\n                                } else {\n                                    anthropicContent.push({\n                                        type: \"image\",\n                                        source: { type: \"url\", url: url }\n                                    })\n                                }\n                            }\n                        }\n                    }\n                }\n\n                result.push({ role: \"user\", content: anthropicContent })\n            } else {\n                result.push({ role: \"user\", content: \"\" })\n            }\n        } else if (type === \"ai\") {\n            const aiMsg = message as AIMessage\n            if (aiMsg.tool_calls?.length) {\n                // Build assistant message with tool_use content blocks\n                // eslint-disable-next-line @typescript-eslint/no-explicit-any\n                const blocks: any[] = []\n                if (typeof aiMsg.content === \"string\" && aiMsg.content) {\n                    blocks.push({ type: \"text\", text: aiMsg.content })\n                }\n                for (const tc of aiMsg.tool_calls) {\n                    blocks.push({\n                        type: \"tool_use\",\n                        id: tc.id || `toolu_${crypto.randomUUID()}`,\n                        name: tc.name,\n                        input: tc.args\n                    })\n                }\n                result.push({ role: \"assistant\", content: blocks })\n            } else {\n                const content = typeof aiMsg.content === \"string\" ? aiMsg.content : \"\"\n                result.push({ role: \"assistant\", content })\n            }\n        } else if (type === \"tool\") {\n            const toolMsg = message as ToolMessage\n            const content = typeof toolMsg.content === \"string\"\n                ? toolMsg.content\n                : JSON.stringify(toolMsg.content)\n            // Merge consecutive tool results into the same user message\n            const prev = result[result.length - 1]\n            if (\n                prev?.role === \"user\" &&\n                Array.isArray(prev.content) &&\n                prev.content[0]?.type === \"tool_result\"\n            ) {\n                prev.content.push({\n                    type: \"tool_result\",\n                    tool_use_id: toolMsg.tool_call_id,\n                    content\n                })\n            } else {\n                result.push({\n                    role: \"user\",\n                    content: [{\n                        type: \"tool_result\",\n                        tool_use_id: toolMsg.tool_call_id,\n                        content\n                    }]\n                })\n            }\n        }\n    }\n\n    return result\n}\n\nexport class ChatAnthropic extends BaseChatModel<AnthropicCallOptions, AIMessageChunk> {\n    apiKey: string\n    temperature = 1\n    topP = 1\n    topK?: number\n    maxTokens = 1024\n    modelName = \"claude-sonnet-4-5-20250929\"\n    stopSequences?: string[]\n    budgetTokens?: number\n\n    static lc_name() {\n        return \"ChatAnthropic\"\n    }\n\n    constructor(inputs?: AnthropicMessageOptions & BaseChatModelParams) {\n        super({\n            callbacks: {} as Callbacks,\n            ...inputs\n        })\n\n        this.apiKey = inputs?.apiKey || (globalThis as any).ANTHROPIC_API_KEY || \"\"\n        this.temperature = inputs?.temperature ?? this.temperature\n        this.topP = inputs?.topP ?? this.topP\n        this.topK = inputs?.topK\n        this.maxTokens = inputs?.maxTokens ?? this.maxTokens\n        this.modelName = inputs?.modelName ?? this.modelName\n        this.stopSequences = inputs?.stopSequences\n        this.budgetTokens = inputs?.budgetTokens\n    }\n\n    _llmType() {\n        return \"anthropic\"\n    }\n\n    getLsParams(_options: this[\"ParsedCallOptions\"]): LangSmithParams {\n        return {\n            ls_provider: \"anthropic\",\n            ls_model_name: this.modelName,\n            ls_model_type: \"chat\" as const,\n            ls_temperature: this.temperature,\n            ls_max_tokens: this.maxTokens,\n            ls_stop: this.stopSequences,\n        }\n    }\n\n    override bindTools(\n        tools: BindToolsInput[],\n        kwargs?: Partial<this[\"ParsedCallOptions\"]>\n    ): Runnable<BaseLanguageModelInput, AIMessageChunk, AnthropicCallOptions> {\n        return this.withConfig({\n            tools: tools.map(tool => convertToOpenAITool(tool)),\n            ...kwargs,\n        } as Partial<AnthropicCallOptions>)\n    }\n\n    async _generate(\n        messages: BaseMessage[],\n        options: this[\"ParsedCallOptions\"],\n        runManager?: CallbackManagerForLLMRun\n    ): Promise<ChatResult> {\n        let finalChunk: AIMessageChunk | undefined\n        for await (const chunk of this._streamResponseChunks(messages, options, runManager)) {\n            if (!finalChunk) {\n                finalChunk = chunk.message as AIMessageChunk\n            } else {\n                finalChunk = concat(finalChunk, chunk.message as AIMessageChunk)\n            }\n        }\n\n        const nonChunkMessage = new AIMessage({\n            content: finalChunk?.content ?? \"\",\n            additional_kwargs: finalChunk?.additional_kwargs,\n            tool_calls: finalChunk?.tool_calls,\n            response_metadata: finalChunk?.response_metadata,\n            usage_metadata: finalChunk?.usage_metadata,\n        })\n        return {\n            generations: [{\n                text: typeof nonChunkMessage.content === \"string\" ? nonChunkMessage.content : \"\",\n                message: nonChunkMessage,\n            }]\n        }\n    }\n\n    async *_streamResponseChunks(\n        messages: BaseMessage[],\n        options: this[\"ParsedCallOptions\"],\n        runManager?: CallbackManagerForLLMRun\n    ): AsyncGenerator<ChatGenerationChunk> {\n        const convertedMessages = convertMessagesToAnthropicFormat(messages)\n\n        let systemPrompt = \"\"\n        for (const message of messages) {\n            if (message._getType() === \"system\") {\n                systemPrompt = typeof message.content === \"string\" ? message.content : \"\"\n                break\n            }\n        }\n\n        // eslint-disable-next-line @typescript-eslint/no-explicit-any\n        const body: any = {\n            model: this.modelName.replaceAll(/[\\t\\n\\r]/g, \"\"),\n            max_tokens: this.maxTokens,\n            messages: convertedMessages\n        }\n\n        if (this.topP !== 1) {\n            body.top_p = this.topP\n        } else {\n            body.temperature = this.temperature\n        }\n\n        if (systemPrompt) {\n            body.system = systemPrompt\n        }\n\n        if (this.topK) {\n            body.top_k = this.topK\n        }\n\n        if (this.stopSequences && this.stopSequences.length > 0) {\n            body.stop_sequences = this.stopSequences\n        }\n\n        if (this.budgetTokens) {\n            body.thinking = {\n                type: \"enabled\",\n                budget_tokens: this.budgetTokens\n            }\n        }\n\n        // Tools from bindTools or direct call options\n        const tools = options?.tools?.length ? options.tools : undefined\n        if (tools) {\n            body.tools = convertToAnthropicTools(tools)\n        }\n\n        body.stream = true\n\n        const headers: Record<string, string> = {\n            \"Content-Type\": \"application/json\",\n            \"anthropic-dangerous-direct-browser-access\": \"true\",\n            \"anthropic-version\": \"2023-06-01\",\n            \"x-api-key\": this.apiKey,\n        }\n\n        if (this.budgetTokens) {\n            headers[\"anthropic-beta\"] = \"interleaved-thinking-2025-05-14\"\n        }\n\n        const response = await fetch(\"https://api.anthropic.com/v1/messages\", {\n            method: \"POST\",\n            headers,\n            body: JSON.stringify(body),\n            signal: options?.signal,\n        })\n\n        if (!response.ok) {\n            const errorData = await response.text()\n            throw new Error(`Anthropic API error: ${response.status} ${errorData}`)\n        }\n\n        const reader = response.body?.getReader()\n        if (!reader) {\n            throw new Error(\"No response body from Anthropic API\")\n        }\n\n        const decoder = new TextDecoder()\n        let buffer = \"\"\n        let currentBlockIndex = 0\n\n        while (true) {\n            const { done, value } = await reader.read()\n            if (done) break\n\n            buffer += decoder.decode(value, { stream: true })\n            const lines = buffer.split(\"\\n\")\n            buffer = lines.pop() || \"\"\n\n            for (const line of lines) {\n                if (!line.startsWith(\"data: \")) continue\n                const data = line.slice(6)\n                if (data === \"[DONE]\") continue\n\n                try {\n                    const event = JSON.parse(data)\n\n                    if (event.type === \"content_block_start\") {\n                        currentBlockIndex = event.index ?? 0\n                        if (event.content_block?.type === \"tool_use\") {\n                            // Beginning of a tool call — emit chunk with name + id\n                            yield new ChatGenerationChunk({\n                                text: \"\",\n                                message: new AIMessageChunk({\n                                    content: \"\",\n                                    tool_call_chunks: [{\n                                        id: event.content_block.id,\n                                        name: event.content_block.name,\n                                        args: \"\",\n                                        index: currentBlockIndex,\n                                        type: \"tool_call_chunk\"\n                                    }]\n                                })\n                            })\n                        }\n                    } else if (event.type === \"content_block_delta\") {\n                        const delta = event.delta\n                        if (delta?.type === \"text_delta\" && delta.text) {\n                            yield new ChatGenerationChunk({\n                                text: delta.text,\n                                message: new AIMessageChunk({\n                                    content: delta.text,\n                                    additional_kwargs: {}\n                                })\n                            })\n                            await runManager?.handleLLMNewToken(delta.text)\n                        } else if (delta?.type === \"thinking_delta\" && delta.thinking) {\n                            // Extended thinking — surface as reasoning_content\n                            yield new ChatGenerationChunk({\n                                text: \"\",\n                                message: new AIMessageChunk({\n                                    content: \"\",\n                                    additional_kwargs: {\n                                        reasoning_content: delta.thinking\n                                    }\n                                })\n                            })\n                        } else if (delta?.type === \"input_json_delta\") {\n                            // Streaming tool call arguments\n                            yield new ChatGenerationChunk({\n                                text: \"\",\n                                message: new AIMessageChunk({\n                                    content: \"\",\n                                    tool_call_chunks: [{\n                                        args: delta.partial_json ?? \"\",\n                                        index: event.index ?? currentBlockIndex,\n                                        type: \"tool_call_chunk\"\n                                    }]\n                                })\n                            })\n                        }\n                    }\n                } catch (e) {\n                    // Ignore JSON parse errors for non-data lines\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/models/CustomChatOpenAI.ts",
    "content": "import { type ClientOptions, OpenAI as OpenAIClient, } from \"openai\"\nimport {\n    AIMessage,\n    AIMessageChunk,\n    BaseMessage,\n    ChatMessage,\n    ChatMessageChunk,\n    FunctionMessageChunk,\n    HumanMessageChunk,\n    SystemMessageChunk,\n    ToolMessage,\n    ToolMessageChunk\n} from \"@langchain/core/messages\"\nimport { ChatGenerationChunk, ChatResult } from \"@langchain/core/outputs\"\nimport { getEnvironmentVariable } from \"@langchain/core/utils/env\"\nimport {\n    BaseChatModel,\n    BaseChatModelParams,\n    type BindToolsInput\n} from \"@langchain/core/language_models/chat_models\"\nimport { convertToOpenAITool } from \"@langchain/core/utils/function_calling\"\nimport {\n    Runnable,\n    RunnablePassthrough,\n    RunnableSequence\n} from \"@langchain/core/runnables\"\nimport {\n    JsonOutputParser,\n    StructuredOutputParser\n} from \"@langchain/core/output_parsers\"\nimport { JsonOutputKeyToolsParser } from \"@langchain/core/output_parsers/openai_tools\"\nimport { wrapOpenAIClientError } from \"./utils/openai.js\"\nimport {\n    ChatOpenAICallOptions,\n    getEndpoint,\n    OpenAIChatInput as OldOpenAIChatInput,\n    OpenAICoreRequestOptions\n} from \"@langchain/openai\"\nimport { CallbackManagerForLLMRun } from \"@langchain/core/callbacks/manager\"\nimport {\n    BaseLanguageModelInput,\n    TokenUsage\n} from \"@langchain/core/language_models/base\"\nimport { LegacyOpenAIInput } from \"./types.js\"\n\ntype OpenAIRoleEnum = \"system\" | \"assistant\" | \"user\" | \"function\" | \"tool\"\ntype ReasoningEffort = 'low' | 'medium' | 'high' | null\n\ninterface ReasoningEffortOptions {\n    reasoning_effort?: ReasoningEffort\n}\n\ntype OpenAIChatInput = OldOpenAIChatInput & ReasoningEffortOptions\nfunction extractGenericMessageCustomRole(message: ChatMessage) {\n    if (\n        message.role !== \"system\" &&\n        message.role !== \"assistant\" &&\n        message.role !== \"user\" &&\n        message.role !== \"function\" &&\n        message.role !== \"tool\"\n    ) {\n        console.warn(`Unknown message role: ${message.role}`)\n    }\n    return message.role\n}\nexport function messageToOpenAIRole(message: BaseMessage): OpenAIRoleEnum {\n    const type = message._getType()\n    switch (type) {\n        case \"system\":\n            return \"system\"\n        case \"ai\":\n            return \"assistant\"\n        case \"human\":\n            return \"user\"\n        case \"function\":\n            return \"function\"\n        case \"tool\":\n            return \"tool\"\n        case \"generic\": {\n            if (!ChatMessage.isInstance(message))\n                throw new Error(\"Invalid generic chat message\")\n            return extractGenericMessageCustomRole(message) as OpenAIRoleEnum\n        }\n        default:\n            return type as OpenAIRoleEnum\n    }\n}\nfunction openAIResponseToChatMessage(\n    message: OpenAIClient.Chat.Completions.ChatCompletionMessage\n) {\n    switch (message.role) {\n        case \"assistant\": {\n            if (message.tool_calls?.length) {\n                return new AIMessage({\n                    content: message.content || \"\",\n                    tool_calls: message.tool_calls.map((tc) => ({\n                        id: tc.id,\n                        name: tc.function.name,\n                        args: (() => {\n                            try {\n                                return JSON.parse(tc.function.arguments)\n                            } catch {\n                                return {}\n                            }\n                        })(),\n                        type: \"tool_call\" as const\n                    }))\n                })\n            }\n            return new AIMessage({ content: message.content || \"\" })\n        }\n        default:\n            return new ChatMessage(message.content || \"\", message.role ?? \"unknown\")\n    }\n}\nfunction _convertDeltaToMessageChunk(\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    delta: Record<string, any>,\n    defaultRole?: OpenAIRoleEnum\n) {\n    const role = delta.role ?? defaultRole\n    const content = delta.content ?? \"\"\n    const reasoning_content: string | undefined | null =\n        delta?.reasoning_content ?? delta?.reasoning ?? undefined\n    let additional_kwargs\n    if (delta?.function_call) {\n        additional_kwargs = {\n            function_call: delta.function_call\n        }\n    } else {\n        additional_kwargs = {}\n    }\n\n    if (reasoning_content != null) {\n        additional_kwargs.reasoning_content = reasoning_content\n    }\n\n    if (delta?.reasoning_details != null) {\n        additional_kwargs.reasoning_details = delta.reasoning_details\n    }\n\n    // Streaming tool call deltas — use proper tool_call_chunks instead of additional_kwargs\n    if (delta?.tool_calls) {\n        return new AIMessageChunk({\n            content,\n            additional_kwargs,\n            // Let LangChain collapse streamed tool deltas into final tool_calls.\n            tool_call_chunks: delta.tool_calls.map((tc: any) => ({\n                id: tc.id,\n                name: tc.function?.name,\n                args: tc.function?.arguments ?? \"\",\n                index: tc.index,\n                type: \"tool_call_chunk\" as const\n            }))\n        })\n    }\n    if (role === \"user\") {\n        return new HumanMessageChunk({ content })\n    } else if (role === \"assistant\") {\n        return new AIMessageChunk({\n            content,\n            additional_kwargs\n        })\n    } else if (role === \"system\") {\n        return new SystemMessageChunk({ content })\n    } else if (role === \"function\") {\n        return new FunctionMessageChunk({\n            content,\n            additional_kwargs,\n            name: delta.name\n        })\n    } else if (role === \"tool\") {\n        return new ToolMessageChunk({\n            content,\n            additional_kwargs,\n            tool_call_id: delta.tool_call_id\n        })\n    } else {\n        return new ChatMessageChunk({ content, role })\n    }\n}\nfunction convertMessagesToOpenAIParams(messages: BaseMessage[]) {\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    return messages.map((message): any => {\n        const role = messageToOpenAIRole(message)\n\n        // ToolMessage → { role: \"tool\", content, tool_call_id }\n        if (role === \"tool\") {\n            const toolMsg = message as ToolMessage\n            return {\n                role: \"tool\",\n                content: typeof toolMsg.content === \"string\"\n                    ? toolMsg.content\n                    : JSON.stringify(toolMsg.content),\n                tool_call_id: toolMsg.tool_call_id ?? \"\",\n            }\n        }\n\n        // AI message with tool_calls → include tool_calls array\n        if (role === \"assistant\") {\n            // eslint-disable-next-line @typescript-eslint/no-explicit-any\n            const aiMsg = message as any\n            if (aiMsg.tool_calls?.length) {\n                return {\n                    role: \"assistant\",\n                    content: typeof aiMsg.content === \"string\" ? aiMsg.content : null,\n                    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n                    tool_calls: aiMsg.tool_calls.map((tc: any) => ({\n                        id: tc.id || \"\",\n                        type: \"function\",\n                        function: {\n                            name: tc.name,\n                            arguments: typeof tc.args === \"string\"\n                                ? tc.args\n                                : JSON.stringify(tc.args ?? {}),\n                        },\n                    })),\n                }\n            }\n        }\n\n        // eslint-disable-next-line @typescript-eslint/no-explicit-any\n        const completionParam: any = {\n            role,\n            content: message.content,\n        }\n        // eslint-disable-next-line @typescript-eslint/no-explicit-any\n        if ((message as any).name != null) {\n            // eslint-disable-next-line @typescript-eslint/no-explicit-any\n            completionParam.name = (message as any).name\n        }\n        return completionParam\n    })\n}\nexport class CustomChatOpenAI<\n    CallOptions extends ChatOpenAICallOptions = ChatOpenAICallOptions\n>\n    extends BaseChatModel<CallOptions>\n    implements OpenAIChatInput {\n    temperature = 1\n\n    topP = 1\n\n    frequencyPenalty = 0\n\n    presencePenalty = 0\n\n    n = 1\n\n    logitBias?: Record<string, number>\n\n    modelName = \"gpt-3.5-turbo\"\n\n    model = \"gpt-3.5-turbo\"\n\n    modelKwargs?: OpenAIChatInput[\"modelKwargs\"]\n\n    stop?: string[]\n\n    stopSequences?: string[]\n\n    user?: string\n\n    timeout?: number\n\n    streaming = false\n\n    streamUsage = true\n\n    maxTokens?: number\n\n    logprobs?: boolean\n\n    topLogprobs?: number\n\n    openAIApiKey?: string\n\n    apiKey?: string\n\n    azureOpenAIApiVersion?: string\n\n    azureOpenAIApiKey?: string\n\n    azureADTokenProvider?: () => Promise<string>\n\n    azureOpenAIApiInstanceName?: string\n\n    azureOpenAIApiDeploymentName?: string\n\n    azureOpenAIBasePath?: string\n\n    organization?: string\n\n    reasoning_effort?: ReasoningEffort | null\n\n    protected client: OpenAIClient\n\n    protected clientConfig: ClientOptions\n    static lc_name() {\n        return \"ChatOpenAI\"\n    }\n    get callKeys() {\n        return [\n            ...super.callKeys,\n            \"options\",\n            \"function_call\",\n            \"functions\",\n            \"tools\",\n            \"tool_choice\",\n            \"promptIndex\",\n            \"response_format\",\n            \"seed\"\n        ]\n    }\n    get lc_secrets() {\n        return {\n            openAIApiKey: \"OPENAI_API_KEY\",\n            azureOpenAIApiKey: \"AZURE_OPENAI_API_KEY\",\n            organization: \"OPENAI_ORGANIZATION\"\n        }\n    }\n    get lc_aliases() {\n        return {\n            modelName: \"model\",\n            openAIApiKey: \"openai_api_key\",\n            azureOpenAIApiVersion: \"azure_openai_api_version\",\n            azureOpenAIApiKey: \"azure_openai_api_key\",\n            azureOpenAIApiInstanceName: \"azure_openai_api_instance_name\",\n            azureOpenAIApiDeploymentName: \"azure_openai_api_deployment_name\"\n        }\n    }\n    constructor(\n        fields?: Partial<OpenAIChatInput> &\n            BaseChatModelParams & {\n                configuration?: ClientOptions & LegacyOpenAIInput & ReasoningEffortOptions\n            },\n        /** @deprecated */\n        configuration?: ClientOptions & LegacyOpenAIInput & ReasoningEffortOptions\n    ) {\n        super(fields ?? {})\n        Object.defineProperty(this, \"lc_serializable\", {\n            enumerable: true,\n            configurable: true,\n            writable: true,\n            value: true\n        })\n        Object.defineProperty(this, \"temperature\", {\n            enumerable: true,\n            configurable: true,\n            writable: true,\n            value: void 0\n        })\n        Object.defineProperty(this, \"topP\", {\n            enumerable: true,\n            configurable: true,\n            writable: true,\n            value: void 0\n        })\n        Object.defineProperty(this, \"frequencyPenalty\", {\n            enumerable: true,\n            configurable: true,\n            writable: true,\n            value: void 0\n        })\n        Object.defineProperty(this, \"presencePenalty\", {\n            enumerable: true,\n            configurable: true,\n            writable: true,\n            value: void 0\n        })\n        Object.defineProperty(this, \"n\", {\n            enumerable: true,\n            configurable: true,\n            writable: true,\n            value: void 0\n        })\n        Object.defineProperty(this, \"logitBias\", {\n            enumerable: true,\n            configurable: true,\n            writable: true,\n            value: void 0\n        })\n        Object.defineProperty(this, \"modelName\", {\n            enumerable: true,\n            configurable: true,\n            writable: true,\n            value: \"gpt-3.5-turbo\"\n        })\n        Object.defineProperty(this, \"modelKwargs\", {\n            enumerable: true,\n            configurable: true,\n            writable: true,\n            value: void 0\n        })\n        Object.defineProperty(this, \"stop\", {\n            enumerable: true,\n            configurable: true,\n            writable: true,\n            value: void 0\n        })\n        Object.defineProperty(this, \"user\", {\n            enumerable: true,\n            configurable: true,\n            writable: true,\n            value: void 0\n        })\n        Object.defineProperty(this, \"timeout\", {\n            enumerable: true,\n            configurable: true,\n            writable: true,\n            value: void 0\n        })\n        Object.defineProperty(this, \"streaming\", {\n            enumerable: true,\n            configurable: true,\n            writable: true,\n            value: false\n        })\n        Object.defineProperty(this, \"maxTokens\", {\n            enumerable: true,\n            configurable: true,\n            writable: true,\n            value: void 0\n        })\n        Object.defineProperty(this, \"logprobs\", {\n            enumerable: true,\n            configurable: true,\n            writable: true,\n            value: void 0\n        })\n        Object.defineProperty(this, \"topLogprobs\", {\n            enumerable: true,\n            configurable: true,\n            writable: true,\n            value: void 0\n        })\n        Object.defineProperty(this, \"openAIApiKey\", {\n            enumerable: true,\n            configurable: true,\n            writable: true,\n            value: void 0\n        })\n        Object.defineProperty(this, \"azureOpenAIApiVersion\", {\n            enumerable: true,\n            configurable: true,\n            writable: true,\n            value: void 0\n        })\n        Object.defineProperty(this, \"azureOpenAIApiKey\", {\n            enumerable: true,\n            configurable: true,\n            writable: true,\n            value: void 0\n        })\n        Object.defineProperty(this, \"azureOpenAIApiInstanceName\", {\n            enumerable: true,\n            configurable: true,\n            writable: true,\n            value: void 0\n        })\n        Object.defineProperty(this, \"azureOpenAIApiDeploymentName\", {\n            enumerable: true,\n            configurable: true,\n            writable: true,\n            value: void 0\n        })\n        Object.defineProperty(this, \"azureOpenAIBasePath\", {\n            enumerable: true,\n            configurable: true,\n            writable: true,\n            value: void 0\n        })\n        Object.defineProperty(this, \"organization\", {\n            enumerable: true,\n            configurable: true,\n            writable: true,\n            value: void 0\n        })\n        Object.defineProperty(this, \"client\", {\n            enumerable: true,\n            configurable: true,\n            writable: true,\n            value: void 0\n        })\n        Object.defineProperty(this, \"clientConfig\", {\n            enumerable: true,\n            configurable: true,\n            writable: true,\n            value: void 0\n        })\n        this.openAIApiKey =\n            (fields?.openAIApiKey ?? getEnvironmentVariable(\"OPENAI_API_KEY\")) as string | undefined\n\n        this.modelName = fields?.modelName ?? this.modelName\n        this.modelKwargs = fields?.modelKwargs ?? {}\n        this.timeout = fields?.timeout\n        this.temperature = fields?.temperature ?? this.temperature\n        this.topP = fields?.topP ?? this.topP\n        this.frequencyPenalty = fields?.frequencyPenalty ?? this.frequencyPenalty\n        this.presencePenalty = fields?.presencePenalty ?? this.presencePenalty\n        this.maxTokens = fields?.maxTokens\n        this.logprobs = fields?.logprobs\n        this.topLogprobs = fields?.topLogprobs\n        this.n = fields?.n ?? this.n\n        this.logitBias = fields?.logitBias\n        this.stop = fields?.stop\n        this.user = fields?.user\n        this.streaming = fields?.streaming ?? false\n        this.reasoning_effort = fields?.reasoning_effort ?? null\n        this.clientConfig = {\n            apiKey: this.openAIApiKey,\n            organization: this.organization,\n            baseURL: configuration?.basePath ?? fields?.configuration?.basePath,\n            dangerouslyAllowBrowser: true,\n            defaultHeaders:\n                configuration?.baseOptions?.headers ??\n                fields?.configuration?.baseOptions?.headers,\n            defaultQuery:\n                configuration?.baseOptions?.params ??\n                fields?.configuration?.baseOptions?.params,\n            ...configuration,\n            ...fields?.configuration\n        }\n    }\n    /**\n     * Get the parameters used to invoke the model\n     */\n    invocationParams(options) {\n        function isStructuredToolArray(tools) {\n            return (\n                tools !== undefined &&\n                tools.every((tool) => Array.isArray(tool.lc_namespace))\n            )\n        }\n        const params = {\n            model: this.modelName,\n            temperature: this.temperature,\n            top_p: this.topP,\n            frequency_penalty: this.frequencyPenalty,\n            presence_penalty: this.presencePenalty,\n            max_tokens: this.maxTokens === -1 ? undefined : this.maxTokens,\n            logprobs: this.logprobs,\n            top_logprobs: this.topLogprobs,\n            n: this.n,\n            logit_bias: this.logitBias,\n            stop: options?.stop ?? this.stop,\n            user: this.user,\n            stream: this.streaming,\n            functions: options?.functions,\n            function_call: options?.function_call,\n            tools: isStructuredToolArray(options?.tools)\n                ? options?.tools.map(convertToOpenAITool)\n                : options?.tools,\n            tool_choice: options?.tool_choice,\n            response_format: options?.response_format,\n            seed: options?.seed,\n            reasoning: this.reasoning_effort ? {\n                method: this.reasoning_effort,\n            } : undefined,\n            ...this.modelKwargs\n        }\n        return params\n    }\n    /** @ignore */\n    _identifyingParams() {\n        return {\n            model_name: this.modelName,\n            //@ts-ignore\n            ...this?.invocationParams(),\n            ...this.clientConfig\n        }\n    }\n    async *_streamResponseChunks(\n        messages: BaseMessage[],\n        options: this[\"ParsedCallOptions\"],\n        runManager?: CallbackManagerForLLMRun\n    ): AsyncGenerator<ChatGenerationChunk> {\n        const messagesMapped = convertMessagesToOpenAIParams(messages)\n        const params = {\n            ...this.invocationParams(options),\n            messages: messagesMapped,\n            stream: true\n        }\n        let defaultRole\n        //@ts-ignore\n        const streamIterable = await this.completionWithRetry(params, options)\n        for await (const data of streamIterable) {\n            const choice = data?.choices[0]\n            if (!choice) {\n                continue\n            }\n            const { delta } = choice\n            if (!delta) {\n                continue\n            }\n            const chunk = _convertDeltaToMessageChunk(delta, defaultRole)\n            defaultRole = delta.role ?? defaultRole\n            const newTokenIndices = {\n                //@ts-ignore\n                prompt: options?.promptIndex ?? 0,\n                completion: choice.index ?? 0\n            }\n            if (typeof chunk.content !== \"string\") {\n                console.log(\n                    \"[WARNING]: Received non-string content from OpenAI. This is currently not supported.\"\n                )\n                continue\n            }\n            // eslint-disable-next-line @typescript-eslint/no-explicit-any\n            const generationInfo = { ...newTokenIndices } as any\n            if (choice.finish_reason !== undefined) {\n                generationInfo.finish_reason = choice.finish_reason\n            }\n            if (this.logprobs) {\n                generationInfo.logprobs = choice.logprobs\n            }\n            const generationChunk = new ChatGenerationChunk({\n                message: chunk,\n                text: chunk.content,\n                generationInfo\n            })\n            yield generationChunk\n            // eslint-disable-next-line no-void\n            void runManager?.handleLLMNewToken(\n                generationChunk.text ?? \"\",\n                newTokenIndices,\n                undefined,\n                undefined,\n                undefined,\n                { chunk: generationChunk }\n            )\n        }\n        if (options.signal?.aborted) {\n            throw new Error(\"AbortError\")\n        }\n    }\n    /**\n     * Get the identifying parameters for the model\n     *\n     */\n    identifyingParams() {\n        return this._identifyingParams()\n    }\n    /** @ignore */\n    async _generate(\n        messages: BaseMessage[],\n        options: this[\"ParsedCallOptions\"],\n        runManager?: CallbackManagerForLLMRun\n    ): Promise<ChatResult> {\n        const tokenUsage: TokenUsage = {}\n        const params = this.invocationParams(options)\n        const messagesMapped: any[] = convertMessagesToOpenAIParams(messages)\n        if (params.stream) {\n            const stream = this._streamResponseChunks(messages, options, runManager)\n            const finalChunks: Record<number, ChatGenerationChunk> = {}\n            for await (const chunk of stream) {\n                //@ts-ignore\n                chunk.message.response_metadata = {\n                    ...chunk.generationInfo,\n                    //@ts-ignore\n                    ...chunk.message.response_metadata\n                }\n                const index = chunk.generationInfo?.completion ?? 0\n                if (finalChunks[index] === undefined) {\n                    finalChunks[index] = chunk\n                } else {\n                    finalChunks[index] = finalChunks[index].concat(chunk)\n                }\n            }\n            const generations = Object.entries(finalChunks)\n                .sort(([aKey], [bKey]) => parseInt(aKey, 10) - parseInt(bKey, 10))\n                .map(([_, value]) => value)\n            const { functions, function_call } = this.invocationParams(options)\n            // OpenAI does not support token usage report under stream mode,\n            // fallback to estimation.\n            const promptTokenUsage = await this.getEstimatedTokenCountFromPrompt(\n                messages,\n                functions,\n                function_call\n            )\n            const completionTokenUsage =\n                await this.getNumTokensFromGenerations(generations)\n            tokenUsage.promptTokens = promptTokenUsage\n            tokenUsage.completionTokens = completionTokenUsage\n            tokenUsage.totalTokens = promptTokenUsage + completionTokenUsage\n            return { generations, llmOutput: { estimatedTokenUsage: tokenUsage } }\n        } else {\n            const data = await this.completionWithRetry(\n                {\n                    ...params,\n                    //@ts-ignore\n                    stream: false,\n                    messages: messagesMapped\n                },\n                {\n                    signal: options?.signal,\n                    //@ts-ignore\n                    ...options?.options\n                }\n            )\n            const {\n                completion_tokens: completionTokens,\n                prompt_tokens: promptTokens,\n                total_tokens: totalTokens\n                //@ts-ignore\n            } = data?.usage ?? {}\n            if (completionTokens) {\n                tokenUsage.completionTokens =\n                    (tokenUsage.completionTokens ?? 0) + completionTokens\n            }\n            if (promptTokens) {\n                tokenUsage.promptTokens = (tokenUsage.promptTokens ?? 0) + promptTokens\n            }\n            if (totalTokens) {\n                tokenUsage.totalTokens = (tokenUsage.totalTokens ?? 0) + totalTokens\n            }\n            const generations = []\n            //@ts-ignore\n            for (const part of data?.choices ?? []) {\n                const text = part.message?.content ?? \"\"\n                const generation = {\n                    text,\n                    message: openAIResponseToChatMessage(\n                        part.message ?? { role: \"assistant\" }\n                    )\n                }\n                //@ts-ignore\n                generation.generationInfo = {\n                    ...(part.finish_reason ? { finish_reason: part.finish_reason } : {}),\n                    ...(part.logprobs ? { logprobs: part.logprobs } : {})\n                }\n                generations.push(generation)\n            }\n            return {\n                generations,\n                llmOutput: { tokenUsage }\n            }\n        }\n    }\n    /**\n     * Estimate the number of tokens a prompt will use.\n     * Modified from: https://github.com/hmarr/openai-chat-tokens/blob/main/src/index.ts\n     */\n    async getEstimatedTokenCountFromPrompt(messages, functions, function_call) {\n        let tokens = (await this.getNumTokensFromMessages(messages)).totalCount\n        if (functions && messages.find((m) => m._getType() === \"system\")) {\n            tokens -= 4\n        }\n        if (function_call === \"none\") {\n            tokens += 1\n        } else if (typeof function_call === \"object\") {\n            tokens += (await this.getNumTokens(function_call.name)) + 4\n        }\n        return tokens\n    }\n    /**\n     * Estimate the number of tokens an array of generations have used.\n     */\n    async getNumTokensFromGenerations(generations) {\n        const generationUsages = await Promise.all(\n            generations.map(async (generation) => {\n                if (generation.message.additional_kwargs?.function_call) {\n                    return (await this.getNumTokensFromMessages([generation.message]))\n                        .countPerMessage[0]\n                } else {\n                    return await this.getNumTokens(generation.message.content)\n                }\n            })\n        )\n        return generationUsages.reduce((a, b) => a + b, 0)\n    }\n    async getNumTokensFromMessages(messages) {\n        let totalCount = 0\n        let tokensPerMessage = 0\n        let tokensPerName = 0\n        // From: https://github.com/openai/openai-cookbook/blob/main/examples/How_to_format_inputs_to_ChatGPT_models.ipynb\n        if (this.modelName === \"gpt-3.5-turbo-0301\") {\n            tokensPerMessage = 4\n            tokensPerName = -1\n        } else {\n            tokensPerMessage = 3\n            tokensPerName = 1\n        }\n        const countPerMessage = await Promise.all(\n            messages.map(async (message) => {\n                const textCount = await this.getNumTokens(message.content)\n                const roleCount = await this.getNumTokens(messageToOpenAIRole(message))\n                const nameCount =\n                    message.name !== undefined\n                        ? tokensPerName + (await this.getNumTokens(message.name))\n                        : 0\n                let count = textCount + tokensPerMessage + roleCount + nameCount\n                // From: https://github.com/hmarr/openai-chat-tokens/blob/main/src/index.ts messageTokenEstimate\n                const openAIMessage = message\n                if (openAIMessage._getType() === \"function\") {\n                    count -= 2\n                }\n                if (openAIMessage.additional_kwargs?.function_call) {\n                    count += 3\n                }\n                if (openAIMessage?.additional_kwargs.function_call?.name) {\n                    count += await this.getNumTokens(\n                        openAIMessage.additional_kwargs.function_call?.name\n                    )\n                }\n                if (openAIMessage.additional_kwargs.function_call?.arguments) {\n                    try {\n                        count += await this.getNumTokens(\n                            // Remove newlines and spaces\n                            JSON.stringify(\n                                JSON.parse(\n                                    openAIMessage.additional_kwargs.function_call?.arguments\n                                )\n                            )\n                        )\n                    } catch (error) {\n                        console.error(\n                            \"Error parsing function arguments\",\n                            error,\n                            JSON.stringify(openAIMessage.additional_kwargs.function_call)\n                        )\n                        count += await this.getNumTokens(\n                            openAIMessage.additional_kwargs.function_call?.arguments\n                        )\n                    }\n                }\n                totalCount += count\n                return count\n            })\n        )\n        totalCount += 3 // every reply is primed with <|start|>assistant<|message|>\n        return { totalCount, countPerMessage }\n    }\n    async completionWithRetry(\n        request: OpenAIClient.Chat.ChatCompletionCreateParamsStreaming,\n        options?: OpenAICoreRequestOptions\n    ) {\n        const requestOptions = this._getClientOptions(options)\n        return this.caller.call(async () => {\n            try {\n                const res = await this.client.chat.completions.create(\n                    request,\n                    requestOptions\n                )\n                return res\n            } catch (e) {\n                const error = wrapOpenAIClientError(e)\n                throw error\n            }\n        })\n    }\n    _getClientOptions(options) {\n        if (!this.client) {\n            const openAIEndpointConfig = {\n                azureOpenAIApiDeploymentName: this.azureOpenAIApiDeploymentName,\n                azureOpenAIApiInstanceName: this.azureOpenAIApiInstanceName,\n                azureOpenAIApiKey: this.azureOpenAIApiKey,\n                azureOpenAIBasePath: this.azureOpenAIBasePath,\n                baseURL: this.clientConfig.baseURL\n            }\n            const endpoint = getEndpoint(openAIEndpointConfig)\n            const params = {\n                ...this.clientConfig,\n                baseURL: endpoint,\n                timeout: this.timeout,\n                maxRetries: 0\n            }\n            if (!params.baseURL) {\n                delete params.baseURL\n            }\n            this.client = new OpenAIClient(params)\n        }\n        const requestOptions = {\n            ...this.clientConfig,\n            ...options\n        }\n        if (this.azureOpenAIApiKey) {\n            requestOptions.headers = {\n                \"api-key\": this.azureOpenAIApiKey,\n                ...requestOptions.headers\n            }\n            requestOptions.query = {\n                \"api-version\": this.azureOpenAIApiVersion,\n                ...requestOptions.query\n            }\n        }\n        return requestOptions\n    }\n    _llmType() {\n        return \"openai\"\n    }\n    override bindTools(\n        tools: BindToolsInput[],\n        kwargs?: Partial<this[\"ParsedCallOptions\"]>\n    ): Runnable<BaseLanguageModelInput, AIMessageChunk, CallOptions> {\n        return this.withConfig({\n            tools: tools.map((tool) => convertToOpenAITool(tool)),\n            ...kwargs\n        } as Partial<CallOptions>)\n    }\n    /** @ignore */\n    _combineLLMOutput(...llmOutputs) {\n        return llmOutputs.reduce(\n            (acc, llmOutput) => {\n                if (llmOutput && llmOutput.tokenUsage) {\n                    acc.tokenUsage.completionTokens +=\n                        llmOutput.tokenUsage.completionTokens ?? 0\n                    acc.tokenUsage.promptTokens += llmOutput.tokenUsage.promptTokens ?? 0\n                    acc.tokenUsage.totalTokens += llmOutput.tokenUsage.totalTokens ?? 0\n                }\n                return acc\n            },\n            {\n                tokenUsage: {\n                    completionTokens: 0,\n                    promptTokens: 0,\n                    totalTokens: 0\n                }\n            }\n        )\n    }\n    withStructuredOutput(outputSchema, config) {\n        // eslint-disable-next-line @typescript-eslint/no-explicit-any\n        let schema\n        let name\n        let method\n        let includeRaw\n        if (isStructuredOutputMethodParams(outputSchema)) {\n            schema = outputSchema.schema\n            name = outputSchema.name\n            method = outputSchema.method\n            includeRaw = outputSchema.includeRaw\n        } else {\n            schema = outputSchema\n            name = config?.name\n            method = config?.method\n            includeRaw = config?.includeRaw\n        }\n        let llm\n        let outputParser\n        if (method === \"jsonMode\") {\n            llm = this\n            if (isZodSchema(schema)) {\n                outputParser = StructuredOutputParser.fromZodSchema(schema)\n            } else {\n                outputParser = new JsonOutputParser()\n            }\n        } else {\n            let functionName = name ?? \"extract\"\n            // Is function calling\n\n            let openAIFunctionDefinition\n            if (\n                typeof schema.name === \"string\" &&\n                typeof schema.parameters === \"object\" &&\n                schema.parameters != null\n            ) {\n                openAIFunctionDefinition = schema\n                functionName = schema.name\n            } else {\n                openAIFunctionDefinition = {\n                    name: schema.title ?? functionName,\n                    description: schema.description ?? \"\",\n                    parameters: schema\n                }\n            }\n            llm = this\n            outputParser = new JsonOutputKeyToolsParser({\n                returnSingle: true,\n                keyName: functionName\n            })\n        }\n        if (!includeRaw) {\n            return llm.pipe(outputParser)\n        }\n        const parserAssign = RunnablePassthrough.assign({\n            // eslint-disable-next-line @typescript-eslint/no-explicit-any\n            parsed: (input, config) => outputParser.invoke(input.raw, config)\n        })\n        const parserNone = RunnablePassthrough.assign({\n            parsed: () => null\n        })\n        const parsedWithFallback = parserAssign.withFallbacks({\n            fallbacks: [parserNone]\n        })\n        return RunnableSequence.from([\n            {\n                raw: llm\n            },\n            parsedWithFallback\n        ] as any)\n    }\n}\nfunction isZodSchema(\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    input\n) {\n    // Check for a characteristic method of Zod schemas\n    return typeof input?.parse === \"function\"\n}\nfunction isStructuredOutputMethodParams(\n    x\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n) {\n    return (\n        x !== undefined &&\n        // eslint-disable-next-line @typescript-eslint/no-explicit-any\n        typeof x.schema === \"object\"\n    )\n}\n"
  },
  {
    "path": "src/models/OAIEmbedding.ts",
    "content": "import { type ClientOptions, OpenAI as OpenAIClient } from \"openai\"\nimport { Embeddings, EmbeddingsParams } from \"@langchain/core/embeddings\"\nimport { chunkArray } from \"@langchain/core/utils/chunk_array\"\nimport { LegacyOpenAIInput } from \"./types\"\nimport { wrapOpenAIClientError } from \"./utils/openai\"\n\nexport interface OpenAIEmbeddingsParams extends EmbeddingsParams {\n  modelName: string\n  model: string\n  dimensions?: number\n  timeout?: number\n  batchSize?: number\n  stripNewLines?: boolean\n  signal?: AbortSignal\n}\n\nexport class OAIEmbedding extends Embeddings {\n  modelName = \"text-embedding-ada-002\"\n\n  model = \"text-embedding-ada-002\"\n  batchSize = 512\n  stripNewLines = true\n\n  dimensions?: number\n\n  timeout?: number\n\n  azureOpenAIApiVersion?: string\n\n  azureOpenAIApiKey?: string\n\n  azureADTokenProvider?: () => Promise<string>\n\n  azureOpenAIApiInstanceName?: string\n\n  azureOpenAIApiDeploymentName?: string\n\n  azureOpenAIBasePath?: string\n\n  organization?: string\n\n  protected client: OpenAIClient\n\n  protected clientConfig: ClientOptions\n\n  signal?: AbortSignal\n\n  constructor(\n    fields?: Partial<OpenAIEmbeddingsParams> & {\n      verbose?: boolean\n      /**\n       * The OpenAI API key to use.\n       * Alias for `apiKey`.\n       */\n      openAIApiKey?: string\n      /** The OpenAI API key to use. */\n      apiKey?: string\n      configuration?: ClientOptions\n    },\n    configuration?: ClientOptions & LegacyOpenAIInput\n  ) {\n    const fieldsWithDefaults = { maxConcurrency: 2, ...fields }\n\n    super(fieldsWithDefaults)\n\n    let apiKey = fieldsWithDefaults?.apiKey ?? fieldsWithDefaults?.openAIApiKey\n\n    this.modelName =\n      fieldsWithDefaults?.model ?? fieldsWithDefaults?.modelName ?? this.model\n    this.model = this.modelName\n    this.batchSize = fieldsWithDefaults?.batchSize || this.batchSize\n    this.stripNewLines = fieldsWithDefaults?.stripNewLines ?? this.stripNewLines\n    this.timeout = fieldsWithDefaults?.timeout\n    this.dimensions = fieldsWithDefaults?.dimensions\n\n    if (fields.signal) {\n      this.signal = fields.signal\n    }\n\n    this.clientConfig = {\n      apiKey,\n      organization: this.organization,\n      baseURL: configuration?.basePath,\n      dangerouslyAllowBrowser: true,\n      defaultHeaders: configuration?.baseOptions?.headers,\n      defaultQuery: configuration?.baseOptions?.params,\n      ...configuration,\n      ...fields?.configuration\n    }\n\n    // initialize the client\n    this.client = new OpenAIClient(this.clientConfig)\n  }\n\n  async embedDocuments(texts: string[]): Promise<number[][]> {\n    const batches = chunkArray(\n      this.stripNewLines ? texts.map((t) => t.replace(/\\n/g, \" \")) : texts,\n      this.batchSize\n    )\n\n    const batchRequests = batches.map((batch) => {\n      const params: OpenAIClient.EmbeddingCreateParams = {\n        model: this.model,\n        input: batch,\n        encoding_format: \"float\"\n      }\n      if (this.dimensions) {\n        params.dimensions = this.dimensions\n      }\n      return this.embeddingWithRetry(params)\n    })\n    const batchResponses = await Promise.all(batchRequests)\n\n    const embeddings: number[][] = []\n    for (let i = 0; i < batchResponses.length; i += 1) {\n      const batch = batches[i]\n      const { data: batchResponse } = batchResponses[i]\n      for (let j = 0; j < batch.length; j += 1) {\n        embeddings.push(batchResponse[j].embedding)\n      }\n    }\n    return embeddings\n  }\n\n  async embedQuery(text: string): Promise<number[]> {\n    const params: OpenAIClient.EmbeddingCreateParams = {\n      model: this.model,\n      input: this.stripNewLines ? text.replace(/\\n/g, \" \") : text,\n      encoding_format: \"float\"\n    }\n    if (this.dimensions) {\n      params.dimensions = this.dimensions\n    }\n    const { data } = await this.embeddingWithRetry(params)\n    return data[0].embedding\n  }\n\n  async _embed(texts: string[]): Promise<number[][]> {\n    const embeddings: number[][] = await Promise.all(\n      texts.map((text) => this.caller.call(() => this.embedQuery(text)))\n    )\n\n    return embeddings\n  }\n\n  protected async embeddingWithRetry(\n    request: OpenAIClient.EmbeddingCreateParams\n  ) {\n    const requestOptions: { headers?: Record<string, string> } = {}\n    if (this.azureOpenAIApiKey) {\n      requestOptions.headers = {\n        \"api-key\": this.azureOpenAIApiKey,\n        ...requestOptions.headers\n      }\n    }\n    return this.caller.call(async () => {\n      try {\n        const res = await this.client.embeddings.create(request, {\n          ...requestOptions,\n          signal: this.signal\n        })\n        return res\n      } catch (e) {\n        const error = wrapOpenAIClientError(e)\n        throw error\n      }\n    })\n  }\n}\n"
  },
  {
    "path": "src/models/OllamaEmbedding.ts",
    "content": "import { Embeddings, EmbeddingsParams } from \"@langchain/core/embeddings\"\nimport type { StringWithAutocomplete } from \"@langchain/core/utils/types\"\nimport { parseKeepAlive } from \"./utils/ollama\"\nimport { getCustomOllamaHeaders } from \"@/services/app\"\n\nexport interface OllamaInput {\n  embeddingOnly?: boolean\n  f16KV?: boolean\n  frequencyPenalty?: number\n  headers?: Record<string, string>\n  keepAlive?: any\n  logitsAll?: boolean\n  lowVram?: boolean\n  mainGpu?: number\n  model?: string\n  baseUrl?: string\n  mirostat?: number\n  mirostatEta?: number\n  mirostatTau?: number\n  numBatch?: number\n  numCtx?: number\n  numGpu?: number\n  numGqa?: number\n  numKeep?: number\n  numPredict?: number\n  numThread?: number\n  penalizeNewline?: boolean\n  presencePenalty?: number\n  repeatLastN?: number\n  repeatPenalty?: number\n  ropeFrequencyBase?: number\n  ropeFrequencyScale?: number\n  temperature?: number\n  stop?: string[]\n  tfsZ?: number\n  topK?: number\n  topP?: number\n  typicalP?: number\n  useMLock?: boolean\n  useMMap?: boolean\n  vocabOnly?: boolean\n  format?: StringWithAutocomplete<\"json\">\n}\nexport interface OllamaRequestParams {\n  model: string\n  format?: StringWithAutocomplete<\"json\">\n  images?: string[]\n  options: {\n    embedding_only?: boolean\n    f16_kv?: boolean\n    frequency_penalty?: number\n    logits_all?: boolean\n    low_vram?: boolean\n    main_gpu?: number\n    mirostat?: number\n    mirostat_eta?: number\n    mirostat_tau?: number\n    num_batch?: number\n    num_ctx?: number\n    num_gpu?: number\n    num_gqa?: number\n    num_keep?: number\n    num_thread?: number\n    num_predict?: number\n    penalize_newline?: boolean\n    presence_penalty?: number\n    repeat_last_n?: number\n    repeat_penalty?: number\n    rope_frequency_base?: number\n    rope_frequency_scale?: number\n    temperature?: number\n    stop?: string[]\n    tfs_z?: number\n    top_k?: number\n    top_p?: number\n    typical_p?: number\n    use_mlock?: boolean\n    use_mmap?: boolean\n    vocab_only?: boolean\n  }\n}\n\ntype CamelCasedRequestOptions = Omit<\n  OllamaInput,\n  \"baseUrl\" | \"model\" | \"format\" | \"headers\"\n>\n\n/**\n * Interface for OllamaEmbeddings parameters. Extends EmbeddingsParams and\n * defines additional parameters specific to the OllamaEmbeddings class.\n */\ninterface OllamaEmbeddingsParams extends EmbeddingsParams {\n  /** The Ollama model to use, e.g: \"llama2:13b\" */\n  model?: string\n\n  /** Base URL of the Ollama server, defaults to \"http://localhost:11434\" */\n  baseUrl?: string\n\n  /** Extra headers to include in the Ollama API request */\n  headers?: Record<string, string>\n\n  /** Defaults to \"5m\" */\n  keepAlive?: any\n\n  /** Advanced Ollama API request parameters in camelCase, see\n   * https://github.com/jmorganca/ollama/blob/main/docs/modelfile.md#valid-parameters-and-values\n   * for details of the available parameters.\n   */\n  requestOptions?: CamelCasedRequestOptions\n\n  signal?: AbortSignal\n}\n\nexport class OllamaEmbeddingsPageAssist extends Embeddings {\n  model = \"llama2\"\n\n  baseUrl = \"http://localhost:11434\"\n\n  headers?: Record<string, string>\n\n  keepAlive?: string\n\n  requestOptions?: OllamaRequestParams[\"options\"]\n\n  signal?: AbortSignal\n\n  constructor(params?: OllamaEmbeddingsParams) {\n    super({ maxConcurrency: 1, ...params })\n\n    if (params?.model) {\n      this.model = params.model\n    }\n\n    if (params?.baseUrl) {\n      this.baseUrl = params.baseUrl\n    }\n\n    if (params?.headers) {\n      this.headers = params.headers\n    }\n\n    if (params?.keepAlive) {\n      this.keepAlive = parseKeepAlive(params.keepAlive)\n    }\n\n    if (params?.requestOptions) {\n      this.requestOptions = this._convertOptions(params.requestOptions)\n    }\n\n    if (params?.signal) {\n      this.signal = params.signal\n    }\n  }\n\n  /** convert camelCased Ollama request options like \"useMMap\" to\n   * the snake_cased equivalent which the ollama API actually uses.\n   * Used only for consistency with the llms/Ollama and chatModels/Ollama classes\n   */\n  _convertOptions(requestOptions: CamelCasedRequestOptions) {\n    const snakeCasedOptions: Record<string, unknown> = {}\n    const mapping: Record<keyof CamelCasedRequestOptions, string> = {\n      embeddingOnly: \"embedding_only\",\n      f16KV: \"f16_kv\",\n      frequencyPenalty: \"frequency_penalty\",\n      keepAlive: \"keep_alive\",\n      logitsAll: \"logits_all\",\n      lowVram: \"low_vram\",\n      mainGpu: \"main_gpu\",\n      mirostat: \"mirostat\",\n      mirostatEta: \"mirostat_eta\",\n      mirostatTau: \"mirostat_tau\",\n      numBatch: \"num_batch\",\n      numCtx: \"num_ctx\",\n      numGpu: \"num_gpu\",\n      numGqa: \"num_gqa\",\n      numKeep: \"num_keep\",\n      numPredict: \"num_predict\",\n      numThread: \"num_thread\",\n      penalizeNewline: \"penalize_newline\",\n      presencePenalty: \"presence_penalty\",\n      repeatLastN: \"repeat_last_n\",\n      repeatPenalty: \"repeat_penalty\",\n      ropeFrequencyBase: \"rope_frequency_base\",\n      ropeFrequencyScale: \"rope_frequency_scale\",\n      temperature: \"temperature\",\n      stop: \"stop\",\n      tfsZ: \"tfs_z\",\n      topK: \"top_k\",\n      topP: \"top_p\",\n      typicalP: \"typical_p\",\n      useMLock: \"use_mlock\",\n      useMMap: \"use_mmap\",\n      vocabOnly: \"vocab_only\"\n    }\n\n    for (const [key, value] of Object.entries(requestOptions)) {\n      const snakeCasedOption = mapping[key as keyof CamelCasedRequestOptions]\n      if (snakeCasedOption) {\n        snakeCasedOptions[snakeCasedOption] = value\n      }\n    }\n    return snakeCasedOptions\n  }\n\n  async _request(prompt: string | string[]): Promise<number[] | number[][]> {\n    const { model, baseUrl, keepAlive, requestOptions } = this\n\n    let formattedBaseUrl = baseUrl\n    if (formattedBaseUrl.startsWith(\"http://localhost:\")) {\n      // Node 18 has issues with resolving \"localhost\"\n      // See https://github.com/node-fetch/node-fetch/issues/1624\n      formattedBaseUrl = formattedBaseUrl.replace(\n        \"http://localhost:\",\n        \"http://127.0.0.1:\"\n      )\n    }\n    const customHeaders = await getCustomOllamaHeaders()\n\n    const response = await fetch(`${formattedBaseUrl}/api/embed`, {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n        ...this.headers,\n        ...customHeaders\n      },\n      body: JSON.stringify({\n        input: prompt,\n        model,\n        keep_alive: keepAlive,\n        options: requestOptions\n      }),\n      signal: this.signal\n    })\n    if (!response.ok) {\n      throw new Error(\n        `Request to Ollama server failed: ${response.status} ${response.statusText}`\n      )\n    }\n\n    const json = await response.json()\n    return json?.embeddings\n  }\n\n  async _embed(texts: string[]): Promise<number[][]> {\n    try {\n      // Try new batch method first\n      const embeddings: number[][] = (await this._request(texts)) as number[][]\n      return embeddings\n    } catch (error) {\n      // Fallback to old method if batch fails\n      const embeddings: number[][] = await Promise.all(\n        texts.map((text) => this.caller.call(() => this._request(text)))\n      ) as any\n      return embeddings\n    }\n  }\n\n  async embedDocuments(documents: string[]) {\n    return this._embed(documents)\n  }\n\n  async embedQuery(document: string) {\n    return (await this.embedDocuments([document]))[0]\n  }\n}\n"
  },
  {
    "path": "src/models/embedding.ts",
    "content": "import { getModelInfo, isCustomModel, isOllamaModel } from \"@/db/dexie/models\"\nimport { OllamaEmbeddingsPageAssist } from \"./OllamaEmbedding\"\nimport { OAIEmbedding } from \"./OAIEmbedding\"\nimport { getOpenAIConfigById } from \"@/db/dexie/openai\"\n\ntype EmbeddingModel = {\n    model: string\n    baseUrl: string\n    signal?: AbortSignal\n    keepAlive?: string\n}\n\n\nexport const pageAssistEmbeddingModel = async ({ baseUrl, model, keepAlive, signal }: EmbeddingModel) => {\n    const isCustom = isCustomModel(model)\n    if (isCustom) {\n        const modelInfo = await getModelInfo(model)\n        const providerInfo = await getOpenAIConfigById(modelInfo.provider_id)\n        return new OAIEmbedding({\n            modelName: modelInfo.model_id,\n            model: modelInfo.model_id,\n            signal,\n            openAIApiKey: providerInfo.apiKey || \"temp\",\n            configuration: {\n                apiKey: providerInfo.apiKey || \"temp\",\n                baseURL: providerInfo.baseUrl || \"\",\n            }\n        }) as any\n    }\n\n    return new OllamaEmbeddingsPageAssist({\n        model,\n        baseUrl,\n        keepAlive,\n        signal\n    })\n}"
  },
  {
    "path": "src/models/index.ts",
    "content": "import { getModelInfo, isCustomModel, isOllamaModel } from \"@/db/dexie/models\"\nimport { ChatChromeAI } from \"./ChatChromeAi\"\nimport { ChatOllama } from \"./ChatOllama\"\nimport { getOpenAIConfigById } from \"@/db/dexie/openai\"\nimport { urlRewriteRuntime } from \"@/libs/runtime\"\nimport { ChatGoogleAI } from \"./ChatGoogleAI\"\nimport { CustomChatOpenAI } from \"./CustomChatOpenAI\"\nimport { ChatAnthropic } from \"./CustomChatAnthropic\"\nimport { getCustomHeaders } from \"@/utils/clean-headers\"\nimport {\n  getAllDefaultModelSettings,\n  getModelSettings\n} from \"@/services/model-settings\"\nimport { useStoreChatModelSettings, normalizeThinking } from \"@/store/model\"\n\nexport const pageAssistModel = async ({\n  model,\n  baseUrl\n}: {\n  model: string\n  baseUrl: string\n}) => {\n  const currentChatModelSettings = useStoreChatModelSettings.getState()\n  const userDefaultModelSettings = await getAllDefaultModelSettings()\n\n  const {\n    keepAlive,\n    temperature,\n    topK,\n    topP,\n    numCtx,\n    seed,\n    numGpu,\n    numPredict,\n    useMMap,\n    minP,\n    repeatLastN,\n    repeatPenalty,\n    tfsZ,\n    numKeep,\n    numThread,\n    useMlock,\n    reasoningEffort\n  } = {\n    keepAlive:\n      currentChatModelSettings?.keepAlive ??\n      userDefaultModelSettings?.keepAlive,\n    temperature:\n      currentChatModelSettings?.temperature ??\n      userDefaultModelSettings?.temperature,\n    topK: currentChatModelSettings?.topK ?? userDefaultModelSettings?.topK,\n    topP: currentChatModelSettings?.topP ?? userDefaultModelSettings?.topP,\n    numCtx:\n      currentChatModelSettings?.numCtx ?? userDefaultModelSettings?.numCtx,\n    seed: currentChatModelSettings?.seed,\n    numGpu:\n      currentChatModelSettings?.numGpu ?? userDefaultModelSettings?.numGpu,\n    numPredict:\n      currentChatModelSettings?.numPredict ??\n      userDefaultModelSettings?.numPredict,\n    useMMap:\n      currentChatModelSettings?.useMMap ?? userDefaultModelSettings?.useMMap,\n    minP: currentChatModelSettings?.minP ?? userDefaultModelSettings?.minP,\n    repeatLastN:\n      currentChatModelSettings?.repeatLastN ??\n      userDefaultModelSettings?.repeatLastN,\n    repeatPenalty:\n      currentChatModelSettings?.repeatPenalty ??\n      userDefaultModelSettings?.repeatPenalty,\n    tfsZ: currentChatModelSettings?.tfsZ ?? userDefaultModelSettings?.tfsZ,\n    numKeep:\n      currentChatModelSettings?.numKeep ?? userDefaultModelSettings?.numKeep,\n    numThread:\n      currentChatModelSettings?.numThread ??\n      userDefaultModelSettings?.numThread,\n    useMlock:\n      currentChatModelSettings?.useMlock ?? userDefaultModelSettings?.useMlock,\n    reasoningEffort: currentChatModelSettings?.reasoningEffort\n  }\n\n  if (model === \"chrome::gemini-nano::page-assist\") {\n    return new ChatChromeAI({\n      temperature,\n      topK\n    })\n  }\n\n  const isCustom = isCustomModel(model)\n  const modelSettings = await getModelSettings(model)\n\n  if (isCustom) {\n    const modelInfo = await getModelInfo(model)\n    const providerInfo = await getOpenAIConfigById(modelInfo.provider_id)\n\n    if (isOllamaModel(model)) {\n      await urlRewriteRuntime(providerInfo.baseUrl || \"\")\n    }\n\n    if (providerInfo?.fix_cors) {\n      console.log(\"Fixing CORS for provider:\", providerInfo.provider)\n      await urlRewriteRuntime(providerInfo.baseUrl || \"\")\n    }\n\n    const modelConfig = {\n      maxTokens: modelSettings?.numPredict || numPredict,\n      temperature: modelSettings?.temperature || temperature,\n      topP: modelSettings?.topP || topP,\n      topK: modelSettings?.topK || topK,\n      minP: modelSettings?.minP || minP,\n      numCtx: modelSettings?.numCtx || numCtx,\n      reasoningEffort:\n        modelSettings?.reasoningEffort || (reasoningEffort as any)\n    }\n\n    if (providerInfo.provider === \"gemini\") {\n      return new ChatGoogleAI({\n        modelName: modelInfo.model_id,\n        openAIApiKey: providerInfo.apiKey || \"temp\",\n        temperature: modelConfig?.temperature,\n        topP: modelConfig?.topP,\n        maxTokens: modelConfig?.maxTokens,\n        configuration: {\n          apiKey: providerInfo.apiKey || \"temp\",\n          baseURL: providerInfo.baseUrl || \"\",\n          defaultHeaders: getCustomHeaders({\n            headers: providerInfo?.headers || []\n          })\n        }\n      }) as any\n    }\n\n    if (providerInfo.provider === \"anthropic\") {\n      return new ChatAnthropic({\n        apiKey: providerInfo.apiKey || \"\",\n        modelName: modelInfo.model_id,\n        temperature: modelConfig?.temperature,\n        topP: modelConfig?.topP,\n        maxTokens: modelConfig?.maxTokens,\n        topK: modelConfig?.topK\n      }) as any\n    }\n\n    if (providerInfo.provider === \"openrouter\") {\n      return new CustomChatOpenAI({\n        modelName: modelInfo.model_id,\n        openAIApiKey: providerInfo.apiKey || \"temp\",\n        temperature: modelConfig?.temperature,\n        topP: modelConfig?.topP,\n        maxTokens: modelConfig?.maxTokens,\n        modelKwargs: {\n          ...(modelConfig?.topK && { top_k: modelConfig.topK }),\n          ...(modelConfig?.minP && { min_p: modelConfig.minP }),\n        },\n        configuration: {\n          apiKey: providerInfo.apiKey || \"temp\",\n          baseURL: providerInfo.baseUrl || \"\",\n          defaultHeaders: {\n            \"HTTP-Referer\": \"https://pageassist.xyz/\",\n            \"X-Title\": \"Page Assist\",\n            ...getCustomHeaders({\n              headers: providerInfo?.headers || []\n            })\n          }\n        },\n        reasoning_effort: modelConfig?.reasoningEffort as any\n      }) as any\n    }\n\n    if (providerInfo.provider === \"ollama2\") {\n      const _keepAlive = currentChatModelSettings?.keepAlive || modelSettings?.keepAlive || keepAlive || \"\"\n      const payload = {\n        keepAlive: _keepAlive.length > 0 ? _keepAlive : undefined,\n        temperature: currentChatModelSettings?.temperature ?? modelSettings?.temperature ?? temperature,\n        topK: currentChatModelSettings?.topK ?? modelSettings?.topK ?? topK,\n        topP: currentChatModelSettings?.topP ?? modelSettings?.topP ?? topP,\n        numCtx: currentChatModelSettings?.numCtx ?? modelSettings?.numCtx ?? numCtx,\n        numGpu: currentChatModelSettings?.numGpu ?? modelSettings?.numGpu ?? numGpu,\n        numPredict: currentChatModelSettings?.numPredict ?? modelSettings?.numPredict ?? numPredict,\n        useMMap: currentChatModelSettings?.useMMap ?? modelSettings?.useMMap ?? useMMap,\n        minP: currentChatModelSettings?.minP ?? modelSettings?.minP ?? minP,\n        repeatPenalty: currentChatModelSettings?.repeatPenalty ?? modelSettings?.repeatPenalty ?? repeatPenalty,\n        repeatLastN: currentChatModelSettings?.repeatLastN ?? modelSettings?.repeatLastN ?? repeatLastN,\n        tfsZ: currentChatModelSettings?.tfsZ ?? modelSettings?.tfsZ ?? tfsZ,\n        numKeep: currentChatModelSettings?.numKeep ?? modelSettings?.numKeep ?? numKeep,\n        numThread: currentChatModelSettings?.numThread ?? modelSettings?.numThread ?? numThread,\n        useMlock: currentChatModelSettings?.useMlock ?? modelSettings?.useMLock ?? useMlock,\n        thinking: normalizeThinking(\n          currentChatModelSettings?.thinking ?? modelSettings?.thinking,\n          model\n        )\n      }\n\n      return new ChatOllama({\n        baseUrl: providerInfo.baseUrl,\n        model: modelInfo.model_id,\n        seed,\n        headers: {\n          ...(providerInfo.apiKey && {\n            Authorization: `Bearer ${providerInfo.apiKey}`\n          }),\n          ...getCustomHeaders({\n            headers: providerInfo?.headers || []\n          })\n        },\n        ...payload\n      })\n    }\n\n    return new CustomChatOpenAI({\n      modelName: modelInfo.model_id,\n      openAIApiKey: providerInfo.apiKey || \"temp\",\n      temperature: modelConfig?.temperature,\n      topP: modelConfig?.topP,\n      maxTokens: modelConfig?.maxTokens,\n      modelKwargs: {\n        ...(modelConfig?.topK && { top_k: topK }),\n        ...(modelConfig?.minP && { min_p: minP }), \n      },\n      configuration: {\n        apiKey: providerInfo.apiKey || \"temp\",\n        baseURL: providerInfo.baseUrl || \"\",\n        defaultHeaders: getCustomHeaders({\n          headers: providerInfo?.headers || []\n        })\n      },\n      reasoning_effort: modelConfig?.reasoningEffort as any\n    }) as any\n  }\n\n  const _keepAlive = currentChatModelSettings?.keepAlive || modelSettings?.keepAlive || keepAlive || \"\"\n  const payload = {\n    keepAlive: _keepAlive.length > 0 ? _keepAlive : undefined,\n    temperature: currentChatModelSettings?.temperature ?? modelSettings?.temperature ?? temperature,\n    topK: currentChatModelSettings?.topK ?? modelSettings?.topK ?? topK,\n    topP: currentChatModelSettings?.topP ?? modelSettings?.topP ?? topP,\n    numCtx: currentChatModelSettings?.numCtx ?? modelSettings?.numCtx ?? numCtx,\n    numGpu: currentChatModelSettings?.numGpu ?? modelSettings?.numGpu ?? numGpu,\n    numPredict: currentChatModelSettings?.numPredict ?? modelSettings?.numPredict ?? numPredict,\n    useMMap: currentChatModelSettings?.useMMap ?? modelSettings?.useMMap ?? useMMap,\n    minP: currentChatModelSettings?.minP ?? modelSettings?.minP ?? minP,\n    repeatPenalty: currentChatModelSettings?.repeatPenalty ?? modelSettings?.repeatPenalty ?? repeatPenalty,\n    repeatLastN: currentChatModelSettings?.repeatLastN ?? modelSettings?.repeatLastN ?? repeatLastN,\n    tfsZ: currentChatModelSettings?.tfsZ ?? modelSettings?.tfsZ ?? tfsZ,\n    numKeep: currentChatModelSettings?.numKeep ?? modelSettings?.numKeep ?? numKeep,\n    numThread: currentChatModelSettings?.numThread ?? modelSettings?.numThread ?? numThread,\n    useMlock: currentChatModelSettings?.useMlock ?? modelSettings?.useMLock ?? useMlock,\n    thinking: normalizeThinking(\n      currentChatModelSettings?.thinking ?? modelSettings?.thinking,\n      model\n    )\n  }\n\n  return new ChatOllama({\n    baseUrl,\n    model,\n    seed,\n    ...payload\n  })\n}\n"
  },
  {
    "path": "src/models/types.ts",
    "content": "export type OpenAICoreRequestOptions<\n    Req extends object = Record<string, unknown>\n> = {\n    path?: string;\n    query?: Req | undefined;\n    body?: Req | undefined;\n    headers?: Record<string, string | null | undefined> | undefined;\n\n    maxRetries?: number;\n    stream?: boolean | undefined;\n    timeout?: number;\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    httpAgent?: any;\n    signal?: AbortSignal | undefined | null;\n    idempotencyKey?: string;\n};\n\nexport interface LegacyOpenAIInput {\n    /** @deprecated Use baseURL instead */\n    basePath?: string;\n    /** @deprecated Use defaultHeaders and defaultQuery instead */\n    baseOptions?: {\n        headers?: Record<string, string>;\n        params?: Record<string, string>;\n    };\n}\n"
  },
  {
    "path": "src/models/utils/chrome.ts",
    "content": "export const checkChromeAIAvailability = async (): Promise<\n  | \"readily\"\n  | \"unavailable\"\n  | \"downloadable\"\n  | \"downloading\"\n  | \"no\"\n  | \"after-download\"\n> => {\n  try {\n    // latest latest newer version\n    if (typeof (globalThis as any).LanguageModel !== \"undefined\") {\n      const availability = await (\n        globalThis as any\n      ).LanguageModel.availability()\n      console.log(\"LanguageModel availability:\", availability) \n      if (availability === \"downloadable\") {\n        return \"downloadable\"\n      }\n      if (availability === \"downloading\") {\n        return \"downloading\"\n      }\n      return availability == \"available\" ? \"readily\" : \"no\"\n    }\n    const ai = (window as any).ai\n\n    // latest i guess\n    if (ai?.languageModel?.capabilities) {\n      const capabilities = await ai.languageModel.capabilities()\n      return capabilities?.available ?? \"no\"\n    }\n\n    // old version change\n    if (ai?.assistant?.capabilities) {\n      const capabilities = await ai.assistant.capabilities()\n      return capabilities?.available ?? \"no\"\n    }\n\n    // too old version\n    if (ai?.canCreateTextSession) {\n      const available = await ai.canCreateTextSession()\n      return available ?? \"no\"\n    }\n\n    return \"no\"\n  } catch (e) {\n    console.error(\"Error checking Chrome AI availability:\", e)\n    return \"no\"\n  }\n}\n\nexport interface AITextSession {\n  prompt(input: string): Promise<string>\n  promptStreaming(input: string): ReadableStream\n  destroy(): void\n  clone(): AITextSession\n}\n\nexport const createAITextSession = async (\n  data: any\n): Promise<AITextSession> => {\n  // even newer version\n  if (typeof (globalThis as any).LanguageModel !== \"undefined\") {\n    const session = await (globalThis as any).LanguageModel.create({\n      ...data\n    })\n    return session\n  }\n  const ai = (window as any).ai\n\n  // new version i guess\n  if (ai?.languageModel?.create) {\n    const session = await ai.languageModel.create({\n      ...data\n    })\n    return session\n  }\n\n  // old version change\n  if (ai?.assistant?.create) {\n    const session = await ai.assistant.create({\n      ...data\n    })\n    return session\n  }\n\n  // too old version\n  if (ai.createTextSession) {\n    const session = await ai.createTextSession({\n      ...data\n    })\n\n    return session\n  }\n\n  throw new Error(\"Chrome AI is not available.\")\n}\n"
  },
  {
    "path": "src/models/utils/ollama.ts",
    "content": "import { IterableReadableStream } from \"@langchain/core/utils/stream\"\nimport type { StringWithAutocomplete } from \"@langchain/core/utils/types\"\nimport { BaseLanguageModelCallOptions } from \"@langchain/core/language_models/base\"\nimport { getCustomOllamaHeaders } from \"@/services/app\"\n\nexport interface OllamaInput {\n  embeddingOnly?: boolean\n  f16KV?: boolean\n  frequencyPenalty?: number\n  headers?: Record<string, string>\n  keepAlive?: any\n  logitsAll?: boolean\n  lowVram?: boolean\n  mainGpu?: number\n  model?: string\n  baseUrl?: string\n  mirostat?: number\n  mirostatEta?: number\n  mirostatTau?: number\n  numBatch?: number\n  numCtx?: number\n  numGpu?: number\n  numGqa?: number\n  numKeep?: number\n  numPredict?: number\n  numThread?: number\n  penalizeNewline?: boolean\n  presencePenalty?: number\n  repeatLastN?: number\n  repeatPenalty?: number\n  ropeFrequencyBase?: number\n  ropeFrequencyScale?: number\n  temperature?: number\n  stop?: string[]\n  tfsZ?: number\n  topK?: number\n  topP?: number\n  minP?: number\n  typicalP?: number\n  useMLock?: boolean\n  useMMap?: boolean\n  vocabOnly?: boolean\n  useMlock?: boolean\n  seed?: number\n  thinking?: boolean | \"low\" | \"medium\" | \"high\"\n  format?: StringWithAutocomplete<\"json\">\n}\n\nexport interface OllamaRequestParams {\n  model: string\n  format?: StringWithAutocomplete<\"json\">\n  images?: string[]\n  keep_alive?: any\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  think?: boolean | \"low\" | \"medium\" | \"high\"\n  options: {\n    embedding_only?: boolean\n    f16_kv?: boolean\n    frequency_penalty?: number\n    logits_all?: boolean\n    low_vram?: boolean\n    main_gpu?: number\n    mirostat?: number\n    mirostat_eta?: number\n    mirostat_tau?: number\n    num_batch?: number\n    num_ctx?: number\n    num_gpu?: number\n    num_gqa?: number\n    num_keep?: number\n    num_thread?: number\n    num_predict?: number\n    penalize_newline?: boolean\n    presence_penalty?: number\n    repeat_last_n?: number\n    repeat_penalty?: number\n    rope_frequency_base?: number\n    rope_frequency_scale?: number\n    temperature?: number\n    stop?: string[]\n    tfs_z?: number\n    top_k?: number\n    top_p?: number\n    typical_p?: number\n    use_mlock?: boolean\n    use_mmap?: boolean\n    vocab_only?: boolean\n  }\n}\n\nexport interface OllamaToolFunction {\n  name: string\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  arguments: Record<string, any>\n}\n\nexport interface OllamaToolCall {\n  function: OllamaToolFunction\n}\n\nexport type OllamaMessage = {\n  role: StringWithAutocomplete<\"user\" | \"assistant\" | \"system\" | \"tool\">\n  content: string\n  thinking?: string\n  images?: string[]\n  tool_calls?: OllamaToolCall[]\n}\n\nexport interface OllamaGenerateRequestParams extends OllamaRequestParams {\n  prompt: string\n}\n\nexport interface OllamaChatRequestParams extends OllamaRequestParams {\n  messages: OllamaMessage[]\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  tools?: any[]\n}\n\nexport type BaseOllamaGenerationChunk = {\n  model: string\n  created_at: string\n  done: boolean\n  total_duration?: number\n  load_duration?: number\n  prompt_eval_count?: number\n  prompt_eval_duration?: number\n  eval_count?: number\n  eval_duration?: number\n}\n\nexport type OllamaGenerationChunk = BaseOllamaGenerationChunk & {\n  response: string\n}\n\nexport type OllamaChatGenerationChunk = BaseOllamaGenerationChunk & {\n  message: OllamaMessage\n}\n\nexport type OllamaCallOptions = BaseLanguageModelCallOptions & {\n  headers?: Record<string, string>\n}\n\nasync function* createOllamaStream(\n  url: string,\n  params: OllamaRequestParams,\n  options: OllamaCallOptions\n) {\n  let formattedUrl = url\n  if (formattedUrl.startsWith(\"http://localhost:\")) {\n    // Node 18 has issues with resolving \"localhost\"\n    // See https://github.com/node-fetch/node-fetch/issues/1624\n    formattedUrl = formattedUrl.replace(\n      \"http://localhost:\",\n      \"http://127.0.0.1:\"\n    )\n  }\n\n  const customHeaders = await getCustomOllamaHeaders()\n\n  const response = await fetch(formattedUrl, {\n    method: \"POST\",\n    body: JSON.stringify(params),\n    headers: {\n      \"Content-Type\": \"application/json\",\n      ...options.headers,\n      ...customHeaders\n    },\n    signal: options.signal\n  })\n  if (!response.ok) {\n    let error\n    const responseText = await response.text()\n    try {\n      const json = JSON.parse(responseText)\n      error = new Error(\n        `Ollama call failed with status code ${response.status}: ${json.error}`\n      )\n      // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    } catch (e: any) {\n      error = new Error(\n        `Ollama call failed with status code ${response.status}: ${responseText}`\n      )\n    }\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    ;(error as any).response = response\n    throw error\n  }\n  if (!response.body) {\n    throw new Error(\n      \"Could not begin Ollama stream. Please check the given URL and try again.\"\n    )\n  }\n\n  const stream = IterableReadableStream.fromReadableStream(response.body)\n\n  const decoder = new TextDecoder()\n  let extra = \"\"\n  for await (const chunk of stream) {\n    const decoded = extra + decoder.decode(chunk)\n    const lines = decoded.split(\"\\n\")\n    extra = lines.pop() || \"\"\n    for (const line of lines) {\n      try {\n        yield JSON.parse(line)\n      } catch (e) {\n        console.warn(`Received a non-JSON parseable chunk: ${line}`)\n      }\n    }\n  }\n}\n\nexport async function* createOllamaGenerateStream(\n  baseUrl: string,\n  params: OllamaGenerateRequestParams,\n  options: OllamaCallOptions\n): AsyncGenerator<OllamaGenerationChunk> {\n  yield* createOllamaStream(`${baseUrl}/api/generate`, params, options)\n}\n\nexport async function* createOllamaChatStream(\n  baseUrl: string,\n  params: OllamaChatRequestParams,\n  options: OllamaCallOptions\n): AsyncGenerator<OllamaChatGenerationChunk> {\n  yield* createOllamaStream(`${baseUrl}/api/chat`, params, options)\n}\n\nexport const parseKeepAlive = (keepAlive: any) => {\n  if (keepAlive === \"-1\") {\n    return -1\n  }\n  return keepAlive\n}\n"
  },
  {
    "path": "src/models/utils/openai.ts",
    "content": "import {\n    APIConnectionTimeoutError,\n    APIUserAbortError,\n    OpenAI as OpenAIClient,\n  } from \"openai\";\n  import { zodToJsonSchema } from \"zod-to-json-schema\";\n  import type { StructuredToolInterface } from \"@langchain/core/tools\";\n  import {\n    convertToOpenAIFunction,\n    convertToOpenAITool,\n  } from \"@langchain/core/utils/function_calling\";\n  \n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  export function wrapOpenAIClientError(e: any) {\n    let error;\n    if (e.constructor.name === APIConnectionTimeoutError.name) {\n      error = new Error(e.message);\n      error.name = \"TimeoutError\";\n    } else if (e.constructor.name === APIUserAbortError.name) {\n      error = new Error(e.message);\n      error.name = \"AbortError\";\n    } else {\n      error = e;\n    }\n    return error;\n  }\n  \n  export {\n    convertToOpenAIFunction as formatToOpenAIFunction,\n    convertToOpenAITool as formatToOpenAITool,\n  };\n  \n  export function formatToOpenAIAssistantTool(tool: StructuredToolInterface) {\n    return {\n      type: \"function\",\n      function: {\n        name: tool.name,\n        description: tool.description,\n        parameters: zodToJsonSchema(tool.schema as any),\n      },\n    };\n  }\n  \n  export type OpenAIToolChoice =\n    | OpenAIClient.ChatCompletionToolChoiceOption\n    | \"any\"\n    | string;\n  \n  export function formatToOpenAIToolChoice(\n    toolChoice?: OpenAIToolChoice\n  ): OpenAIClient.ChatCompletionToolChoiceOption | undefined {\n    if (!toolChoice) {\n      return undefined;\n    } else if (toolChoice === \"any\" || toolChoice === \"required\") {\n      return \"required\";\n    } else if (toolChoice === \"auto\") {\n      return \"auto\";\n    } else if (toolChoice === \"none\") {\n      return \"none\";\n    } else if (typeof toolChoice === \"string\") {\n      return {\n        type: \"function\",\n        function: {\n          name: toolChoice,\n        },\n      };\n    } else {\n      return toolChoice;\n    }\n  }"
  },
  {
    "path": "src/parser/amazon.ts",
    "content": "import TurndownService from \"turndown\"\nimport * as cheerio from \"cheerio\"\n\nexport const isAmazonURL = (url: string) => {\n  const AMAZON_REGEX = /amazon\\.[a-z]{2,}/gi\n  return AMAZON_REGEX.test(url)\n}\n\nexport const parseAmazonWebsite = (html: string) => {\n  if (!html) {\n    return \"\"\n  }\n\n  const $ = cheerio.load(html)\n\n  // Remove unnecessary elements\n  $(\"script, style, link, svg, nav, footer, header, [src^='data:image/']\").remove()\n  $(\".a-popover, .a-declarative, .a-offscreen\").remove()\n  $(\"#navbar, #navFooter, #rhf\").remove()\n\n  // Amazon product-specific selectors\n  const productSelectors = [\n    // Product title\n    \"#productTitle\",\n    \"h1.a-size-large\",\n    \n    // Price information\n    \".a-price\",\n    \".a-price-whole\",\n    \".a-price-fraction\",\n    \".a-price-symbol\",\n    \"#priceblock_dealprice\",\n    \"#priceblock_ourprice\",\n    \".a-price-current\",\n    \n    // Product details\n    \"#feature-bullets\",\n    \"#productDescription\",\n    \"#aplus\",\n    \".a-unordered-list.a-nostyle.a-vertical.feature\",\n    \n    // Product specifications\n    \"#productDetails_techSpec_section_1\",\n    \"#productDetails_detailBullets_sections1\",\n    \".a-keyvalue\",\n    \n    // Reviews summary\n    \"#averageCustomerReviews\",\n    \".a-icon-alt\",\n    \"#acrPopover\",\n    \n    // Availability\n    \"#availability\",\n    \".a-color-success\",\n    \".a-color-price\",\n    \n    // Product images (keep alt text)\n    \"#landingImage\",\n    \".a-dynamic-image\",\n    \n    // Brand and manufacturer\n    \"#bylineInfo\",\n    \".a-brand\",\n    \n    // Product variations (size, color, etc.)\n    \"#variation_size_name\",\n    \"#variation_color_name\",\n    \".a-button-text\",\n    \n    // Key product features\n    \".a-spacing-mini\",\n    \".a-list-item\"\n  ]\n\n  // Extract only product-related content\n  let productContent = \"\"\n  \n  productSelectors.forEach(selector => {\n    const elements = $(selector)\n    elements.each((_, element) => {\n      const $element = $(element)\n      // Clean up attributes but keep essential ones\n      $element.find(\"*\").each((_, child) => {\n        if (\"attribs\" in child) {\n          const attributes = child.attribs\n          for (const attr in attributes) {\n            if (![\"href\", \"src\", \"alt\"].includes(attr)) {\n              $(child).removeAttr(attr)\n            }\n          }\n        }\n      })\n      productContent += $element.html() || \"\"\n    })\n  })\n\n  // If no specific product content found, fallback to main content but filter it\n  if (!productContent.trim()) {\n    const mainContent = $('[role=\"main\"]').html() || $(\"main\").html() || $(\"#dp\").html() || \"\"\n    if (mainContent) {\n      const $main = cheerio.load(mainContent)\n      // Remove non-product elements from main content\n      $main(\"nav, footer, .nav-sprite, .a-popover, #navbar\").remove()\n      productContent = $main.html() || \"\"\n    }\n  }\n\n  if (!productContent.trim()) {\n    return \"\"\n  }\n\n  // Clean up attributes in the final content\n  const $final = cheerio.load(productContent)\n  $final(\"*\").each((_, element) => {\n    if (\"attribs\" in element) {\n      const attributes = element.attribs\n      for (const attr in attributes) {\n        if (![\"href\", \"src\", \"alt\"].includes(attr)) {\n          $final(element).removeAttr(attr)\n        }\n      }\n    }\n  })\n\n  const turndownService = new TurndownService({\n    headingStyle: \"atx\",\n    codeBlockStyle: \"fenced\"\n  })\n\n  // Configure turndown to handle product-specific elements better\n  turndownService.addRule('productPrice', {\n    filter: function (node) {\n      return node.className && node.className.includes('a-price')\n    },\n    replacement: function (content) {\n      return `**Price: ${content.trim()}**\\n\\n`\n    }\n  })\n\n  turndownService.addRule('productTitle', {\n    filter: function (node) {\n      return node.id === 'productTitle' || (node.tagName === 'H1' && node.className && node.className.includes('a-size-large'))\n    },\n    replacement: function (content) {\n      return `# ${content.trim()}\\n\\n`\n    }\n  })\n\n  const markdown = turndownService.turndown($final.html() || \"\")\n\n  return markdown.trim()\n}\n"
  },
  {
    "path": "src/parser/default.ts",
    "content": "import * as cheerio from \"cheerio\"\nimport TurndownService from \"turndown\"\nimport { Readability, isProbablyReaderable } from \"@mozilla/readability\"\n\nexport const defaultExtractContent = (html: string) => {\n  const doc = new DOMParser().parseFromString(html, \"text/html\")\n  if (isProbablyReaderable(doc)) {\n    const reader = new Readability(doc)\n    const article = reader.parse()\n    if (article && article.content) {\n      const $article = cheerio.load(article.content)\n      $article(\"script, style, link, svg, [src^='data:image/']\").remove()\n      article.content = $article.html() || \"\"\n    }\n    const turndownService = new TurndownService({\n      headingStyle: \"atx\",\n      codeBlockStyle: \"fenced\"\n    })\n    return turndownService.turndown(article?.content || \"\").trim()\n  }\n\n  const $ = cheerio.load(html)\n\n  $(\"script, style, link, svg, [src^='data:image/']\").remove()\n\n  $(\"*\").each((_, element) => {\n    if (\"attribs\" in element) {\n      const attributes = element.attribs\n      for (const attr in attributes) {\n        if (attr !== \"href\" && attr !== \"src\") {\n          $(element).removeAttr(attr)\n        }\n      }\n    }\n  })\n\n  const mainContent =\n    $('[role=\"main\"]').html() || $(\"main\").html() || $(\"body\").html() || \"\"\n\n  const turndownService = new TurndownService({\n    headingStyle: \"atx\",\n    codeBlockStyle: \"fenced\"\n  })\n  const markdown = turndownService.turndown(mainContent)\n\n  return markdown.trim()\n}\n"
  },
  {
    "path": "src/parser/google-docs.ts",
    "content": "export const isGoogleDocs = (url: string) => {\n  const GOOGLE_DOCS_REGEX = /docs\\.google\\.com\\/document/g\n  return GOOGLE_DOCS_REGEX.test(url)\n}\n\nconst getGoogleDocs = () => {\n  try {\n    function traverse(\n      obj: { [x: string]: any },\n      predicate: { (_: any, value: any): boolean; (arg0: any, arg1: any): any },\n      maxDepth: number,\n      propNames = Object.getOwnPropertyNames(obj)\n    ) {\n      const visited = new Set()\n      const results = []\n      let iterations = 0\n\n      const traverseObj = (\n        name: string,\n        value: unknown,\n        path: any[],\n        depth = 0\n      ) => {\n        iterations++\n        if (name === \"prototype\" || value instanceof Window || depth > maxDepth)\n          return\n\n        const currentPath = [...path, name]\n\n        try {\n          if (predicate(name, value)) {\n            results.push({ path: currentPath, value })\n            return\n          }\n        } catch (error) {}\n\n        if (value != null && !visited.has(value)) {\n          visited.add(value)\n          if (Array.isArray(value)) {\n            value.forEach((val, index) => {\n              try {\n                traverseObj(index.toString(), val, currentPath, depth + 1)\n              } catch (error) {}\n            })\n          } else if (value instanceof Object) {\n            const propNamesForValue =\n              value &&\n              // @ts-ignore\n              value.nodeType === 1 &&\n              // @ts-ignore\n              typeof value.nodeName === \"string\"\n                ? Object.getOwnPropertyNames(obj)\n                : Object.getOwnPropertyNames(value)\n\n            propNamesForValue.forEach((prop) => {\n              try {\n                traverseObj(prop, value[prop], currentPath, depth + 1)\n              } catch (error) {}\n            })\n          }\n        }\n      }\n\n      propNames.forEach((prop) => {\n        try {\n          traverseObj(prop, obj[prop], [])\n        } catch (error) {}\n      })\n\n      return { results, iterations }\n    }\n\n    const result = traverse(\n      // @ts-ignore\n      window.KX_kixApp,\n      (_: any, value: { toString: () => string }) =>\n        value && \"\\x03\" === value.toString().charAt(0),\n      5\n    )\n    if (result.results?.[0]?.value) {\n      return {\n        content: result.results[0].value\n      }\n    }\n\n    return {\n      content: null\n    }\n  } catch (error) {\n    return {\n      content: null\n    }\n  }\n}\n\nexport const parseGoogleDocs = async () => {\n  const result = new Promise((resolve) => {\n    if (import.meta.env.BROWSER === \"chrome\" || import.meta.env.BROWSER === \"edge\") {\n      chrome.tabs.query({ active: true, currentWindow: true }, async (tabs) => {\n        const tab = tabs[0]\n\n        const data = await chrome.scripting.executeScript({\n          target: { tabId: tab.id },\n          world: \"MAIN\",\n          func: getGoogleDocs\n        })\n\n        if (data.length > 0) {\n          resolve(data[0].result)\n        }\n      })\n    } else {\n      browser.tabs\n        .query({ active: true, currentWindow: true })\n        .then(async (tabs) => {\n          const tab = tabs[0]\n\n          const data = await browser.scripting.executeScript({\n            target: { tabId: tab.id },\n            func: getGoogleDocs\n          })\n\n          if (data.length > 0) {\n            resolve(data[0].result)\n          }\n        })\n    }\n  }) as Promise<{\n    content?: string\n  }>\n\n  const { content } = await result\n\n  return content\n}\n"
  },
  {
    "path": "src/parser/google-sheets.ts",
    "content": "import * as cheerio from 'cheerio';\n\nexport const parseGoogleSheets = (html: string) => {\n  const $ = cheerio.load(html);\n};"
  },
  {
    "path": "src/parser/reader.ts",
    "content": "import { Readability } from \"@mozilla/readability\"\nimport { defaultExtractContent } from \"./default\"\nexport const extractReadabilityContent = async (url: string) => {\n  const response = await fetch(url, {\n    headers: {\n      \"User-Agent\":\n        \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36\",\n      Accept:\n        \"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8\",\n      \"Accept-Language\": \"en-US,en;q=0.9\",\n      \"Accept-Encoding\": \"gzip, deflate, br\",\n      Connection: \"keep-alive\",\n      \"Upgrade-Insecure-Requests\": \"1\",\n      \"Sec-Fetch-Dest\": \"document\",\n      \"Sec-Fetch-Mode\": \"navigate\",\n      \"Sec-Fetch-Site\": \"none\",\n      \"Sec-Fetch-User\": \"?1\",\n      \"Cache-Control\": \"max-age=0\"\n    }\n  })\n  if (!response.ok) {\n    throw new Error(`Failed to fetch ${url}`)\n  }\n\n  const html = await response.text()\n\n  const doc = new DOMParser().parseFromString(html, \"text/html\")\n  const reader = new Readability(doc)\n  const article = reader.parse()\n\n  const markdown = defaultExtractContent(article.content)\n  return markdown\n}\n"
  },
  {
    "path": "src/parser/twitter.ts",
    "content": "import * as cheerio from \"cheerio\"\n\nexport const isTweet = (url: string) => {\n  const TWEET_REGEX = /twitter\\.com\\/[a-zA-Z0-9_]+\\/status\\/[0-9]+/g\n  const X_REGEX = /x\\.com\\/[a-zA-Z0-9_]+\\/status\\/[0-9]+/g\n  return TWEET_REGEX.test(url) || X_REGEX.test(url)\n}\n\nexport const isTwitterTimeline = (url: string) => {\n  return url === \"https://twitter.com/home\" || url === \"https://x.com/home\"\n}\n\nexport const isTwitterProfile = (url: string) => {\n  const PROFILE_REGEX = /x\\.com\\/[a-zA-Z0-9_]+/g\n  const X_REGEX = /x\\.com\\/[a-zA-Z0-9_]+/g\n  return PROFILE_REGEX.test(url) || X_REGEX.test(url)\n}\n\nexport const parseTwitterTimeline = (html: string) => {\n  const $ = cheerio.load(html)\n  const postElements = $(\"[data-testid=tweetText]\")\n  const authorElements = $(\"[data-testid=User-Name]\")\n\n  const posts = postElements\n    .map((index, element) => {\n      const post = $(element).text()\n      const author = $(authorElements[index]).text()\n      return {\n        author,\n        post\n      }\n    })\n    .get()\n\n  return posts\n    .map((post) => {\n      return `## Author: ${post.author}\\n\\n${post.post}\\n\\n---\\n\\n`\n    })\n    .filter((value, index, self) => self.indexOf(value) === index)\n    .join(\"\\n\")\n}\n\nexport const parseTweet = (html: string) => {\n  const $ = cheerio.load(html)\n  const postElements = $(\"[data-testid=tweetText]\")\n  const authorElements = $(\"[data-testid=User-Name]\")\n\n  const posts = postElements\n    .map((index, element) => {\n      const post = $(element).text()\n      const author = $(authorElements[index]).text()\n      return {\n        author,\n        post,\n        isReply: index !== 0\n      }\n    })\n    .get()\n\n  return posts\n    .map((post) => {\n      return `##Author: ${post.author}\\n\\n${post.isReply ? \"Reply:\" : \"Post:\"} ${post.post}\\n\\n---\\n\\n`\n    })\n    .join(\"\\n\")\n}\n\nexport const parseTweetProfile = (html: string) => {\n  const $ = cheerio.load(html)\n\n  const profileName = $(\"[data-testid=UserProfileHeader_Items]\")\n    .find(\"h1\")\n    .text()\n  const profileBio = $(\"[data-testid=UserProfileHeader_Items]\").find(\"p\").text()\n  const profileLocation = $(\"[data-testid=UserProfileHeader_Items]\")\n    .find(\"span\")\n    .text()\n  const profileJoinDate = $(\"[data-testid=UserProfileHeader_Items]\")\n    .find(\"span\")\n    .text()\n  const profileFollowers = $(\n    \"[data-testid=UserProfileHeader_Items] span\"\n  ).text()\n  const profileFollowing = $(\n    \"[data-testid=UserProfileHeader_Items] span\"\n  ).text()\n\n  const postElements = $(\"[data-testid=tweetText]\")\n  const authorElements = $(\"[data-testid=User-Name]\")\n\n  const posts = postElements\n    .map((index, element) => {\n      const post = $(element).text()\n      const author = $(authorElements[index]).text()\n      return {\n        author,\n        post\n      }\n    })\n    .get()\n\n  return `## Profile: ${profileName}\\n\\nBio: ${profileBio}\\n\\nLocation: ${profileLocation}\\n\\nJoin Date: ${profileJoinDate}\\n\\nFollowers: ${profileFollowers}\\n\\nFollowing: ${profileFollowing}\\n\\nPosts: ${posts.map((post) => `Author: ${post.author}\\n\\nPost: ${post.post}\\n\\n---\\n\\n`).join(\"\\n\")}`\n}\n"
  },
  {
    "path": "src/parser/wiki.ts",
    "content": "import * as cheerio from \"cheerio\"\nimport { defaultExtractContent } from \"./default\"\n\nexport const isWikipedia = (url: string) => {\n  const WIKI_REGEX = /wikipedia\\.org\\/wiki\\//g\n  return WIKI_REGEX.test(url)\n}\n\nexport const parseWikipedia = (html: string) => {\n  if (!html) {\n    return \"\"\n  }\n  const $ = cheerio.load(html)\n  const title = $(\"h1#firstHeading\")\n  const content = $(\"#mw-content-text\")\n  content?.find(\"sup.reference\")?.remove()\n  content?.find(\"div.thumb\")?.remove()\n  content?.find(\"div.reflist\")?.remove()\n  content?.find(\"div.navbox\")?.remove()\n  content?.find(\"table.infobox\")?.remove()\n  content?.find(\"div.sister-wikipedia\")?.remove()\n  content?.find(\"div.sister-projects\")?.remove()\n  content?.find(\"div.metadata\")?.remove()\n  content?.find(\"div.vertical-navbox\")?.remove()\n  content?.find(\"div.toc\")?.remove()\n  const newHtml = content?.html()\n\n  return defaultExtractContent(`<div>TITLE: ${title?.text()}</div><div>${newHtml}</div>`)\n}\n"
  },
  {
    "path": "src/public/_locales/da/messages.json",
    "content": "{\n    \"extName\": {\n        \"message\": \"Page Assist - En Web UI for at køre AI modeller lokalt\"\n    },\n    \"extDescription\": {\n        \"message\": \"Brug dine lokalt kørende AI-modeller til at hjælpe dig med at surfe på nettet.\"\n    },\n    \"openSidePanelToChat\": {\n        \"message\": \"Åben Copilot for at Chatte\"\n    },\n    \"openOptionToChat\": {\n        \"message\": \"Åben Web UI for at Chatte\"\n    },\n    \"contextSummarize\": {\n        \"message\": \"Opsummer\"\n    },\n    \"contextExplain\": {\n        \"message\": \"Forklar\"\n    },\n    \"contextRephrase\": {\n        \"message\": \"Omskriv\"\n    },\n    \"contextTranslate\": {\n        \"message\": \"Oversæt\"\n    },\n    \"contextCustom\": {\n        \"message\": \"Tilpasset\"\n    }\n}\n"
  },
  {
    "path": "src/public/_locales/de/messages.json",
    "content": "{\n    \"extName\": {\n        \"message\": \"Page Assist - Eine Web-Benutzeroberfläche für lokale KI-Modelle\"\n    },\n    \"extDescription\": {\n        \"message\": \"Nutzen Sie Ihre lokal laufenden KI-Modelle, um Sie beim Surfen im Web zu unterstützen.\"\n    },\n    \"openSidePanelToChat\": {\n        \"message\": \"Seitenleiste zum Chatten öffnen\"\n    },\n    \"openOptionToChat\": {\n        \"message\": \"Web-Benutzeroberfläche zum Chatten öffnen\"\n    },\n    \"contextSummarize\": {\n        \"message\": \"Zusammenfassen\"\n    },\n    \"contextExplain\": {\n        \"message\": \"Erklären\"\n    },\n    \"contextRephrase\": {\n        \"message\": \"Umformulieren\"\n    },\n    \"contextTranslate\": {\n        \"message\": \"Übersetzen\"\n    },\n    \"contextCustom\": {\n        \"message\": \"Benutzerdefiniert\"\n    }\n}"
  },
  {
    "path": "src/public/_locales/en/messages.json",
    "content": "{\n    \"extName\": {\n        \"message\": \"Page Assist - A Web UI for Local AI Models\"\n    },\n    \"extDescription\": {\n        \"message\": \"Use your locally running AI models to assist you in your web browsing.\"\n    },\n    \"openSidePanelToChat\": {\n        \"message\": \"Open Side Panel to Chat\"\n    },\n    \"openOptionToChat\": {\n        \"message\": \"Open Web UI to Chat\"\n    },\n    \"contextSummarize\": {\n        \"message\": \"Summarize\"\n    },\n    \"contextExplain\": {\n        \"message\": \"Explain\"\n    },\n    \"contextRephrase\": {\n        \"message\": \"Rephrase\"\n    },\n    \"contextTranslate\": {\n        \"message\": \"Translate\"\n    },\n    \"contextCustom\": {\n        \"message\": \"Custom\"\n    }\n}\n"
  },
  {
    "path": "src/public/_locales/es/messages.json",
    "content": "{\n    \"extName\": {\n        \"message\": \"Page Assist - Una Web UI para modelos de IA Locales\"\n    },\n    \"extDescription\": {\n        \"message\": \"Usa tu modelo de IA corriendo localmente para asistirte en la navegación.\"\n    },\n    \"openSidePanelToChat\": {\n        \"message\": \"Abrir Panel Lateral para Chatear\"\n    },\n    \"openOptionToChat\": {\n        \"message\": \"Abrir Web UI para Chatear\"\n    },\n    \"contextSummarize\": {\n        \"message\": \"Resumir\"\n    },\n    \"contextExplain\": {\n        \"message\": \"Explicar\"\n    },\n    \"contextRephrase\": {\n        \"message\": \"Reformular\"\n    },\n    \"contextTranslate\": {\n        \"message\": \"Traducir\"\n    },\n    \"contextCustom\": {\n        \"message\": \"Personalizado\"\n    }\n}"
  },
  {
    "path": "src/public/_locales/fa/messages.json",
    "content": "{\n    \"extName\": {\n        \"message\": \"دستیار صفحه - یک رابط کاربری وب برای مدل های هوش مصنوعی لوکال\"\n    },\n    \"extDescription\": {\n        \"message\": \"از مدل های هوش مصنوعی لوکال خود برای دریافت کمک در هنگام مرور وب استفاده کنید.\"\n    },\n    \"openSidePanelToChat\": {\n        \"message\": \"باز کردن پنل کناری برای گفتگو\"\n    },\n    \"openOptionToChat\": {\n        \"message\": \"باز کردن رابط کاربری وب برای گفتگو\"\n    },\n    \"contextSummarize\": {\n        \"message\": \"خلاصه کردن\"\n    },\n    \"contextExplain\": {\n        \"message\": \"توضیح دادن\"\n    },\n    \"contextRephrase\": {\n        \"message\": \"بازنویسی\"\n    },\n    \"contextTranslate\": {\n        \"message\": \"ترجمه کردن\"\n    },\n    \"contextCustom\": {\n        \"message\": \"سفارشی\"\n    }\n}"
  },
  {
    "path": "src/public/_locales/fr/messages.json",
    "content": "{\n    \"extName\": {\n        \"message\": \"Page Assist - Une interface Web pour les modèles d'IA locaux\"\n    },\n    \"extDescription\": {\n        \"message\": \"Utilisez vos modèles d'IA locaux pour vous aider dans votre navigation web.\"\n    },\n    \"openSidePanelToChat\": {\n        \"message\": \"Ouvrir le panneau latéral pour discuter\"\n    },\n    \"contextSummarize\": {\n        \"message\": \"Résumer\"\n    },\n    \"contextExplain\": {\n        \"message\": \"Expliquer\"\n    },\n    \"contextRephrase\": {\n        \"message\": \"Reformuler\"\n    },\n    \"contextTranslate\": {\n        \"message\": \"Traduire\"\n    },\n    \"contextCustom\": {\n        \"message\": \"Personnalisé\"\n    }\n}"
  },
  {
    "path": "src/public/_locales/it/messages.json",
    "content": "{\n    \"extName\": {\n        \"message\": \"Page Assist - Un'interfaccia web per modelli AI locali\"\n    },\n    \"extDescription\": {\n        \"message\": \"Usa i tuoi modelli AI in esecuzione locale per assisterti nella navigazione web.\"\n    },\n    \"openSidePanelToChat\": {\n        \"message\": \"Apri il pannello laterale per chattare\"\n    },\n    \"contextSummarize\": {\n        \"message\": \"Riassumi\"\n    },\n    \"contextExplain\": {\n        \"message\": \"Spiega\"\n    },\n    \"contextRephrase\": {\n        \"message\": \"Riformula\"\n    },\n    \"contextTranslate\": {\n        \"message\": \"Traduci\"\n    },\n    \"contextCustom\": {\n        \"message\": \"Personalizzato\"\n    }\n}"
  },
  {
    "path": "src/public/_locales/ja/messages.json",
    "content": "{\n  \"extName\": {\n    \"message\": \"Page Assist - ローカルAIモデル用のWeb UI\"\n  },\n  \"extDescription\": {\n    \"message\": \"ローカルで実行中のAIモデルを使って、Webブラウジングをアシストします。\"\n  },\n  \"openSidePanelToChat\": {\n    \"message\": \"サイドパネルを開いてチャットする\"\n  },\n  \"contextSummarize\": {\n    \"message\": \"要約\"\n  },\n  \"contextExplain\": {\n    \"message\": \"説明\"\n  },\n  \"contextRephrase\": {\n    \"message\": \"言い換え\"\n  },\n  \"contextTranslate\": {\n    \"message\": \"翻訳\"\n  },\n  \"contextCustom\": {\n    \"message\": \"カスタム\"\n  }\n}"
  },
  {
    "path": "src/public/_locales/ml/messages.json",
    "content": "{\n    \"extName\": {\n        \"message\": \"പേജ് അസിസ്റ്റ് - ഒള്ളമ മോഡലുകളിക്ക് ഒരു വെബ്യൂഐ\"\n    },\n    \"extDescription\": {\n        \"message\": \"ഇന്റർനെറ്റ് ബ്രൌസ് ചെയ്യുമ്പോൾ സ്ഥലിപ്പായി പ്രവർത്തിക്കുന്ന എയ്‌ മോഡൽ ഉപയോഗിക്കുക.\"\n    },\n    \"openSidePanelToChat\": {\n        \"message\": \"ചാറ്റ് ചെയ്യാന്‍ സൈഡ് പാനല്‍ തുറക്കുക\"\n    },\n    \"contextSummarize\": {\n        \"message\": \"സംഗ്രഹിക്കുക\"\n    },\n    \"contextExplain\": {\n        \"message\": \"വിശദീകരിക്കുക\"\n    },\n    \"contextRephrase\": {\n        \"message\": \"പുനഃരൂപീകരിക്കുക\"\n    },\n    \"contextTranslate\": {\n        \"message\": \"വിവർത്തനം ചെയ്യുക\"\n    },\n    \"contextCustom\": {\n        \"message\": \"ഇഷ്ടാനുസൃതം\"\n    }\n}\n"
  },
  {
    "path": "src/public/_locales/no/messages.json",
    "content": "{\n    \"extName\": {\n        \"message\": \"Page Assist - Et Web UI for at kjøre AI-modeller lokalt\"\n    },\n    \"extDescription\": {\n        \"message\": \"Bruk dine lokalt kjørende AI-modeller til at hjælpe dig med at surfe på nettet.\"\n    },\n    \"openSidePanelToChat\": {\n        \"message\": \"Åpne sidepanel for å chatte\"\n    },\n    \"openOptionToChat\": {\n        \"message\": \"Åben Web UI for at Chatte\"\n    },\n    \"contextSummarize\": {\n        \"message\": \"Oppsummer\"\n    },\n    \"contextExplain\": {\n        \"message\": \"Forklar\"\n    },\n    \"contextRephrase\": {\n        \"message\": \"Omskrive\"\n    },\n    \"contextTranslate\": {\n        \"message\": \"Oversett\"\n    },\n    \"contextCustom\": {\n        \"message\": \"Tilpasset\"\n    }\n}"
  },
  {
    "path": "src/public/_locales/ru/messages.json",
    "content": "{\n    \"extName\": {\n        \"message\": \"Page Assist - Веб-интерфейс для локальных моделей искусственного интеллекта\"\n    },\n    \"extDescription\": {\n        \"message\": \"Используйте запущенные локально модели искусственного интеллекта для помощи в веб-просмотре.\"\n    },\n    \"openSidePanelToChat\": {\n        \"message\": \"Открыть боковую панель для чата\"\n    },\n    \"contextSummarize\": {\n        \"message\": \"Обобщить\"\n    },\n    \"contextExplain\": {\n        \"message\": \"Объяснить\"\n    },\n    \"contextRephrase\": {\n        \"message\": \"Перефразировать\"\n    },\n    \"contextTranslate\": {\n        \"message\": \"Перевести\"\n    },\n    \"contextCustom\": {\n        \"message\": \"Пользовательский\"\n    }\n}"
  },
  {
    "path": "src/public/_locales/sv/messages.json",
    "content": "{\n    \"extName\": {\n        \"message\": \"Page Assist - Ett webbaserat användargränssnitt för lokala AI-modeller\"\n    },\n    \"extDescription\": {\n        \"message\": \"Använd dina lokalt körande AI-modeller för att hjälpa dig i din webbsurfning.\"\n    },\n    \"openSidePanelToChat\": {\n        \"message\": \"Öppna sidopanelen för att chatta\"\n    },\n    \"openOptionToChat\": {\n        \"message\": \"Öppna Web UI för att chatta\"\n    },\n    \"contextSummarize\": {\n        \"message\": \"Sammanfatta\"\n    },\n    \"contextExplain\": {\n        \"message\": \"Förklara\"\n    },\n    \"contextRephrase\": {\n        \"message\": \"Skriv om\"\n    },\n    \"contextTranslate\": {\n        \"message\": \"Översätt\"\n    },\n    \"contextCustom\": {\n        \"message\": \"Custom\"\n    }\n}"
  },
  {
    "path": "src/public/_locales/zh_CN/messages.json",
    "content": "{\n    \"extName\": {\n        \"message\": \"Page Assist - 本地 AI 模型的 Web UI\"\n    },\n    \"extDescription\": {\n        \"message\": \"使用本地运行的 AI 模型来辅助您的网络浏览。\"\n    },\n    \"openSidePanelToChat\": {\n        \"message\": \"打开侧边栏进行对话\"\n    },\n    \"contextSummarize\": {\n        \"message\": \"总结\"\n    },\n    \"contextExplain\": {\n        \"message\": \"解释\"\n    },\n    \"contextRephrase\": {\n        \"message\": \"改述\"\n    },\n    \"contextTranslate\": {\n        \"message\": \"翻译\"\n    },\n    \"contextCustom\": {\n        \"message\": \"自定义\"\n    }\n}"
  },
  {
    "path": "src/public/_locales/zh_TW/messages.json",
    "content": "{\n    \"extName\": {\n        \"message\": \"Page Assist - 本地 AI 模型的網頁介面\"\n    },\n    \"extDescription\": {\n        \"message\": \"使用本地運行的 AI 模型來協助您的網頁瀏覽。\"\n    },\n    \"openSidePanelToChat\": {\n        \"message\": \"開啟側邊面板聊天\"\n    },\n    \"openOptionToChat\": {\n        \"message\": \"開啟網頁介面聊天\"\n    },\n    \"contextSummarize\": {\n        \"message\": \"總結\"\n    },\n    \"contextExplain\": {\n        \"message\": \"解釋\"\n    },\n    \"contextRephrase\": {\n        \"message\": \"重寫\"\n    },\n    \"contextTranslate\": {\n        \"message\": \"翻譯\"\n    },\n    \"contextCustom\": {\n        \"message\": \"自訂\"\n    }\n}\n"
  },
  {
    "path": "src/public/ocr/tesseract-core-simd.js",
    "content": "\nvar TesseractCore = (() => {\n  var _scriptDir = typeof document !== 'undefined' && document.currentScript ? document.currentScript.src : undefined;\n  if (typeof __filename !== 'undefined') _scriptDir = _scriptDir || __filename;\n  return (\nfunction(TesseractCore) {\n  TesseractCore = TesseractCore || {};\n\n\nvar b;b||(b=typeof TesseractCore !== 'undefined' ? TesseractCore : {});var aa,ba;b.ready=new Promise(function(a,c){aa=a;ba=c});var ca=Object.assign({},b),da=\"./this.program\",ea=(a,c)=>{throw c;},fa=\"object\"==typeof window,ha=\"function\"==typeof importScripts,ia=\"object\"==typeof process&&\"object\"==typeof process.versions&&\"string\"==typeof process.versions.node,f=\"\",ja,ka,la,fs,ma,na;\nif(ia)f=ha?require(\"path\").dirname(f)+\"/\":__dirname+\"/\",na=()=>{ma||(fs=require(\"fs\"),ma=require(\"path\"))},ja=function(a,c){na();a=ma.normalize(a);return fs.readFileSync(a,c?void 0:\"utf8\")},la=a=>{a=ja(a,!0);a.buffer||(a=new Uint8Array(a));return a},ka=(a,c,d)=>{na();a=ma.normalize(a);fs.readFile(a,function(e,g){e?d(e):c(g.buffer)})},1<process.argv.length&&(da=process.argv[1].replace(/\\\\/g,\"/\")),process.argv.slice(2),process.on(\"uncaughtException\",function(a){if(!(a instanceof oa))throw a;}),process.on(\"unhandledRejection\",\nfunction(a){throw a;}),ea=(a,c)=>{if(noExitRuntime)throw process.exitCode=a,c;c instanceof oa||pa(\"exiting due to exception: \"+c);process.exit(a)},b.inspect=function(){return\"[Emscripten Module object]\"};else if(fa||ha)ha?f=self.location.href:\"undefined\"!=typeof document&&document.currentScript&&(f=document.currentScript.src),_scriptDir&&(f=_scriptDir),0!==f.indexOf(\"blob:\")?f=f.substr(0,f.replace(/[?#].*/,\"\").lastIndexOf(\"/\")+1):f=\"\",ja=a=>{var c=new XMLHttpRequest;c.open(\"GET\",a,!1);c.send(null);\nreturn c.responseText},ha&&(la=a=>{var c=new XMLHttpRequest;c.open(\"GET\",a,!1);c.responseType=\"arraybuffer\";c.send(null);return new Uint8Array(c.response)}),ka=(a,c,d)=>{var e=new XMLHttpRequest;e.open(\"GET\",a,!0);e.responseType=\"arraybuffer\";e.onload=()=>{200==e.status||0==e.status&&e.response?c(e.response):d()};e.onerror=d;e.send(null)};var qa=b.print||console.log.bind(console),pa=b.printErr||console.warn.bind(console);Object.assign(b,ca);ca=null;b.thisProgram&&(da=b.thisProgram);b.quit&&(ea=b.quit);\nvar ra=0,sa;b.wasmBinary&&(sa=b.wasmBinary);var noExitRuntime=b.noExitRuntime||!0;\"object\"!=typeof WebAssembly&&n(\"no native wasm support detected\");var ta,ua=!1,wa=\"undefined\"!=typeof TextDecoder?new TextDecoder(\"utf8\"):void 0;\nfunction xa(a,c){for(var d=c+NaN,e=c;a[e]&&!(e>=d);)++e;if(16<e-c&&a.buffer&&wa)return wa.decode(a.subarray(c,e));for(d=\"\";c<e;){var g=a[c++];if(g&128){var h=a[c++]&63;if(192==(g&224))d+=String.fromCharCode((g&31)<<6|h);else{var k=a[c++]&63;g=224==(g&240)?(g&15)<<12|h<<6|k:(g&7)<<18|h<<12|k<<6|a[c++]&63;65536>g?d+=String.fromCharCode(g):(g-=65536,d+=String.fromCharCode(55296|g>>10,56320|g&1023))}}else d+=String.fromCharCode(g)}return d}function q(a){return a?xa(ya,a):\"\"}\nfunction za(a,c,d,e){if(!(0<e))return 0;var g=d;e=d+e-1;for(var h=0;h<a.length;++h){var k=a.charCodeAt(h);if(55296<=k&&57343>=k){var m=a.charCodeAt(++h);k=65536+((k&1023)<<10)|m&1023}if(127>=k){if(d>=e)break;c[d++]=k}else{if(2047>=k){if(d+1>=e)break;c[d++]=192|k>>6}else{if(65535>=k){if(d+2>=e)break;c[d++]=224|k>>12}else{if(d+3>=e)break;c[d++]=240|k>>18;c[d++]=128|k>>12&63}c[d++]=128|k>>6&63}c[d++]=128|k&63}}c[d]=0;return d-g}\nfunction Aa(a){for(var c=0,d=0;d<a.length;++d){var e=a.charCodeAt(d);127>=e?c++:2047>=e?c+=2:55296<=e&&57343>=e?(c+=4,++d):c+=3}return c}var Ba,r,ya,Ca,v,y,Da,Ea;function Fa(){var a=ta.buffer;Ba=a;b.HEAP8=r=new Int8Array(a);b.HEAP16=Ca=new Int16Array(a);b.HEAP32=v=new Int32Array(a);b.HEAPU8=ya=new Uint8Array(a);b.HEAPU16=new Uint16Array(a);b.HEAPU32=y=new Uint32Array(a);b.HEAPF32=Da=new Float32Array(a);b.HEAPF64=Ea=new Float64Array(a)}var Ga,Ha=[],Ia=[],Ka=[],La=!1;\nfunction Ma(){var a=b.preRun.shift();Ha.unshift(a)}var Na=0,Oa=null,Pa=null;function Qa(){Na++;b.monitorRunDependencies&&b.monitorRunDependencies(Na)}function Ra(){Na--;b.monitorRunDependencies&&b.monitorRunDependencies(Na);if(0==Na&&(null!==Oa&&(clearInterval(Oa),Oa=null),Pa)){var a=Pa;Pa=null;a()}}function n(a){if(b.onAbort)b.onAbort(a);a=\"Aborted(\"+a+\")\";pa(a);ua=!0;a=new WebAssembly.RuntimeError(a+\". Build with -sASSERTIONS for more info.\");ba(a);throw a;}\nfunction Sa(){return z.startsWith(\"data:application/octet-stream;base64,\")}var z;z=\"tesseract-core-simd.wasm\";if(!Sa()){var Ta=z;z=b.locateFile?b.locateFile(Ta,f):f+Ta}function Ua(){var a=z;try{if(a==z&&sa)return new Uint8Array(sa);if(la)return la(a);throw\"both async and sync fetching of the wasm failed\";}catch(c){n(c)}}\nfunction Va(){if(!sa&&(fa||ha)){if(\"function\"==typeof fetch&&!z.startsWith(\"file://\"))return fetch(z,{credentials:\"same-origin\"}).then(function(a){if(!a.ok)throw\"failed to load wasm binary file at '\"+z+\"'\";return a.arrayBuffer()}).catch(function(){return Ua()});if(ka)return new Promise(function(a,c){ka(z,function(d){a(new Uint8Array(d))},c)})}return Promise.resolve().then(function(){return Ua()})}\nvar A,C,Wa={627260:a=>{b.TesseractProgress&&b.TesseractProgress(a)},627329:a=>{b.TesseractProgress&&b.TesseractProgress(a)},627398:a=>{b.TesseractProgress&&b.TesseractProgress(a)}};function oa(a){this.name=\"ExitStatus\";this.message=\"Program terminated with exit(\"+a+\")\";this.status=a}function Xa(a){for(;0<a.length;)a.shift()(b)}\nfunction Ya(a,c=\"i8\"){c.endsWith(\"*\")&&(c=\"*\");switch(c){case \"i1\":return r[a>>0];case \"i8\":return r[a>>0];case \"i16\":return Ca[a>>1];case \"i32\":return v[a>>2];case \"i64\":return v[a>>2];case \"float\":return Da[a>>2];case \"double\":return Ea[a>>3];case \"*\":return y[a>>2];default:n(\"invalid type for getValue: \"+c)}return null}\nfunction Za(a,c,d=\"i8\"){d.endsWith(\"*\")&&(d=\"*\");switch(d){case \"i1\":r[a>>0]=c;break;case \"i8\":r[a>>0]=c;break;case \"i16\":Ca[a>>1]=c;break;case \"i32\":v[a>>2]=c;break;case \"i64\":C=[c>>>0,(A=c,1<=+Math.abs(A)?0<A?(Math.min(+Math.floor(A/4294967296),4294967295)|0)>>>0:~~+Math.ceil((A-+(~~A>>>0))/4294967296)>>>0:0)];v[a>>2]=C[0];v[a+4>>2]=C[1];break;case \"float\":Da[a>>2]=c;break;case \"double\":Ea[a>>3]=c;break;case \"*\":y[a>>2]=c;break;default:n(\"invalid type for setValue: \"+d)}}\nfunction $a(a){this.If=a-24;this.Ih=function(c){y[this.If+4>>2]=c};this.Eh=function(c){y[this.If+8>>2]=c};this.Fh=function(){v[this.If>>2]=0};this.Jg=function(){r[this.If+12>>0]=0};this.Hh=function(){r[this.If+13>>0]=0};this.rg=function(c,d){this.Vf();this.Ih(c);this.Eh(d);this.Fh();this.Jg();this.Hh()};this.Vf=function(){y[this.If+16>>2]=0}}\nvar ab=0,bb=(a,c)=>{for(var d=0,e=a.length-1;0<=e;e--){var g=a[e];\".\"===g?a.splice(e,1):\"..\"===g?(a.splice(e,1),d++):d&&(a.splice(e,1),d--)}if(c)for(;d;d--)a.unshift(\"..\");return a},cb=a=>{var c=\"/\"===a.charAt(0),d=\"/\"===a.substr(-1);(a=bb(a.split(\"/\").filter(e=>!!e),!c).join(\"/\"))||c||(a=\".\");a&&d&&(a+=\"/\");return(c?\"/\":\"\")+a},db=a=>{var c=/^(\\/?|)([\\s\\S]*?)((?:\\.{1,2}|[^\\/]+?|)(\\.[^.\\/]*|))(?:[\\/]*)$/.exec(a).slice(1);a=c[0];c=c[1];if(!a&&!c)return\".\";c&&(c=c.substr(0,c.length-1));return a+c},eb=\na=>{if(\"/\"===a)return\"/\";a=cb(a);a=a.replace(/\\/$/,\"\");var c=a.lastIndexOf(\"/\");return-1===c?a:a.substr(c+1)},fb=(a,c)=>cb(a+\"/\"+c);function gb(){if(\"object\"==typeof crypto&&\"function\"==typeof crypto.getRandomValues){var a=new Uint8Array(1);return()=>{crypto.getRandomValues(a);return a[0]}}if(ia)try{var c=require(\"crypto\");return()=>c.randomBytes(1)[0]}catch(d){}return()=>n(\"randomDevice\")}\nfunction hb(){for(var a=\"\",c=!1,d=arguments.length-1;-1<=d&&!c;d--){c=0<=d?arguments[d]:D.cwd();if(\"string\"!=typeof c)throw new TypeError(\"Arguments to path.resolve must be strings\");if(!c)return\"\";a=c+\"/\"+a;c=\"/\"===c.charAt(0)}a=bb(a.split(\"/\").filter(e=>!!e),!c).join(\"/\");return(c?\"/\":\"\")+a||\".\"}\nvar ib=(a,c)=>{function d(k){for(var m=0;m<k.length&&\"\"===k[m];m++);for(var t=k.length-1;0<=t&&\"\"===k[t];t--);return m>t?[]:k.slice(m,t-m+1)}a=hb(a).substr(1);c=hb(c).substr(1);a=d(a.split(\"/\"));c=d(c.split(\"/\"));for(var e=Math.min(a.length,c.length),g=e,h=0;h<e;h++)if(a[h]!==c[h]){g=h;break}e=[];for(h=g;h<a.length;h++)e.push(\"..\");e=e.concat(c.slice(g));return e.join(\"/\")};function jb(a,c){var d=Array(Aa(a)+1);a=za(a,d,0,d.length);c&&(d.length=a);return d}var kb=[];\nfunction lb(a,c){kb[a]={input:[],output:[],tg:c};D.gh(a,mb)}\nvar mb={open:function(a){var c=kb[a.node.rdev];if(!c)throw new D.Jf(43);a.tty=c;a.seekable=!1},close:function(a){a.tty.tg.flush(a.tty)},flush:function(a){a.tty.tg.flush(a.tty)},read:function(a,c,d,e){if(!a.tty||!a.tty.tg.wh)throw new D.Jf(60);for(var g=0,h=0;h<e;h++){try{var k=a.tty.tg.wh(a.tty)}catch(m){throw new D.Jf(29);}if(void 0===k&&0===g)throw new D.Jf(6);if(null===k||void 0===k)break;g++;c[d+h]=k}g&&(a.node.timestamp=Date.now());return g},write:function(a,c,d,e){if(!a.tty||!a.tty.tg.dh)throw new D.Jf(60);\ntry{for(var g=0;g<e;g++)a.tty.tg.dh(a.tty,c[d+g])}catch(h){throw new D.Jf(29);}e&&(a.node.timestamp=Date.now());return g}},nb={wh:function(a){if(!a.input.length){var c=null;if(ia){var d=Buffer.alloc(256),e=0;try{e=fs.readSync(process.stdin.fd,d,0,256,-1)}catch(g){if(g.toString().includes(\"EOF\"))e=0;else throw g;}0<e?c=d.slice(0,e).toString(\"utf-8\"):c=null}else\"undefined\"!=typeof window&&\"function\"==typeof window.prompt?(c=window.prompt(\"Input: \"),null!==c&&(c+=\"\\n\")):\"function\"==typeof readline&&\n(c=readline(),null!==c&&(c+=\"\\n\"));if(!c)return null;a.input=jb(c,!0)}return a.input.shift()},dh:function(a,c){null===c||10===c?(qa(xa(a.output,0)),a.output=[]):0!=c&&a.output.push(c)},flush:function(a){a.output&&0<a.output.length&&(qa(xa(a.output,0)),a.output=[])}},ob={dh:function(a,c){null===c||10===c?(pa(xa(a.output,0)),a.output=[]):0!=c&&a.output.push(c)},flush:function(a){a.output&&0<a.output.length&&(pa(xa(a.output,0)),a.output=[])}};\nfunction pb(a){a=65536*Math.ceil(a/65536);var c=qb(65536,a);if(!c)return 0;ya.fill(0,c,c+a);return c}\nvar E={ag:null,Sf:function(){return E.createNode(null,\"/\",16895,0)},createNode:function(a,c,d,e){if(D.ui(d)||D.isFIFO(d))throw new D.Jf(63);E.ag||(E.ag={dir:{node:{Yf:E.Kf.Yf,Uf:E.Kf.Uf,lookup:E.Kf.lookup,eg:E.Kf.eg,rename:E.Kf.rename,unlink:E.Kf.unlink,rmdir:E.Kf.rmdir,readdir:E.Kf.readdir,symlink:E.Kf.symlink},stream:{Zf:E.Mf.Zf}},file:{node:{Yf:E.Kf.Yf,Uf:E.Kf.Uf},stream:{Zf:E.Mf.Zf,read:E.Mf.read,write:E.Mf.write,vg:E.Mf.vg,lg:E.Mf.lg,sg:E.Mf.sg}},link:{node:{Yf:E.Kf.Yf,Uf:E.Kf.Uf,readlink:E.Kf.readlink},\nstream:{}},mh:{node:{Yf:E.Kf.Yf,Uf:E.Kf.Uf},stream:D.Oh}});d=D.createNode(a,c,d,e);D.Tf(d.mode)?(d.Kf=E.ag.dir.node,d.Mf=E.ag.dir.stream,d.Lf={}):D.isFile(d.mode)?(d.Kf=E.ag.file.node,d.Mf=E.ag.file.stream,d.Qf=0,d.Lf=null):D.yg(d.mode)?(d.Kf=E.ag.link.node,d.Mf=E.ag.link.stream):D.Cg(d.mode)&&(d.Kf=E.ag.mh.node,d.Mf=E.ag.mh.stream);d.timestamp=Date.now();a&&(a.Lf[c]=d,a.timestamp=d.timestamp);return d},Oi:function(a){return a.Lf?a.Lf.subarray?a.Lf.subarray(0,a.Qf):new Uint8Array(a.Lf):new Uint8Array(0)},\nth:function(a,c){var d=a.Lf?a.Lf.length:0;d>=c||(c=Math.max(c,d*(1048576>d?2:1.125)>>>0),0!=d&&(c=Math.max(c,256)),d=a.Lf,a.Lf=new Uint8Array(c),0<a.Qf&&a.Lf.set(d.subarray(0,a.Qf),0))},Ei:function(a,c){if(a.Qf!=c)if(0==c)a.Lf=null,a.Qf=0;else{var d=a.Lf;a.Lf=new Uint8Array(c);d&&a.Lf.set(d.subarray(0,Math.min(c,a.Qf)));a.Qf=c}},Kf:{Yf:function(a){var c={};c.dev=D.Cg(a.mode)?a.id:1;c.ino=a.id;c.mode=a.mode;c.nlink=1;c.uid=0;c.gid=0;c.rdev=a.rdev;D.Tf(a.mode)?c.size=4096:D.isFile(a.mode)?c.size=a.Qf:\nD.yg(a.mode)?c.size=a.link.length:c.size=0;c.atime=new Date(a.timestamp);c.mtime=new Date(a.timestamp);c.ctime=new Date(a.timestamp);c.Lh=4096;c.blocks=Math.ceil(c.size/c.Lh);return c},Uf:function(a,c){void 0!==c.mode&&(a.mode=c.mode);void 0!==c.timestamp&&(a.timestamp=c.timestamp);void 0!==c.size&&E.Ei(a,c.size)},lookup:function(){throw D.Pg[44];},eg:function(a,c,d,e){return E.createNode(a,c,d,e)},rename:function(a,c,d){if(D.Tf(a.mode)){try{var e=D.dg(c,d)}catch(h){}if(e)for(var g in e.Lf)throw new D.Jf(55);\n}delete a.parent.Lf[a.name];a.parent.timestamp=Date.now();a.name=d;c.Lf[d]=a;c.timestamp=a.parent.timestamp;a.parent=c},unlink:function(a,c){delete a.Lf[c];a.timestamp=Date.now()},rmdir:function(a,c){var d=D.dg(a,c),e;for(e in d.Lf)throw new D.Jf(55);delete a.Lf[c];a.timestamp=Date.now()},readdir:function(a){var c=[\".\",\"..\"],d;for(d in a.Lf)a.Lf.hasOwnProperty(d)&&c.push(d);return c},symlink:function(a,c,d){a=E.createNode(a,c,41471,0);a.link=d;return a},readlink:function(a){if(!D.yg(a.mode))throw new D.Jf(28);\nreturn a.link}},Mf:{read:function(a,c,d,e,g){var h=a.node.Lf;if(g>=a.node.Qf)return 0;a=Math.min(a.node.Qf-g,e);if(8<a&&h.subarray)c.set(h.subarray(g,g+a),d);else for(e=0;e<a;e++)c[d+e]=h[g+e];return a},write:function(a,c,d,e,g,h){c.buffer===r.buffer&&(h=!1);if(!e)return 0;a=a.node;a.timestamp=Date.now();if(c.subarray&&(!a.Lf||a.Lf.subarray)){if(h)return a.Lf=c.subarray(d,d+e),a.Qf=e;if(0===a.Qf&&0===g)return a.Lf=c.slice(d,d+e),a.Qf=e;if(g+e<=a.Qf)return a.Lf.set(c.subarray(d,d+e),g),e}E.th(a,g+\ne);if(a.Lf.subarray&&c.subarray)a.Lf.set(c.subarray(d,d+e),g);else for(h=0;h<e;h++)a.Lf[g+h]=c[d+h];a.Qf=Math.max(a.Qf,g+e);return e},Zf:function(a,c,d){1===d?c+=a.position:2===d&&D.isFile(a.node.mode)&&(c+=a.node.Qf);if(0>c)throw new D.Jf(28);return c},vg:function(a,c,d){E.th(a.node,c+d);a.node.Qf=Math.max(a.node.Qf,c+d)},lg:function(a,c,d,e,g){if(!D.isFile(a.node.mode))throw new D.Jf(43);a=a.node.Lf;if(g&2||a.buffer!==Ba){if(0<d||d+c<a.length)a.subarray?a=a.subarray(d,d+c):a=Array.prototype.slice.call(a,\nd,d+c);d=!0;c=pb(c);if(!c)throw new D.Jf(48);r.set(a,c)}else d=!1,c=a.byteOffset;return{If:c,kh:d}},sg:function(a,c,d,e,g){if(!D.isFile(a.node.mode))throw new D.Jf(43);if(g&2)return 0;E.Mf.write(a,c,0,e,d,!1);return 0}}};function rb(a,c,d){var e=\"al \"+a;ka(a,g=>{g||n('Loading data file \"'+a+'\" failed (no arrayBuffer).');c(new Uint8Array(g));e&&Ra(e)},()=>{if(d)d();else throw'Loading data file \"'+a+'\" failed.';});e&&Qa(e)}\nvar D={root:null,Ag:[],rh:{},streams:[],zi:1,$f:null,qh:\"/\",Xg:!1,Ah:!0,Jf:null,Pg:{},Wh:null,Gg:0,Pf:(a,c={})=>{a=hb(D.cwd(),a);if(!a)return{path:\"\",node:null};c=Object.assign({Ng:!0,fh:0},c);if(8<c.fh)throw new D.Jf(32);a=bb(a.split(\"/\").filter(k=>!!k),!1);for(var d=D.root,e=\"/\",g=0;g<a.length;g++){var h=g===a.length-1;if(h&&c.parent)break;d=D.dg(d,a[g]);e=cb(e+\"/\"+a[g]);D.jg(d)&&(!h||h&&c.Ng)&&(d=d.zg.root);if(!h||c.Xf)for(h=0;D.yg(d.mode);)if(d=D.readlink(e),e=hb(db(e),d),d=D.Pf(e,{fh:c.fh+1}).node,\n40<h++)throw new D.Jf(32);}return{path:e,node:d}},fg:a=>{for(var c;;){if(D.Dg(a))return a=a.Sf.Bh,c?\"/\"!==a[a.length-1]?a+\"/\"+c:a+c:a;c=c?a.name+\"/\"+c:a.name;a=a.parent}},Wg:(a,c)=>{for(var d=0,e=0;e<c.length;e++)d=(d<<5)-d+c.charCodeAt(e)|0;return(a+d>>>0)%D.$f.length},yh:a=>{var c=D.Wg(a.parent.id,a.name);a.mg=D.$f[c];D.$f[c]=a},zh:a=>{var c=D.Wg(a.parent.id,a.name);if(D.$f[c]===a)D.$f[c]=a.mg;else for(c=D.$f[c];c;){if(c.mg===a){c.mg=a.mg;break}c=c.mg}},dg:(a,c)=>{var d=D.wi(a);if(d)throw new D.Jf(d,\na);for(d=D.$f[D.Wg(a.id,c)];d;d=d.mg){var e=d.name;if(d.parent.id===a.id&&e===c)return d}return D.lookup(a,c)},createNode:(a,c,d,e)=>{a=new D.Dh(a,c,d,e);D.yh(a);return a},Mg:a=>{D.zh(a)},Dg:a=>a===a.parent,jg:a=>!!a.zg,isFile:a=>32768===(a&61440),Tf:a=>16384===(a&61440),yg:a=>40960===(a&61440),Cg:a=>8192===(a&61440),ui:a=>24576===(a&61440),isFIFO:a=>4096===(a&61440),isSocket:a=>49152===(a&49152),Xh:{r:0,\"r+\":2,w:577,\"w+\":578,a:1089,\"a+\":1090},yi:a=>{var c=D.Xh[a];if(\"undefined\"==typeof c)throw Error(\"Unknown file open mode: \"+\na);return c},uh:a=>{var c=[\"r\",\"w\",\"rw\"][a&3];a&512&&(c+=\"w\");return c},ng:(a,c)=>{if(D.Ah)return 0;if(!c.includes(\"r\")||a.mode&292){if(c.includes(\"w\")&&!(a.mode&146)||c.includes(\"x\")&&!(a.mode&73))return 2}else return 2;return 0},wi:a=>{var c=D.ng(a,\"x\");return c?c:a.Kf.lookup?0:2},bh:(a,c)=>{try{return D.dg(a,c),20}catch(d){}return D.ng(a,\"wx\")},Eg:(a,c,d)=>{try{var e=D.dg(a,c)}catch(g){return g.Rf}if(a=D.ng(a,\"wx\"))return a;if(d){if(!D.Tf(e.mode))return 54;if(D.Dg(e)||D.fg(e)===D.cwd())return 10}else if(D.Tf(e.mode))return 31;\nreturn 0},xi:(a,c)=>a?D.yg(a.mode)?32:D.Tf(a.mode)&&(\"r\"!==D.uh(c)||c&512)?31:D.ng(a,D.uh(c)):44,Gh:4096,Ai:(a=0,c=D.Gh)=>{for(;a<=c;a++)if(!D.streams[a])return a;throw new D.Jf(33);},gg:a=>D.streams[a],ph:(a,c,d)=>{D.Bg||(D.Bg=function(){this.Vf={}},D.Bg.prototype={},Object.defineProperties(D.Bg.prototype,{object:{get:function(){return this.node},set:function(e){this.node=e}},flags:{get:function(){return this.Vf.flags},set:function(e){this.Vf.flags=e}},position:{get:function(){return this.Vf.position},\nset:function(e){this.Vf.position=e}}}));a=Object.assign(new D.Bg,a);c=D.Ai(c,d);a.fd=c;return D.streams[c]=a},Ph:a=>{D.streams[a]=null},Oh:{open:a=>{a.Mf=D.Yh(a.node.rdev).Mf;a.Mf.open&&a.Mf.open(a)},Zf:()=>{throw new D.Jf(70);}},ah:a=>a>>8,Ri:a=>a&255,kg:(a,c)=>a<<8|c,gh:(a,c)=>{D.rh[a]={Mf:c}},Yh:a=>D.rh[a],vh:a=>{var c=[];for(a=[a];a.length;){var d=a.pop();c.push(d);a.push.apply(a,d.Ag)}return c},Ch:(a,c)=>{function d(k){D.Gg--;return c(k)}function e(k){if(k){if(!e.Vh)return e.Vh=!0,d(k)}else++h>=\ng.length&&d(null)}\"function\"==typeof a&&(c=a,a=!1);D.Gg++;1<D.Gg&&pa(\"warning: \"+D.Gg+\" FS.syncfs operations in flight at once, probably just doing extra work\");var g=D.vh(D.root.Sf),h=0;g.forEach(k=>{if(!k.type.Ch)return e(null);k.type.Ch(k,a,e)})},Sf:(a,c,d)=>{var e=\"/\"===d,g=!d;if(e&&D.root)throw new D.Jf(10);if(!e&&!g){var h=D.Pf(d,{Ng:!1});d=h.path;h=h.node;if(D.jg(h))throw new D.Jf(10);if(!D.Tf(h.mode))throw new D.Jf(54);}c={type:a,Ui:c,Bh:d,Ag:[]};a=a.Sf(c);a.Sf=c;c.root=a;e?D.root=a:h&&(h.zg=\nc,h.Sf&&h.Sf.Ag.push(c));return a},Yi:a=>{a=D.Pf(a,{Ng:!1});if(!D.jg(a.node))throw new D.Jf(28);a=a.node;var c=a.zg,d=D.vh(c);Object.keys(D.$f).forEach(e=>{for(e=D.$f[e];e;){var g=e.mg;d.includes(e.Sf)&&D.Mg(e);e=g}});a.zg=null;a.Sf.Ag.splice(a.Sf.Ag.indexOf(c),1)},lookup:(a,c)=>a.Kf.lookup(a,c),eg:(a,c,d)=>{var e=D.Pf(a,{parent:!0}).node;a=eb(a);if(!a||\".\"===a||\"..\"===a)throw new D.Jf(28);var g=D.bh(e,a);if(g)throw new D.Jf(g);if(!e.Kf.eg)throw new D.Jf(63);return e.Kf.eg(e,a,c,d)},create:(a,c)=>\nD.eg(a,(void 0!==c?c:438)&4095|32768,0),mkdir:(a,c)=>D.eg(a,(void 0!==c?c:511)&1023|16384,0),Si:(a,c)=>{a=a.split(\"/\");for(var d=\"\",e=0;e<a.length;++e)if(a[e]){d+=\"/\"+a[e];try{D.mkdir(d,c)}catch(g){if(20!=g.Rf)throw g;}}},Fg:(a,c,d)=>{\"undefined\"==typeof d&&(d=c,c=438);return D.eg(a,c|8192,d)},symlink:(a,c)=>{if(!hb(a))throw new D.Jf(44);var d=D.Pf(c,{parent:!0}).node;if(!d)throw new D.Jf(44);c=eb(c);var e=D.bh(d,c);if(e)throw new D.Jf(e);if(!d.Kf.symlink)throw new D.Jf(63);return d.Kf.symlink(d,\nc,a)},rename:(a,c)=>{var d=db(a),e=db(c),g=eb(a),h=eb(c);var k=D.Pf(a,{parent:!0});var m=k.node;k=D.Pf(c,{parent:!0});k=k.node;if(!m||!k)throw new D.Jf(44);if(m.Sf!==k.Sf)throw new D.Jf(75);var t=D.dg(m,g);a=ib(a,e);if(\".\"!==a.charAt(0))throw new D.Jf(28);a=ib(c,d);if(\".\"!==a.charAt(0))throw new D.Jf(55);try{var u=D.dg(k,h)}catch(p){}if(t!==u){c=D.Tf(t.mode);if(g=D.Eg(m,g,c))throw new D.Jf(g);if(g=u?D.Eg(k,h,c):D.bh(k,h))throw new D.Jf(g);if(!m.Kf.rename)throw new D.Jf(63);if(D.jg(t)||u&&D.jg(u))throw new D.Jf(10);\nif(k!==m&&(g=D.ng(m,\"w\")))throw new D.Jf(g);D.zh(t);try{m.Kf.rename(t,k,h)}catch(p){throw p;}finally{D.yh(t)}}},rmdir:a=>{var c=D.Pf(a,{parent:!0}).node;a=eb(a);var d=D.dg(c,a),e=D.Eg(c,a,!0);if(e)throw new D.Jf(e);if(!c.Kf.rmdir)throw new D.Jf(63);if(D.jg(d))throw new D.Jf(10);c.Kf.rmdir(c,a);D.Mg(d)},readdir:a=>{a=D.Pf(a,{Xf:!0}).node;if(!a.Kf.readdir)throw new D.Jf(54);return a.Kf.readdir(a)},unlink:a=>{var c=D.Pf(a,{parent:!0}).node;if(!c)throw new D.Jf(44);a=eb(a);var d=D.dg(c,a),e=D.Eg(c,a,\n!1);if(e)throw new D.Jf(e);if(!c.Kf.unlink)throw new D.Jf(63);if(D.jg(d))throw new D.Jf(10);c.Kf.unlink(c,a);D.Mg(d)},readlink:a=>{a=D.Pf(a).node;if(!a)throw new D.Jf(44);if(!a.Kf.readlink)throw new D.Jf(28);return hb(D.fg(a.parent),a.Kf.readlink(a))},stat:(a,c)=>{a=D.Pf(a,{Xf:!c}).node;if(!a)throw new D.Jf(44);if(!a.Kf.Yf)throw new D.Jf(63);return a.Kf.Yf(a)},lstat:a=>D.stat(a,!0),chmod:(a,c,d)=>{a=\"string\"==typeof a?D.Pf(a,{Xf:!d}).node:a;if(!a.Kf.Uf)throw new D.Jf(63);a.Kf.Uf(a,{mode:c&4095|a.mode&\n-4096,timestamp:Date.now()})},lchmod:(a,c)=>{D.chmod(a,c,!0)},fchmod:(a,c)=>{a=D.gg(a);if(!a)throw new D.Jf(8);D.chmod(a.node,c)},chown:(a,c,d,e)=>{a=\"string\"==typeof a?D.Pf(a,{Xf:!e}).node:a;if(!a.Kf.Uf)throw new D.Jf(63);a.Kf.Uf(a,{timestamp:Date.now()})},lchown:(a,c,d)=>{D.chown(a,c,d,!0)},fchown:(a,c,d)=>{a=D.gg(a);if(!a)throw new D.Jf(8);D.chown(a.node,c,d)},truncate:(a,c)=>{if(0>c)throw new D.Jf(28);a=\"string\"==typeof a?D.Pf(a,{Xf:!0}).node:a;if(!a.Kf.Uf)throw new D.Jf(63);if(D.Tf(a.mode))throw new D.Jf(31);\nif(!D.isFile(a.mode))throw new D.Jf(28);var d=D.ng(a,\"w\");if(d)throw new D.Jf(d);a.Kf.Uf(a,{size:c,timestamp:Date.now()})},Ni:(a,c)=>{a=D.gg(a);if(!a)throw new D.Jf(8);if(0===(a.flags&2097155))throw new D.Jf(28);D.truncate(a.node,c)},Zi:(a,c,d)=>{a=D.Pf(a,{Xf:!0}).node;a.Kf.Uf(a,{timestamp:Math.max(c,d)})},open:(a,c,d)=>{if(\"\"===a)throw new D.Jf(44);c=\"string\"==typeof c?D.yi(c):c;d=c&64?(\"undefined\"==typeof d?438:d)&4095|32768:0;if(\"object\"==typeof a)var e=a;else{a=cb(a);try{e=D.Pf(a,{Xf:!(c&131072)}).node}catch(h){}}var g=\n!1;if(c&64)if(e){if(c&128)throw new D.Jf(20);}else e=D.eg(a,d,0),g=!0;if(!e)throw new D.Jf(44);D.Cg(e.mode)&&(c&=-513);if(c&65536&&!D.Tf(e.mode))throw new D.Jf(54);if(!g&&(d=D.xi(e,c)))throw new D.Jf(d);c&512&&!g&&D.truncate(e,0);c&=-131713;e=D.ph({node:e,path:D.fg(e),flags:c,seekable:!0,position:0,Mf:e.Mf,Li:[],error:!1});e.Mf.open&&e.Mf.open(e);!b.logReadFiles||c&1||(D.eh||(D.eh={}),a in D.eh||(D.eh[a]=1));return e},close:a=>{if(D.xg(a))throw new D.Jf(8);a.Vg&&(a.Vg=null);try{a.Mf.close&&a.Mf.close(a)}catch(c){throw c;\n}finally{D.Ph(a.fd)}a.fd=null},xg:a=>null===a.fd,Zf:(a,c,d)=>{if(D.xg(a))throw new D.Jf(8);if(!a.seekable||!a.Mf.Zf)throw new D.Jf(70);if(0!=d&&1!=d&&2!=d)throw new D.Jf(28);a.position=a.Mf.Zf(a,c,d);a.Li=[];return a.position},read:(a,c,d,e,g)=>{if(0>e||0>g)throw new D.Jf(28);if(D.xg(a))throw new D.Jf(8);if(1===(a.flags&2097155))throw new D.Jf(8);if(D.Tf(a.node.mode))throw new D.Jf(31);if(!a.Mf.read)throw new D.Jf(28);var h=\"undefined\"!=typeof g;if(!h)g=a.position;else if(!a.seekable)throw new D.Jf(70);\nc=a.Mf.read(a,c,d,e,g);h||(a.position+=c);return c},write:(a,c,d,e,g,h)=>{if(0>e||0>g)throw new D.Jf(28);if(D.xg(a))throw new D.Jf(8);if(0===(a.flags&2097155))throw new D.Jf(8);if(D.Tf(a.node.mode))throw new D.Jf(31);if(!a.Mf.write)throw new D.Jf(28);a.seekable&&a.flags&1024&&D.Zf(a,0,2);var k=\"undefined\"!=typeof g;if(!k)g=a.position;else if(!a.seekable)throw new D.Jf(70);c=a.Mf.write(a,c,d,e,g,h);k||(a.position+=c);return c},vg:(a,c,d)=>{if(D.xg(a))throw new D.Jf(8);if(0>c||0>=d)throw new D.Jf(28);\nif(0===(a.flags&2097155))throw new D.Jf(8);if(!D.isFile(a.node.mode)&&!D.Tf(a.node.mode))throw new D.Jf(43);if(!a.Mf.vg)throw new D.Jf(138);a.Mf.vg(a,c,d)},lg:(a,c,d,e,g)=>{if(0!==(e&2)&&0===(g&2)&&2!==(a.flags&2097155))throw new D.Jf(2);if(1===(a.flags&2097155))throw new D.Jf(2);if(!a.Mf.lg)throw new D.Jf(43);return a.Mf.lg(a,c,d,e,g)},sg:(a,c,d,e,g)=>a&&a.Mf.sg?a.Mf.sg(a,c,d,e,g):0,Ti:()=>0,Yg:(a,c,d)=>{if(!a.Mf.Yg)throw new D.Jf(59);return a.Mf.Yg(a,c,d)},readFile:(a,c={})=>{c.flags=c.flags||0;\nc.encoding=c.encoding||\"binary\";if(\"utf8\"!==c.encoding&&\"binary\"!==c.encoding)throw Error('Invalid encoding type \"'+c.encoding+'\"');var d,e=D.open(a,c.flags);a=D.stat(a).size;var g=new Uint8Array(a);D.read(e,g,0,a,0);\"utf8\"===c.encoding?d=xa(g,0):\"binary\"===c.encoding&&(d=g);D.close(e);return d},writeFile:(a,c,d={})=>{d.flags=d.flags||577;a=D.open(a,d.flags,d.mode);if(\"string\"==typeof c){var e=new Uint8Array(Aa(c)+1);c=za(c,e,0,e.length);D.write(a,e,0,c,void 0,d.Nh)}else if(ArrayBuffer.isView(c))D.write(a,\nc,0,c.byteLength,void 0,d.Nh);else throw Error(\"Unsupported data type\");D.close(a)},cwd:()=>D.qh,chdir:a=>{a=D.Pf(a,{Xf:!0});if(null===a.node)throw new D.Jf(44);if(!D.Tf(a.node.mode))throw new D.Jf(54);var c=D.ng(a.node,\"x\");if(c)throw new D.Jf(c);D.qh=a.path},Rh:()=>{D.mkdir(\"/tmp\");D.mkdir(\"/home\");D.mkdir(\"/home/web_user\")},Qh:()=>{D.mkdir(\"/dev\");D.gh(D.kg(1,3),{read:()=>0,write:(c,d,e,g)=>g});D.Fg(\"/dev/null\",D.kg(1,3));lb(D.kg(5,0),nb);lb(D.kg(6,0),ob);D.Fg(\"/dev/tty\",D.kg(5,0));D.Fg(\"/dev/tty1\",\nD.kg(6,0));var a=gb();D.Wf(\"/dev\",\"random\",a);D.Wf(\"/dev\",\"urandom\",a);D.mkdir(\"/dev/shm\");D.mkdir(\"/dev/shm/tmp\")},Th:()=>{D.mkdir(\"/proc\");var a=D.mkdir(\"/proc/self\");D.mkdir(\"/proc/self/fd\");D.Sf({Sf:()=>{var c=D.createNode(a,\"fd\",16895,73);c.Kf={lookup:(d,e)=>{var g=D.gg(+e);if(!g)throw new D.Jf(8);d={parent:null,Sf:{Bh:\"fake\"},Kf:{readlink:()=>g.path}};return d.parent=d}};return c}},{},\"/proc/self/fd\")},Uh:()=>{b.stdin?D.Wf(\"/dev\",\"stdin\",b.stdin):D.symlink(\"/dev/tty\",\"/dev/stdin\");b.stdout?\nD.Wf(\"/dev\",\"stdout\",null,b.stdout):D.symlink(\"/dev/tty\",\"/dev/stdout\");b.stderr?D.Wf(\"/dev\",\"stderr\",null,b.stderr):D.symlink(\"/dev/tty1\",\"/dev/stderr\");D.open(\"/dev/stdin\",0);D.open(\"/dev/stdout\",1);D.open(\"/dev/stderr\",1)},sh:()=>{D.Jf||(D.Jf=function(a,c){this.node=c;this.Fi=function(d){this.Rf=d};this.Fi(a);this.message=\"FS error\"},D.Jf.prototype=Error(),D.Jf.prototype.constructor=D.Jf,[44].forEach(a=>{D.Pg[a]=new D.Jf(a);D.Pg[a].stack=\"<generic error, no stack>\"}))},Gi:()=>{D.sh();D.$f=Array(4096);\nD.Sf(E,{},\"/\");D.Rh();D.Qh();D.Th();D.Wh={MEMFS:E}},rg:(a,c,d)=>{D.rg.Xg=!0;D.sh();b.stdin=a||b.stdin;b.stdout=c||b.stdout;b.stderr=d||b.stderr;D.Uh()},Vi:()=>{D.rg.Xg=!1;for(var a=0;a<D.streams.length;a++){var c=D.streams[a];c&&D.close(c)}},Qg:(a,c)=>{var d=0;a&&(d|=365);c&&(d|=146);return d},Mi:(a,c)=>{a=D.Kg(a,c);return a.exists?a.object:null},Kg:(a,c)=>{try{var d=D.Pf(a,{Xf:!c});a=d.path}catch(g){}var e={Dg:!1,exists:!1,error:0,name:null,path:null,object:null,Bi:!1,Di:null,Ci:null};try{d=D.Pf(a,\n{parent:!0}),e.Bi=!0,e.Di=d.path,e.Ci=d.node,e.name=eb(a),d=D.Pf(a,{Xf:!c}),e.exists=!0,e.path=d.path,e.object=d.node,e.name=d.node.name,e.Dg=\"/\"===d.path}catch(g){e.error=g.Rf}return e},Lg:(a,c)=>{a=\"string\"==typeof a?a:D.fg(a);for(c=c.split(\"/\").reverse();c.length;){var d=c.pop();if(d){var e=cb(a+\"/\"+d);try{D.mkdir(e)}catch(g){}a=e}}return e},Sh:(a,c,d,e,g)=>{a=\"string\"==typeof a?a:D.fg(a);c=cb(a+\"/\"+c);return D.create(c,D.Qg(e,g))},wg:(a,c,d,e,g,h)=>{var k=c;a&&(a=\"string\"==typeof a?a:D.fg(a),\nk=c?cb(a+\"/\"+c):a);a=D.Qg(e,g);k=D.create(k,a);if(d){if(\"string\"==typeof d){c=Array(d.length);e=0;for(g=d.length;e<g;++e)c[e]=d.charCodeAt(e);d=c}D.chmod(k,a|146);c=D.open(k,577);D.write(c,d,0,d.length,0,h);D.close(c);D.chmod(k,a)}return k},Wf:(a,c,d,e)=>{a=fb(\"string\"==typeof a?a:D.fg(a),c);c=D.Qg(!!d,!!e);D.Wf.ah||(D.Wf.ah=64);var g=D.kg(D.Wf.ah++,0);D.gh(g,{open:h=>{h.seekable=!1},close:()=>{e&&e.buffer&&e.buffer.length&&e(10)},read:(h,k,m,t)=>{for(var u=0,p=0;p<t;p++){try{var x=d()}catch(N){throw new D.Jf(29);\n}if(void 0===x&&0===u)throw new D.Jf(6);if(null===x||void 0===x)break;u++;k[m+p]=x}u&&(h.node.timestamp=Date.now());return u},write:(h,k,m,t)=>{for(var u=0;u<t;u++)try{e(k[m+u])}catch(p){throw new D.Jf(29);}t&&(h.node.timestamp=Date.now());return u}});return D.Fg(a,c,g)},Og:a=>{if(a.Zg||a.vi||a.link||a.Lf)return!0;if(\"undefined\"!=typeof XMLHttpRequest)throw Error(\"Lazy loading should have been performed (contents set) in createLazyFile, but it was not. Lazy loading only works in web workers. Use --embed-file or --preload-file in emcc on the main thread.\");\nif(ja)try{a.Lf=jb(ja(a.url),!0),a.Qf=a.Lf.length}catch(c){throw new D.Jf(29);}else throw Error(\"Cannot load without read() or XMLHttpRequest.\");},nh:(a,c,d,e,g)=>{function h(){this.$g=!1;this.Vf=[]}function k(p,x,N,l,w){p=p.node.Lf;if(w>=p.length)return 0;l=Math.min(p.length-w,l);if(p.slice)for(var B=0;B<l;B++)x[N+B]=p[w+B];else for(B=0;B<l;B++)x[N+B]=p.get(w+B);return l}h.prototype.get=function(p){if(!(p>this.length-1||0>p)){var x=p%this.chunkSize;return this.xh(p/this.chunkSize|0)[x]}};h.prototype.Jg=\nfunction(p){this.xh=p};h.prototype.lh=function(){var p=new XMLHttpRequest;p.open(\"HEAD\",d,!1);p.send(null);if(!(200<=p.status&&300>p.status||304===p.status))throw Error(\"Couldn't load \"+d+\". Status: \"+p.status);var x=Number(p.getResponseHeader(\"Content-length\")),N,l=(N=p.getResponseHeader(\"Accept-Ranges\"))&&\"bytes\"===N;p=(N=p.getResponseHeader(\"Content-Encoding\"))&&\"gzip\"===N;var w=1048576;l||(w=x);var B=this;B.Jg(U=>{var va=U*w,Ja=(U+1)*w-1;Ja=Math.min(Ja,x-1);if(\"undefined\"==typeof B.Vf[U]){var Sh=\nB.Vf;if(va>Ja)throw Error(\"invalid range (\"+va+\", \"+Ja+\") or no bytes requested!\");if(Ja>x-1)throw Error(\"only \"+x+\" bytes available! programmer error!\");var W=new XMLHttpRequest;W.open(\"GET\",d,!1);x!==w&&W.setRequestHeader(\"Range\",\"bytes=\"+va+\"-\"+Ja);W.responseType=\"arraybuffer\";W.overrideMimeType&&W.overrideMimeType(\"text/plain; charset=x-user-defined\");W.send(null);if(!(200<=W.status&&300>W.status||304===W.status))throw Error(\"Couldn't load \"+d+\". Status: \"+W.status);va=void 0!==W.response?new Uint8Array(W.response||\n[]):jb(W.responseText||\"\",!0);Sh[U]=va}if(\"undefined\"==typeof B.Vf[U])throw Error(\"doXHR failed!\");return B.Vf[U]});if(p||!x)w=x=1,w=x=this.xh(0).length,qa(\"LazyFiles on gzip forces download of the whole file when length is accessed\");this.Kh=x;this.Jh=w;this.$g=!0};if(\"undefined\"!=typeof XMLHttpRequest){if(!ha)throw\"Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc\";var m=new h;Object.defineProperties(m,{length:{get:function(){this.$g||\nthis.lh();return this.Kh}},chunkSize:{get:function(){this.$g||this.lh();return this.Jh}}});m={Zg:!1,Lf:m}}else m={Zg:!1,url:d};var t=D.Sh(a,c,m,e,g);m.Lf?t.Lf=m.Lf:m.url&&(t.Lf=null,t.url=m.url);Object.defineProperties(t,{Qf:{get:function(){return this.Lf.length}}});var u={};Object.keys(t.Mf).forEach(p=>{var x=t.Mf[p];u[p]=function(){D.Og(t);return x.apply(null,arguments)}});u.read=(p,x,N,l,w)=>{D.Og(t);return k(p,x,N,l,w)};u.lg=(p,x,N)=>{D.Og(t);var l=pb(x);if(!l)throw new D.Jf(48);k(p,r,l,x,N);\nreturn{If:l,kh:!0}};t.Mf=u;return t},oh:(a,c,d,e,g,h,k,m,t,u)=>{function p(l){function w(B){u&&u();m||D.wg(a,c,B,e,g,t);h&&h();Ra(N)}sb.Pi(l,x,w,()=>{k&&k();Ra(N)})||w(l)}var x=c?hb(cb(a+\"/\"+c)):a,N=\"cp \"+x;Qa(N);\"string\"==typeof d?rb(d,l=>p(l),k):p(d)},indexedDB:()=>window.indexedDB||window.mozIndexedDB||window.webkitIndexedDB||window.msIndexedDB,ih:()=>\"EM_FS_\"+window.location.pathname,jh:20,ug:\"FILE_DATA\",Wi:(a,c,d)=>{c=c||(()=>{});d=d||(()=>{});var e=D.indexedDB();try{var g=e.open(D.ih(),D.jh)}catch(h){return d(h)}g.onupgradeneeded=\n()=>{qa(\"creating db\");g.result.createObjectStore(D.ug)};g.onsuccess=()=>{var h=g.result.transaction([D.ug],\"readwrite\"),k=h.objectStore(D.ug),m=0,t=0,u=a.length;a.forEach(p=>{p=k.put(D.Kg(p).object.Lf,p);p.onsuccess=()=>{m++;m+t==u&&(0==t?c():d())};p.onerror=()=>{t++;m+t==u&&(0==t?c():d())}});h.onerror=d};g.onerror=d},Qi:(a,c,d)=>{c=c||(()=>{});d=d||(()=>{});var e=D.indexedDB();try{var g=e.open(D.ih(),D.jh)}catch(h){return d(h)}g.onupgradeneeded=d;g.onsuccess=()=>{var h=g.result;try{var k=h.transaction([D.ug],\n\"readonly\")}catch(x){d(x);return}var m=k.objectStore(D.ug),t=0,u=0,p=a.length;a.forEach(x=>{var N=m.get(x);N.onsuccess=()=>{D.Kg(x).exists&&D.unlink(x);D.wg(db(x),eb(x),N.result,!0,!0,!0);t++;t+u==p&&(0==u?c():d())};N.onerror=()=>{u++;t+u==p&&(0==u?c():d())}});k.onerror=d};g.onerror=d}};function tb(a,c,d){if(\"/\"===c.charAt(0))return c;if(-100===a)a=D.cwd();else{a=D.gg(a);if(!a)throw new D.Jf(8);a=a.path}if(0==c.length){if(!d)throw new D.Jf(44);return a}return cb(a+\"/\"+c)}\nfunction ub(a,c,d){try{var e=a(c)}catch(g){if(g&&g.node&&cb(c)!==cb(D.fg(g.node)))return-54;throw g;}v[d>>2]=e.dev;v[d+8>>2]=e.ino;v[d+12>>2]=e.mode;v[d+16>>2]=e.nlink;v[d+20>>2]=e.uid;v[d+24>>2]=e.gid;v[d+28>>2]=e.rdev;C=[e.size>>>0,(A=e.size,1<=+Math.abs(A)?0<A?(Math.min(+Math.floor(A/4294967296),4294967295)|0)>>>0:~~+Math.ceil((A-+(~~A>>>0))/4294967296)>>>0:0)];v[d+40>>2]=C[0];v[d+44>>2]=C[1];v[d+48>>2]=4096;v[d+52>>2]=e.blocks;C=[Math.floor(e.atime.getTime()/1E3)>>>0,(A=Math.floor(e.atime.getTime()/\n1E3),1<=+Math.abs(A)?0<A?(Math.min(+Math.floor(A/4294967296),4294967295)|0)>>>0:~~+Math.ceil((A-+(~~A>>>0))/4294967296)>>>0:0)];v[d+56>>2]=C[0];v[d+60>>2]=C[1];v[d+64>>2]=0;C=[Math.floor(e.mtime.getTime()/1E3)>>>0,(A=Math.floor(e.mtime.getTime()/1E3),1<=+Math.abs(A)?0<A?(Math.min(+Math.floor(A/4294967296),4294967295)|0)>>>0:~~+Math.ceil((A-+(~~A>>>0))/4294967296)>>>0:0)];v[d+72>>2]=C[0];v[d+76>>2]=C[1];v[d+80>>2]=0;C=[Math.floor(e.ctime.getTime()/1E3)>>>0,(A=Math.floor(e.ctime.getTime()/1E3),1<=+Math.abs(A)?\n0<A?(Math.min(+Math.floor(A/4294967296),4294967295)|0)>>>0:~~+Math.ceil((A-+(~~A>>>0))/4294967296)>>>0:0)];v[d+88>>2]=C[0];v[d+92>>2]=C[1];v[d+96>>2]=0;C=[e.ino>>>0,(A=e.ino,1<=+Math.abs(A)?0<A?(Math.min(+Math.floor(A/4294967296),4294967295)|0)>>>0:~~+Math.ceil((A-+(~~A>>>0))/4294967296)>>>0:0)];v[d+104>>2]=C[0];v[d+108>>2]=C[1];return 0}var vb=void 0;function wb(){vb+=4;return v[vb-4>>2]}function xb(a){a=D.gg(a);if(!a)throw new D.Jf(8);return a}\nfunction yb(a){var c=Aa(a)+1,d=zb(c);d&&za(a,r,d,c);return d}function Ab(a,c,d){function e(t){return(t=t.toTimeString().match(/\\(([A-Za-z ]+)\\)$/))?t[1]:\"GMT\"}var g=(new Date).getFullYear(),h=new Date(g,0,1),k=new Date(g,6,1);g=h.getTimezoneOffset();var m=k.getTimezoneOffset();v[a>>2]=60*Math.max(g,m);v[c>>2]=Number(g!=m);a=e(h);c=e(k);a=yb(a);c=yb(c);m<g?(y[d>>2]=a,y[d+4>>2]=c):(y[d>>2]=c,y[d+4>>2]=a)}function Bb(a,c,d){Bb.Mh||(Bb.Mh=!0,Ab(a,c,d))}var Cb=[],Db;\nDb=ia?()=>{var a=process.hrtime();return 1E3*a[0]+a[1]/1E6}:()=>performance.now();var Eb={};function Fb(){if(!Gb){var a={USER:\"web_user\",LOGNAME:\"web_user\",PATH:\"/\",PWD:\"/\",HOME:\"/home/web_user\",LANG:(\"object\"==typeof navigator&&navigator.languages&&navigator.languages[0]||\"C\").replace(\"-\",\"_\")+\".UTF-8\",_:da||\"./this.program\"},c;for(c in Eb)void 0===Eb[c]?delete a[c]:a[c]=Eb[c];var d=[];for(c in a)d.push(c+\"=\"+a[c]);Gb=d}return Gb}var Gb;function Hb(a){return 0===a%4&&(0!==a%100||0===a%400)}\nvar Ib=[31,29,31,30,31,30,31,31,30,31,30,31],Jb=[31,28,31,30,31,30,31,31,30,31,30,31];\nfunction Kb(a,c,d,e){function g(l,w,B){for(l=\"number\"==typeof l?l.toString():l||\"\";l.length<w;)l=B[0]+l;return l}function h(l,w){return g(l,w,\"0\")}function k(l,w){function B(va){return 0>va?-1:0<va?1:0}var U;0===(U=B(l.getFullYear()-w.getFullYear()))&&0===(U=B(l.getMonth()-w.getMonth()))&&(U=B(l.getDate()-w.getDate()));return U}function m(l){switch(l.getDay()){case 0:return new Date(l.getFullYear()-1,11,29);case 1:return l;case 2:return new Date(l.getFullYear(),0,3);case 3:return new Date(l.getFullYear(),\n0,2);case 4:return new Date(l.getFullYear(),0,1);case 5:return new Date(l.getFullYear()-1,11,31);case 6:return new Date(l.getFullYear()-1,11,30)}}function t(l){var w=l.pg;for(l=new Date((new Date(l.qg+1900,0,1)).getTime());0<w;){var B=l.getMonth(),U=(Hb(l.getFullYear())?Ib:Jb)[B];if(w>U-l.getDate())w-=U-l.getDate()+1,l.setDate(1),11>B?l.setMonth(B+1):(l.setMonth(0),l.setFullYear(l.getFullYear()+1));else{l.setDate(l.getDate()+w);break}}B=new Date(l.getFullYear()+1,0,4);w=m(new Date(l.getFullYear(),\n0,4));B=m(B);return 0>=k(w,l)?0>=k(B,l)?l.getFullYear()+1:l.getFullYear():l.getFullYear()-1}var u=v[e+40>>2];e={Ji:v[e>>2],Ii:v[e+4>>2],Hg:v[e+8>>2],hh:v[e+12>>2],Ig:v[e+16>>2],qg:v[e+20>>2],bg:v[e+24>>2],pg:v[e+28>>2],Xi:v[e+32>>2],Hi:v[e+36>>2],Ki:u?q(u):\"\"};d=q(d);u={\"%c\":\"%a %b %d %H:%M:%S %Y\",\"%D\":\"%m/%d/%y\",\"%F\":\"%Y-%m-%d\",\"%h\":\"%b\",\"%r\":\"%I:%M:%S %p\",\"%R\":\"%H:%M\",\"%T\":\"%H:%M:%S\",\"%x\":\"%m/%d/%y\",\"%X\":\"%H:%M:%S\",\"%Ec\":\"%c\",\"%EC\":\"%C\",\"%Ex\":\"%m/%d/%y\",\"%EX\":\"%H:%M:%S\",\"%Ey\":\"%y\",\"%EY\":\"%Y\",\"%Od\":\"%d\",\n\"%Oe\":\"%e\",\"%OH\":\"%H\",\"%OI\":\"%I\",\"%Om\":\"%m\",\"%OM\":\"%M\",\"%OS\":\"%S\",\"%Ou\":\"%u\",\"%OU\":\"%U\",\"%OV\":\"%V\",\"%Ow\":\"%w\",\"%OW\":\"%W\",\"%Oy\":\"%y\"};for(var p in u)d=d.replace(new RegExp(p,\"g\"),u[p]);var x=\"Sunday Monday Tuesday Wednesday Thursday Friday Saturday\".split(\" \"),N=\"January February March April May June July August September October November December\".split(\" \");u={\"%a\":function(l){return x[l.bg].substring(0,3)},\"%A\":function(l){return x[l.bg]},\"%b\":function(l){return N[l.Ig].substring(0,3)},\"%B\":function(l){return N[l.Ig]},\n\"%C\":function(l){return h((l.qg+1900)/100|0,2)},\"%d\":function(l){return h(l.hh,2)},\"%e\":function(l){return g(l.hh,2,\" \")},\"%g\":function(l){return t(l).toString().substring(2)},\"%G\":function(l){return t(l)},\"%H\":function(l){return h(l.Hg,2)},\"%I\":function(l){l=l.Hg;0==l?l=12:12<l&&(l-=12);return h(l,2)},\"%j\":function(l){for(var w=0,B=0;B<=l.Ig-1;w+=(Hb(l.qg+1900)?Ib:Jb)[B++]);return h(l.hh+w,3)},\"%m\":function(l){return h(l.Ig+1,2)},\"%M\":function(l){return h(l.Ii,2)},\"%n\":function(){return\"\\n\"},\"%p\":function(l){return 0<=\nl.Hg&&12>l.Hg?\"AM\":\"PM\"},\"%S\":function(l){return h(l.Ji,2)},\"%t\":function(){return\"\\t\"},\"%u\":function(l){return l.bg||7},\"%U\":function(l){return h(Math.floor((l.pg+7-l.bg)/7),2)},\"%V\":function(l){var w=Math.floor((l.pg+7-(l.bg+6)%7)/7);2>=(l.bg+371-l.pg-2)%7&&w++;if(w)53==w&&(B=(l.bg+371-l.pg)%7,4==B||3==B&&Hb(l.qg)||(w=1));else{w=52;var B=(l.bg+7-l.pg-1)%7;(4==B||5==B&&Hb(l.qg%400-1))&&w++}return h(w,2)},\"%w\":function(l){return l.bg},\"%W\":function(l){return h(Math.floor((l.pg+7-(l.bg+6)%7)/7),2)},\n\"%y\":function(l){return(l.qg+1900).toString().substring(2)},\"%Y\":function(l){return l.qg+1900},\"%z\":function(l){l=l.Hi;var w=0<=l;l=Math.abs(l)/60;return(w?\"+\":\"-\")+String(\"0000\"+(l/60*100+l%60)).slice(-4)},\"%Z\":function(l){return l.Ki},\"%%\":function(){return\"%\"}};d=d.replace(/%%/g,\"\\x00\\x00\");for(p in u)d.includes(p)&&(d=d.replace(new RegExp(p,\"g\"),u[p](e)));d=d.replace(/\\0\\0/g,\"%\");p=jb(d,!1);if(p.length>c)return 0;r.set(p,a);return p.length-1}var Lb=[];\nfunction Mb(a){var c=Lb[a];c||(a>=Lb.length&&(Lb.length=a+1),Lb[a]=c=Ga.get(a));return c}function Nb(a,c,d,e){a||(a=this);this.parent=a;this.Sf=a.Sf;this.zg=null;this.id=D.zi++;this.name=c;this.mode=d;this.Kf={};this.Mf={};this.rdev=e}\nObject.defineProperties(Nb.prototype,{read:{get:function(){return 365===(this.mode&365)},set:function(a){a?this.mode|=365:this.mode&=-366}},write:{get:function(){return 146===(this.mode&146)},set:function(a){a?this.mode|=146:this.mode&=-147}},vi:{get:function(){return D.Tf(this.mode)}},Zg:{get:function(){return D.Cg(this.mode)}}});D.Dh=Nb;D.Gi();var sb;b.FS_createPath=D.Lg;b.FS_createDataFile=D.wg;b.FS_createPreloadedFile=D.oh;b.FS_unlink=D.unlink;b.FS_createLazyFile=D.nh;b.FS_createDevice=D.Wf;\nvar $b={c:function(a,c,d,e){n(\"Assertion failed: \"+q(a)+\", at: \"+[c?q(c):\"unknown filename\",d,e?q(e):\"unknown function\"])},q:function(a){return zb(a+24)+24},p:function(a,c,d){(new $a(a)).rg(c,d);ab++;throw a;},w:function(a,c,d){vb=d;try{var e=xb(a);switch(c){case 0:var g=wb();return 0>g?-28:D.ph(e,g).fd;case 1:case 2:return 0;case 3:return e.flags;case 4:return g=wb(),e.flags|=g,0;case 5:return g=wb(),Ca[g+0>>1]=2,0;case 6:case 7:return 0;case 16:case 8:return-28;case 9:return v[Ob()>>2]=28,-1;default:return-28}}catch(h){if(\"undefined\"==\ntypeof D||!(h instanceof D.Jf))throw h;return-h.Rf}},M:function(a,c){try{var d=xb(a);return ub(D.stat,d.path,c)}catch(e){if(\"undefined\"==typeof D||!(e instanceof D.Jf))throw e;return-e.Rf}},J:function(a,c){try{if(0===c)return-28;var d=D.cwd(),e=Aa(d)+1;if(c<e)return-68;za(d,ya,a,c);return e}catch(g){if(\"undefined\"==typeof D||!(g instanceof D.Jf))throw g;return-g.Rf}},U:function(a,c,d){vb=d;try{var e=xb(a);switch(c){case 21509:case 21505:return e.tty?0:-59;case 21510:case 21511:case 21512:case 21506:case 21507:case 21508:return e.tty?\n0:-59;case 21519:if(!e.tty)return-59;var g=wb();return v[g>>2]=0;case 21520:return e.tty?-28:-59;case 21531:return g=wb(),D.Yg(e,c,g);case 21523:return e.tty?0:-59;case 21524:return e.tty?0:-59;default:return-28}}catch(h){if(\"undefined\"==typeof D||!(h instanceof D.Jf))throw h;return-h.Rf}},K:function(a,c,d,e){try{c=q(c);var g=e&256;c=tb(a,c,e&4096);return ub(g?D.lstat:D.stat,c,d)}catch(h){if(\"undefined\"==typeof D||!(h instanceof D.Jf))throw h;return-h.Rf}},u:function(a,c,d,e){vb=e;try{c=q(c);c=tb(a,\nc);var g=e?wb():0;return D.open(c,d,g).fd}catch(h){if(\"undefined\"==typeof D||!(h instanceof D.Jf))throw h;return-h.Rf}},D:function(a){try{return a=q(a),D.rmdir(a),0}catch(c){if(\"undefined\"==typeof D||!(c instanceof D.Jf))throw c;return-c.Rf}},L:function(a,c){try{return a=q(a),ub(D.stat,a,c)}catch(d){if(\"undefined\"==typeof D||!(d instanceof D.Jf))throw d;return-d.Rf}},E:function(a,c,d){try{return c=q(c),c=tb(a,c),0===d?D.unlink(c):512===d?D.rmdir(c):n(\"Invalid flags passed to unlinkat\"),0}catch(e){if(\"undefined\"==\ntypeof D||!(e instanceof D.Jf))throw e;return-e.Rf}},o:function(){return Date.now()},V:function(a){do{var c=y[a>>2];a+=4;var d=y[a>>2];a+=4;var e=y[a>>2];a+=4;c=q(c);D.Lg(\"/\",db(c),!0,!0);D.wg(c,null,r.subarray(e,e+d),!0,!0,!0)}while(y[a>>2])},O:function(){return!0},B:function(){throw Infinity;},P:function(a,c){a=new Date(1E3*(y[a>>2]+4294967296*v[a+4>>2]));v[c>>2]=a.getUTCSeconds();v[c+4>>2]=a.getUTCMinutes();v[c+8>>2]=a.getUTCHours();v[c+12>>2]=a.getUTCDate();v[c+16>>2]=a.getUTCMonth();v[c+20>>\n2]=a.getUTCFullYear()-1900;v[c+24>>2]=a.getUTCDay();v[c+28>>2]=(a.getTime()-Date.UTC(a.getUTCFullYear(),0,1,0,0,0,0))/864E5|0},Q:function(a,c){a=new Date(1E3*(y[a>>2]+4294967296*v[a+4>>2]));v[c>>2]=a.getSeconds();v[c+4>>2]=a.getMinutes();v[c+8>>2]=a.getHours();v[c+12>>2]=a.getDate();v[c+16>>2]=a.getMonth();v[c+20>>2]=a.getFullYear()-1900;v[c+24>>2]=a.getDay();var d=new Date(a.getFullYear(),0,1);v[c+28>>2]=(a.getTime()-d.getTime())/864E5|0;v[c+36>>2]=-(60*a.getTimezoneOffset());var e=(new Date(a.getFullYear(),\n6,1)).getTimezoneOffset();d=d.getTimezoneOffset();v[c+32>>2]=(e!=d&&a.getTimezoneOffset()==Math.min(d,e))|0},R:function(a){var c=new Date(v[a+20>>2]+1900,v[a+16>>2],v[a+12>>2],v[a+8>>2],v[a+4>>2],v[a>>2],0),d=v[a+32>>2],e=c.getTimezoneOffset(),g=new Date(c.getFullYear(),0,1),h=(new Date(c.getFullYear(),6,1)).getTimezoneOffset(),k=g.getTimezoneOffset(),m=Math.min(k,h);0>d?v[a+32>>2]=Number(h!=k&&m==e):0<d!=(m==e)&&(h=Math.max(k,h),c.setTime(c.getTime()+6E4*((0<d?m:h)-e)));v[a+24>>2]=c.getDay();v[a+\n28>>2]=(c.getTime()-g.getTime())/864E5|0;v[a>>2]=c.getSeconds();v[a+4>>2]=c.getMinutes();v[a+8>>2]=c.getHours();v[a+12>>2]=c.getDate();v[a+16>>2]=c.getMonth();return c.getTime()/1E3|0},F:function(a,c,d,e,g,h){try{var k=D.gg(e);if(!k)return-8;var m=D.lg(k,a,g,c,d),t=m.If;v[h>>2]=m.kh;return t}catch(u){if(\"undefined\"==typeof D||!(u instanceof D.Jf))throw u;return-u.Rf}},G:function(a,c,d,e,g,h){try{var k=D.gg(g);if(k&&d&2){var m=ya.slice(a,a+c);D.sg(k,m,h,c,e)}}catch(t){if(\"undefined\"==typeof D||!(t instanceof\nD.Jf))throw t;return-t.Rf}},S:Bb,n:function(){n(\"\")},t:function(a,c,d){Cb.length=0;var e;for(d>>=2;e=ya[c++];)d+=105!=e&d,Cb.push(105==e?v[d]:Ea[d++>>1]),++d;return Wa[a].apply(null,Cb)},N:Db,T:function(a,c,d){ya.copyWithin(a,c,c+d)},C:function(a){var c=ya.length;a>>>=0;if(2147483648<a)return!1;for(var d=1;4>=d;d*=2){var e=c*(1+.2/d);e=Math.min(e,a+100663296);var g=Math;e=Math.max(a,e);g=g.min.call(g,2147483648,e+(65536-e%65536)%65536);a:{try{ta.grow(g-Ba.byteLength+65535>>>16);Fa();var h=1;break a}catch(k){}h=\nvoid 0}if(h)return!0}return!1},H:function(a,c){var d=0;Fb().forEach(function(e,g){var h=c+d;g=y[a+4*g>>2]=h;for(h=0;h<e.length;++h)r[g++>>0]=e.charCodeAt(h);r[g>>0]=0;d+=e.length+1});return 0},I:function(a,c){var d=Fb();y[a>>2]=d.length;var e=0;d.forEach(function(g){e+=g.length+1});y[c>>2]=e;return 0},m:function(a){if(!noExitRuntime){if(b.onExit)b.onExit(a);ua=!0}ea(a,new oa(a))},s:function(a){try{var c=xb(a);D.close(c);return 0}catch(d){if(\"undefined\"==typeof D||!(d instanceof D.Jf))throw d;return d.Rf}},\nv:function(a,c,d,e){try{a:{var g=xb(a);a=c;for(var h=c=0;h<d;h++){var k=y[a>>2],m=y[a+4>>2];a+=8;var t=D.read(g,r,k,m,void 0);if(0>t){var u=-1;break a}c+=t;if(t<m)break}u=c}v[e>>2]=u;return 0}catch(p){if(\"undefined\"==typeof D||!(p instanceof D.Jf))throw p;return p.Rf}},z:function(a,c,d,e,g){try{c=d+2097152>>>0<4194305-!!c?(c>>>0)+4294967296*d:NaN;if(isNaN(c))return 61;var h=xb(a);D.Zf(h,c,e);C=[h.position>>>0,(A=h.position,1<=+Math.abs(A)?0<A?(Math.min(+Math.floor(A/4294967296),4294967295)|0)>>>0:\n~~+Math.ceil((A-+(~~A>>>0))/4294967296)>>>0:0)];v[g>>2]=C[0];v[g+4>>2]=C[1];h.Vg&&0===c&&0===e&&(h.Vg=null);return 0}catch(k){if(\"undefined\"==typeof D||!(k instanceof D.Jf))throw k;return k.Rf}},r:function(a,c,d,e){try{a:{var g=xb(a);a=c;for(var h=c=0;h<d;h++){var k=y[a>>2],m=y[a+4>>2];a+=8;var t=D.write(g,r,k,m,void 0);if(0>t){var u=-1;break a}c+=t}u=c}y[e>>2]=u;return 0}catch(p){if(\"undefined\"==typeof D||!(p instanceof D.Jf))throw p;return p.Rf}},a:function(){return ra},e:Pb,h:Qb,d:Rb,j:Sb,k:Tb,\ng:Ub,f:Vb,i:Wb,l:Xb,x:Yb,y:Zb,b:function(a){ra=a},W:Kb,A:function(a,c,d,e){return Kb(a,c,d,e)}};\n(function(){function a(g){b.asm=g.exports;ta=b.asm.X;Fa();Ga=b.asm.tf;Ia.unshift(b.asm.Y);Ra(\"wasm-instantiate\")}function c(g){a(g.instance)}function d(g){return Va().then(function(h){return WebAssembly.instantiate(h,e)}).then(function(h){return h}).then(g,function(h){pa(\"failed to asynchronously prepare wasm: \"+h);n(h)})}var e={a:$b};Qa(\"wasm-instantiate\");if(b.instantiateWasm)try{return b.instantiateWasm(e,a)}catch(g){return pa(\"Module.instantiateWasm callback failed with error: \"+g),!1}(function(){return sa||\n\"function\"!=typeof WebAssembly.instantiateStreaming||Sa()||z.startsWith(\"file://\")||ia||\"function\"!=typeof fetch?d(c):fetch(z,{credentials:\"same-origin\"}).then(function(g){return WebAssembly.instantiateStreaming(g,e).then(c,function(h){pa(\"wasm streaming compile failed: \"+h);pa(\"falling back to ArrayBuffer instantiation\");return d(c)})})})().catch(ba);return{}})();b.___wasm_call_ctors=function(){return(b.___wasm_call_ctors=b.asm.Y).apply(null,arguments)};\nvar ac=b._emscripten_bind_ParagraphJustification___destroy___0=function(){return(ac=b._emscripten_bind_ParagraphJustification___destroy___0=b.asm.Z).apply(null,arguments)},bc=b._emscripten_bind_BoolPtr___destroy___0=function(){return(bc=b._emscripten_bind_BoolPtr___destroy___0=b.asm._).apply(null,arguments)},cc=b._emscripten_bind_TessResultRenderer_BeginDocument_1=function(){return(cc=b._emscripten_bind_TessResultRenderer_BeginDocument_1=b.asm.$).apply(null,arguments)},dc=b._emscripten_bind_TessResultRenderer_AddImage_1=\nfunction(){return(dc=b._emscripten_bind_TessResultRenderer_AddImage_1=b.asm.aa).apply(null,arguments)},ec=b._emscripten_bind_TessResultRenderer_EndDocument_0=function(){return(ec=b._emscripten_bind_TessResultRenderer_EndDocument_0=b.asm.ba).apply(null,arguments)},fc=b._emscripten_bind_TessResultRenderer_happy_0=function(){return(fc=b._emscripten_bind_TessResultRenderer_happy_0=b.asm.ca).apply(null,arguments)},gc=b._emscripten_bind_TessResultRenderer_file_extension_0=function(){return(gc=b._emscripten_bind_TessResultRenderer_file_extension_0=\nb.asm.da).apply(null,arguments)},hc=b._emscripten_bind_TessResultRenderer_title_0=function(){return(hc=b._emscripten_bind_TessResultRenderer_title_0=b.asm.ea).apply(null,arguments)},ic=b._emscripten_bind_TessResultRenderer_imagenum_0=function(){return(ic=b._emscripten_bind_TessResultRenderer_imagenum_0=b.asm.fa).apply(null,arguments)},jc=b._emscripten_bind_TessResultRenderer___destroy___0=function(){return(jc=b._emscripten_bind_TessResultRenderer___destroy___0=b.asm.ga).apply(null,arguments)},kc=\nb._emscripten_bind_LongStarPtr___destroy___0=function(){return(kc=b._emscripten_bind_LongStarPtr___destroy___0=b.asm.ha).apply(null,arguments)},lc=b._emscripten_bind_VoidPtr___destroy___0=function(){return(lc=b._emscripten_bind_VoidPtr___destroy___0=b.asm.ia).apply(null,arguments)},mc=b._emscripten_bind_ResultIterator_ResultIterator_1=function(){return(mc=b._emscripten_bind_ResultIterator_ResultIterator_1=b.asm.ja).apply(null,arguments)},nc=b._emscripten_bind_ResultIterator_Begin_0=function(){return(nc=\nb._emscripten_bind_ResultIterator_Begin_0=b.asm.ka).apply(null,arguments)},oc=b._emscripten_bind_ResultIterator_RestartParagraph_0=function(){return(oc=b._emscripten_bind_ResultIterator_RestartParagraph_0=b.asm.la).apply(null,arguments)},pc=b._emscripten_bind_ResultIterator_IsWithinFirstTextlineOfParagraph_0=function(){return(pc=b._emscripten_bind_ResultIterator_IsWithinFirstTextlineOfParagraph_0=b.asm.ma).apply(null,arguments)},qc=b._emscripten_bind_ResultIterator_RestartRow_0=function(){return(qc=\nb._emscripten_bind_ResultIterator_RestartRow_0=b.asm.na).apply(null,arguments)},rc=b._emscripten_bind_ResultIterator_Next_1=function(){return(rc=b._emscripten_bind_ResultIterator_Next_1=b.asm.oa).apply(null,arguments)},sc=b._emscripten_bind_ResultIterator_IsAtBeginningOf_1=function(){return(sc=b._emscripten_bind_ResultIterator_IsAtBeginningOf_1=b.asm.pa).apply(null,arguments)},tc=b._emscripten_bind_ResultIterator_IsAtFinalElement_2=function(){return(tc=b._emscripten_bind_ResultIterator_IsAtFinalElement_2=\nb.asm.qa).apply(null,arguments)},uc=b._emscripten_bind_ResultIterator_Cmp_1=function(){return(uc=b._emscripten_bind_ResultIterator_Cmp_1=b.asm.ra).apply(null,arguments)},vc=b._emscripten_bind_ResultIterator_SetBoundingBoxComponents_2=function(){return(vc=b._emscripten_bind_ResultIterator_SetBoundingBoxComponents_2=b.asm.sa).apply(null,arguments)},wc=b._emscripten_bind_ResultIterator_BoundingBox_5=function(){return(wc=b._emscripten_bind_ResultIterator_BoundingBox_5=b.asm.ta).apply(null,arguments)},\nxc=b._emscripten_bind_ResultIterator_BoundingBox_6=function(){return(xc=b._emscripten_bind_ResultIterator_BoundingBox_6=b.asm.ua).apply(null,arguments)},yc=b._emscripten_bind_ResultIterator_BoundingBoxInternal_5=function(){return(yc=b._emscripten_bind_ResultIterator_BoundingBoxInternal_5=b.asm.va).apply(null,arguments)},zc=b._emscripten_bind_ResultIterator_Empty_1=function(){return(zc=b._emscripten_bind_ResultIterator_Empty_1=b.asm.wa).apply(null,arguments)},Ac=b._emscripten_bind_ResultIterator_BlockType_0=\nfunction(){return(Ac=b._emscripten_bind_ResultIterator_BlockType_0=b.asm.xa).apply(null,arguments)},Bc=b._emscripten_bind_ResultIterator_BlockPolygon_0=function(){return(Bc=b._emscripten_bind_ResultIterator_BlockPolygon_0=b.asm.ya).apply(null,arguments)},Cc=b._emscripten_bind_ResultIterator_GetBinaryImage_1=function(){return(Cc=b._emscripten_bind_ResultIterator_GetBinaryImage_1=b.asm.za).apply(null,arguments)},Dc=b._emscripten_bind_ResultIterator_GetImage_5=function(){return(Dc=b._emscripten_bind_ResultIterator_GetImage_5=\nb.asm.Aa).apply(null,arguments)},Ec=b._emscripten_bind_ResultIterator_Baseline_5=function(){return(Ec=b._emscripten_bind_ResultIterator_Baseline_5=b.asm.Ba).apply(null,arguments)},Fc=b._emscripten_bind_ResultIterator_Orientation_4=function(){return(Fc=b._emscripten_bind_ResultIterator_Orientation_4=b.asm.Ca).apply(null,arguments)},Gc=b._emscripten_bind_ResultIterator_ParagraphInfo_4=function(){return(Gc=b._emscripten_bind_ResultIterator_ParagraphInfo_4=b.asm.Da).apply(null,arguments)},Hc=b._emscripten_bind_ResultIterator_ParagraphIsLtr_0=\nfunction(){return(Hc=b._emscripten_bind_ResultIterator_ParagraphIsLtr_0=b.asm.Ea).apply(null,arguments)},Ic=b._emscripten_bind_ResultIterator_GetUTF8Text_1=function(){return(Ic=b._emscripten_bind_ResultIterator_GetUTF8Text_1=b.asm.Fa).apply(null,arguments)},Jc=b._emscripten_bind_ResultIterator_SetLineSeparator_1=function(){return(Jc=b._emscripten_bind_ResultIterator_SetLineSeparator_1=b.asm.Ga).apply(null,arguments)},Kc=b._emscripten_bind_ResultIterator_SetParagraphSeparator_1=function(){return(Kc=\nb._emscripten_bind_ResultIterator_SetParagraphSeparator_1=b.asm.Ha).apply(null,arguments)},Lc=b._emscripten_bind_ResultIterator_Confidence_1=function(){return(Lc=b._emscripten_bind_ResultIterator_Confidence_1=b.asm.Ia).apply(null,arguments)},Mc=b._emscripten_bind_ResultIterator_WordFontAttributes_8=function(){return(Mc=b._emscripten_bind_ResultIterator_WordFontAttributes_8=b.asm.Ja).apply(null,arguments)},Nc=b._emscripten_bind_ResultIterator_WordRecognitionLanguage_0=function(){return(Nc=b._emscripten_bind_ResultIterator_WordRecognitionLanguage_0=\nb.asm.Ka).apply(null,arguments)},Oc=b._emscripten_bind_ResultIterator_WordDirection_0=function(){return(Oc=b._emscripten_bind_ResultIterator_WordDirection_0=b.asm.La).apply(null,arguments)},Pc=b._emscripten_bind_ResultIterator_WordIsFromDictionary_0=function(){return(Pc=b._emscripten_bind_ResultIterator_WordIsFromDictionary_0=b.asm.Ma).apply(null,arguments)},Qc=b._emscripten_bind_ResultIterator_WordIsNumeric_0=function(){return(Qc=b._emscripten_bind_ResultIterator_WordIsNumeric_0=b.asm.Na).apply(null,\narguments)},Rc=b._emscripten_bind_ResultIterator_HasBlamerInfo_0=function(){return(Rc=b._emscripten_bind_ResultIterator_HasBlamerInfo_0=b.asm.Oa).apply(null,arguments)},Sc=b._emscripten_bind_ResultIterator_HasTruthString_0=function(){return(Sc=b._emscripten_bind_ResultIterator_HasTruthString_0=b.asm.Pa).apply(null,arguments)},Tc=b._emscripten_bind_ResultIterator_EquivalentToTruth_1=function(){return(Tc=b._emscripten_bind_ResultIterator_EquivalentToTruth_1=b.asm.Qa).apply(null,arguments)},Uc=b._emscripten_bind_ResultIterator_WordTruthUTF8Text_0=\nfunction(){return(Uc=b._emscripten_bind_ResultIterator_WordTruthUTF8Text_0=b.asm.Ra).apply(null,arguments)},Vc=b._emscripten_bind_ResultIterator_WordNormedUTF8Text_0=function(){return(Vc=b._emscripten_bind_ResultIterator_WordNormedUTF8Text_0=b.asm.Sa).apply(null,arguments)},Wc=b._emscripten_bind_ResultIterator_WordLattice_1=function(){return(Wc=b._emscripten_bind_ResultIterator_WordLattice_1=b.asm.Ta).apply(null,arguments)},Xc=b._emscripten_bind_ResultIterator_SymbolIsSuperscript_0=function(){return(Xc=\nb._emscripten_bind_ResultIterator_SymbolIsSuperscript_0=b.asm.Ua).apply(null,arguments)},Yc=b._emscripten_bind_ResultIterator_SymbolIsSubscript_0=function(){return(Yc=b._emscripten_bind_ResultIterator_SymbolIsSubscript_0=b.asm.Va).apply(null,arguments)},Zc=b._emscripten_bind_ResultIterator_SymbolIsDropcap_0=function(){return(Zc=b._emscripten_bind_ResultIterator_SymbolIsDropcap_0=b.asm.Wa).apply(null,arguments)},$c=b._emscripten_bind_ResultIterator___destroy___0=function(){return($c=b._emscripten_bind_ResultIterator___destroy___0=\nb.asm.Xa).apply(null,arguments)},ad=b._emscripten_bind_TextlineOrder___destroy___0=function(){return(ad=b._emscripten_bind_TextlineOrder___destroy___0=b.asm.Ya).apply(null,arguments)},bd=b._emscripten_bind_ETEXT_DESC___destroy___0=function(){return(bd=b._emscripten_bind_ETEXT_DESC___destroy___0=b.asm.Za).apply(null,arguments)},cd=b._emscripten_bind_PageIterator_Begin_0=function(){return(cd=b._emscripten_bind_PageIterator_Begin_0=b.asm._a).apply(null,arguments)},dd=b._emscripten_bind_PageIterator_RestartParagraph_0=\nfunction(){return(dd=b._emscripten_bind_PageIterator_RestartParagraph_0=b.asm.$a).apply(null,arguments)},ed=b._emscripten_bind_PageIterator_IsWithinFirstTextlineOfParagraph_0=function(){return(ed=b._emscripten_bind_PageIterator_IsWithinFirstTextlineOfParagraph_0=b.asm.ab).apply(null,arguments)},fd=b._emscripten_bind_PageIterator_RestartRow_0=function(){return(fd=b._emscripten_bind_PageIterator_RestartRow_0=b.asm.bb).apply(null,arguments)},gd=b._emscripten_bind_PageIterator_Next_1=function(){return(gd=\nb._emscripten_bind_PageIterator_Next_1=b.asm.cb).apply(null,arguments)},hd=b._emscripten_bind_PageIterator_IsAtBeginningOf_1=function(){return(hd=b._emscripten_bind_PageIterator_IsAtBeginningOf_1=b.asm.db).apply(null,arguments)},jd=b._emscripten_bind_PageIterator_IsAtFinalElement_2=function(){return(jd=b._emscripten_bind_PageIterator_IsAtFinalElement_2=b.asm.eb).apply(null,arguments)},kd=b._emscripten_bind_PageIterator_Cmp_1=function(){return(kd=b._emscripten_bind_PageIterator_Cmp_1=b.asm.fb).apply(null,\narguments)},ld=b._emscripten_bind_PageIterator_SetBoundingBoxComponents_2=function(){return(ld=b._emscripten_bind_PageIterator_SetBoundingBoxComponents_2=b.asm.gb).apply(null,arguments)},md=b._emscripten_bind_PageIterator_BoundingBox_5=function(){return(md=b._emscripten_bind_PageIterator_BoundingBox_5=b.asm.hb).apply(null,arguments)},nd=b._emscripten_bind_PageIterator_BoundingBox_6=function(){return(nd=b._emscripten_bind_PageIterator_BoundingBox_6=b.asm.ib).apply(null,arguments)},od=b._emscripten_bind_PageIterator_BoundingBoxInternal_5=\nfunction(){return(od=b._emscripten_bind_PageIterator_BoundingBoxInternal_5=b.asm.jb).apply(null,arguments)},pd=b._emscripten_bind_PageIterator_Empty_1=function(){return(pd=b._emscripten_bind_PageIterator_Empty_1=b.asm.kb).apply(null,arguments)},qd=b._emscripten_bind_PageIterator_BlockType_0=function(){return(qd=b._emscripten_bind_PageIterator_BlockType_0=b.asm.lb).apply(null,arguments)},rd=b._emscripten_bind_PageIterator_BlockPolygon_0=function(){return(rd=b._emscripten_bind_PageIterator_BlockPolygon_0=\nb.asm.mb).apply(null,arguments)},sd=b._emscripten_bind_PageIterator_GetBinaryImage_1=function(){return(sd=b._emscripten_bind_PageIterator_GetBinaryImage_1=b.asm.nb).apply(null,arguments)},td=b._emscripten_bind_PageIterator_GetImage_5=function(){return(td=b._emscripten_bind_PageIterator_GetImage_5=b.asm.ob).apply(null,arguments)},ud=b._emscripten_bind_PageIterator_Baseline_5=function(){return(ud=b._emscripten_bind_PageIterator_Baseline_5=b.asm.pb).apply(null,arguments)},vd=b._emscripten_bind_PageIterator_Orientation_4=\nfunction(){return(vd=b._emscripten_bind_PageIterator_Orientation_4=b.asm.qb).apply(null,arguments)},wd=b._emscripten_bind_PageIterator_ParagraphInfo_4=function(){return(wd=b._emscripten_bind_PageIterator_ParagraphInfo_4=b.asm.rb).apply(null,arguments)},xd=b._emscripten_bind_PageIterator___destroy___0=function(){return(xd=b._emscripten_bind_PageIterator___destroy___0=b.asm.sb).apply(null,arguments)},yd=b._emscripten_bind_WritingDirection___destroy___0=function(){return(yd=b._emscripten_bind_WritingDirection___destroy___0=\nb.asm.tb).apply(null,arguments)},zd=b._emscripten_bind_WordChoiceIterator_WordChoiceIterator_1=function(){return(zd=b._emscripten_bind_WordChoiceIterator_WordChoiceIterator_1=b.asm.ub).apply(null,arguments)},Ad=b._emscripten_bind_WordChoiceIterator_Next_0=function(){return(Ad=b._emscripten_bind_WordChoiceIterator_Next_0=b.asm.vb).apply(null,arguments)},Bd=b._emscripten_bind_WordChoiceIterator_GetUTF8Text_0=function(){return(Bd=b._emscripten_bind_WordChoiceIterator_GetUTF8Text_0=b.asm.wb).apply(null,\narguments)},Cd=b._emscripten_bind_WordChoiceIterator_Confidence_0=function(){return(Cd=b._emscripten_bind_WordChoiceIterator_Confidence_0=b.asm.xb).apply(null,arguments)},Dd=b._emscripten_bind_WordChoiceIterator___destroy___0=function(){return(Dd=b._emscripten_bind_WordChoiceIterator___destroy___0=b.asm.yb).apply(null,arguments)},Ed=b._emscripten_bind_Box_get_x_0=function(){return(Ed=b._emscripten_bind_Box_get_x_0=b.asm.zb).apply(null,arguments)},Fd=b._emscripten_bind_Box_get_y_0=function(){return(Fd=\nb._emscripten_bind_Box_get_y_0=b.asm.Ab).apply(null,arguments)},Gd=b._emscripten_bind_Box_get_w_0=function(){return(Gd=b._emscripten_bind_Box_get_w_0=b.asm.Bb).apply(null,arguments)},Hd=b._emscripten_bind_Box_get_h_0=function(){return(Hd=b._emscripten_bind_Box_get_h_0=b.asm.Cb).apply(null,arguments)},Id=b._emscripten_bind_Box_get_refcount_0=function(){return(Id=b._emscripten_bind_Box_get_refcount_0=b.asm.Db).apply(null,arguments)},Jd=b._emscripten_bind_Box___destroy___0=function(){return(Jd=b._emscripten_bind_Box___destroy___0=\nb.asm.Eb).apply(null,arguments)},Kd=b._emscripten_bind_TessPDFRenderer_TessPDFRenderer_3=function(){return(Kd=b._emscripten_bind_TessPDFRenderer_TessPDFRenderer_3=b.asm.Fb).apply(null,arguments)},Ld=b._emscripten_bind_TessPDFRenderer_BeginDocument_1=function(){return(Ld=b._emscripten_bind_TessPDFRenderer_BeginDocument_1=b.asm.Gb).apply(null,arguments)},Md=b._emscripten_bind_TessPDFRenderer_AddImage_1=function(){return(Md=b._emscripten_bind_TessPDFRenderer_AddImage_1=b.asm.Hb).apply(null,arguments)},\nNd=b._emscripten_bind_TessPDFRenderer_EndDocument_0=function(){return(Nd=b._emscripten_bind_TessPDFRenderer_EndDocument_0=b.asm.Ib).apply(null,arguments)},Od=b._emscripten_bind_TessPDFRenderer_happy_0=function(){return(Od=b._emscripten_bind_TessPDFRenderer_happy_0=b.asm.Jb).apply(null,arguments)},Pd=b._emscripten_bind_TessPDFRenderer_file_extension_0=function(){return(Pd=b._emscripten_bind_TessPDFRenderer_file_extension_0=b.asm.Kb).apply(null,arguments)},Qd=b._emscripten_bind_TessPDFRenderer_title_0=\nfunction(){return(Qd=b._emscripten_bind_TessPDFRenderer_title_0=b.asm.Lb).apply(null,arguments)},Rd=b._emscripten_bind_TessPDFRenderer_imagenum_0=function(){return(Rd=b._emscripten_bind_TessPDFRenderer_imagenum_0=b.asm.Mb).apply(null,arguments)},Sd=b._emscripten_bind_TessPDFRenderer___destroy___0=function(){return(Sd=b._emscripten_bind_TessPDFRenderer___destroy___0=b.asm.Nb).apply(null,arguments)},Td=b._emscripten_bind_PixaPtr___destroy___0=function(){return(Td=b._emscripten_bind_PixaPtr___destroy___0=\nb.asm.Ob).apply(null,arguments)},Ud=b._emscripten_bind_FloatPtr___destroy___0=function(){return(Ud=b._emscripten_bind_FloatPtr___destroy___0=b.asm.Pb).apply(null,arguments)},Vd=b._emscripten_bind_ChoiceIterator_ChoiceIterator_1=function(){return(Vd=b._emscripten_bind_ChoiceIterator_ChoiceIterator_1=b.asm.Qb).apply(null,arguments)},Wd=b._emscripten_bind_ChoiceIterator_Next_0=function(){return(Wd=b._emscripten_bind_ChoiceIterator_Next_0=b.asm.Rb).apply(null,arguments)},Xd=b._emscripten_bind_ChoiceIterator_GetUTF8Text_0=\nfunction(){return(Xd=b._emscripten_bind_ChoiceIterator_GetUTF8Text_0=b.asm.Sb).apply(null,arguments)},Yd=b._emscripten_bind_ChoiceIterator_Confidence_0=function(){return(Yd=b._emscripten_bind_ChoiceIterator_Confidence_0=b.asm.Tb).apply(null,arguments)},Zd=b._emscripten_bind_ChoiceIterator___destroy___0=function(){return(Zd=b._emscripten_bind_ChoiceIterator___destroy___0=b.asm.Ub).apply(null,arguments)},$d=b._emscripten_bind_PixPtr___destroy___0=function(){return($d=b._emscripten_bind_PixPtr___destroy___0=\nb.asm.Vb).apply(null,arguments)},ae=b._emscripten_bind_UNICHARSET_get_script_from_script_id_1=function(){return(ae=b._emscripten_bind_UNICHARSET_get_script_from_script_id_1=b.asm.Wb).apply(null,arguments)},be=b._emscripten_bind_UNICHARSET_get_script_id_from_name_1=function(){return(be=b._emscripten_bind_UNICHARSET_get_script_id_from_name_1=b.asm.Xb).apply(null,arguments)},ce=b._emscripten_bind_UNICHARSET_get_script_table_size_0=function(){return(ce=b._emscripten_bind_UNICHARSET_get_script_table_size_0=\nb.asm.Yb).apply(null,arguments)},de=b._emscripten_bind_UNICHARSET___destroy___0=function(){return(de=b._emscripten_bind_UNICHARSET___destroy___0=b.asm.Zb).apply(null,arguments)},ee=b._emscripten_bind_IntPtr___destroy___0=function(){return(ee=b._emscripten_bind_IntPtr___destroy___0=b.asm._b).apply(null,arguments)},fe=b._emscripten_bind_Orientation___destroy___0=function(){return(fe=b._emscripten_bind_Orientation___destroy___0=b.asm.$b).apply(null,arguments)},ge=b._emscripten_bind_OSBestResult_get_orientation_id_0=\nfunction(){return(ge=b._emscripten_bind_OSBestResult_get_orientation_id_0=b.asm.ac).apply(null,arguments)},he=b._emscripten_bind_OSBestResult_get_script_id_0=function(){return(he=b._emscripten_bind_OSBestResult_get_script_id_0=b.asm.bc).apply(null,arguments)},ie=b._emscripten_bind_OSBestResult_get_sconfidence_0=function(){return(ie=b._emscripten_bind_OSBestResult_get_sconfidence_0=b.asm.cc).apply(null,arguments)},je=b._emscripten_bind_OSBestResult_get_oconfidence_0=function(){return(je=b._emscripten_bind_OSBestResult_get_oconfidence_0=\nb.asm.dc).apply(null,arguments)},ke=b._emscripten_bind_OSBestResult___destroy___0=function(){return(ke=b._emscripten_bind_OSBestResult___destroy___0=b.asm.ec).apply(null,arguments)},le=b._emscripten_bind_Boxa_get_n_0=function(){return(le=b._emscripten_bind_Boxa_get_n_0=b.asm.fc).apply(null,arguments)},me=b._emscripten_bind_Boxa_get_nalloc_0=function(){return(me=b._emscripten_bind_Boxa_get_nalloc_0=b.asm.gc).apply(null,arguments)},ne=b._emscripten_bind_Boxa_get_refcount_0=function(){return(ne=b._emscripten_bind_Boxa_get_refcount_0=\nb.asm.hc).apply(null,arguments)},oe=b._emscripten_bind_Boxa_get_box_0=function(){return(oe=b._emscripten_bind_Boxa_get_box_0=b.asm.ic).apply(null,arguments)},pe=b._emscripten_bind_Boxa___destroy___0=function(){return(pe=b._emscripten_bind_Boxa___destroy___0=b.asm.jc).apply(null,arguments)},qe=b._emscripten_bind_PixColormap_get_array_0=function(){return(qe=b._emscripten_bind_PixColormap_get_array_0=b.asm.kc).apply(null,arguments)},re=b._emscripten_bind_PixColormap_get_depth_0=function(){return(re=\nb._emscripten_bind_PixColormap_get_depth_0=b.asm.lc).apply(null,arguments)},se=b._emscripten_bind_PixColormap_get_nalloc_0=function(){return(se=b._emscripten_bind_PixColormap_get_nalloc_0=b.asm.mc).apply(null,arguments)},te=b._emscripten_bind_PixColormap_get_n_0=function(){return(te=b._emscripten_bind_PixColormap_get_n_0=b.asm.nc).apply(null,arguments)},ue=b._emscripten_bind_PixColormap___destroy___0=function(){return(ue=b._emscripten_bind_PixColormap___destroy___0=b.asm.oc).apply(null,arguments)},\nve=b._emscripten_bind_Pta_get_n_0=function(){return(ve=b._emscripten_bind_Pta_get_n_0=b.asm.pc).apply(null,arguments)},we=b._emscripten_bind_Pta_get_nalloc_0=function(){return(we=b._emscripten_bind_Pta_get_nalloc_0=b.asm.qc).apply(null,arguments)},xe=b._emscripten_bind_Pta_get_refcount_0=function(){return(xe=b._emscripten_bind_Pta_get_refcount_0=b.asm.rc).apply(null,arguments)},ye=b._emscripten_bind_Pta_get_x_0=function(){return(ye=b._emscripten_bind_Pta_get_x_0=b.asm.sc).apply(null,arguments)},ze=\nb._emscripten_bind_Pta_get_y_0=function(){return(ze=b._emscripten_bind_Pta_get_y_0=b.asm.tc).apply(null,arguments)},Ae=b._emscripten_bind_Pta___destroy___0=function(){return(Ae=b._emscripten_bind_Pta___destroy___0=b.asm.uc).apply(null,arguments)},Be=b._emscripten_bind_Pix_get_w_0=function(){return(Be=b._emscripten_bind_Pix_get_w_0=b.asm.vc).apply(null,arguments)},Ce=b._emscripten_bind_Pix_get_h_0=function(){return(Ce=b._emscripten_bind_Pix_get_h_0=b.asm.wc).apply(null,arguments)},De=b._emscripten_bind_Pix_get_d_0=\nfunction(){return(De=b._emscripten_bind_Pix_get_d_0=b.asm.xc).apply(null,arguments)},Ee=b._emscripten_bind_Pix_get_spp_0=function(){return(Ee=b._emscripten_bind_Pix_get_spp_0=b.asm.yc).apply(null,arguments)},Fe=b._emscripten_bind_Pix_get_wpl_0=function(){return(Fe=b._emscripten_bind_Pix_get_wpl_0=b.asm.zc).apply(null,arguments)},Ge=b._emscripten_bind_Pix_get_refcount_0=function(){return(Ge=b._emscripten_bind_Pix_get_refcount_0=b.asm.Ac).apply(null,arguments)},He=b._emscripten_bind_Pix_get_xres_0=\nfunction(){return(He=b._emscripten_bind_Pix_get_xres_0=b.asm.Bc).apply(null,arguments)},Ie=b._emscripten_bind_Pix_get_yres_0=function(){return(Ie=b._emscripten_bind_Pix_get_yres_0=b.asm.Cc).apply(null,arguments)},Je=b._emscripten_bind_Pix_get_informat_0=function(){return(Je=b._emscripten_bind_Pix_get_informat_0=b.asm.Dc).apply(null,arguments)},Ke=b._emscripten_bind_Pix_get_special_0=function(){return(Ke=b._emscripten_bind_Pix_get_special_0=b.asm.Ec).apply(null,arguments)},Le=b._emscripten_bind_Pix_get_text_0=\nfunction(){return(Le=b._emscripten_bind_Pix_get_text_0=b.asm.Fc).apply(null,arguments)},Me=b._emscripten_bind_Pix_get_colormap_0=function(){return(Me=b._emscripten_bind_Pix_get_colormap_0=b.asm.Gc).apply(null,arguments)},Ne=b._emscripten_bind_Pix_get_data_0=function(){return(Ne=b._emscripten_bind_Pix_get_data_0=b.asm.Hc).apply(null,arguments)},Oe=b._emscripten_bind_Pix___destroy___0=function(){return(Oe=b._emscripten_bind_Pix___destroy___0=b.asm.Ic).apply(null,arguments)},Pe=b._emscripten_bind_DoublePtr___destroy___0=\nfunction(){return(Pe=b._emscripten_bind_DoublePtr___destroy___0=b.asm.Jc).apply(null,arguments)},Qe=b._emscripten_bind_Dawg___destroy___0=function(){return(Qe=b._emscripten_bind_Dawg___destroy___0=b.asm.Kc).apply(null,arguments)},Re=b._emscripten_bind_BoxPtr___destroy___0=function(){return(Re=b._emscripten_bind_BoxPtr___destroy___0=b.asm.Lc).apply(null,arguments)},Se=b._emscripten_bind_TessBaseAPI_TessBaseAPI_0=function(){return(Se=b._emscripten_bind_TessBaseAPI_TessBaseAPI_0=b.asm.Mc).apply(null,\narguments)},Te=b._emscripten_bind_TessBaseAPI_Version_0=function(){return(Te=b._emscripten_bind_TessBaseAPI_Version_0=b.asm.Nc).apply(null,arguments)},Ue=b._emscripten_bind_TessBaseAPI_SetInputName_1=function(){return(Ue=b._emscripten_bind_TessBaseAPI_SetInputName_1=b.asm.Oc).apply(null,arguments)},Ve=b._emscripten_bind_TessBaseAPI_GetInputName_0=function(){return(Ve=b._emscripten_bind_TessBaseAPI_GetInputName_0=b.asm.Pc).apply(null,arguments)},We=b._emscripten_bind_TessBaseAPI_SetInputImage_1=function(){return(We=\nb._emscripten_bind_TessBaseAPI_SetInputImage_1=b.asm.Qc).apply(null,arguments)},Xe=b._emscripten_bind_TessBaseAPI_GetInputImage_0=function(){return(Xe=b._emscripten_bind_TessBaseAPI_GetInputImage_0=b.asm.Rc).apply(null,arguments)},Ye=b._emscripten_bind_TessBaseAPI_GetSourceYResolution_0=function(){return(Ye=b._emscripten_bind_TessBaseAPI_GetSourceYResolution_0=b.asm.Sc).apply(null,arguments)},Ze=b._emscripten_bind_TessBaseAPI_GetDatapath_0=function(){return(Ze=b._emscripten_bind_TessBaseAPI_GetDatapath_0=\nb.asm.Tc).apply(null,arguments)},$e=b._emscripten_bind_TessBaseAPI_SetOutputName_1=function(){return($e=b._emscripten_bind_TessBaseAPI_SetOutputName_1=b.asm.Uc).apply(null,arguments)},af=b._emscripten_bind_TessBaseAPI_SetVariable_2=function(){return(af=b._emscripten_bind_TessBaseAPI_SetVariable_2=b.asm.Vc).apply(null,arguments)},bf=b._emscripten_bind_TessBaseAPI_SetDebugVariable_2=function(){return(bf=b._emscripten_bind_TessBaseAPI_SetDebugVariable_2=b.asm.Wc).apply(null,arguments)},cf=b._emscripten_bind_TessBaseAPI_GetIntVariable_2=\nfunction(){return(cf=b._emscripten_bind_TessBaseAPI_GetIntVariable_2=b.asm.Xc).apply(null,arguments)},df=b._emscripten_bind_TessBaseAPI_GetBoolVariable_2=function(){return(df=b._emscripten_bind_TessBaseAPI_GetBoolVariable_2=b.asm.Yc).apply(null,arguments)},ef=b._emscripten_bind_TessBaseAPI_GetDoubleVariable_2=function(){return(ef=b._emscripten_bind_TessBaseAPI_GetDoubleVariable_2=b.asm.Zc).apply(null,arguments)},ff=b._emscripten_bind_TessBaseAPI_GetStringVariable_1=function(){return(ff=b._emscripten_bind_TessBaseAPI_GetStringVariable_1=\nb.asm._c).apply(null,arguments)},gf=b._emscripten_bind_TessBaseAPI_SaveParameters_1=function(){return(gf=b._emscripten_bind_TessBaseAPI_SaveParameters_1=b.asm.$c).apply(null,arguments)},hf=b._emscripten_bind_TessBaseAPI_RestoreParameters_1=function(){return(hf=b._emscripten_bind_TessBaseAPI_RestoreParameters_1=b.asm.ad).apply(null,arguments)},jf=b._emscripten_bind_TessBaseAPI_Init_2=function(){return(jf=b._emscripten_bind_TessBaseAPI_Init_2=b.asm.bd).apply(null,arguments)},kf=b._emscripten_bind_TessBaseAPI_Init_3=\nfunction(){return(kf=b._emscripten_bind_TessBaseAPI_Init_3=b.asm.cd).apply(null,arguments)},lf=b._emscripten_bind_TessBaseAPI_Init_4=function(){return(lf=b._emscripten_bind_TessBaseAPI_Init_4=b.asm.dd).apply(null,arguments)},mf=b._emscripten_bind_TessBaseAPI_GetInitLanguagesAsString_0=function(){return(mf=b._emscripten_bind_TessBaseAPI_GetInitLanguagesAsString_0=b.asm.ed).apply(null,arguments)},nf=b._emscripten_bind_TessBaseAPI_InitForAnalysePage_0=function(){return(nf=b._emscripten_bind_TessBaseAPI_InitForAnalysePage_0=\nb.asm.fd).apply(null,arguments)},of=b._emscripten_bind_TessBaseAPI_ReadConfigFile_1=function(){return(of=b._emscripten_bind_TessBaseAPI_ReadConfigFile_1=b.asm.gd).apply(null,arguments)},pf=b._emscripten_bind_TessBaseAPI_ReadDebugConfigFile_1=function(){return(pf=b._emscripten_bind_TessBaseAPI_ReadDebugConfigFile_1=b.asm.hd).apply(null,arguments)},qf=b._emscripten_bind_TessBaseAPI_SetPageSegMode_1=function(){return(qf=b._emscripten_bind_TessBaseAPI_SetPageSegMode_1=b.asm.id).apply(null,arguments)},\nrf=b._emscripten_bind_TessBaseAPI_GetPageSegMode_0=function(){return(rf=b._emscripten_bind_TessBaseAPI_GetPageSegMode_0=b.asm.jd).apply(null,arguments)},sf=b._emscripten_bind_TessBaseAPI_TesseractRect_7=function(){return(sf=b._emscripten_bind_TessBaseAPI_TesseractRect_7=b.asm.kd).apply(null,arguments)},tf=b._emscripten_bind_TessBaseAPI_ClearAdaptiveClassifier_0=function(){return(tf=b._emscripten_bind_TessBaseAPI_ClearAdaptiveClassifier_0=b.asm.ld).apply(null,arguments)},uf=b._emscripten_bind_TessBaseAPI_SetImage_1=\nfunction(){return(uf=b._emscripten_bind_TessBaseAPI_SetImage_1=b.asm.md).apply(null,arguments)},vf=b._emscripten_bind_TessBaseAPI_SetImage_5=function(){return(vf=b._emscripten_bind_TessBaseAPI_SetImage_5=b.asm.nd).apply(null,arguments)},wf=b._emscripten_bind_TessBaseAPI_SetImageFile_1=function(){return(wf=b._emscripten_bind_TessBaseAPI_SetImageFile_1=b.asm.od).apply(null,arguments)},xf=b._emscripten_bind_TessBaseAPI_SetSourceResolution_1=function(){return(xf=b._emscripten_bind_TessBaseAPI_SetSourceResolution_1=\nb.asm.pd).apply(null,arguments)},yf=b._emscripten_bind_TessBaseAPI_SetRectangle_4=function(){return(yf=b._emscripten_bind_TessBaseAPI_SetRectangle_4=b.asm.qd).apply(null,arguments)},zf=b._emscripten_bind_TessBaseAPI_GetThresholdedImage_0=function(){return(zf=b._emscripten_bind_TessBaseAPI_GetThresholdedImage_0=b.asm.rd).apply(null,arguments)},Af=b._emscripten_bind_TessBaseAPI_WriteImage_0=function(){return(Af=b._emscripten_bind_TessBaseAPI_WriteImage_0=b.asm.sd).apply(null,arguments)},Bf=b._emscripten_bind_TessBaseAPI_FindLines_0=\nfunction(){return(Bf=b._emscripten_bind_TessBaseAPI_FindLines_0=b.asm.td).apply(null,arguments)},Cf=b._emscripten_bind_TessBaseAPI_GetGradient_0=function(){return(Cf=b._emscripten_bind_TessBaseAPI_GetGradient_0=b.asm.ud).apply(null,arguments)},Df=b._emscripten_bind_TessBaseAPI_GetRegions_1=function(){return(Df=b._emscripten_bind_TessBaseAPI_GetRegions_1=b.asm.vd).apply(null,arguments)},Ef=b._emscripten_bind_TessBaseAPI_GetTextlines_2=function(){return(Ef=b._emscripten_bind_TessBaseAPI_GetTextlines_2=\nb.asm.wd).apply(null,arguments)},Ff=b._emscripten_bind_TessBaseAPI_GetTextlines_5=function(){return(Ff=b._emscripten_bind_TessBaseAPI_GetTextlines_5=b.asm.xd).apply(null,arguments)},Gf=b._emscripten_bind_TessBaseAPI_GetStrips_2=function(){return(Gf=b._emscripten_bind_TessBaseAPI_GetStrips_2=b.asm.yd).apply(null,arguments)},Hf=b._emscripten_bind_TessBaseAPI_GetWords_1=function(){return(Hf=b._emscripten_bind_TessBaseAPI_GetWords_1=b.asm.zd).apply(null,arguments)},If=b._emscripten_bind_TessBaseAPI_GetConnectedComponents_1=\nfunction(){return(If=b._emscripten_bind_TessBaseAPI_GetConnectedComponents_1=b.asm.Ad).apply(null,arguments)},Jf=b._emscripten_bind_TessBaseAPI_GetComponentImages_4=function(){return(Jf=b._emscripten_bind_TessBaseAPI_GetComponentImages_4=b.asm.Bd).apply(null,arguments)},Kf=b._emscripten_bind_TessBaseAPI_GetComponentImages_7=function(){return(Kf=b._emscripten_bind_TessBaseAPI_GetComponentImages_7=b.asm.Cd).apply(null,arguments)},Lf=b._emscripten_bind_TessBaseAPI_GetThresholdedImageScaleFactor_0=function(){return(Lf=\nb._emscripten_bind_TessBaseAPI_GetThresholdedImageScaleFactor_0=b.asm.Dd).apply(null,arguments)},Mf=b._emscripten_bind_TessBaseAPI_AnalyseLayout_0=function(){return(Mf=b._emscripten_bind_TessBaseAPI_AnalyseLayout_0=b.asm.Ed).apply(null,arguments)},Nf=b._emscripten_bind_TessBaseAPI_AnalyseLayout_1=function(){return(Nf=b._emscripten_bind_TessBaseAPI_AnalyseLayout_1=b.asm.Fd).apply(null,arguments)},Of=b._emscripten_bind_TessBaseAPI_Recognize_1=function(){return(Of=b._emscripten_bind_TessBaseAPI_Recognize_1=\nb.asm.Gd).apply(null,arguments)},Pf=b._emscripten_bind_TessBaseAPI_ProcessPages_4=function(){return(Pf=b._emscripten_bind_TessBaseAPI_ProcessPages_4=b.asm.Hd).apply(null,arguments)},Qf=b._emscripten_bind_TessBaseAPI_ProcessPage_6=function(){return(Qf=b._emscripten_bind_TessBaseAPI_ProcessPage_6=b.asm.Id).apply(null,arguments)},Rf=b._emscripten_bind_TessBaseAPI_GetIterator_0=function(){return(Rf=b._emscripten_bind_TessBaseAPI_GetIterator_0=b.asm.Jd).apply(null,arguments)},Sf=b._emscripten_bind_TessBaseAPI_GetUTF8Text_0=\nfunction(){return(Sf=b._emscripten_bind_TessBaseAPI_GetUTF8Text_0=b.asm.Kd).apply(null,arguments)},Tf=b._emscripten_bind_TessBaseAPI_GetHOCRText_1=function(){return(Tf=b._emscripten_bind_TessBaseAPI_GetHOCRText_1=b.asm.Ld).apply(null,arguments)},Uf=b._emscripten_bind_TessBaseAPI_GetTSVText_1=function(){return(Uf=b._emscripten_bind_TessBaseAPI_GetTSVText_1=b.asm.Md).apply(null,arguments)},Vf=b._emscripten_bind_TessBaseAPI_GetBoxText_1=function(){return(Vf=b._emscripten_bind_TessBaseAPI_GetBoxText_1=\nb.asm.Nd).apply(null,arguments)},Wf=b._emscripten_bind_TessBaseAPI_GetUNLVText_0=function(){return(Wf=b._emscripten_bind_TessBaseAPI_GetUNLVText_0=b.asm.Od).apply(null,arguments)},Xf=b._emscripten_bind_TessBaseAPI_GetOsdText_1=function(){return(Xf=b._emscripten_bind_TessBaseAPI_GetOsdText_1=b.asm.Pd).apply(null,arguments)},Yf=b._emscripten_bind_TessBaseAPI_MeanTextConf_0=function(){return(Yf=b._emscripten_bind_TessBaseAPI_MeanTextConf_0=b.asm.Qd).apply(null,arguments)},Zf=b._emscripten_bind_TessBaseAPI_AllWordConfidences_0=\nfunction(){return(Zf=b._emscripten_bind_TessBaseAPI_AllWordConfidences_0=b.asm.Rd).apply(null,arguments)},$f=b._emscripten_bind_TessBaseAPI_AdaptToWordStr_2=function(){return($f=b._emscripten_bind_TessBaseAPI_AdaptToWordStr_2=b.asm.Sd).apply(null,arguments)},ag=b._emscripten_bind_TessBaseAPI_Clear_0=function(){return(ag=b._emscripten_bind_TessBaseAPI_Clear_0=b.asm.Td).apply(null,arguments)},bg=b._emscripten_bind_TessBaseAPI_End_0=function(){return(bg=b._emscripten_bind_TessBaseAPI_End_0=b.asm.Ud).apply(null,\narguments)},cg=b._emscripten_bind_TessBaseAPI_ClearPersistentCache_0=function(){return(cg=b._emscripten_bind_TessBaseAPI_ClearPersistentCache_0=b.asm.Vd).apply(null,arguments)},dg=b._emscripten_bind_TessBaseAPI_IsValidWord_1=function(){return(dg=b._emscripten_bind_TessBaseAPI_IsValidWord_1=b.asm.Wd).apply(null,arguments)},eg=b._emscripten_bind_TessBaseAPI_IsValidCharacter_1=function(){return(eg=b._emscripten_bind_TessBaseAPI_IsValidCharacter_1=b.asm.Xd).apply(null,arguments)},fg=b._emscripten_bind_TessBaseAPI_DetectOS_1=\nfunction(){return(fg=b._emscripten_bind_TessBaseAPI_DetectOS_1=b.asm.Yd).apply(null,arguments)},gg=b._emscripten_bind_TessBaseAPI_GetUnichar_1=function(){return(gg=b._emscripten_bind_TessBaseAPI_GetUnichar_1=b.asm.Zd).apply(null,arguments)},hg=b._emscripten_bind_TessBaseAPI_GetDawg_1=function(){return(hg=b._emscripten_bind_TessBaseAPI_GetDawg_1=b.asm._d).apply(null,arguments)},ig=b._emscripten_bind_TessBaseAPI_NumDawgs_0=function(){return(ig=b._emscripten_bind_TessBaseAPI_NumDawgs_0=b.asm.$d).apply(null,\narguments)},jg=b._emscripten_bind_TessBaseAPI_oem_0=function(){return(jg=b._emscripten_bind_TessBaseAPI_oem_0=b.asm.ae).apply(null,arguments)},kg=b._emscripten_bind_TessBaseAPI___destroy___0=function(){return(kg=b._emscripten_bind_TessBaseAPI___destroy___0=b.asm.be).apply(null,arguments)},lg=b._emscripten_bind_OSResults_OSResults_0=function(){return(lg=b._emscripten_bind_OSResults_OSResults_0=b.asm.ce).apply(null,arguments)},mg=b._emscripten_bind_OSResults_print_scores_0=function(){return(mg=b._emscripten_bind_OSResults_print_scores_0=\nb.asm.de).apply(null,arguments)},ng=b._emscripten_bind_OSResults_get_best_result_0=function(){return(ng=b._emscripten_bind_OSResults_get_best_result_0=b.asm.ee).apply(null,arguments)},og=b._emscripten_bind_OSResults_get_unicharset_0=function(){return(og=b._emscripten_bind_OSResults_get_unicharset_0=b.asm.fe).apply(null,arguments)},pg=b._emscripten_bind_OSResults___destroy___0=function(){return(pg=b._emscripten_bind_OSResults___destroy___0=b.asm.ge).apply(null,arguments)},qg=b._emscripten_bind_Pixa_get_n_0=\nfunction(){return(qg=b._emscripten_bind_Pixa_get_n_0=b.asm.he).apply(null,arguments)},rg=b._emscripten_bind_Pixa_get_nalloc_0=function(){return(rg=b._emscripten_bind_Pixa_get_nalloc_0=b.asm.ie).apply(null,arguments)},sg=b._emscripten_bind_Pixa_get_refcount_0=function(){return(sg=b._emscripten_bind_Pixa_get_refcount_0=b.asm.je).apply(null,arguments)},tg=b._emscripten_bind_Pixa_get_pix_0=function(){return(tg=b._emscripten_bind_Pixa_get_pix_0=b.asm.ke).apply(null,arguments)},ug=b._emscripten_bind_Pixa_get_boxa_0=\nfunction(){return(ug=b._emscripten_bind_Pixa_get_boxa_0=b.asm.le).apply(null,arguments)},vg=b._emscripten_bind_Pixa___destroy___0=function(){return(vg=b._emscripten_bind_Pixa___destroy___0=b.asm.me).apply(null,arguments)},wg=b._emscripten_enum_PageIteratorLevel_RIL_BLOCK=function(){return(wg=b._emscripten_enum_PageIteratorLevel_RIL_BLOCK=b.asm.ne).apply(null,arguments)},xg=b._emscripten_enum_PageIteratorLevel_RIL_PARA=function(){return(xg=b._emscripten_enum_PageIteratorLevel_RIL_PARA=b.asm.oe).apply(null,\narguments)},yg=b._emscripten_enum_PageIteratorLevel_RIL_TEXTLINE=function(){return(yg=b._emscripten_enum_PageIteratorLevel_RIL_TEXTLINE=b.asm.pe).apply(null,arguments)},zg=b._emscripten_enum_PageIteratorLevel_RIL_WORD=function(){return(zg=b._emscripten_enum_PageIteratorLevel_RIL_WORD=b.asm.qe).apply(null,arguments)},Ag=b._emscripten_enum_PageIteratorLevel_RIL_SYMBOL=function(){return(Ag=b._emscripten_enum_PageIteratorLevel_RIL_SYMBOL=b.asm.re).apply(null,arguments)},Bg=b._emscripten_enum_OcrEngineMode_OEM_TESSERACT_ONLY=\nfunction(){return(Bg=b._emscripten_enum_OcrEngineMode_OEM_TESSERACT_ONLY=b.asm.se).apply(null,arguments)},Cg=b._emscripten_enum_OcrEngineMode_OEM_LSTM_ONLY=function(){return(Cg=b._emscripten_enum_OcrEngineMode_OEM_LSTM_ONLY=b.asm.te).apply(null,arguments)},Dg=b._emscripten_enum_OcrEngineMode_OEM_TESSERACT_LSTM_COMBINED=function(){return(Dg=b._emscripten_enum_OcrEngineMode_OEM_TESSERACT_LSTM_COMBINED=b.asm.ue).apply(null,arguments)},Eg=b._emscripten_enum_OcrEngineMode_OEM_DEFAULT=function(){return(Eg=\nb._emscripten_enum_OcrEngineMode_OEM_DEFAULT=b.asm.ve).apply(null,arguments)},Fg=b._emscripten_enum_OcrEngineMode_OEM_COUNT=function(){return(Fg=b._emscripten_enum_OcrEngineMode_OEM_COUNT=b.asm.we).apply(null,arguments)},Gg=b._emscripten_enum_WritingDirection__WRITING_DIRECTION_LEFT_TO_RIGHT=function(){return(Gg=b._emscripten_enum_WritingDirection__WRITING_DIRECTION_LEFT_TO_RIGHT=b.asm.xe).apply(null,arguments)},Hg=b._emscripten_enum_WritingDirection__WRITING_DIRECTION_RIGHT_TO_LEFT=function(){return(Hg=\nb._emscripten_enum_WritingDirection__WRITING_DIRECTION_RIGHT_TO_LEFT=b.asm.ye).apply(null,arguments)},Ig=b._emscripten_enum_WritingDirection__WRITING_DIRECTION_TOP_TO_BOTTOM=function(){return(Ig=b._emscripten_enum_WritingDirection__WRITING_DIRECTION_TOP_TO_BOTTOM=b.asm.ze).apply(null,arguments)},Jg=b._emscripten_enum_PolyBlockType_PT_UNKNOWN=function(){return(Jg=b._emscripten_enum_PolyBlockType_PT_UNKNOWN=b.asm.Ae).apply(null,arguments)},Kg=b._emscripten_enum_PolyBlockType_PT_FLOWING_TEXT=function(){return(Kg=\nb._emscripten_enum_PolyBlockType_PT_FLOWING_TEXT=b.asm.Be).apply(null,arguments)},Lg=b._emscripten_enum_PolyBlockType_PT_HEADING_TEXT=function(){return(Lg=b._emscripten_enum_PolyBlockType_PT_HEADING_TEXT=b.asm.Ce).apply(null,arguments)},Mg=b._emscripten_enum_PolyBlockType_PT_PULLOUT_TEXT=function(){return(Mg=b._emscripten_enum_PolyBlockType_PT_PULLOUT_TEXT=b.asm.De).apply(null,arguments)},Ng=b._emscripten_enum_PolyBlockType_PT_EQUATION=function(){return(Ng=b._emscripten_enum_PolyBlockType_PT_EQUATION=\nb.asm.Ee).apply(null,arguments)},Og=b._emscripten_enum_PolyBlockType_PT_INLINE_EQUATION=function(){return(Og=b._emscripten_enum_PolyBlockType_PT_INLINE_EQUATION=b.asm.Fe).apply(null,arguments)},Pg=b._emscripten_enum_PolyBlockType_PT_TABLE=function(){return(Pg=b._emscripten_enum_PolyBlockType_PT_TABLE=b.asm.Ge).apply(null,arguments)},Qg=b._emscripten_enum_PolyBlockType_PT_VERTICAL_TEXT=function(){return(Qg=b._emscripten_enum_PolyBlockType_PT_VERTICAL_TEXT=b.asm.He).apply(null,arguments)},Rg=b._emscripten_enum_PolyBlockType_PT_CAPTION_TEXT=\nfunction(){return(Rg=b._emscripten_enum_PolyBlockType_PT_CAPTION_TEXT=b.asm.Ie).apply(null,arguments)},Sg=b._emscripten_enum_PolyBlockType_PT_FLOWING_IMAGE=function(){return(Sg=b._emscripten_enum_PolyBlockType_PT_FLOWING_IMAGE=b.asm.Je).apply(null,arguments)},Tg=b._emscripten_enum_PolyBlockType_PT_HEADING_IMAGE=function(){return(Tg=b._emscripten_enum_PolyBlockType_PT_HEADING_IMAGE=b.asm.Ke).apply(null,arguments)},Ug=b._emscripten_enum_PolyBlockType_PT_PULLOUT_IMAGE=function(){return(Ug=b._emscripten_enum_PolyBlockType_PT_PULLOUT_IMAGE=\nb.asm.Le).apply(null,arguments)},Vg=b._emscripten_enum_PolyBlockType_PT_HORZ_LINE=function(){return(Vg=b._emscripten_enum_PolyBlockType_PT_HORZ_LINE=b.asm.Me).apply(null,arguments)},Wg=b._emscripten_enum_PolyBlockType_PT_VERT_LINE=function(){return(Wg=b._emscripten_enum_PolyBlockType_PT_VERT_LINE=b.asm.Ne).apply(null,arguments)},Xg=b._emscripten_enum_PolyBlockType_PT_NOISE=function(){return(Xg=b._emscripten_enum_PolyBlockType_PT_NOISE=b.asm.Oe).apply(null,arguments)},Yg=b._emscripten_enum_PolyBlockType_PT_COUNT=\nfunction(){return(Yg=b._emscripten_enum_PolyBlockType_PT_COUNT=b.asm.Pe).apply(null,arguments)},Zg=b._emscripten_enum_StrongScriptDirection_DIR_NEUTRAL=function(){return(Zg=b._emscripten_enum_StrongScriptDirection_DIR_NEUTRAL=b.asm.Qe).apply(null,arguments)},$g=b._emscripten_enum_StrongScriptDirection_DIR_LEFT_TO_RIGHT=function(){return($g=b._emscripten_enum_StrongScriptDirection_DIR_LEFT_TO_RIGHT=b.asm.Re).apply(null,arguments)},ah=b._emscripten_enum_StrongScriptDirection_DIR_RIGHT_TO_LEFT=function(){return(ah=\nb._emscripten_enum_StrongScriptDirection_DIR_RIGHT_TO_LEFT=b.asm.Se).apply(null,arguments)},bh=b._emscripten_enum_StrongScriptDirection_DIR_MIX=function(){return(bh=b._emscripten_enum_StrongScriptDirection_DIR_MIX=b.asm.Te).apply(null,arguments)},ch=b._emscripten_enum_ParagraphJustification__JUSTIFICATION_UNKNOWN=function(){return(ch=b._emscripten_enum_ParagraphJustification__JUSTIFICATION_UNKNOWN=b.asm.Ue).apply(null,arguments)},dh=b._emscripten_enum_ParagraphJustification__JUSTIFICATION_LEFT=function(){return(dh=\nb._emscripten_enum_ParagraphJustification__JUSTIFICATION_LEFT=b.asm.Ve).apply(null,arguments)},eh=b._emscripten_enum_ParagraphJustification__JUSTIFICATION_CENTER=function(){return(eh=b._emscripten_enum_ParagraphJustification__JUSTIFICATION_CENTER=b.asm.We).apply(null,arguments)},fh=b._emscripten_enum_ParagraphJustification__JUSTIFICATION_RIGHT=function(){return(fh=b._emscripten_enum_ParagraphJustification__JUSTIFICATION_RIGHT=b.asm.Xe).apply(null,arguments)},gh=b._emscripten_enum_TextlineOrder__TEXTLINE_ORDER_LEFT_TO_RIGHT=\nfunction(){return(gh=b._emscripten_enum_TextlineOrder__TEXTLINE_ORDER_LEFT_TO_RIGHT=b.asm.Ye).apply(null,arguments)},hh=b._emscripten_enum_TextlineOrder__TEXTLINE_ORDER_RIGHT_TO_LEFT=function(){return(hh=b._emscripten_enum_TextlineOrder__TEXTLINE_ORDER_RIGHT_TO_LEFT=b.asm.Ze).apply(null,arguments)},ih=b._emscripten_enum_TextlineOrder__TEXTLINE_ORDER_TOP_TO_BOTTOM=function(){return(ih=b._emscripten_enum_TextlineOrder__TEXTLINE_ORDER_TOP_TO_BOTTOM=b.asm._e).apply(null,arguments)},jh=b._emscripten_enum_Orientation__ORIENTATION_PAGE_UP=\nfunction(){return(jh=b._emscripten_enum_Orientation__ORIENTATION_PAGE_UP=b.asm.$e).apply(null,arguments)},kh=b._emscripten_enum_Orientation__ORIENTATION_PAGE_RIGHT=function(){return(kh=b._emscripten_enum_Orientation__ORIENTATION_PAGE_RIGHT=b.asm.af).apply(null,arguments)},lh=b._emscripten_enum_Orientation__ORIENTATION_PAGE_DOWN=function(){return(lh=b._emscripten_enum_Orientation__ORIENTATION_PAGE_DOWN=b.asm.bf).apply(null,arguments)},mh=b._emscripten_enum_Orientation__ORIENTATION_PAGE_LEFT=function(){return(mh=\nb._emscripten_enum_Orientation__ORIENTATION_PAGE_LEFT=b.asm.cf).apply(null,arguments)},nh=b._emscripten_enum_PageSegMode_PSM_OSD_ONLY=function(){return(nh=b._emscripten_enum_PageSegMode_PSM_OSD_ONLY=b.asm.df).apply(null,arguments)},oh=b._emscripten_enum_PageSegMode_PSM_AUTO_OSD=function(){return(oh=b._emscripten_enum_PageSegMode_PSM_AUTO_OSD=b.asm.ef).apply(null,arguments)},ph=b._emscripten_enum_PageSegMode_PSM_AUTO_ONLY=function(){return(ph=b._emscripten_enum_PageSegMode_PSM_AUTO_ONLY=b.asm.ff).apply(null,\narguments)},qh=b._emscripten_enum_PageSegMode_PSM_AUTO=function(){return(qh=b._emscripten_enum_PageSegMode_PSM_AUTO=b.asm.gf).apply(null,arguments)},rh=b._emscripten_enum_PageSegMode_PSM_SINGLE_COLUMN=function(){return(rh=b._emscripten_enum_PageSegMode_PSM_SINGLE_COLUMN=b.asm.hf).apply(null,arguments)},sh=b._emscripten_enum_PageSegMode_PSM_SINGLE_BLOCK_VERT_TEXT=function(){return(sh=b._emscripten_enum_PageSegMode_PSM_SINGLE_BLOCK_VERT_TEXT=b.asm.jf).apply(null,arguments)},th=b._emscripten_enum_PageSegMode_PSM_SINGLE_BLOCK=\nfunction(){return(th=b._emscripten_enum_PageSegMode_PSM_SINGLE_BLOCK=b.asm.kf).apply(null,arguments)},uh=b._emscripten_enum_PageSegMode_PSM_SINGLE_LINE=function(){return(uh=b._emscripten_enum_PageSegMode_PSM_SINGLE_LINE=b.asm.lf).apply(null,arguments)},vh=b._emscripten_enum_PageSegMode_PSM_SINGLE_WORD=function(){return(vh=b._emscripten_enum_PageSegMode_PSM_SINGLE_WORD=b.asm.mf).apply(null,arguments)},wh=b._emscripten_enum_PageSegMode_PSM_CIRCLE_WORD=function(){return(wh=b._emscripten_enum_PageSegMode_PSM_CIRCLE_WORD=\nb.asm.nf).apply(null,arguments)},xh=b._emscripten_enum_PageSegMode_PSM_SINGLE_CHAR=function(){return(xh=b._emscripten_enum_PageSegMode_PSM_SINGLE_CHAR=b.asm.of).apply(null,arguments)},yh=b._emscripten_enum_PageSegMode_PSM_SPARSE_TEXT=function(){return(yh=b._emscripten_enum_PageSegMode_PSM_SPARSE_TEXT=b.asm.pf).apply(null,arguments)},zh=b._emscripten_enum_PageSegMode_PSM_SPARSE_TEXT_OSD=function(){return(zh=b._emscripten_enum_PageSegMode_PSM_SPARSE_TEXT_OSD=b.asm.qf).apply(null,arguments)},Ah=b._emscripten_enum_PageSegMode_PSM_RAW_LINE=\nfunction(){return(Ah=b._emscripten_enum_PageSegMode_PSM_RAW_LINE=b.asm.rf).apply(null,arguments)},Bh=b._emscripten_enum_PageSegMode_PSM_COUNT=function(){return(Bh=b._emscripten_enum_PageSegMode_PSM_COUNT=b.asm.sf).apply(null,arguments)};b._pixDestroy=function(){return(b._pixDestroy=b.asm.uf).apply(null,arguments)};b._ptaDestroy=function(){return(b._ptaDestroy=b.asm.vf).apply(null,arguments)};b._pixaDestroy=function(){return(b._pixaDestroy=b.asm.wf).apply(null,arguments)};\nb._boxaDestroy=function(){return(b._boxaDestroy=b.asm.xf).apply(null,arguments)};b._pixReadMem=function(){return(b._pixReadMem=b.asm.yf).apply(null,arguments)};var Ob=b.___errno_location=function(){return(Ob=b.___errno_location=b.asm.zf).apply(null,arguments)},Ch=b._free=function(){return(Ch=b._free=b.asm.Af).apply(null,arguments)},zb=b._malloc=function(){return(zb=b._malloc=b.asm.Bf).apply(null,arguments)};b._pixReadHeaderMem=function(){return(b._pixReadHeaderMem=b.asm.Cf).apply(null,arguments)};\nvar qb=b._emscripten_builtin_memalign=function(){return(qb=b._emscripten_builtin_memalign=b.asm.Df).apply(null,arguments)},F=b._setThrew=function(){return(F=b._setThrew=b.asm.Ef).apply(null,arguments)},Dh=b.stackSave=function(){return(Dh=b.stackSave=b.asm.Ff).apply(null,arguments)},Eh=b.stackRestore=function(){return(Eh=b.stackRestore=b.asm.Gf).apply(null,arguments)};b.___cxa_is_pointer_type=function(){return(b.___cxa_is_pointer_type=b.asm.Hf).apply(null,arguments)};\nb.___emscripten_embedded_file_data=600112;function Rb(a,c,d,e){var g=Dh();try{return Mb(a)(c,d,e)}catch(h){Eh(g);if(h!==h+0)throw h;F(1,0)}}function Ub(a,c){var d=Dh();try{Mb(a)(c)}catch(e){Eh(d);if(e!==e+0)throw e;F(1,0)}}function Pb(a,c){var d=Dh();try{return Mb(a)(c)}catch(e){Eh(d);if(e!==e+0)throw e;F(1,0)}}function Wb(a,c,d,e){var g=Dh();try{Mb(a)(c,d,e)}catch(h){Eh(g);if(h!==h+0)throw h;F(1,0)}}function Vb(a,c,d){var e=Dh();try{Mb(a)(c,d)}catch(g){Eh(e);if(g!==g+0)throw g;F(1,0)}}\nfunction Qb(a,c,d){var e=Dh();try{return Mb(a)(c,d)}catch(g){Eh(e);if(g!==g+0)throw g;F(1,0)}}function Sb(a,c,d,e,g){var h=Dh();try{return Mb(a)(c,d,e,g)}catch(k){Eh(h);if(k!==k+0)throw k;F(1,0)}}function Xb(a,c,d,e,g){var h=Dh();try{Mb(a)(c,d,e,g)}catch(k){Eh(h);if(k!==k+0)throw k;F(1,0)}}function Tb(a,c,d,e,g,h){var k=Dh();try{return Mb(a)(c,d,e,g,h)}catch(m){Eh(k);if(m!==m+0)throw m;F(1,0)}}\nfunction Zb(a,c,d,e,g,h,k,m,t,u){var p=Dh();try{Mb(a)(c,d,e,g,h,k,m,t,u)}catch(x){Eh(p);if(x!==x+0)throw x;F(1,0)}}function Yb(a,c,d,e,g,h){var k=Dh();try{Mb(a)(c,d,e,g,h)}catch(m){Eh(k);if(m!==m+0)throw m;F(1,0)}}b.addRunDependency=Qa;b.removeRunDependency=Ra;b.FS_createPath=D.Lg;b.FS_createDataFile=D.wg;b.FS_createPreloadedFile=D.oh;b.FS_createLazyFile=D.nh;b.FS_createDevice=D.Wf;b.FS_unlink=D.unlink;b.setValue=Za;b.getValue=Ya;b.FS=D;var Fh;Pa=function Gh(){Fh||Hh();Fh||(Pa=Gh)};\nfunction Hh(){function a(){if(!Fh&&(Fh=!0,b.calledRun=!0,!ua)){La=!0;b.noFSInit||D.rg.Xg||D.rg();D.Ah=!1;Xa(Ia);aa(b);if(b.onRuntimeInitialized)b.onRuntimeInitialized();if(b.postRun)for(\"function\"==typeof b.postRun&&(b.postRun=[b.postRun]);b.postRun.length;){var c=b.postRun.shift();Ka.unshift(c)}Xa(Ka)}}if(!(0<Na)){if(b.preRun)for(\"function\"==typeof b.preRun&&(b.preRun=[b.preRun]);b.preRun.length;)Ma();Xa(Ha);0<Na||(b.setStatus?(b.setStatus(\"Running...\"),setTimeout(function(){setTimeout(function(){b.setStatus(\"\")},\n1);a()},1)):a())}}if(b.preInit)for(\"function\"==typeof b.preInit&&(b.preInit=[b.preInit]);0<b.preInit.length;)b.preInit.pop()();Hh();function G(){}G.prototype=Object.create(G.prototype);G.prototype.constructor=G;G.prototype.Nf=G;G.Of={};b.WrapperObject=G;function Ih(a){return(a||G).Of}b.getCache=Ih;function H(a,c){var d=Ih(c),e=d[a];if(e)return e;e=Object.create((c||G).prototype);e.If=a;return d[a]=e}b.wrapPointer=H;b.castObject=function(a,c){return H(a.If,c)};b.NULL=H(0);\nb.destroy=function(a){if(!a.__destroy__)throw\"Error: Cannot destroy object. (Did you create it yourself?)\";a.__destroy__();delete Ih(a.Nf)[a.If]};b.compare=function(a,c){return a.If===c.If};b.getPointer=function(a){return a.If};b.getClass=function(a){return a.Nf};var Jh=0,Kh=0,Lh=0,Mh=[],Nh=0;function I(){if(Nh){for(var a=0;a<Mh.length;a++)b._free(Mh[a]);Mh.length=0;b._free(Jh);Jh=0;Kh+=Nh;Nh=0}Jh||(Kh+=128,(Jh=b._malloc(Kh))||n());Lh=0}\nfunction J(a){if(\"string\"===typeof a){a=jb(a);var c=r;Jh||n();c=a.length*c.BYTES_PER_ELEMENT;c=c+7&-8;if(Lh+c>=Kh){0<c||n();Nh+=c;var d=b._malloc(c);Mh.push(d)}else d=Jh+Lh,Lh+=c;c=d;d=r;var e=c;switch(d.BYTES_PER_ELEMENT){case 2:e>>=1;break;case 4:e>>=2;break;case 8:e>>=3}for(var g=0;g<a.length;g++)d[e+g]=a[g];return c}return a}function Oh(){throw\"cannot construct a ParagraphJustification, no constructor in IDL\";}Oh.prototype=Object.create(G.prototype);Oh.prototype.constructor=Oh;\nOh.prototype.Nf=Oh;Oh.Of={};b.ParagraphJustification=Oh;Oh.prototype.__destroy__=function(){ac(this.If)};function Ph(){throw\"cannot construct a BoolPtr, no constructor in IDL\";}Ph.prototype=Object.create(G.prototype);Ph.prototype.constructor=Ph;Ph.prototype.Nf=Ph;Ph.Of={};b.BoolPtr=Ph;Ph.prototype.__destroy__=function(){bc(this.If)};function K(){throw\"cannot construct a TessResultRenderer, no constructor in IDL\";}K.prototype=Object.create(G.prototype);K.prototype.constructor=K;K.prototype.Nf=K;\nK.Of={};b.TessResultRenderer=K;K.prototype.BeginDocument=function(a){var c=this.If;I();a=a&&\"object\"===typeof a?a.If:J(a);return!!cc(c,a)};K.prototype.AddImage=function(a){var c=this.If;a&&\"object\"===typeof a&&(a=a.If);return!!dc(c,a)};K.prototype.EndDocument=function(){return!!ec(this.If)};K.prototype.happy=function(){return!!fc(this.If)};K.prototype.file_extension=function(){return q(gc(this.If))};K.prototype.title=K.prototype.title=function(){return q(hc(this.If))};K.prototype.imagenum=function(){return ic(this.If)};\nK.prototype.__destroy__=function(){jc(this.If)};function Qh(){throw\"cannot construct a LongStarPtr, no constructor in IDL\";}Qh.prototype=Object.create(G.prototype);Qh.prototype.constructor=Qh;Qh.prototype.Nf=Qh;Qh.Of={};b.LongStarPtr=Qh;Qh.prototype.__destroy__=function(){kc(this.If)};function Rh(){throw\"cannot construct a VoidPtr, no constructor in IDL\";}Rh.prototype=Object.create(G.prototype);Rh.prototype.constructor=Rh;Rh.prototype.Nf=Rh;Rh.Of={};b.VoidPtr=Rh;Rh.prototype.__destroy__=function(){lc(this.If)};\nfunction L(a){a&&\"object\"===typeof a&&(a=a.If);this.If=mc(a);Ih(L)[this.If]=this}L.prototype=Object.create(G.prototype);L.prototype.constructor=L;L.prototype.Nf=L;L.Of={};b.ResultIterator=L;L.prototype.Begin=function(){nc(this.If)};L.prototype.RestartParagraph=function(){oc(this.If)};L.prototype.IsWithinFirstTextlineOfParagraph=function(){return!!pc(this.If)};L.prototype.RestartRow=function(){qc(this.If)};L.prototype.Next=function(a){var c=this.If;a&&\"object\"===typeof a&&(a=a.If);return!!rc(c,a)};\nL.prototype.IsAtBeginningOf=function(a){var c=this.If;a&&\"object\"===typeof a&&(a=a.If);return!!sc(c,a)};L.prototype.IsAtFinalElement=function(a,c){var d=this.If;a&&\"object\"===typeof a&&(a=a.If);c&&\"object\"===typeof c&&(c=c.If);return!!tc(d,a,c)};L.prototype.Cmp=function(a){var c=this.If;a&&\"object\"===typeof a&&(a=a.If);return uc(c,a)};L.prototype.SetBoundingBoxComponents=function(a,c){var d=this.If;a&&\"object\"===typeof a&&(a=a.If);c&&\"object\"===typeof c&&(c=c.If);vc(d,a,c)};\nL.prototype.BoundingBox=function(a,c,d,e,g,h){var k=this.If;a&&\"object\"===typeof a&&(a=a.If);c&&\"object\"===typeof c&&(c=c.If);d&&\"object\"===typeof d&&(d=d.If);e&&\"object\"===typeof e&&(e=e.If);g&&\"object\"===typeof g&&(g=g.If);h&&\"object\"===typeof h&&(h=h.If);return void 0===h?!!wc(k,a,c,d,e,g):!!xc(k,a,c,d,e,g,h)};\nL.prototype.BoundingBoxInternal=function(a,c,d,e,g){var h=this.If;a&&\"object\"===typeof a&&(a=a.If);c&&\"object\"===typeof c&&(c=c.If);d&&\"object\"===typeof d&&(d=d.If);e&&\"object\"===typeof e&&(e=e.If);g&&\"object\"===typeof g&&(g=g.If);return!!yc(h,a,c,d,e,g)};L.prototype.Empty=function(a){var c=this.If;a&&\"object\"===typeof a&&(a=a.If);return!!zc(c,a)};L.prototype.BlockType=function(){return Ac(this.If)};L.prototype.BlockPolygon=function(){return H(Bc(this.If),M)};\nL.prototype.GetBinaryImage=function(a){var c=this.If;a&&\"object\"===typeof a&&(a=a.If);return H(Cc(c,a),O)};L.prototype.GetImage=function(a,c,d,e,g){var h=this.If;a&&\"object\"===typeof a&&(a=a.If);c&&\"object\"===typeof c&&(c=c.If);d&&\"object\"===typeof d&&(d=d.If);e&&\"object\"===typeof e&&(e=e.If);g&&\"object\"===typeof g&&(g=g.If);return H(Dc(h,a,c,d,e,g),O)};\nL.prototype.Baseline=function(a,c,d,e,g){var h=this.If;a&&\"object\"===typeof a&&(a=a.If);c&&\"object\"===typeof c&&(c=c.If);d&&\"object\"===typeof d&&(d=d.If);e&&\"object\"===typeof e&&(e=e.If);g&&\"object\"===typeof g&&(g=g.If);return!!Ec(h,a,c,d,e,g)};L.prototype.Orientation=function(a,c,d,e){var g=this.If;a&&\"object\"===typeof a&&(a=a.If);c&&\"object\"===typeof c&&(c=c.If);d&&\"object\"===typeof d&&(d=d.If);e&&\"object\"===typeof e&&(e=e.If);Fc(g,a,c,d,e)};\nL.prototype.ParagraphInfo=function(a,c,d,e){var g=this.If;a&&\"object\"===typeof a&&(a=a.If);c&&\"object\"===typeof c&&(c=c.If);d&&\"object\"===typeof d&&(d=d.If);e&&\"object\"===typeof e&&(e=e.If);Gc(g,a,c,d,e)};L.prototype.ParagraphIsLtr=function(){return!!Hc(this.If)};L.prototype.GetUTF8Text=function(a){var c=this.If;a&&\"object\"===typeof a&&(a=a.If);return q(Ic(c,a))};L.prototype.SetLineSeparator=function(a){var c=this.If;I();a=a&&\"object\"===typeof a?a.If:J(a);Jc(c,a)};\nL.prototype.SetParagraphSeparator=function(a){var c=this.If;I();a=a&&\"object\"===typeof a?a.If:J(a);Kc(c,a)};L.prototype.Confidence=function(a){var c=this.If;a&&\"object\"===typeof a&&(a=a.If);return Lc(c,a)};\nL.prototype.WordFontAttributes=function(a,c,d,e,g,h,k,m){var t=this.If;a&&\"object\"===typeof a&&(a=a.If);c&&\"object\"===typeof c&&(c=c.If);d&&\"object\"===typeof d&&(d=d.If);e&&\"object\"===typeof e&&(e=e.If);g&&\"object\"===typeof g&&(g=g.If);h&&\"object\"===typeof h&&(h=h.If);k&&\"object\"===typeof k&&(k=k.If);m&&\"object\"===typeof m&&(m=m.If);return q(Mc(t,a,c,d,e,g,h,k,m))};L.prototype.WordRecognitionLanguage=function(){return q(Nc(this.If))};L.prototype.WordDirection=function(){return Oc(this.If)};\nL.prototype.WordIsFromDictionary=function(){return!!Pc(this.If)};L.prototype.WordIsNumeric=function(){return!!Qc(this.If)};L.prototype.HasBlamerInfo=function(){return!!Rc(this.If)};L.prototype.HasTruthString=function(){return!!Sc(this.If)};L.prototype.EquivalentToTruth=function(a){var c=this.If;I();a=a&&\"object\"===typeof a?a.If:J(a);return!!Tc(c,a)};L.prototype.WordTruthUTF8Text=function(){return q(Uc(this.If))};L.prototype.WordNormedUTF8Text=function(){return q(Vc(this.If))};\nL.prototype.WordLattice=function(a){var c=this.If;a&&\"object\"===typeof a&&(a=a.If);return q(Wc(c,a))};L.prototype.SymbolIsSuperscript=function(){return!!Xc(this.If)};L.prototype.SymbolIsSubscript=function(){return!!Yc(this.If)};L.prototype.SymbolIsDropcap=function(){return!!Zc(this.If)};L.prototype.__destroy__=function(){$c(this.If)};function Th(){throw\"cannot construct a TextlineOrder, no constructor in IDL\";}Th.prototype=Object.create(G.prototype);Th.prototype.constructor=Th;Th.prototype.Nf=Th;\nTh.Of={};b.TextlineOrder=Th;Th.prototype.__destroy__=function(){ad(this.If)};function Uh(){throw\"cannot construct a ETEXT_DESC, no constructor in IDL\";}Uh.prototype=Object.create(G.prototype);Uh.prototype.constructor=Uh;Uh.prototype.Nf=Uh;Uh.Of={};b.ETEXT_DESC=Uh;Uh.prototype.__destroy__=function(){bd(this.If)};function P(){throw\"cannot construct a PageIterator, no constructor in IDL\";}P.prototype=Object.create(G.prototype);P.prototype.constructor=P;P.prototype.Nf=P;P.Of={};b.PageIterator=P;\nP.prototype.Begin=function(){cd(this.If)};P.prototype.RestartParagraph=function(){dd(this.If)};P.prototype.IsWithinFirstTextlineOfParagraph=function(){return!!ed(this.If)};P.prototype.RestartRow=function(){fd(this.If)};P.prototype.Next=function(a){var c=this.If;a&&\"object\"===typeof a&&(a=a.If);return!!gd(c,a)};P.prototype.IsAtBeginningOf=function(a){var c=this.If;a&&\"object\"===typeof a&&(a=a.If);return!!hd(c,a)};\nP.prototype.IsAtFinalElement=function(a,c){var d=this.If;a&&\"object\"===typeof a&&(a=a.If);c&&\"object\"===typeof c&&(c=c.If);return!!jd(d,a,c)};P.prototype.Cmp=function(a){var c=this.If;a&&\"object\"===typeof a&&(a=a.If);return kd(c,a)};P.prototype.SetBoundingBoxComponents=function(a,c){var d=this.If;a&&\"object\"===typeof a&&(a=a.If);c&&\"object\"===typeof c&&(c=c.If);ld(d,a,c)};\nP.prototype.BoundingBox=function(a,c,d,e,g,h){var k=this.If;a&&\"object\"===typeof a&&(a=a.If);c&&\"object\"===typeof c&&(c=c.If);d&&\"object\"===typeof d&&(d=d.If);e&&\"object\"===typeof e&&(e=e.If);g&&\"object\"===typeof g&&(g=g.If);h&&\"object\"===typeof h&&(h=h.If);return void 0===h?!!md(k,a,c,d,e,g):!!nd(k,a,c,d,e,g,h)};\nP.prototype.BoundingBoxInternal=function(a,c,d,e,g){var h=this.If;a&&\"object\"===typeof a&&(a=a.If);c&&\"object\"===typeof c&&(c=c.If);d&&\"object\"===typeof d&&(d=d.If);e&&\"object\"===typeof e&&(e=e.If);g&&\"object\"===typeof g&&(g=g.If);return!!od(h,a,c,d,e,g)};P.prototype.Empty=function(a){var c=this.If;a&&\"object\"===typeof a&&(a=a.If);return!!pd(c,a)};P.prototype.BlockType=function(){return qd(this.If)};P.prototype.BlockPolygon=function(){return H(rd(this.If),M)};\nP.prototype.GetBinaryImage=function(a){var c=this.If;a&&\"object\"===typeof a&&(a=a.If);return H(sd(c,a),O)};P.prototype.GetImage=function(a,c,d,e,g){var h=this.If;a&&\"object\"===typeof a&&(a=a.If);c&&\"object\"===typeof c&&(c=c.If);d&&\"object\"===typeof d&&(d=d.If);e&&\"object\"===typeof e&&(e=e.If);g&&\"object\"===typeof g&&(g=g.If);return H(td(h,a,c,d,e,g),O)};\nP.prototype.Baseline=function(a,c,d,e,g){var h=this.If;a&&\"object\"===typeof a&&(a=a.If);c&&\"object\"===typeof c&&(c=c.If);d&&\"object\"===typeof d&&(d=d.If);e&&\"object\"===typeof e&&(e=e.If);g&&\"object\"===typeof g&&(g=g.If);return!!ud(h,a,c,d,e,g)};P.prototype.Orientation=function(a,c,d,e){var g=this.If;a&&\"object\"===typeof a&&(a=a.If);c&&\"object\"===typeof c&&(c=c.If);d&&\"object\"===typeof d&&(d=d.If);e&&\"object\"===typeof e&&(e=e.If);vd(g,a,c,d,e)};\nP.prototype.ParagraphInfo=function(a,c,d,e){var g=this.If;a&&\"object\"===typeof a&&(a=a.If);c&&\"object\"===typeof c&&(c=c.If);d&&\"object\"===typeof d&&(d=d.If);e&&\"object\"===typeof e&&(e=e.If);wd(g,a,c,d,e)};P.prototype.__destroy__=function(){xd(this.If)};function Vh(){throw\"cannot construct a WritingDirection, no constructor in IDL\";}Vh.prototype=Object.create(G.prototype);Vh.prototype.constructor=Vh;Vh.prototype.Nf=Vh;Vh.Of={};b.WritingDirection=Vh;Vh.prototype.__destroy__=function(){yd(this.If)};\nfunction Wh(a){a&&\"object\"===typeof a&&(a=a.If);this.If=zd(a);Ih(Wh)[this.If]=this}Wh.prototype=Object.create(G.prototype);Wh.prototype.constructor=Wh;Wh.prototype.Nf=Wh;Wh.Of={};b.WordChoiceIterator=Wh;Wh.prototype.Next=function(){return!!Ad(this.If)};Wh.prototype.GetUTF8Text=function(){return q(Bd(this.If))};Wh.prototype.Confidence=function(){return Cd(this.If)};Wh.prototype.__destroy__=function(){Dd(this.If)};function Q(){throw\"cannot construct a Box, no constructor in IDL\";}Q.prototype=Object.create(G.prototype);\nQ.prototype.constructor=Q;Q.prototype.Nf=Q;Q.Of={};b.Box=Q;Q.prototype.get_x=Q.prototype.Tg=function(){return Ed(this.If)};Object.defineProperty(Q.prototype,\"x\",{get:Q.prototype.Tg});Q.prototype.get_y=Q.prototype.Ug=function(){return Fd(this.If)};Object.defineProperty(Q.prototype,\"y\",{get:Q.prototype.Ug});Q.prototype.get_w=Q.prototype.Sg=function(){return Gd(this.If)};Object.defineProperty(Q.prototype,\"w\",{get:Q.prototype.Sg});Q.prototype.get_h=Q.prototype.Rg=function(){return Hd(this.If)};\nObject.defineProperty(Q.prototype,\"h\",{get:Q.prototype.Rg});Q.prototype.get_refcount=Q.prototype.cg=function(){return Id(this.If)};Object.defineProperty(Q.prototype,\"refcount\",{get:Q.prototype.cg});Q.prototype.__destroy__=function(){Jd(this.If)};function R(a,c,d){I();a=a&&\"object\"===typeof a?a.If:J(a);c=c&&\"object\"===typeof c?c.If:J(c);d&&\"object\"===typeof d&&(d=d.If);this.If=Kd(a,c,d);Ih(R)[this.If]=this}R.prototype=Object.create(G.prototype);R.prototype.constructor=R;R.prototype.Nf=R;R.Of={};\nb.TessPDFRenderer=R;R.prototype.BeginDocument=function(a){var c=this.If;I();a=a&&\"object\"===typeof a?a.If:J(a);return!!Ld(c,a)};R.prototype.AddImage=function(a){var c=this.If;a&&\"object\"===typeof a&&(a=a.If);return!!Md(c,a)};R.prototype.EndDocument=function(){return!!Nd(this.If)};R.prototype.happy=function(){return!!Od(this.If)};R.prototype.file_extension=function(){return q(Pd(this.If))};R.prototype.title=R.prototype.title=function(){return q(Qd(this.If))};R.prototype.imagenum=function(){return Rd(this.If)};\nR.prototype.__destroy__=function(){Sd(this.If)};function Xh(){throw\"cannot construct a PixaPtr, no constructor in IDL\";}Xh.prototype=Object.create(G.prototype);Xh.prototype.constructor=Xh;Xh.prototype.Nf=Xh;Xh.Of={};b.PixaPtr=Xh;Xh.prototype.__destroy__=function(){Td(this.If)};function Yh(){throw\"cannot construct a FloatPtr, no constructor in IDL\";}Yh.prototype=Object.create(G.prototype);Yh.prototype.constructor=Yh;Yh.prototype.Nf=Yh;Yh.Of={};b.FloatPtr=Yh;Yh.prototype.__destroy__=function(){Ud(this.If)};\nfunction Zh(a){a&&\"object\"===typeof a&&(a=a.If);this.If=Vd(a);Ih(Zh)[this.If]=this}Zh.prototype=Object.create(G.prototype);Zh.prototype.constructor=Zh;Zh.prototype.Nf=Zh;Zh.Of={};b.ChoiceIterator=Zh;Zh.prototype.Next=function(){return!!Wd(this.If)};Zh.prototype.GetUTF8Text=function(){return q(Xd(this.If))};Zh.prototype.Confidence=function(){return Yd(this.If)};Zh.prototype.__destroy__=function(){Zd(this.If)};function $h(){throw\"cannot construct a PixPtr, no constructor in IDL\";}$h.prototype=Object.create(G.prototype);\n$h.prototype.constructor=$h;$h.prototype.Nf=$h;$h.Of={};b.PixPtr=$h;$h.prototype.__destroy__=function(){$d(this.If)};function ai(){throw\"cannot construct a UNICHARSET, no constructor in IDL\";}ai.prototype=Object.create(G.prototype);ai.prototype.constructor=ai;ai.prototype.Nf=ai;ai.Of={};b.UNICHARSET=ai;ai.prototype.get_script_from_script_id=function(a){var c=this.If;a&&\"object\"===typeof a&&(a=a.If);return q(ae(c,a))};\nai.prototype.get_script_id_from_name=function(a){var c=this.If;I();a=a&&\"object\"===typeof a?a.If:J(a);return be(c,a)};ai.prototype.get_script_table_size=function(){return ce(this.If)};ai.prototype.__destroy__=function(){de(this.If)};function bi(){throw\"cannot construct a IntPtr, no constructor in IDL\";}bi.prototype=Object.create(G.prototype);bi.prototype.constructor=bi;bi.prototype.Nf=bi;bi.Of={};b.IntPtr=bi;bi.prototype.__destroy__=function(){ee(this.If)};\nfunction ci(){throw\"cannot construct a Orientation, no constructor in IDL\";}ci.prototype=Object.create(G.prototype);ci.prototype.constructor=ci;ci.prototype.Nf=ci;ci.Of={};b.Orientation=ci;ci.prototype.__destroy__=function(){fe(this.If)};function S(){throw\"cannot construct a OSBestResult, no constructor in IDL\";}S.prototype=Object.create(G.prototype);S.prototype.constructor=S;S.prototype.Nf=S;S.Of={};b.OSBestResult=S;S.prototype.get_orientation_id=S.prototype.ii=function(){return ge(this.If)};\nObject.defineProperty(S.prototype,\"orientation_id\",{get:S.prototype.ii});S.prototype.get_script_id=S.prototype.li=function(){return he(this.If)};Object.defineProperty(S.prototype,\"script_id\",{get:S.prototype.li});S.prototype.get_sconfidence=S.prototype.ki=function(){return ie(this.If)};Object.defineProperty(S.prototype,\"sconfidence\",{get:S.prototype.ki});S.prototype.get_oconfidence=S.prototype.hi=function(){return je(this.If)};Object.defineProperty(S.prototype,\"oconfidence\",{get:S.prototype.hi});\nS.prototype.__destroy__=function(){ke(this.If)};function T(){throw\"cannot construct a Boxa, no constructor in IDL\";}T.prototype=Object.create(G.prototype);T.prototype.constructor=T;T.prototype.Nf=T;T.Of={};b.Boxa=T;T.prototype.get_n=T.prototype.hg=function(){return le(this.If)};Object.defineProperty(T.prototype,\"n\",{get:T.prototype.hg});T.prototype.get_nalloc=T.prototype.ig=function(){return me(this.If)};Object.defineProperty(T.prototype,\"nalloc\",{get:T.prototype.ig});\nT.prototype.get_refcount=T.prototype.cg=function(){return ne(this.If)};Object.defineProperty(T.prototype,\"refcount\",{get:T.prototype.cg});T.prototype.get_box=T.prototype.ai=function(){return H(oe(this.If),di)};Object.defineProperty(T.prototype,\"box\",{get:T.prototype.ai});T.prototype.__destroy__=function(){pe(this.If)};function V(){throw\"cannot construct a PixColormap, no constructor in IDL\";}V.prototype=Object.create(G.prototype);V.prototype.constructor=V;V.prototype.Nf=V;V.Of={};b.PixColormap=V;\nV.prototype.get_array=V.prototype.Zh=function(){return qe(this.If)};Object.defineProperty(V.prototype,\"array\",{get:V.prototype.Zh});V.prototype.get_depth=V.prototype.fi=function(){return re(this.If)};Object.defineProperty(V.prototype,\"depth\",{get:V.prototype.fi});V.prototype.get_nalloc=V.prototype.ig=function(){return se(this.If)};Object.defineProperty(V.prototype,\"nalloc\",{get:V.prototype.ig});V.prototype.get_n=V.prototype.hg=function(){return te(this.If)};Object.defineProperty(V.prototype,\"n\",{get:V.prototype.hg});\nV.prototype.__destroy__=function(){ue(this.If)};function M(){throw\"cannot construct a Pta, no constructor in IDL\";}M.prototype=Object.create(G.prototype);M.prototype.constructor=M;M.prototype.Nf=M;M.Of={};b.Pta=M;M.prototype.get_n=M.prototype.hg=function(){return ve(this.If)};Object.defineProperty(M.prototype,\"n\",{get:M.prototype.hg});M.prototype.get_nalloc=M.prototype.ig=function(){return we(this.If)};Object.defineProperty(M.prototype,\"nalloc\",{get:M.prototype.ig});\nM.prototype.get_refcount=M.prototype.cg=function(){return xe(this.If)};Object.defineProperty(M.prototype,\"refcount\",{get:M.prototype.cg});M.prototype.get_x=M.prototype.Tg=function(){return H(ye(this.If),Yh)};Object.defineProperty(M.prototype,\"x\",{get:M.prototype.Tg});M.prototype.get_y=M.prototype.Ug=function(){return H(ze(this.If),Yh)};Object.defineProperty(M.prototype,\"y\",{get:M.prototype.Ug});M.prototype.__destroy__=function(){Ae(this.If)};\nfunction O(){throw\"cannot construct a Pix, no constructor in IDL\";}O.prototype=Object.create(G.prototype);O.prototype.constructor=O;O.prototype.Nf=O;O.Of={};b.Pix=O;O.prototype.get_w=O.prototype.Sg=function(){return Be(this.If)};Object.defineProperty(O.prototype,\"w\",{get:O.prototype.Sg});O.prototype.get_h=O.prototype.Rg=function(){return Ce(this.If)};Object.defineProperty(O.prototype,\"h\",{get:O.prototype.Rg});O.prototype.get_d=O.prototype.di=function(){return De(this.If)};\nObject.defineProperty(O.prototype,\"d\",{get:O.prototype.di});O.prototype.get_spp=O.prototype.ni=function(){return Ee(this.If)};Object.defineProperty(O.prototype,\"spp\",{get:O.prototype.ni});O.prototype.get_wpl=O.prototype.ri=function(){return Fe(this.If)};Object.defineProperty(O.prototype,\"wpl\",{get:O.prototype.ri});O.prototype.get_refcount=O.prototype.cg=function(){return Ge(this.If)};Object.defineProperty(O.prototype,\"refcount\",{get:O.prototype.cg});O.prototype.get_xres=O.prototype.si=function(){return He(this.If)};\nObject.defineProperty(O.prototype,\"xres\",{get:O.prototype.si});O.prototype.get_yres=O.prototype.ti=function(){return Ie(this.If)};Object.defineProperty(O.prototype,\"yres\",{get:O.prototype.ti});O.prototype.get_informat=O.prototype.gi=function(){return Je(this.If)};Object.defineProperty(O.prototype,\"informat\",{get:O.prototype.gi});O.prototype.get_special=O.prototype.mi=function(){return Ke(this.If)};Object.defineProperty(O.prototype,\"special\",{get:O.prototype.mi});\nO.prototype.get_text=O.prototype.oi=function(){return q(Le(this.If))};Object.defineProperty(O.prototype,\"text\",{get:O.prototype.oi});O.prototype.get_colormap=O.prototype.ci=function(){return H(Me(this.If),V)};Object.defineProperty(O.prototype,\"colormap\",{get:O.prototype.ci});O.prototype.get_data=O.prototype.ei=function(){return Ne(this.If)};Object.defineProperty(O.prototype,\"data\",{get:O.prototype.ei});O.prototype.__destroy__=function(){Oe(this.If)};\nfunction ei(){throw\"cannot construct a DoublePtr, no constructor in IDL\";}ei.prototype=Object.create(G.prototype);ei.prototype.constructor=ei;ei.prototype.Nf=ei;ei.Of={};b.DoublePtr=ei;ei.prototype.__destroy__=function(){Pe(this.If)};function fi(){throw\"cannot construct a Dawg, no constructor in IDL\";}fi.prototype=Object.create(G.prototype);fi.prototype.constructor=fi;fi.prototype.Nf=fi;fi.Of={};b.Dawg=fi;fi.prototype.__destroy__=function(){Qe(this.If)};\nfunction di(){throw\"cannot construct a BoxPtr, no constructor in IDL\";}di.prototype=Object.create(G.prototype);di.prototype.constructor=di;di.prototype.Nf=di;di.Of={};b.BoxPtr=di;di.prototype.__destroy__=function(){Re(this.If)};function X(){this.If=Se();Ih(X)[this.If]=this}X.prototype=Object.create(G.prototype);X.prototype.constructor=X;X.prototype.Nf=X;X.Of={};b.TessBaseAPI=X;X.prototype.Version=function(){return q(Te(this.If))};\nX.prototype.SetInputName=function(a){var c=this.If;I();a=a&&\"object\"===typeof a?a.If:J(a);Ue(c,a)};X.prototype.GetInputName=function(){return q(Ve(this.If))};X.prototype.SetInputImage=function(a){var c=this.If;a&&\"object\"===typeof a&&(a=a.If);We(c,a)};X.prototype.GetInputImage=function(){return H(Xe(this.If),O)};X.prototype.GetSourceYResolution=function(){return Ye(this.If)};X.prototype.GetDatapath=function(){return q(Ze(this.If))};\nX.prototype.SetOutputName=function(a){var c=this.If;I();a=a&&\"object\"===typeof a?a.If:J(a);$e(c,a)};X.prototype.SetVariable=X.prototype.SetVariable=function(a,c){var d=this.If;I();a=a&&\"object\"===typeof a?a.If:J(a);c=c&&\"object\"===typeof c?c.If:J(c);return!!af(d,a,c)};X.prototype.SetDebugVariable=function(a,c){var d=this.If;I();a=a&&\"object\"===typeof a?a.If:J(a);c=c&&\"object\"===typeof c?c.If:J(c);return!!bf(d,a,c)};\nX.prototype.GetIntVariable=function(a,c){var d=this.If;I();a=a&&\"object\"===typeof a?a.If:J(a);c&&\"object\"===typeof c&&(c=c.If);return!!cf(d,a,c)};X.prototype.GetBoolVariable=function(a,c){var d=this.If;I();a=a&&\"object\"===typeof a?a.If:J(a);c&&\"object\"===typeof c&&(c=c.If);return!!df(d,a,c)};X.prototype.GetDoubleVariable=function(a,c){var d=this.If;I();a=a&&\"object\"===typeof a?a.If:J(a);c&&\"object\"===typeof c&&(c=c.If);return!!ef(d,a,c)};\nX.prototype.GetStringVariable=function(a){var c=this.If;I();a=a&&\"object\"===typeof a?a.If:J(a);return q(ff(c,a))};X.prototype.Init=function(a,c,d,e){void 0===d&&void 0!==e&&(d=3);var g=this.If;I();a=a&&\"object\"===typeof a?a.If:J(a);c=c&&\"object\"===typeof c?c.If:J(c);e=e&&\"object\"===typeof e?e.If:J(e);d&&\"object\"===typeof d&&(d=d.If);return void 0===d&&void 0!==e?lf(g,a,c,3,e):void 0===d?jf(g,a,c):void 0===e?kf(g,a,c,d):lf(g,a,c,d,e)};X.prototype.GetInitLanguagesAsString=function(){return q(mf(this.If))};\nX.prototype.InitForAnalysePage=function(){nf(this.If)};X.prototype.SaveParameters=function(){gf(this.If)};X.prototype.RestoreParameters=function(){hf(this.If)};X.prototype.ReadConfigFile=function(a){var c=this.If;I();a=a&&\"object\"===typeof a?a.If:J(a);of(c,a)};X.prototype.ReadDebugConfigFile=function(a){var c=this.If;I();a=a&&\"object\"===typeof a?a.If:J(a);pf(c,a)};X.prototype.SetPageSegMode=function(a){var c=this.If;a&&\"object\"===typeof a&&(a=a.If);qf(c,a)};X.prototype.GetPageSegMode=function(){return rf(this.If)};\nX.prototype.TesseractRect=function(a,c,d,e,g,h,k){var m=this.If;a&&\"object\"===typeof a&&(a=a.If);c&&\"object\"===typeof c&&(c=c.If);d&&\"object\"===typeof d&&(d=d.If);e&&\"object\"===typeof e&&(e=e.If);g&&\"object\"===typeof g&&(g=g.If);h&&\"object\"===typeof h&&(h=h.If);k&&\"object\"===typeof k&&(k=k.If);return q(sf(m,a,c,d,e,g,h,k))};X.prototype.ClearAdaptiveClassifier=function(){tf(this.If)};\nX.prototype.SetImage=function(a,c,d,e,g,h=1,k=0){var m=this.If;a&&\"object\"===typeof a&&(a=a.If);c&&\"object\"===typeof c&&(c=c.If);d&&\"object\"===typeof d&&(d=d.If);e&&\"object\"===typeof e&&(e=e.If);g&&\"object\"===typeof g&&(g=g.If);void 0===c||null===c?uf(m,a,h,k):vf(m,a,c,d,e,g,h,k)};X.prototype.SetImageFile=function(a=1,c=0){return wf(this.If,a,c)};X.prototype.SetSourceResolution=function(a){var c=this.If;a&&\"object\"===typeof a&&(a=a.If);xf(c,a)};\nX.prototype.SetRectangle=function(a,c,d,e){var g=this.If;a&&\"object\"===typeof a&&(a=a.If);c&&\"object\"===typeof c&&(c=c.If);d&&\"object\"===typeof d&&(d=d.If);e&&\"object\"===typeof e&&(e=e.If);yf(g,a,c,d,e)};X.prototype.GetThresholdedImage=function(){return H(zf(this.If),O)};X.prototype.WriteImage=function(a){Af(this.If,a)};X.prototype.GetRegions=function(a){var c=this.If;a&&\"object\"===typeof a&&(a=a.If);return H(Df(c,a),T)};\nX.prototype.GetTextlines=function(a,c,d,e,g){var h=this.If;a&&\"object\"===typeof a&&(a=a.If);c&&\"object\"===typeof c&&(c=c.If);d&&\"object\"===typeof d&&(d=d.If);e&&\"object\"===typeof e&&(e=e.If);g&&\"object\"===typeof g&&(g=g.If);return void 0===d?H(Ef(h,a,c),T):void 0===e?H(_emscripten_bind_TessBaseAPI_GetTextlines_3(h,a,c,d),T):void 0===g?H(_emscripten_bind_TessBaseAPI_GetTextlines_4(h,a,c,d,e),T):H(Ff(h,a,c,d,e,g),T)};\nX.prototype.GetStrips=function(a,c){var d=this.If;a&&\"object\"===typeof a&&(a=a.If);c&&\"object\"===typeof c&&(c=c.If);return H(Gf(d,a,c),T)};X.prototype.GetWords=function(a){var c=this.If;a&&\"object\"===typeof a&&(a=a.If);return H(Hf(c,a),T)};X.prototype.GetConnectedComponents=function(a){var c=this.If;a&&\"object\"===typeof a&&(a=a.If);return H(If(c,a),T)};\nX.prototype.GetComponentImages=function(a,c,d,e,g,h,k){var m=this.If;a&&\"object\"===typeof a&&(a=a.If);c&&\"object\"===typeof c&&(c=c.If);d&&\"object\"===typeof d&&(d=d.If);e&&\"object\"===typeof e&&(e=e.If);g&&\"object\"===typeof g&&(g=g.If);h&&\"object\"===typeof h&&(h=h.If);k&&\"object\"===typeof k&&(k=k.If);return void 0===g?H(Jf(m,a,c,d,e),T):void 0===h?H(_emscripten_bind_TessBaseAPI_GetComponentImages_5(m,a,c,d,e,g),T):void 0===k?H(_emscripten_bind_TessBaseAPI_GetComponentImages_6(m,a,c,d,e,g,h),T):H(Kf(m,\na,c,d,e,g,h,k),T)};X.prototype.GetThresholdedImageScaleFactor=function(){return Lf(this.If)};X.prototype.AnalyseLayout=function(a){var c=this.If;a&&\"object\"===typeof a&&(a=a.If);return void 0===a?H(Mf(c),P):H(Nf(c,a),P)};X.prototype.Recognize=function(a){var c=this.If;a&&\"object\"===typeof a&&(a=a.If);return Of(c,a)};X.prototype.FindLines=function(){return Bf(this.If)};X.prototype.GetGradient=function(){return Cf(this.If)};\nX.prototype.ProcessPages=function(a,c,d,e){var g=this.If;I();a=a&&\"object\"===typeof a?a.If:J(a);c=c&&\"object\"===typeof c?c.If:J(c);d&&\"object\"===typeof d&&(d=d.If);e&&\"object\"===typeof e&&(e=e.If);return!!Pf(g,a,c,d,e)};\nX.prototype.ProcessPage=function(a,c,d,e,g,h){var k=this.If;I();a&&\"object\"===typeof a&&(a=a.If);c&&\"object\"===typeof c&&(c=c.If);d=d&&\"object\"===typeof d?d.If:J(d);e=e&&\"object\"===typeof e?e.If:J(e);g&&\"object\"===typeof g&&(g=g.If);h&&\"object\"===typeof h&&(h=h.If);return!!Qf(k,a,c,d,e,g,h)};X.prototype.GetIterator=function(){return H(Rf(this.If),L)};X.prototype.GetUTF8Text=function(){return q(Sf(this.If))};\nX.prototype.GetHOCRText=function(a){var c=this.If;a&&\"object\"===typeof a&&(a=a.If);return q(Tf(c,a))};X.prototype.GetTSVText=function(a){var c=this.If;a&&\"object\"===typeof a&&(a=a.If);return q(Uf(c,a))};X.prototype.GetBoxText=function(a){var c=this.If;a&&\"object\"===typeof a&&(a=a.If);return q(Vf(c,a))};X.prototype.GetUNLVText=function(){return q(Wf(this.If))};X.prototype.GetOsdText=function(a){var c=this.If;a&&\"object\"===typeof a&&(a=a.If);return q(Xf(c,a))};X.prototype.MeanTextConf=function(){return Yf(this.If)};\nX.prototype.AllWordConfidences=function(){return H(Zf(this.If),bi)};X.prototype.AdaptToWordStr=function(a,c){var d=this.If;I();a&&\"object\"===typeof a&&(a=a.If);c=c&&\"object\"===typeof c?c.If:J(c);return!!$f(d,a,c)};X.prototype.Clear=function(){ag(this.If)};X.prototype.End=function(){bg(this.If)};X.prototype.ClearPersistentCache=function(){cg(this.If)};X.prototype.IsValidWord=function(a){var c=this.If;I();a=a&&\"object\"===typeof a?a.If:J(a);return dg(c,a)};\nX.prototype.IsValidCharacter=function(a){var c=this.If;I();a=a&&\"object\"===typeof a?a.If:J(a);return!!eg(c,a)};X.prototype.DetectOS=function(a){var c=this.If;a&&\"object\"===typeof a&&(a=a.If);return!!fg(c,a)};X.prototype.GetUnichar=function(a){var c=this.If;a&&\"object\"===typeof a&&(a=a.If);return q(gg(c,a))};X.prototype.GetDawg=function(a){var c=this.If;a&&\"object\"===typeof a&&(a=a.If);return H(hg(c,a),fi)};X.prototype.NumDawgs=function(){return ig(this.If)};X.prototype.oem=function(){return jg(this.If)};\nX.prototype.__destroy__=function(){kg(this.If)};function Y(){this.If=lg();Ih(Y)[this.If]=this}Y.prototype=Object.create(G.prototype);Y.prototype.constructor=Y;Y.prototype.Nf=Y;Y.Of={};b.OSResults=Y;Y.prototype.print_scores=function(){mg(this.If)};Y.prototype.get_best_result=Y.prototype.$h=function(){return H(ng(this.If),S)};Object.defineProperty(Y.prototype,\"best_result\",{get:Y.prototype.$h});Y.prototype.get_unicharset=Y.prototype.pi=function(){return H(og(this.If),ai)};\nObject.defineProperty(Y.prototype,\"unicharset\",{get:Y.prototype.pi});Y.prototype.__destroy__=function(){pg(this.If)};function Z(){throw\"cannot construct a Pixa, no constructor in IDL\";}Z.prototype=Object.create(G.prototype);Z.prototype.constructor=Z;Z.prototype.Nf=Z;Z.Of={};b.Pixa=Z;Z.prototype.get_n=Z.prototype.hg=function(){return qg(this.If)};Object.defineProperty(Z.prototype,\"n\",{get:Z.prototype.hg});Z.prototype.get_nalloc=Z.prototype.ig=function(){return rg(this.If)};\nObject.defineProperty(Z.prototype,\"nalloc\",{get:Z.prototype.ig});Z.prototype.get_refcount=Z.prototype.cg=function(){return sg(this.If)};Object.defineProperty(Z.prototype,\"refcount\",{get:Z.prototype.cg});Z.prototype.get_pix=Z.prototype.ji=function(){return H(tg(this.If),$h)};Object.defineProperty(Z.prototype,\"pix\",{get:Z.prototype.ji});Z.prototype.get_boxa=Z.prototype.bi=function(){return H(ug(this.If),T)};Object.defineProperty(Z.prototype,\"boxa\",{get:Z.prototype.bi});Z.prototype.__destroy__=function(){vg(this.If)};\n(function(){function a(){b.RIL_BLOCK=wg();b.RIL_PARA=xg();b.RIL_TEXTLINE=yg();b.RIL_WORD=zg();b.RIL_SYMBOL=Ag();b.OEM_TESSERACT_ONLY=Bg();b.OEM_LSTM_ONLY=Cg();b.OEM_TESSERACT_LSTM_COMBINED=Dg();b.OEM_DEFAULT=Eg();b.OEM_COUNT=Fg();b.WRITING_DIRECTION_LEFT_TO_RIGHT=Gg();b.WRITING_DIRECTION_RIGHT_TO_LEFT=Hg();b.WRITING_DIRECTION_TOP_TO_BOTTOM=Ig();b.PT_UNKNOWN=Jg();b.PT_FLOWING_TEXT=Kg();b.PT_HEADING_TEXT=Lg();b.PT_PULLOUT_TEXT=Mg();b.PT_EQUATION=Ng();b.PT_INLINE_EQUATION=Og();b.PT_TABLE=Pg();b.PT_VERTICAL_TEXT=\nQg();b.PT_CAPTION_TEXT=Rg();b.PT_FLOWING_IMAGE=Sg();b.PT_HEADING_IMAGE=Tg();b.PT_PULLOUT_IMAGE=Ug();b.PT_HORZ_LINE=Vg();b.PT_VERT_LINE=Wg();b.PT_NOISE=Xg();b.PT_COUNT=Yg();b.DIR_NEUTRAL=Zg();b.DIR_LEFT_TO_RIGHT=$g();b.DIR_RIGHT_TO_LEFT=ah();b.DIR_MIX=bh();b.JUSTIFICATION_UNKNOWN=ch();b.JUSTIFICATION_LEFT=dh();b.JUSTIFICATION_CENTER=eh();b.JUSTIFICATION_RIGHT=fh();b.TEXTLINE_ORDER_LEFT_TO_RIGHT=gh();b.TEXTLINE_ORDER_RIGHT_TO_LEFT=hh();b.TEXTLINE_ORDER_TOP_TO_BOTTOM=ih();b.ORIENTATION_PAGE_UP=jh();\nb.ORIENTATION_PAGE_RIGHT=kh();b.ORIENTATION_PAGE_DOWN=lh();b.ORIENTATION_PAGE_LEFT=mh();b.PSM_OSD_ONLY=nh();b.PSM_AUTO_OSD=oh();b.PSM_AUTO_ONLY=ph();b.PSM_AUTO=qh();b.PSM_SINGLE_COLUMN=rh();b.PSM_SINGLE_BLOCK_VERT_TEXT=sh();b.PSM_SINGLE_BLOCK=th();b.PSM_SINGLE_LINE=uh();b.PSM_SINGLE_WORD=vh();b.PSM_CIRCLE_WORD=wh();b.PSM_SINGLE_CHAR=xh();b.PSM_SPARSE_TEXT=yh();b.PSM_SPARSE_TEXT_OSD=zh();b.PSM_RAW_LINE=Ah();b.PSM_COUNT=Bh()}La?a():Ia.unshift(a)})();\nPh.prototype.getValue=function(a){return!!Ya(this.If+NaN*(a||0))};bi.prototype.getValue=function(a){return Ya(this.If+4*(a||0),\"i32\")};Yh.prototype.getValue=function(a){return Ya(this.If+4*(a||0),\"float\")};ei.prototype.getValue=function(a){return Ya(this.If+8*(a||0),\"double\")};di.prototype.get=Xh.prototype.get=$h.prototype.get=function(a){return Ya(this.If+4*(a||0),\"*\")};function gi(){this.og={}}gi.prototype.wrap=function(a,c){var d=zb(4);Za(d,0,\"i32\");return this.og[a]=H(d,c)};\ngi.prototype.bool=function(a){return this.wrap(a,Ph)};gi.prototype.i32=function(a){return this.wrap(a,bi)};gi.prototype.f32=function(a){return this.wrap(a,Yh)};gi.prototype.f64=function(a){return this.og[a]=H(zb(8),ei)};gi.prototype.peek=function(){var a={},c;for(c in this.og)a[c]=this.og[c].getValue();return a};gi.prototype.get=function(){var a={},c;for(c in this.og)a[c]=this.og[c].getValue(),Ch(this.og[c].If);return a};\nL.prototype.getBoundingBox=function(a){var c=new gi;this.BoundingBox(a,c.i32(\"x0\"),c.i32(\"y0\"),c.i32(\"x1\"),c.i32(\"y1\"));return c.get()};L.prototype.getBaseline=function(a){var c=new gi;a=!!this.Baseline(a,c.i32(\"x0\"),c.i32(\"y0\"),c.i32(\"x1\"),c.i32(\"y1\"));c=c.get();c.has_baseline=a;return c};\nL.prototype.getWordFontAttributes=function(){var a=new gi,c=this.WordFontAttributes(a.bool(\"is_bold\"),a.bool(\"is_italic\"),a.bool(\"is_underlined\"),a.bool(\"is_monospace\"),a.bool(\"is_serif\"),a.bool(\"is_smallcaps\"),a.i32(\"pointsize\"),a.i32(\"font_id\"));a=a.get();a.font_name=c;return a};b.pointerHelper=gi;\n\n\n  return TesseractCore.ready\n}\n);\n})();\nif (typeof exports === 'object' && typeof module === 'object')\n  module.exports = TesseractCore;\nelse if (typeof define === 'function' && define['amd'])\n  define([], function() { return TesseractCore; });\nelse if (typeof exports === 'object')\n  exports[\"TesseractCore\"] = TesseractCore;\n"
  },
  {
    "path": "src/queue/index.ts",
    "content": "import { processKnowledge } from \"@/libs/process-knowledge\"\nimport PubSub from \"pubsub-js\"\n\nexport const KNOWLEDGE_QUEUE = Symbol(\"queue\")\n\nlet isProcessing = false\n\nPubSub.subscribe(KNOWLEDGE_QUEUE, async (msg, id) => {\n  try {\n    isProcessing = true\n    await processKnowledge(msg, id)\n    isProcessing = false\n  } catch (error) {\n    console.error(error)\n    isProcessing = false\n  }\n})\n\nwindow.addEventListener(\"beforeunload\", (event) => {\n  if (isProcessing) {\n    event.preventDefault()\n    event.returnValue = \"\"\n  }\n})\n"
  },
  {
    "path": "src/routes/chrome-route.tsx",
    "content": "import { Suspense } from \"react\"\nimport { useDarkMode } from \"~/hooks/useDarkmode\"\nimport { OptionRoutingChrome, SidepanelRoutingChrome } from \"./chrome\"\nimport { PageAssistLoader } from \"@/components/Common/PageAssistLoader\"\n\nexport const OptionRouting = () => {\n  const { mode } = useDarkMode()\n\n  return (\n    <div className={`${mode === \"dark\" ? \"dark\" : \"light\"} arimo`}>\n      <Suspense fallback={<PageAssistLoader />}>\n        <OptionRoutingChrome />\n      </Suspense>\n    </div>\n  )\n}\n\nexport const SidepanelRouting = () => {\n  const { mode } = useDarkMode()\n\n  return (\n    <div className={`${mode === \"dark\" ? \"dark\" : \"light\"} arimo`}>\n      <Suspense fallback={<PageAssistLoader />}>\n        <SidepanelRoutingChrome />\n      </Suspense>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/routes/chrome.tsx",
    "content": "import { Route, Routes } from \"react-router-dom\"\nimport OptionIndex from \"./option-index\"\nimport OptionSettings from \"./option-settings\"\nimport OptionModal from \"./option-settings-model\"\nimport OptionPrompt from \"./option-settings-prompt\"\nimport OptionOllamaSettings from \"./options-settings-ollama\"\nimport OptionShare from \"./option-settings-share\"\nimport OptionKnowledgeBase from \"./option-settings-knowledge\"\nimport OptionAbout from \"./option-settings-about\"\nimport SidepanelChat from \"./sidepanel-chat\"\nimport SidepanelSettings from \"./sidepanel-settings\"\nimport OptionRagSettings from \"./option-rag\"\nimport OptionChrome from \"./option-settings-chrome\"\nimport OptionOpenAI from \"./option-settings-openai\"\nimport OptionMCP from \"./option-settings-mcp\"\nimport SidepanelSettingsOpenAI from \"./sidepanel-settings-openai\"\nimport SidepanelSettingsModel from \"./sidepanel-settings-model\"\n\nexport const OptionRoutingChrome = () => {\n  return (\n    <Routes>\n      <Route path=\"/\" element={<OptionIndex />} />\n      <Route path=\"/settings\" element={<OptionSettings />} />\n      <Route path=\"/settings/model\" element={<OptionModal />} />\n      <Route path=\"/settings/prompt\" element={<OptionPrompt />} />\n      <Route path=\"/settings/ollama\" element={<OptionOllamaSettings />} />\n      <Route path=\"/settings/chrome\" element={<OptionChrome />} />\n      <Route path=\"/settings/openai\" element={<OptionOpenAI />} />\n      <Route path=\"/settings/mcp\" element={<OptionMCP />} />\n      <Route path=\"/settings/share\" element={<OptionShare />} />\n      <Route path=\"/settings/knowledge\" element={<OptionKnowledgeBase />} />\n      <Route path=\"/settings/rag\" element={<OptionRagSettings />} />\n      <Route path=\"/settings/about\" element={<OptionAbout />} />\n    </Routes>\n  )\n}\n\nexport const SidepanelRoutingChrome = () => {\n  return (\n    <Routes>\n      <Route path=\"/\" element={<SidepanelChat />} />\n      <Route path=\"/settings\" element={<SidepanelSettings />} />\n      <Route path=\"/settings/openai\" element={<SidepanelSettingsOpenAI />} />\n      <Route path=\"/settings/model\" element={<SidepanelSettingsModel />} />\n    </Routes>\n  )\n}\n"
  },
  {
    "path": "src/routes/firefox-route.tsx",
    "content": "import { Suspense } from \"react\"\nimport { useDarkMode } from \"~/hooks/useDarkmode\"\nimport { OptionRoutingFirefox, SidepanelRoutingFirefox } from \"./firefox\"\nimport { PageAssistLoader } from \"@/components/Common/PageAssistLoader\"\n\nexport const OptionRouting = () => {\n  const { mode } = useDarkMode()\n\n  return (\n    <div className={`${mode === \"dark\" ? \"dark\" : \"light\"} arimo`}>\n      <Suspense fallback={<PageAssistLoader />}>\n        <OptionRoutingFirefox />\n      </Suspense>\n    </div>\n  )\n}\n\nexport const SidepanelRouting = () => {\n  const { mode } = useDarkMode()\n\n  return (\n    <div className={`${mode === \"dark\" ? \"dark\" : \"light\"} arimo`}>\n      <Suspense fallback={<PageAssistLoader />}>\n        <SidepanelRoutingFirefox />\n      </Suspense>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/routes/firefox.tsx",
    "content": "// this is a temp fix for firefox\n// because chunks getting 4mb+ and it's not working on firefox addon store\nimport { lazy } from \"react\"\nimport { Route, Routes } from \"react-router-dom\"\n\nconst SidepanelChat = lazy(() => import(\"./sidepanel-chat\"))\nconst SidepanelSettings = lazy(() => import(\"./sidepanel-settings\"))\nconst SidepanelSettingsOpenAI = lazy(() => import(\"./sidepanel-settings-openai\"))\nconst SidepanelSettingsModel = lazy(() => import(\"./sidepanel-settings-model\"))  \n\nconst OptionIndex = lazy(() => import(\"./option-index\"))\nconst OptionModal = lazy(() => import(\"./option-settings-model\"))\nconst OptionPrompt = lazy(() => import(\"./option-settings-prompt\"))\nconst OptionOllamaSettings = lazy(() => import(\"./options-settings-ollama\"))\nconst OptionSettings = lazy(() => import(\"./option-settings\"))\nconst OptionShare = lazy(() => import(\"./option-settings-share\"))\nconst OptionKnowledgeBase = lazy(() => import(\"./option-settings-knowledge\"))\nconst OptionAbout = lazy(() => import(\"./option-settings-about\"))\nconst OptionRagSettings = lazy(() => import(\"./option-rag\"))\nconst OptionOpenAI = lazy(() => import(\"./option-settings-openai\"))\nconst OptionMCP = lazy(() => import(\"./option-settings-mcp\"))\n\nexport const OptionRoutingFirefox = () => {\n  return (\n    <Routes>\n      <Route path=\"/\" element={<OptionIndex />} />\n      <Route path=\"/settings\" element={<OptionSettings />} />\n      <Route path=\"/settings/model\" element={<OptionModal />} />\n      <Route path=\"/settings/prompt\" element={<OptionPrompt />} />\n      <Route path=\"/settings/ollama\" element={<OptionOllamaSettings />} />\n      <Route path=\"/settings/openai\" element={<OptionOpenAI />} />\n      <Route path=\"/settings/mcp\" element={<OptionMCP />} />\n      <Route path=\"/settings/share\" element={<OptionShare />} />\n      <Route path=\"/settings/knowledge\" element={<OptionKnowledgeBase />} />\n      <Route path=\"/settings/about\" element={<OptionAbout />} />\n      <Route path=\"/settings/rag\" element={<OptionRagSettings />} />\n    </Routes>\n  )\n}\n\nexport const SidepanelRoutingFirefox = () => {\n  return (\n    <Routes>\n      <Route path=\"/\" element={<SidepanelChat />} />\n      <Route path=\"/settings\" element={<SidepanelSettings />} />\n      <Route path=\"/settings/openai\" element={<SidepanelSettingsOpenAI />} /> \n      <Route path=\"/settings/model\" element={<SidepanelSettingsModel />} /> \n    </Routes>\n  )\n}\n"
  },
  {
    "path": "src/routes/option-index.tsx",
    "content": "import { useStorage } from \"@plasmohq/storage/hook\"\nimport OptionLayout from \"~/components/Layouts/Layout\"\nimport { Playground } from \"~/components/Option/Playground/Playground\"\n\nconst OptionIndex = () => {\n  return (\n    <OptionLayout>\n      <Playground />\n    </OptionLayout>\n  )\n}\n\nexport default OptionIndex\n"
  },
  {
    "path": "src/routes/option-rag.tsx",
    "content": "import { SettingsLayout } from \"~/components/Layouts/SettingsOptionLayout\"\nimport OptionLayout from \"~/components/Layouts/Layout\"\nimport { RagSettings } from \"@/components/Option/Settings/rag\"\n\nconst OptionRagSettings = () => {\n  return (\n    <OptionLayout>\n      <SettingsLayout>\n        <RagSettings />\n      </SettingsLayout>\n    </OptionLayout>\n  )\n}\n\nexport default OptionRagSettings\n"
  },
  {
    "path": "src/routes/option-settings-about.tsx",
    "content": "import { SettingsLayout } from \"~/components/Layouts/SettingsOptionLayout\"\nimport OptionLayout from \"~/components/Layouts/Layout\"\nimport { AboutApp } from \"@/components/Option/Settings/about\"\n\nconst OptionAbout = () => {\n  return (\n    <OptionLayout>\n      <SettingsLayout>\n        <AboutApp />\n      </SettingsLayout>\n    </OptionLayout>\n  )\n}\n\nexport default OptionAbout\n"
  },
  {
    "path": "src/routes/option-settings-chrome.tsx",
    "content": "import { SettingsLayout } from \"~/components/Layouts/SettingsOptionLayout\"\nimport OptionLayout from \"~/components/Layouts/Layout\"\nimport { ChromeApp } from \"@/components/Option/Settings/chrome\"\n\nconst OptionChrome = () => {\n  return (\n    <OptionLayout>\n      <SettingsLayout>\n        <ChromeApp />\n      </SettingsLayout>\n    </OptionLayout>\n  )\n}\n\nexport default OptionChrome\n"
  },
  {
    "path": "src/routes/option-settings-knowledge.tsx",
    "content": "import { SettingsLayout } from \"~/components/Layouts/SettingsOptionLayout\"\nimport OptionLayout from \"~/components/Layouts/Layout\"\nimport { KnowledgeSettings } from \"@/components/Option/Knowledge\"\n\n const OptionKnowledgeBase = () => {\n  return (\n    <OptionLayout>\n      <SettingsLayout>\n        <KnowledgeSettings />\n      </SettingsLayout>\n    </OptionLayout>\n  )\n}\n\nexport default OptionKnowledgeBase"
  },
  {
    "path": "src/routes/option-settings-mcp.tsx",
    "content": "import { SettingsLayout } from \"~/components/Layouts/SettingsOptionLayout\"\nimport OptionLayout from \"~/components/Layouts/Layout\"\nimport { MCPSettingsApp } from \"@/components/Option/Settings/mcp\"\n\nconst OptionMCP = () => {\n  return (\n    <OptionLayout>\n      <SettingsLayout>\n        <MCPSettingsApp />\n      </SettingsLayout>\n    </OptionLayout>\n  )\n}\n\nexport default OptionMCP\n"
  },
  {
    "path": "src/routes/option-settings-model.tsx",
    "content": "import { SettingsLayout } from \"~/components/Layouts/SettingsOptionLayout\"\nimport OptionLayout from \"~/components/Layouts/Layout\"\nimport { ModelsBody } from \"~/components/Option/Models\"\n\nconst OptionModal = () => {\n  return (\n    <OptionLayout>\n      <SettingsLayout>\n        <ModelsBody />\n      </SettingsLayout>\n    </OptionLayout>\n  )\n}\n\nexport default OptionModal\n"
  },
  {
    "path": "src/routes/option-settings-openai.tsx",
    "content": "import { SettingsLayout } from \"~/components/Layouts/SettingsOptionLayout\"\nimport OptionLayout from \"~/components/Layouts/Layout\"\nimport { OpenAIApp } from \"@/components/Option/Settings/openai\"\n\nconst OptionOpenAI = () => {\n  return (\n    <OptionLayout>\n      <SettingsLayout>\n        <OpenAIApp />\n      </SettingsLayout>\n    </OptionLayout>\n  )\n}\n\nexport default OptionOpenAI\n"
  },
  {
    "path": "src/routes/option-settings-prompt.tsx",
    "content": "import { SettingsLayout } from \"~/components/Layouts/SettingsOptionLayout\"\nimport OptionLayout from \"~/components/Layouts/Layout\"\nimport { PromptBody } from \"~/components/Option/Prompt\"\n\n const OptionPrompt = () => {\n  return (\n    <OptionLayout>\n      <SettingsLayout>\n        <PromptBody />\n      </SettingsLayout>\n    </OptionLayout>\n  )\n}\n\nexport default OptionPrompt"
  },
  {
    "path": "src/routes/option-settings-share.tsx",
    "content": "import { SettingsLayout } from \"~/components/Layouts/SettingsOptionLayout\"\nimport OptionLayout from \"~/components/Layouts/Layout\"\nimport { OptionShareBody } from \"~/components/Option/Share\"\n\n const OptionShare = () => {\n  return (\n    <OptionLayout>\n      <SettingsLayout>\n        <OptionShareBody />\n      </SettingsLayout>\n    </OptionLayout>\n  )\n}\n\nexport default OptionShare"
  },
  {
    "path": "src/routes/option-settings.tsx",
    "content": "import { SettingsLayout } from \"~/components/Layouts/SettingsOptionLayout\"\nimport OptionLayout from \"~/components/Layouts/Layout\"\nimport { GeneralSettings } from \"~/components/Option/Settings/general-settings\"\n\n const OptionSettings = () => {\n  return (\n    <OptionLayout>\n      <SettingsLayout>\n        <GeneralSettings />\n      </SettingsLayout>\n    </OptionLayout>\n  )\n}\n\nexport default OptionSettings"
  },
  {
    "path": "src/routes/options-settings-ollama.tsx",
    "content": "import { SettingsLayout } from \"~/components/Layouts/SettingsOptionLayout\"\nimport OptionLayout from \"~/components/Layouts/Layout\"\nimport { SettingsOllama } from \"~/components/Option/Settings/ollama\"\n\n const OptionOllamaSettings = () => {\n  return (\n    <OptionLayout>\n      <SettingsLayout>\n        <SettingsOllama />\n      </SettingsLayout>\n    </OptionLayout>\n  )\n}\n\nexport default OptionOllamaSettings"
  },
  {
    "path": "src/routes/sidepanel-chat.tsx",
    "content": "import {\n  formatToChatHistory,\n  formatToMessage,\n  getRecentChatFromCopilot,\n  getPromptById\n} from \"@/db/dexie/helpers\"\nimport useBackgroundMessage from \"@/hooks/useBackgroundMessage\"\nimport { useMigration } from \"@/hooks/useMigration\"\nimport { useSmartScroll } from \"@/hooks/useSmartScroll\"\nimport {\n  useChatShortcuts,\n  useSidebarShortcuts,\n  useChatModeShortcuts\n} from \"@/hooks/keyboard/useKeyboardShortcuts\"\nimport { copilotResumeLastChat } from \"@/services/app\"\nimport { Storage } from \"@plasmohq/storage\"\nimport { useStorage } from \"@plasmohq/storage/hook\"\nimport { notification } from \"antd\"\nimport { ChevronDown } from \"lucide-react\"\nimport React from \"react\"\nimport { useTranslation } from \"react-i18next\"\nimport { SidePanelBody } from \"~/components/Sidepanel/Chat/body\"\nimport { SidepanelForm } from \"~/components/Sidepanel/Chat/form\"\nimport { SidepanelHeader } from \"~/components/Sidepanel/Chat/header\"\nimport { useMessage } from \"~/hooks/useMessage\"\nimport { useStoreChatModelSettings } from \"@/store/model\"\n\nconst SidepanelChat = () => {\n  const drop = React.useRef<HTMLDivElement>(null)\n  const [dropedFile, setDropedFile] = React.useState<File | undefined>()\n  const [sidebarOpen, setSidebarOpen] = React.useState(false)\n  const { t } = useTranslation([\"playground\"])\n  const [dropState, setDropState] = React.useState<\n    \"idle\" | \"dragging\" | \"error\"\n  >(\"idle\")\n\n  const [defaultCopilotPrompt] = useStorage(\"defaultCopilotPrompt\", undefined)\n\n  useMigration()\n  const {\n    streaming,\n    onSubmit,\n    messages,\n    setHistory,\n    setHistoryId,\n    historyId,\n    setMessages,\n    selectedModel,\n    defaultChatWithWebsite,\n    chatMode,\n    setChatMode,\n    setTemporaryChat,\n    sidepanelTemporaryChat,\n    clearChat,\n    setSelectedSystemPrompt,\n    setSelectedQuickPrompt\n  } = useMessage()\n  const { containerRef, isAutoScrollToBottom, autoScrollToBottom } =\n    useSmartScroll(messages, streaming, 100, historyId)\n\n  const toggleSidebar = () => {\n    setSidebarOpen((prev) => !prev)\n  }\n\n  const toggleChatMode = () => {\n    setChatMode(chatMode === \"rag\" ? \"normal\" : \"rag\")\n  }\n\n  useChatShortcuts(clearChat, true)\n  useSidebarShortcuts(toggleSidebar, true)\n  useChatModeShortcuts(toggleChatMode, true)\n\n  const [chatBackgroundImage] = useStorage({\n    key: \"chatBackgroundImage\",\n    instance: new Storage({\n      area: \"local\"\n    })\n  })\n  const bgMsg = useBackgroundMessage()\n\n  const setRecentMessagesOnLoad = async () => {\n    const isEnabled = await copilotResumeLastChat()\n    if (!isEnabled) {\n      return\n    }\n    if (messages.length === 0) {\n      const recentChat = await getRecentChatFromCopilot()\n      if (recentChat) {\n        setHistoryId(recentChat.history.id)\n        setHistory(formatToChatHistory(recentChat.messages))\n        setMessages(formatToMessage(recentChat.messages))\n      }\n    }\n  }\n\n  React.useEffect(() => {\n    if (!drop.current) {\n      return\n    }\n    const handleDragOver = (e: DragEvent) => {\n      e.preventDefault()\n      e.stopPropagation()\n    }\n\n    const handleDrop = (e: DragEvent) => {\n      e.preventDefault()\n      e.stopPropagation()\n\n      setDropState(\"idle\")\n\n      const files = Array.from(e.dataTransfer?.files || [])\n\n      const isImage = files.every((file) => file.type.startsWith(\"image/\"))\n\n      if (!isImage) {\n        setDropState(\"error\")\n        return\n      }\n\n      const newFiles = Array.from(e.dataTransfer?.files || []).slice(0, 1)\n      if (newFiles.length > 0) {\n        setDropedFile(newFiles[0])\n      }\n    }\n\n    const handleDragEnter = (e: DragEvent) => {\n      e.preventDefault()\n      e.stopPropagation()\n      setDropState(\"dragging\")\n    }\n\n    const handleDragLeave = (e: DragEvent) => {\n      e.preventDefault()\n      e.stopPropagation()\n      setDropState(\"idle\")\n    }\n\n    drop.current.addEventListener(\"dragover\", handleDragOver)\n    drop.current.addEventListener(\"drop\", handleDrop)\n    drop.current.addEventListener(\"dragenter\", handleDragEnter)\n    drop.current.addEventListener(\"dragleave\", handleDragLeave)\n\n    return () => {\n      if (drop.current) {\n        drop.current.removeEventListener(\"dragover\", handleDragOver)\n        drop.current.removeEventListener(\"drop\", handleDrop)\n        drop.current.removeEventListener(\"dragenter\", handleDragEnter)\n        drop.current.removeEventListener(\"dragleave\", handleDragLeave)\n      }\n    }\n  }, [])\n\n  React.useEffect(() => {\n    const loadDefaultPrompt = async () => {\n      if (defaultCopilotPrompt && messages.length === 0) {\n        try {\n          const prompt = await getPromptById(defaultCopilotPrompt)\n          console.log(\"Loaded default copilot prompt:\", prompt)\n          if (prompt.is_system) {\n            setSelectedSystemPrompt(prompt.id)\n          } else {\n            setSelectedSystemPrompt(undefined)\n            setSelectedQuickPrompt(prompt!.content)\n          }\n        } catch (error) {\n          console.error(\"Failed to load default prompt:\", error)\n        }\n      }\n    }\n\n    loadDefaultPrompt()\n  }, [defaultCopilotPrompt])\n\n  React.useEffect(() => {\n    setRecentMessagesOnLoad()\n  }, [])\n\n  React.useEffect(() => {\n    if (defaultChatWithWebsite) {\n      setChatMode(\"rag\")\n    }\n    if (sidepanelTemporaryChat) {\n      setTemporaryChat(true)\n    }\n  }, [defaultChatWithWebsite, sidepanelTemporaryChat])\n\n  React.useEffect(() => {\n    if (bgMsg && !streaming) {\n      if (selectedModel) {\n        if (bgMsg.type === \"yt_summarize\") {\n          onSubmit({\n            message: bgMsg.text,\n            image: \"\",\n            chatType: \"youtube\"\n          })\n        } else {\n          onSubmit({\n            message: bgMsg.text,\n            messageType: bgMsg.type,\n            image: \"\"\n          })\n        }\n      } else {\n        notification.error({\n          message: t(\"formError.noModel\")\n        })\n      }\n    }\n  }, [bgMsg])\n\n  return (\n    <div className=\"flex h-full w-full\">\n      <main className=\"relative h-dvh w-full\">\n        <div className=\"relative z-20 w-full\">\n          <SidepanelHeader\n            sidebarOpen={sidebarOpen}\n            setSidebarOpen={setSidebarOpen}\n          />\n        </div>\n        <div\n          ref={drop}\n          className={`relative flex h-full flex-col items-center ${\n            dropState === \"dragging\" ? \"bg-gray-100 dark:bg-gray-800\" : \"\"\n          } bg-white dark:bg-[#1a1a1a]`}\n          style={\n            chatBackgroundImage\n              ? {\n                  backgroundImage: `url(${chatBackgroundImage})`,\n                  backgroundSize: \"cover\",\n                  backgroundPosition: \"center\",\n                  backgroundRepeat: \"no-repeat\"\n                }\n              : {}\n          }>\n          {/* Background overlay for opacity effect */}\n          {chatBackgroundImage && (\n            <div\n              className=\"absolute inset-0 bg-white dark:bg-[#1a1a1a]\"\n              style={{ opacity: 0.9, pointerEvents: \"none\" }}\n            />\n          )}\n\n          <div\n            ref={containerRef}\n            className=\"custom-scrollbar flex h-full w-full flex-col items-center overflow-x-hidden overflow-y-auto px-5 relative z-10\">\n            <SidePanelBody />\n          </div>\n\n          <div className=\"absolute bottom-0 w-full z-10\">\n            {!isAutoScrollToBottom && (\n              <div className=\"fixed bottom-32 z-20 left-0 right-0 flex justify-center\">\n                <button\n                  onClick={() => autoScrollToBottom()}\n                  className=\"bg-gray-50 shadow border border-gray-200 dark:border-none dark:bg-white/20 p-1.5 rounded-full pointer-events-auto hover:bg-gray-100 dark:hover:bg-white/30 transition-colors\">\n                  <ChevronDown className=\"size-4 text-gray-600 dark:text-gray-300\" />\n                </button>\n              </div>\n            )}\n            <SidepanelForm dropedFile={dropedFile} />\n          </div>\n        </div>\n      </main>\n    </div>\n  )\n}\n\nexport default SidepanelChat\n"
  },
  {
    "path": "src/routes/sidepanel-settings-model.tsx",
    "content": "import { SidePanelSettingsLayout } from \"@/components/Layouts/SidePanelSettingsLayout\"\nimport { ModelsBody } from \"@/components/Option/Models\"\n\nconst SidepanelSettingsModel = () => {\n  return (\n    <SidePanelSettingsLayout>\n      <ModelsBody />\n    </SidePanelSettingsLayout>\n  )\n}\n\nexport default SidepanelSettingsModel\n"
  },
  {
    "path": "src/routes/sidepanel-settings-openai.tsx",
    "content": "import { SidePanelSettingsLayout } from \"@/components/Layouts/SidePanelSettingsLayout\"\nimport { OpenAIApp } from \"@/components/Option/Settings/openai\"\n\nconst SidepanelSettingsOpenAI = () => {\n  return (\n    <SidePanelSettingsLayout>\n      <OpenAIApp />\n    </SidePanelSettingsLayout>\n  )\n}\n\nexport default SidepanelSettingsOpenAI\n"
  },
  {
    "path": "src/routes/sidepanel-settings.tsx",
    "content": "import { SidePanelSettingsLayout } from \"@/components/Layouts/SidePanelSettingsLayout\"\nimport { SettingsBody } from \"~/components/Sidepanel/Settings/body\"\n\nconst SidepanelSettings = () => {\n  return (\n      <SidePanelSettingsLayout>\n        <SettingsBody />\n      </SidePanelSettingsLayout>\n  )\n}\n\nexport default SidepanelSettings\n"
  },
  {
    "path": "src/services/action.ts",
    "content": "import { Storage } from \"@plasmohq/storage\"\n\nconst storage = new Storage({\n    area: \"local\"\n})\n\n\nexport const getInitialConfig = async () => {\n    const actionIconClickValue = await storage.get(\"actionIconClick\")\n    const contextMenuClickValue = await storage.get(\"contextMenuClick\")\n\n    let actionIconClick = actionIconClickValue || \"webui\"\n    let contextMenuClick = contextMenuClickValue || \"sidePanel\"\n\n    return {\n        actionIconClick,\n        contextMenuClick\n    }\n\n}\n\nexport const getActionIconClick = async () => {\n    const actionIconClickValue = await storage.get(\"actionIconClick\")\n    return actionIconClickValue || \"webui\"\n}\n"
  },
  {
    "path": "src/services/app.ts",
    "content": "import { Storage } from \"@plasmohq/storage\"\nconst storage = new Storage()\nconst storage2 = new Storage({\n  area: \"local\"\n})\n\nconst DEFAULT_URL_REWRITE_URL = \"http://127.0.0.1:11434\"\n\nexport const isUrlRewriteEnabled = async () => {\n  const enabled = await storage.get<boolean | undefined>(\"urlRewriteEnabled\")\n  return enabled ?? false\n}\nexport const setUrlRewriteEnabled = async (enabled: boolean) => {\n  await storage.set(\"urlRewriteEnabled\", enabled)\n}\n\nexport const getIsAutoCORSFix = async () => {\n  try {\n    const enabled = await storage2.get<boolean | undefined>(\"autoCORSFix\")\n    return enabled ?? true\n  } catch (e) {\n    return true\n  }\n}\n\nexport const setAutoCORSFix = async (enabled: boolean) => {\n  await storage2.set(\"autoCORSFix\", enabled)\n}\n\nexport const getOllamaEnabled = async () => {\n  try {\n    const enabled = await storage.get<boolean | undefined>(\n      \"ollamaEnabledStatus\"\n    )\n    return enabled ?? true\n  } catch (e) {\n    return true\n  }\n}\n\nexport const setOllamaEnabled = async (enabled: boolean) => {\n  await storage.set(\"ollamaEnabledStatus\", enabled)\n}\n\nexport const getRewriteUrl = async () => {\n  const rewriteUrl = await storage.get(\"rewriteUrl\")\n  if (!rewriteUrl || rewriteUrl.trim() === \"\") {\n    return DEFAULT_URL_REWRITE_URL\n  }\n  return rewriteUrl\n}\n\nexport const setRewriteUrl = async (url: string) => {\n  await storage.set(\"rewriteUrl\", url)\n}\n\nexport const getAdvancedOllamaSettings = async () => {\n  const [isEnableRewriteUrl, rewriteUrl, autoCORSFix] = await Promise.all([\n    isUrlRewriteEnabled(),\n    getRewriteUrl(),\n    getIsAutoCORSFix()\n  ])\n\n  return {\n    isEnableRewriteUrl,\n    rewriteUrl,\n    autoCORSFix\n  }\n}\n\nexport const copilotResumeLastChat = async () => {\n  return await storage.get<boolean>(\"copilotResumeLastChat\")\n}\n\nexport const webUIResumeLastChat = async () => {\n  return await storage.get<boolean>(\"webUIResumeLastChat\")\n}\n\nexport const defaultSidebarOpen = async () => {\n  const sidebarOpen = await storage.get(\"sidebarOpen\")\n  if (!sidebarOpen || sidebarOpen === \"\") {\n    return \"right_clk\"\n  }\n  return sidebarOpen\n}\n\nexport const setSidebarOpen = async (sidebarOpen: string) => {\n  await storage.set(\"sidebarOpen\", sidebarOpen)\n}\n\nexport const customOllamaHeaders = async (): Promise<\n  { key: string; value: string }[]\n> => {\n  const headers = await storage.get<\n    { key: string; value: string }[] | undefined\n  >(\"customOllamaHeaders\")\n  if (!headers) {\n    return []\n  }\n  return headers\n}\n\nexport const setCustomOllamaHeaders = async (headers: string[]) => {\n  await storage.set(\"customOllamaHeaders\", headers)\n}\n\nexport const getCustomOllamaHeaders = async (): Promise<\n  Record<string, string>\n> => {\n  const headers = await customOllamaHeaders()\n\n  const headerMap: Record<string, string> = {}\n\n  for (const header of headers) {\n    headerMap[header.key] = header.value\n  }\n\n  return headerMap\n}\n\nexport const getOpenOnIconClick = async (): Promise<string> => {\n  const openOnIconClick = await storage.get<string>(\"openOnIconClick\")\n  return openOnIconClick || \"webUI\"\n}\n\nexport const setOpenOnIconClick = async (\n  option: \"webUI\" | \"sidePanel\"\n): Promise<void> => {\n  await storage.set(\"openOnIconClick\", option)\n}\n\nexport const getOpenOnRightClick = async (): Promise<string> => {\n  const openOnRightClick = await storage.get<string>(\"openOnRightClick\")\n  return openOnRightClick || \"sidePanel\"\n}\n\nexport const setOpenOnRightClick = async (\n  option: \"webUI\" | \"sidePanel\"\n): Promise<void> => {\n  await storage.set(\"openOnRightClick\", option)\n}\n\nexport const getTotalFilePerKB = async (): Promise<number> => {\n  const totalFilePerKB = await storage.get<number>(\"totalFilePerKB\")\n  return totalFilePerKB || 5\n}\n\nexport const setTotalFilePerKB = async (\n  totalFilePerKB: number\n): Promise<void> => {\n  await storage.set(\"totalFilePerKB\", totalFilePerKB)\n}\n\nexport const getNoOfRetrievedDocs = async (): Promise<number> => {\n  const noOfRetrievedDocs = await storage.get<number>(\"noOfRetrievedDocs\")\n  return noOfRetrievedDocs || 4\n}\n\nexport const setNoOfRetrievedDocs = async (\n  noOfRetrievedDocs: number\n): Promise<void> => {\n  await storage.set(\"noOfRetrievedDocs\", noOfRetrievedDocs)\n}\n\nexport const isRemoveReasoningTagFromCopy = async (): Promise<boolean> => {\n  const removeReasoningTagFromCopy = await storage.get<boolean>(\n    \"removeReasoningTagFromCopy\"\n  )\n  return removeReasoningTagFromCopy ?? true\n}\n\nexport const getStorageSyncEnabled = async (): Promise<boolean> => {\n  const enabled = await storage2.get<boolean>(\"storageSyncEnabled\")\n  return enabled ?? true\n}\n\nexport const setStorageSyncEnabled = async (\n  enabled: boolean\n): Promise<void> => {\n  await storage2.set(\"storageSyncEnabled\", enabled)\n}\n"
  },
  {
    "path": "src/services/application.ts",
    "content": "import { Storage } from \"@plasmohq/storage\"\nimport { browser } from \"wxt/browser\"\nconst storage = new Storage()\n\nconst DEFAULT_SUMMARY_PROMPT = `Provide a concise summary of the following text, capturing its main ideas and key points:\n\nText:\n---------\n{text}\n---------\n\nSummarize the text in no more than 3-4 sentences.\n\nResponse:`\n\nconst DEFAULT_REPHRASE_PROMPT = `Rewrite the following text in a different way, maintaining its original meaning but using alternative vocabulary and sentence structures:\n\nText:\n---------\n{text}\n---------\n\nEnsure that your rephrased version conveys the same information and intent as the original.\n\nResponse:`\n\nconst DEFAULT_TRANSLATE_PROMPT = `Translate the following text from its original language into \"english\". Maintain the tone and style of the original text as much as possible:\n\nText:\n---------\n{text}\n---------\n\nResponse:`\n\nconst DEFAULT_EXPLAIN_PROMPT = `Provide a detailed explanation of the following text, breaking down its key concepts, implications, and context:\n\nText:\n---------\n{text}\n---------\n\nYour explanation should:\n\nClarify any complex terms or ideas\nProvide relevant background information\nDiscuss the significance or implications of the content\nAddress any potential questions a reader might have\nUse examples or analogies to illustrate points when appropriate\n\nAim for a comprehensive explanation that would help someone with little prior knowledge fully understand the text.\n\nResponse:`\n\nconst DEFAULT_CUSTOM_PROMPT = `{text}`\n\nexport const getSummaryPrompt = async () => {\n    return (await storage.get(\"copilotSummaryPrompt\")) || DEFAULT_SUMMARY_PROMPT\n}\n\nexport const setSummaryPrompt = async (prompt: string) => {\n    await storage.set(\"copilotSummaryPrompt\", prompt)\n}\n\nexport const getRephrasePrompt = async () => {\n    return (await storage.get(\"copilotRephrasePrompt\")) || DEFAULT_REPHRASE_PROMPT\n}\n\nexport const setRephrasePrompt = async (prompt: string) => {\n    await storage.set(\"copilotRephrasePrompt\", prompt)\n}\n\nexport const getTranslatePrompt = async () => {\n    return (\n        (await storage.get(\"copilotTranslatePrompt\")) || DEFAULT_TRANSLATE_PROMPT\n    )\n}\n\nexport const setTranslatePrompt = async (prompt: string) => {\n    await storage.set(\"copilotTranslatePrompt\", prompt)\n}\n\nexport const getExplainPrompt = async () => {\n    return (await storage.get(\"copilotExplainPrompt\")) || DEFAULT_EXPLAIN_PROMPT\n}\n\nexport const setExplainPrompt = async (prompt: string) => {\n    await storage.set(\"copilotExplainPrompt\", prompt)\n}\n\nexport const getCustomPrompt = async () => {\n    return (await storage.get(\"copilotCustomPrompt\")) || DEFAULT_CUSTOM_PROMPT\n}\n\nexport const setCustomPrompt = async (prompt: string) => {\n    await storage.set(\"copilotCustomPrompt\", prompt)\n}\n\nexport const getAllCopilotPrompts = async () => {\n    const [\n        summaryPrompt,\n        rephrasePrompt,\n        translatePrompt,\n        explainPrompt,\n        customPrompt,\n        enabledStates\n    ] = await Promise.all([\n        getSummaryPrompt(),\n        getRephrasePrompt(),\n        getTranslatePrompt(),\n        getExplainPrompt(),\n        getCustomPrompt(),\n        getCopilotPromptsEnabledState()\n    ])\n\n    return [\n        { key: \"summary\", prompt: summaryPrompt, enabled: enabledStates.summary },\n        { key: \"rephrase\", prompt: rephrasePrompt, enabled: enabledStates.rephrase },\n        { key: \"translate\", prompt: translatePrompt, enabled: enabledStates.translate },\n        { key: \"explain\", prompt: explainPrompt, enabled: enabledStates.explain },\n        { key: \"custom\", prompt: customPrompt, enabled: enabledStates.custom }\n    ]\n}\n\nexport const getCopilotPromptsEnabledState = async () => {\n    const state = await storage.get<Record<string, boolean>>(\"copilotPromptsEnabled\")\n    return state || {\n        summary: true,\n        rephrase: true,\n        translate: true,\n        explain: true,\n        custom: true\n    }\n}\n\nexport const toggleCopilotPromptEnabled = async (key: string, enabled: boolean) => {\n    const state = await getCopilotPromptsEnabledState()\n    state[key] = enabled\n    await storage.set(\"copilotPromptsEnabled\", state)\n\n    // Notify background worker to refresh context menus\n    try {\n        await browser.runtime.sendMessage({\n            type: \"refresh_builtin_copilot_menus\"\n        })\n    } catch (e) {\n        // Background worker might not be ready, ignore\n    }\n}\n\nexport const setAllCopilotPrompts = async (\n    prompts: { key: string; prompt: string }[]\n) => {\n    for (const { key, prompt } of prompts) {\n        switch (key) {\n            case \"summary\":\n                await setSummaryPrompt(prompt)\n                break\n            case \"rephrase\":\n                await setRephrasePrompt(prompt)\n                break\n            case \"translate\":\n                await setTranslatePrompt(prompt)\n                break\n            case \"explain\":\n                await setExplainPrompt(prompt)\n                break\n            case \"custom\":\n                await setCustomPrompt(prompt)\n                break\n\n        }\n    }\n}\n\nexport const getPrompt = async (key: string) => {\n    // Check if it's a custom copilot prompt\n    if (key.startsWith(\"custom_copilot_\")) {\n        const customPrompts = await getCustomCopilotPrompts()\n        const promptId = key.replace(\"custom_copilot_\", \"\")\n        const customPrompt = customPrompts.find(p => p.id === promptId)\n        return customPrompt?.prompt || \"\"\n    }\n\n    switch (key) {\n        case \"summary\":\n            return await getSummaryPrompt()\n        case \"rephrase\":\n            return await getRephrasePrompt()\n        case \"translate\":\n            return await getTranslatePrompt()\n        case \"explain\":\n            return await getExplainPrompt()\n        case \"custom\":\n            return await getCustomPrompt()\n        default:\n            return \"\"\n    }\n}\n\n// Custom Copilot Prompts Management\nexport interface CustomCopilotPrompt {\n    id: string\n    title: string\n    prompt: string\n    enabled: boolean\n    createdAt: number\n}\n\nconst generateID = () => {\n    return \"custom_xxxx-xxxx-xxx-xxxx\".replace(/[x]/g, () => {\n        const r = Math.floor(Math.random() * 16)\n        return r.toString(16)\n    })\n}\n\nexport const getCustomCopilotPrompts = async (): Promise<CustomCopilotPrompt[]> => {\n    const prompts = await storage.get<CustomCopilotPrompt[]>(\"customCopilotPrompts\")\n    return prompts || []\n}\n\nexport const saveCustomCopilotPrompt = async (data: {\n    title: string\n    prompt: string\n}) => {\n    const customPrompts = await getCustomCopilotPrompts()\n    const newPrompt: CustomCopilotPrompt = {\n        id: generateID(),\n        title: data.title,\n        prompt: data.prompt,\n        enabled: true,\n        createdAt: Date.now()\n    }\n    customPrompts.push(newPrompt)\n    await storage.set(\"customCopilotPrompts\", customPrompts)\n\n    // Notify background worker to refresh context menus\n    try {\n        await browser.runtime.sendMessage({\n            type: \"refresh_custom_copilot_menus\"\n        })\n    } catch (e) {\n        // Background worker might not be ready, ignore\n    }\n\n    return newPrompt\n}\n\nexport const updateCustomCopilotPrompt = async (\n    id: string,\n    data: Partial<Omit<CustomCopilotPrompt, \"id\" | \"createdAt\">>\n) => {\n    const customPrompts = await getCustomCopilotPrompts()\n    const index = customPrompts.findIndex(p => p.id === id)\n    if (index !== -1) {\n        customPrompts[index] = {\n            ...customPrompts[index],\n            ...data\n        }\n        await storage.set(\"customCopilotPrompts\", customPrompts)\n\n        // Notify background worker to refresh context menus\n        try {\n            await browser.runtime.sendMessage({\n                type: \"refresh_custom_copilot_menus\"\n            })\n        } catch (e) {\n            // Background worker might not be ready, ignore\n        }\n\n        return customPrompts[index]\n    }\n    return null\n}\n\nexport const deleteCustomCopilotPrompt = async (id: string) => {\n    const customPrompts = await getCustomCopilotPrompts()\n    const filtered = customPrompts.filter(p => p.id !== id)\n    await storage.set(\"customCopilotPrompts\", filtered)\n\n    // Notify background worker to refresh context menus\n    try {\n        await browser.runtime.sendMessage({\n            type: \"refresh_custom_copilot_menus\"\n        })\n    } catch (e) {\n        // Background worker might not be ready, ignore\n    }\n\n    return id\n}\n\nexport const toggleCustomCopilotPrompt = async (id: string, enabled: boolean) => {\n    return await updateCustomCopilotPrompt(id, { enabled })\n}\n"
  },
  {
    "path": "src/services/chrome.ts",
    "content": "import { Storage } from \"@plasmohq/storage\"\n\nconst storage = new Storage()\n\nconst DEFAULT_CHROME_AI_MODEL = {\n  name: \"Gemini Nano\",\n  model: \"chrome::gemini-nano::page-assist\",\n  modified_at: \"\",\n  provider: \"chrome\",\n  size: 0,\n  digest: \"\",\n  nickname: \"Gemini Nano\",\n  avatar: undefined,\n  details: {\n    parent_model: \"\",\n    format: \"\",\n    family: \"\",\n    families: [],\n    parameter_size: \"\",\n    quantization_level: \"\"\n  }\n}\n\nexport const getChromeAIStatus = async (): Promise<boolean> => {\n  const aiStatus = await storage.get<boolean | undefined>(\"chromeAIStatus\")\n  return aiStatus ?? false\n}\n\nexport const setChromeAIStatus = async (status: boolean): Promise<void> => {\n  await storage.set(\"chromeAIStatus\", status)\n}\n\nexport const getChromeAIModel = async () => {\n  const isEnable = await getChromeAIStatus()\n  if (isEnable) {\n    return [DEFAULT_CHROME_AI_MODEL]\n  } else {\n    return []\n  }\n}\n"
  },
  {
    "path": "src/services/elevenlabs.ts",
    "content": "import axios from 'axios';\nexport interface Voice {\n  voice_id: string;\n  name: string;\n}\n\nexport interface Model {\n  model_id: string;\n  name: string;\n}\n\nconst BASE_URL = 'https://api.elevenlabs.io/v1';\n\nexport const getVoices = async (apiKey: string): Promise<Voice[]> => {\n  const response = await axios.get(`${BASE_URL}/voices`, {\n    headers: { 'xi-api-key': apiKey }\n  });\n  return response.data.voices;\n};\n\nexport const getModels = async (apiKey: string): Promise<Model[]> => {\n  const response = await axios.get(`${BASE_URL}/models`, {\n    headers: { 'xi-api-key': apiKey }\n  });\n  return response.data;\n};\n\nexport const generateSpeech = async (\n  apiKey: string,\n  text: string,\n  voiceId: string,\n  modelId: string\n): Promise<ArrayBuffer> => {\n  const response = await axios.post(\n    `${BASE_URL}/text-to-speech/${voiceId}`,\n    {\n      text,\n      model_id: modelId,\n    },\n    {\n      headers: {\n        'xi-api-key': apiKey,\n        'Content-Type': 'application/json',\n      },\n      responseType: 'arraybuffer',\n    }\n  );\n  return response.data;\n};"
  },
  {
    "path": "src/services/kb.ts",
    "content": "import { Storage } from \"@plasmohq/storage\"\n\nconst storage = new Storage()\n\nexport const isChatWithWebsiteEnabled = async (): Promise<boolean> => {\n    const isChatWithWebsiteEnabled = await storage.get<boolean | undefined>(\n        \"chatWithWebsiteEmbedding\"\n    )\n    return isChatWithWebsiteEnabled ?? false\n}\n\n\nexport const getMaxContextSize = async (): Promise<number> => {\n    const maxWebsiteContext = await storage.get<number | undefined>(\n        \"maxWebsiteContext\"\n    )\n    return maxWebsiteContext ?? 7028\n}"
  },
  {
    "path": "src/services/model-settings.ts",
    "content": "import { Storage } from \"@plasmohq/storage\"\nconst storage = new Storage({\n  area: \"local\"\n})\n\nconst storage2 = new Storage()\n\ntype ModelSettings = {\n  f16KV?: boolean\n  frequencyPenalty?: number\n  keepAlive?: string\n  logitsAll?: boolean\n  mirostat?: number\n  mirostatEta?: number\n  mirostatTau?: number\n  numBatch?: number\n  numCtx?: number\n  numGpu?: number\n  numGqa?: number\n  numKeep?: number\n  numPredict?: number\n  numThread?: number\n  penalizeNewline?: boolean\n  presencePenalty?: number\n  repeatLastN?: number\n  repeatPenalty?: number\n  ropeFrequencyBase?: number\n  ropeFrequencyScale?: number\n  temperature?: number\n  tfsZ?: number\n  topK?: number\n  topP?: number\n  typicalP?: number\n  useMLock?: boolean\n  useMMap?: boolean\n  vocabOnly?: boolean\n  minP?: number\n  useMlock?: boolean\n  reasoningEffort?: any\n  thinking?: boolean | \"low\" | \"medium\" | \"high\"\n}\n\nconst keys = [\n  \"f16KV\",\n  \"frequencyPenalty\",\n  \"keepAlive\",\n  \"logitsAll\",\n  \"mirostat\",\n  \"mirostatEta\",\n  \"mirostatTau\",\n  \"numBatch\",\n  \"numCtx\",\n  \"numGpu\",\n  \"numGqa\",\n  \"numKeep\",\n  \"numPredict\",\n  \"numThread\",\n  \"penalizeNewline\",\n  \"presencePenalty\",\n  \"repeatLastN\",\n  \"repeatPenalty\",\n  \"ropeFrequencyBase\",\n  \"ropeFrequencyScale\",\n  \"temperature\",\n  \"tfsZ\",\n  \"topK\",\n  \"topP\",\n  \"typicalP\",\n  \"useMLock\",\n  \"useMMap\",\n  \"vocabOnly\",\n  \"minP\",\n  \"useMlock\",\n  \"reasoningEffort\"\n]\n\nexport const getAllModelSettings = async () => {\n  try {\n    const settings: ModelSettings = {}\n    for (const key of keys) {\n      const value = await storage.get(key)\n      settings[key] = value\n      // if (!value && key === \"keepAlive\") {\n      //   settings[key] = \"5m\"\n      // }\n    }\n    return settings\n  } catch (error) {\n    console.error(error)\n    return {}\n  }\n}\n\nexport const setModelSetting = async (\n  key: string,\n  value: string | number | boolean\n) => {\n  await storage.set(key, value)\n}\n\nexport const getAllDefaultModelSettings = async (): Promise<ModelSettings> => {\n  const settings: ModelSettings = {}\n  for (const key of keys) {\n    const value = await storage.get(key)\n    settings[key] = value\n    // if (!value && key === \"keepAlive\") {\n    //   settings[key] = \"5m\"\n    // }\n  }\n  return settings\n}\n\nexport const lastUsedChatModelEnabled = async (): Promise<boolean> => {\n  const isLastUsedChatModelEnabled = await storage2.get<boolean | undefined>(\n    \"restoreLastChatModel\"\n  )\n  return isLastUsedChatModelEnabled ?? false\n}\n\nexport const setLastUsedChatModelEnabled = async (\n  enabled: boolean\n): Promise<void> => {\n  await storage.set(\"restoreLastChatModel\", enabled)\n}\n\nexport const getLastUsedChatModel = async (\n  historyId: string\n): Promise<string | undefined> => {\n  return await storage.get<string | undefined>(`lastUsedChatModel-${historyId}`)\n}\n\nexport const setLastUsedChatModel = async (\n  historyId: string,\n  model: string\n): Promise<void> => {\n  await storage.set(`lastUsedChatModel-${historyId}`, model)\n}\n\n\nexport const getLastUsedChatSystemPrompt = async (\n  historyId: string\n): Promise<{ prompt_id?: string; prompt_content?: string } | undefined> => {\n  return await storage.get<{ prompt_id?: string; prompt_content?: string } | undefined>(\n    `lastUsedChatSystemPrompt-${historyId}`\n  )\n}\n\nexport const setLastUsedChatSystemPrompt = async (\n  historyId: string,\n  prompt: {\n    prompt_id?: string\n    prompt_content?: string\n  }\n): Promise<void> => {\n  await storage.set(`lastUsedChatSystemPrompt-${historyId}`, prompt)\n}\n\n\nexport const getModelSettings = async (model_id: string) => {\n  try {\n    const settings = await storage.get<ModelSettings>(`modelSettings:${model_id}`)\n    if (!settings) {\n      return {}\n    }\n    return settings\n  } catch (error) {\n    console.error(error)\n    return {}\n  }\n}\n\nexport const setModelSettings = async ({model_id,settings}: {model_id: string, settings: Partial<ModelSettings>}) => {\n  try {\n    await storage.set(`modelSettings:${model_id}`, settings)\n  } catch (error) {\n    console.error(error)\n  }\n}"
  },
  {
    "path": "src/services/ocr.ts",
    "content": "import { getDefaultOcrLanguage } from \"@/data/ocr-language\"\nimport { useStoreChatModelSettings } from \"@/store/model\"\nimport { Storage } from \"@plasmohq/storage\"\n\nconst storage = new Storage()\n\n\nexport const getOCRLanguage = async () => {\n    const data = await storage.get<string | undefined | null>(\"defaultOCRLanguage\")\n    if (!data || data.length === 0) {\n        return getDefaultOcrLanguage()\n    }\n    return data\n}\n\nexport const getOCRLanguageToUse = async () => {\n    const currentChatModelSettings = useStoreChatModelSettings.getState()\n    if (currentChatModelSettings?.ocrLanguage) {\n        return currentChatModelSettings.ocrLanguage\n    }\n\n    const defaultOCRLanguage = await getOCRLanguage()\n    return defaultOCRLanguage\n}\n\n\nexport const isOfflineOCR = (lang: string) => {\n    return lang !== \"eng-fast\"\n}"
  },
  {
    "path": "src/services/ollama.ts",
    "content": "import { Storage } from \"@plasmohq/storage\"\nimport { cleanUrl } from \"../libs/clean-url\"\nimport { urlRewriteRuntime } from \"../libs/runtime\"\nimport { getChromeAIModel } from \"./chrome\"\nimport {\n  getOllamaEnabled,\n  setNoOfRetrievedDocs,\n  setTotalFilePerKB\n} from \"./app\"\nimport fetcher from \"@/libs/fetcher\"\nimport { ollamaFormatAllCustomModels } from \"@/db/dexie/models\"\nimport { getAllModelNicknames } from \"@/db/dexie/nickname\"\nimport { getAllModelStates } from \"@/db/dexie/modelState\"\n\nconst storage = new Storage()\n\nconst DEFAULT_OLLAMA_URL = \"http://127.0.0.1:11434\"\nconst DEFAULT_ASK_FOR_MODEL_SELECTION_EVERY_TIME = true\nconst DEFAULT_PAGE_SHARE_URL = \"https://pageassist.xyz\"\n\nconst DEFAULT_RAG_QUESTION_PROMPT =\n  \"Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.   Chat History: {chat_history} Follow Up Input: {question} Standalone question:\"\n\nconst DEFAUTL_RAG_SYSTEM_PROMPT = `You are a helpful AI assistant. Use the following pieces of context to answer the question at the end. If you don't know the answer, just say you don't know. DO NOT try to make up an answer. If the question is not related to the context, politely respond that you are tuned to only answer questions that are related to the context.  {context}  Question: {question} Helpful answer:`\n\nconst DEFAULT_WEBSEARCH_PROMPT = `You are an AI model who is expert at searching the web and answering user's queries.\n\nGenerate a response that is informative and relevant to the user's query based on provided search results. the current date and time are {current_date_time}.\n\n\\`search-results\\` block provides knowledge from the web search results. You can use this information to generate a meaningful response.\n\n<search-results>\n {search_results}\n</search-results>\n`\n\nconst DEFAULT_WEBSEARCH_FOLLOWUP_PROMPT = `You will rephrase follow-up questions into concise, standalone search queries optimized for internet search engines. Transform conversational questions into keyword-focused search terms by removing unnecessary words, question formats, and context dependencies while preserving the core information need.\n\nONLY RETURN QUERY WITHOUT ANY TEXT\n\nExamples:\nFollow-up question: What are the symptoms of a heart attack?\nheart attack symptoms\n\nFollow-up question: Where is the upcoming Olympics being held?\nupcoming Olympics \n\nFollow-up question: Taylor Swift's latest album?\nTaylor Swift latest album ${new Date().getFullYear()}\n\nFollow-up question: How does photosynthesis work in plants?\nphotosynthesis process plants\n\nFollow-up question: What's the current stock price of Apple?\nApple stock price today\n\nPrevious Conversation:\n{chat_history}\n\nFollow-up question: {question}\n`\n\nexport const getOllamaURL = async () => {\n  const ollamaURL = await storage.get(\"ollamaURL\")\n  if (!ollamaURL || ollamaURL.length === 0) {\n    await urlRewriteRuntime(DEFAULT_OLLAMA_URL)\n    return DEFAULT_OLLAMA_URL\n  }\n  await urlRewriteRuntime(cleanUrl(ollamaURL))\n  return ollamaURL\n}\n\nexport const askForModelSelectionEveryTime = async () => {\n  const askForModelSelectionEveryTime = await storage.get(\n    \"askForModelSelectionEveryTime\"\n  )\n  if (\n    !askForModelSelectionEveryTime ||\n    askForModelSelectionEveryTime.length === 0\n  )\n    return DEFAULT_ASK_FOR_MODEL_SELECTION_EVERY_TIME\n  return askForModelSelectionEveryTime\n}\n\nexport const defaultModel = async () => {\n  const defaultModel = await storage.get(\"defaultModel\")\n  return defaultModel\n}\n\nexport const isOllamaRunning = async () => {\n  try {\n    const baseUrl = await getOllamaURL()\n    const response = await fetcher(`${cleanUrl(baseUrl)}`)\n    if (!response.ok) {\n      throw new Error(response.statusText)\n    }\n    return true\n  } catch (e) {\n    console.error(e)\n    return false\n  }\n}\n\nexport const getAllModels = async ({\n  returnEmpty = false,\n  includeDisabled = false\n}: {\n  returnEmpty?: boolean\n  includeDisabled?: boolean\n}) => {\n  try {\n    const modelNicknames = await getAllModelNicknames()\n    const modelStates = await getAllModelStates()\n    const isEnabled = await getOllamaEnabled()\n\n    if (!isEnabled) {\n      return []\n    }\n\n    const baseUrl = await getOllamaURL()\n    const response = await fetcher(`${cleanUrl(baseUrl)}/api/tags`)\n    if (!response.ok) {\n      if (returnEmpty) {\n        return []\n      }\n      throw new Error(response.statusText)\n    }\n    const json = await response.json()\n\n    const allModels = json.models.map((model: any) => {\n      const isModelEnabled = modelStates[model.name] ?? true\n      return {\n        ...model,\n        nickname: modelNicknames[model.name]?.model_name || model.name,\n        avatar: modelNicknames[model.name]?.model_avatar || undefined,\n        is_enabled: isModelEnabled\n      }\n    }) as {\n      name: string\n      model: string\n      modified_at: string\n      size: number\n      digest: string\n      nickname?: string\n      avatar?: string\n      is_enabled: boolean\n      details: {\n        parent_model: string\n        format: string\n        family: string\n        families: string[]\n        parameter_size: string\n        quantization_level: string\n      }\n    }[]\n\n    // Filter out disabled models unless includeDisabled is true\n    if (includeDisabled) {\n      return allModels\n    }\n\n    return allModels.filter(model => model.is_enabled)\n  } catch (e) {\n    console.error(e)\n    return []\n  }\n}\n\nexport const getSelectedModel = async () => {\n  const selectedModel = await storage.get(\"selectedModel\")\n  return selectedModel\n}\n\nexport const getEmbeddingModels = async ({\n  returnEmpty\n}: {\n  returnEmpty?: boolean\n}) => {\n  try {\n    const ollamaModels = await getAllModels({ returnEmpty })\n    const customModels = await ollamaFormatAllCustomModels(\"embedding\")\n\n    return [\n      ...ollamaModels.map((model) => {\n        return {\n          ...model,\n          provider: \"ollama\"\n        }\n      }),\n      ...customModels\n    ]\n  } catch (e) {\n    console.error(e)\n    return []\n  }\n}\n\nexport const deleteModel = async (model: string) => {\n  const baseUrl = await getOllamaURL()\n  const response = await fetcher(`${cleanUrl(baseUrl)}/api/delete`, {\n    method: \"DELETE\",\n    headers: {\n      \"Content-Type\": \"application/json\"\n    },\n    body: JSON.stringify({ name: model })\n  })\n\n  if (!response.ok) {\n    throw new Error(response.statusText)\n  }\n  return \"ok\"\n}\n\nexport const fetchChatModels = async ({\n  returnEmpty = false\n}: {\n  returnEmpty?: boolean\n}) => {\n  try {\n    const models = await getAllModels({ returnEmpty })\n\n    const chatModels = models\n      ?.filter((model) => {\n        return (\n          !model?.details?.families?.includes(\"bert\") &&\n          !model?.details?.families?.includes(\"nomic-bert\")\n        )\n      })\n      .map((model) => {\n        return {\n          ...model,\n          provider: \"ollama\"\n        }\n      })\n    const chromeModel = await getChromeAIModel()\n\n    const customModels = await ollamaFormatAllCustomModels(\"chat\")\n\n    return [...chatModels, ...chromeModel, ...customModels]\n  } catch (e) {\n    console.error(\"error\", e)\n    const allModels = await getAllModels({ returnEmpty })\n    const models = allModels.map((model) => {\n      return {\n        ...model,\n        provider: \"ollama\"\n      }\n    })\n    const chromeModel = await getChromeAIModel()\n    const customModels = await ollamaFormatAllCustomModels(\"chat\")\n    return [...models, ...chromeModel, ...customModels]\n  }\n}\n\nexport const setOllamaURL = async (ollamaURL: string) => {\n  let formattedUrl = ollamaURL\n  if (formattedUrl.startsWith(\"http://localhost:\")) {\n    formattedUrl = formattedUrl.replace(\n      \"http://localhost:\",\n      \"http://127.0.0.1:\"\n    )\n  }\n  await storage.set(\"ollamaURL\", cleanUrl(formattedUrl))\n  await urlRewriteRuntime(cleanUrl(formattedUrl))\n}\n\nexport const systemPromptForNonRag = async () => {\n  const prompt = await storage.get(\"systemPromptForNonRag\")\n  return prompt\n}\n\nexport const promptForRag = async () => {\n  const prompt = await storage.get(\"systemPromptForRag\")\n  const questionPrompt = await storage.get(\"questionPromptForRag\")\n\n  let ragPrompt = prompt\n  let ragQuestionPrompt = questionPrompt\n\n  if (!ragPrompt || ragPrompt.length === 0) {\n    ragPrompt = DEFAUTL_RAG_SYSTEM_PROMPT\n  }\n\n  if (!ragQuestionPrompt || ragQuestionPrompt.length === 0) {\n    ragQuestionPrompt = DEFAULT_RAG_QUESTION_PROMPT\n  }\n\n  return {\n    ragPrompt,\n    ragQuestionPrompt\n  }\n}\n\nexport const setSystemPromptForNonRag = async (prompt: string) => {\n  await storage.set(\"systemPromptForNonRag\", prompt)\n}\n\nexport const setPromptForRag = async (\n  prompt: string,\n  questionPrompt: string\n) => {\n  await storage.set(\"systemPromptForRag\", prompt)\n  await storage.set(\"questionPromptForRag\", questionPrompt)\n}\n\nexport const systemPromptForNonRagOption = async () => {\n  const prompt = await storage.get(\"systemPromptForNonRagOption\")\n  return prompt\n}\n\nexport const setSystemPromptForNonRagOption = async (prompt: string) => {\n  await storage.set(\"systemPromptForNonRagOption\", prompt)\n}\n\nexport const sendWhenEnter = async () => {\n  const sendWhenEnter = await storage.get(\"sendWhenEnter\")\n  if (!sendWhenEnter || sendWhenEnter.length === 0) {\n    return true\n  }\n  return sendWhenEnter === \"true\"\n}\n\nexport const setSendWhenEnter = async (sendWhenEnter: boolean) => {\n  await storage.set(\"sendWhenEnter\", sendWhenEnter.toString())\n}\n\nexport const defaultEmbeddingModelForRag = async () => {\n  const embeddingMode = await storage.get(\"defaultEmbeddingModel\")\n  if (!embeddingMode || embeddingMode.length === 0) {\n    return null\n  }\n  return embeddingMode\n}\n\nexport const defaultEmbeddingChunkSize = async () => {\n  const embeddingChunkSize = await storage.get(\"defaultEmbeddingChunkSize\")\n  if (!embeddingChunkSize || embeddingChunkSize.length === 0) {\n    return 1000\n  }\n  return parseInt(embeddingChunkSize)\n}\n\nexport const defaultSplittingStrategy = async () => {\n  const splittingStrategy = await storage.get(\"defaultSplittingStrategy\")\n  if (!splittingStrategy || splittingStrategy.length === 0) {\n    return \"RecursiveCharacterTextSplitter\"\n  }\n  return splittingStrategy\n}\n\nexport const defaultSsplttingSeparator = async () => {\n  const splittingSeparator = await storage.get(\"defaultSplittingSeparator\")\n  if (!splittingSeparator || splittingSeparator.length === 0) {\n    return \"\\\\n\\\\n\"\n  }\n  return splittingSeparator\n}\n\nexport const defaultEmbeddingChunkOverlap = async () => {\n  const embeddingChunkOverlap = await storage.get(\n    \"defaultEmbeddingChunkOverlap\"\n  )\n  if (!embeddingChunkOverlap || embeddingChunkOverlap.length === 0) {\n    return 200\n  }\n  return parseInt(embeddingChunkOverlap)\n}\n\nexport const setDefaultSplittingStrategy = async (strategy: string) => {\n  await storage.set(\"defaultSplittingStrategy\", strategy)\n}\n\nexport const setDefaultSplittingSeparator = async (separator: string) => {\n  await storage.set(\"defaultSplittingSeparator\", separator)\n}\n\nexport const setDefaultEmbeddingModelForRag = async (model: string) => {\n  await storage.set(\"defaultEmbeddingModel\", model)\n}\n\nexport const setDefaultEmbeddingChunkSize = async (size: number) => {\n  await storage.set(\"defaultEmbeddingChunkSize\", size.toString())\n}\n\nexport const setDefaultEmbeddingChunkOverlap = async (overlap: number) => {\n  await storage.set(\"defaultEmbeddingChunkOverlap\", overlap.toString())\n}\n\nexport const saveForRag = async (\n  model: string,\n  chunkSize: number,\n  overlap: number,\n  totalFilePerKB: number,\n  noOfRetrievedDocs?: number,\n  strategy?: string,\n  separator?: string\n) => {\n  await setDefaultEmbeddingModelForRag(model)\n  await setDefaultEmbeddingChunkSize(chunkSize)\n  await setDefaultEmbeddingChunkOverlap(overlap)\n  await setTotalFilePerKB(totalFilePerKB)\n  if (noOfRetrievedDocs) {\n    await setNoOfRetrievedDocs(noOfRetrievedDocs)\n  }\n  if (strategy) {\n    await setDefaultSplittingStrategy(strategy)\n  }\n  if (separator) {\n    await setDefaultSplittingSeparator(separator)\n  }\n}\n\nexport const getWebSearchPrompt = async () => {\n  const prompt = await storage.get(\"webSearchPrompt\")\n  if (!prompt || prompt.length === 0) {\n    return DEFAULT_WEBSEARCH_PROMPT\n  }\n  return prompt\n}\n\nexport const setWebSearchPrompt = async (prompt: string) => {\n  await storage.set(\"webSearchPrompt\", prompt)\n}\n\nexport const geWebSearchFollowUpPrompt = async () => {\n  const prompt = await storage.get(\"webSearchFollowUpPrompt\")\n  if (!prompt || prompt.length === 0) {\n    return DEFAULT_WEBSEARCH_FOLLOWUP_PROMPT\n  }\n  return prompt\n}\n\nexport const setWebSearchFollowUpPrompt = async (prompt: string) => {\n  await storage.set(\"webSearchFollowUpPrompt\", prompt)\n}\n\nexport const setWebPrompts = async (prompt: string, followUpPrompt: string) => {\n  await setWebSearchPrompt(prompt)\n  await setWebSearchFollowUpPrompt(followUpPrompt)\n}\n\nexport const getPageShareUrl = async () => {\n  const pageShareUrl = await storage.get(\"pageShareUrl\")\n  if (!pageShareUrl || pageShareUrl.length === 0) {\n    return DEFAULT_PAGE_SHARE_URL\n  }\n  return pageShareUrl\n}\n\nexport const setPageShareUrl = async (pageShareUrl: string) => {\n  await storage.set(\"pageShareUrl\", pageShareUrl)\n}\n\nexport const isOllamaEnabled = async () => {\n  const ollamaStatus = await storage.get<boolean>(\"checkOllamaStatus\")\n  // if data is empty or null then return true\n  if (typeof ollamaStatus === \"undefined\" || ollamaStatus === null) {\n    return true\n  }\n  return ollamaStatus\n}\n"
  },
  {
    "path": "src/services/openai-tts.ts",
    "content": "import OpenAI from \"openai\"\nimport {\n  getOpenAITTSApiKey,\n  getOpenAITTSBaseUrl,\n  getOpenAITTSModel,\n  getOpenAITTSVoice\n} from \"./tts\"\n\nexport const generateOpenAITTS = async ({\n  text\n}: {\n  text: string\n}): Promise<ArrayBuffer> => {\n  const baseURL = await getOpenAITTSBaseUrl()\n  const apiKey = await getOpenAITTSApiKey()\n  const model = await getOpenAITTSModel()\n  const voice = await getOpenAITTSVoice()\n\n  const openai = new OpenAI({\n    baseURL: baseURL,\n    apiKey: apiKey,\n    dangerouslyAllowBrowser: true\n  })\n\n  const mp3 = await openai.audio.speech.create({\n    model: model,\n    voice: voice,\n    input: text\n  })\n\n  const arrBuff = await mp3.arrayBuffer()\n\n  return arrBuff\n}\n"
  },
  {
    "path": "src/services/search.ts",
    "content": "import { Storage } from \"@plasmohq/storage\"\n\nconst storage = new Storage()\nconst storage2 = new Storage({\n  area: \"local\"\n})\n\nconst TOTAL_SEARCH_RESULTS = 2\nconst DEFAULT_PROVIDER = \"duckduckgo\"\n\nconst AVAILABLE_PROVIDERS = [\"google\", \"duckduckgo\"] as const\n\nexport const getIsSimpleInternetSearch = async () => {\n  try {\n    const isSimpleInternetSearch = await storage.get(\"isSimpleInternetSearch\")\n    if (!isSimpleInternetSearch || isSimpleInternetSearch.length === 0) {\n      return true\n    }\n    return isSimpleInternetSearch === \"true\"\n  } catch (e) {\n    return true\n  }\n}\n\nexport const getIsVisitSpecificWebsite = async () => {\n  const isVisitSpecificWebsite = await storage.get(\"isVisitSpecificWebsite\")\n  if (!isVisitSpecificWebsite || isVisitSpecificWebsite.length === 0) {\n    return true\n  }\n  return isVisitSpecificWebsite === \"true\"\n}\n\nexport const setIsVisitSpecificWebsite = async (\n  isVisitSpecificWebsite: boolean\n) => {\n  await storage.set(\"isVisitSpecificWebsite\", isVisitSpecificWebsite.toString())\n}\n\nexport const setIsSimpleInternetSearch = async (\n  isSimpleInternetSearch: boolean\n) => {\n  await storage.set(\"isSimpleInternetSearch\", isSimpleInternetSearch.toString())\n}\n\nexport const getSearchProvider = async (): Promise<\n  (typeof AVAILABLE_PROVIDERS)[number]\n> => {\n  const searchProvider = await storage.get(\"searchProvider\")\n  if (!searchProvider || searchProvider.length === 0) {\n    return DEFAULT_PROVIDER\n  }\n  return searchProvider as (typeof AVAILABLE_PROVIDERS)[number]\n}\n\nexport const setSearchProvider = async (searchProvider: string) => {\n  await storage.set(\"searchProvider\", searchProvider)\n}\n\nexport const totalSearchResults = async () => {\n  const totalSearchResults = await storage.get(\"totalSearchResults\")\n  if (!totalSearchResults || totalSearchResults.length === 0) {\n    return TOTAL_SEARCH_RESULTS\n  }\n  return parseInt(totalSearchResults)\n}\n\nexport const setTotalSearchResults = async (totalSearchResults: number) => {\n  await storage.set(\"totalSearchResults\", totalSearchResults.toString())\n}\n\nexport const getSearxngURL = async () => {\n  const searxngURL = await storage.get(\"searxngURL\")\n  return searxngURL || \"\"\n}\n\nexport const isSearxngJSONMode = async () => {\n  const searxngJSONMode = await storage.get<boolean>(\"searxngJSONMode\")\n  return searxngJSONMode ?? false\n}\n\nexport const setSearxngJSONMode = async (searxngJSONMode: boolean) => {\n  await storage.set(\"searxngJSONMode\", searxngJSONMode)\n}\n\nexport const setSearxngURL = async (searxngURL: string) => {\n  await storage.set(\"searxngURL\", searxngURL)\n}\n\nexport const getBraveApiKey = async () => {\n  const braveApiKey = await storage2.get(\"braveApiKey\")\n  return braveApiKey || \"\"\n}\n\nexport const getOllamaSearchApiKey = async () => {\n  const ollamaSearchApiKey = await storage2.get(\"ollamaSearchApiKey\")\n  return ollamaSearchApiKey || \"\"\n}\n\nexport const getKagiApiKey = async () => {\n  const kagiApiKey = await storage2.get(\"kagiApiKey\")\n  return kagiApiKey || \"\"\n}\n\nexport const getPerplexityApiKey = async () => {\n  const perplexityApiKey = await storage2.get(\"perplexityApiKey\")\n  return perplexityApiKey || \"\"\n}\n\nexport const getTavilyApiKey = async () => {\n  const tavilyApiKey = await storage2.get(\"tavilyApiKey\")\n  return tavilyApiKey || \"\"\n}\n\nexport const getFirecrawlAPIKey = async () => {\n  const firecrawlAPIKey = await storage2.get(\"firecrawlAPIKey\")\n  return firecrawlAPIKey || \"\"\n}\n\nexport const setBraveApiKey = async (braveApiKey: string) => {\n  await storage2.set(\"braveApiKey\", braveApiKey)\n}\n\nexport const setOllamaSearchApiKey = async (ollamaSearchApiKey: string) => {\n  await storage2.set(\"ollamaSearchApiKey\", ollamaSearchApiKey)\n}\n\nexport const setKagiApiKey = async (kagiApiKey: string) => {\n  await storage2.set(\"kagiApiKey\", kagiApiKey)\n}\n\nexport const setPerplexityApiKey = async (perplexityApiKey: string) => {\n  await storage2.set(\"perplexityApiKey\", perplexityApiKey)\n}\n\nexport const setFirecrawlAPIKey = async (firecrawlAPIKey: string) => {\n  await storage2.set(\"firecrawlAPIKey\", firecrawlAPIKey)\n}\n\nexport const getExaAPIKey = async () => {\n  const exaAPIKey = await storage2.get(\"exaAPIKey\")\n  return exaAPIKey || \"\"\n}\n\nexport const setExaAPIKey = async (exaAPIKey: string) => {\n  await storage2.set(\"exaAPIKey\", exaAPIKey)\n}\n\nexport const setTavilyApiKey = async (tavilyApiKey: string) => {\n  await storage2.set(\"tavilyApiKey\", tavilyApiKey)\n}\n\nexport const getGoogleDomain = async () => {\n  const domain = await storage2.get(\"searchGoogleDomain\")\n  return domain || \"google.com\"\n}\n\nexport const setGoogleDomain = async (domain: string) => {\n  await storage2.set(\"searchGoogleDomain\", domain)\n}\n\nexport const getDomainFilterList = async (): Promise<string[]> => {\n  const domainFilterList = await storage.get(\"domainFilterList\")\n  if (!domainFilterList || domainFilterList.length === 0) {\n    return []\n  }\n  try {\n    return JSON.parse(domainFilterList)\n  } catch (e) {\n    return []\n  }\n}\n\nexport const setDomainFilterList = async (domainFilterList: string[]) => {\n  await storage.set(\"domainFilterList\", JSON.stringify(domainFilterList))\n}\n\nexport const getBlockedDomainList = async (): Promise<string[]> => {\n  const blockedDomainList = await storage.get(\"blockedDomainList\")\n  if (!blockedDomainList || blockedDomainList.length === 0) {\n    return []\n  }\n  try {\n    return JSON.parse(blockedDomainList)\n  } catch (e) {\n    return []\n  }\n}\n\nexport const setBlockedDomainList = async (blockedDomainList: string[]) => {\n  await storage.set(\"blockedDomainList\", JSON.stringify(blockedDomainList))\n}\n\nexport const getInternetSearchOn = async () => {\n  const defaultInternetSearchOn = await storage.get<boolean | undefined>(\n    \"defaultInternetSearchOn\"\n  )\n  return defaultInternetSearchOn ?? false\n}\n\nexport const setInternetSearchOn = async (defaultInternetSearchOn: boolean) => {\n  await storage.set(\"defaultInternetSearchOn\", defaultInternetSearchOn)\n}\n\nexport const getSearchSettings = async () => {\n  const [\n    isSimpleInternetSearch,\n    searchProvider,\n    totalSearchResult,\n    visitSpecificWebsite,\n    searxngURL,\n    searxngJSONMode,\n    braveApiKey,\n    tavilyApiKey,\n    googleDomain,\n    defaultInternetSearchOn,\n    exaAPIKey,\n    firecrawlAPIKey,\n    ollamaSearchApiKey,\n    kagiApiKey,\n    perplexityApiKey,\n    domainFilterList,\n    blockedDomainList\n  ] = await Promise.all([\n    getIsSimpleInternetSearch(),\n    getSearchProvider(),\n    totalSearchResults(),\n    getIsVisitSpecificWebsite(),\n    getSearxngURL(),\n    isSearxngJSONMode(),\n    getBraveApiKey(),\n    getTavilyApiKey(),\n    getGoogleDomain(),\n    getInternetSearchOn(),\n    getExaAPIKey(),\n    getFirecrawlAPIKey(),\n    getOllamaSearchApiKey(),\n    getKagiApiKey(),\n    getPerplexityApiKey(),\n    getDomainFilterList(),\n    getBlockedDomainList()\n  ])\n\n  return {\n    isSimpleInternetSearch,\n    searchProvider,\n    totalSearchResults: totalSearchResult,\n    visitSpecificWebsite,\n    searxngURL,\n    searxngJSONMode,\n    braveApiKey,\n    tavilyApiKey,\n    googleDomain,\n    defaultInternetSearchOn,\n    exaAPIKey,\n    firecrawlAPIKey,\n    ollamaSearchApiKey,\n    kagiApiKey,\n    perplexityApiKey,\n    domainFilterList,\n    blockedDomainList\n  }\n}\n\nexport const setSearchSettings = async ({\n  isSimpleInternetSearch,\n  searchProvider,\n  totalSearchResults,\n  visitSpecificWebsite,\n  searxngJSONMode,\n  searxngURL,\n  braveApiKey,\n  tavilyApiKey,\n  googleDomain,\n  defaultInternetSearchOn,\n  exaAPIKey,\n  firecrawlAPIKey,\n  ollamaSearchApiKey,\n  kagiApiKey,\n  perplexityApiKey,\n  domainFilterList,\n  blockedDomainList\n}: {\n  isSimpleInternetSearch: boolean\n  searchProvider: string\n  totalSearchResults: number\n  visitSpecificWebsite: boolean\n  searxngURL: string\n  searxngJSONMode: boolean\n  braveApiKey: string\n  tavilyApiKey: string\n  googleDomain: string\n  defaultInternetSearchOn: boolean\n  exaAPIKey: string\n  firecrawlAPIKey: string\n  ollamaSearchApiKey: string\n  kagiApiKey: string\n  perplexityApiKey: string\n  domainFilterList: string[]\n  blockedDomainList: string[]\n}) => {\n  await Promise.all([\n    setIsSimpleInternetSearch(isSimpleInternetSearch),\n    setSearchProvider(searchProvider),\n    setTotalSearchResults(totalSearchResults),\n    setIsVisitSpecificWebsite(visitSpecificWebsite),\n    setSearxngJSONMode(searxngJSONMode),\n    setSearxngURL(searxngURL),\n    setBraveApiKey(braveApiKey),\n    setTavilyApiKey(tavilyApiKey),\n    setGoogleDomain(googleDomain),\n    setInternetSearchOn(defaultInternetSearchOn),\n    setExaAPIKey(exaAPIKey),\n    setFirecrawlAPIKey(firecrawlAPIKey),\n    setOllamaSearchApiKey(ollamaSearchApiKey),\n    setKagiApiKey(kagiApiKey),\n    setPerplexityApiKey(perplexityApiKey),\n    setDomainFilterList(domainFilterList),\n    setBlockedDomainList(blockedDomainList)\n  ])\n}\n"
  },
  {
    "path": "src/services/title.ts",
    "content": "import { pageAssistModel } from \"@/models\"\nimport { Storage } from \"@plasmohq/storage\"\nimport { getOllamaURL } from \"./ollama\"\nimport { cleanUrl } from \"@/libs/clean-url\"\nimport { HumanMessage } from \"@langchain/core/messages\"\nimport { removeReasoning } from \"@/libs/reasoning\"\nimport { ChatHistory } from \"@/store/option\"\nimport { isConversationMessage } from \"@/libs/mcp/utils\"\nconst storage = new Storage()\n\nexport const DEFAULT_TITLE_GEN_PROMPT = `Here is the conversation:\n\n--------------\n\n{{query}}\n\n--------------\n\nCreate a concise, 3-5 word phrase as a title for this conversation. Avoid quotation marks or special formatting. RESPOND ONLY WITH THE TITLE TEXT. ANSWER USING THE SAME LANGUAGE AS THE CONVERSATION.\n\nExamples of titles:\n\nStellar Achievement Celebration\nFamily Bonding Activities\n🇫🇷 Voyage à Paris\n🍜 Receta de Ramen Casero\nShakespeare Analyse Literarische\n日本の春祭り体験\nДревнегреческая Философия Обзор\n\nResponse:`\n\nconst formatHistoryAsQuery = (history: ChatHistory): string => {\n    const conversationHistory = history.filter((message) =>\n        isConversationMessage(message)\n    )\n\n    if (conversationHistory.length === 0) return \"\"\n\n    if (conversationHistory.length === 1) {\n        return conversationHistory[0].content\n    }\n\n    return conversationHistory\n        .map(msg => `${msg.role === \"user\" ? \"User\" : \"Assistant\"}: ${removeReasoning(msg.content)}`)\n        .join(\"\\n\")\n}\n\n\nexport const isTitleGenEnabled = async () => {\n    const enabled = await storage.get<boolean | undefined>(\"titleGenEnabled\")\n    return enabled ?? false\n}\n\nexport const setTitleGenEnabled = async (enabled: boolean) => {\n    await storage.set(\"titleGenEnabled\", enabled)\n}\n\nexport const getTitleGenerationPrompt = async () => {\n    const title = await storage.get<string | undefined>(\"titleGenerationPrompt\")\n    return title ?? DEFAULT_TITLE_GEN_PROMPT\n}\n\n\nexport const setTitleGenerationPrompt = async (prompt: string) => {\n    await storage.set(\"titleGenerationPrompt\", prompt)\n}\n\n\nexport const titleGenerationModel = async () => {\n    const model = await storage.get<string | undefined>(\"titleGenerationModel\")\n    return model\n}\n\nexport const setTitleGenerationModel = async (model: string) => {\n    await storage.set(\"titleGenerationModel\", model)\n}\n\nexport const generateTitle = async (model: string, history: ChatHistory, fallBackTitle: string) => {\n\n    const isEnabled = await isTitleGenEnabled()\n\n    if (!isEnabled) {\n        return fallBackTitle\n    }\n\n    try {\n        const url = await getOllamaURL()\n\n\n        const defaultTitleModel = await titleGenerationModel();\n        const titleGenModel = defaultTitleModel || model\n\n        const titleModel = await pageAssistModel({\n            baseUrl: cleanUrl(url),\n            model: titleGenModel\n        })\n\n        const titlePrompt = await getTitleGenerationPrompt()\n\n        const query = formatHistoryAsQuery(history) || fallBackTitle\n\n        const formattedPrompt = titlePrompt.replace(\"{{query}}\", query)\n\n        const messages = [new HumanMessage(formattedPrompt)]\n\n        const title = await titleModel.invoke(messages)\n\n        return removeReasoning(title.content.toString())\n    } catch (error) {\n        console.error(`Error generating title: ${error}`)\n        return fallBackTitle\n    }\n}\n"
  },
  {
    "path": "src/services/tts.ts",
    "content": "import { Storage } from \"@plasmohq/storage\"\n\nconst storage = new Storage()\nconst storage2 = new Storage({\n  area: \"local\"\n})\n\nconst DEFAULT_TTS_PROVIDER = \"browser\"\n\nconst AVAILABLE_TTS_PROVIDERS = [\"browser\", \"elevenlabs\"] as const\n\nexport const getTTSProvider = async (): Promise<\n  (typeof AVAILABLE_TTS_PROVIDERS)[number]\n> => {\n  const ttsProvider = await storage.get(\"ttsProvider\")\n  if (!ttsProvider || ttsProvider.length === 0) {\n    return DEFAULT_TTS_PROVIDER\n  }\n  return ttsProvider as (typeof AVAILABLE_TTS_PROVIDERS)[number]\n}\n\nexport const setTTSProvider = async (ttsProvider: string) => {\n  await storage.set(\"ttsProvider\", ttsProvider)\n}\n\nexport const getBrowserTTSVoices = async () => {\n  if (import.meta.env.BROWSER === \"chrome\" || import.meta.env.BROWSER === \"edge\") {\n    const tts = await chrome.tts.getVoices()\n    return tts\n  } else {\n    const tts = await speechSynthesis.getVoices()\n    return tts.map((voice) => ({\n      voiceName: voice.name,\n      lang: voice.lang\n    }))\n  }\n}\n\nexport const getVoice = async () => {\n  const voice = await storage.get(\"voice\")\n  return voice\n}\n\nexport const setVoice = async (voice: string) => {\n  await storage.set(\"voice\", voice)\n}\n\nexport const isTTSEnabled = async () => {\n  const data = await storage.get(\"isTTSEnabled\")\n  if (!data || data.length === 0) {\n    return true\n  }\n  return data === \"true\"\n}\n\nexport const setTTSEnabled = async (isTTSEnabled: boolean) => {\n  await storage.set(\"isTTSEnabled\", isTTSEnabled.toString())\n}\n\nexport const isSSMLEnabled = async () => {\n  const data = await storage.get(\"isSSMLEnabled\")\n  return data === \"true\"\n}\n\nexport const setSSMLEnabled = async (isSSMLEnabled: boolean) => {\n  await storage.set(\"isSSMLEnabled\", isSSMLEnabled.toString())\n}\n\nexport const getElevenLabsApiKey = async () => {\n  const data = await storage.get(\"elevenLabsApiKey\")\n  return data\n}\n\nexport const setElevenLabsApiKey = async (elevenLabsApiKey: string) => {\n  await storage.set(\"elevenLabsApiKey\", elevenLabsApiKey)\n}\n\nexport const getElevenLabsVoiceId = async () => {\n  const data = await storage.get(\"elevenLabsVoiceId\")\n  return data\n}\n\nexport const setElevenLabsVoiceId = async (elevenLabsVoiceId: string) => {\n  await storage.set(\"elevenLabsVoiceId\", elevenLabsVoiceId)\n}\n\nexport const getElevenLabsModel = async () => {\n  const data = await storage.get(\"elevenLabsModel\")\n  return data\n}\n\nexport const setElevenLabsModel = async (elevenLabsModel: string) => {\n  await storage.set(\"elevenLabsModel\", elevenLabsModel)\n}\n\nexport const getOpenAITTSBaseUrl = async () => {\n  const data = await storage.get(\"openAITTSBaseUrl\")\n  if (!data || data.length === 0) {\n    return \"https://api.openai.com/v1\"\n  }\n  return data\n}\n\nexport const setOpenAITTSBaseUrl = async (openAITTSBaseUrl: string) => {\n  await storage.set(\"openAITTSBaseUrl\", openAITTSBaseUrl)\n}\n\nexport const getOpenAITTSApiKey = async () => {\n  const data = await storage.get(\"openAITTSApiKey\")\n  return data || ''\n}\n\nexport const getOpenAITTSModel = async () => {\n  const data = await storage.get(\"openAITTSModel\")\n  if (!data || data.length === 0) {\n    return \"tts-1\"\n  }\n  return data\n}\n\nexport const setOpenAITTSModel = async (openAITTSModel: string) => {\n  await storage.set(\"openAITTSModel\", openAITTSModel)\n}\n\n\nexport const setOpenAITTSApiKey = async (openAITTSApiKey: string) => {\n  await storage.set(\"openAITTSApiKey\", openAITTSApiKey)\n}\n\nexport const getOpenAITTSVoice = async () => {\n  const data = await storage.get(\"openAITTSVoice\")\n  if (!data || data.length === 0) {\n    return \"alloy\"\n  }\n  return data\n}\n\nexport const setOpenAITTSVoice = async (openAITTSVoice: string) => {\n  await storage.set(\"openAITTSVoice\", openAITTSVoice)\n}\n\n\nexport const getResponseSplitting = async () => {\n  const data = await storage.get(\"ttsResponseSplitting\")\n  if (!data || data.length === 0 || data === \"\") {\n    return \"punctuation\"\n  }\n  return data\n}\n\nexport const getRemoveReasoningTagTTS = async () => {\n  const data = await storage2.get(\"removeReasoningTagTTS\")\n  if (!data || data.length === 0 || data === \"\") {\n    return true\n  }\n  return data === \"true\"\n}\n\nexport const setResponseSplitting = async (responseSplitting: string) => {\n  await storage.set(\"ttsResponseSplitting\", responseSplitting)\n}\n\nexport const setRemoveReasoningTagTTS = async (removeReasoningTagTTS: boolean) => {\n  await storage2.set(\"removeReasoningTagTTS\", removeReasoningTagTTS.toString())\n}\n\n\nexport const isTTSAutoPlayEnabled = async () => {\n  const data = await storage.get<boolean | undefined>(\"isTTSAutoPlayEnabled\")\n  return data || false\n}\n\nexport const setTTSAutoPlayEnabled = async (isTTSAutoPlayEnabled: boolean) => {\n  await storage.set(\"isTTSAutoPlayEnabled\", isTTSAutoPlayEnabled)\n}\n\nexport const getSpeechPlaybackSpeed = async () => {\n  const data = await storage.get<number | undefined>(\"speechPlaybackSpeed\")\n  return data || 1\n}\n\nexport const setSpeechPlaybackSpeed = async (speechPlaybackSpeed: number) => {\n  await storage.set(\"speechPlaybackSpeed\", speechPlaybackSpeed)\n}\n\nexport const getTTSSettings = async () => {\n  const [\n    ttsEnabled,\n    ttsProvider,\n    browserTTSVoices,\n    voice,\n    ssmlEnabled,\n    elevenLabsApiKey,\n    elevenLabsVoiceId,\n    elevenLabsModel,\n    responseSplitting,\n    removeReasoningTagTTS,\n    // OPENAI\n    openAITTSBaseUrl,\n    openAITTSApiKey,\n    openAITTSModel,\n    openAITTSVoice,\n    // UTILS\n    ttsAutoPlay,\n    playbackSpeed,\n  ] = await Promise.all([\n    isTTSEnabled(),\n    getTTSProvider(),\n    getBrowserTTSVoices(),\n    getVoice(),\n    isSSMLEnabled(),\n    getElevenLabsApiKey(),\n    getElevenLabsVoiceId(),\n    getElevenLabsModel(),\n    getResponseSplitting(),\n    getRemoveReasoningTagTTS(),\n    // OPENAI \n    getOpenAITTSBaseUrl(),\n    getOpenAITTSApiKey(),\n    getOpenAITTSModel(),\n    getOpenAITTSVoice(),\n    // UTILS\n    isTTSAutoPlayEnabled(),\n    getSpeechPlaybackSpeed(),\n  ])\n\n  return {\n    ttsEnabled,\n    ttsProvider,\n    browserTTSVoices,\n    voice,\n    ssmlEnabled,\n    elevenLabsApiKey,\n    elevenLabsVoiceId,\n    elevenLabsModel,\n    responseSplitting,\n    removeReasoningTagTTS,\n    // OPENAI\n    openAITTSBaseUrl,\n    openAITTSApiKey,\n    openAITTSModel,\n    openAITTSVoice,\n    ttsAutoPlay,\n    playbackSpeed,\n  }\n}\n\nexport const setTTSSettings = async ({\n  ttsEnabled,\n  ttsProvider,\n  voice,\n  ssmlEnabled,\n  elevenLabsApiKey,\n  elevenLabsVoiceId,\n  elevenLabsModel,\n  responseSplitting,\n  removeReasoningTagTTS,\n  openAITTSBaseUrl,\n  openAITTSApiKey,\n  openAITTSModel,\n  openAITTSVoice,\n  ttsAutoPlay,\n  playbackSpeed,\n}: {\n  ttsEnabled: boolean\n  ttsProvider: string\n  voice: string\n  ssmlEnabled: boolean\n  elevenLabsApiKey: string\n  elevenLabsVoiceId: string\n  elevenLabsModel: string\n  responseSplitting: string\n  removeReasoningTagTTS: boolean\n  openAITTSBaseUrl: string,\n  openAITTSApiKey: string,\n  openAITTSModel: string,\n  openAITTSVoice: string,\n  ttsAutoPlay: boolean,\n  playbackSpeed: number,\n}) => {\n  await Promise.all([\n    setTTSEnabled(ttsEnabled),\n    setTTSProvider(ttsProvider),\n    setVoice(voice),\n    setSSMLEnabled(ssmlEnabled),\n    setElevenLabsApiKey(elevenLabsApiKey),\n    setElevenLabsVoiceId(elevenLabsVoiceId),\n    setElevenLabsModel(elevenLabsModel),\n    setResponseSplitting(responseSplitting),\n    setRemoveReasoningTagTTS(removeReasoningTagTTS),\n    setOpenAITTSBaseUrl(openAITTSBaseUrl),\n    setOpenAITTSApiKey(openAITTSApiKey),\n    setOpenAITTSModel(openAITTSModel),\n    setOpenAITTSVoice(openAITTSVoice),\n    setTTSAutoPlayEnabled(ttsAutoPlay),\n    setSpeechPlaybackSpeed(playbackSpeed),\n  ])\n}\n"
  },
  {
    "path": "src/store/index.tsx",
    "content": "import { create } from \"zustand\"\nimport { ChatMessageKind, McpToolCall } from \"@/libs/mcp/types\"\n\nexport type Message = {\n  isBot: boolean\n  name: string\n  message: string\n  sources: any[]\n  images?: string[]\n  modelName?: string\n  modelImage?: string\n  id?: string\n  messageType?: string\n  generationInfo?: any\n  reasoning_time_taken?: number\n  messageKind?: ChatMessageKind\n  toolCalls?: McpToolCall[]\n  toolCallId?: string\n  toolName?: string\n  toolServerName?: string\n  toolError?: boolean\n}\n\nexport type ChatHistory = {\n  role: \"user\" | \"assistant\" | \"system\" | \"tool\"\n  content: string\n  image?: string\n  images?: string[]\n  messageType?: string\n  messageKind?: ChatMessageKind\n  toolCalls?: McpToolCall[]\n  toolCallId?: string\n  toolName?: string\n  toolServerName?: string\n  toolError?: boolean\n}[]\n\ntype State = {\n  messages: Message[]\n  setMessages: (messages: Message[]) => void\n  history: ChatHistory\n  setHistory: (history: ChatHistory) => void\n  streaming: boolean\n  setStreaming: (streaming: boolean) => void\n  isFirstMessage: boolean\n  setIsFirstMessage: (isFirstMessage: boolean) => void\n  historyId: string | null\n  setHistoryId: (history_id: string | null) => void\n  isLoading: boolean\n  setIsLoading: (isLoading: boolean) => void\n  isProcessing: boolean\n  setIsProcessing: (isProcessing: boolean) => void\n  selectedModel: string | null\n  setSelectedModel: (selectedModel: string) => void\n  chatMode: \"normal\" | \"rag\" | \"vision\"\n  setChatMode: (chatMode: \"normal\" | \"rag\" | \"vision\") => void\n  isEmbedding: boolean\n  setIsEmbedding: (isEmbedding: boolean) => void\n  speechToTextLanguage: string\n  setSpeechToTextLanguage: (speechToTextLanguage: string) => void\n  currentURL: string\n  setCurrentURL: (currentURL: string) => void\n  selectedSystemPrompt: string | null\n  setSelectedSystemPrompt: (selectedSystemPrompt: string) => void\n\n  selectedQuickPrompt: string | null\n  setSelectedQuickPrompt: (selectedQuickPrompt: string) => void\n\n  useOCR: boolean\n  setUseOCR: (useOCR: boolean) => void\n}\n\nexport const useStoreMessage = create<State>((set) => ({\n  messages: [],\n  setMessages: (messages) => set({ messages }),\n  history: [],\n  setHistory: (history) => set({ history }),\n  streaming: false,\n  setStreaming: (streaming) => set({ streaming }),\n  isFirstMessage: true,\n  setIsFirstMessage: (isFirstMessage) => set({ isFirstMessage }),\n  historyId: null,\n  setHistoryId: (historyId) => set({ historyId }),\n  isLoading: false,\n  setIsLoading: (isLoading) => set({ isLoading }),\n  isProcessing: false,\n  setIsProcessing: (isProcessing) => set({ isProcessing }),\n  defaultSpeechToTextLanguage: \"en-US\",\n  selectedModel: null,\n  setSelectedModel: (selectedModel) => set({ selectedModel }),\n  chatMode: \"normal\",\n  setChatMode: (chatMode) => set({ chatMode }),\n  isEmbedding: false,\n  setIsEmbedding: (isEmbedding) => set({ isEmbedding }),\n  speechToTextLanguage: \"en-US\",\n  setSpeechToTextLanguage: (speechToTextLanguage) =>\n    set({ speechToTextLanguage }),\n  currentURL: \"\",\n  setCurrentURL: (currentURL) => set({ currentURL }),\n\n  selectedSystemPrompt: null,\n  setSelectedSystemPrompt: (selectedSystemPrompt) =>\n    set({ selectedSystemPrompt }),\n  selectedQuickPrompt: null,\n  setSelectedQuickPrompt: (selectedQuickPrompt) => set({ selectedQuickPrompt }),\n\n  useOCR: false,\n  setUseOCR: (useOCR) => set({ useOCR })\n}))\n"
  },
  {
    "path": "src/store/model.tsx",
    "content": "import { create } from \"zustand\"\nimport { isGptOssModel } from \"~/libs/model-utils\"\n\ntype CurrentChatModelSettings = {\n  f16KV?: boolean\n  frequencyPenalty?: number\n  keepAlive?: string\n  logitsAll?: boolean\n  mirostat?: number\n  mirostatEta?: number\n  mirostatTau?: number\n  numBatch?: number\n  numCtx?: number\n  numGpu?: number\n  numGqa?: number\n  numKeep?: number\n  numPredict?: number\n  numThread?: number\n  penalizeNewline?: boolean\n  presencePenalty?: number\n  repeatLastN?: number\n  repeatPenalty?: number\n  ropeFrequencyBase?: number\n  ropeFrequencyScale?: number\n  temperature?: number\n  tfsZ?: number\n  topK?: number\n  topP?: number\n  typicalP?: number\n  useMLock?: boolean\n  useMMap?: boolean\n  vocabOnly?: boolean\n  seed?: number\n  minP?: number\n\n  setF16KV?: (f16KV: boolean) => void\n  setFrequencyPenalty?: (frequencyPenalty: number) => void\n  setKeepAlive?: (keepAlive: string) => void\n  setLogitsAll?: (logitsAll: boolean) => void\n  setMirostat?: (mirostat: number) => void\n  setMirostatEta?: (mirostatEta: number) => void\n  setMirostatTau?: (mirostatTau: number) => void\n  setNumBatch?: (numBatch: number) => void\n  setNumCtx?: (numCtx: number) => void\n  setNumGpu?: (numGpu: number) => void\n  setNumGqa?: (numGqa: number) => void\n  setNumKeep?: (numKeep: number) => void\n  setNumPredict?: (numPredict: number) => void\n  setNumThread?: (numThread: number) => void\n  setPenalizeNewline?: (penalizeNewline: boolean) => void\n  setPresencePenalty?: (presencePenalty: number) => void\n  setRepeatLastN?: (repeatLastN: number) => void\n  setRepeatPenalty?: (repeatPenalty: number) => void\n  setRopeFrequencyBase?: (ropeFrequencyBase: number) => void\n  setRopeFrequencyScale?: (ropeFrequencyScale: number) => void\n  setTemperature?: (temperature: number) => void\n  setTfsZ?: (tfsZ: number) => void\n  setTopK?: (topK: number) => void\n  setTopP?: (topP: number) => void\n  setTypicalP?: (typicalP: number) => void\n  setUseMLock?: (useMLock: boolean) => void\n  setUseMMap?: (useMMap: boolean) => void\n  setVocabOnly?: (vocabOnly: boolean) => void\n  seetSeed?: (seed: number) => void\n\n  setX: (key: string, value: any) => void\n  reset: () => void\n  systemPrompt?: string\n  setSystemPrompt: (systemPrompt: string) => void\n  useMlock?: boolean\n  setUseMlock: (useMlock: boolean) => void\n\n  setMinP: (minP: number) => void\n\n  reasoningEffort?: string\n  setReasoningEffort?: (reasoningEffort: string) => void\n\n  thinking?: boolean | \"low\" | \"medium\" | \"high\"\n  setThinking?: (thinking: boolean | \"low\" | \"medium\" | \"high\") => void\n\n\n  ocrLanguage?: string\n  setOcrLanguage?: (ocrLanguage: string) => void \n}\n\nexport const useStoreChatModelSettings = create<CurrentChatModelSettings>(\n  (set) => ({\n    setF16KV: (f16KV: boolean) => set({ f16KV }),\n    setFrequencyPenalty: (frequencyPenalty: number) =>\n      set({ frequencyPenalty }),\n    setKeepAlive: (keepAlive: string) => set({ keepAlive }),\n    setLogitsAll: (logitsAll: boolean) => set({ logitsAll }),\n    setMirostat: (mirostat: number) => set({ mirostat }),\n    setMirostatEta: (mirostatEta: number) => set({ mirostatEta }),\n    setMirostatTau: (mirostatTau: number) => set({ mirostatTau }),\n    setNumBatch: (numBatch: number) => set({ numBatch }),\n    setNumCtx: (numCtx: number) => set({ numCtx }),\n    setNumGpu: (numGpu: number) => set({ numGpu }),\n    setNumGqa: (numGqa: number) => set({ numGqa }),\n    setNumKeep: (numKeep: number) => set({ numKeep }),\n    setNumPredict: (numPredict: number) => set({ numPredict }),\n    setNumThread: (numThread: number) => set({ numThread }),\n    setPenalizeNewline: (penalizeNewline: boolean) => set({ penalizeNewline }),\n    setPresencePenalty: (presencePenalty: number) => set({ presencePenalty }),\n    setRepeatLastN: (repeatLastN: number) => set({ repeatLastN }),\n    setRepeatPenalty: (repeatPenalty: number) => set({ repeatPenalty }),\n    setRopeFrequencyBase: (ropeFrequencyBase: number) =>\n      set({ ropeFrequencyBase }),\n    setRopeFrequencyScale: (ropeFrequencyScale: number) =>\n      set({ ropeFrequencyScale }),\n    setTemperature: (temperature: number) => set({ temperature }),\n    setTfsZ: (tfsZ: number) => set({ tfsZ }),\n    setTopK: (topK: number) => set({ topK }),\n    setTopP: (topP: number) => set({ topP }),\n    setTypicalP: (typicalP: number) => set({ typicalP }),\n    setUseMLock: (useMLock: boolean) => set({ useMLock }),\n    setUseMMap: (useMMap: boolean) => set({ useMMap }),\n    setVocabOnly: (vocabOnly: boolean) => set({ vocabOnly }),\n    seetSeed: (seed: number) => set({ seed }),\n    setX: (key: string, value: any) => set({ [key]: value }),\n    systemPrompt: undefined,\n    setMinP: (minP: number) => set({ minP }),\n    setSystemPrompt: (systemPrompt: string) => set({ systemPrompt }),\n    setUseMlock: (useMlock: boolean) => set({ useMlock }),\n    setReasoningEffort: (reasoningEffort: string) => set({ reasoningEffort }),\n    setThinking: (thinking: boolean | \"low\" | \"medium\" | \"high\") => set({ thinking }),\n    ocrLanguage: undefined, \n    setOcrLanguage: (ocrLanguage: string) => set({ ocrLanguage }), \n    reset: () =>\n      set({\n        f16KV: undefined,\n        frequencyPenalty: undefined,\n        keepAlive: undefined,\n        logitsAll: undefined,\n        mirostat: undefined,\n        mirostatEta: undefined,\n        mirostatTau: undefined,\n        numBatch: undefined,\n        numCtx: undefined,\n        numGpu: undefined,\n        numGqa: undefined,\n        numKeep: undefined,\n        numPredict: undefined,\n        numThread: undefined,\n        penalizeNewline: undefined,\n        presencePenalty: undefined,\n        repeatLastN: undefined,\n        repeatPenalty: undefined,\n        ropeFrequencyBase: undefined,\n        ropeFrequencyScale: undefined,\n        temperature: undefined,\n        tfsZ: undefined,\n        topK: undefined,\n        topP: undefined,\n        typicalP: undefined,\n        useMLock: undefined,\n        useMMap: undefined,\n        vocabOnly: undefined,\n        seed: undefined,\n        systemPrompt: undefined,\n        minP: undefined,\n        useMlock: undefined,\n        reasoningEffort: undefined,\n        thinking: undefined,\n        ocrLanguage: undefined\n      })\n  })\n)\n\n/**\n * Normalize thinking value for API compatibility\n * GPT-OSS requires string levels (\"low\"|\"medium\"|\"high\") and cannot disable thinking\n * Other models use boolean (true|false)\n *\n * @param thinking - The thinking value from state\n * @param modelId - The model identifier\n * @returns normalized thinking value appropriate for the model\n */\nexport const normalizeThinking = (\n  thinking: boolean | \"low\" | \"medium\" | \"high\" | undefined,\n  modelId: string | null\n): boolean | \"low\" | \"medium\" | \"high\" | undefined => {\n  // Handle undefined\n  if (thinking === undefined) return undefined;\n\n  // Handle false (disable thinking)\n  if (thinking === false) {\n    if (isGptOssModel(modelId)) {\n      // GPT-OSS cannot disable thinking - use minimum reasoning level\n      return \"low\";\n    }\n    // Other models can disable thinking\n    return false;\n  }\n\n  if (isGptOssModel(modelId)) {\n    // GPT-OSS requires string levels\n    if (thinking === true) {\n      // Default to \"medium\" when converting boolean true to level\n      return \"medium\";\n    }\n    // If already a level string, return as-is\n    if (thinking === \"low\" || thinking === \"medium\" || thinking === \"high\") {\n      return thinking;\n    }\n    return \"medium\"; // Fallback\n  } else {\n    // Other models use boolean\n    if (typeof thinking === \"string\") {\n      // Convert any level string to boolean true for non-gpt-oss models\n      return true;\n    }\n    return thinking;\n  }\n}\n"
  },
  {
    "path": "src/store/option.tsx",
    "content": "import { Knowledge } from \"@/db/knowledge\"\nimport { ChatDocuments } from \"@/models/ChatTypes\"\nimport { create } from \"zustand\"\nimport { type UploadedFile } from \"@/db/dexie/types\"\nimport { isFireFoxPrivateMode } from \"@/utils/is-private-mode\"\nimport { ChatActionInfo, ChatMessageKind, McpToolCall } from \"@/libs/mcp/types\"\n\ntype WebSearch = {\n  search_engine: string\n  search_url: string\n  search_query: string\n  search_results: {\n    title: string\n    link: string\n  }[]\n}\nexport type Message = {\n  isBot: boolean\n  name: string\n  message: string\n  sources: any[]\n  images?: string[]\n  search?: WebSearch\n  reasoning_time_taken?: number\n  id?: string\n  messageType?: string\n  modelName?: string\n  modelImage?: string\n  documents?: ChatDocuments\n  generationInfo?: any\n  messageKind?: ChatMessageKind\n  toolCalls?: McpToolCall[]\n  toolCallId?: string\n  toolName?: string\n  toolServerName?: string\n  toolError?: boolean\n}\n\nexport type ChatHistory = {\n  role: \"user\" | \"assistant\" | \"system\" | \"tool\"\n  content: string\n  image?: string\n  images?: string[]\n  messageType?: string\n  messageKind?: ChatMessageKind\n  toolCalls?: McpToolCall[]\n  toolCallId?: string\n  toolName?: string\n  toolServerName?: string\n  toolError?: boolean\n}[]\n\ntype State = {\n  messages: Message[]\n  setMessages: (messages: Message[]) => void\n  history: ChatHistory\n  setHistory: (history: ChatHistory) => void\n  streaming: boolean\n  setStreaming: (streaming: boolean) => void\n  isFirstMessage: boolean\n  setIsFirstMessage: (isFirstMessage: boolean) => void\n  historyId: string | null\n  setHistoryId: (history_id: string | null) => void\n  isLoading: boolean\n  setIsLoading: (isLoading: boolean) => void\n  isProcessing: boolean\n  setIsProcessing: (isProcessing: boolean) => void\n  selectedModel: string | null\n  setSelectedModel: (selectedModel: string) => void\n  chatMode: \"normal\" | \"rag\"\n  setChatMode: (chatMode: \"normal\" | \"rag\") => void\n  isEmbedding: boolean\n  setIsEmbedding: (isEmbedding: boolean) => void\n  webSearch: boolean\n  setWebSearch: (webSearch: boolean) => void\n  isSearchingInternet: boolean\n  setIsSearchingInternet: (isSearchingInternet: boolean) => void\n\n  selectedSystemPrompt: string | null\n  setSelectedSystemPrompt: (selectedSystemPrompt: string) => void\n\n  selectedQuickPrompt: string | null\n  setSelectedQuickPrompt: (selectedQuickPrompt: string) => void\n\n  selectedKnowledge: Knowledge | null\n  setSelectedKnowledge: (selectedKnowledge: Knowledge) => void\n\n  setSpeechToTextLanguage: (language: string) => void\n  speechToTextLanguage: string\n\n  temporaryChat: boolean\n  setTemporaryChat: (temporaryChat: boolean) => void\n\n  useOCR: boolean\n  setUseOCR: (useOCR: boolean) => void\n\n  documentContext: ChatDocuments | null\n  setDocumentContext: (documentContext: ChatDocuments) => void\n\n  uploadedFiles: UploadedFile[]\n  setUploadedFiles: (uploadedFiles: UploadedFile[]) => void\n\n  contextFiles: UploadedFile[]\n  setContextFiles: (contextFiles: UploadedFile[]) => void\n\n  actionInfo: ChatActionInfo | null\n  setActionInfo: (actionInfo: ChatActionInfo | null) => void\n\n  fileRetrievalEnabled: boolean\n  setFileRetrievalEnabled: (fileRetrievalEnabled: boolean) => void\n}\n\nexport const useStoreMessageOption = create<State>((set) => ({\n  messages: [],\n  setMessages: (messages) => set({ messages }),\n  history: [],\n  setHistory: (history) => set({ history }),\n  streaming: false,\n  setStreaming: (streaming) => set({ streaming }),\n  isFirstMessage: true,\n  setIsFirstMessage: (isFirstMessage) => set({ isFirstMessage }),\n  historyId: null,\n  setHistoryId: (historyId) => set({ historyId }),\n  isLoading: false,\n  setIsLoading: (isLoading) => set({ isLoading }),\n  isProcessing: false,\n  setIsProcessing: (isProcessing) => set({ isProcessing }),\n  speechToTextLanguage: \"en-US\",\n  setSpeechToTextLanguage: (language) =>\n    set({ speechToTextLanguage: language }),\n  selectedModel: null,\n  setSelectedModel: (selectedModel) => set({ selectedModel }),\n  chatMode: \"normal\",\n  setChatMode: (chatMode) => set({ chatMode }),\n  isEmbedding: false,\n  setIsEmbedding: (isEmbedding) => set({ isEmbedding }),\n  webSearch: false,\n  setWebSearch: (webSearch) => set({ webSearch }),\n  isSearchingInternet: false,\n  setIsSearchingInternet: (isSearchingInternet) => set({ isSearchingInternet }),\n  selectedSystemPrompt: null,\n  setSelectedSystemPrompt: (selectedSystemPrompt) =>\n    set({ selectedSystemPrompt }),\n  selectedQuickPrompt: null,\n  setSelectedQuickPrompt: (selectedQuickPrompt) => set({ selectedQuickPrompt }),\n\n  selectedKnowledge: null,\n  setSelectedKnowledge: (selectedKnowledge) => set({ selectedKnowledge }),\n\n  temporaryChat: isFireFoxPrivateMode,\n  setTemporaryChat: (temporaryChat) => set({ temporaryChat }),\n\n  useOCR: false,\n  setUseOCR: (useOCR) => set({ useOCR }),\n\n  documentContext: null,\n  setDocumentContext: (documentContext) => set({ documentContext }),\n\n  uploadedFiles: [],\n  setUploadedFiles: (uploadedFiles) => set({ uploadedFiles }),\n  contextFiles: [],\n  setContextFiles: (contextFiles) => set({ contextFiles }),\n\n  actionInfo: null,\n  setActionInfo: (actionInfo) => set({ actionInfo }),\n\n  fileRetrievalEnabled: false,\n  setFileRetrievalEnabled: (fileRetrievalEnabled) =>\n    set({ fileRetrievalEnabled })\n}))\n"
  },
  {
    "path": "src/store/webui.tsx",
    "content": "import { create } from \"zustand\"\n\ntype State = {\n  sendWhenEnter: boolean\n  setSendWhenEnter: (sendWhenEnter: boolean) => void\n\n  ttsEnabled: boolean\n  setTTSEnabled: (isTTSEnabled: boolean) => void\n}\n\nexport const useWebUI = create<State>((set) => ({\n  sendWhenEnter: true,\n  setSendWhenEnter: (sendWhenEnter) => set({ sendWhenEnter }),\n\n  ttsEnabled: true,\n  setTTSEnabled: (ttsEnabled) => set({ ttsEnabled })\n}))\n"
  },
  {
    "path": "src/types/index.ts",
    "content": "import { ChatHistory } from \"@/store\"\n\nexport type BotResponse = {\n    bot: {\n        text: string\n        sourceDocuments: any[]\n    }\n    history: ChatHistory\n    history_id: string\n}"
  },
  {
    "path": "src/types/message.ts",
    "content": "import { ChatDocuments } from \"@/models/ChatTypes\"\nimport { ChatMessageKind, McpToolCall } from \"@/libs/mcp/types\"\n\ntype WebSearch = {\n  search_engine: string\n  search_url: string\n  search_query: string\n  search_results: {\n    title: string\n    link: string\n  }[]\n}\nexport type Message = {\n  isBot: boolean\n  name: string\n  message: string\n  sources: any[]\n  images?: string[]\n  search?: WebSearch\n  messageType?: string\n  id?: string\n  generationInfo?: any\n  reasoning_time_taken?: number\n  modelImage?: string\n  modelName?: string\n  documents?: ChatDocuments\n  messageKind?: ChatMessageKind\n  toolCalls?: McpToolCall[]\n  toolCallId?: string\n  toolName?: string\n  toolServerName?: string\n  toolError?: boolean\n}\n"
  },
  {
    "path": "src/utils/action.ts",
    "content": "import { browser } from \"wxt/browser\"\n\nexport const setTitle = ({ title }: { title: string }) => {\n  if (import.meta.env.BROWSER === \"chrome\" || import.meta.env.BROWSER === \"edge\") {\n    chrome.action.setTitle({ title })\n  } else {\n    browser.browserAction.setTitle({ title })\n  }\n}\n\nexport const setBadgeBackgroundColor = ({ color }: { color: string }) => {\n  if (import.meta.env.BROWSER === \"chrome\" || import.meta.env.BROWSER === \"edge\") {\n    chrome.action.setBadgeBackgroundColor({ color })\n  } else {\n    browser.browserAction.setBadgeBackgroundColor({ color })\n  }\n}\n\nexport const setBadgeText = ({ text }: { text: string }) => {\n  if (import.meta.env.BROWSER === \"chrome\" || import.meta.env.BROWSER === \"edge\") {\n    chrome.action.setBadgeText({ text })\n  } else {\n    browser.browserAction.setBadgeText({ text })\n  }\n}\n"
  },
  {
    "path": "src/utils/chrome-download.ts",
    "content": "export interface DownloadProgressEvent {\n  loaded: number\n  total?: number\n}\n\nexport const downloadChromeAIModel = async (\n  onProgress?: (progress: DownloadProgressEvent) => void\n): Promise<void> => {\n  try {\n    // Check if the newer LanguageModel API is available\n    if (typeof (globalThis as any).LanguageModel !== \"undefined\") {\n      const session = await (globalThis as any).LanguageModel.create({\n        monitor(m: any) {\n          if (onProgress) {\n            m.addEventListener(\"downloadprogress\", (e: DownloadProgressEvent) => {\n              onProgress({\n                loaded: e.loaded,\n                total: e.total\n              })\n            })\n          }\n        }\n      })\n      \n      // Clean up the session after download\n      if (session && session.destroy) {\n        session.destroy()\n      }\n      \n      return\n    }\n\n    // Fallback for older APIs\n    const ai = (window as any).ai\n    \n    if (ai?.languageModel?.create) {\n      const session = await ai.languageModel.create({\n        monitor(m: any) {\n          if (onProgress) {\n            m.addEventListener(\"downloadprogress\", (e: DownloadProgressEvent) => {\n              onProgress({\n                loaded: e.loaded,\n                total: e.total\n              })\n            })\n          }\n        }\n      })\n      \n      if (session && session.destroy) {\n        session.destroy()\n      }\n      \n      return\n    }\n\n    throw new Error(\"Chrome AI download is not supported on this version\")\n  } catch (error) {\n    console.error(\"Error downloading Chrome AI model:\", error)\n    throw error\n  }\n}\n"
  },
  {
    "path": "src/utils/chrome.ts",
    "content": "import { checkChromeAIAvailability } from \"@/models/utils/chrome\"\n\nexport const getChromeAISupported = async () => {\n  try {\n    let browserInfo = navigator.userAgent.match(/Chrom(e|ium)\\/([0-9]+)\\./)\n    let version = browserInfo ? parseInt(browserInfo[2], 10) : 0\n\n    if (version < 138) {\n      return \"browser_not_supported\"\n    }\n\n    if (!(\"LanguageModel\" in globalThis) && !(\"ai\" in globalThis)) {\n      return \"ai_not_supported\"\n    }\n\n    const capabilities = await checkChromeAIAvailability()\n    if (capabilities === 'downloadable') {\n      return \"downloadable\"\n    }\n\n    if (capabilities === 'downloading') {\n      return \"downloading\"\n    }\n\n    if (capabilities !== \"readily\") {\n      return \"ai_not_ready\"\n    }\n\n    return \"success\"\n  } catch (e) {\n    console.error(e)\n    return \"internal_error\"\n  }\n}\n\nexport const isChromeAISupported = async () => {\n  const result = await getChromeAISupported()\n  return result === \"success\"\n}\n"
  },
  {
    "path": "src/utils/clean-headers.ts",
    "content": "export const getCustomHeaders = ({\n    headers\n}: {\n    headers?: { key: string; value: string }[] | any\n}) => {\n    try {\n        if (!headers) return {}\n        //@ts-ignore\n        if (headers == {}) return {}\n\n        // Check if headers is actually an array\n        if (!Array.isArray(headers)) return {}\n\n        const customHeaders: Record<string, string> = {}\n        for (const header of headers) {\n            if (header && typeof header.key === 'string' && header.value !== undefined) {\n                customHeaders[header.key] = header.value\n            }\n        }\n        return customHeaders\n    } catch (e) {\n        console.error(e, headers)\n        return {}\n    }\n}\n"
  },
  {
    "path": "src/utils/clean.ts",
    "content": "export const cleanUnwantedUnicode = (text: string) => {\n  const UNICODE_REGEX = /[\\u200B-\\u200D\\uFEFF]/g\n  return text.replace(UNICODE_REGEX, \"\").trim()\n}\n"
  },
  {
    "path": "src/utils/clipboard.ts",
    "content": "import { marked } from \"marked\"\nimport markedKatexExtension from \"./marked/katex\"\nimport { removeReasoning, replaceThinkTagToEM } from \"@/libs/reasoning\"\nimport { isRemoveReasoningTagFromCopy } from \"@/services/app\"\nimport { convertMathDelimiters } from \"./math-delimiter\"\n\nexport const copyToClipboard = async ({\n  text,\n  formatted = false\n}: {\n  text: string\n  formatted?: boolean\n}) => {\n  const isClean = await isRemoveReasoningTagFromCopy()\n\n  text = convertMathDelimiters(text)\n\n  if (isClean) {\n    text = removeReasoning(text)\n  }\n\n  if (formatted) {\n    try {\n      const options: any = {\n        throwOnError: false\n      }\n\n      marked.use(markedKatexExtension(options))\n\n      const html = marked.parse(replaceThinkTagToEM(text))\n      const styledHtml = `\n                <div style=\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333;\">\n                    ${html}\n                </div>\n            `\n\n      if (navigator.clipboard && navigator.clipboard.write) {\n        await navigator.clipboard.write([\n          new ClipboardItem({\n            \"text/plain\": new Blob([text], { type: \"text/plain\" }),\n            \"text/html\": new Blob([styledHtml], { type: \"text/html\" })\n          })\n        ])\n        return\n      }\n    } catch (e) {\n      console.log(e)\n      console.log(\"Using fallback\")\n    }\n  }\n\n  // Fallback to plain text copying\n  navigator.clipboard.writeText(text)\n}\n"
  },
  {
    "path": "src/utils/color.ts",
    "content": "export const tagColors = {\n  summary: \"blue\",\n  explain: \"green\",\n  translate: \"purple\",\n  custom: \"orange\",\n  rephrase: \"yellow\"\n}\n"
  },
  {
    "path": "src/utils/compress.ts",
    "content": "/**\n * Compresses text data using the browser's built-in CompressionStream API with gzip\n * @param text - The string to compress\n * @returns Promise resolving to an ArrayBuffer containing the compressed data\n */\nexport async function compressText(text: string): Promise<ArrayBuffer> {\n    const encoder = new TextEncoder();\n    const encodedData = encoder.encode(text);\n    const cs = new CompressionStream('gzip');\n    const writer = cs.writable.getWriter();\n    writer.write(encodedData);\n    writer.close();\n    return new Response(cs.readable).arrayBuffer();\n}\n\n/**\n * Decompresses binary data using the browser's built-in DecompressionStream API with gzip\n * @param compressedData - The ArrayBuffer containing compressed data\n * @returns Promise resolving to the original string\n */\nexport async function decompressData(compressedData: ArrayBuffer): Promise<string> {\n    const ds = new DecompressionStream('gzip');\n    const writer = ds.writable.getWriter();\n    writer.write(new Uint8Array(compressedData));\n    writer.close();\n    const decompressedArrayBuffer = await new Response(ds.readable).arrayBuffer();\n    const decoder = new TextDecoder();\n    return decoder.decode(decompressedArrayBuffer);\n}\n\n/**\n * Converts an ArrayBuffer to a base64 string for storage or transmission\n * @param buffer - The ArrayBuffer to convert\n * @returns Base64 encoded string\n */\nexport function arrayBufferToBase64(buffer: ArrayBuffer): string {\n    const bytes = new Uint8Array(buffer);\n    let binary = '';\n    for (let i = 0; i < bytes.byteLength; i++) {\n        binary += String.fromCharCode(bytes[i]);\n    }\n    return window.btoa(binary);\n}\n\nexport function base64ToArrayBuffer(base64: string): ArrayBuffer {\n    const binaryString = window.atob(base64);\n    const len = binaryString.length;\n    const bytes = new Uint8Array(len);\n    for (let i = 0; i < len; i++) {\n        bytes[i] = binaryString.charCodeAt(i);\n    }\n    return bytes.buffer;\n}\n\nexport function getCompressionStats(originalText: string, compressedData: ArrayBuffer): {\n    originalSize: number;\n    compressedSize: number;\n    compressionRatio: number;\n} {\n    const originalSize = new TextEncoder().encode(originalText).length;\n    const compressedSize = compressedData.byteLength;\n    const compressionRatio = originalSize > 0 ?\n        Math.round((1 - (compressedSize / originalSize)) * 100) : 0;\n\n    return {\n        originalSize,\n        compressedSize,\n        compressionRatio\n    };\n}\n"
  },
  {
    "path": "src/utils/constant.ts",
    "content": "export const PASTED_TEXT_CHAR_LIMIT = 1_500"
  },
  {
    "path": "src/utils/ff-error.ts",
    "content": "export const isDatabaseClosedError = (error: unknown): boolean => {\n  if (error instanceof Error) {\n    return error.name === \"DatabaseClosedError\";\n  }\n  return false;\n}"
  },
  {
    "path": "src/utils/file-processor.ts",
    "content": "export const processFileUpload = async (file: File) => {\n  const { convertFileToSource } = await import(\"./to-source\")\n  return convertFileToSource({ file })\n}"
  },
  {
    "path": "src/utils/format-file-size.ts",
    "content": "export const formatFileSize = (bytes: number, decimals: number = 2): string => {\n  if (bytes === 0) return \"0 Bytes\";\n\n  const k = 1024;\n  const dm = decimals < 0 ? 0 : decimals;\n  const sizes = [\"Bytes\", \"KB\", \"MB\", \"GB\", \"TB\"];\n\n  const i = Math.floor(Math.log(bytes) / Math.log(k));\n\n  return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;\n};\n"
  },
  {
    "path": "src/utils/generate-history.ts",
    "content": "import { isCustomModel } from \"@/db/dexie/models\"\nimport { isTextMessageKind } from \"@/libs/mcp/utils\"\nimport { removeReasoning } from \"@/libs/reasoning\"\nimport {\n  HumanMessage,\n  AIMessage,\n  ToolMessage,\n  type MessageContent\n} from \"@langchain/core/messages\"\n\nexport const generateHistory = (\n  messages: {\n    role: \"user\" | \"assistant\" | \"system\" | \"tool\"\n    content: string\n    image?: string\n    images?: string[]\n    messageKind?: \"text\" | \"assistant_tool_calls\" | \"tool_result\"\n    toolCalls?: {\n      id: string\n      name: string\n      args?: unknown\n      type?: \"tool_call\"\n      serverName?: string\n      displayName?: string\n    }[]\n    toolCallId?: string\n    toolName?: string\n    toolServerName?: string\n    toolError?: boolean\n  }[],\n  model: string\n) => {\n  let history = []\n  const isCustom = isCustomModel(model)\n  for (const message of messages) {\n    if (message.role === \"user\") {\n      let content: MessageContent = isCustom\n        ? message.content\n        : [\n            {\n              type: \"text\",\n              text: message.content\n            }\n          ]\n\n      // Use images array if available, otherwise fall back to single image\n      const imagesToUse = message.images && message.images.length > 0\n        ? message.images\n        : (message.image ? [message.image] : [])\n\n      if (imagesToUse.length > 0) {\n        content = [\n          {\n            type: \"text\",\n            text: message.content\n          }\n        ]\n\n        // Add all images to content\n        imagesToUse.forEach((img) => {\n          if (img && img.length > 0) {\n            //@ts-ignore\n            content.push({\n              type: \"image_url\",\n              image_url: !isCustom\n                ? img\n                : {\n                    url: img\n                  }\n            })\n          }\n        })\n      }\n      history.push(\n        new HumanMessage({\n          content: content\n        })\n      )\n    } else if (message.role === \"assistant\") {\n      if (message.messageKind === \"assistant_tool_calls\") {\n        history.push(\n          new AIMessage({\n            content: message.content || \"\",\n            tool_calls: (message.toolCalls || []).map((toolCall) => ({\n              id: toolCall.id,\n              name: toolCall.name,\n              args: toolCall.args || {},\n              type: \"tool_call\"\n            }))\n          })\n        )\n        continue\n      }\n\n      history.push(\n        new AIMessage({\n          content: isTextMessageKind(message.messageKind)\n            ? isCustom\n              ? removeReasoning(message.content)\n              : [\n                  {\n                    type: \"text\",\n                    text: removeReasoning(message.content)\n                  }\n                ]\n            : message.content\n        })\n      )\n    } else if (message.role === \"tool\") {\n      history.push(\n        new ToolMessage({\n          content: message.content,\n          tool_call_id: message.toolCallId || \"\",\n          status: message.toolError ? \"error\" : \"success\"\n        })\n      )\n    }\n  }\n  return history\n}\n"
  },
  {
    "path": "src/utils/google-domains.ts",
    "content": "export const ALL_GOOGLE_DOMAINS = [\n    \"google.ad\",\n    \"google.ae\",\n    \"google.al\",\n    \"google.am\",\n    \"google.as\",\n    \"google.at\",\n    \"google.az\",\n    \"google.ba\",\n    \"google.be\",\n    \"google.bf\",\n    \"google.bg\",\n    \"google.bi\",\n    \"google.bj\",\n    \"google.bs\",\n    \"google.bt\",\n    \"google.by\",\n    \"google.ca\",\n    \"google.cd\",\n    \"google.cf\",\n    \"google.cg\",\n    \"google.ch\",\n    \"google.ci\",\n    \"google.cl\",\n    \"google.cm\",\n    \"google.co.ao\",\n    \"google.co.bw\",\n    \"google.co.ck\",\n    \"google.co.cr\",\n    \"google.co.id\",\n    \"google.co.il\",\n    \"google.co.in\",\n    \"google.co.jp\",\n    \"google.co.ke\",\n    \"google.co.kr\",\n    \"google.co.ls\",\n    \"google.co.ma\",\n    \"google.co.mz\",\n    \"google.co.nz\",\n    \"google.co.th\",\n    \"google.co.tz\",\n    \"google.co.ug\",\n    \"google.co.uk\",\n    \"google.co.uz\",\n    \"google.co.ve\",\n    \"google.co.vi\",\n    \"google.co.za\",\n    \"google.co.zm\",\n    \"google.co.zw\",\n    \"google.com\",\n    \"google.com.af\",\n    \"google.com.ag\",\n    \"google.com.ai\",\n    \"google.com.ar\",\n    \"google.com.au\",\n    \"google.com.bd\",\n    \"google.com.bh\",\n    \"google.com.bn\",\n    \"google.com.bo\",\n    \"google.com.br\",\n    \"google.com.bz\",\n    \"google.com.co\",\n    \"google.com.cu\",\n    \"google.com.cy\",\n    \"google.com.do\",\n    \"google.com.ec\",\n    \"google.com.eg\",\n    \"google.com.et\",\n    \"google.com.fj\",\n    \"google.com.gh\",\n    \"google.com.gi\",\n    \"google.com.gt\",\n    \"google.com.hk\",\n    \"google.com.jm\",\n    \"google.com.kh\",\n    \"google.com.kw\",\n    \"google.com.lb\",\n    \"google.com.ly\",\n    \"google.com.mm\",\n    \"google.com.mt\",\n    \"google.com.mx\",\n    \"google.com.my\",\n    \"google.com.na\",\n    \"google.com.ng\",\n    \"google.com.ni\",\n    \"google.com.np\",\n    \"google.com.om\",\n    \"google.com.pa\",\n    \"google.com.pe\",\n    \"google.com.pg\",\n    \"google.com.ph\",\n    \"google.com.pk\",\n    \"google.com.pr\",\n    \"google.com.py\",\n    \"google.com.qa\",\n    \"google.com.sa\",\n    \"google.com.sb\",\n    \"google.com.sg\",\n    \"google.com.sl\",\n    \"google.com.sv\",\n    \"google.com.tj\",\n    \"google.com.tr\",\n    \"google.com.tw\",\n    \"google.com.ua\",\n    \"google.com.uy\",\n    \"google.com.vc\",\n    \"google.com.vn\",\n    \"google.cv\",\n    \"google.cz\",\n    \"google.de\",\n    \"google.dj\",\n    \"google.dk\",\n    \"google.dm\",\n    \"google.dz\",\n    \"google.ee\",\n    \"google.es\",\n    \"google.fi\",\n    \"google.fm\",\n    \"google.fr\",\n    \"google.ga\",\n    \"google.ge\",\n    \"google.gl\",\n    \"google.gm\",\n    \"google.gp\",\n    \"google.gr\",\n    \"google.gy\",\n    \"google.hn\",\n    \"google.hr\",\n    \"google.ht\",\n    \"google.hu\",\n    \"google.ie\",\n    \"google.iq\",\n    \"google.is\",\n    \"google.it\",\n    \"google.je\",\n    \"google.jo\",\n    \"google.kg\",\n    \"google.ki\",\n    \"google.kz\",\n    \"google.la\",\n    \"google.li\",\n    \"google.lk\",\n    \"google.lt\",\n    \"google.lu\",\n    \"google.lv\",\n    \"google.md\",\n    \"google.mg\",\n    \"google.mk\",\n    \"google.ml\",\n    \"google.mn\",\n    \"google.ms\",\n    \"google.mu\",\n    \"google.mv\",\n    \"google.mw\",\n    \"google.ne\",\n    \"google.nl\",\n    \"google.no\",\n    \"google.nr\",\n    \"google.nu\",\n    \"google.pl\",\n    \"google.ps\",\n    \"google.pt\",\n    \"google.ro\",\n    \"google.rs\",\n    \"google.ru\",\n    \"google.rw\",\n    \"google.sc\",\n    \"google.se\",\n    \"google.sh\",\n    \"google.si\",\n    \"google.sk\",\n    \"google.sm\",\n    \"google.sn\",\n    \"google.so\",\n    \"google.sr\",\n    \"google.td\",\n    \"google.tg\",\n    \"google.tk\",\n    \"google.tl\",\n    \"google.tm\",\n    \"google.tn\",\n    \"google.to\",\n    \"google.tt\",\n    \"google.vg\",\n    \"google.vu\",\n    \"google.ws\"\n]"
  },
  {
    "path": "src/utils/human-message.tsx",
    "content": "import { isCustomModel } from \"@/db/dexie/models\"\nimport { HumanMessage, type MessageContent } from \"@langchain/core/messages\"\nimport { processImageForOCR } from \"./ocr\"\nimport { Storage } from \"@plasmohq/storage\"\nimport { getMemoriesAsContext } from \"@/db/dexie/memory\"\n\nconst storage = new Storage()\n\ntype HumanMessageType = {\n  content: MessageContent\n  model: string\n  useOCR: boolean\n}\n\nexport const humanMessageFormatter = async ({\n  content,\n  model,\n  useOCR = false\n}: HumanMessageType) => {\n  try {\n    // Get memory context if enabled\n    const enableMemory = await storage.get(\"enableMemory\")\n    let memoryContext = \"\"\n\n    if (enableMemory) {\n      const context = await getMemoriesAsContext()\n      if (context) {\n        memoryContext = `\\n\\n${context}`\n      }\n    }\n\n    const isCustom = isCustomModel(model)\n\n    if (isCustom) {\n      if (typeof content !== \"string\") {\n        if (content.length > 1) {\n          if (useOCR) {\n            // Process all images for OCR\n            const imageContents = content.filter(\n              (c: any) => c.type === \"image_url\"\n            )\n            const ocrTexts = await Promise.all(\n              imageContents.map((c: any) => processImageForOCR(c.image_url))\n            )\n            //@ts-ignore\n            const ocrPROMPT = `${content[0].text}\n\n[IMAGE OCR TEXT]\n${ocrTexts.join(\"\\n\\n---\\n\\n\")}${memoryContext}`\n            return new HumanMessage({\n              content: ocrPROMPT\n            })\n          }\n\n          // Reformat the image_url for all images\n          const newContent: MessageContent = [\n            {\n              type: \"text\",\n              //@ts-ignore\n              text: content[0].text + memoryContext\n            }\n          ]\n\n          // Add all images\n          const imageContents = content.filter(\n            (c: any) => c.type === \"image_url\"\n          )\n          if (imageContents.length > 0) {\n            imageContents.forEach((c: any) => {\n              if (c.image_url.length > 0) {\n                console.log(\n                  \"Adding image to custom model message:\",\n                  c.image_url\n                )\n                newContent.push({\n                  type: \"image_url\",\n                  image_url: {\n                    url: c.image_url\n                  }\n                })\n              }\n            })\n          }\n\n          return new HumanMessage({\n            content: newContent\n          })\n        } else {\n          return new HumanMessage({\n            //@ts-ignore\n            content: content[0].text + memoryContext\n          })\n        }\n      }\n    }\n\n    if (useOCR) {\n      if (typeof content !== \"string\" && content.length > 1) {\n        // Process all images for OCR\n        const imageContents = content.filter((c: any) => c.type === \"image_url\")\n        const ocrTexts = await Promise.all(\n          imageContents.map((c: any) => processImageForOCR(c.image_url))\n        )\n        //@ts-ignore\n        const ocrPROMPT = `${content[0].text}\n\n[IMAGE OCR TEXT]\n${ocrTexts.join(\"\\n\\n---\\n\\n\")}${memoryContext}`\n        return new HumanMessage({\n          content: ocrPROMPT\n        })\n      }\n    }\n \n    // Handle string content or fallback\n    if (typeof content === \"string\") {\n      return new HumanMessage({\n        content: content + memoryContext\n      })\n    }\n\n\n    if (Array.isArray(content)) {\n      return new HumanMessage({\n        content: content?.map((c: any, index: number) => \n          c.type === \"text\" && index === 0 ? { ...c, text: c.text + memoryContext } : c\n        )\n      })\n    }\n\n\n    return new HumanMessage({\n      content\n    })\n  } catch (e) {\n    return new HumanMessage({\n      content\n    })\n  }\n}\n"
  },
  {
    "path": "src/utils/humanize-milliseconds.ts",
    "content": "import dayjs from 'dayjs'\nimport duration from 'dayjs/plugin/duration'\n\ndayjs.extend(duration)\n\nexport const humanizeMilliseconds = (milliseconds: number): string => {\n    try {\n        const duration = dayjs.duration(milliseconds)\n\n        if (milliseconds < 1000) {\n            return `${milliseconds}ms`\n        }\n\n        if (milliseconds < 60000) {\n            return `${Math.floor(duration.asSeconds())}s`\n        }\n\n        if (milliseconds < 3600000) {\n            return `${Math.floor(duration.asMinutes())}m`\n        }\n\n        if (milliseconds < 86400000) {\n            return `${Math.floor(duration.asHours())}h`\n        }\n\n        return `${Math.floor(duration.asDays())}d`\n    } catch (e) {\n        return `${milliseconds}ms`\n    }\n}\n"
  },
  {
    "path": "src/utils/is-private-mode.ts",
    "content": "export const isFireFox = import.meta.env.BROWSER === \"firefox\"\n\nexport const isFireFoxPrivateMode =\n  isFireFox && browser.extension.inIncognitoContext\n"
  },
  {
    "path": "src/utils/is-youtube.ts",
    "content": "\nconst YT_REGEX =\n  /(?:https?:\\/\\/)?(?:www\\.)?(?:youtube\\.com|youtu\\.be)\\/(?:watch\\?v=)?([a-zA-Z0-9_-]+)/\n\n\nexport const isYoutubeLink = (url: string) => {\n    return YT_REGEX.test(url)\n}"
  },
  {
    "path": "src/utils/key-down.tsx",
    "content": "export const handleChatInputKeyDown = ({\n    e,\n    sendWhenEnter,\n    typing,\n    isSending\n}: {\n    e: React.KeyboardEvent\n    typing: boolean\n    sendWhenEnter: boolean\n    isSending: boolean\n}) => {\n    return import.meta.env.BROWSER === \"firefox\"\n        ? e.key === \"Enter\" &&\n        !e.shiftKey &&\n        !e.nativeEvent.isComposing &&\n        !isSending &&\n        sendWhenEnter\n        : !typing && e.key === \"Enter\" && !e.shiftKey && !isSending && sendWhenEnter\n}\n"
  },
  {
    "path": "src/utils/langauge-extension.ts",
    "content": "export const programmingLanguages = {\n    html: \"html\",\n    javascript: \"js\",\n    typescript: \"ts\",\n    python: \"py\",\n    java: \"java\",\n    cpp: \"cpp\",\n    c: \"c\",\n    csharp: \"cs\",\n    ruby: \"rb\",\n    php: \"php\",\n    swift: \"swift\",\n    go: \"go\",\n    rust: \"rs\",\n    kotlin: \"kt\",\n    sql: \"sql\",\n    shell: \"sh\",\n    markdown: \"md\",\n    json: \"json\",\n    yaml: \"yml\",\n    xml: \"xml\",\n    css: \"css\",\n    scss: \"scss\",\n    jsx: \"jsx\",\n    tsx: \"tsx\",\n    vue: \"vue\",\n    dart: \"dart\",\n    lua: \"lua\"\n}"
  },
  {
    "path": "src/utils/latex.ts",
    "content": "// Following code is copied from LibreChat by danny-avila \n// Github Repo: https://github.com/danny-avila/LibreChat\nexport const preprocessLaTeX = (content: string) => {\n    const codeBlocks: string[] = [];\n    content = content.replace(/(```[\\s\\S]*?```|`[^`\\n]+`)/g, (match, code) => {\n        codeBlocks.push(code);\n        return `<<CODE_BLOCK_${codeBlocks.length - 1}>>`;\n    });\n\n    const latexExpressions: string[] = [];\n    content = content.replace(/(\\$\\$[\\s\\S]*?\\$\\$|\\\\\\[[\\s\\S]*?\\\\\\]|\\\\\\(.*?\\\\\\))/g, (match) => {\n        latexExpressions.push(match);\n        return `<<LATEX_${latexExpressions.length - 1}>>`;\n    });\n    content = content.replace(/\\$(?=\\d)/g, '\\\\$');\n\n    content = content.replace(/<<LATEX_(\\d+)>>/g, (_, index) => latexExpressions[parseInt(index)]);\n\n    content = content.replace(/<<CODE_BLOCK_(\\d+)>>/g, (_, index) => codeBlocks[parseInt(index)]);\n\n    content = escapeBrackets(content);\n    content = escapeMhchem(content);\n\n    return content;\n}\n\n\nexport function escapeBrackets(text: string): string {\n    const pattern = /(```[\\S\\s]*?```|`.*?`)|\\\\\\[([\\S\\s]*?[^\\\\])\\\\]|\\\\\\((.*?)\\\\\\)/g;\n    return text.replace(\n        pattern,\n        (\n            match: string,\n            codeBlock: string | undefined,\n            squareBracket: string | undefined,\n            roundBracket: string | undefined,\n        ): string => {\n            if (codeBlock != null) {\n                return codeBlock;\n            } else if (squareBracket != null) {\n                return `$$${squareBracket}$$`;\n            } else if (roundBracket != null) {\n                return `$${roundBracket}$`;\n            }\n            return match;\n        },\n    );\n}\n\nexport function escapeMhchem(text: string) {\n    return text.replaceAll('$\\\\ce{', '$\\\\\\\\ce{').replaceAll('$\\\\pu{', '$\\\\\\\\pu{');\n}"
  },
  {
    "path": "src/utils/markdown-to-ssml.ts",
    "content": "export function markdownToSSML(markdown: string): string {\n  let ssml = markdown.replace(/\\\\n/g, \"<break/>\")\n\n  ssml = ssml.replace(\n    /^(#{1,6}) (.*?)(?=\\r?\\n\\s*?(?:\\r?\\n|$))/gm,\n    (match, hashes, heading) => {\n      const level = hashes.length\n      const rate = (level - 1) * 10 + 100\n      return `<prosody rate=\"${rate}%\">${heading}</prosody>`\n    }\n  )\n\n  ssml = ssml.replace(/\\\\\\*\\\\\\*(.\\*?)\\\\\\*\\\\\\*/g, \"<emphasis>$1</emphasis>\")\n  ssml = ssml.replace(\n    /\\\\\\*(.\\*?)\\\\\\*/g,\n    '<amazon:effect name=\"whispered\">$1</amazon:effect>'\n  )\n  ssml = `<speak>${ssml}</speak>`\n  return `<?xml version=\"1.0\"?>${ssml}`\n}\n"
  },
  {
    "path": "src/utils/markdown-to-text.ts",
    "content": "export function markdownToText(markdown: string): string {\n    if (!markdown) {\n      return '';\n    }\n  \n    let text = markdown.replace(/```[\\s\\S]*?```/g, '');\n  \n    // Remove inline code\n    text = text.replace(/`([^`]+)`/g, '$1');\n  \n    // Remove SVG content\n    text = text.replace(/<svg[\\s\\S]*?<\\/svg>/g, '');\n  \n    // Remove HTML tags\n    text = text.replace(/<[^>]*>/g, '');\n  \n    // Replace headers\n    text = text.replace(/^#{1,6}\\s+(.+)$/gm, '$1');\n  \n    // Replace bold/italic\n    text = text.replace(/(\\*\\*|__)(.*?)\\1/g, '$2');\n    text = text.replace(/(\\*|_)(.*?)\\1/g, '$2');\n  \n    // Replace links\n    text = text.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '$1');\n  \n    // Replace images\n    text = text.replace(/!\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '$1');\n  \n    // Replace horizontal rules\n    text = text.replace(/^\\s*[-*_]{3,}\\s*$/gm, '');\n  \n    // Replace blockquotes\n    text = text.replace(/^>\\s+(.+)$/gm, '$1');\n  \n    // Replace ordered and unordered lists\n    text = text.replace(/^(\\s*)[-*+]\\s+(.+)$/gm, '$1$2');\n    text = text.replace(/^(\\s*)\\d+\\.\\s+(.+)$/gm, '$1$2');\n  \n    // Replace multiple newlines with a single one\n    text = text.replace(/\\n{3,}/g, '\\n\\n');\n  \n    // Trim whitespace\n    text = text.trim();\n  \n    return text;\n  }\n  "
  },
  {
    "path": "src/utils/marked/katex.tsx",
    "content": "import katex from \"katex\"\n\ninterface KatexOptions {\n  displayMode?: boolean\n  throwOnError?: boolean\n  errorColor?: string\n  macros?: Record<string, string>\n  minRuleThickness?: number\n  colorIsTextColor?: boolean\n  maxSize?: number\n  maxExpand?: number\n  strict?: boolean | string | Function\n  trust?: boolean | Function\n  fleqn?: boolean\n  leqno?: boolean\n  output?: \"html\" | \"mathml\" | \"htmlAndMathml\"\n  nonStandard?: boolean\n}\n\ninterface KatexToken {\n  type: \"inlineKatex\" | \"blockKatex\"\n  raw: string\n  text: string\n  displayMode: boolean\n}\n\ninterface MarkedToken {\n  type: string\n  raw: string\n  [key: string]: any\n}\n\ninterface TokenizerThis {\n  lexer: {\n    state: {\n      inLink: boolean\n      inRawBlock: boolean\n      top: boolean\n    }\n  }\n}\n\ninterface MarkedExtension {\n  name: string\n  level: \"inline\" | \"block\"\n  start?: (src: string) => number | void\n  tokenizer: (\n    this: TokenizerThis,\n    src: string,\n    tokens: MarkedToken[]\n  ) => KatexToken | void\n  renderer: (token: KatexToken) => string\n}\n\ninterface KatexMarkedExtension {\n  extensions: MarkedExtension[]\n}\n\ntype RendererFunction = (token: KatexToken) => string\n\nconst inlineRule =\n  /^(\\${1,2})(?!\\$)((?:\\\\.|[^\\\\\\n])*?(?:\\\\.|[^\\\\\\n\\$]))\\1(?=[\\s?!\\.,:？！。，：]|$)/\nconst inlineRuleNonStandard =\n  /^(\\${1,2})(?!\\$)((?:\\\\.|[^\\\\\\n])*?(?:\\\\.|[^\\\\\\n\\$]))\\1/ // Non-standard, even if there are no spaces before and after $ or $, try to parse\n\nconst blockRule = /^(\\${1,2})\\n((?:\\\\[^]|[^\\\\])+?)\\n\\1(?:\\n|$)/\n\nexport default function markedKatexExtension(options: KatexOptions = {}): KatexMarkedExtension {\n  return {\n    extensions: [\n      inlineKatex(options, createRenderer(options, false)),\n      blockKatex(options, createRenderer(options, true))\n    ]\n  }\n}\n\nfunction createRenderer(options: any, newlineAfter: boolean): RendererFunction {\n  return (token: KatexToken): string => {\n    try {\n      return (\n        katex.renderToString(token.text, {\n          ...options,\n          displayMode: token.displayMode\n        }) + (newlineAfter ? \"\\n\" : \"\")\n      )\n    } catch (error) {\n      console.error(\"KaTeX rendering error:\", error)\n      return `<span class=\"katex-error\" title=\"${error instanceof Error ? error.message : \"Unknown error\"}\">${token.text}</span>`\n    }\n  }\n}\n\nfunction inlineKatex(\n  options: KatexOptions,\n  renderer: RendererFunction\n): MarkedExtension {\n  const nonStandard = options && options.nonStandard\n  const ruleReg = nonStandard ? inlineRuleNonStandard : inlineRule\n\n  return {\n    name: \"inlineKatex\",\n    level: \"inline\",\n    start(src: string): number | void {\n      let index: number\n      let indexSrc = src\n\n      while (indexSrc) {\n        index = indexSrc.indexOf(\"$\")\n        if (index === -1) {\n          return\n        }\n        const f = nonStandard\n          ? index > -1\n          : index === 0 || indexSrc.charAt(index - 1) === \" \"\n        if (f) {\n          const possibleKatex = indexSrc.substring(index)\n\n          if (possibleKatex.match(ruleReg)) {\n            return index\n          }\n        }\n\n        indexSrc = indexSrc.substring(index + 1).replace(/^\\$+/, \"\")\n      }\n    },\n    tokenizer(\n      this: TokenizerThis,\n      src: string,\n      tokens: MarkedToken[]\n    ): KatexToken | void {\n      const match = src.match(ruleReg)\n      if (match) {\n        return {\n          type: \"inlineKatex\",\n          raw: match[0],\n          text: match[2].trim(),\n          displayMode: match[1].length === 2\n        }\n      }\n    },\n    renderer\n  }\n}\n\nfunction blockKatex(\n  options: KatexOptions,\n  renderer: RendererFunction\n): MarkedExtension {\n  return {\n    name: \"blockKatex\",\n    level: \"block\",\n    tokenizer(\n      this: TokenizerThis,\n      src: string,\n      tokens: MarkedToken[]\n    ): KatexToken | void {\n      const match = src.match(blockRule)\n      if (match) {\n        return {\n          type: \"blockKatex\",\n          raw: match[0],\n          text: match[2].trim(),\n          displayMode: match[1].length === 2\n        }\n      }\n    },\n    renderer\n  }\n}\n"
  },
  {
    "path": "src/utils/math-delimiter.ts",
    "content": "export const convertMathDelimiters = (text: string): string => {\n  text = text.replace(/\\\\\\[([\\s\\S]*?)\\\\\\]/g, \"$$\\n$1\\n$$\")\n  text = text.replace(/\\\\\\(([\\s\\S]*?)\\\\\\)/g, \"$$$1$$\")\n  return text\n}"
  },
  {
    "path": "src/utils/memory-embeddings.ts",
    "content": "import { PageAssistHtmlLoader } from \"@/loader/html\"\n\nimport { PageAssistPDFLoader } from \"@/loader/pdf\"\nimport { PAMemoryVectorStore } from \"@/libs/PAMemoryVectorStore\"\nimport { getPageAssistTextSplitter } from \"./text-splitter\"\n\nexport const getLoader = ({\n  html,\n  pdf,\n  type,\n  url\n}: {\n  url: string\n  html: string\n  type: string\n  pdf: { content: string; page: number }[]\n}) => {\n  if (type === \"pdf\") {\n    return new PageAssistPDFLoader({\n      pdf,\n      url\n    })\n  } else {\n    return new PageAssistHtmlLoader({\n      html,\n      url\n    })\n  }\n}\n\nexport const memoryEmbedding = async ({\n  html,\n  keepTrackOfEmbedding,\n  ollamaEmbedding,\n  pdf,\n  setIsEmbedding,\n  setKeepTrackOfEmbedding,\n  type,\n  url\n}: {\n  url: string\n  html: string\n  type: string\n  pdf: { content: string; page: number }[]\n  keepTrackOfEmbedding: Record<string, PAMemoryVectorStore>\n  ollamaEmbedding: any\n  setIsEmbedding: (value: boolean) => void\n  setKeepTrackOfEmbedding: (value: Record<string, PAMemoryVectorStore>) => void\n}) => {\n  setIsEmbedding(true)\n  const loader = getLoader({ html, pdf, type, url })\n  const docs = await loader.load()\n  const textSplitter = await getPageAssistTextSplitter()\n\n  const chunks = await textSplitter.splitDocuments(docs)\n\n  const store = new PAMemoryVectorStore(ollamaEmbedding)\n\n  await store.addDocuments(chunks)\n  setKeepTrackOfEmbedding({\n    ...keepTrackOfEmbedding,\n    [url]: store\n  })\n  setIsEmbedding(false)\n  return store\n}\n"
  },
  {
    "path": "src/utils/model.ts",
    "content": "import { getModelInfo, isCustomModel } from \"@/db/dexie/models\"\nimport { Storage } from \"@plasmohq/storage\"\nconst storage = new Storage()\n\nexport const getSelectedModelName = async (): Promise<string> => {\n    const selectedModel = await storage.get(\"selectedModel\")\n    const isCustom = isCustomModel(selectedModel)\n    if (isCustom) {\n        const customModel = await getModelInfo(selectedModel)\n        if (customModel) {\n            return customModel.name\n        } else {\n            return selectedModel\n        }\n    }\n    return selectedModel\n}"
  },
  {
    "path": "src/utils/oai-api-providers.ts",
    "content": "export const OAI_API_PROVIDERS = [\n  {\n    label: \"Custom\",\n    value: \"custom\",\n    baseUrl: \"\"\n  },\n  {\n    label: \"LLaMa.cpp\",\n    value: \"llamacpp\",\n    baseUrl: \"http://localhost:8080/v1\"\n  },\n  {\n    label: \"LM Studio\",\n    value: \"lmstudio\",\n    baseUrl: \"http://localhost:1234/v1\"\n  },\n  {\n    label: \"Llamafile\",\n    value: \"llamafile\",\n    baseUrl: \"http://127.0.0.1:8080/v1\"\n  },\n  {\n    label: \"Ollama\",\n    value: \"ollama2\",\n    baseUrl: \"http://localhost:11434/v1\"\n  },\n  {\n    label: \"OpenAI\",\n    value: \"openai\",\n    baseUrl: \"https://api.openai.com/v1\"\n  },\n  {\n    label: \"DeepSeek\",\n    value: \"deepseek\",\n    baseUrl: \"https://api.deepseek.com\"\n  },\n  {\n    label: \"Fireworks\",\n    value: \"fireworks\",\n    baseUrl: \"https://api.fireworks.ai/inference/v1\"\n  },\n  {\n    label: \"Novita AI\",\n    value: \"novita\",\n    baseUrl: \"https://api.novita.ai/v3/openai\"\n  },\n  {\n    label: \"Hugging Face\",\n    value: \"huggingface\",\n    baseUrl: \"https://router.huggingface.co/v1\"\n  },\n  {\n    label: \"Groq\",\n    value: \"groq\",\n    baseUrl: \"https://api.groq.com/openai/v1\"\n  },\n  {\n    label: \"Together\",\n    value: \"together\",\n    baseUrl: \"https://api.together.xyz/v1\"\n  },\n  {\n    label: \"OpenRouter\",\n    value: \"openrouter\",\n    baseUrl: \"https://openrouter.ai/api/v1\"\n  },\n  {\n    label: \"Google AI\",\n    value: \"gemini\",\n    baseUrl: \"https://generativelanguage.googleapis.com/v1beta/openai\"\n  },\n  {\n    label: \"Mistral\",\n    value: \"mistral\",\n    baseUrl: \"https://api.mistral.ai/v1\"\n  },\n  {\n    label: \"Infinigence AI\",\n    value: \"infinitenceai\",\n    baseUrl: \"https://cloud.infini-ai.com/maas/v1\"\n  },\n  {\n    label: \"SiliconFlow\",\n    value: \"siliconflow\",\n    baseUrl: \"https://api.siliconflow.cn/v1\"\n  },\n  {\n    label: \"VolcEngine\",\n    value: \"volcengine\",\n    baseUrl: \"https://ark.cn-beijing.volces.com/api/v3\"\n  },\n  {\n    label: \"TencentCloud\",\n    value: \"tencentcloud\",\n    baseUrl: \"https://api.lkeap.cloud.tencent.com/v1\"\n  },\n  {\n    label: \"AliBaBaCloud\",\n    value: \"alibabacloud\",\n    baseUrl: \"https://dashscope.aliyuncs.com/compatible-mode/v1\"\n  },\n  {\n    label: \"vLLM\",\n    value: \"vllm\",\n    baseUrl: \"http://localhost:8000/v1\"\n  },\n  {\n    label: \"Moonshot\",\n    value: \"moonshot\",\n    baseUrl: \"https://api.moonshot.ai/v1\"\n  },\n  {\n    label: \"xAI\",\n    value: \"xai\",\n    baseUrl: \"https://api.x.ai/v1\"\n  },\n  {\n    label: \"Vercel AI Gateway\",\n    value: \"vercel\",\n    baseUrl: \"https://ai-gateway.vercel.sh/v1\"\n  },\n  {\n    label: \"Chutes\",\n    value: \"chutes\",\n    baseUrl: \"https://llm.chutes.ai/v1\"\n  },\n  {\n    label: \"Anthropic (Claude)\",\n    value: \"anthropic\",\n    baseUrl: \"https://api.anthropic.com/v1\"\n  },\n  {\n    label: \"Canopy Wave\",\n    value: \"canopywave\",\n    baseUrl: \"https://inference.canopywave.io/v1\"\n  },\n  {\n    label: 'BigModel (Zhipu)',\n    value: 'zhipu',\n    baseUrl: 'https://open.bigmodel.cn/api/paas/v4'\n  },\n  {\n    label: 'MiniMax',\n    value: 'minimax',\n    baseUrl: 'https://api.minimax.io/v1'\n  }\n]"
  },
  {
    "path": "src/utils/ocr.ts",
    "content": "import { getOCRLanguageToUse, isOfflineOCR } from \"@/services/ocr\"\nimport { createWorker } from \"pa-tesseract.js\"\n\nexport async function processImageForOCR(imageData: string): Promise<string> {\n    try {\n        const lang = await getOCRLanguageToUse() \n        const isOCROffline = isOfflineOCR(lang)\n        \n        const worker = await createWorker(lang, undefined, {\n            workerPath: \"/ocr/worker.min.js\",\n            workerBlobURL: false,\n            corePath: \"/ocr/tesseract-core-simd.js\",\n            errorHandler: (e) => console.error(e),\n            langPath: !isOCROffline ? \"/ocr/lang\" : undefined\n        })\n\n        const result = await worker.recognize(imageData)\n\n        await worker.terminate()\n\n        return result.data.text\n    } catch (error) {\n        console.error(\"Error processing image for OCR:\", error)\n        return \"\"\n    }\n}\n"
  },
  {
    "path": "src/utils/ollama-pull-inject.ts",
    "content": "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\">\n  <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\" />\n  </svg>`\n\nfunction makeDownloadButton(\n  id: string,\n  modelName: string,\n  className: string,\n  sendMessage: (modelName: string) => Promise<void>\n): HTMLButtonElement {\n  const btn = document.createElement(\"button\")\n  btn.id = id\n  btn.innerHTML = downloadSVG\n  btn.title = \"Download model via Page Assist\"\n  btn.className = className\n  btn.addEventListener(\"click\", async (e) => {\n    e.preventDefault()\n    e.stopPropagation()\n    const ok = confirm(\n      `[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.`\n    )\n    if (ok) {\n      alert(\n        `[Page Assist Extension] Pulling ${modelName} model. For more details, check the extension icon.`\n      )\n      await sendMessage(modelName)\n    }\n  })\n  return btn\n}\n\nexport function injectOllamaPullButtons(\n  sendMessage: (modelName: string) => Promise<void>\n) {\n  const usageSection = document.querySelector(\"section[data-usage-section]\")\n  if (usageSection && !document.getElementById(\"pa-download-cli\")) {\n    const cliPanel = usageSection.querySelector<HTMLElement>(\n      '.use-panel[data-panel=\"cli\"]'\n    )\n    const copyBtn = usageSection.querySelector<HTMLButtonElement>(\n      \"button.use-copy-btn\"\n    )\n    if (cliPanel && copyBtn) {\n      const modelName = cliPanel\n        .querySelector(\"pre\")\n        ?.textContent?.replace(\"ollama run\", \"\")\n        .replace(\"ollama pull\", \"\")\n        .trim()\n      if (modelName) {\n        const btn = makeDownloadButton(\n          \"pa-download-cli\",\n          modelName,\n          copyBtn.className,\n          sendMessage\n        )\n        copyBtn.insertAdjacentElement(\"afterend\", btn)\n      }\n    }\n  }\n\n  const appSection = document.getElementById(\"external-tools-section\")\n  const commandInputs =\n    document.querySelectorAll<HTMLInputElement>(\"input.command\")\n\n  for (let i = 0; i < commandInputs.length; i++) {\n    const input = commandInputs[i]\n\n    if (appSection?.contains(input)) continue\n\n    const modelName = input.value.trim()\n    if (!modelName) continue\n\n    const buttonId = `pa-download-model-${i}`\n    if (document.getElementById(buttonId)) continue\n\n    const copyButton = input.nextElementSibling as HTMLButtonElement | null\n    if (!copyButton || copyButton.tagName !== \"BUTTON\") continue\n\n    const btn = makeDownloadButton(\n      buttonId,\n      modelName,\n      copyButton.className,\n      sendMessage\n    )\n    copyButton.insertAdjacentElement(\"afterend\", btn)\n  }\n}\n"
  },
  {
    "path": "src/utils/pull-ollama.ts",
    "content": "import { setBadgeBackgroundColor, setBadgeText, setTitle } from \"@/utils/action\"\nimport fetcher from \"@/libs/fetcher\"\nimport { Storage } from \"@plasmohq/storage\"\n\nconst storage = new Storage({\n  area: \"local\"\n})\n\nexport const progressHuman = (completed: number, total: number) => {\n  return ((completed / total) * 100).toFixed(0) + \"%\"\n}\n\nexport const clearBadge = () => {\n  setBadgeText({ text: \"\" })\n  setTitle({ title: \"\" })\n}\n\nexport const setDownloadState = async (\n  modelName: string | null,\n  isDownloading: boolean\n) => {\n  await storage.set(\"downloadingModel\", {\n    modelName: decodeURIComponent(modelName || \"\"),\n    isDownloading\n  })\n}\n\nexport const getDownloadState = async () => {\n  const state = await storage.get(\"downloadingModel\")\n  return state || { modelName: null, isDownloading: false }\n}\n\nexport const cancelDownload = async () => {\n  await storage.set(\"cancelDownload\", true)\n}\nexport const streamDownload = async (url: string, model: string) => {\n  url += \"/api/pull\"\n\n  await setDownloadState(model, true)\n\n  await storage.set(\"cancelDownload\", false)\n\n  const abortController = new AbortController()\n\n  const response = await fetcher(url, {\n    method: \"POST\",\n    headers: {\n      \"Content-Type\": \"application/json\"\n    },\n    body: JSON.stringify({ model, stream: true }),\n    signal: abortController.signal\n  })\n\n  const reader = response.body?.getReader()\n  const decoder = new TextDecoder()\n\n  let isSuccess = true\n  let isCancelled = false\n\n  while (true) {\n    if (!reader) {\n      break\n    }\n\n    const cancelFlag = await storage.get(\"cancelDownload\")\n    if (cancelFlag) {\n      abortController.abort()\n      isCancelled = true\n      break\n    }\n\n    const { done, value } = await reader.read()\n\n    if (done) {\n      break\n    }\n\n    const text = decoder.decode(value)\n    try {\n      const json = JSON.parse(text.trim()) as {\n        status: string\n        total?: number\n        completed?: number\n      }\n      if (json.total && json.completed) {\n        setBadgeText({\n          text: progressHuman(json.completed, json.total)\n        })\n        setBadgeBackgroundColor({ color: \"#0000FF\" })\n      } else {\n        setBadgeText({ text: \"🏋️‍♂️\" })\n        setBadgeBackgroundColor({ color: \"#FFFFFF\" })\n      }\n\n      setTitle({ title: json.status })\n\n      if (json.status === \"success\") {\n        isSuccess = true\n      }\n    } catch (e) {\n      console.error(e)\n    }\n  }\n\n  await setDownloadState(null, false)\n  await storage.set(\"cancelDownload\", false)\n\n  if (isCancelled) {\n    setBadgeText({ text: \"⭕\" })\n    setBadgeBackgroundColor({ color: \"#FFA500\" })\n    setTitle({ title: \"Model download cancelled\" })\n  } else if (isSuccess) {\n    setBadgeText({ text: \"✅\" })\n    setBadgeBackgroundColor({ color: \"#00FF00\" })\n    setTitle({ title: \"Model pulled successfully\" })\n  } else {\n    setBadgeText({ text: \"❌\" })\n    setBadgeBackgroundColor({ color: \"#FF0000\" })\n    setTitle({ title: \"Model pull failed\" })\n  }\n\n  setTimeout(() => {\n    clearBadge()\n  }, 5000)\n}\n"
  },
  {
    "path": "src/utils/rerank.ts",
    "content": "import type { EmbeddingsInterface } from \"@langchain/core/embeddings\"\nimport type { Document } from \"@langchain/core/documents\"\nimport * as ml_distance from \"ml-distance\"\n\nexport const rerankDocs = async ({\n  query,\n  docs,\n  embedding\n}: {\n  query: string\n  docs: Document[]\n  embedding: EmbeddingsInterface\n}) => {\n  if (docs.length === 0) {\n    return docs\n  }\n\n  const docsWithContent = docs.filter(\n    (doc) => doc.pageContent && doc.pageContent.length > 0\n  )\n\n  const [docEmbeddings, queryEmbedding] = await Promise.all([\n    embedding.embedDocuments(docsWithContent.map((doc) => doc.pageContent)),\n    embedding.embedQuery(query)\n  ])\n\n  const similarity = docEmbeddings.map((docEmbedding, i) => {\n    // perform cosine similarity between query and document\n    const sim = ml_distance.similarity.cosine(queryEmbedding, docEmbedding)\n\n    return {\n      index: i,\n      similarity: sim\n    }\n  })\n\n  const sortedDocs = similarity\n    .sort((a, b) => b.similarity - a.similarity)\n    .filter((sim) => sim.similarity > 0.5)\n    .slice(0, 15)\n    .map((sim) => docsWithContent[sim.index])\n\n  return sortedDocs\n}\n"
  },
  {
    "path": "src/utils/search-provider.ts",
    "content": "export const SUPPORTED_SEARCH_PROVIDERS = [\n    {\n        label: \"Google\",\n        value: \"google\"\n    },\n    {\n        label: \"DuckDuckGo\",\n        value: \"duckduckgo\"\n    },\n    {\n        label: \"Sogou\",\n        value: \"sogou\"\n    },\n    {\n        label: \"Baidu\",\n        value: \"baidu\"\n    },\n    {\n        label: \"Brave\",\n        value: \"brave\"\n    },\n    {\n        label: \"Searxng\",\n        value: \"searxng\"\n    },\n    {\n        label: \"Brave Search API\",\n        value: \"brave-api\"\n    },\n    {\n        label: \"Tavily Search API\",\n        value: \"tavily-api\"\n    },\n    {\n        label: \"Bing\",\n        value: \"bing\"\n    },\n    {\n        label: \"Stract\",\n        value: \"stract\"\n    },\n    {\n        label: \"Startpage\",\n        value: \"startpage\"\n    },\n    {\n        label: \"Exa\",\n        value: \"exa\"\n    },\n    {\n        label: \"Firecrawl\",\n        value: \"firecrawl\"\n    },\n    {\n        label: \"Ollama Web search\",\n        value: \"ollama-search\"\n    },\n    {\n        label: \"Kagi Search API*\",\n        value: \"kagi-api\",\n    },\n    {\n        label: \"Perplexity Search API\",\n        value: \"perplexity-api\"\n    }\n]"
  },
  {
    "path": "src/utils/select-variable.ts",
    "content": "export const getVariable = (text: string) => {\n    const regex = /{([^}]+)}/g;\n    let data : {\n        word: string,\n        start: number,\n        end: number\n    } | null = null;\n\n\n    let m: RegExpExecArray | null;\n\n    while ((m = regex.exec(text)) !== null) {\n        if (m.index === regex.lastIndex) {\n            regex.lastIndex++;\n        }\n        data = {\n            word: m[1],\n            start: m.index,\n            end: m.index + m[0].length\n        }\n    }\n\n    return data;\n}"
  },
  {
    "path": "src/utils/supported-languages.ts",
    "content": "export const SUPPORTED_LANGUAGES = [\n  {\n    label: \"Afrikaans (Namibië)\",\n    value: \"af-NA\"\n  },\n  {\n    label: \"Afrikaans \",\n    value: \"af-ZA\"\n  },\n  {\n    label: \"አማርኛ\",\n    value: \"am-ET\"\n  },\n  {\n    label: \"العربية (الإمارات العربية المتحدة)\",\n    value: \"ar-AE\"\n  },\n  {\n    label: \"العربية (الجزائر)\",\n    value: \"ar-DZ\"\n  },\n  {\n    label: \"العربية (مصر)\",\n    value: \"ar-EG\"\n  },\n  {\n    label: \"العربية (العراق)\",\n    value: \"ar-IQ\"\n  },\n  {\n    label: \"العربية (الأردن)\",\n    value: \"ar-JO\"\n  },\n  {\n    label: \"العربية (الكويت)\",\n    value: \"ar-KW\"\n  },\n  {\n    label: \"العربية (لبنان)\",\n    value: \"ar-LB\"\n  },\n  {\n    label: \"العربية (المغرب)\",\n    value: \"ar-MA\"\n  },\n  {\n    label: \"العربية (قطر)\",\n    value: \"ar-QA\"\n  },\n  {\n    label: \"العربية\",\n    value: \"ar-SA\"\n  },\n  {\n    label: \"العربية (سوريا)\",\n    value: \"ar-SY\"\n  },\n  {\n    label: \"العربية (تونس)\",\n    value: \"ar-TN\"\n  },\n  {\n    label: \"অসমীয়া\",\n    value: \"as-IN\"\n  },\n  {\n    label: \"Azərbaycanca\",\n    value: \"az-AZ\"\n  },\n  {\n    label: \"Беларуская\",\n    value: \"be-BY\"\n  },\n  {\n    label: \"български\",\n    value: \"bg-BG\"\n  },\n  {\n    label: \"বাংলা\",\n    value: \"bn-BD\"\n  },\n  {\n    label: \"বাংলাদেশ\",\n    value: \"bn-IN\"\n  },\n  {\n    label: \"Bosanski\",\n    value: \"bs-BA\"\n  },\n  {\n    label: \"Català\",\n    value: \"ca-ES\"\n  },\n  {\n    label: \"中文 (普通话 中国大陆)\",\n    value: \"cmn-Hans-CN\"\n  },\n  {\n    label: \"中文 (普通话 香港)\",\n    value: \"cmn-Hans-HK\"\n  },\n  {\n    label: \"中文 (普通话 马来西亚)\",\n    value: \"cmn-Hans-MY\"\n  },\n  {\n    label: \"中文 (普通话 新加坡)\",\n    value: \"cmn-Hans-SG\"\n  },\n  {\n    label: \"中文 (台灣)\",\n    value: \"cmn-Hant-TW\"\n  },\n  {\n    label: \"Čeština\",\n    value: \"cs-CZ\"\n  },\n  {\n    label: \"Dansk\",\n    value: \"da-DK\"\n  },\n  {\n    label: \"Deutsch (Österreich)\",\n    value: \"de-AT\"\n  },\n  {\n    label: \"Deutsch (Schweiz)\",\n    value: \"de-CH\"\n  },\n  {\n    label: \"Deutsch\",\n    value: \"de-DE\"\n  },\n  {\n    label: \"Ελληνικά \",\n    value: \"el-GR\"\n  },\n  {\n    label: \"English (Australia)\",\n    value: \"en-AU\"\n  },\n  {\n    label: \"English (Canada)\",\n    value: \"en-CA\"\n  },\n  {\n    label: \"English (United Kingdom)\",\n    value: \"en-GB\"\n  },\n  {\n    label: \"English (Ghana)\",\n    value: \"en-GH\"\n  },\n  {\n    label: \"English (Ireland)\",\n    value: \"en-IE\"\n  },\n  {\n    label: \"English (India)\",\n    value: \"en-IN\"\n  },\n  {\n    label: \"English (Jamaica)\",\n    value: \"en-JM\"\n  },\n  {\n    label: \"English (Kenya)\",\n    value: \"en-KE\"\n  },\n  {\n    label: \"English (Nigeria)\",\n    value: \"en-NG\"\n  },\n  {\n    label: \"English (New Zealand)\",\n    value: \"en-NZ\"\n  },\n  {\n    label: \"English (Philippines)\",\n    value: \"en-PH\"\n  },\n  {\n    label: \"English (Trinidad and Tobago)\",\n    value: \"en-TT\"\n  },\n  {\n    label: \"English (Tanzania)\",\n    value: \"en-TZ\"\n  },\n  {\n    label: \"English (United States)\",\n    value: \"en-US\"\n  },\n  {\n    label: \"English (South Africa)\",\n    value: \"en-ZA\"\n  },\n  {\n    label: \"Español (Argentina)\",\n    value: \"es-AR\"\n  },\n  {\n    label: \"Español (Argentina)\",\n    value: \"es-AR\"\n  },\n  {\n    label: \"Español (Bolivia)\",\n    value: \"es-BO\"\n  },\n  {\n    label: \"Español (Chile)\",\n    value: \"es-CL\"\n  },\n  {\n    label: \"Español (Colombia)\",\n    value: \"es-CO\"\n  },\n  {\n    label: \"Español (Costa Rica)\",\n    value: \"es-CR\"\n  },\n  {\n    label: \"Español (República Dominicana)\",\n    value: \"es-DO\"\n  },\n  {\n    label: \"Español (Ecuador)\",\n    value: \"es-EC\"\n  },\n  {\n    label: \"Español (España)\",\n    value: \"es-ES\"\n  },\n  {\n    label: \"Español (Guatemala)\",\n    value: \"es-GT\"\n  },\n  {\n    label: \"Español (Honduras)\",\n    value: \"es-HN\"\n  },\n  {\n    label: \"Español (México)\",\n    value: \"es-MX\"\n  },\n  {\n    label: \"Español (Nicaragua)\",\n    value: \"es-NI\"\n  },\n  {\n    label: \"Español (Panamá)\",\n    value: \"es-PA\"\n  },\n  {\n    label: \"Español (Perú)\",\n    value: \"es-PE\"\n  },\n  {\n    label: \"Español (Puerto Rico)\",\n    value: \"es-PR\"\n  },\n  {\n    label: \"Español (Paraguay)\",\n    value: \"es-PY\"\n  },\n  {\n    label: \"Español (El Salvador)\",\n    value: \"es-SV\"\n  },\n  {\n    label: \"Español (Estados Unidos)\",\n    value: \"es-US\"\n  },\n  {\n    label: \"Español (Uruguay)\",\n    value: \"es-UY\"\n  },\n  {\n    label: \"Español (Venezuela)\",\n    value: \"es-VE\"\n  },\n  {\n    label: \"Eesti\",\n    value: \"et-EE\"\n  },\n  {\n    label: \"Euskara\",\n    value: \"eu-ES\"\n  },\n  {\n    label: \"فارسی (افغانستان)\",\n    value: \"fa-AF\"\n  },\n  {\n    label: \"فارسی (ایران)\",\n    value: \"fa-IR\"\n  },\n  {\n    label: \"Suomi\",\n    value: \"fi-FI\"\n  },\n  {\n    label: \"Filipino\",\n    value: \"fil-PH\"\n  },\n  {\n    label: \"Français (Belgique)\",\n    value: \"fr-BE\"\n  },\n  {\n    label: \"Français (Canada)\",\n    value: \"fr-CA\"\n  },\n  {\n    label: \"Français (Suisse)\",\n    value: \"fr-CH\"\n  },\n  {\n    label: \"Français\",\n    value: \"fr-FR\"\n  },\n  {\n    label: \"Galego\",\n    value: \"gl-ES\"\n  },\n  {\n    label: \"ગુજરાતી\",\n    value: \"gu-IN\"\n  },\n  {\n    label: \"Hausa\",\n    value: \"ha-NG\"\n  },\n  {\n    label: \"עברית\",\n    value: \"he-IL\"\n  },\n  {\n    label: \"हिन्दी \",\n    value: \"hi-IN\"\n  },\n  {\n    label: \"Hrvatski\",\n    value: \"hr-HR\"\n  },\n  {\n    label: \"Magyar\",\n    value: \"hu-HU\"\n  },\n  {\n    label: \"Հայերեն\",\n    value: \"hy-AM\"\n  },\n  {\n    label: \"Bahasa Indonesia\",\n    value: \"id-ID\"\n  },\n  {\n    label: \"Igbo\",\n    value: \"ig-NG\"\n  },\n  {\n    label: \"Íslenska\",\n    value: \"is-IS\"\n  },\n  {\n    label: \"Italiano (Svizzera)\",\n    value: \"it-CH\"\n  },\n  {\n    label: \"Italiano (Italia)\",\n    value: \"it-IT\"\n  },\n  {\n    label: \"日本語\",\n    value: \"ja-JP\"\n  },\n  {\n    label: \"Basa Jawa\",\n    value: \"jv-ID\"\n  },\n  {\n    label: \"ქართული \",\n    value: \"ka-GE\"\n  },\n  {\n    label: \"Қазақ тілі\",\n    value: \"kk-KZ\"\n  },\n  {\n    label: \"ភាសាខ្មែរ\",\n    value: \"km-KH\"\n  },\n  {\n    label: \"ಕನ್ನಡ\",\n    value: \"kn-IN\"\n  },\n  {\n    label: \"한국어 \",\n    value: \"ko-KR\"\n  },\n  {\n    label: \"Кыргызча\",\n    value: \"ky-KG\"\n  },\n  {\n    label: \"ລາວ\",\n    value: \"lo-LA\"\n  },\n  {\n    label: \"Lietuvių\",\n    value: \"lt-LT\"\n  },\n  {\n    label: \"Latviešu)\",\n    value: \"lv-LV\"\n  },\n  {\n    label: \"मैथिली\",\n    value: \"mai-IN\"\n  },\n  {\n    label: \"Crnogorski\",\n    value: \"me-ME\"\n  },\n  {\n    label: \"Reo Māori\",\n    value: \"mi-NZ\"\n  },\n  {\n    label: \"Македонски\",\n    value: \"mk-MK\"\n  },\n  {\n    label: \"മലയാളം\",\n    value: \"ml-IN\"\n  },\n  {\n    label: \"Монгол\",\n    value: \"mn-MN\"\n  },\n  {\n    label: \"मराठी \",\n    value: \"mr-IN\"\n  },\n  {\n    label: \"Bahasa Melayu\",\n    value: \"ms-MY\"\n  },\n  {\n    label: \"မြန်မာဘာသာ\",\n    value: \"my-MM\"\n  },\n  {\n    label: \"Norsk bokmål\",\n    value: \"nb-NO\"\n  },\n  {\n    label: \"नेपाली भाषा\",\n    value: \"ne-NP\"\n  },\n  {\n    label: \"Nederlands\",\n    value: \"nl-NL\"\n  },\n  {\n    label: \"Norsk nynorsk\",\n    value: \"nn-NO\"\n  },\n  {\n    label: \"ଓଡ଼ିଆ\",\n    value: \"or-IN\"\n  },\n  {\n    label: \"ਗੁਰਮੁਖੀ\",\n    value: \"pa-Guru-IN\"\n  },\n  {\n    label: \"ਪੰਜਾਬੀ (ਭਾਰਤ)\",\n    value: \"pa-IN\"\n  },\n  {\n    label: \"پنجابی (پاکستان)\",\n    value: \"pa-PK\"\n  },\n  {\n    label: \"Polski\",\n    value: \"pl-PL\"\n  },\n  {\n    label: \"پښتو\",\n    value: \"ps-AF\"\n  },\n  {\n    label: \"Português (Angola)\",\n    value: \"pt-AO\"\n  },\n  {\n    label: \"Português (Brasil)\",\n    value: \"pt-BR\"\n  },\n  {\n    label: \"Português (Moçambique)\",\n    value: \"pt-MZ\"\n  },\n  {\n    label: \"Português (Portugal)\",\n    value: \"pt-PT\"\n  },\n  {\n    label: \"Română\",\n    value: \"ro-RO\"\n  },\n  {\n    label: \"Русский\",\n    value: \"ru-RU\"\n  },\n  {\n    label: \"سنڌي\",\n    value: \"sd-PK\"\n  },\n  {\n    label: \"සිංහල\",\n    value: \"si-LK\"\n  },\n  {\n    label: \"Slovenčina \",\n    value: \"sk-SK\"\n  },\n  {\n    label: \"Slovenščina\",\n    value: \"sl-SI\"\n  },\n  {\n    label: \"Shqip\",\n    value: \"sq-AL\"\n  },\n  {\n    label: \"Српски\",\n    value: \"sr-RS\"\n  },\n  {\n    label: \"Sesotho\",\n    value: \"st-ZA\"\n  },\n  {\n    label: \"Basa Sunda\",\n    value: \"su-ID\"\n  },\n  {\n    label: \"Svenska\",\n    value: \"sv-SE\"\n  },\n  {\n    label: \"Kiswahili (Kenya)\",\n    value: \"sw-KE\"\n  },\n  {\n    label: \"Kiswahili (Tanzania)\",\n    value: \"sw-TZ\"\n  },\n  {\n    label: \"தமிழ் (இந்தியா)\",\n    value: \"ta-IN\"\n  },\n  {\n    label: \"தமிழ் (இலங்கை)\",\n    value: \"ta-LK\"\n  },\n  {\n    label: \"தமிழ் (மலேசியா)\",\n    value: \"ta-MY\"\n  },\n  {\n    label: \"தமிழ் (சிங்கப்பூர்)\",\n    value: \"ta-SG\"\n  },\n  {\n    label: \"తెలుగు\",\n    value: \"te-IN\"\n  },\n  {\n    label: \"Тоҷикӣ\",\n    value: \"tg-TJ\"\n  },\n  {\n    label: \"ภาษาไทย\",\n    value: \"th-TH\"\n  },\n  {\n    label: \"Türkmen\",\n    value: \"tk-TM\"\n  },\n  {\n    label: \"Setswana\",\n    value: \"tn-ZA\"\n  },\n  {\n    label: \"Türkçe\",\n    value: \"tr-TR\"\n  },\n  {\n    label: \"Українська\",\n    value: \"uk-UA\"\n  },\n  {\n    label: \"اُردُو (بھارت)\",\n    value: \"ur-IN\"\n  },\n  {\n    label: \"اُردُو (پاکستان)\",\n    value: \"ur-PK\"\n  },\n  {\n    label: \"O'zbek\",\n    value: \"uz-UZ\"\n  },\n  {\n    label: \"Tiếng Việt\",\n    value: \"vi-VN\"\n  },\n  {\n    label: \"isiXhosa\",\n    value: \"xh-ZA\"\n  },\n  {\n    label: \"Yorùbá\",\n    value: \"yo-NG\"\n  },\n  {\n    label: \"粵語 (香港)\",\n    value: \"yue-Hant-HK\"\n  },\n  {\n    label: \"IsiZulu\",\n    value: \"zu-ZA\"\n  }\n]\n"
  },
  {
    "path": "src/utils/system-message.ts",
    "content": "import { SystemMessage } from \"@langchain/core/messages\"\nimport { getSelectedModelName } from \"./model\" \n\nexport const systemPromptFormatter = async ({ content }: { content: string }) => {\n  const currentDate = new Date()\n  const model = await getSelectedModelName()\n  const replacements = {\n    \"{current_date_time}\": currentDate.toLocaleString(),\n    \"{current_year}\": currentDate.getFullYear().toString(),\n    \"{current_month}\": currentDate.getMonth().toString(),\n    \"{current_day}\": currentDate.getDate().toString(),\n    \"{current_hour}\": currentDate.getHours().toString(),\n    \"{current_minute}\": currentDate.getMinutes().toString(),\n    \"{model}\": model,\n    \"{model_name}\": model,\n  }\n\n  for (const [key, value] of Object.entries(replacements)) {\n    content = content.replaceAll(key, value)\n  }\n\n  return new SystemMessage({\n    content: content\n  })\n}\n"
  },
  {
    "path": "src/utils/text-splitter.ts",
    "content": "import {\n  RecursiveCharacterTextSplitter,\n  CharacterTextSplitter\n} from \"@langchain/classic/text_splitter\"\n\nimport {\n  defaultEmbeddingChunkOverlap,\n  defaultEmbeddingChunkSize,\n  defaultSsplttingSeparator,\n  defaultSplittingStrategy\n} from \"@/services/ollama\"\n\nexport const getPageAssistTextSplitter = async () => {\n  const chunkSize = await defaultEmbeddingChunkSize()\n  const chunkOverlap = await defaultEmbeddingChunkOverlap()\n  const splittingStrategy = await defaultSplittingStrategy()\n\n  switch (splittingStrategy) {\n    case \"CharacterTextSplitter\":\n      const splittingSeparator = await defaultSsplttingSeparator()\n      const processedSeparator = splittingSeparator\n        .replace(/\\\\n/g, \"\\n\")\n        .replace(/\\\\t/g, \"\\t\")\n        .replace(/\\\\r/g, \"\\r\")\n      return new CharacterTextSplitter({\n        chunkSize,\n        chunkOverlap,\n        separator: processedSeparator\n      })\n    default:\n      return new RecursiveCharacterTextSplitter({\n        chunkSize,\n        chunkOverlap\n      })\n  }\n}\n"
  },
  {
    "path": "src/utils/to-source.ts",
    "content": "import { Source } from \"@/db/knowledge\"\nimport { processSource } from \"@/libs/process-source\"\nimport { UploadFile } from \"antd\"\n\nexport const toBase64 = (file: File | Blob): Promise<string> => {\n  return new Promise((resolve, reject) => {\n    if (!file) {\n      reject(new Error(\"File does not exist\"))\n      return\n    }\n\n    const reader = new FileReader()\n    reader.readAsDataURL(file)\n    reader.onload = () => resolve(reader.result as string)\n    reader.onerror = (error) => {\n      console.error(\"Failed to convert file to Base64:\", error)\n      reject(error)\n    }\n  })\n}\n\nexport const toArrayBufferFromBase64 = async (base64: string) => {\n  const res = await fetch(base64)\n  const blob = await res.blob()\n  return await blob.arrayBuffer()\n}\n\nexport const generateSourceId = () => {\n  return \"XXXXXXXX-XXXX-4XXX-YXXX-XXXXXXXXXXXX\".replace(/[XY]/g, (c) => {\n    const r = (Math.random() * 16) | 0\n    const v = c === \"X\" ? r : (r & 0x3) | 0x8\n    return v.toString(16)\n  })\n}\n\nexport const convertToSource = async ({\n  file,\n  mime,\n  sourceType\n}: {\n  file: UploadFile, mime?: string, sourceType?: string\n}): Promise<Source> => {\n  let type = mime || file.type\n  let filename = file.name\n  const content = await toBase64(file.originFileObj)\n  return { content, type, filename, source_id: generateSourceId(), sourceType }\n}\n\n\nexport const convertFileToSource = async ({\n  file,\n  mime,\n  sourceType\n}: {\n  file: File, mime?: string, sourceType?: string\n}): Promise<Source> => {\n  const allowedTypes = [\n    \"application/pdf\",\n    \"text/csv\",\n    \"text/plain\",\n    \"text/markdown\",\n    \"application/vnd.openxmlformats-officedocument.wordprocessingml.document\"\n  ]\n  let type = mime || file.type\n  if (!allowedTypes.includes(type)) {\n    type = \"text/plain\"\n  }\n  let filename = file.name\n  const url = await toBase64(file)\n  const content = await processSource({\n    filename,\n    url,\n    type\n  })\n  return { content, type, filename, source_id: generateSourceId(), sourceType }\n}\n\n// Helper to convert raw text into a synthetic text file and then into a Source\nexport const convertTextToSource = async ({\n  text,\n  filename = \"pasted.txt\",\n  mime = \"text/plain\",\n  asMarkdown = false,\n  sourceType = \"text_input\"\n}: {\n  text: string,\n  filename?: string,\n  mime?: string,\n  asMarkdown?: boolean,\n  sourceType?: string\n}): Promise<Source> => {\n  const finalMime = asMarkdown ? \"text/markdown\" : mime\n  const blob = new Blob([text], { type: finalMime })\n  const file = new File([blob], filename, { type: finalMime })\n  const content = await toBase64(file)\n  return { content, type: finalMime, filename, source_id: generateSourceId(), sourceType }\n}\n"
  },
  {
    "path": "src/utils/tts.ts",
    "content": "// inspired from https://github.com/open-webui/open-webui/blob/2299f4843003759290cc6bf823595c6578ee4470/src/lib/utils/index.ts\n\nconst CODE_BLOCK_PATTERN = /```[\\s\\S]*?```/g;\n\nexport const sanitizeEmojis = (text: string): string => {\n    const EMOJI_PATTERN = /[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]|\\uD83C[\\uDC00-\\uDFFF]|\\uD83D[\\uDC00-\\uDE4F]/g;\n    return text.replace(EMOJI_PATTERN, '');\n};\n\nexport const sanitizeMarkdown = (text: string): string => {\n    return text\n        .replace(/(```[\\s\\S]*?```)/g, '')\n        .replace(/^\\|.*\\|$/gm, '')\n        .replace(/(?:\\*\\*|__)(.*?)(?:\\*\\*|__)/g, '$1')\n        .replace(/(?:[*_])(.*?)(?:[*_])/g, '$1')\n        .replace(/~~(.*?)~~/g, '$1')\n        .replace(/`([^`]+)`/g, '$1')\n        .replace(/!?\\[([^\\]]*)\\](?:\\([^)]+\\)|\\[[^\\]]*\\])/g, '$1')\n        .replace(/^\\[[^\\]]+\\]:\\s*.*$/gm, '')\n        .replace(/^#{1,6}\\s+/gm, '')\n        .replace(/^\\s*[-*+]\\s+/gm, '')\n        .replace(/^\\s*(?:\\d+\\.)\\s+/gm, '')\n        .replace(/^\\s*>[> ]*/gm, '')\n        .replace(/^\\s*:\\s+/gm, '')\n        .replace(/\\[\\^[^\\]]*\\]/g, '')\n        .replace(/[-*_~]/g, '')\n        .replace(/\\n{2,}/g, '\\n');\n};\n\nexport const sanitizeText = (content: string): string => {\n    return sanitizeMarkdown(sanitizeEmojis(content.trim()));\n};\n\nexport const parseTextIntoSentences = (text: string): string[] => {\n    const codeBlocks: string[] = [];\n    let blockIndex = 0;\n\n    const processedText = text.replace(CODE_BLOCK_PATTERN, (match) => {\n        const placeholder = `\\u0000${blockIndex}\\u0000`;\n        codeBlocks[blockIndex++] = match;\n        return placeholder;\n    });\n\n    const sentences = processedText.split(/(?<=[.!?])\\s+/);\n\n    return sentences\n        .map(sentence =>\n            sentence.replace(/\\u0000(\\d+)\\u0000/g, (_, idx) => codeBlocks[idx])\n        )\n        .map(sanitizeText)\n        .filter(Boolean);\n};\n\nexport const parseTextIntoParagraphs = (text: string): string[] => {\n    const codeBlocks: string[] = [];\n    let blockIndex = 0;\n\n    const processedText = text.replace(CODE_BLOCK_PATTERN, (match) => {\n        const placeholder = `\\u0000${blockIndex}\\u0000`;\n        codeBlocks[blockIndex++] = match;\n        return placeholder;\n    });\n\n    return processedText\n        .split(/\\n+/)\n        .map(paragraph =>\n            paragraph.replace(/\\u0000(\\d+)\\u0000/g, (_, idx) => codeBlocks[idx])\n        )\n        .map(sanitizeText)\n        .filter(Boolean);\n};\n\nexport const optimizeSentencesForSpeech = (text: string): string[] => {\n    return parseTextIntoSentences(text).reduce((optimizedTexts, currentText) => {\n        const lastIndex = optimizedTexts.length - 1;\n\n        if (lastIndex >= 0) {\n            const previousText = optimizedTexts[lastIndex];\n            const wordCount = previousText.split(/\\s+/).length;\n            const charCount = previousText.length;\n\n            if (wordCount < 4 || charCount < 50) {\n                optimizedTexts[lastIndex] = `${previousText} ${currentText}`;\n            } else {\n                optimizedTexts.push(currentText);\n            }\n        } else {\n            optimizedTexts.push(currentText);\n        }\n\n        return optimizedTexts;\n    }, [] as string[]);\n};\n\nexport const splitMessageContent = (content: string, splitBy: string = 'punctuation') => {\n    const messageContentParts: string[] = [];\n\n    switch (splitBy) {\n        case 'punctuation':\n            messageContentParts.push(...optimizeSentencesForSpeech(content));\n            break;\n        case 'paragraph':\n            messageContentParts.push(...parseTextIntoParagraphs(content));\n            break;\n        case 'none':\n            messageContentParts.push(sanitizeText(content));\n            break;\n        default:\n    }\n\n    return messageContentParts;\n};"
  },
  {
    "path": "src/utils/update-page-title.ts",
    "content": "export const updatePageTitle = (title: string = 'Page Assist - A Web UI for Local AI Models') => {\n  const pageTitle = document.querySelector(\"title\")\n  if (pageTitle) {\n    pageTitle.textContent = title\n  } else {\n    console.warn(\"No title element found to update.\")\n  }\n}\n"
  },
  {
    "path": "src/utils/verify-page-share.ts",
    "content": "import { cleanUrl } from \"~/libs/clean-url\"\nimport fetcher from \"@/libs/fetcher\"\n\nexport const verifyPageShareURL = async (url: string) => {\n    const res = await fetcher(`${cleanUrl(url)}/api/v1/ping`)\n    if (!res.ok) {\n        throw new Error(\"Unable to verify page share\")\n    }\n    const data = await res.text()\n    return data === \"pong\"\n}"
  },
  {
    "path": "src/web/search-engines/baidu.ts",
    "content": "import { cleanUrl } from \"@/libs/clean-url\"\nimport { PageAssistHtmlLoader } from \"@/loader/html\"\nimport { pageAssistEmbeddingModel } from \"@/models/embedding\"\nimport {\n    defaultEmbeddingModelForRag,\n    getOllamaURL,\n    getSelectedModel\n} from \"@/services/ollama\"\nimport {\n    getIsSimpleInternetSearch,\n    totalSearchResults\n} from \"@/services/search\"\nimport { getPageAssistTextSplitter } from \"@/utils/text-splitter\"\nimport type { Document } from \"@langchain/core/documents\"\nimport { MemoryVectorStore } from \"@langchain/classic/vectorstores/memory\"\n\nexport const localBaiduSearch = async (query: string) => {\n    const TOTAL_SEARCH_RESULTS = await totalSearchResults()\n\n    const abortController = new AbortController()\n    setTimeout(() => abortController.abort(), 10000)\n\n    const jsonRes = await fetch(\n        \"https://www.baidu.com/s?wd=\" + encodeURIComponent(query) + \"&tn=json&rn=\" + TOTAL_SEARCH_RESULTS,\n        {\n            signal: abortController.signal\n        }\n    )\n        .then((response) => response.json())\n        .catch((e) => {\n            console.log(e)\n            return {\n                feed: {\n                    entry: []\n                }\n            }\n        })\n\n    const data = jsonRes?.feed?.entry || []\n\n    const searchResults = data.map((result: any) => {\n        const title = result?.title || \"\"\n        const link = result?.url\n        const content = result?.abs || \"\"\n        return { title, link, content }\n    })\n\n\n    return searchResults.filter((result) => result?.link)\n}\n\nexport const webBaiduSearch = async (query: string) => {\n    const searchResults = await localBaiduSearch(query)\n\n    const isSimpleMode = await getIsSimpleInternetSearch()\n\n    if (isSimpleMode) {\n        await getOllamaURL()\n        return searchResults.map((result) => {\n            return {\n                url: result.link,\n                content: result.content\n            }\n        })\n    }\n\n    const docs: Document<Record<string, any>>[] = []\n    for (const result of searchResults) {\n        const loader = new PageAssistHtmlLoader({\n            html: \"\",\n            url: result.link\n        })\n\n        const documents = await loader.loadByURL()\n\n        documents.forEach((doc) => {\n            docs.push(doc)\n        })\n    }\n    const ollamaUrl = await getOllamaURL()\n\n    const embeddingModle = await defaultEmbeddingModelForRag()\n    const selectedModel = await getSelectedModel()\n\n    const ollamaEmbedding = await pageAssistEmbeddingModel({\n        model: embeddingModle || selectedModel || \"\",\n        baseUrl: cleanUrl(ollamaUrl)\n    })\n\n    const textSplitter = await getPageAssistTextSplitter()\n\n    const chunks = await textSplitter.splitDocuments(docs)\n\n    const store = new MemoryVectorStore(ollamaEmbedding)\n\n    await store.addDocuments(chunks)\n\n    const resultsWithEmbeddings = await store.similaritySearch(query, 3)\n\n    const searchResult = resultsWithEmbeddings.map((result) => {\n        return {\n            url: result.metadata.url,\n            content: result.pageContent\n        }\n    })\n\n    return searchResult\n}\n"
  },
  {
    "path": "src/web/search-engines/bing.ts",
    "content": "import { cleanUrl } from \"@/libs/clean-url\"\nimport { urlRewriteRuntime } from \"@/libs/runtime\"\nimport { PageAssistHtmlLoader } from \"@/loader/html\"\nimport { pageAssistEmbeddingModel } from \"@/models/embedding\"\nimport {\n    defaultEmbeddingModelForRag,\n    getOllamaURL,\n    getSelectedModel\n} from \"@/services/ollama\"\nimport {\n    getIsSimpleInternetSearch,\n    totalSearchResults\n} from \"@/services/search\"\nimport { getPageAssistTextSplitter } from \"@/utils/text-splitter\"\nimport type { Document } from \"@langchain/core/documents\"\nimport * as cheerio from \"cheerio\"\nimport { MemoryVectorStore } from \"@langchain/classic/vectorstores/memory\"\n\nconst BING_SEARCH_URL = \"https://www.bing.com/search?q=\"\n\nexport const localBingSearch = async (query: string) => {\n    await urlRewriteRuntime(\n        cleanUrl(BING_SEARCH_URL + query),\n        \"bing\"\n    )\n\n    const abortController = new AbortController()\n    setTimeout(() => abortController.abort(), 10000)\n\n    const htmlString = await fetch(BING_SEARCH_URL + query, {\n        signal: abortController.signal\n    })\n        .then((response) => response.text())\n        .catch()\n\n    const $ = cheerio.load(htmlString)\n    const $results = $(\"#b_content #b_results\")\n    const $snippets = $results.find(\".b_algo\")\n    const searchResults = Array.from($snippets).map((result) => {\n        const link = $(result).find(\".tilk\").attr(\"href\")\n        const title = $(result).find(\"h2\").find(\"a\").text()\n        const content = $(result).find(\".b_caption\").find(\"p\").text()\n        return { title, link, content }\n    }).filter((result) => result.link && result.title && result.content)\n\n    const $newsSnippets = $results.find(\".b_nwsAns\")\n    if ($newsSnippets.length > 0) {\n        const newsResults = Array.from($newsSnippets).map((result) => {\n            const link = $(result).find(\"a.itm_link\").attr(\"href\")\n            const title = $(result).find(\".na_t_news_caption\").text()\n            const content = $(result).find(\".itm_spt_news_caption\").text()\n            const _source = 'Bing News'\n            return { title, link, content, _source }\n        }).filter((result) => result.link && result.title && result.content)\n        searchResults.push(...newsResults)\n    }\n\n    return searchResults\n}\n\nexport const webBingSearch = async (query: string) => {\n    const results = await localBingSearch(query)\n    const TOTAL_SEARCH_RESULTS = await totalSearchResults()\n    const searchResults = results.slice(0, TOTAL_SEARCH_RESULTS)\n\n    const isSimpleMode = await getIsSimpleInternetSearch()\n\n    if (isSimpleMode) {\n        await getOllamaURL()\n        return searchResults.map((result) => {\n            return {\n                url: result.link,\n                content: result.content\n            }\n        })\n    }\n\n    const docs: Document<Record<string, any>>[] = []\n    for (const result of searchResults) {\n        const loader = new PageAssistHtmlLoader({\n            html: \"\",\n            url: result.link\n        })\n\n        const documents = await loader.loadByURL()\n\n        documents.forEach((doc) => {\n            docs.push(doc)\n        })\n    }\n\n    const ollamaUrl = await getOllamaURL()\n\n    const embeddingModle = await defaultEmbeddingModelForRag()\n    const selectedModel = await getSelectedModel()\n\n    const ollamaEmbedding = await pageAssistEmbeddingModel({\n        model: embeddingModle || selectedModel || \"\",\n        baseUrl: cleanUrl(ollamaUrl)\n    })\n\n\n    const textSplitter = await getPageAssistTextSplitter();\n\n    const chunks = await textSplitter.splitDocuments(docs)\n\n    const store = new MemoryVectorStore(ollamaEmbedding)\n\n    await store.addDocuments(chunks)\n\n    const resultsWithEmbeddings = await store.similaritySearch(query, 3)\n\n    const searchResult = resultsWithEmbeddings.map((result) => {\n        return {\n            url: result.metadata.url,\n            content: result.pageContent\n        }\n    })\n\n    return searchResult\n\n}"
  },
  {
    "path": "src/web/search-engines/brave-api.ts",
    "content": "import { cleanUrl } from \"~/libs/clean-url\"\nimport { getIsSimpleInternetSearch, totalSearchResults, getBraveApiKey } from \"@/services/search\"\nimport { pageAssistEmbeddingModel } from \"@/models/embedding\"\nimport type { Document } from \"@langchain/core/documents\"\nimport { MemoryVectorStore } from \"@langchain/classic/vectorstores/memory\"\nimport { PageAssistHtmlLoader } from \"@/loader/html\"\nimport {\n    defaultEmbeddingModelForRag,\n    getOllamaURL,\n    getSelectedModel\n} from \"~/services/ollama\"\nimport { getPageAssistTextSplitter } from \"@/utils/text-splitter\"\n\ninterface BraveAPIResult {\n    title: string\n    url: string\n    description: string\n}\n\ninterface BraveAPIResponse {\n    web: {\n        results: BraveAPIResult[]\n    }\n}\n\nexport const braveAPISearch = async (query: string) => {\n    const braveApiKey = await getBraveApiKey()\n    if (!braveApiKey || braveApiKey.trim() === \"\") {\n        throw new Error(\"Brave API key not configured\")\n    }\n    const results = await apiBraveSearch(braveApiKey, query)\n    const TOTAL_SEARCH_RESULTS = await totalSearchResults()\n\n    const searchResults = results.slice(0, TOTAL_SEARCH_RESULTS)\n\n    const isSimpleMode = await getIsSimpleInternetSearch()\n\n    if (isSimpleMode) {\n        await getOllamaURL()\n        return searchResults.map((result) => {\n            return {\n                url: result.link,\n                content: result.content\n            }\n        })\n    }\n\n    const docs: Document<Record<string, any>>[] = []\n    try {\n        for (const result of searchResults) {\n            const loader = new PageAssistHtmlLoader({\n                html: \"\",\n                url: result.link\n            })\n\n            const documents = await loader.loadByURL()\n            documents.forEach((doc) => {\n                docs.push(doc)\n            })\n        }\n    } catch (error) {\n        console.error(error)\n    }\n\n    const ollamaUrl = await getOllamaURL()\n    const embeddingModel = await defaultEmbeddingModelForRag()\n    const selectedModel = await getSelectedModel()\n    const ollamaEmbedding = await pageAssistEmbeddingModel({\n        model: embeddingModel || selectedModel || \"\",\n        baseUrl: cleanUrl(ollamaUrl)\n    })\n\n    const textSplitter = await getPageAssistTextSplitter()\n\n    const chunks = await textSplitter.splitDocuments(docs)\n    const store = new MemoryVectorStore(ollamaEmbedding)\n    await store.addDocuments(chunks)\n\n    const resultsWithEmbeddings = await store.similaritySearch(query, 3)\n\n    const searchResult = resultsWithEmbeddings.map((result) => {\n        return {\n            url: result.metadata.url,\n            content: result.pageContent\n        }\n    })\n\n    return searchResult\n}\n\nconst apiBraveSearch = async (braveApiKey: string, query: string) => {\n    const TOTAL_SEARCH_RESULTS = await totalSearchResults()\n\n    const searchURL = `https://api.search.brave.com/res/v1/web/search?q=${query}&count=${TOTAL_SEARCH_RESULTS}`\n\n    const abortController = new AbortController()\n    setTimeout(() => abortController.abort(), 20000)\n\n    try {\n        const response = await fetch(searchURL, {\n            signal: abortController.signal,\n            headers: {\n                \"X-Subscription-Token\": braveApiKey,\n                Accept: \"application/json\",\n            }\n        })\n\n        if (!response.ok) {\n            return []\n        }\n\n        const data = await response.json() as BraveAPIResponse\n        \n        return data?.web?.results.map(result => ({\n            title: result.title,\n            link: result.url,\n            content: result.description\n        }))\n    } catch (error) {\n        console.error('Brave API search failed:', error)\n        return []\n    }\n}\n"
  },
  {
    "path": "src/web/search-engines/brave.ts",
    "content": "import { cleanUrl } from \"@/libs/clean-url\"\nimport { urlRewriteRuntime } from \"@/libs/runtime\"\nimport { PageAssistHtmlLoader } from \"@/loader/html\"\nimport { pageAssistEmbeddingModel } from \"@/models/embedding\"\nimport {\n    defaultEmbeddingModelForRag,\n    getOllamaURL,\n    getSelectedModel\n} from \"@/services/ollama\"\nimport {\n    getIsSimpleInternetSearch,\n    totalSearchResults\n} from \"@/services/search\"\nimport { getPageAssistTextSplitter } from \"@/utils/text-splitter\"\n\nimport type { Document } from \"@langchain/core/documents\"\nimport * as cheerio from \"cheerio\"\nimport { MemoryVectorStore } from \"@langchain/classic/vectorstores/memory\"\n\nexport const localBraveSearch = async (query: string) => {\n    await urlRewriteRuntime(cleanUrl(\"https://search.brave.com/search?q=\" + query), \"duckduckgo\")\n\n    const abortController = new AbortController()\n    setTimeout(() => abortController.abort(), 10000)\n\n    const htmlString = await fetch(\n        \"https://search.brave.com/search?q=\" + query,\n        {\n            signal: abortController.signal\n        }\n    )\n        .then((response) => response.text())\n        .catch()\n\n    const $ = cheerio.load(htmlString)\n    const $results = $(\"div#results\")\n    const $snippets = $results.find(\"div.snippet\")\n\n    const searchResults = Array.from($snippets).map((result) => {\n        const link = $(result).find(\"a\").attr(\"href\")\n        const title = $(result).find(\"div.title\").text()\n        const content = $(result).find(\"div.snippet-description\").text()\n        return { title, link, content }\n    }).filter((result) => result.link && result.title && result.content)\n\n\n    return searchResults\n}\n\nexport const webBraveSearch = async (query: string) => {\n    const results = await localBraveSearch(query)\n    const TOTAL_SEARCH_RESULTS = await totalSearchResults()\n    const searchResults = results.slice(0, TOTAL_SEARCH_RESULTS)\n\n    const isSimpleMode = await getIsSimpleInternetSearch()\n\n    if (isSimpleMode) {\n        await getOllamaURL()\n        return searchResults.map((result) => {\n            return {\n                url: result.link,\n                content: result.content\n            }\n        })\n    }\n\n    const docs: Document<Record<string, any>>[] = []\n    for (const result of searchResults) {\n        const loader = new PageAssistHtmlLoader({\n            html: \"\",\n            url: result.link\n        })\n\n        const documents = await loader.loadByURL()\n\n        documents.forEach((doc) => {\n            docs.push(doc)\n        })\n    }\n    const ollamaUrl = await getOllamaURL()\n    const selectedModel = await getSelectedModel()\n\n    const embeddingModle = await defaultEmbeddingModelForRag()\n    const ollamaEmbedding = await pageAssistEmbeddingModel({\n        model: embeddingModle || selectedModel || \"\",\n        baseUrl: cleanUrl(ollamaUrl)\n    })\n\n\n    const textSplitter = await getPageAssistTextSplitter();\n\n    const chunks = await textSplitter.splitDocuments(docs)\n\n    const store = new MemoryVectorStore(ollamaEmbedding)\n\n    await store.addDocuments(chunks)\n\n    const resultsWithEmbeddings = await store.similaritySearch(query, 3)\n\n    const searchResult = resultsWithEmbeddings.map((result) => {\n        return {\n            url: result.metadata.url,\n            content: result.pageContent\n        }\n    })\n\n    return searchResult\n}\n"
  },
  {
    "path": "src/web/search-engines/duckduckgo.ts",
    "content": "import { cleanUrl } from \"@/libs/clean-url\"\nimport { PageAssistHtmlLoader } from \"@/loader/html\"\nimport { pageAssistEmbeddingModel } from \"@/models/embedding\"\nimport {\n  defaultEmbeddingModelForRag,\n  getOllamaURL,\n  getSelectedModel\n} from \"@/services/ollama\"\nimport {\n  getIsSimpleInternetSearch,\n  totalSearchResults\n} from \"@/services/search\"\nimport { getPageAssistTextSplitter } from \"@/utils/text-splitter\"\nimport type { Document } from \"@langchain/core/documents\"\nimport * as cheerio from \"cheerio\"\nimport { MemoryVectorStore } from \"@langchain/classic/vectorstores/memory\"\n\nexport const localDuckDuckGoSearch = async (query: string) => {\n\n  const abortController = new AbortController()\n  setTimeout(() => abortController.abort(), 10000)\n\n  const htmlString = await fetch(\n    \"https://html.duckduckgo.com/html/?q=\" + query,\n    {\n      signal: abortController.signal\n    }\n  )\n    .then((response) => response.text())\n    .catch()\n\n  const $ = cheerio.load(htmlString)\n\n  const searchResults = Array.from($(\"div.results_links_deep\")).map(\n    (result) => {\n      const title = $(result).find(\"a.result__a\").text()\n      const link = $(result)\n        .find(\"a.result__snippet\")\n        .attr(\"href\")\n        .replace(\"//duckduckgo.com/l/?uddg=\", \"\")\n        .replace(/&rut=.*/, \"\")\n\n      const content = $(result).find(\"a.result__snippet\").text()\n      const decodedLink = decodeURIComponent(link)\n      return { title, link: decodedLink, content }\n    }\n  )\n\n  return searchResults\n}\n\nexport const webDuckDuckGoSearch = async (query: string) => {\n  const results = await localDuckDuckGoSearch(query)\n  const TOTAL_SEARCH_RESULTS = await totalSearchResults()\n  const searchResults = results.slice(0, TOTAL_SEARCH_RESULTS)\n\n  const isSimpleMode = await getIsSimpleInternetSearch()\n\n  if (isSimpleMode) {\n    await getOllamaURL()\n    return searchResults.map((result) => {\n      return {\n        url: result.link,\n        content: result.content\n      }\n    })\n  }\n\n  const docs: Document<Record<string, any>>[] = []\n  for (const result of searchResults) {\n    const loader = new PageAssistHtmlLoader({\n      html: \"\",\n      url: result.link\n    })\n\n    const documents = await loader.loadByURL()\n\n    documents.forEach((doc) => {\n      docs.push(doc)\n    })\n  }\n  const ollamaUrl = await getOllamaURL()\n\n\n  const embeddingModle = await defaultEmbeddingModelForRag()\n  const selectedModel = await getSelectedModel()\n  const ollamaEmbedding = await pageAssistEmbeddingModel({\n    model: embeddingModle || selectedModel || \"\",\n    baseUrl: cleanUrl(ollamaUrl)\n  })\n\n  const textSplitter = await getPageAssistTextSplitter()\n\n  const chunks = await textSplitter.splitDocuments(docs)\n\n  const store = new MemoryVectorStore(ollamaEmbedding)\n\n  await store.addDocuments(chunks)\n\n  const resultsWithEmbeddings = await store.similaritySearch(query, 3)\n\n  const searchResult = resultsWithEmbeddings.map((result) => {\n    return {\n      url: result.metadata.url,\n      content: result.pageContent\n    }\n  })\n\n  return searchResult\n}\n"
  },
  {
    "path": "src/web/search-engines/exa.ts",
    "content": "import { cleanUrl } from \"~/libs/clean-url\"\nimport {\n  getIsSimpleInternetSearch,\n  totalSearchResults,\n  getExaAPIKey\n} from \"@/services/search\"\nimport { pageAssistEmbeddingModel } from \"@/models/embedding\"\nimport type { Document } from \"@langchain/core/documents\"\nimport { MemoryVectorStore } from \"@langchain/classic/vectorstores/memory\"\nimport { PageAssistHtmlLoader } from \"@/loader/html\"\nimport {\n  defaultEmbeddingModelForRag,\n  getOllamaURL,\n  getSelectedModel\n} from \"~/services/ollama\"\nimport { getPageAssistTextSplitter } from \"@/utils/text-splitter\"\n\ninterface ExaAPIResult {\n  title: string\n  url: string\n  text: string\n}\n\ninterface ExaAPIResponse {\n    results: ExaAPIResult[]\n}\n\nexport const exaAPISearch = async (query: string) => {\n  const exaAPIKey = await getExaAPIKey()\n  if (!exaAPIKey || exaAPIKey.trim() === \"\") {\n    throw new Error(\"Exa API key not configured\")\n  }\n  const results = await apiExaSearch(exaAPIKey, query)\n  const TOTAL_SEARCH_RESULTS = await totalSearchResults()\n\n  const searchResults = results.slice(0, TOTAL_SEARCH_RESULTS)\n\n  const isSimpleMode = await getIsSimpleInternetSearch()\n\n  if (isSimpleMode) {\n    await getOllamaURL()\n    return searchResults.map((result) => {\n      return {\n        url: result.link,\n        content: result?.content || result?.title\n      }\n    })\n  }\n\n  const docs: Document<Record<string, any>>[] = []\n  try {\n    for (const result of searchResults) {\n      const loader = new PageAssistHtmlLoader({\n        html: \"\",\n        url: result.link\n      })\n\n      const documents = await loader.loadByURL()\n      documents.forEach((doc) => {\n        docs.push(doc)\n      })\n    }\n  } catch (error) {\n    console.error(error)\n  }\n\n  const ollamaUrl = await getOllamaURL()\n  const embeddingModel = await defaultEmbeddingModelForRag()\n  const selectedModel = await getSelectedModel()\n  const ollamaEmbedding = await pageAssistEmbeddingModel({\n    model: embeddingModel || selectedModel || \"\",\n    baseUrl: cleanUrl(ollamaUrl)\n  })\n\n  const textSplitter = await getPageAssistTextSplitter()\n\n  const chunks = await textSplitter.splitDocuments(docs)\n  const store = new MemoryVectorStore(ollamaEmbedding)\n  await store.addDocuments(chunks)\n\n  const resultsWithEmbeddings = await store.similaritySearch(query, 3)\n\n  const searchResult = resultsWithEmbeddings.map((result) => {\n    return {\n      url: result.metadata.url,\n      content: result.pageContent\n    }\n  })\n\n  return searchResult\n}\n\nconst apiExaSearch = async (exaApiKey: string, query: string) => {\n  const TOTAL_SEARCH_RESULTS = await totalSearchResults()\n\n  const searchURL = \"https://api.exa.ai/search\"\n\n  const abortController = new AbortController()\n  setTimeout(() => abortController.abort(), 20000)\n\n  try {\n    const response = await fetch(searchURL, {\n      signal: abortController.signal,\n      method: \"POST\",\n      body: JSON.stringify({\n        query,\n        numResults: TOTAL_SEARCH_RESULTS,\n        text: true\n      }),\n      headers: {\n        Authorization: `Bearer ${exaApiKey}`,\n        \"Content-Type\": \"application/json\"\n      }\n    })\n\n    if (!response.ok) {\n      return []\n    }\n\n    const data = (await response.json()) as ExaAPIResponse\n\n    return data?.results.map((result) => ({\n      title: result.title,\n      link: result.url,\n      content: result.text\n    }))\n  } catch (error) {\n    console.error(\"EXA API search failed:\", error)\n    return []\n  }\n}\n"
  },
  {
    "path": "src/web/search-engines/firecrawl.ts",
    "content": "import { cleanUrl } from \"~/libs/clean-url\"\nimport {\n    getIsSimpleInternetSearch,\n    totalSearchResults,\n    getBraveApiKey,\n    getFirecrawlAPIKey\n} from \"@/services/search\"\nimport { pageAssistEmbeddingModel } from \"@/models/embedding\"\nimport type { Document } from \"@langchain/core/documents\"\nimport { MemoryVectorStore } from \"@langchain/classic/vectorstores/memory\"\nimport { PageAssistHtmlLoader } from \"@/loader/html\"\nimport {\n    defaultEmbeddingModelForRag,\n    getOllamaURL,\n    getSelectedModel\n} from \"~/services/ollama\"\nimport { getPageAssistTextSplitter } from \"@/utils/text-splitter\"\n\ninterface FirecrawlAPIResult {\n    title: string\n    url: string\n    description: string\n    markdown: string\n}\n\ninterface FirecrawlAPIResponse {\n    data: FirecrawlAPIResult[]\n}\n\nexport const firecrawlAPISearch = async (query: string) => {\n    const firecrawlAPIKey = await getFirecrawlAPIKey()\n    if (!firecrawlAPIKey || firecrawlAPIKey.trim() === \"\") {\n        throw new Error(\"Brave API key not configured\")\n    }\n    const results = await apiFirecrawlSearch(firecrawlAPIKey, query)\n    const TOTAL_SEARCH_RESULTS = await totalSearchResults()\n\n    const searchResults = results.slice(0, TOTAL_SEARCH_RESULTS)\n\n    const isSimpleMode = await getIsSimpleInternetSearch()\n\n    if (isSimpleMode) {\n        await getOllamaURL()\n        return searchResults.map((result) => {\n            return {\n                url: result.link,\n                content: result.content\n            }\n        })\n    }\n\n    const docs: Document<Record<string, any>>[] = []\n    try {\n        for (const result of searchResults) {\n            const loader = new PageAssistHtmlLoader({\n                html: \"\",\n                url: result.link\n            })\n\n            const documents = await loader.loadByURL()\n            documents.forEach((doc) => {\n                docs.push(doc)\n            })\n        }\n    } catch (error) {\n        console.error(error)\n    }\n\n    const ollamaUrl = await getOllamaURL()\n    const embeddingModel = await defaultEmbeddingModelForRag()\n    const selectedModel = await getSelectedModel()\n    const ollamaEmbedding = await pageAssistEmbeddingModel({\n        model: embeddingModel || selectedModel || \"\",\n        baseUrl: cleanUrl(ollamaUrl)\n    })\n\n    const textSplitter = await getPageAssistTextSplitter()\n\n    const chunks = await textSplitter.splitDocuments(docs)\n    const store = new MemoryVectorStore(ollamaEmbedding)\n    await store.addDocuments(chunks)\n\n    const resultsWithEmbeddings = await store.similaritySearch(query, 3)\n\n    const searchResult = resultsWithEmbeddings.map((result) => {\n        return {\n            url: result.metadata.url,\n            content: result.pageContent\n        }\n    })\n\n    return searchResult\n}\n\nconst apiFirecrawlSearch = async (firecrawlAPIKey: string, query: string) => {\n    const TOTAL_SEARCH_RESULTS = await totalSearchResults()\n\n    const searchURL = \"https://api.firecrawl.dev/v1/search\"\n\n    const abortController = new AbortController()\n    setTimeout(() => abortController.abort(), 20000)\n\n    try {\n        const response = await fetch(searchURL, {\n            signal: abortController.signal,\n            method: \"POST\",\n            headers: {\n                Authorization: `Bearer ${firecrawlAPIKey}`,\n                Accept: \"application/json\",\n                'Content-Type': 'application/json'\n            },\n            body: JSON.stringify({\n                limit: TOTAL_SEARCH_RESULTS,\n                lang: \"\",\n                country: \"\",\n                timeout: 60000,\n                scrapeOptions: { formats: [] },\n                query: query\n            })\n        })\n\n        if (!response.ok) {\n            return []\n        }\n\n        const data = (await response.json()) as FirecrawlAPIResponse\n\n        return data?.data.map((result) => ({\n            title: result.title,\n            link: result.url,\n            content: result.description\n        }))\n    } catch (error) {\n        console.error(\"Firecrarwl API search failed:\", error)\n        return []\n    }\n}\n"
  },
  {
    "path": "src/web/search-engines/google.ts",
    "content": "import { pageAssistEmbeddingModel } from \"@/models/embedding\"\nimport {\n  getGoogleDomain,\n  getIsSimpleInternetSearch,\n  totalSearchResults\n} from \"@/services/search\"\nimport { getPageAssistTextSplitter } from \"@/utils/text-splitter\"\nimport type { Document } from \"@langchain/core/documents\"\nimport { MemoryVectorStore } from \"@langchain/classic/vectorstores/memory\"\nimport { cleanUrl } from \"~/libs/clean-url\"\nimport { PageAssistHtmlLoader } from \"@/loader/html\"\nimport {\n  defaultEmbeddingModelForRag,\n  getOllamaURL,\n  getSelectedModel\n} from \"~/services/ollama\"\n\n\nexport const localGoogleSearch = async (query: string, start: number = 0) => {\n  const baseGoogleDomain = await getGoogleDomain()\n  const abortController = new AbortController()\n  setTimeout(() => abortController.abort(), 10000)\n\n  const htmlString = await fetch(\n    `https://www.${baseGoogleDomain}/search?hl=en&q=${encodeURIComponent(query)}&start=${start}`,\n    {\n      signal: abortController.signal,\n      headers: {\n        \"User-Agent\": navigator.userAgent,\n        \"Accept\": \"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\",\n        \"Accept-Language\": \"en-US,en;q=0.5\",\n        \"Accept-Encoding\": \"gzip, deflate, br\",\n        \"DNT\": \"1\",\n        \"Connection\": \"keep-alive\",\n        \"Upgrade-Insecure-Requests\": \"1\",\n        \"Sec-Fetch-Dest\": \"document\",\n        \"Sec-Fetch-Mode\": \"navigate\",\n        \"Sec-Fetch-Site\": \"none\",\n        \"Sec-Fetch-User\": \"?1\"\n      }\n    }\n  ).then((response) => response.text())\n    .catch()\n  const parser = new DOMParser()\n\n  const doc = parser.parseFromString(htmlString, \"text/html\")\n\n  const searchResults = Array.from(doc.querySelectorAll(\"div.g\")).map(\n    (result) => {\n      const title = result.querySelector(\"h3\")?.textContent\n      const link = result.querySelector(\"a\")?.getAttribute(\"href\")\n      const content = Array.from(result.querySelectorAll(\"span\"))\n        .map((span) => span.textContent)\n        .join(\" \")\n      return { title, link, content }\n    }\n  )\n  return searchResults\n}\n\nexport const webGoogleSearch = async (query: string) => {\n  const TOTAL_SEARCH_RESULTS = await totalSearchResults();\n  let results = [];\n  let currentPage = 0;\n  const seenLinks = new Set();\n\n  while (results.length < TOTAL_SEARCH_RESULTS) {\n    const start = currentPage * 10;\n    const pageResults = await localGoogleSearch(query, start);\n\n    // Filter duplicates within current page\n    const uniquePageResults = pageResults.filter(result => {\n      if (!result.link || seenLinks.has(result.link)) return false;\n      seenLinks.add(result.link);\n      return true;\n    });\n\n    results = [...results, ...uniquePageResults];\n\n    // Add random delay between requests (1-3 seconds)\n    await new Promise(resolve =>\n      setTimeout(resolve, 1000 + Math.random() * 2000));\n\n    if (pageResults.length === 0) break;\n    currentPage++;\n\n    // Safety limit to prevent infinite loops\n    if (currentPage > 100) break;\n  }\n\n  // Final deduplication and slicing\n  const uniqueResults = results.filter((result, index, self) =>\n    index === self.findIndex(r => r.link === result.link)\n  );\n  const searchResults = uniqueResults.slice(0, TOTAL_SEARCH_RESULTS);\n\n  const isSimpleMode = await getIsSimpleInternetSearch()\n\n  if (isSimpleMode) {\n    await getOllamaURL()\n    return searchResults.map((result) => {\n      return {\n        url: result.link,\n        content: result.content\n      }\n    })\n  }\n\n  const docs: Document<Record<string, any>>[] = []\n  for (const result of searchResults) {\n    const loader = new PageAssistHtmlLoader({\n      html: \"\",\n      url: result.link\n    })\n\n    const documents = await loader.loadByURL()\n\n    documents.forEach((doc) => {\n      docs.push(doc)\n    })\n  }\n  const ollamaUrl = await getOllamaURL()\n  const selectedModel = await getSelectedModel()\n  const embeddingModle = await defaultEmbeddingModelForRag()\n  const ollamaEmbedding = await pageAssistEmbeddingModel({\n    model: embeddingModle || selectedModel || \"\",\n    baseUrl: cleanUrl(ollamaUrl)\n  })\n\n\n  const textSplitter = await getPageAssistTextSplitter()\n\n  const chunks = await textSplitter.splitDocuments(docs)\n\n  const store = new MemoryVectorStore(ollamaEmbedding)\n\n  await store.addDocuments(chunks)\n\n  const resultsWithEmbeddings = await store.similaritySearch(query, 3)\n\n  const searchResult = resultsWithEmbeddings.map((result) => {\n    return {\n      url: result.metadata.url,\n      content: result.pageContent\n    }\n  })\n\n  return searchResult\n}\n"
  },
  {
    "path": "src/web/search-engines/kagi-api.ts",
    "content": "import { cleanUrl } from \"~/libs/clean-url\"\nimport { getIsSimpleInternetSearch, totalSearchResults, getKagiApiKey } from \"@/services/search\"\nimport { pageAssistEmbeddingModel } from \"@/models/embedding\"\nimport type { Document } from \"@langchain/core/documents\"\nimport { MemoryVectorStore } from \"@langchain/classic/vectorstores/memory\"\nimport { PageAssistHtmlLoader } from \"@/loader/html\"\nimport {\n    defaultEmbeddingModelForRag,\n    getOllamaURL,\n    getSelectedModel\n} from \"~/services/ollama\"\nimport { getPageAssistTextSplitter } from \"@/utils/text-splitter\"\n\ninterface KagiSearchResult {\n    t: number\n    url?: string\n    title?: string\n    snippet?: string\n    published?: number\n    thumbnail?: {\n        url: string\n        height?: number\n        width?: number\n    }\n    list?: string[]\n}\n\ninterface KagiAPIResponse {\n    meta: {\n        id: string\n        node: string\n        ms: number\n    }\n    data: KagiSearchResult[]\n    error?: Array<{\n        code: number\n        msg: string\n        ref?: string\n    }>\n}\n\nexport const kagiAPISearch = async (query: string) => {\n    const kagiApiKey = await getKagiApiKey()\n    if (!kagiApiKey || kagiApiKey.trim() === \"\") {\n        throw new Error(\"Kagi API key not configured\")\n    }\n    const results = await apiKagiSearch(kagiApiKey, query)\n    const TOTAL_SEARCH_RESULTS = await totalSearchResults()\n\n    const searchResults = results.slice(0, TOTAL_SEARCH_RESULTS)\n\n    const isSimpleMode = await getIsSimpleInternetSearch()\n\n    if (isSimpleMode) {\n        await getOllamaURL()\n        return searchResults.map((result) => {\n            return {\n                url: result.link,\n                content: result.content\n            }\n        })\n    }\n\n    const docs: Document<Record<string, any>>[] = []\n    try {\n        for (const result of searchResults) {\n            const loader = new PageAssistHtmlLoader({\n                html: \"\",\n                url: result.link\n            })\n\n            const documents = await loader.loadByURL()\n            documents.forEach((doc) => {\n                docs.push(doc)\n            })\n        }\n    } catch (error) {\n        console.error(error)\n    }\n\n    const ollamaUrl = await getOllamaURL()\n    const embeddingModel = await defaultEmbeddingModelForRag()\n    const selectedModel = await getSelectedModel()\n    const ollamaEmbedding = await pageAssistEmbeddingModel({\n        model: embeddingModel || selectedModel || \"\",\n        baseUrl: cleanUrl(ollamaUrl)\n    })\n\n    const textSplitter = await getPageAssistTextSplitter()\n\n    const chunks = await textSplitter.splitDocuments(docs)\n    const store = new MemoryVectorStore(ollamaEmbedding)\n    await store.addDocuments(chunks)\n\n    const resultsWithEmbeddings = await store.similaritySearch(query, 3)\n\n    const searchResult = resultsWithEmbeddings.map((result) => {\n        return {\n            url: result.metadata.url,\n            content: result.pageContent\n        }\n    })\n\n    return searchResult\n}\n\nconst apiKagiSearch = async (kagiApiKey: string, query: string) => {\n    const TOTAL_SEARCH_RESULTS = await totalSearchResults()\n\n    const searchURL = `https://kagi.com/api/v0/search?q=${encodeURIComponent(query)}&limit=${TOTAL_SEARCH_RESULTS}`\n\n    const abortController = new AbortController()\n    setTimeout(() => abortController.abort(), 20000)\n\n    try {\n        const response = await fetch(searchURL, {\n            signal: abortController.signal,\n            headers: {\n                \"Authorization\": `Bot ${kagiApiKey}`,\n                \"Accept\": \"application/json\",\n            }\n        })\n\n        if (!response.ok) {\n            return []\n        }\n\n        const data = await response.json() as KagiAPIResponse\n\n        // Filter only Search Result objects (t === 0) and map them\n        return data?.data\n            ?.filter(result => result.t === 0 && result.url && result.title)\n            .map(result => ({\n                title: result.title!,\n                link: result.url!,\n                content: result.snippet || \"\"\n            })) || []\n    } catch (error) {\n        console.error('Kagi API search failed:', error)\n        return []\n    }\n}\n"
  },
  {
    "path": "src/web/search-engines/ollama.ts",
    "content": "import { cleanUrl } from \"~/libs/clean-url\"\nimport {\n  getIsSimpleInternetSearch,\n  totalSearchResults,\n  getBraveApiKey,\n  getOllamaSearchApiKey\n} from \"@/services/search\"\nimport { pageAssistEmbeddingModel } from \"@/models/embedding\"\nimport type { Document } from \"@langchain/core/documents\"\nimport { MemoryVectorStore } from \"@langchain/classic/vectorstores/memory\"\nimport { PageAssistHtmlLoader } from \"@/loader/html\"\nimport {\n  defaultEmbeddingModelForRag,\n  getOllamaURL,\n  getSelectedModel\n} from \"~/services/ollama\"\nimport { getPageAssistTextSplitter } from \"@/utils/text-splitter\"\n\ninterface OllamaAPIResult {\n  title: string\n  url: string\n  content: string\n}\n\ninterface OllamaAPIResponse {\n  results: OllamaAPIResult[]\n}\n\nexport const ollamaAPISearch = async (query: string) => {\n  const ollamaApiKey = await getOllamaSearchApiKey()\n  if (!ollamaApiKey || ollamaApiKey.trim() === \"\") {\n    throw new Error(\"Ollama API key not configured\")\n  }\n  const results = await apiOllamaSearch(ollamaApiKey, query)\n  const TOTAL_SEARCH_RESULTS = await totalSearchResults()\n\n  const searchResults = results.slice(0, TOTAL_SEARCH_RESULTS)\n\n  const isSimpleMode = await getIsSimpleInternetSearch()\n\n  if (isSimpleMode) {\n    await getOllamaURL()\n    return searchResults.map((result) => {\n      return {\n        url: result.link,\n        content: result.content\n      }\n    })\n  }\n\n  const docs: Document<Record<string, any>>[] = []\n  try {\n    for (const result of searchResults) {\n      const loader = new PageAssistHtmlLoader({\n        html: \"\",\n        url: result.link\n      })\n\n      const documents = await loader.loadByURL()\n      documents.forEach((doc) => {\n        docs.push(doc)\n      })\n    }\n  } catch (error) {\n    console.error(error)\n  }\n\n  const ollamaUrl = await getOllamaURL()\n  const embeddingModel = await defaultEmbeddingModelForRag()\n  const selectedModel = await getSelectedModel()\n  const ollamaEmbedding = await pageAssistEmbeddingModel({\n    model: embeddingModel || selectedModel || \"\",\n    baseUrl: cleanUrl(ollamaUrl)\n  })\n\n  const textSplitter = await getPageAssistTextSplitter()\n\n  const chunks = await textSplitter.splitDocuments(docs)\n  const store = new MemoryVectorStore(ollamaEmbedding)\n  await store.addDocuments(chunks)\n\n  const resultsWithEmbeddings = await store.similaritySearch(query, 3)\n\n  const searchResult = resultsWithEmbeddings.map((result) => {\n    return {\n      url: result.metadata.url,\n      content: result.pageContent\n    }\n  })\n\n  return searchResult\n}\n\nconst apiOllamaSearch = async (ollamaApiKey: string, query: string) => {\n  const TOTAL_SEARCH_RESULTS = await totalSearchResults()\n\n  const searchURL = \"https://ollama.com/api/web_search\"\n\n  const abortController = new AbortController()\n  setTimeout(() => abortController.abort(), 20000)\n\n  try {\n    const response = await fetch(searchURL, {\n      signal: abortController.signal,\n      method: \"POST\",\n      headers: {\n        Authorization: `Bearer ${ollamaApiKey}`,\n        Accept: \"application/json\"\n      },\n      body: JSON.stringify({\n        query,\n        max_results: TOTAL_SEARCH_RESULTS\n      })\n    })\n\n    if (!response.ok) {\n      return []\n    }\n\n    const data = (await response.json()) as OllamaAPIResponse\n\n    return data?.results.map((result) => ({\n      title: result.title,\n      link: result.url,\n      content: result.content\n    }))\n  } catch (error) {\n    console.error(\"Ollama API search failed:\", error)\n    return []\n  }\n}\n"
  },
  {
    "path": "src/web/search-engines/perplexity-api.ts",
    "content": "import { cleanUrl } from \"~/libs/clean-url\"\nimport {\n  getIsSimpleInternetSearch,\n  totalSearchResults,\n  getPerplexityApiKey\n} from \"@/services/search\"\nimport { pageAssistEmbeddingModel } from \"@/models/embedding\"\nimport type { Document } from \"@langchain/core/documents\"\nimport { MemoryVectorStore } from \"@langchain/classic/vectorstores/memory\"\nimport { PageAssistHtmlLoader } from \"@/loader/html\"\nimport {\n  defaultEmbeddingModelForRag,\n  getOllamaURL,\n  getSelectedModel\n} from \"~/services/ollama\"\nimport { getPageAssistTextSplitter } from \"@/utils/text-splitter\"\n\ninterface PerplexitySearchResult {\n  title: string\n  url: string\n  snippet: string\n  date?: string\n  last_updated?: string\n}\n\ninterface PerplexityAPIResponse {\n  results: PerplexitySearchResult[]\n}\n\nexport const perplexityAPISearch = async (query: string) => {\n  const perplexityApiKey = await getPerplexityApiKey()\n  if (!perplexityApiKey || perplexityApiKey.trim() === \"\") {\n    throw new Error(\"Perplexity API key not configured\")\n  }\n  const results = await apiPerplexitySearch(perplexityApiKey, query)\n  const TOTAL_SEARCH_RESULTS = await totalSearchResults()\n\n  const searchResults = results.slice(0, TOTAL_SEARCH_RESULTS)\n\n  const isSimpleMode = await getIsSimpleInternetSearch()\n\n  if (isSimpleMode) {\n    await getOllamaURL()\n    return searchResults.map((result) => {\n      return {\n        url: result.link,\n        content: result?.content || result?.title\n      }\n    })\n  }\n\n  const docs: Document<Record<string, any>>[] = []\n  try {\n    for (const result of searchResults) {\n      const loader = new PageAssistHtmlLoader({\n        html: \"\",\n        url: result.link\n      })\n\n      const documents = await loader.loadByURL()\n      documents.forEach((doc) => {\n        docs.push(doc)\n      })\n    }\n  } catch (error) {\n    console.error(error)\n  }\n\n  const ollamaUrl = await getOllamaURL()\n  const embeddingModel = await defaultEmbeddingModelForRag()\n  const selectedModel = await getSelectedModel()\n  const ollamaEmbedding = await pageAssistEmbeddingModel({\n    model: embeddingModel || selectedModel || \"\",\n    baseUrl: cleanUrl(ollamaUrl)\n  })\n\n  const textSplitter = await getPageAssistTextSplitter()\n\n  const chunks = await textSplitter.splitDocuments(docs)\n  const store = new MemoryVectorStore(ollamaEmbedding)\n  await store.addDocuments(chunks)\n\n  const resultsWithEmbeddings = await store.similaritySearch(query, 3)\n\n  const searchResult = resultsWithEmbeddings.map((result) => {\n    return {\n      url: result.metadata.url,\n      content: result.pageContent\n    }\n  })\n\n  return searchResult\n}\n\nconst apiPerplexitySearch = async (perplexityApiKey: string, query: string) => {\n  const TOTAL_SEARCH_RESULTS = await totalSearchResults()\n\n  const searchURL = \"https://api.perplexity.ai/search\"\n\n  const abortController = new AbortController()\n  setTimeout(() => abortController.abort(), 20000)\n\n  try {\n    const response = await fetch(searchURL, {\n      signal: abortController.signal,\n      method: \"POST\",\n      body: JSON.stringify({\n        query,\n        max_results: TOTAL_SEARCH_RESULTS\n      }),\n      headers: {\n        Authorization: `Bearer ${perplexityApiKey}`,\n        \"Content-Type\": \"application/json\"\n      }\n    })\n\n    if (!response.ok) {\n      return []\n    }\n\n    const data = (await response.json()) as PerplexityAPIResponse\n\n    return data?.results.map((result) => ({\n      title: result.title,\n      link: result.url,\n      content: result.snippet\n    }))\n  } catch (error) {\n    console.error(\"Perplexity API search failed:\", error)\n    return []\n  }\n}\n"
  },
  {
    "path": "src/web/search-engines/searxng.ts",
    "content": "import { urlRewriteRuntime } from \"~/libs/runtime\"\nimport { cleanUrl } from \"~/libs/clean-url\"\nimport { getSearxngURL, isSearxngJSONMode, getIsSimpleInternetSearch, totalSearchResults } from \"@/services/search\"\nimport { pageAssistEmbeddingModel } from \"@/models/embedding\"\nimport type { Document } from \"@langchain/core/documents\"\nimport { MemoryVectorStore } from \"@langchain/classic/vectorstores/memory\"\nimport { PageAssistHtmlLoader } from \"@/loader/html\"\nimport {\n  defaultEmbeddingModelForRag,\n  getOllamaURL,\n  getSelectedModel\n} from \"~/services/ollama\"\nimport { getPageAssistTextSplitter } from \"@/utils/text-splitter\"\n\ninterface SearxNGJSONResult {\n  title: string\n  url: string\n  content: string\n}\n\ninterface SearxNGJSONResponse {\n  results: SearxNGJSONResult[]\n}\n\nexport const searxngSearch = async (query: string) => {\n  const searxngURL = await getSearxngURL()\n  if (!searxngURL) {\n    throw new Error(\"SearXNG URL not configured\")\n  }\n\n  const isJSONMode = await isSearxngJSONMode()\n  const results = isJSONMode\n    ? await searxngJSONSearch(searxngURL, query)\n    : await searxngWebSearch(searxngURL, query)\n\n  const TOTAL_SEARCH_RESULTS = await totalSearchResults()\n  const searchResults = results.slice(0, TOTAL_SEARCH_RESULTS)\n\n  const isSimpleMode = await getIsSimpleInternetSearch()\n\n  if (isSimpleMode) {\n    await getOllamaURL()\n    return searchResults.map((result) => {\n      return {\n        url: result.link,\n        content: result.content\n      }\n    })\n  }\n\n  const docs: Document<Record<string, any>>[] = []\n  try {\n    for (const result of searchResults) {\n      const loader = new PageAssistHtmlLoader({\n        html: \"\",\n        url: result.link\n      })\n\n      const documents = await loader.loadByURL()\n      documents.forEach((doc) => {\n        docs.push(doc)\n      })\n    }\n  } catch (error) {\n    console.error(error)\n  }\n\n  const ollamaUrl = await getOllamaURL()\n  const embeddingModel = await defaultEmbeddingModelForRag()\n  const selectedModel = await getSelectedModel()\n  const ollamaEmbedding = await pageAssistEmbeddingModel({\n    model: embeddingModel || selectedModel || \"\",\n    baseUrl: cleanUrl(ollamaUrl)\n  })\n\n\n  const textSplitter = await getPageAssistTextSplitter();\n\n  const chunks = await textSplitter.splitDocuments(docs)\n  const store = new MemoryVectorStore(ollamaEmbedding)\n  await store.addDocuments(chunks)\n\n  const resultsWithEmbeddings = await store.similaritySearch(query, 3)\n\n  const searchResult = resultsWithEmbeddings.map((result) => {\n    return {\n      url: result.metadata.url,\n      content: result.pageContent\n    }\n  })\n\n  return searchResult\n}\n\nconst searxngJSONSearch = async (baseURL: string, query: string) => {\n  const searchURL = `${cleanUrl(baseURL)}?q=${encodeURIComponent(query)}&format=json`\n\n  const abortController = new AbortController()\n  setTimeout(() => abortController.abort(), 20000)\n\n  try {\n    const response = await fetch(searchURL, {\n      signal: abortController.signal,\n      headers: {\n        'Accept': 'application/json'\n      }\n    })\n\n    if (!response.ok) {\n      throw new Error(`SearXNG search failed: ${response.statusText}`)\n    }\n\n    const data = await response.json() as SearxNGJSONResponse\n\n    return data.results.map(result => ({\n      title: result.title,\n      link: result.url,\n      content: result.content\n    }))\n  } catch (error) {\n    console.error('SearXNG JSON search failed:', error)\n    return []\n  }\n}\n\nconst searxngWebSearch = async (baseURL: string, query: string) => {\n  const searchURL = `${cleanUrl(baseURL)}?q=${encodeURIComponent(query)}`\n\n  await urlRewriteRuntime(cleanUrl(searchURL), \"searxng\")\n\n  const abortController = new AbortController()\n  setTimeout(() => abortController.abort(), 10000)\n\n  try {\n    const htmlString = await fetch(searchURL, {\n      signal: abortController.signal\n    }).then(response => response.text())\n\n    const parser = new DOMParser()\n    const doc = parser.parseFromString(htmlString, \"text/html\")\n\n    const searchResults = Array.from(doc.querySelectorAll(\"article.result\")).map(result => {\n      const title = result.querySelector(\"h3\")?.textContent?.trim()\n      const link = result.querySelector(\"a.url_header\")?.getAttribute(\"href\")\n      const content = result.querySelector(\"p.content\")?.textContent?.trim()\n      return { title, link, content }\n    }).filter(result => result.title && result.link && result.content)\n\n    return searchResults\n  } catch (error) {\n    console.error('SearXNG web search failed:', error)\n    return []\n  }\n}\n"
  },
  {
    "path": "src/web/search-engines/sogou.ts",
    "content": "import { cleanUrl } from \"@/libs/clean-url\"\nimport { urlRewriteRuntime } from \"@/libs/runtime\"\nimport { PageAssistHtmlLoader } from \"@/loader/html\"\nimport { pageAssistEmbeddingModel } from \"@/models/embedding\"\nimport {\n  defaultEmbeddingModelForRag,\n  getOllamaURL,\n  getSelectedModel\n} from \"@/services/ollama\"\nimport {\n  getIsSimpleInternetSearch,\n  totalSearchResults\n} from \"@/services/search\"\nimport { getPageAssistTextSplitter } from \"@/utils/text-splitter\"\nimport type { Document } from \"@langchain/core/documents\"\nimport * as cheerio from \"cheerio\"\nimport { MemoryVectorStore } from \"@langchain/classic/vectorstores/memory\"\nconst getCorrectTargeUrl = async (url: string) => {\n  if (!url) return \"\"\n  const res = await fetch(url)\n  const $ = cheerio.load(await res.text())\n  const link = $(\"script\").text()\n  const matches = link.match(/\"(.*?)\"/)\n  return matches?.[1] || \"\"\n}\nexport const localSogouSearch = async (query: string) => {\n  try {\n    await urlRewriteRuntime(\n      cleanUrl(\"https://www.sogou.com/web?query=\" + query),\n      \"sogou\"\n    )\n\n    const abortController = new AbortController()\n    setTimeout(() => abortController.abort(), 10000)\n\n    const htmlString = await fetch(\"https://www.sogou.com/web?query=\" + query, {\n      signal: abortController.signal\n    })\n      .then((response) => {\n        if (!response.ok) {\n          throw new Error(`HTTP error! Status: ${response.status}`)\n        }\n        return response.text()\n      })\n      .catch((error) => {\n        console.error(\"Sogou search request failed:\", error)\n        return \"\"\n      })\n\n    if (!htmlString) {\n      return []\n    }\n\n    const $ = cheerio.load(htmlString)\n    const $result = $(\"#main .results\")\n    const nodes = $result.children().map(async (i, el) => {\n      const $el = $(el)\n      const title = $el.find(\".vr-title\").text().replace(/\\n/g, \"\").trim()\n      let link = $el.find(\".vr-title > a\").get(0)?.attribs.href\n      const content = [\".star-wiki\", \".fz-mid\", \".attribute-centent\"]\n        .map((selector) => {\n          ;[\".text-lightgray\", \".zan-box\", \".tag-website\"].forEach((cls) => {\n            $el.find(cls).remove()\n          })\n          return $el.find(selector).text().trim() ?? \"\"\n        })\n        .join(\" \")\n      if (link?.startsWith(\"/\")) {\n        link = await getCorrectTargeUrl(`https://www.sogou.com${link}`)\n      }\n      return { title, link, content }\n    })\n\n    const searchResults = await Promise.all(nodes)\n    return searchResults.filter(\n      (result) => result.link && result.title && result.content\n    )\n  } catch (error) {\n    console.error(\"Sogou search processing failed:\", error)\n    return []\n  }\n}\n\nexport const webSogouSearch = async (query: string) => {\n  const results = await localSogouSearch(query)\n  const TOTAL_SEARCH_RESULTS = await totalSearchResults()\n  const searchResults = results.slice(0, TOTAL_SEARCH_RESULTS)\n\n  const isSimpleMode = await getIsSimpleInternetSearch()\n\n  if (isSimpleMode) {\n    await getOllamaURL()\n    return searchResults.map((result) => {\n      return {\n        url: result.link,\n        content: result.content\n      }\n    })\n  }\n\n  const docs: Document<Record<string, any>>[] = []\n  for (const result of searchResults) {\n    const loader = new PageAssistHtmlLoader({\n      html: \"\",\n      url: result.link\n    })\n\n    const documents = await loader.loadByURL()\n\n    documents.forEach((doc) => {\n      docs.push(doc)\n    })\n  }\n  const ollamaUrl = await getOllamaURL()\n\n  const embeddingModle = await defaultEmbeddingModelForRag()\n  const selectedModel = await getSelectedModel()\n  const ollamaEmbedding = await pageAssistEmbeddingModel({\n    model: embeddingModle || selectedModel || \"\",\n    baseUrl: cleanUrl(ollamaUrl)\n  })\n\n  const textSplitter = await getPageAssistTextSplitter()\n\n  const chunks = await textSplitter.splitDocuments(docs)\n\n  const store = new MemoryVectorStore(ollamaEmbedding)\n\n  await store.addDocuments(chunks)\n\n  const resultsWithEmbeddings = await store.similaritySearch(query, 3)\n\n  const searchResult = resultsWithEmbeddings.map((result) => {\n    return {\n      url: result.metadata.url,\n      content: result.pageContent\n    }\n  })\n\n  return searchResult\n}\n"
  },
  {
    "path": "src/web/search-engines/startpage.ts",
    "content": "import { cleanUrl } from \"@/libs/clean-url\"\nimport { PageAssistHtmlLoader } from \"@/loader/html\"\nimport { pageAssistEmbeddingModel } from \"@/models/embedding\"\nimport {\n    defaultEmbeddingModelForRag,\n    getOllamaURL,\n    getSelectedModel\n} from \"@/services/ollama\"\nimport {\n    getIsSimpleInternetSearch,\n    totalSearchResults\n} from \"@/services/search\"\nimport { getPageAssistTextSplitter } from \"@/utils/text-splitter\"\nimport type { Document } from \"@langchain/core/documents\"\nimport { MemoryVectorStore } from \"@langchain/classic/vectorstores/memory\"\n\nexport const localStartPageSearch = async (query: string) => {\n    const TOTAL_SEARCH_RESULTS = await totalSearchResults()\n\n    const abortController = new AbortController()\n    setTimeout(() => abortController.abort(), 10000)\n\n    const htmlString = await fetch(\n        \"https://www.startpage.com/sp/search?query=\" + encodeURIComponent(query) + \"&cat=web&pl=Opensearch\",\n        {\n            signal: abortController.signal\n        }\n    ).then((response) => response.text())\n        .catch()\n    const parser = new DOMParser()\n\n    const doc = parser.parseFromString(htmlString, \"text/html\")\n\n    // Get all search results\n    const results = doc.querySelectorAll('.result');\n    const parsedResults = [];\n\n    results.forEach(result => {\n        const titleElement = result.querySelector('.wgl-title');\n        const descriptionElement = result.querySelector('.description');\n        const urlElement = result.querySelector('.wgl-display-url .default-link-text');\n\n        if (titleElement && descriptionElement && urlElement) {\n            parsedResults.push({\n                title: titleElement.textContent.trim(),\n                content: descriptionElement.textContent.trim(),\n                link: urlElement.textContent.trim()\n            });\n        }\n    });\n\n\n    return parsedResults.slice(0, TOTAL_SEARCH_RESULTS);\n\n}\n\nexport const startpageSearch = async (query: string) => {\n    const searchResults = await localStartPageSearch(query)\n\n    const isSimpleMode = await getIsSimpleInternetSearch()\n\n    if (isSimpleMode) {\n        await getOllamaURL()\n        return searchResults.map((result) => {\n            return {\n                url: result.link,\n                content: result.content\n            }\n        })\n    }\n\n    const docs: Document<Record<string, any>>[] = []\n    for (const result of searchResults) {\n        const loader = new PageAssistHtmlLoader({\n            html: \"\",\n            url: result.link\n        })\n\n        const documents = await loader.loadByURL()\n\n        documents.forEach((doc) => {\n            docs.push(doc)\n        })\n    }\n    const ollamaUrl = await getOllamaURL()\n\n    const embeddingModle = await defaultEmbeddingModelForRag()\n    const selectedModel = await getSelectedModel()\n\n    const ollamaEmbedding = await pageAssistEmbeddingModel({\n        model: embeddingModle || selectedModel || \"\",\n        baseUrl: cleanUrl(ollamaUrl)\n    })\n\n    const textSplitter = await getPageAssistTextSplitter()\n\n    const chunks = await textSplitter.splitDocuments(docs)\n\n    const store = new MemoryVectorStore(ollamaEmbedding)\n\n    await store.addDocuments(chunks)\n\n    const resultsWithEmbeddings = await store.similaritySearch(query, 3)\n\n    const searchResult = resultsWithEmbeddings.map((result) => {\n        return {\n            url: result.metadata.url,\n            content: result.pageContent\n        }\n    })\n\n    return searchResult\n}\n"
  },
  {
    "path": "src/web/search-engines/stract.ts",
    "content": "import { cleanUrl } from \"~/libs/clean-url\"\nimport { getIsSimpleInternetSearch, totalSearchResults } from \"@/services/search\"\nimport { pageAssistEmbeddingModel } from \"@/models/embedding\"\nimport type { Document } from \"@langchain/core/documents\"\nimport { MemoryVectorStore } from \"@langchain/classic/vectorstores/memory\"\nimport { PageAssistHtmlLoader } from \"@/loader/html\"\nimport {\n    defaultEmbeddingModelForRag,\n    getOllamaURL,\n    getSelectedModel\n} from \"~/services/ollama\"\nimport { getPageAssistTextSplitter } from \"@/utils/text-splitter\"\n\n\nexport interface Snippet {\n    date: any\n    text: Text\n}\n\nexport interface Text {\n    fragments: Fragment[]\n}\n\nexport interface Fragment {\n    kind: string\n    text: string\n}\n\ninterface StractAPIResult {\n    title: string\n    url: string\n    snippet: Snippet\n\n}\n\ninterface StractAPIResponse {\n    webpages: StractAPIResult[]\n}\n\nexport const stractSearch = async (query: string) => {\n\n    const results = await apiStractSearch(query)\n    const TOTAL_SEARCH_RESULTS = await totalSearchResults()\n\n    const searchResults = results.slice(0, TOTAL_SEARCH_RESULTS)\n\n    const isSimpleMode = await getIsSimpleInternetSearch()\n\n    if (isSimpleMode) {\n        await getOllamaURL()\n        return searchResults.map((result) => {\n            return {\n                url: result.link,\n                content: result.content\n            }\n        })\n    }\n\n    const docs: Document<Record<string, any>>[] = []\n    try {\n        for (const result of searchResults) {\n            const loader = new PageAssistHtmlLoader({\n                html: \"\",\n                url: result.link\n            })\n\n            const documents = await loader.loadByURL()\n            documents.forEach((doc) => {\n                docs.push(doc)\n            })\n        }\n    } catch (error) {\n        console.error(error)\n    }\n\n    const ollamaUrl = await getOllamaURL()\n    const embeddingModel = await defaultEmbeddingModelForRag()\n    const selectedModel = await getSelectedModel()\n    const ollamaEmbedding = await pageAssistEmbeddingModel({\n        model: embeddingModel || selectedModel || \"\",\n        baseUrl: cleanUrl(ollamaUrl)\n    })\n\n    const textSplitter = await getPageAssistTextSplitter()\n\n    const chunks = await textSplitter.splitDocuments(docs)\n    const store = new MemoryVectorStore(ollamaEmbedding)\n    await store.addDocuments(chunks)\n\n    const resultsWithEmbeddings = await store.similaritySearch(query, 3)\n\n    const searchResult = resultsWithEmbeddings.map((result) => {\n        return {\n            url: result.metadata.url,\n            content: result.pageContent\n        }\n    })\n\n    return searchResult\n}\n\nconst apiStractSearch = async (query: string) => {\n    const TOTAL_SEARCH_RESULTS = await totalSearchResults()\n\n    const searchURL = 'https://stract.com/beta/api/search'\n\n    const abortController = new AbortController()\n    setTimeout(() => abortController.abort(), 20000)\n\n    try {\n        const response = await fetch(searchURL, {\n            signal: abortController.signal,\n            headers: {\n                Accept: \"application/json\",\n                \"Content-Type\": \"application/json\",\n            },\n            method: \"POST\",\n            body: JSON.stringify({\n                query,\n                num_results: TOTAL_SEARCH_RESULTS\n            })\n        })\n\n        if (!response.ok) {\n            return []\n        }\n\n        const data = await response.json() as StractAPIResponse\n\n        return data?.webpages?.map(result => ({\n            title: result.title,\n            link: result.url,\n            content: result.snippet.text.fragments.map(e => e.text).join(\" \")\n        }))\n    } catch (error) {\n        console.error('Stract search failed:', error)\n        return []\n    }\n}\n"
  },
  {
    "path": "src/web/search-engines/tavily-api.ts",
    "content": "import { cleanUrl } from \"~/libs/clean-url\"\nimport { getIsSimpleInternetSearch, totalSearchResults, getTavilyApiKey } from \"@/services/search\"\nimport { pageAssistEmbeddingModel } from \"@/models/embedding\"\nimport type { Document } from \"@langchain/core/documents\"\nimport { MemoryVectorStore } from \"@langchain/classic/vectorstores/memory\"\nimport { PageAssistHtmlLoader } from \"@/loader/html\"\nimport {\n    defaultEmbeddingModelForRag,\n    getOllamaURL,\n    getSelectedModel\n} from \"~/services/ollama\"\nimport { getPageAssistTextSplitter } from \"@/utils/text-splitter\"\n\ninterface Results {\n    title: string,\n    link: string,\n    content: string,\n}\n\ninterface TavilyAPIResult {\n    answer: string\n    results: Array<Results>\n}\n\nexport const tavilyAPISearch = async (query: string) => {\n    const tavilyApiKey = await getTavilyApiKey()\n    if (!tavilyApiKey || tavilyApiKey.trim() === \"\") {\n        throw new Error(\"Tavily API key not configured\")\n    }\n    const result = await apiTavilySearch(tavilyApiKey, query)\n    const TOTAL_SEARCH_RESULTS = await totalSearchResults()\n\n    let searchResults: Results[] = []\n    if('results' in result) {\n        searchResults = result.results.slice(0, TOTAL_SEARCH_RESULTS) \n    }\n\n    const isSimpleMode = await getIsSimpleInternetSearch()\n\n    if (isSimpleMode && 'answer' in result) {\n        await getOllamaURL()\n        return {\n            answer: result.answer,\n            results: searchResults.map((result) => {\n                return {\n                    url: result.link,\n                    content: result.content\n                }\n            })\n        }\n    }\n\n    const docs: Document<Record<string, any>>[] = []\n    try {\n        \n        for (const result of searchResults) {\n            const loader = new PageAssistHtmlLoader({\n                html: \"\",\n                url: result.link\n            })\n\n            const documents = await loader.loadByURL()\n            documents.forEach((doc) => {\n                docs.push(doc)\n            })\n        }\n    } catch (error) {\n        console.error(error)\n    }\n\n    const ollamaUrl = await getOllamaURL()\n    const embeddingModel = await defaultEmbeddingModelForRag()\n    const selectedModel = await getSelectedModel()\n    const ollamaEmbedding = await pageAssistEmbeddingModel({\n        model: embeddingModel || selectedModel || \"\",\n        baseUrl: cleanUrl(ollamaUrl)\n    })\n\n    const textSplitter = await getPageAssistTextSplitter()\n\n    const chunks = await textSplitter.splitDocuments(docs)\n    const store = new MemoryVectorStore(ollamaEmbedding)\n    await store.addDocuments(chunks)\n\n    const resultsWithEmbeddings = await store.similaritySearch(query, 3)\n\n    const searchResult = resultsWithEmbeddings.map((result) => {\n        return {\n            url: result.metadata.url,\n            content: result.pageContent\n        }\n    })\n\n    return searchResult\n}\n\nconst apiTavilySearch = async (tavilySearchApi: string, query: string): Promise<TavilyAPIResult | []> => {\n    const MAX_SEARCH_RESULTS = await totalSearchResults()\n    const isSimpleMode = await getIsSimpleInternetSearch()\n\n    const abortController = new AbortController()\n    const timeout = setTimeout(() => abortController.abort(), 20000)\n\n    try {\n        const response = await fetch('https://api.tavily.com/search', {\n            method: 'POST',\n            headers: {\n                'Content-Type': 'application/json',\n            },\n            body: JSON.stringify({\n                api_key: tavilySearchApi,\n                query,\n                max_results: MAX_SEARCH_RESULTS,\n                include_answer: isSimpleMode\n            }),\n            signal: abortController.signal\n        })\n\n        if (!response.ok) {\n            const errorBody = await response.text()\n            console.error('Corpo do erro:', errorBody)\n            throw new Error(`Erro na API: ${response.status} - ${response.statusText}`)\n        }\n\n        const data = await response.json()\n        \n        // Validação básica da resposta\n        if (!data.results || !Array.isArray(data.results)) {\n            throw new Error('Resposta inválida da API do Tavily')\n        }\n\n        return {\n            answer: data.answer || '',\n            results: data.results.map((result: any) => ({\n                title: result.title || '',\n                link: result.url || '',\n                content: result.content || '',\n            }))\n        }\n    } catch (error) {\n        console.error('Tavily API search failed:', error)\n        return []\n    } finally {\n        clearTimeout(timeout)\n    }\n}"
  },
  {
    "path": "src/web/web.ts",
    "content": "import { getWebSearchPrompt } from \"~/services/ollama\"\nimport { webGoogleSearch } from \"./search-engines/google\"\nimport { webDuckDuckGoSearch } from \"./search-engines/duckduckgo\"\nimport { getIsVisitSpecificWebsite, getSearchProvider, getDomainFilterList, getBlockedDomainList } from \"@/services/search\"\nimport { webSogouSearch } from \"./search-engines/sogou\"\nimport { webBraveSearch } from \"./search-engines/brave\"\nimport { getWebsiteFromQuery, processSingleWebsite } from \"./website\"\nimport { searxngSearch } from \"./search-engines/searxng\"\nimport { braveAPISearch } from \"./search-engines/brave-api\"\nimport { tavilyAPISearch } from \"./search-engines/tavily-api\"\nimport { webBaiduSearch } from \"./search-engines/baidu\"\nimport { webBingSearch } from \"./search-engines/bing\"\nimport { stractSearch } from \"./search-engines/stract\"\nimport { startpageSearch } from \"./search-engines/startpage\"\nimport { exaAPISearch } from \"./search-engines/exa\"\nimport { firecrawlAPISearch } from \"./search-engines/firecrawl\"\nimport { ollamaAPISearch } from \"./search-engines/ollama\"\nimport { kagiAPISearch } from \"./search-engines/kagi-api\"\nimport { perplexityAPISearch } from \"./search-engines/perplexity-api\"\n\ninterface ProviderResults {\n  url: any\n  content: string | null\n}\n\ninterface SearchProviderResult {\n  url: string\n  content: string | null\n  answer: string | null\n  results: ProviderResults[] | null\n}\n\nconst getHostName = (url: string) => {\n  try {\n    return new URL(url).hostname\n  } catch (e) {\n    console.error(\"Failed to get hostname:\", e)\n    return \"\"\n  }\n}\n\nconst searchWeb = (provider: string, query: string) => {\n  switch (provider) {\n    case \"duckduckgo\":\n      return webDuckDuckGoSearch(query)\n    case \"sogou\":\n      return webSogouSearch(query)\n    case \"brave\":\n      return webBraveSearch(query)\n    case \"searxng\":\n      return searxngSearch(query)\n    case \"brave-api\":\n      return braveAPISearch(query)\n    case \"tavily-api\":\n      return tavilyAPISearch(query)\n    case \"baidu\":\n      return webBaiduSearch(query)\n    case \"bing\":\n      return webBingSearch(query)\n    case \"stract\":\n      return stractSearch(query)\n    case \"startpage\":\n      return startpageSearch(query)\n    case \"exa\":\n      return exaAPISearch(query)\n    case \"firecrawl\":\n      return firecrawlAPISearch(query)\n    case \"ollama-search\":\n      return ollamaAPISearch(query)\n    case \"kagi-api\":\n      return kagiAPISearch(query)\n    case \"perplexity-api\":\n      return perplexityAPISearch(query)\n    default:\n      return webGoogleSearch(query)\n  }\n}\n\nconst getProvidedURLs = (\n  searchOnProviders: SearchProviderResult | ProviderResults[],\n  searchOnAWebSite: ProviderResults[]\n) => {\n  let urlList = []\n  if ('results' in searchOnProviders) {\n    urlList = searchOnProviders.results\n  } else if (searchOnProviders.length >= 1) {\n    urlList = searchOnProviders\n  } else {\n    urlList = searchOnAWebSite\n  }\n  return urlList\n}\n\nconst filterResultsByDomain = (\n  results: ProviderResults[],\n  domainFilterList: string[],\n  blockedDomainList: string[]\n): ProviderResults[] => {\n  let filteredResults = results\n\n  // First, filter out blocked domains\n  if (blockedDomainList && blockedDomainList.length > 0) {\n    filteredResults = filteredResults.filter((result) => {\n      const hostname = getHostName(result.url)\n      return !blockedDomainList.some((domain) => {\n        const cleanDomain = domain.trim().toLowerCase()\n        return hostname.toLowerCase().includes(cleanDomain) || cleanDomain.includes(hostname.toLowerCase())\n      })\n    })\n  }\n\n  // Then, if allow list exists, only keep those domains\n  if (domainFilterList && domainFilterList.length > 0) {\n    filteredResults = filteredResults.filter((result) => {\n      const hostname = getHostName(result.url)\n      return domainFilterList.some((domain) => {\n        const cleanDomain = domain.trim().toLowerCase()\n        return hostname.toLowerCase().includes(cleanDomain) || cleanDomain.includes(hostname.toLowerCase())\n      })\n    })\n  }\n\n  return filteredResults\n}\n\nexport const isQueryHaveWebsite = async (query: string) => {\n  const websiteVisit = getWebsiteFromQuery(query)\n\n  const isVisitSpecificWebsite = await getIsVisitSpecificWebsite()\n\n  return isVisitSpecificWebsite && websiteVisit.hasUrl\n}\n\nexport const getSystemPromptForWeb = async (query: string, returnSearchResults: boolean = false) => {\n  try {\n    const websiteVisit = getWebsiteFromQuery(query)\n    let searchOnAWebSite: ProviderResults[] = []\n    let searchOnProviders: SearchProviderResult | ProviderResults[] = []\n\n    const isVisitSpecificWebsite = await getIsVisitSpecificWebsite()\n    const domainFilterList = await getDomainFilterList()\n    const blockedDomainList = await getBlockedDomainList()\n    let search_results: string = \"\"\n\n    if (isVisitSpecificWebsite && websiteVisit.hasUrl) {\n      const url = websiteVisit.url\n      const queryWithoutUrl = websiteVisit.queryWithouUrls\n      searchOnAWebSite = await processSingleWebsite(url, queryWithoutUrl)\n      searchOnAWebSite = filterResultsByDomain(searchOnAWebSite, domainFilterList, blockedDomainList)\n      for (const result of searchOnAWebSite) {\n        search_results += `<result source=\"${result.url}\" id=\"0\">${result?.content}</result>`\n        search_results += (`\\n`)\n      }\n    } else {\n      const searchProvider = await getSearchProvider()\n      searchOnProviders = await searchWeb(searchProvider, query)\n      if ('answer' in searchOnProviders) {\n        search_results += `<result id=\"0\">${searchOnProviders.answer}</result>`\n        search_results += (`\\n`)\n      } else {\n        const filteredResults = filterResultsByDomain(searchOnProviders as ProviderResults[], domainFilterList, blockedDomainList)\n        search_results = filteredResults.map((result: ProviderResults, idx) =>\n          `<result source=\"${result.url}\" id=\"${idx}\">${result?.content}</result>`\n        )\n          .join(\"\\n\")\n        searchOnProviders = filteredResults\n      }\n    }\n\n    const urlProvided = getProvidedURLs(searchOnProviders, searchOnAWebSite)\n\n    if (returnSearchResults) {\n      return {\n        prompt: search_results,\n        source: urlProvided.map((result) => {\n          return {\n            url: result.url,\n            name: getHostName(result.url),\n            type: \"url\"\n          }\n        })\n      }\n    }\n\n    const current_date_time = new Date().toLocaleString()\n\n    const system = await getWebSearchPrompt()\n\n    const prompt = system\n      .replace(\"{current_date_time}\", current_date_time)\n      .replace(\"{search_results}\", search_results)\n      .replace(\"{query}\", query)\n\n\n    return {\n      prompt,\n      source: urlProvided.map((result) => {\n        return {\n          url: result.url,\n          name: getHostName(result.url),\n          type: \"url\"\n        }\n      })\n    }\n  } catch (e) {\n    console.error(e)\n    return {\n      prompt: \"\",\n      source: []\n    }\n  }\n}\n"
  },
  {
    "path": "src/web/website/index.ts",
    "content": "import { cleanUrl } from \"@/libs/clean-url\"\nimport { PageAssistHtmlLoader } from \"@/loader/html\"\nimport { pageAssistEmbeddingModel } from \"@/models/embedding\"\nimport { getNoOfRetrievedDocs } from \"@/services/app\"\nimport { getMaxContextSize, isChatWithWebsiteEnabled } from \"@/services/kb\"\nimport { defaultEmbeddingModelForRag, getOllamaURL } from \"@/services/ollama\"\nimport { getPageAssistTextSplitter } from \"@/utils/text-splitter\"\n\nimport { MemoryVectorStore } from \"@langchain/classic/vectorstores/memory\"\n\nexport const processSingleWebsite = async (url: string, query: string) => {\n    const loader = new PageAssistHtmlLoader({\n        html: \"\",\n        url\n    })\n    const docs = await loader.loadByURL()\n\n    const maxContextSize = await getMaxContextSize()\n\n    const useVS = await isChatWithWebsiteEnabled()\n\n    if (useVS) {\n        if (docs.length > 0) {\n            const doc = docs[0]\n            return [\n                {\n                    url: doc?.metadata?.url,\n                    content: doc?.pageContent?.substring(0, maxContextSize),\n                }\n            ]\n        }\n    }\n\n\n    const ollamaUrl = await getOllamaURL()\n\n    const embeddingModle = await defaultEmbeddingModelForRag()\n    const ollamaEmbedding = await pageAssistEmbeddingModel({\n        model: embeddingModle || \"\",\n        baseUrl: cleanUrl(ollamaUrl)\n    })\n\n\n    const textSplitter = await getPageAssistTextSplitter()\n\n    const chunks = await textSplitter.splitDocuments(docs)\n\n    const store = new MemoryVectorStore(ollamaEmbedding)\n\n    await store.addDocuments(chunks)\n\n    const resultsWithEmbeddings = await store.similaritySearch(query, 4)\n\n    const searchResult = resultsWithEmbeddings.map((result) => {\n        return {\n            url: result.metadata.url,\n            content: result.pageContent\n        }\n    })\n\n    return searchResult\n}\n\n\nexport const getWebsiteFromQuery = (query: string): {\n    queryWithouUrls: string,\n    url: string,\n    hasUrl: boolean\n} => {\n\n    const urlRegex = /https?:\\/\\/[^\\s]+/g\n\n    const urls = query.match(urlRegex)\n\n    if (!urls) {\n        return {\n            queryWithouUrls: query,\n            url: \"\",\n            hasUrl: false\n        }\n    }\n\n    const url = urls[0]\n\n    const queryWithouUrls = query.replace(url, \"\")\n\n    return {\n        queryWithouUrls,\n        url,\n        hasUrl: true\n    }\n}"
  },
  {
    "path": "tailwind.config.js",
    "content": "/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n  mode: \"jit\",\n  darkMode: \"class\",\n  content: [\"./src/**/*.tsx\"],\n  theme: {\n    extend: {\n      backgroundImage: {\n        'bottom-mask-light': 'linear-gradient(0deg, transparent 0, #ffffff 160px)',\n        'bottom-mask-dark': 'linear-gradient(0deg, transparent 0, #1a1a1a 160px)',\n      },\n      maskImage: {\n        'bottom-fade': 'linear-gradient(0deg, transparent 0, #000 160px)',\n      }\n    }\n  },\n  plugins: [require(\"@tailwindcss/forms\"), require(\"@tailwindcss/typography\")]\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"extends\": \"./.wxt/tsconfig.json\",\n  \"compilerOptions\": {\n    \"noEmit\": true,\n    \"allowImportingTsExtensions\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"esModuleInterop\": true,\n    \"jsx\": \"react-jsx\",\n    \"strict\": false\n  },\n  \"exclude\": [\n    \"node_modules\"\n  ],\n}"
  },
  {
    "path": "wxt.config.ts",
    "content": "import { defineConfig } from \"wxt\"\nimport react from \"@vitejs/plugin-react\"\nimport topLevelAwait from \"vite-plugin-top-level-await\"\n\nconst chromeMV3Permissions = [\n  \"storage\",\n  \"sidePanel\",\n  \"activeTab\",\n  \"scripting\",\n  \"declarativeNetRequest\",\n  \"action\",\n  \"unlimitedStorage\",\n  \"contextMenus\",\n  \"tts\",\n  \"notifications\"\n]\n\nconst firefoxMV2Permissions = [\n  \"storage\",\n  \"activeTab\",\n  \"scripting\",\n  \"unlimitedStorage\",\n  \"contextMenus\",\n  \"webRequest\",\n  \"webRequestBlocking\",\n  \"notifications\",\n  \"http://*/*\",\n  \"https://*/*\",\n  \"file://*/*\"\n]\n\n// See https://wxt.dev/api/config.html\nexport default defineConfig({\n  vite: () => ({\n    plugins: [\n      react(),\n      topLevelAwait({\n        promiseExportName: \"__tla\",\n        promiseImportName: (i) => `__tla_${i}`\n      }) as any\n    ],\n    build: {\n      rollupOptions: {\n        external: [\"langchain\", \"@langchain/community\"]\n      }\n    }\n  }),\n  entrypointsDir:\n    process.env.TARGET === \"firefox\" ? \"entries-firefox\" : \"entries\",\n  srcDir: \"src\",\n  outDir: \"build\",\n\n  manifest: {\n    version: \"1.5.57\",\n    name:\n      process.env.TARGET === \"firefox\"\n        ? \"Page Assist - A Web UI for Local AI Models\"\n        : \"__MSG_extName__\",\n    description: \"__MSG_extDescription__\",\n    default_locale: \"en\",\n    action: {},\n    author: \"n4ze3m\",\n    browser_specific_settings:\n      process.env.TARGET === \"firefox\"\n        ? {\n          gecko: {\n            id: \"page-assist@nazeem\"\n          }\n        }\n        : undefined,\n    host_permissions:\n      process.env.TARGET !== \"firefox\"\n        ? [\"http://*/*\", \"https://*/*\", \"file://*/*\"]\n        : undefined,\n    commands: {\n      _execute_action: {\n        description: \"Open the Web UI\",\n        suggested_key: {\n          default: \"Ctrl+Shift+L\"\n        }\n      },\n      execute_side_panel: {\n        description: \"Open the side panel\",\n        suggested_key: {\n          default: \"Ctrl+Shift+Y\"\n        }\n      }\n    },\n    content_security_policy:\n      process.env.TARGET !== \"firefox\" ?\n        {\n          extension_pages:\n            \"script-src 'self' 'wasm-unsafe-eval'; object-src 'self';\"\n        } :  \"script-src 'self' 'wasm-unsafe-eval' blob:; object-src 'self'; worker-src 'self' blob:;\",\n    permissions:\n      process.env.TARGET === \"firefox\"\n        ? firefoxMV2Permissions\n        : chromeMV3Permissions\n  }\n}) as any"
  }
]